freeipa/smartproxy/ipa-smartproxy.py
Rob Crittenden 74412dbf88 Clean up Smartproxy support, drop unused code
Drop the logrotate file because Apache manages the logs
Drop the systemd configuration because we run in Apache
Import json_encode_binary from ipalib
Fix Requires

Reviewed-By: Nathaniel McCallum <npmccallum@redhat.com>
2014-05-13 13:12:15 +02:00

399 lines
11 KiB
Python

# Authors:
# Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2014 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
sys.stdout = sys.stderr
import cherrypy
import os
import json
from functools import wraps
import traceback as tb_internal
from cherrypy import response
from ipalib import api
from ipalib import errors
from ipalib.request import context
from ipalib.rpc import json_encode_binary
from ipapython.version import VERSION, API_VERSION
from ipapython.ipa_log_manager import root_logger
def jsonout(func):
'''JSON output decorator'''
@wraps(func)
def wrapper(*args, **kw):
value = func(*args, **kw)
response.headers["Content-Type"] = "application/json;charset=utf-8"
data = json_encode_binary(value, version=API_VERSION)
return json.dumps(data, sort_keys=True, indent=2)
return wrapper
def handle_error(status, message, traceback, version):
"""
Return basic messages to user and log backtrace in case of 500
error.
"""
if status.startswith('500'):
root_logger.error(message)
root_logger.error(tb_internal.format_exc())
resp = cherrypy.response
resp.headers['Content-Type'] = 'application/json'
return json.dumps({'status': status, 'message': message})
def convert_unicode(value):
"""
IPA requires all incoming values to be unicode. Recursively
convert the values.
"""
if not isinstance(value, basestring):
return value
if value is not None:
return unicode(value)
else:
return None
def raise_rest_exception(e):
"""
Raise a REST-specific exception.
"""
try:
raise e
except (errors.DuplicateEntry, errors.DNSNotARecordError,
errors.ValidationError, errors.ConversionError,) as e:
raise IPAError(
status=400,
message=e
)
except errors.ACIError as e:
raise IPAError(
status=401,
message=e
)
except errors.NotFound as e:
raise IPAError(
status=404,
message=e
)
except Exception as e:
raise IPAError(
status=500,
message=e
)
def popifnone(params, option):
"""
If option, a string, exists in params, a dict, and is None then
remove it from the dict.
No return value. The dict is updated in-place if necessary.
"""
if params.get(option) is None:
params.pop(option, None)
def Command(command, *args, **options):
"""
Execute an IPA command with the given arguments and options.
This doesn't care what the options are, it passes them along to
the IPA API. The exceptions are:
:param nomaskexceptions: boolean to decide if we raise the real IPA
exception or a REST-specific exceptioon.
"""
if (cherrypy.request.config.get('local_only', False) and
cherrypy.request.remote.ip not in ['::1', '127.0.0.1']):
raise IPAError(
status=401,
message="Not a local request"
)
try:
if not api.Backend.rpcclient.isconnected():
api.Backend.rpcclient.connect()
except errors.CCacheError as e:
root_logger.info('Connection failed: %s', e)
raise IPAError(
status=401,
message=e
)
# IPA wants all its strings as unicode
args = map(lambda v: convert_unicode(v), args)
options = dict(zip(options, map(convert_unicode, options.values())))
nomaskexception = options.pop('nomaskexception', False)
api.Command[command].args_options_2_params(*args, **options)
try:
return api.Command[command](*args, **options)['result']
except Exception as e:
if not nomaskexception:
raise_rest_exception(e)
else:
# The caller needs to be able to handle IPA-specific
# exceptions.
raise e
@jsonout
def GET(command, *args, **options):
return Command(command, *args, **options)
@jsonout
def POST(status, command, *args, **options):
cherrypy.response.status = status
return Command(command, *args, **options)
@jsonout
def DELETE(command, *args, **options):
return Command(command, *args, **options)
class IPAError(cherrypy.HTTPError):
"""
Return errors in IPA-style json.
Local errors are treated as strings so do not include the code and
name attributes within the error dict.
This is not padded for IE.
"""
def set_response(self):
response = cherrypy.serving.response
cherrypy._cperror.clean_headers(self.code)
# In all cases, finalize will be called after this method,
# so don't bother cleaning up response values here.
response.status = self.status
if isinstance(self._message, Exception):
try:
code = self._message.errno
except AttributeError:
code = 0
error = {'code': code,
'message': self._message.message,
'name': self._message.__class__.__name__}
elif isinstance(self._message, basestring):
error = {'message': self._message}
else:
error = {'message':
'Unable to handle error message type %s' %
type(self._message)}
principal = getattr(context, 'principal', None)
response.headers["Content-Type"] = "application/json;charset=utf-8"
response.body = json.dumps({'error': error,
'id': 0,
'principal': principal,
'result': None,
'version': VERSION},
sort_keys=True, indent=2)
class Host(object):
"""
Manage IPA host objects
"""
exposed = True
def GET(self, fqdn=None):
if fqdn is None:
command = 'host_find'
else:
command = 'host_show'
return GET(command, fqdn)
def POST(self, hostname, description=None,
macaddress=None, userclass=None, ip_address=None,
password=None, rebuild=None):
cmd = 'host_add'
if password is None:
random = True
else:
random = False
params = {'description' : description,
'random' : random,
'macaddress' : macaddress,
'userclass' : userclass,
'userpassword' : password}
# If the host is being rebuilt, disable it in order to revoke
# existing certs, keytabs, etc.
try:
Command('host_show', hostname, nomaskexception=True)
except errors.NotFound:
# Adding a new host
status = 201
params['ip_address'] = ip_address
params['force'] = True
except Exception as e:
raise_rest_exception(e)
else:
if ip_address is not None:
raise IPAError(
status=400,
message='IP address must be changed in DNS'
)
cmd = 'host_mod'
# Foreman doesn't pass these in on update so drop them otherwise
# IPA will consider these as being set to None which deletes them.
popifnone(params, 'description')
popifnone(params, 'macaddress')
popifnone(params, 'userclass')
popifnone(params, 'userpassword')
status = 200
if rebuild:
root_logger.info("Attempting to disable %s", hostname)
try:
Command('host_disable', hostname, nomaskexception=True)
except errors.AlreadyInactive as e:
pass
else:
raise e
return POST(status, cmd, hostname, **params)
def DELETE(self, fqdn):
# The host-del behavior is a bit off due to
# https://fedorahosted.org/freeipa/ticket/4329
# A NotFound is returned if the user can't read DNS.
# Do a GET to see if the host exists, then we can more blindly
# try the delete.
# If the GET is ok then we know there is a host, though this is a
# bit racy.
GET('host_show', fqdn)
remove_dns = cherrypy.request.config.get('remove_dns', False)
return DELETE('host_del', fqdn, updatedns=remove_dns)
class Hostgroup(object):
"""
Manage IPA hostgroup objects
"""
exposed = True
def GET(self, name=None):
if name is None:
command = 'hostgroup_find'
else:
command = 'hostgroup_show'
return GET(command, name)
def POST(self, name=None, description=None):
cherrypy.response.status = 201
return POST(201, 'hostgroup_add', name,
description=description,)
def DELETE(self, name):
return DELETE('hostgroup_del', name)
class Features(object):
exposed = True
def GET(self):
return '["realm"]'
def start(config=None):
# Set the umask so only the owner can read the log files
old_umask = os.umask(077)
cherrypy.tree.mount(
Features(), '/features',
{'/':
{'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
}
)
cherrypy.tree.mount(
Host(), '/ipa/smartproxy/host',
{'/':
{'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
}
)
cherrypy.tree.mount(
Hostgroup(), '/ipa/smartproxy/hostgroup',
{'/':
{'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
}
)
# Register the realm for requests from Foreman
root_logger.info("Mounting /realm/%s", api.env.realm)
cherrypy.tree.mount(
Host(), '/realm/%s' % api.env.realm,
{'/':
{'request.dispatch': cherrypy.dispatch.MethodDispatcher()}
}
)
for c in config or []:
try:
cherrypy.config.update(c)
except (IOError, OSError) as e:
root_logger.error("Exception trying to load %s: %s", c, e)
return 1
# Log files are created, reset umask
os.umask(old_umask)
cherrypy.config.update({'error_page.500': handle_error})
return 0
def application(environ, start_response):
root_logger.info("IPA smartproxy WSGI start")
return cherrypy.tree(environ, start_response)
wsgi_config = {'environment': 'embedded',
'log.screen': False,
'show_tracebacks': False,
'engine.autoreload_on': False
}
api.bootstrap(context='ipasmartproxy', log='/dev/null')
api.finalize()
cherrypy.config.update(wsgi_config)
start(['/etc/ipa/ipa-smartproxy.conf'])