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
graft tests/
graft ipawebui/static/
include ipawebui/templates/*.kid
include LICENSE TODO lite-server.py
include tests/*/*.py

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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):
"""

View File

@@ -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):
"""

View File

@@ -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):
"""

View File

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

View File

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

View File

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

View File

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

View File

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

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.
"""
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'],
)

View File

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

View File

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

View File

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