mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Giant webui patch take 2
This commit is contained in:
@@ -1,4 +1,2 @@
|
||||
include LICENSE TODO lite-webui.py lite-xmlrpc.py
|
||||
graft tests/
|
||||
graft ipawebui/static/
|
||||
include ipawebui/templates/*.kid
|
||||
include LICENSE TODO lite-server.py
|
||||
include tests/*/*.py
|
||||
|
||||
@@ -35,7 +35,6 @@ BuildRequires: popt-devel
|
||||
BuildRequires: /usr/share/selinux/devel/Makefile
|
||||
BuildRequires: m4
|
||||
BuildRequires: policycoreutils >= %{POLICYCOREUTILSVER}
|
||||
BuildRequires: python-cherrypy
|
||||
BuildRequires: python-setuptools
|
||||
BuildRequires: python-krbV
|
||||
BuildRequires: xmlrpc-c-devel
|
||||
@@ -75,7 +74,8 @@ Requires: mod_nss
|
||||
%endif
|
||||
Requires: python-ldap
|
||||
Requires: python-krbV
|
||||
Requires: python-cherrypy
|
||||
Requires: python-assets
|
||||
Requires: python-wehjit
|
||||
Requires: acl
|
||||
Requires: python-pyasn1
|
||||
Requires: libcap
|
||||
@@ -440,6 +440,10 @@ fi
|
||||
%endif
|
||||
|
||||
%changelog
|
||||
* Mon Oct 12 2009 Jason Gerard DeRose <jderose@redhat.com> - 1.99-8
|
||||
- Removed python-cherrypy from BuildRequires and Requires
|
||||
- Added Requires python-assets, python-wehjit
|
||||
|
||||
* Mon Aug 24 2009 Rob Crittenden <rcritten@redhat.com> - 1.99-7
|
||||
- Added httpd SELinux policy so CRLs can be read
|
||||
|
||||
|
||||
@@ -836,7 +836,7 @@ cli_plugins = (
|
||||
def run(api):
|
||||
error = None
|
||||
try:
|
||||
argv = api.bootstrap_with_global_options(context='cli')
|
||||
(options, argv) = api.bootstrap_with_global_options(context='cli')
|
||||
for klass in cli_plugins:
|
||||
api.register(klass)
|
||||
api.load_plugins()
|
||||
|
||||
@@ -99,11 +99,23 @@ DEFAULT_CONFIG = (
|
||||
('container_virtual', 'cn=virtual operations'),
|
||||
|
||||
# Ports, hosts, and URIs:
|
||||
('lite_xmlrpc_port', 8888),
|
||||
('lite_webui_port', 9999),
|
||||
('xmlrpc_uri', 'http://localhost:8888'),
|
||||
# FIXME: let's renamed xmlrpc_uri to rpc_xml_uri
|
||||
('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
|
||||
('rpc_json_uri', 'http://localhost:8888/ipa/json'),
|
||||
('ldap_uri', 'ldap://localhost:389'),
|
||||
|
||||
# Web Application mount points
|
||||
('mount_ipa', '/ipa/'),
|
||||
('mount_xmlserver', 'xml'),
|
||||
('mount_jsonserver', 'json'),
|
||||
('mount_webui', 'ui/'),
|
||||
('mount_webui_assets', '_/'),
|
||||
|
||||
# WebUI stuff:
|
||||
('webui_prod', True),
|
||||
('webui_assets_dir', None),
|
||||
('webui_assets_dburi', None),
|
||||
|
||||
# Debugging:
|
||||
('verbose', False),
|
||||
('debug', False),
|
||||
|
||||
@@ -112,6 +112,7 @@ class PrivateError(StandardError):
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.msg = self.format % kw
|
||||
self.kw = kw
|
||||
for (key, value) in kw.iteritems():
|
||||
assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % (
|
||||
self.__class__.__name__, key, value,
|
||||
@@ -244,6 +245,7 @@ class PublicError(StandardError):
|
||||
format = None
|
||||
|
||||
def __init__(self, format=None, message=None, **kw):
|
||||
self.kw = kw
|
||||
name = self.__class__.__name__
|
||||
if self.format is not None and format is not None:
|
||||
raise ValueError(
|
||||
@@ -407,6 +409,15 @@ class ServerNetworkError(PublicError):
|
||||
format = _('error on server %(server)r: %(error)s')
|
||||
|
||||
|
||||
class JSONError(PublicError):
|
||||
"""
|
||||
**909** Raised when server recieved a malformed JSON-RPC request.
|
||||
"""
|
||||
|
||||
errno = 909
|
||||
format = _('Invalid JSON-RPC request: %(error)s')
|
||||
|
||||
|
||||
|
||||
##############################################################################
|
||||
# 1000 - 1999: Authentication errors
|
||||
|
||||
@@ -375,6 +375,7 @@ class Command(HasParam):
|
||||
options = None
|
||||
params = None
|
||||
output_for_cli = None
|
||||
obj = None
|
||||
|
||||
def __call__(self, *args, **options):
|
||||
"""
|
||||
|
||||
@@ -132,6 +132,13 @@ class DefaultFrom(ReadOnly):
|
||||
)
|
||||
lock(self)
|
||||
|
||||
def __repr__(self):
|
||||
args = (self.callback.__name__,) + tuple(repr(k) for k in self.keys)
|
||||
return '%s(%s)' % (
|
||||
self.__class__.__name__,
|
||||
', '.join(args)
|
||||
)
|
||||
|
||||
def __call__(self, **kw):
|
||||
"""
|
||||
Call the callback if all keys are present.
|
||||
@@ -376,7 +383,12 @@ class Param(ReadOnly):
|
||||
for rule in self.rules:
|
||||
yield rule.__name__
|
||||
for key in sorted(self.__kw):
|
||||
yield '%s=%r' % (key, self.__kw[key])
|
||||
value = self.__kw[key]
|
||||
if callable(value) and hasattr(value, '__name__'):
|
||||
value = value.__name__
|
||||
else:
|
||||
value = repr(value)
|
||||
yield '%s=%s' % (key, value)
|
||||
|
||||
def __call__(self, value, **kw):
|
||||
"""
|
||||
@@ -389,6 +401,16 @@ class Param(ReadOnly):
|
||||
self.validate(value)
|
||||
return value
|
||||
|
||||
def kw(self):
|
||||
"""
|
||||
Iterate through ``(key,value)`` for all kwargs passed to constructor.
|
||||
"""
|
||||
for key in sorted(self.__kw):
|
||||
value = self.__kw[key]
|
||||
if callable(value) and hasattr(value, '__name__'):
|
||||
value = value.__name__
|
||||
yield (key, value)
|
||||
|
||||
def use_in_context(self, env):
|
||||
"""
|
||||
Return ``True`` if this parameter should be used in ``env.context``.
|
||||
@@ -770,6 +792,27 @@ class Bool(Param):
|
||||
type = bool
|
||||
type_error = _('must be True or False')
|
||||
|
||||
# FIXME: This my quick hack to get some UI stuff working, change these defaults
|
||||
# --jderose 2009-08-28
|
||||
kwargs = Param.kwargs + (
|
||||
('truths', frozenset, frozenset([1, u'1', u'True'])),
|
||||
('falsehoods', frozenset, frozenset([0, u'0', u'False'])),
|
||||
)
|
||||
|
||||
def _convert_scalar(self, value, index=None):
|
||||
"""
|
||||
Convert a single scalar value.
|
||||
"""
|
||||
if type(value) is self.type:
|
||||
return value
|
||||
if value in self.truths:
|
||||
return True
|
||||
if value in self.falsehoods:
|
||||
return False
|
||||
raise ConversionError(name=self.name, index=index,
|
||||
error=ugettext(self.type_error),
|
||||
)
|
||||
|
||||
|
||||
class Flag(Bool):
|
||||
"""
|
||||
|
||||
@@ -524,7 +524,7 @@ class API(DictProxy):
|
||||
if context is not None:
|
||||
overrides['context'] = context
|
||||
self.bootstrap(**overrides)
|
||||
return args
|
||||
return (options, args)
|
||||
|
||||
def load_plugins(self):
|
||||
"""
|
||||
|
||||
@@ -28,12 +28,7 @@ from ipalib import api
|
||||
from ipalib.backend import Backend
|
||||
import krbV
|
||||
|
||||
|
||||
# FIXME: Is it safe to assume the Kerberos library is using UTF-8 for the
|
||||
# principal and realm? If not, how do we query the Kerberos library to find
|
||||
# the encoding it's using?
|
||||
ENCODING = 'UTF-8'
|
||||
FS_ENCODING = (sys.getfilesystemencoding() or sys.getdefaultencoding())
|
||||
|
||||
|
||||
class krb(Backend):
|
||||
@@ -61,7 +56,7 @@ class krb(Backend):
|
||||
"""
|
||||
Return the ``krbV.CCache`` for the ``ccname`` credential ccache.
|
||||
"""
|
||||
return krbV.CCache(ccname.encode(FS_ENCODING))
|
||||
return krbV.CCache(ccname)
|
||||
|
||||
def __get_principal(self, ccname):
|
||||
"""
|
||||
@@ -78,7 +73,7 @@ class krb(Backend):
|
||||
This cannot return anything meaningful if used in the server as a
|
||||
request is processed.
|
||||
"""
|
||||
return self.__default_ccache().name.decode(FS_ENCODING)
|
||||
return self.__default_ccache().name
|
||||
|
||||
def default_principal(self):
|
||||
"""
|
||||
|
||||
@@ -45,20 +45,21 @@ class env(LocalOrRemote):
|
||||
keys.add(key)
|
||||
elif query in self.env:
|
||||
keys.add(query)
|
||||
return sorted(keys)
|
||||
return keys
|
||||
|
||||
def execute(self, variables, **options):
|
||||
if variables is None:
|
||||
keys = self.env
|
||||
else:
|
||||
keys = self.__find_keys(variables)
|
||||
return tuple(
|
||||
return dict(
|
||||
(key, self.env[key]) for key in keys
|
||||
)
|
||||
|
||||
def output_for_cli(self, textui, result, variables, **options):
|
||||
if len(result) == 0:
|
||||
return
|
||||
result = tuple((k, result[k]) for k in sorted(result))
|
||||
if len(result) == 1:
|
||||
textui.print_keyval(result)
|
||||
return
|
||||
|
||||
@@ -70,15 +70,15 @@ class user(LDAPObject):
|
||||
takes_params = (
|
||||
Str('givenname',
|
||||
cli_name='first',
|
||||
doc='first name',
|
||||
doc='First name',
|
||||
),
|
||||
Str('sn',
|
||||
cli_name='last',
|
||||
doc='last name',
|
||||
doc='Last name',
|
||||
),
|
||||
Str('uid',
|
||||
cli_name='user',
|
||||
doc='login name',
|
||||
doc='Login name',
|
||||
primary_key=True,
|
||||
default_from=lambda givenname, sn: givenname[0] + sn,
|
||||
normalizer=lambda value: value.lower(),
|
||||
@@ -90,7 +90,7 @@ class user(LDAPObject):
|
||||
),
|
||||
Str('homedirectory?',
|
||||
cli_name='homedir',
|
||||
doc='home directory',
|
||||
doc='Home directory',
|
||||
default_from=lambda uid: '/home/%s' % uid,
|
||||
),
|
||||
Str('loginshell?',
|
||||
@@ -251,4 +251,3 @@ class user_unlock(LDAPQuery):
|
||||
textui.print_dashed('Unlocked user "%s".' % keys[-1])
|
||||
|
||||
api.register(user_unlock)
|
||||
|
||||
|
||||
@@ -25,5 +25,6 @@ XML-RPC client plugin.
|
||||
from ipalib import api
|
||||
|
||||
if 'in_server' in api.env and api.env.in_server is True:
|
||||
from ipaserver.rpcserver import xmlserver
|
||||
from ipaserver.rpcserver import xmlserver, jsonserver
|
||||
api.register(xmlserver)
|
||||
api.register(jsonserver)
|
||||
|
||||
@@ -23,11 +23,25 @@ RPC server.
|
||||
Also see the `ipalib.rpc` module.
|
||||
"""
|
||||
|
||||
from urlparse import parse_qs
|
||||
from xmlrpclib import Fault
|
||||
from ipalib.backend import Executioner
|
||||
from ipalib.errors import PublicError, InternalError, CommandError
|
||||
from ipalib.errors import PublicError, InternalError, CommandError, JSONError
|
||||
from ipalib.request import context, Connection, destroy_context
|
||||
from ipalib.rpc import xml_dumps, xml_loads
|
||||
from ipalib.util import make_repr
|
||||
import json
|
||||
|
||||
|
||||
def read_input(environ):
|
||||
"""
|
||||
Read the request body from environ['wsgi.input'].
|
||||
"""
|
||||
try:
|
||||
length = int(environ.get('CONTENT_LENGTH'))
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
return environ['wsgi.input'].read(length)
|
||||
|
||||
|
||||
def params_2_args_options(params):
|
||||
@@ -39,13 +53,116 @@ def params_2_args_options(params):
|
||||
return (params, dict())
|
||||
|
||||
|
||||
class xmlserver(Executioner):
|
||||
def nicify_query(query, encoding='utf-8'):
|
||||
if not query:
|
||||
return
|
||||
for (key, value) in query.iteritems():
|
||||
if len(value) == 0:
|
||||
yield (key, None)
|
||||
elif len(value) == 1:
|
||||
yield (key, value[0].decode(encoding))
|
||||
else:
|
||||
yield (key, tuple(v.decode(encoding) for v in value))
|
||||
|
||||
|
||||
def extract_query(environ):
|
||||
"""
|
||||
Return the query as a ``dict``, or ``None`` if no query is presest.
|
||||
"""
|
||||
qstr = None
|
||||
if environ['REQUEST_METHOD'] == 'POST':
|
||||
if environ['CONTENT_TYPE'] == 'application/x-www-form-urlencoded':
|
||||
qstr = read_input(environ)
|
||||
elif environ['REQUEST_METHOD'] == 'GET':
|
||||
qstr = environ['QUERY_STRING']
|
||||
if qstr:
|
||||
query = dict(nicify_query(
|
||||
parse_qs(qstr, keep_blank_values=True)
|
||||
))
|
||||
else:
|
||||
query = {}
|
||||
environ['wsgi.query'] = query
|
||||
return query
|
||||
|
||||
|
||||
class WSGIExecutioner(Executioner):
|
||||
"""
|
||||
Base class for execution backends with a WSGI application interface.
|
||||
"""
|
||||
|
||||
def finalize(self):
|
||||
url = self.env['mount_' + self.name]
|
||||
if url.startswith('/'):
|
||||
self.url = url
|
||||
else:
|
||||
self.url = self.env.mount_ipa + url
|
||||
super(WSGIExecutioner, self).finalize()
|
||||
|
||||
def execute(self, environ):
|
||||
result = None
|
||||
error = None
|
||||
_id = None
|
||||
try:
|
||||
self.create_context(ccache=environ.get('KRB5CCNAME'))
|
||||
if (
|
||||
environ.get('CONTENT_TYPE', '').startswith(self.content_type)
|
||||
and environ['REQUEST_METHOD'] == 'POST'
|
||||
):
|
||||
data = read_input(environ)
|
||||
(name, args, options, _id) = self.unmarshal(data)
|
||||
else:
|
||||
(name, args, options, _id) = self.simple_unmarshal(environ)
|
||||
if name not in self.Command:
|
||||
raise CommandError(name=name)
|
||||
result = self.Command[name](*args, **options)
|
||||
except PublicError, e:
|
||||
error = e
|
||||
except StandardError, e:
|
||||
self.exception(
|
||||
'non-public: %s: %s', e.__class__.__name__, str(e)
|
||||
)
|
||||
error = InternalError()
|
||||
finally:
|
||||
destroy_context()
|
||||
return self.marshal(result, error, _id)
|
||||
|
||||
def simple_unmarshal(self, environ):
|
||||
name = environ['PATH_INFO'].strip('/')
|
||||
options = extract_query(environ)
|
||||
return (name, tuple(), options, None)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""
|
||||
WSGI application for execution.
|
||||
"""
|
||||
try:
|
||||
status = '200 OK'
|
||||
response = self.execute(environ)
|
||||
headers = [('Content-Type', self.content_type + '; charset=utf-8')]
|
||||
except StandardError, e:
|
||||
self.exception('%s.__call__():', self.name)
|
||||
status = '500 Internal Server Error'
|
||||
response = status
|
||||
headers = [('Content-Type', 'text/plain')]
|
||||
start_response(status, headers)
|
||||
return [response]
|
||||
|
||||
def unmarshal(self, data):
|
||||
raise NotImplementedError('%s.unmarshal()' % self.fullname)
|
||||
|
||||
def marshal(self, result, error, _id=None):
|
||||
raise NotImplementedError('%s.marshal()' % self.fullname)
|
||||
|
||||
|
||||
class xmlserver(WSGIExecutioner):
|
||||
"""
|
||||
Execution backend plugin for XML-RPC server.
|
||||
|
||||
Also see the `ipalib.rpc.xmlclient` plugin.
|
||||
"""
|
||||
|
||||
content_type = 'text/xml'
|
||||
|
||||
def finalize(self):
|
||||
self.__system = {
|
||||
'system.listMethods': self.listMethods,
|
||||
@@ -79,3 +196,75 @@ class xmlserver(Executioner):
|
||||
self.info('response: %s: %s', e.__class__.__name__, str(e))
|
||||
response = Fault(e.errno, e.strerror)
|
||||
return xml_dumps(response, methodresponse=True)
|
||||
|
||||
def unmarshal(self, data):
|
||||
(params, name) = xml_loads(data)
|
||||
(args, options) = params_2_args_options(params)
|
||||
return (name, args, options, None)
|
||||
|
||||
def marshal(self, result, error, _id=None):
|
||||
if error:
|
||||
response = Fault(error.errno, error.strerror)
|
||||
else:
|
||||
response = (result,)
|
||||
return xml_dumps(response, methodresponse=True)
|
||||
|
||||
|
||||
class jsonserver(WSGIExecutioner):
|
||||
"""
|
||||
JSON RPC server.
|
||||
|
||||
For information on the JSON-RPC spec, see:
|
||||
|
||||
http://json-rpc.org/wiki/specification
|
||||
"""
|
||||
|
||||
content_type = 'application/json'
|
||||
|
||||
def marshal(self, result, error, _id=None):
|
||||
if error:
|
||||
assert isinstance(error, PublicError)
|
||||
error = dict(
|
||||
code=error.errno,
|
||||
message=error.strerror,
|
||||
name=error.__class__.__name__,
|
||||
kw=dict(error.kw),
|
||||
)
|
||||
response = dict(
|
||||
result=result,
|
||||
error=error,
|
||||
id=_id,
|
||||
)
|
||||
return json.dumps(response, sort_keys=True, indent=4)
|
||||
|
||||
def unmarshal(self, data):
|
||||
try:
|
||||
d = json.loads(data)
|
||||
except ValueError, e:
|
||||
raise JSONError(error=e)
|
||||
if not isinstance(d, dict):
|
||||
raise JSONError(error='Request must be a dict')
|
||||
if 'method' not in d:
|
||||
raise JSONError(error='Request is missing "method"')
|
||||
if 'params' not in d:
|
||||
raise JSONError(error='Request is missing "params"')
|
||||
method = d['method']
|
||||
params = d['params']
|
||||
_id = d.get('id')
|
||||
if not isinstance(params, (list, tuple)):
|
||||
raise JSONError(error='params must be a list')
|
||||
if len(params) != 2:
|
||||
raise JSONError(
|
||||
error='params must contain [args, options]'
|
||||
)
|
||||
args = params[0]
|
||||
if not isinstance(args, (list, tuple)):
|
||||
raise JSONError(
|
||||
error='params[0] (aka args) must be a list'
|
||||
)
|
||||
options = params[1]
|
||||
if not isinstance(options, dict):
|
||||
raise JSONError(
|
||||
error='params[1] (aka options) must be a dict'
|
||||
)
|
||||
return (method, args, options, _id)
|
||||
|
||||
@@ -17,8 +17,40 @@
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Package containing web-based UI components.
|
||||
IPA web UI.
|
||||
"""
|
||||
|
||||
import kid
|
||||
kid.enable_import()
|
||||
from controllers import JSON
|
||||
from engine import Engine
|
||||
from widgets import create_widgets
|
||||
|
||||
from assetslib import Assets
|
||||
from wehjit import Application
|
||||
|
||||
|
||||
def join_url(base, url):
|
||||
if url.startswith('/'):
|
||||
return url
|
||||
return base + url
|
||||
|
||||
|
||||
def create_wsgi_app(api):
|
||||
baseurl = api.env.mount_ipa
|
||||
assets = Assets(
|
||||
url=join_url(baseurl, api.env.mount_webui_assets),
|
||||
dir=api.env.webui_assets_dir,
|
||||
prod=api.env.webui_prod,
|
||||
)
|
||||
app = Application(
|
||||
url=join_url(baseurl, api.env.mount_webui),
|
||||
assets=assets,
|
||||
widgets=create_widgets(),
|
||||
prod=api.env.webui_prod,
|
||||
)
|
||||
|
||||
engine = Engine(api, app)
|
||||
engine.build()
|
||||
|
||||
app.finalize()
|
||||
|
||||
return app
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# Authors: Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Controller classes.
|
||||
"""
|
||||
|
||||
import simplejson
|
||||
from ipalib.plugable import ReadOnly, lock
|
||||
|
||||
|
||||
class Controller(ReadOnly):
|
||||
exposed = True
|
||||
|
||||
def __init__(self, template=None):
|
||||
self.template = template
|
||||
lock(self)
|
||||
|
||||
def output_xhtml(self, **kw):
|
||||
return self.template.serialize(
|
||||
output='xhtml-strict',
|
||||
format='pretty',
|
||||
**kw
|
||||
)
|
||||
|
||||
def output_json(self, **kw):
|
||||
return simplejson.dumps(kw, sort_keys=True, indent=4)
|
||||
|
||||
def __call__(self, **kw):
|
||||
json = bool(kw.pop('_format', None) == 'json')
|
||||
result = self.run(**kw)
|
||||
assert type(result) is dict
|
||||
if json or self.template is None:
|
||||
return self.output_json(**result)
|
||||
return self.output_xhtml(**result)
|
||||
|
||||
def run(self, **kw):
|
||||
return {}
|
||||
|
||||
|
||||
class Command(Controller):
|
||||
def __init__(self, command, template=None):
|
||||
self.command = command
|
||||
super(Command, self).__init__(template)
|
||||
|
||||
def run(self, **kw):
|
||||
return dict(command=self.command)
|
||||
|
||||
|
||||
class Index(Controller):
|
||||
def __init__(self, api, template=None):
|
||||
self.api = api
|
||||
super(Index, self).__init__(template)
|
||||
|
||||
def run(self):
|
||||
return dict(api=self.api)
|
||||
59
ipawebui/controllers.py
Normal file
59
ipawebui/controllers.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Authors: Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Controllers.
|
||||
"""
|
||||
|
||||
from wehjit import util
|
||||
import json
|
||||
|
||||
|
||||
class JSON(object):
|
||||
def __init__(self, url, api):
|
||||
self.url = url
|
||||
self.api = api
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(url=%r)' % (self.__class__.__name__, self.url)
|
||||
|
||||
def __call__(self, env, start):
|
||||
util.extract_query(env)
|
||||
start('200 OK', [('Content-Type', 'text/plain')])
|
||||
for key in sorted(env):
|
||||
yield '%s = %r\n' % (key, env[key])
|
||||
|
||||
|
||||
class Command(object):
|
||||
def __init__(self, url, cmd, api):
|
||||
self.url = url
|
||||
self.cmd = cmd
|
||||
self.api = api
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(url=%r)' % (self.__class__.__name__, self.url)
|
||||
|
||||
def __call__(self, env, start):
|
||||
kw = util.extract_query(env)
|
||||
ccname = env['KRB5CCNAME']
|
||||
self.api.Backend.xmlserver.create_context(ccname)
|
||||
result = self.api.Backend.xmlserver.execute(self.cmd.name, **kw)
|
||||
start('200 OK', [('Content-Type', 'text/plain')])
|
||||
return [
|
||||
json.dumps(result, sort_keys=True, indent=4)
|
||||
]
|
||||
152
ipawebui/engine.py
Normal file
152
ipawebui/engine.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# Authors: Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Engine to map ipalib plugins to wehjit widgets.
|
||||
"""
|
||||
|
||||
from controllers import Command
|
||||
|
||||
class ParamMapper(object):
|
||||
def __init__(self, api, app):
|
||||
self._api = api
|
||||
self._app = app
|
||||
self.__methods = dict()
|
||||
for name in dir(self):
|
||||
if name.startswith('_') or name.endswith('_'):
|
||||
continue
|
||||
attr = getattr(self, name)
|
||||
if not callable(attr):
|
||||
continue
|
||||
self.__methods[name] = attr
|
||||
|
||||
def __call__(self, param, cmd):
|
||||
key = param.__class__.__name__
|
||||
if key in self.__methods:
|
||||
method = self.__methods[key]
|
||||
else:
|
||||
#raise Warning('No ParamMapper for %r' % key)
|
||||
method = self.Str
|
||||
return method(param, cmd)
|
||||
|
||||
def Str(self, param, cmd):
|
||||
return self._app.new('TextRow',
|
||||
label=param.cli_name,
|
||||
name=param.name,
|
||||
required=param.required,
|
||||
value=param.default,
|
||||
)
|
||||
|
||||
def Password(self, param, cmd):
|
||||
return self._app.new('PasswordRow',
|
||||
name=param.name,
|
||||
required=param.required,
|
||||
)
|
||||
|
||||
def Flag(self, param, cmd):
|
||||
return self._app.new('SelectRow',
|
||||
name=param.name,
|
||||
label=param.cli_name,
|
||||
)
|
||||
|
||||
|
||||
class Engine(object):
|
||||
def __init__(self, api, app):
|
||||
self.api = api
|
||||
self.app = app
|
||||
self.param_mapper = ParamMapper(api, app)
|
||||
self.pages = dict()
|
||||
self.jsonurl = self.api.Backend.jsonserver.url.rstrip('/')
|
||||
self.info_pages = []
|
||||
|
||||
def add_object_menuitems(self, menu, name):
|
||||
obj = self.api.Object[name]
|
||||
for cmd in obj.methods():
|
||||
p = self.pages[cmd.name]
|
||||
menu.add(
|
||||
menu.new('MenuItem',
|
||||
label=p.title,
|
||||
href=p.url,
|
||||
)
|
||||
)
|
||||
|
||||
def build(self):
|
||||
for cmd in self.api.Command():
|
||||
self.pages[cmd.name] = self.build_page(cmd)
|
||||
for page in self.pages.itervalues():
|
||||
page.menu.label = 'Users'
|
||||
self.add_object_menuitems(page.menu, 'user')
|
||||
|
||||
menu = page.new('Menu', label='Groups')
|
||||
page.menuset.add(menu)
|
||||
self.add_object_menuitems(menu, 'group')
|
||||
|
||||
# Add in the info pages:
|
||||
page = self.app.new('PageApp', id='api', title='api')
|
||||
page.view.add(
|
||||
self.app.new('API', api=self.api)
|
||||
)
|
||||
self.info_pages.append(page)
|
||||
|
||||
for kind in self.api:
|
||||
self.build_info_page(kind)
|
||||
for page in self.info_pages:
|
||||
for p in self.info_pages:
|
||||
page.menuset.add(
|
||||
self.app.new('MenuItem',
|
||||
href=p.url,
|
||||
label=p.title,
|
||||
)
|
||||
)
|
||||
|
||||
def build_info_page(self, kind):
|
||||
# Add in the Object page:
|
||||
plugins = tuple(self.api[kind]())
|
||||
page = self.app.new('PageApp', id=kind, title=kind)
|
||||
info = self.app.new('IPAPlugins', kind=kind, plugins=plugins)
|
||||
quick_jump = self.app.new('QuickJump',
|
||||
options=tuple((p.name, p.name) for p in plugins)
|
||||
)
|
||||
page.view.add(info)
|
||||
page.actions.add(quick_jump)
|
||||
self.info_pages.append(page)
|
||||
if kind in self.app.widgets:
|
||||
info.add(
|
||||
self.app.new(kind)
|
||||
)
|
||||
return page
|
||||
|
||||
def build_page(self, cmd):
|
||||
page = self.app.new('PageApp',
|
||||
id=cmd.name,
|
||||
title=cmd.summary.rstrip('.'),
|
||||
)
|
||||
#page.form.action = self.app.url + '__json__'
|
||||
page.actions.add(
|
||||
self.app.new('Submit')
|
||||
)
|
||||
table = self.app.new('FieldTable')
|
||||
page.view.add(table)
|
||||
for param in cmd.params():
|
||||
field = self.param_mapper(param, cmd)
|
||||
table.add(field)
|
||||
|
||||
page.form.action = '/'.join([self.jsonurl, cmd.name])
|
||||
|
||||
|
||||
return page
|
||||
@@ -1,22 +0,0 @@
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Production Web UI using mod_python.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
# Authors: Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Sub-package containing Kid templates.
|
||||
"""
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<html xmlns:py="http://purl.org/kid/ns#">
|
||||
|
||||
<head>
|
||||
<title>Hello</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table>
|
||||
<tr py:for="param in command.params()">
|
||||
<td py:content="param.name"/>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<html xmlns:py="http://purl.org/kid/ns#">
|
||||
|
||||
<head>
|
||||
<title>FreeIPA</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p py:for="name in api.Command">
|
||||
<a href="${name}" py:content="name"/>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
228
ipawebui/widgets.py
Normal file
228
ipawebui/widgets.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# Authors: Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Custom IPA widgets.
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
from wehjit import Collection, base, freeze
|
||||
from wehjit.util import Alternator
|
||||
from wehjit import Static, Dynamic, StaticProp, DynamicProp
|
||||
|
||||
|
||||
class IPAPlugins(base.Container):
|
||||
plugins = Static('plugins', default=tuple())
|
||||
kind = Static('kind')
|
||||
|
||||
@DynamicProp
|
||||
def row(self):
|
||||
return Alternator(['odd', 'even'])
|
||||
|
||||
xml = """
|
||||
<div
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
class="${css_classes}"
|
||||
id="${id}"
|
||||
>
|
||||
<p py:content="'%d %s plugins' % (len(plugins), kind)" />
|
||||
|
||||
<div py:for="p in plugins">
|
||||
<h2 id="${p.name}"><a href="#${p.name}" py:content="p.name" /></h2>
|
||||
|
||||
<table class="${row.reset()}">
|
||||
|
||||
<tr class="${row.next()}">
|
||||
<td>module</td>
|
||||
<td>
|
||||
<a
|
||||
title="Link to module documentation"
|
||||
href="http://freeipa.org/developer-docs/${p.module}-module.html"
|
||||
py:content="p.module"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr py:if="p.doc" class="${row.next()}">
|
||||
<td>docstring</td>
|
||||
<td><pre py:content="p.doc" /></td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
py:for="child in children"
|
||||
py:replace="child.generate(plugin=p, row=row)"
|
||||
/>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
"""
|
||||
|
||||
style_global = (
|
||||
('tr.odd', (
|
||||
('background-color', '#ddd'),
|
||||
)),
|
||||
('tr.even', (
|
||||
('background-color', '#eee'),
|
||||
)),
|
||||
|
||||
('td', (
|
||||
('vertical-align', 'top'),
|
||||
('padding', '0.25em 0.5em'),
|
||||
)),
|
||||
)
|
||||
|
||||
style = (
|
||||
('', (
|
||||
('font-size', '%(font_size_mono)s'),
|
||||
('font-family', 'monospace'),
|
||||
)),
|
||||
|
||||
('table', (
|
||||
('width', '100%%'),
|
||||
)),
|
||||
|
||||
('pre', (
|
||||
('margin', '0'),
|
||||
)),
|
||||
|
||||
('th', (
|
||||
('color', '#0a0'),
|
||||
)),
|
||||
|
||||
('h2', (
|
||||
('font-family', 'monospace'),
|
||||
('font-weight', 'normal'),
|
||||
('margin-top', '1.5em'),
|
||||
('margin-bottom', '0'),
|
||||
)),
|
||||
|
||||
('h2 a', (
|
||||
('text-decoration', 'none'),
|
||||
('color', 'inherit'),
|
||||
)),
|
||||
|
||||
('h2 a:hover', (
|
||||
('background-color', '#eee'),
|
||||
)),
|
||||
|
||||
('h2:target', (
|
||||
('color', '#e02'),
|
||||
)),
|
||||
)
|
||||
|
||||
|
||||
class API(base.Widget):
|
||||
api = Static('api')
|
||||
|
||||
@DynamicProp
|
||||
def row(self):
|
||||
return Alternator(['odd', 'even'])
|
||||
|
||||
xml = """
|
||||
<div
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
class="${css_classes}"
|
||||
id="${id}"
|
||||
>
|
||||
<p py:content="'%d namespaces in API' % len(api)" />
|
||||
<table>
|
||||
<tr py:for="key in api" class="${row.next()}">
|
||||
<td>
|
||||
<a href="${key}" py:content="'api.' + key" />
|
||||
</td>
|
||||
<td py:content="repr(api[key])" />
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
class Command(base.Widget):
|
||||
xml = """
|
||||
<table
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
py:strip="True"
|
||||
>
|
||||
|
||||
<tr py:if="plugin.obj" class="${row.next()}">
|
||||
<td>Object</td>
|
||||
<td>
|
||||
<a href="Object#${plugin.obj.name}" py:content="plugin.obj.fullname" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr py:if="plugin.args" class="${row.next()}">
|
||||
<th colspan="2" py:content="'args (%d)' % len(plugin.args)" />
|
||||
</tr>
|
||||
<tr py:for="arg in plugin.args()" class="${row.next()}">
|
||||
<td py:content="arg.name"/>
|
||||
<td py:content="repr(arg)" />
|
||||
</tr>
|
||||
|
||||
<tr py:if="plugin.options" class="${row.next()}">
|
||||
<th colspan="2" py:content="'options (%d)' % len(plugin.options)" />
|
||||
</tr>
|
||||
<tr py:for="option in plugin.options()" class="${row.next()}">
|
||||
<td py:content="option.name"/>
|
||||
<td py:content="repr(option)" />
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
"""
|
||||
|
||||
|
||||
class Object(base.Widget):
|
||||
xml = """
|
||||
<table
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
py:strip="True"
|
||||
>
|
||||
<tr py:if="plugin.methods" class="${row.next()}">
|
||||
<th colspan="2" py:content="'methods (%d)' % len(plugin.methods)" />
|
||||
</tr>
|
||||
<tr py:for="method in plugin.methods()" class="${row.next()}">
|
||||
<td><a href="${'Command#' + method.name}" py:content="method.name"/></td>
|
||||
<td py:content="method.summary" />
|
||||
</tr>
|
||||
|
||||
<tr py:if="plugin.params" class="${row.next()}">
|
||||
<th colspan="2" py:content="'params (%d)' % len(plugin.params)" />
|
||||
</tr>
|
||||
<tr py:for="param in plugin.params()" class="${row.next()}">
|
||||
<td py:content="param.name"/>
|
||||
<td py:content="repr(param)" />
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
"""
|
||||
|
||||
|
||||
def create_widgets():
|
||||
widgets = Collection('freeIPA')
|
||||
widgets.register_builtins()
|
||||
|
||||
widgets.register(API)
|
||||
widgets.register(IPAPlugins)
|
||||
widgets.register(Command)
|
||||
widgets.register(Object)
|
||||
|
||||
|
||||
freeze(widgets)
|
||||
return widgets
|
||||
108
lite-server.py
Executable file
108
lite-server.py
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
In-tree paste-based test server.
|
||||
|
||||
This uses the *Python Paste* WSGI server. For more info, see:
|
||||
|
||||
http://pythonpaste.org/
|
||||
|
||||
Unfortunately, SSL support is broken under Python 2.6 with paste 1.7.2, see:
|
||||
|
||||
http://trac.pythonpaste.org/pythonpaste/ticket/314
|
||||
"""
|
||||
|
||||
from os import path
|
||||
import optparse
|
||||
from paste import httpserver
|
||||
import paste.gzipper
|
||||
from paste.urlmap import URLMap
|
||||
from assetslib.wsgi import AssetsApp
|
||||
from ipalib import api
|
||||
import ipawebui
|
||||
|
||||
|
||||
class KRBCheater(object):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.url = app.url
|
||||
self.ccname = api.Backend.krb.default_ccname()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
environ['KRB5CCNAME'] = self.ccname
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = optparse.OptionParser()
|
||||
|
||||
parser.add_option('--dev',
|
||||
help='Run WebUI in development mode (requires FireBug)',
|
||||
default=True,
|
||||
action='store_false',
|
||||
dest='prod',
|
||||
)
|
||||
parser.add_option('--host',
|
||||
help='Listen on address HOST (default 127.0.0.1)',
|
||||
default='127.0.0.1',
|
||||
)
|
||||
parser.add_option('--port',
|
||||
help='Listen on PORT (default 8888)',
|
||||
default=8888,
|
||||
type='int',
|
||||
)
|
||||
|
||||
api.env.in_server = True
|
||||
(options, args) = api.bootstrap_with_global_options(parser, context='lite')
|
||||
api.env._merge(
|
||||
lite_port=options.port,
|
||||
lite_host=options.host,
|
||||
webui_prod=options.prod,
|
||||
lite_pem=api.env._join('dot_ipa', 'lite.pem'),
|
||||
)
|
||||
api.finalize()
|
||||
|
||||
ui = ipawebui.create_wsgi_app(api)
|
||||
ui.render_assets()
|
||||
|
||||
urlmap = URLMap()
|
||||
apps = [
|
||||
('XML RPC', api.Backend.xmlserver),
|
||||
('JSON RPC', api.Backend.jsonserver),
|
||||
('Assets', AssetsApp(ui.assets)),
|
||||
('Web UI', ui),
|
||||
]
|
||||
for (name, app) in apps:
|
||||
urlmap[app.url] = KRBCheater(app)
|
||||
api.log.info('Mounting %s at %s', name, app.url)
|
||||
|
||||
if path.isfile(api.env.lite_pem):
|
||||
pem = api.env.lite_pem
|
||||
else:
|
||||
api.log.info('To enable SSL, place PEM file at %r', api.env.lite_pem)
|
||||
pem = None
|
||||
|
||||
httpserver.serve(paste.gzipper.middleware(urlmap),
|
||||
host=api.env.lite_host,
|
||||
port=api.env.lite_port,
|
||||
ssl_pem=pem,
|
||||
)
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
In-tree Web UI using cherrypy.
|
||||
"""
|
||||
|
||||
from cherrypy import expose, config, quickstart
|
||||
from ipawebui.templates import form, main
|
||||
from ipawebui import controller
|
||||
from ipalib import api
|
||||
|
||||
api.load_plugins()
|
||||
api.finalize()
|
||||
|
||||
|
||||
class root(object):
|
||||
index = controller.Index(api, main)
|
||||
|
||||
def __init__(self):
|
||||
for cmd in api.Command():
|
||||
ctr = controller.Command(cmd, form)
|
||||
setattr(self, cmd.name, ctr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
quickstart(root())
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Authors:
|
||||
# Jason Gerard DeRose <jderose@redhat.com>
|
||||
#
|
||||
# Copyright (C) 2008 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; version 2 only
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
In-tree XML-RPC server using SimpleXMLRPCServer.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
|
||||
import krbV
|
||||
from ipalib import api
|
||||
|
||||
|
||||
class Server(SimpleXMLRPCServer):
|
||||
"""
|
||||
Custom server implementing `Server._marshaled_dispatch()`.
|
||||
"""
|
||||
|
||||
def _marshaled_dispatch(self, data, dispatch_method=None):
|
||||
"""
|
||||
Use `ipaserver.rpcserver.xmlserver.marshaled_dispatch()`.
|
||||
"""
|
||||
try:
|
||||
ccache=krbV.default_context().default_ccache().name
|
||||
return api.Backend.xmlserver.marshaled_dispatch(data, ccache)
|
||||
except Exception, e:
|
||||
api.log.exception('lite-xmlrpc: caught error in _marshaled_dispatch()')
|
||||
raise e
|
||||
|
||||
|
||||
class RequestHandler(SimpleXMLRPCRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
try:
|
||||
client = '%r %r' % self.client_address
|
||||
except Exception, e:
|
||||
api.log.exception('lite-xmlrpc: caught error in do_POST()')
|
||||
raise e
|
||||
return SimpleXMLRPCRequestHandler.do_POST(self)
|
||||
|
||||
|
||||
api.bootstrap_with_global_options(context='server')
|
||||
api.finalize()
|
||||
|
||||
kw = dict(requestHandler=RequestHandler, logRequests=False)
|
||||
if sys.version_info[:2] != (2, 4):
|
||||
kw.update(dict(encoding='UTF-8', allow_none=True))
|
||||
server = Server(('', api.env.lite_xmlrpc_port), **kw)
|
||||
|
||||
api.log.info('Logging to file %r', api.env.log)
|
||||
api.log.info('Listening on port %d', api.env.lite_xmlrpc_port)
|
||||
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
api.log.info('KeyboardInterrupt: shutting down server...')
|
||||
server.server_close()
|
||||
api.log.info('Server shutdown.')
|
||||
7
setup.py
7
setup.py
@@ -23,7 +23,7 @@
|
||||
Python-level packaging using distutils.
|
||||
"""
|
||||
|
||||
from setuptools import setup
|
||||
from distutils.core import setup
|
||||
import ipalib
|
||||
|
||||
setup(
|
||||
@@ -38,11 +38,6 @@ setup(
|
||||
'ipaserver.plugins',
|
||||
'ipaserver.install',
|
||||
'ipawebui',
|
||||
'ipawebui.templates',
|
||||
],
|
||||
package_data={
|
||||
'ipawebui.templates': ['*.kid'],
|
||||
'ipawebui': ['static/*'],
|
||||
},
|
||||
scripts=['ipa'],
|
||||
)
|
||||
|
||||
@@ -60,6 +60,27 @@ class test_DefaultFrom(ClassChecker):
|
||||
e = raises(TypeError, self.cls, callback, 'givenname', 17)
|
||||
assert str(e) == TYPE_ERROR % ('keys', str, 17, int)
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Test the `ipalib.parameters.DefaultFrom.__repr__` method.
|
||||
"""
|
||||
def stuff(one, two):
|
||||
pass
|
||||
|
||||
o = self.cls(stuff)
|
||||
assert repr(o) == "DefaultFrom(stuff, 'one', 'two')"
|
||||
|
||||
o = self.cls(stuff, 'aye', 'bee', 'see')
|
||||
assert repr(o) == "DefaultFrom(stuff, 'aye', 'bee', 'see')"
|
||||
|
||||
cb = lambda first, last: first[0] + last
|
||||
|
||||
o = self.cls(cb)
|
||||
assert repr(o) == "DefaultFrom(<lambda>, 'first', 'last')"
|
||||
|
||||
o = self.cls(cb, 'aye', 'bee', 'see')
|
||||
assert repr(o) == "DefaultFrom(<lambda>, 'aye', 'bee', 'see')"
|
||||
|
||||
def test_call(self):
|
||||
"""
|
||||
Test the `ipalib.parameters.DefaultFrom.__call__` method.
|
||||
|
||||
@@ -25,6 +25,7 @@ from tests.util import create_test_api, raises, PluginTester
|
||||
from tests.data import unicode_str
|
||||
from ipalib import errors, Command
|
||||
from ipaserver import rpcserver
|
||||
import json
|
||||
|
||||
|
||||
def test_params_2_args_options():
|
||||
@@ -50,3 +51,65 @@ class test_xmlserver(PluginTester):
|
||||
|
||||
def test_marshaled_dispatch(self):
|
||||
(o, api, home) = self.instance('Backend', in_server=True)
|
||||
|
||||
|
||||
class test_jsonserver(PluginTester):
|
||||
"""
|
||||
Test the `ipaserver.rpcserver.jsonserver` plugin.
|
||||
"""
|
||||
|
||||
_plugin = rpcserver.jsonserver
|
||||
|
||||
def test_unmarshal(self):
|
||||
"""
|
||||
Test the `ipaserver.rpcserver.jsonserver.unmarshal` method.
|
||||
"""
|
||||
(o, api, home) = self.instance('Backend', in_server=True)
|
||||
|
||||
# Test with invalid JSON-data:
|
||||
e = raises(errors.JSONError, o.unmarshal, 'this wont work')
|
||||
assert isinstance(e.error, ValueError)
|
||||
assert str(e.error) == 'No JSON object could be decoded'
|
||||
|
||||
# Test with non-dict type:
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps([1, 2, 3]))
|
||||
assert str(e.error) == 'Request must be a dict'
|
||||
|
||||
params = [[1, 2], dict(three=3, four=4)]
|
||||
# Test with missing method:
|
||||
d = dict(params=params, id=18)
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
|
||||
assert str(e.error) == 'Request is missing "method"'
|
||||
|
||||
# Test with missing params:
|
||||
d = dict(method='echo', id=18)
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
|
||||
assert str(e.error) == 'Request is missing "params"'
|
||||
|
||||
# Test with non-list params:
|
||||
for p in ('hello', dict(args=tuple(), options=dict())):
|
||||
d = dict(method='echo', id=18, params=p)
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
|
||||
assert str(e.error) == 'params must be a list'
|
||||
|
||||
# Test with other than 2 params:
|
||||
for p in ([], [tuple()], [None, dict(), tuple()]):
|
||||
d = dict(method='echo', id=18, params=p)
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
|
||||
assert str(e.error) == 'params must contain [args, options]'
|
||||
|
||||
# Test when args is not a list:
|
||||
d = dict(method='echo', id=18, params=['args', dict()])
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
|
||||
assert str(e.error) == 'params[0] (aka args) must be a list'
|
||||
|
||||
# Test when options is not a dict:
|
||||
d = dict(method='echo', id=18, params=[('hello', 'world'), 'options'])
|
||||
e = raises(errors.JSONError, o.unmarshal, json.dumps(d))
|
||||
assert str(e.error) == 'params[1] (aka options) must be a dict'
|
||||
|
||||
# Test with valid values:
|
||||
args = [u'jdoe']
|
||||
options = dict(givenname=u'John', sn='Doe')
|
||||
d = dict(method=u'user_add', params=[args, options], id=18)
|
||||
assert o.unmarshal(json.dumps(d)) == (u'user_add', args, options, 18)
|
||||
|
||||
@@ -17,54 +17,8 @@
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
Test the `ipawebui.controller` module.
|
||||
Test the `ipawebui.controllers` module.
|
||||
"""
|
||||
|
||||
from ipawebui import controller
|
||||
|
||||
|
||||
|
||||
class test_Controller(object):
|
||||
"""
|
||||
Test the `controller.Controller` class.
|
||||
"""
|
||||
|
||||
def test_init(self):
|
||||
"""
|
||||
Test the `ipawebui.controller.Controller.__init__()` method.
|
||||
"""
|
||||
o = controller.Controller()
|
||||
assert o.template is None
|
||||
template = 'The template.'
|
||||
o = controller.Controller(template)
|
||||
assert o.template is template
|
||||
|
||||
def test_output_xhtml(self):
|
||||
"""
|
||||
Test the `ipawebui.controller.Controller.output_xhtml` method.
|
||||
"""
|
||||
class Template(object):
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
self.kw = {}
|
||||
|
||||
def serialize(self, **kw):
|
||||
self.calls += 1
|
||||
self.kw = kw
|
||||
return dict(kw)
|
||||
|
||||
d = dict(output='xhtml-strict', format='pretty')
|
||||
t = Template()
|
||||
o = controller.Controller(t)
|
||||
assert o.output_xhtml() == d
|
||||
assert t.calls == 1
|
||||
|
||||
def test_output_json(self):
|
||||
"""
|
||||
Test the `ipawebui.controller.Controller.output_json` method.
|
||||
"""
|
||||
o = controller.Controller()
|
||||
assert o.output_json() == '{}'
|
||||
e = '{\n "age": 27, \n "first": "John", \n "last": "Doe"\n}'
|
||||
j = o.output_json(last='Doe', first='John', age=27)
|
||||
assert j == e
|
||||
from wsgiref import util
|
||||
from wsgiref.validate import validator
|
||||
|
||||
Reference in New Issue
Block a user