mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Consolidate to single WSGI entry point
This commit is contained in:
@@ -11,14 +11,6 @@ PythonImport ipaserver main_interpreter
|
||||
# This is required so the auto-configuration works with Firefox 2+
|
||||
AddType application/java-archive jar
|
||||
|
||||
# This is where we redirect on failed auth
|
||||
Alias /ipa/errors "/usr/share/ipa/html"
|
||||
|
||||
# For the MIT Windows config files
|
||||
Alias /ipa/config "/usr/share/ipa/html"
|
||||
|
||||
# For CRL publishing
|
||||
Alias /ipa/crl "/var/lib/pki-ca/publish"
|
||||
|
||||
|
||||
<Location "/ipa">
|
||||
@@ -32,34 +24,42 @@ Alias /ipa/crl "/var/lib/pki-ca/publish"
|
||||
KrbSaveCredentials on
|
||||
Require valid-user
|
||||
ErrorDocument 401 /ipa/errors/unauthorized.html
|
||||
</Location>
|
||||
|
||||
<Location "/ipa/xml">
|
||||
SetHandler python-program
|
||||
PythonInterpreter main_interpreter
|
||||
PythonHandler ipaserver::xmlrpc
|
||||
PythonHandler ipaserver::handler
|
||||
PythonDebug Off
|
||||
PythonOption SCRIPT_NAME /ipa/xml
|
||||
PythonOption SCRIPT_NAME /ipa
|
||||
PythonAutoReload Off
|
||||
|
||||
</Location>
|
||||
|
||||
<Location "/ipa/json">
|
||||
SetHandler python-program
|
||||
PythonInterpreter main_interpreter
|
||||
PythonHandler ipaserver::jsonrpc
|
||||
PythonDebug Off
|
||||
PythonOption SCRIPT_NAME /ipa/json
|
||||
PythonAutoReload Off
|
||||
</Location>
|
||||
#<Location "/ipa/xml">
|
||||
# SetHandler python-program
|
||||
# PythonInterpreter main_interpreter
|
||||
# PythonHandler ipaserver::xmlrpc
|
||||
# PythonDebug Off
|
||||
# PythonOption SCRIPT_NAME /ipa/xml
|
||||
# PythonAutoReload Off
|
||||
#</Location>
|
||||
|
||||
<Location "/ipa/ui">
|
||||
SetHandler python-program
|
||||
PythonInterpreter main_interpreter
|
||||
PythonHandler ipaserver::webui
|
||||
PythonDebug Off
|
||||
PythonOption SCRIPT_NAME /ipa/ui
|
||||
PythonAutoReload Off
|
||||
</Location>
|
||||
#<Location "/ipa/json">
|
||||
# SetHandler python-program
|
||||
# PythonInterpreter main_interpreter
|
||||
# PythonHandler ipaserver::jsonrpc
|
||||
# PythonDebug Off
|
||||
# PythonOption SCRIPT_NAME /ipa/json
|
||||
# PythonAutoReload Off
|
||||
#</Location>
|
||||
|
||||
#<Location "/ipa/ui">
|
||||
# SetHandler python-program
|
||||
# PythonInterpreter main_interpreter
|
||||
# PythonHandler ipaserver::webui
|
||||
# PythonDebug Off
|
||||
# PythonOption SCRIPT_NAME /ipa/ui
|
||||
# PythonAutoReload Off
|
||||
#</Location>
|
||||
|
||||
Alias /ipa-assets/ "/var/cache/ipa/assets/"
|
||||
<Directory "/var/cache/ipa/assets">
|
||||
@@ -72,14 +72,39 @@ Alias /ipa-assets/ "/var/cache/ipa/assets/"
|
||||
</Directory>
|
||||
|
||||
|
||||
<Location "/ipa/errors">
|
||||
SetHandler None
|
||||
</Location>
|
||||
|
||||
<Location "/ipa/config">
|
||||
SetHandler None
|
||||
</Location>
|
||||
|
||||
<Location "/ipa/crl">
|
||||
SetHandler None
|
||||
</Location>
|
||||
|
||||
|
||||
# This is where we redirect on failed auth
|
||||
Alias /ipa/errors "/usr/share/ipa/html"
|
||||
|
||||
# For the MIT Windows config files
|
||||
Alias /ipa/config "/usr/share/ipa/html"
|
||||
|
||||
# Do no authentication on the directory that contains error messages
|
||||
<Directory "/usr/share/ipa/html">
|
||||
SetHandler None
|
||||
AllowOverride None
|
||||
Satisfy Any
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
|
||||
# For CRL publishing
|
||||
Alias /ipa/crl "/var/lib/pki-ca/publish"
|
||||
|
||||
<Directory "/var/lib/pki-ca/publish">
|
||||
SetHandler None
|
||||
AllowOverride None
|
||||
Options Indexes FollowSymLinks
|
||||
Satisfy Any
|
||||
|
@@ -108,7 +108,7 @@ DEFAULT_CONFIG = (
|
||||
('mount_ipa', '/ipa/'),
|
||||
('mount_xmlserver', 'xml'),
|
||||
('mount_jsonserver', 'json'),
|
||||
('mount_webui', 'ui/'),
|
||||
('mount_webui', 'ui'),
|
||||
('mount_webui_assets', '/ipa-assets/'),
|
||||
|
||||
# WebUI stuff:
|
||||
|
@@ -222,3 +222,7 @@ def webui(req):
|
||||
mod_python handler for web-UI requests (place holder).
|
||||
"""
|
||||
return adapter(req, ui)
|
||||
|
||||
|
||||
def handler(req):
|
||||
return adapter(req, api.Backend.session)
|
||||
|
@@ -19,17 +19,13 @@
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
XML-RPC client plugin.
|
||||
Loads WSGI server plugins.
|
||||
"""
|
||||
|
||||
from ipalib import api
|
||||
|
||||
if 'in_server' in api.env and api.env.in_server is True:
|
||||
from ipaserver.rpcserver import xmlserver, jsonserver
|
||||
from ipalib.backend import Executioner
|
||||
from ipaserver.rpcserver import session, xmlserver, jsonserver
|
||||
api.register(session)
|
||||
api.register(xmlserver)
|
||||
api.register(jsonserver)
|
||||
|
||||
class session(Executioner):
|
||||
pass
|
||||
api.register(session)
|
||||
|
@@ -24,6 +24,7 @@ Also see the `ipalib.rpc` module.
|
||||
"""
|
||||
|
||||
from cgi import parse_qs
|
||||
from xml.sax.saxutils import escape
|
||||
from xmlrpclib import Fault
|
||||
from ipalib.backend import Executioner
|
||||
from ipalib.errors import PublicError, InternalError, CommandError, JSONError
|
||||
@@ -31,6 +32,33 @@ from ipalib.request import context, Connection, destroy_context
|
||||
from ipalib.rpc import xml_dumps, xml_loads
|
||||
from ipalib.util import make_repr
|
||||
from ipalib.compat import json
|
||||
from wsgiref.util import shift_path_info
|
||||
|
||||
|
||||
_not_found_template = """<html>
|
||||
<head>
|
||||
<title>404 Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
The requested URL <strong>%(url)s</strong> was not found on this server.
|
||||
</p>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def not_found(environ, start_response):
|
||||
"""
|
||||
Return a 404 Not Found error.
|
||||
"""
|
||||
status = '404 Not Found'
|
||||
response_headers = [('Content-Type', 'text/html')]
|
||||
start_response(status, response_headers)
|
||||
output = _not_found_template % dict(
|
||||
url=escape(environ['SCRIPT_NAME'] + environ['PATH_INFO'])
|
||||
)
|
||||
return [output]
|
||||
|
||||
|
||||
def read_input(environ):
|
||||
@@ -85,17 +113,81 @@ def extract_query(environ):
|
||||
return query
|
||||
|
||||
|
||||
class session(Executioner):
|
||||
"""
|
||||
WSGI routing middleware and entry point into IPA server.
|
||||
|
||||
The `session` plugin is the entry point into the IPA server. It will create
|
||||
an LDAP connection (from a session cookie or the KRB5CCNAME header) and then
|
||||
dispatch the request to the appropriate application. In WSGI parlance,
|
||||
`session` is *middleware*.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(session, self).__init__()
|
||||
self.__apps = {}
|
||||
|
||||
def __iter__(self):
|
||||
for key in sorted(self.__apps):
|
||||
yield key
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__apps[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__apps
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
try:
|
||||
self.create_context(ccache=environ.get('KRB5CCNAME'))
|
||||
return self.route(environ, start_response)
|
||||
finally:
|
||||
destroy_context()
|
||||
|
||||
def finalize(self):
|
||||
self.url = self.env['mount_ipa']
|
||||
super(session, self).finalize()
|
||||
|
||||
def route(self, environ, start_response):
|
||||
key = shift_path_info(environ)
|
||||
if key in self.__apps:
|
||||
app = self.__apps[key]
|
||||
return app(environ, start_response)
|
||||
return not_found(environ, start_response)
|
||||
|
||||
def mount(self, app, key):
|
||||
"""
|
||||
Mount the WSGI application *app* at *key*.
|
||||
"""
|
||||
# if self.__islocked__():
|
||||
# raise StandardError('%s.mount(): locked, cannot mount %r at %r' % (
|
||||
# self.name, app, key)
|
||||
# )
|
||||
if key in self.__apps:
|
||||
raise StandardError('%s.mount(): cannot replace %r with %r at %r' % (
|
||||
self.name, self.__apps[key], app, key)
|
||||
)
|
||||
self.info('Mounting %r at %r', app, key)
|
||||
self.__apps[key] = app
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class WSGIExecutioner(Executioner):
|
||||
"""
|
||||
Base class for execution backends with a WSGI application interface.
|
||||
"""
|
||||
|
||||
key = ''
|
||||
|
||||
def set_api(self, api):
|
||||
super(WSGIExecutioner, self).set_api(api)
|
||||
if 'session' in self.api.Backend:
|
||||
self.api.Backend.session.mount(self, self.key)
|
||||
|
||||
def finalize(self):
|
||||
url = self.env['mount_' + self.name]
|
||||
if url.startswith('/'):
|
||||
self.url = url
|
||||
else:
|
||||
self.url = self.env.mount_ipa + url
|
||||
self.url = self.env.mount_ipa + self.key
|
||||
super(WSGIExecutioner, self).finalize()
|
||||
|
||||
def wsgi_execute(self, environ):
|
||||
@@ -103,28 +195,24 @@ class WSGIExecutioner(Executioner):
|
||||
error = None
|
||||
_id = None
|
||||
try:
|
||||
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()
|
||||
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()
|
||||
return self.marshal(result, error, _id)
|
||||
|
||||
def simple_unmarshal(self, environ):
|
||||
@@ -155,11 +243,6 @@ class WSGIExecutioner(Executioner):
|
||||
raise NotImplementedError('%s.marshal()' % self.fullname)
|
||||
|
||||
|
||||
|
||||
class session(Executioner):
|
||||
pass
|
||||
|
||||
|
||||
class xmlserver(WSGIExecutioner):
|
||||
"""
|
||||
Execution backend plugin for XML-RPC server.
|
||||
@@ -168,6 +251,7 @@ class xmlserver(WSGIExecutioner):
|
||||
"""
|
||||
|
||||
content_type = 'text/xml'
|
||||
key = 'xml'
|
||||
|
||||
def finalize(self):
|
||||
self.__system = {
|
||||
@@ -226,6 +310,7 @@ class jsonserver(WSGIExecutioner):
|
||||
"""
|
||||
|
||||
content_type = 'application/json'
|
||||
key = 'json'
|
||||
|
||||
def marshal(self, result, error, _id=None):
|
||||
if error:
|
||||
|
@@ -47,7 +47,6 @@ def join_url(base, url):
|
||||
class WebUI(Application):
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
self.session = api.Backend.session
|
||||
baseurl = api.env.mount_ipa
|
||||
assets = Assets(
|
||||
url=join_url(baseurl, api.env.mount_webui_assets),
|
||||
@@ -60,16 +59,8 @@ class WebUI(Application):
|
||||
widgets=create_widgets(),
|
||||
prod=api.env.webui_prod,
|
||||
)
|
||||
self.api.Backend.session.mount(self, api.env.mount_webui)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
self.session.create_context(ccache=environ.get('KRB5CCNAME'))
|
||||
try:
|
||||
query = extract_query(environ)
|
||||
print query
|
||||
response = super(WebUI, self).__call__(environ, start_response)
|
||||
finally:
|
||||
destroy_context()
|
||||
return response
|
||||
|
||||
|
||||
def create_wsgi_app(api):
|
||||
|
@@ -86,13 +86,11 @@ if __name__ == '__main__':
|
||||
|
||||
urlmap = URLMap()
|
||||
apps = [
|
||||
('XML RPC', api.Backend.xmlserver),
|
||||
('JSON RPC', api.Backend.jsonserver),
|
||||
('IPA', KRBCheater(api.Backend.session)),
|
||||
('Assets', AssetsApp(ui.assets)),
|
||||
('Web UI', ui),
|
||||
]
|
||||
for (name, app) in apps:
|
||||
urlmap[app.url] = KRBCheater(app)
|
||||
urlmap[app.url] = app
|
||||
api.log.info('Mounting %s at %s', name, app.url)
|
||||
|
||||
if path.isfile(api.env.lite_pem):
|
||||
|
@@ -21,13 +21,56 @@
|
||||
Test the `ipaserver.rpc` module.
|
||||
"""
|
||||
|
||||
from tests.util import create_test_api, raises, PluginTester
|
||||
from tests.util import create_test_api, assert_equal, raises, PluginTester
|
||||
from tests.data import unicode_str
|
||||
from ipalib import errors, Command
|
||||
from ipaserver import rpcserver
|
||||
from ipalib.compat import json
|
||||
|
||||
|
||||
class StartResponse(object):
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.status = None
|
||||
self.headers = None
|
||||
|
||||
def __call__(self, status, headers):
|
||||
assert self.status is None
|
||||
assert self.headers is None
|
||||
assert isinstance(status, str)
|
||||
assert isinstance(headers, list)
|
||||
self.status = status
|
||||
self.headers = headers
|
||||
|
||||
|
||||
def test_not_found():
|
||||
f = rpcserver.not_found
|
||||
t = rpcserver._not_found_template
|
||||
s = StartResponse()
|
||||
|
||||
# Test with an innocent URL:
|
||||
d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/foo/stuff')
|
||||
assert_equal(
|
||||
f(d, s),
|
||||
[t % dict(url='/ipa/foo/stuff')]
|
||||
)
|
||||
assert s.status == '404 Not Found'
|
||||
assert s.headers == [('Content-Type', 'text/html')]
|
||||
|
||||
# Test when URL contains any of '<>&'
|
||||
s.reset()
|
||||
d = dict(SCRIPT_NAME=' ', PATH_INFO='<script>do_bad_stuff();</script>')
|
||||
assert_equal(
|
||||
f(d, s),
|
||||
[t % dict(url='&nbsp;<script>do_bad_stuff();</script>')]
|
||||
)
|
||||
assert s.status == '404 Not Found'
|
||||
assert s.headers == [('Content-Type', 'text/html')]
|
||||
|
||||
|
||||
|
||||
def test_params_2_args_options():
|
||||
"""
|
||||
Test the `ipaserver.rpcserver.params_2_args_options` function.
|
||||
@@ -42,6 +85,57 @@ def test_params_2_args_options():
|
||||
assert f((options,) + args) == ((options,) + args, dict())
|
||||
|
||||
|
||||
class test_session(object):
|
||||
klass = rpcserver.session
|
||||
|
||||
def test_route(self):
|
||||
def app1(environ, start_response):
|
||||
return (
|
||||
'from 1',
|
||||
[environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')]
|
||||
)
|
||||
|
||||
def app2(environ, start_response):
|
||||
return (
|
||||
'from 2',
|
||||
[environ[k] for k in ('SCRIPT_NAME', 'PATH_INFO')]
|
||||
)
|
||||
|
||||
inst = self.klass()
|
||||
inst.mount(app1, 'foo')
|
||||
inst.mount(app2, 'bar')
|
||||
|
||||
d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/foo/stuff')
|
||||
assert inst.route(d, None) == ('from 1', ['/ipa/foo', '/stuff'])
|
||||
|
||||
d = dict(SCRIPT_NAME='/ipa', PATH_INFO='/bar')
|
||||
assert inst.route(d, None) == ('from 2', ['/ipa/bar', ''])
|
||||
|
||||
def test_mount(self):
|
||||
def app1(environ, start_response):
|
||||
pass
|
||||
|
||||
def app2(environ, start_response):
|
||||
pass
|
||||
|
||||
# Test that mount works:
|
||||
inst = self.klass()
|
||||
inst.mount(app1, 'foo')
|
||||
assert inst['foo'] is app1
|
||||
assert list(inst) == ['foo']
|
||||
|
||||
# Test that StandardError is raise if trying override a mount:
|
||||
e = raises(StandardError, inst.mount, app2, 'foo')
|
||||
assert str(e) == '%s.mount(): cannot replace %r with %r at %r' % (
|
||||
'session', app1, app2, 'foo'
|
||||
)
|
||||
|
||||
# Test mounting a second app:
|
||||
inst.mount(app2, 'bar')
|
||||
assert inst['bar'] is app2
|
||||
assert list(inst) == ['bar', 'foo']
|
||||
|
||||
|
||||
class test_xmlserver(PluginTester):
|
||||
"""
|
||||
Test the `ipaserver.rpcserver.xmlserver` plugin.
|
||||
|
Reference in New Issue
Block a user