Giant webui patch take 2

This commit is contained in:
Jason Gerard DeRose
2009-10-13 11:28:00 -06:00
parent 1d6e23136a
commit f58ff2921d
30 changed files with 956 additions and 4302 deletions

View File

@@ -1,4 +1,2 @@
include LICENSE TODO lite-webui.py lite-xmlrpc.py include LICENSE TODO lite-server.py
graft tests/ include tests/*/*.py
graft ipawebui/static/
include ipawebui/templates/*.kid

View File

@@ -35,7 +35,6 @@ BuildRequires: popt-devel
BuildRequires: /usr/share/selinux/devel/Makefile BuildRequires: /usr/share/selinux/devel/Makefile
BuildRequires: m4 BuildRequires: m4
BuildRequires: policycoreutils >= %{POLICYCOREUTILSVER} BuildRequires: policycoreutils >= %{POLICYCOREUTILSVER}
BuildRequires: python-cherrypy
BuildRequires: python-setuptools BuildRequires: python-setuptools
BuildRequires: python-krbV BuildRequires: python-krbV
BuildRequires: xmlrpc-c-devel BuildRequires: xmlrpc-c-devel
@@ -75,7 +74,8 @@ Requires: mod_nss
%endif %endif
Requires: python-ldap Requires: python-ldap
Requires: python-krbV Requires: python-krbV
Requires: python-cherrypy Requires: python-assets
Requires: python-wehjit
Requires: acl Requires: acl
Requires: python-pyasn1 Requires: python-pyasn1
Requires: libcap Requires: libcap
@@ -440,6 +440,10 @@ fi
%endif %endif
%changelog %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 * Mon Aug 24 2009 Rob Crittenden <rcritten@redhat.com> - 1.99-7
- Added httpd SELinux policy so CRLs can be read - Added httpd SELinux policy so CRLs can be read

View File

@@ -836,7 +836,7 @@ cli_plugins = (
def run(api): def run(api):
error = None error = None
try: try:
argv = api.bootstrap_with_global_options(context='cli') (options, argv) = api.bootstrap_with_global_options(context='cli')
for klass in cli_plugins: for klass in cli_plugins:
api.register(klass) api.register(klass)
api.load_plugins() api.load_plugins()

View File

@@ -99,11 +99,23 @@ DEFAULT_CONFIG = (
('container_virtual', 'cn=virtual operations'), ('container_virtual', 'cn=virtual operations'),
# Ports, hosts, and URIs: # Ports, hosts, and URIs:
('lite_xmlrpc_port', 8888), # FIXME: let's renamed xmlrpc_uri to rpc_xml_uri
('lite_webui_port', 9999), ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
('xmlrpc_uri', 'http://localhost:8888'), ('rpc_json_uri', 'http://localhost:8888/ipa/json'),
('ldap_uri', 'ldap://localhost:389'), ('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: # Debugging:
('verbose', False), ('verbose', False),
('debug', False), ('debug', False),

View File

@@ -112,6 +112,7 @@ class PrivateError(StandardError):
def __init__(self, **kw): def __init__(self, **kw):
self.msg = self.format % kw self.msg = self.format % kw
self.kw = kw
for (key, value) in kw.iteritems(): for (key, value) in kw.iteritems():
assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % ( assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % (
self.__class__.__name__, key, value, self.__class__.__name__, key, value,
@@ -244,6 +245,7 @@ class PublicError(StandardError):
format = None format = None
def __init__(self, format=None, message=None, **kw): def __init__(self, format=None, message=None, **kw):
self.kw = kw
name = self.__class__.__name__ name = self.__class__.__name__
if self.format is not None and format is not None: if self.format is not None and format is not None:
raise ValueError( raise ValueError(
@@ -407,6 +409,15 @@ class ServerNetworkError(PublicError):
format = _('error on server %(server)r: %(error)s') 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 # 1000 - 1999: Authentication errors

View File

@@ -375,6 +375,7 @@ class Command(HasParam):
options = None options = None
params = None params = None
output_for_cli = None output_for_cli = None
obj = None
def __call__(self, *args, **options): def __call__(self, *args, **options):
""" """

View File

@@ -132,6 +132,13 @@ class DefaultFrom(ReadOnly):
) )
lock(self) 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): def __call__(self, **kw):
""" """
Call the callback if all keys are present. Call the callback if all keys are present.
@@ -376,7 +383,12 @@ class Param(ReadOnly):
for rule in self.rules: for rule in self.rules:
yield rule.__name__ yield rule.__name__
for key in sorted(self.__kw): 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): def __call__(self, value, **kw):
""" """
@@ -389,6 +401,16 @@ class Param(ReadOnly):
self.validate(value) self.validate(value)
return 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): def use_in_context(self, env):
""" """
Return ``True`` if this parameter should be used in ``env.context``. Return ``True`` if this parameter should be used in ``env.context``.
@@ -770,6 +792,27 @@ class Bool(Param):
type = bool type = bool
type_error = _('must be True or False') 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): class Flag(Bool):
""" """
@@ -1220,7 +1263,7 @@ class GeneralizedTime(Str):
mm = int(t[2:4]) mm = int(t[2:4])
if mm < 0 or mm > 59: if mm < 0 or mm > 59:
raise ValueError('MM out of range') raise ValueError('MM out of range')
def _check_dotw(self, t): def _check_dotw(self, t):
if t.isnumeric(): if t.isnumeric():
value = int(t) value = int(t)
@@ -1266,7 +1309,7 @@ class GeneralizedTime(Str):
raise ValueError('month number non-numeric') raise ValueError('month number non-numeric')
value = int(t) value = int(t)
if value < 1 or value > 12: if value < 1 or value > 12:
raise ValueError('month number out of range') raise ValueError('month number out of range')
def _check_interval(self, t, check_func): def _check_interval(self, t, check_func):
intervals = t.split(',') intervals = t.split(',')
@@ -1364,7 +1407,7 @@ class GeneralizedTime(Str):
raise ValidationError( raise ValidationError(
name=self.cli_name, errors='incomplete time value' name=self.cli_name, errors='incomplete time value'
) )
return None return None
def create_param(spec): def create_param(spec):

View File

@@ -524,7 +524,7 @@ class API(DictProxy):
if context is not None: if context is not None:
overrides['context'] = context overrides['context'] = context
self.bootstrap(**overrides) self.bootstrap(**overrides)
return args return (options, args)
def load_plugins(self): def load_plugins(self):
""" """

View File

@@ -28,12 +28,7 @@ from ipalib import api
from ipalib.backend import Backend from ipalib.backend import Backend
import krbV 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' ENCODING = 'UTF-8'
FS_ENCODING = (sys.getfilesystemencoding() or sys.getdefaultencoding())
class krb(Backend): class krb(Backend):
@@ -61,7 +56,7 @@ class krb(Backend):
""" """
Return the ``krbV.CCache`` for the ``ccname`` credential ccache. 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): def __get_principal(self, ccname):
""" """
@@ -78,7 +73,7 @@ class krb(Backend):
This cannot return anything meaningful if used in the server as a This cannot return anything meaningful if used in the server as a
request is processed. request is processed.
""" """
return self.__default_ccache().name.decode(FS_ENCODING) return self.__default_ccache().name
def default_principal(self): def default_principal(self):
""" """

View File

@@ -45,20 +45,21 @@ class env(LocalOrRemote):
keys.add(key) keys.add(key)
elif query in self.env: elif query in self.env:
keys.add(query) keys.add(query)
return sorted(keys) return keys
def execute(self, variables, **options): def execute(self, variables, **options):
if variables is None: if variables is None:
keys = self.env keys = self.env
else: else:
keys = self.__find_keys(variables) keys = self.__find_keys(variables)
return tuple( return dict(
(key, self.env[key]) for key in keys (key, self.env[key]) for key in keys
) )
def output_for_cli(self, textui, result, variables, **options): def output_for_cli(self, textui, result, variables, **options):
if len(result) == 0: if len(result) == 0:
return return
result = tuple((k, result[k]) for k in sorted(result))
if len(result) == 1: if len(result) == 1:
textui.print_keyval(result) textui.print_keyval(result)
return return

View File

@@ -70,15 +70,15 @@ class user(LDAPObject):
takes_params = ( takes_params = (
Str('givenname', Str('givenname',
cli_name='first', cli_name='first',
doc='first name', doc='First name',
), ),
Str('sn', Str('sn',
cli_name='last', cli_name='last',
doc='last name', doc='Last name',
), ),
Str('uid', Str('uid',
cli_name='user', cli_name='user',
doc='login name', doc='Login name',
primary_key=True, primary_key=True,
default_from=lambda givenname, sn: givenname[0] + sn, default_from=lambda givenname, sn: givenname[0] + sn,
normalizer=lambda value: value.lower(), normalizer=lambda value: value.lower(),
@@ -90,7 +90,7 @@ class user(LDAPObject):
), ),
Str('homedirectory?', Str('homedirectory?',
cli_name='homedir', cli_name='homedir',
doc='home directory', doc='Home directory',
default_from=lambda uid: '/home/%s' % uid, default_from=lambda uid: '/home/%s' % uid,
), ),
Str('loginshell?', Str('loginshell?',
@@ -251,4 +251,3 @@ class user_unlock(LDAPQuery):
textui.print_dashed('Unlocked user "%s".' % keys[-1]) textui.print_dashed('Unlocked user "%s".' % keys[-1])
api.register(user_unlock) api.register(user_unlock)

View File

@@ -25,5 +25,6 @@ XML-RPC client plugin.
from ipalib import api from ipalib import api
if 'in_server' in api.env and api.env.in_server is True: 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(xmlserver)
api.register(jsonserver)

View File

@@ -23,11 +23,25 @@ RPC server.
Also see the `ipalib.rpc` module. Also see the `ipalib.rpc` module.
""" """
from urlparse import parse_qs
from xmlrpclib import Fault from xmlrpclib import Fault
from ipalib.backend import Executioner 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.rpc import xml_dumps, xml_loads
from ipalib.util import make_repr 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): def params_2_args_options(params):
@@ -39,13 +53,116 @@ def params_2_args_options(params):
return (params, dict()) 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. Execution backend plugin for XML-RPC server.
Also see the `ipalib.rpc.xmlclient` plugin. Also see the `ipalib.rpc.xmlclient` plugin.
""" """
content_type = 'text/xml'
def finalize(self): def finalize(self):
self.__system = { self.__system = {
'system.listMethods': self.listMethods, 'system.listMethods': self.listMethods,
@@ -79,3 +196,75 @@ class xmlserver(Executioner):
self.info('response: %s: %s', e.__class__.__name__, str(e)) self.info('response: %s: %s', e.__class__.__name__, str(e))
response = Fault(e.errno, e.strerror) response = Fault(e.errno, e.strerror)
return xml_dumps(response, methodresponse=True) 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)

View File

@@ -17,8 +17,40 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
""" """
Package containing web-based UI components. IPA web UI.
""" """
import kid from controllers import JSON
kid.enable_import() 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

View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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.
"""

View File

@@ -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>

View File

@@ -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
View 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
View 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,
)

View File

@@ -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())

View File

@@ -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.')

View File

@@ -23,7 +23,7 @@
Python-level packaging using distutils. Python-level packaging using distutils.
""" """
from setuptools import setup from distutils.core import setup
import ipalib import ipalib
setup( setup(
@@ -38,11 +38,6 @@ setup(
'ipaserver.plugins', 'ipaserver.plugins',
'ipaserver.install', 'ipaserver.install',
'ipawebui', 'ipawebui',
'ipawebui.templates',
], ],
package_data={
'ipawebui.templates': ['*.kid'],
'ipawebui': ['static/*'],
},
scripts=['ipa'], scripts=['ipa'],
) )

View File

@@ -60,6 +60,27 @@ class test_DefaultFrom(ClassChecker):
e = raises(TypeError, self.cls, callback, 'givenname', 17) e = raises(TypeError, self.cls, callback, 'givenname', 17)
assert str(e) == TYPE_ERROR % ('keys', str, 17, int) 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): def test_call(self):
""" """
Test the `ipalib.parameters.DefaultFrom.__call__` method. Test the `ipalib.parameters.DefaultFrom.__call__` method.

View File

@@ -25,6 +25,7 @@ from tests.util import create_test_api, raises, PluginTester
from tests.data import unicode_str from tests.data import unicode_str
from ipalib import errors, Command from ipalib import errors, Command
from ipaserver import rpcserver from ipaserver import rpcserver
import json
def test_params_2_args_options(): def test_params_2_args_options():
@@ -50,3 +51,65 @@ class test_xmlserver(PluginTester):
def test_marshaled_dispatch(self): def test_marshaled_dispatch(self):
(o, api, home) = self.instance('Backend', in_server=True) (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)

View File

@@ -17,54 +17,8 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # 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 from wsgiref import util
from wsgiref.validate import validator
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