| Updated to use Pylons 0.9.7 and MoinMoin 1.8.4 |
| Includes references to PylonsHQ-located documentation. |
A Pylons controller with MoinMoin as a WSGI callable
One of the benefits of WSGI middleware apps is that their positioning in the call stack enables them to add data to the environment, to be used by upstream applications. One example of this is authentication / authorization: if you use an auth'n'auth middleware app or roll your own, you can set things up so that by the time your controller is called, any authentication / authorization data is readily available. This is useful if you have a need for site-wide authentication and authorization.
In this simple recipe we demonstrate the principle with MoinMoin ...
Introduction
This is a simple walk through of how to plug MoinMoin into a Pylons controller, demonstrating that in about 20 minutes you can have a full-featured wiki with access control driven by a WSGI middleware authentication and authorization tool set or a project-specific solution implemented in the project.
In essence it is a lightweight demonstration of the intriguingly powerful capabilities of WSGI (defined in PEP 333).
Requirements are Pylons 0.9.7 (or later) and MoinMoin 1.8.4.
The Process
The four-step sequence is:
- add a Pylons wiki controller, add a couple of routes and configure MoinMoin logging
- edit the error controller to prevent Pylons livery from leaking into MoinMoin's 404 pages
- install MoinMoin
- create a new wiki instance and configure it
WSGI makes this so easy that the bulk of the task is actually the setting up and configuring of a standard MoinMoin wiki instance.
There are a couple of deviations from the usual MoinMoin setup procedure (which I will detail) but other than that, it's basically a matter of following the standard process of wiki instance creation as described in the MoinMoin documentation.
Step 1 - create a wiki controller, add a couple of routes and configure MoinMoin logging
The first step is to create a controller which will hand off the request to MoinMoin, collect the resulting response and pass it back to paste.
Create a new file: myproject/controllers/wiki.py and add the following as its content (changing the three instances of myproject to your app's name):
controllers/wiki.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | # -*- coding: utf-8 -*- from MoinMoin.server.server_wsgi import moinmoinApp, WsgiConfig import StringIO, urllib import pylons from pylons import request from myproject.lib.base import * import myproject.lib.helpers as h # Need to add MoinMoin's share/data directory to the path # Change this to match wherever you choose to place this # directory. I put it in myproject/data for convenience. import sys sys.path.insert(0,'/path/to/myproject/data/share/moin/config') class Config(WsgiConfig): pass config = Config() # you MUST create an instance class WikiController(BaseController): def index(self): request._current_obj().headers['x-moin-location'] = '/wiki/' return moinmoinApp(request.environ, self.start_response) def page(self, *args, **kwargs): # Thanks to ianbicking for identifying both the necessity for and # the description of this workaround for reconstituting wsgi.input if request.environ.get('paste.parsed_formvars', False): wsgi_input = urllib.urlencode( request.environ['paste.parsed_formvars'][0], True) request.environ['wsgi.input'] = StringIO.StringIO(wsgi_input) request.environ['CONTENT_LENGTH'] = len(wsgi_input) # Uncomment this if you're using PrefixMiddleware # request.environ['SCRIPT_NAME'] = '' request.environ['PATH_INFO'] = request.environ['PATH_INFO'].__str__() request._current_obj().headers['x-moin-location'] = '/wiki/' return moinmoinApp(request.environ, self.start_response) |
(Ian Bicking observed that the workaround may well become redundant at some point in the future.)
Next, edit myproject/config/routing.py, adding the following couple of routes:
config/routing.py
1 2 | map.connect('wiki', '/wiki/', controller='wiki', action='index') map.connect('wikipage', '/wiki/*path_info', controller='wiki', action='page') |
Configure MoinMoin logging by simply copying the code below into the end of the _init_ function in myproject/lib/app_globals.py
lib/app_globals.py
1 2 3 | def __init__(self): import os os.environ['MOINLOGGINGCONF'] = '/path/to/myproject/development.ini' |
Step 2 - edit the error controller to prevent Pylons livery from leaking into MoinMoin's 404 pages
Wikis are a special case in which the usual '404 File not found' is not treated as an error but instead is interpreted as a request to create a new page. However, even though MoinMoin offers a 'Create a new page' form, the response headers show a 404 response which is picked up by Pylons and the error controller is called to render the expected 404 page. Here we just insert a guard condition to suppress the inclusion of the Pylons livery, otherwise the rendered MoinMoin 'Create new' page is framed in the standard black Pylons livery.
Edit myproject/controllers/error.py and make the following change:
controllers/error.py
BEFORE:
1 2 3 4 5 6 7 8 9 | def document(self): """Render the error document""" resp = request.environ.get('pylons.original_response') content = literal(resp.body) or cgi.escape(request.GET.get('message')) page = error_document_template % \ dict(prefix=request.environ.get('SCRIPT_NAME', ''), code=cgi.escape(request.GET.get('code', str(resp.status_int))), message=content) return page |
AFTER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def document(self): """Render the error document""" resp = request.environ.get('pylons.original_response') content = literal(resp.body) or cgi.escape(request.GET.get('message')) try: if resp.environ['wsgiorg.routing_args'][1]['action'] == 'page': return content except: pass page = error_document_template % \ dict(prefix=request.environ.get('SCRIPT_NAME', ''), code=cgi.escape(request.GET.get('code', str(resp.status_int))), message=content) return page |
That's it, you're done with hacking Pylons. Pylons doesn't need anything more in order to be able to run MoinMoin from a controller, all that remains is just normal MoinMoin set-up.
This is what I meant by the "intriguingly powerful capabilities of WSGI".
Step 3 - install MoinMoin
MoinMoin does not yet support easy_install eggs, so you need to visit the download page of the MoinMoin website, download the current version (1.8.1 at the time of writing) and expand it. From the expanded directory, you can either run python setup.py install or easy_install.
Briefly familiarize yourself with the basic setup and configuration of MoinMoin, especially wiki instance creation. For those in a terminal hurry: each wiki instance needs a copy of the *share directory* that was created when MoinMoin was installed.
The share directory is usually PREFIX/share/moin on U**x systems and it is where the templates are located, these are the directories and files in which we are interested:
/usr/share/moin/data wiki pages, users, cache, etc. /usr/share/moin/underlay wiki pages /usr/share/moin/config/wikiconfig.py example configuration file /usr/share/moin/htdocs html support
Step 4: create and configure the wiki instance
In your project space, at the same level as models, create a new directory called parts and inside that, create another directory called wiki (or, if you already have a system for handling extra libs and stuff, just adapt as appropriate for your scheme). For my own convenience, I put the instance-specific MoinMoin code in myproject/data, so I shall use that approach for the example.
Copy files and directories from the moin share directory so that your parts and wiki directories looks like this:
myproject/data/share/moin/data/ myproject/data/share/moin/underlay/ myproject/data/share/moin/config/wikiconfig.py
Copy the htdocs directory from the MoinMoin share directory into your app's public directory, changing its name from htdocs to {{moin_static181}}e.g.:
cp -R /usr/share/moin/htdocs myproject/public/moin_static181
The MoinMoin help docs are the authoritative source for configuration details, so I'll just describe the changes to myproject/part/wikiconfig.py which are specific to running the instance as part of a Pylons app.
Here's a diff that shows the basic changes you need to make to the standard wikiconfig.py template:
data/share/moin/config/wikiconfig.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | --- wikiconfig.py +++ wikiconfig.new.py @@ -25,11 +25,55 @@ from the wikifarm directory instead! ** """ +from MoinMoin.auth import BaseAuth + +class PylonsAuth(BaseAuth): + """ handle login from moin login form """ + def __init__(self, verbose=False): + BaseAuth.__init__(self) + self.verbose = verbose + + name = 'pylons_auth' + logout_possible = False + + def request(self, req, user_obj, **kw): + + user = None + try_next = True # if True, moin tries the next auth method + + # What it should be, when auth is configured + # auth_username = req.auth_username + + # What you can use to get started ... + auth_username = "YourName" + + from MoinMoin.user import User + # giving auth_username to User constructor + # means that authentication has already been done. + user = User(req, name=auth_username, + auth_username=auth_username, + auth_method='pylons_auth') + changed = False + if user: + user.create_or_update(changed) + if user and user.valid: # did we succeed making up a valid user? + try_next = False # stop processing auth method list + return user, try_next + from MoinMoin.config.multiconfig import DefaultConfig class Config(DefaultConfig): + # # More authentication options + # from MoinMoin.auth import moin_cookie, http + # # first try the external_cookie, then http basic auth, then + # # the usual moin_cookie + # auth = [external_cookie, http, moin_cookie] + + # Actually, just use our own auth scheme ... + auth = [PylonsAuth()] + # Wiki identity ---------------------------------------------------- # Site name, used by default for wiki name-logo [Unicode] @@ -65,14 +109,14 @@ # Where your mutable wiki pages are. You want to make regular # backups of this directory. - data_dir = './data/' + data_dir = '/path/to/project/data/share/moin/data/' # Where read-only system and help page are. You might want to share # this directory between several wikis. When you update MoinMoin, # you can safely replace the underlay directory with a new one. This # directory is part of MoinMoin distribution, you don't have to # backup it. - data_underlay_dir = './underlay/' + data_underlay_dir = '/path/to/project/data/share/moin/underlay/' # The URL prefix we use to access the static stuff (img, css, js). # NOT touching this is maybe the best way to handle this setting as moin |
(If you choose to use a directory name other than moin_static181 for the static resources then you will need to update the URL prefix in wikiconfig.py to match – but as the rubric states: "NOT touching this is maybe the best way" ... so, be told.)
In summary: add an PylonsAuth class, this will allow MoinMoin to pick up any authorization data added by middleware, the BaseController or the WikiController.
| Note: MoinMoin has its own independent file-based mechanism for storing and maintaining user details and preferences, a close integration of any Pylons-based auth'n'auth and MoinMoin user data will require additional work. |
Change the values of logo_string, data_dir, data_underlay_dir (& url_prefix if required) to suit.
A small amendment to the MoinMoin code is required to enable attachments to be received by adjusting the behaviour of MoinMoin's BaseRequest class so that it handles the MultiDict generated by paste.parsed_formvars. The target method is _setup_args_from_cgi_form and the changes are listed here:
MoinMoin/request/_init_.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | --- MoinMoin/request/__init__.py +++ MoinMoin/request/__init__.new.py @@ -1085,8 +1085,9 @@ @rtype: dict @return: dict with form keys, each contains a list of values """ + form = self.env['paste.parsed_formvars'][0] args = {} - for key in form: + for key in form.keys(): values = form[key] if not isinstance(values, list): values = [values] @@ -1097,7 +1098,7 @@ # Save upload file name in a separate key args[key + '__filename__'] = item.filename else: - fixedResult.append(item.value) + fixedResult.append(item) args[key] = fixedResult return self.decodeArgs(args) |
This completes the Pylons-specific additions to the MoinMoin config and changes to the MoinMoin code. You should take the opportunity to set up the remaining half-dozen standard configuration values (e.g. sitename, acl_rights, etc.) whilst following the advice in the MoinMoin docs but ... if you'd rather see it working and then tweak the config, what we have so far will be sufficient to get MoinMoin up and running.
Check the results
Browse to [http://localhost:5000/wiki/] and start using your 20-minute wiki.
I hope this has demonstrated some of the capabilities of WSGI and has been enough to get you started.
Comments (4)
Jun 20, 2007
alain says:
Hi graham i tried your setup i works well except the appauth func, no 'get' avai...Hi graham
i tried your setup
i works well
except the appauth func, no 'get' available in moin request
i think i will try to write my own auth handlers on moin side
and attachments / files upload dont work at all
no error message nothing, it just redirects to the previous page
any idea about this one? does it work for you?
thanx
alain
Jan 13, 2009
Graham Higgins says:
++ attachments / files upload dont work at all It transpired (after much furckl...++ attachments / files upload dont work at all
It transpired (after much furckling about in the innards of MoinMoin) than MoinMoin expects the uploaded file / form to be stored in a FieldStorage class but Paste passes in a MultiDict instead, so it is necessary to make a couple of changes to one of the methods of MoinMoin's BaseRequest class.
I've stripped out all the references to Authkit, the choice of auth'n'authn is completely free and will happily work to the schemes suggested by Peter (below).
Jun 13, 2009
Graham Higgins says:
Update to this. Uploading attachments works straight out of the box with MoinMoi...Update to this. Uploading attachments works straight out of the box with MoinMoin 1.8.4 and WebOb 0.9.7dev-r7935. The Pylons controller code (above) has been updated to reflect the recommended MoinMoin 1.8.4 approach.
Nov 29, 2008
Peter says:
Hi alain, I realise this thread is a bit stale but the 'request' object is a Re...Hi alain,
I realise this thread is a bit stale but the 'request' object is a RequestWSGI instance and does not support the 'get()' method in the way shown by the above code. You probably want to use:
{{
}}
However if your WikiController is already using authkit to authenticate every method, you can assume the User has been authenticated by the time the appauth() function is called.
Any context you need from Pylons can be accessed by:
{{
}}