Finish mono account management for biblio, improve mapping management

and fix model

 * mandaye/__init__.py: load sqlachemy session
 * mandaye/auth/authform.py: add account management
 * mandaye/auth/vincennes.py: add account management
 * mandaye/configs/biblio_vincennes.py: add vincennes conf
 * mandaye/dispatcher.py; improve mapping management
This commit is contained in:
Jérôme Schneider 2011-09-16 19:13:17 +02:00
parent 7285c43a26
commit e6fc815ff5
14 changed files with 327 additions and 147 deletions

View File

@ -1 +1,15 @@
VERSION=0.1
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from mandaye.config import db_url
# an Engine, which the Session will use for connection
# resources
sql_engine = create_engine(db_url)
# create a configured "Session" class
Session = sessionmaker(bind=sql_engine)
# create a Session
sql_session = Session()

View File

@ -1,4 +1,3 @@
import ipdb
"""
Dispatcher for basic auth form authentifications
"""
@ -9,7 +8,10 @@ import urllib
from urlparse import parse_qs
from mandaye.http import HTTPResponse
import mandaye
from mandaye.models import Site, ExtUser, LocalUser
from mandaye.http import HTTPResponse, HTTPHeader
from mandaye.response import _500, _302, _401
from mandaye.response import template_response
@ -47,42 +49,71 @@ class AuthForm(object):
"""
return template_response(values['template'], values)
def associate_submit(self, env, values, request, condition=None):
def associate_submit(self, env, values, condition, request, response):
""" Associate your login / password with Mandaye
"""
"""
session = env['beaker.session']
pseudo = env['beaker.session'].get('pseudo')
if request.msg:
post = parse_qs(request.msg.read())
response = self._replay(env, post['username'][0], post['password'][0])
replay_response = self._replay(env, post['username'][0], post['password'][0])
if eval(condition):
if not session.has_key('pseudo'):
# Find a way to put this in the configuration
return _302("/mandaye/connection")
if not pseudo:
return _302(values.get('connection_url'))
else:
# TODO associate this login / password in the db
pass
return template_response(values['template'], values)
site = mandaye.sql_session.query(Site).\
filter_by(name=values.get('site_name')).first()
if not site:
site = Site(values.get('site_name'))
mandaye.sql_session.add(site)
local_user = mandaye.sql_session.query(LocalUser).\
filter_by(token=pseudo).first()
if not local_user:
local_user = LocalUser(token=pseudo)
mandaye.sql_session.add(local_user)
ext_user = mandaye.sql_session.query(ExtUser).join(LocalUser).\
filter(LocalUser.token==pseudo).first()
if not ext_user:
ext_user = ExtUser()
mandaye.sql_session.add(ext_user)
ext_user.login = post['username'][0]
ext_user.password = post['password'][0]
ext_user.local_user = local_user
mandaye.sql_session.commit()
return _302(values.get('connection_url'))
return response
def check_auth(self, env, values, request=None):
""" values: dict witth username, login
"""
pass
def login(self, env, values, request):
def login(self, env, values, condition, request):
""" Automatic login on a site with a form
"""
login = self.local_auth.get_current_login(env)
if not login:
return _401('Access denied: invalid token')
#if not values.has_key('form_action') \
# or not values.has_key('form_values') \
# or not values.has_key('username_field') \
# or not values.has_key('password_field'):
# return _500(env['PATH_INFO'],
# 'Invalid values for AuthFormDispatcher.login')
# TODO: Test get the username / password here
#response = self._replay(env,
#return _302(location, cookies)
ext_user = mandaye.sql_session.query(ExtUser).\
join(LocalUser).\
filter(LocalUser.token==login).\
first()
if not ext_user:
return _302(values.get('associate_url'))
if not ext_user.login or not ext_user.password:
return _500(env['PATH_INFO'],
'Invalid values for AuthFormDispatcher.login')
response = self._replay(env, ext_user.login, ext_user.password)
headers = HTTPHeader()
headers.load_from_list(response.getheaders())
cookies = Cookie.BaseCookie(response.msg.getheader('set-cookie'))
response = HTTPResponse(response.status, response.reason, headers,
response.read(), cookies)
if condition and eval(condition):
return response
else:
# TODO; find a better solution
return _302(values.get('associate_url'))
def connection(self, values, response):
pass

View File

@ -6,7 +6,7 @@ import traceback
from Crypto.Cipher import AES
from mandaye.dispatchers.auth import AuthFormDispatcher
#from mandaye.dispatchers.auth import AuthFormDispatcher
from mandaye.response import _502, _302
class VincennesAuth(object):
@ -44,7 +44,6 @@ class VincennesAuth(object):
session = env['beaker.session']
session['pseudo'] = info['pseudo']
session.save()
print info['pseudo']
return info['pseudo']
except Exception, e:
if config.debug:
@ -55,8 +54,10 @@ class VincennesAuth(object):
def connection(self, env, values, request):
""" Connection to the compte citoyen
"""
dest_url = "%s://%s%s" % (env['wsgi.url_scheme'], env['HTTP_HOST'],
values.get('next_url'))
location = "%s?next_url=%s&service=%s" % \
(self.url, values.get('next_url'), values.get('service_name'))
(self.url, dest_url, values.get('service_name'))
return _302(location)

View File

View File

@ -0,0 +1,78 @@
from mandaye.filters.vincennes import biblio_html, biblio_html_login_page
from mandaye.filters.vincennes import biblio_associate_login, biblio_associate_req
from mandaye.filters.default import MandayeFilter
from mandaye.configs.vincennes import auth_vincennes, authform
biblio_mapping = [
{
'path': r'/',
'on_response': [{'filter': biblio_html}]
},
{
'path': r'/mandaye/login$',
'method': 'GET',
'response': {
'filter': authform.login,
'values': { 'associate_url': '/mandaye/associate' },
'condition': 'response.code==302',
},
},
{
'path': r'/mandaye/associate$',
'method': 'GET',
'target': 'http://biblio.vincennes.fr/sezhame/page/connexion-abonne',
'on_response': [{
'filter': biblio_associate_login,
'values': {
'action': '/mandaye/associate',
'template': 'biblio_associate.html',
},
}]
},
{
'path': r'/mandaye/associate$',
'method': 'POST',
'target': 'http://biblio.vincennes.fr/sezhame/page/connexion-abonne',
'on_request': [{'filter': biblio_associate_req}],
'on_response': [
{
'filter': authform.associate_submit,
'values': {
'site_name': 'biblio',
'connection_url': '/mandaye/connection'
},
'condition': "replay_response.status==302"
},
{
'filter': biblio_associate_login,
'values': {
'action': '/mandaye/associate',
'template': 'biblio_associate.html',
'error_msg': "Mauvais numéro ou mot de passe",
},
},
]
},
{
'path': r'/mandaye/connection$',
'method': 'GET',
'response': {
'filter': auth_vincennes.connection,
'values': {
'destination': 'https://test.vincennes.fr/comptecitoyen/auth',
'next_url': '/mandaye/login',
'service_name': 'biblio'
}
}
},
{
'path': r'/sezhame/page/connexion-abonne$',
'method': 'GET',
'on_response': [{'filter': biblio_html_login_page}]
},
]

View File

@ -0,0 +1,16 @@
from mandaye.auth.authform import AuthForm
from mandaye.auth.vincennes import VincennesAuth
form_values = {
'form_action': '/sezhame/page/connexion-abonne',
'from_headers': { 'Content-Type': 'application/x-www-form-urlencoded' },
'form_values': { 'user': '',
'password': '',
'op': 'Se connecter',
'form_id': 'dk_opac15_login_form' },
'username_field': 'user',
'password_field': 'password'
}
auth_vincennes = VincennesAuth('https://test.vincennes.fr/comptecitoyen/auth')
authform = AuthForm(auth_vincennes, **form_values)

View File

@ -27,11 +27,10 @@ class Dispatcher(object):
""" parse the mapping on every request
"""
req_mapping = {
'on_request_hook': None,
'on_request_filters': [],
'on_response_hook': None,
'on_response_filters': [],
'on_request': [],
'on_response': [],
'response': None,
'target': None
}
for conf in mapping:
@ -47,12 +46,17 @@ class Dispatcher(object):
req_mapping[hookname] = conf[hookname]
return req_mapping
def _call_dispatcher_hook(self, hook, *args):
if hook and hook.has_key('method'):
def _call_hook(self, hook, *args):
if hook and hook.has_key('filter'):
values = hook.get('values')
if not values:
values = dict()
return hook['method'](self.env, values, *args)
if hook.has_key('condition'):
return hook['filter'](self.env, values, hook['condition'], *args)
else:
return hook['filter'](self.env, values, *args)
else:
logging.warning("%s hook failed (no filter option)" % self.env['PATH_INFO'])
return None
def get_target_url(self):
@ -61,8 +65,10 @@ class Dispatcher(object):
# Disable reverse proxy if we have a static response
if self.req_mapping['response']:
return None
# TODO: manage redirect here
url = self.target.geturl() + self.env['PATH_INFO']
if self.req_mapping['target']:
url = self.req_mapping['target']
else:
url = self.target.geturl() + self.env['PATH_INFO']
if self.env['QUERY_STRING']:
return url + "?" + self.env['QUERY_STRING']
else:
@ -71,18 +77,12 @@ class Dispatcher(object):
def get_response(self, request):
""" Called if you have a response hook for this request
"""
if self.req_mapping['response'].has_key('condition'):
response = self._call_dispatcher_hook(self.req_mapping['response'],
request, self.req_mapping['response']['condition'])
else:
response = self._call_dispatcher_hook(self.req_mapping['response'],
response = self._call_hook(self.req_mapping['response'],
request)
if not response:
return _500(self.env["PATH_INFO"], "The response hook failed")
# loading defaults filters
response.headers = self.filter.response_headers(response.headers)
response.cookies = self.filter.response_cookies(response.cookies)
response.msg = self.filter.response_msg(response.msg)
response = self.filter.on_response(response)
return response
def mod_request(self, request):
@ -90,23 +90,16 @@ class Dispatcher(object):
request: MandayeRequest object with cookies and headers
Return the request object """
# Loading defaults filters
request.headers = self.filter.request_headers(request.headers)
request.cookies = self.filter.request_cookies(request.cookies)
request.msg = self.filter.request_msg(request.msg)
# Loading specific filters
for filter in self.req_mapping['on_request_filters']:
if filter['type'] == "data":
request.msg = filter['filter'](request.msg, self.env)
elif filter['type'] == "cookies":
request.cookies = filter['filter'](request.cookies, self.env)
elif filter['type'] == "headers":
request.headers = filter['filter'](request.headers, self.env)
request = self.filter.on_request(request)
# Calling hook function
if self.req_mapping['on_request_hook']:
return self._call_dispatcher_hook(
self.req_mapping['on_request_hook'], request)
for hook in self.req_mapping['on_request']:
new_request = self._call_hook(hook, request)
if new_request:
request = new_request
else:
logging.warning("%s On_request hook %s failed" % (self.env['PATH_INFO'],
self.req_mapping['on_request']['filter']))
return request
def mod_response(self, request, response):
@ -116,23 +109,16 @@ class Dispatcher(object):
you can modify the cookies and the HTTP headers """
# loading defaults filters
response.headers = self.filter.response_headers(response.headers)
response.cookies = self.filter.response_cookies(response.cookies)
response.msg = self.filter.response_msg(response.msg)
# Loading specific filters
for filter in self.req_mapping['on_response_filters']:
if filter['type'] == "data":
response.msg = filter['filter'](response.msg, self.env)
elif filter['type'] == "cookies":
response.cookies = filter['filter'](response.cookies, self.env)
elif filter['type'] == "headers":
response.headers = filter['filter'](response.headers, self.env)
response = self.filter.on_response(response)
# Calling hook function
if self.req_mapping['on_response_hook']:
return self._call_dispatcher_hook(
self.req_mapping['on_response_hook'], request, response)
for hook in self.req_mapping['on_response']:
new_response = self._call_hook(hook, request, response)
if new_response:
response = new_response
else:
logging.warning("%s On_response hook %s failed" % (self.env['PATH_INFO'],
self.req_mapping['on_response']['filter']))
return response

View File

@ -14,57 +14,28 @@ class MandayeFilter(object):
self.env = env
self.target = target
def request_cookies(self, cookies):
""" cookies: a Cookie.BaseCookie
http://docs.python.org/library/cookie.html
return a Cookie.BaseCookie
"""
return cookies
def request_headers(self, headers):
""" headers: HTTPHeader instance
return HTTPHeader object
"""
referer = headers.getheader('referer')
def on_request(self, request):
referer = request.headers.getheader('referer')
if referer:
referer = referer.replace(self.self.env["HTTP_HOST"], self.target.hostname)
headers.addheader("Referer", referer)
return headers
referer = referer.replace(self.env["HTTP_HOST"], self.target.hostname)
request.headers.addheader("Referer", referer)
return request
def request_msg(self, msg):
""" msg: request message body
return the modify message
"""
return msg
def response_cookies(self, cookies):
""" cookies: a Cookie.BaseCookie object
return a Cookie.BaseCookie
"""
def on_response(self, response):
# Modify cookies
src_hostname = re.sub(":\d*$", "", self.env["HTTP_HOST"])
for cookie in cookies.values():
for cookie in response.cookies.values():
if cookie.has_key('domain'):
cookie['domain'] = cookie['domain'].replace(self.target.hostname, src_hostname)
return cookies
def response_headers(self, headers):
""" headers: HTTPHeader instance
return a HTTPHeader object
"""
# Modify headers
blacklist = ['transfer-encoding', 'content-length']
for key, value in headers.iteritems():
for key, value in response.headers.iteritems():
if key == 'location':
location = value[0].replace(self.target.hostname,
self.env["HTTP_HOST"])
headers.addheader(key, location)
response.headers.addheader(key, location)
for name in blacklist:
if headers.has_key(name):
del headers[name]
return headers
def response_msg(self, msg):
""" msg: response message body
return the modify message """
return msg
if response.headers.has_key(name):
del response.headers[name]
return response

View File

@ -1,18 +1,39 @@
import re
def biblio_html_login_page(data, env):
""" data: response message body
from mandaye.response import serve_template
def biblio_html_login_page(env, values, request, response):
""" msg: response message body
env: Mandaye environment
"""
data = re.sub("</center></div>",
response.msg = re.sub("</center></div>",
"<a href=\"/mandaye/connection\">Connection via mon compte citoyen</center></div>",
data)
return data
response.msg)
return response
def biblio_html(data, env):
def biblio_html(env, values, request, response):
""" Global html filter the Vincenne library
This fix the fucking absolute url of the biblio site
"""
data = data.replace(env["target"].hostname, env["HTTP_HOST"])
return data
response.msg = response.msg.replace(env["target"].hostname, env["HTTP_HOST"])
return response
def biblio_associate_req(env, values, request):
""" Modify the association request
"""
request.req_method = 'GET'
if request.headers.has_key('Content-Type'):
del request.headers['Content-Type']
return request
def biblio_associate_login(env, values, request, response):
""" Use default login page to associate an account on the SSO
"""
pattern = re.compile('<div class="inner">.*</center></div>\s+</div></div>',
re.MULTILINE | re.DOTALL)
form = serve_template(values.get('template'), **values)
form = form.encode('utf-8')
response.msg = re.sub(pattern, form, response.msg)
return response

View File

@ -14,6 +14,12 @@ class HTTPHeader(dict):
for key in headers.keys():
self[key] = headers.getheaders(key)
def load_from_list(self, headers):
""" headers: list with the headers
"""
for header in headers:
self[header[0]] = [header[1]]
def addheader(self, name, value):
""" name: field name
value: string with the field value """
@ -55,27 +61,28 @@ class HTTPHeader(dict):
class HTTPRequest(object):
def __init__(self, cookies, headers, msg=None):
""" cookies:
def __init__(self, cookies, headers, req_method, msg=None):
"""
"""
self.cookies = cookies
self.headers = headers
self.msg = msg
self.req_method = req_method
class HTTPResponse(object):
""" Mandaye HTTP Response
"""
def __init__(self, code=None, status=None, headers=None, msg=None, cookies=None):
def __init__(self, code=None, reason=None, headers=None, msg=None, cookies=None):
"""
code: integer return code (200, 302, 404, ..)
status: string with http status (ex: OK, Not Found, ...)
reason: string with http status (ex: OK, Not Found, ...)
headers: dictionary with the headers
msg: message body str
"""
self.code = code
self.status = status
self.reason = reason
self.headers = headers
self.msg = msg
self.cookies = cookies
@ -88,11 +95,9 @@ class HTTPResponse(object):
""" response: urrlib.addinfourl
"""
self.headers.load_httpmessage(response.headers)
if self.headers.has_key('set-cookie'):
del self.headers['set-cookie']
for setcookie in response.headers.getheaders('set-cookie'):
self.cookies.load("Set-Cookie: %s" % setcookie)
self.code = response.code
self.status = response.msg
self.reason = response.msg
self.msg = response.read()

View File

@ -13,22 +13,36 @@ class Site(Base):
id = Column(Integer, primary_key=True)
name = Column(String, unique=True, nullable=False)
def __init__(self, name):
self.name = name
def __repr__(self):
return "<Site('%s')>" % (self.name)
class LocalUser(Base):
""" Mandaye local user
"""
__tablename__ = 'local_users'
id = Column(Integer, primary_key=True)
login = Column(String, nullable=True)
fullname = Column(String, nullable=True)
login = Column(String(80), nullable=True)
fullname = Column(String(150), nullable=True)
password = Column(String, nullable=True)
token = Column(String, nullable=True)
token = Column(String(150), nullable=True)
def __init__(self, login, password):
def __init__(self, login=None, password=None,
fullname=None, token=None):
self.login = login
self.password = password
self.fullname = fullname
self.token = token
def __repr__(self):
return "<User('%s)>" % (self.login)
if self.login:
return "<LocalUser('%s')>" % (self.login)
elif self.token:
return "<LocalUser('%s')>" % (self.token)
return "<LocalUser>"
class ExtUser(Base):
""" User of externals applications
@ -36,22 +50,26 @@ class ExtUser(Base):
__tablename__ = 'ext_users'
id = Column(Integer, primary_key=True)
login = Column(String, nullable=True)
login = Column(String(80), nullable=True)
password = Column(String, nullable=True)
token = Column(String, nullable=True)
token = Column(String(200), nullable=True)
local_user_id = Column(Integer, ForeignKey('local_users.id'))
site_id = Column(Integer, ForeignKey('sites.id'))
local_user = relationship("LocalUser", backref=backref('ext_users', order_by=id))
site = relationship("LocalUser", backref=backref('users'))
def __init__(self, login, password, token):
def __init__(self, login=None, password=None, token=None):
self.login = login
self.password = password
self.token = token
def __repr__(self):
return "<User('%s')>" % (self.login)
if self.login:
return "<ExtUser('%s')>" % (self.login)
elif self.token:
return "<ExtUser('%s')>" % (self.token)
return "<ExtUser>"

View File

@ -9,7 +9,8 @@ from mako.template import Template
from mandaye.http import HTTPResponse, HTTPHeader
from mandaye.config import template_directory, debug
mylookup = TemplateLookup(directories=[template_directory])
mylookup = TemplateLookup(directories=[template_directory],
input_encoding='utf-8')
def serve_template(templatename, **kwargs):
mytemplate = mylookup.get_template(templatename)

View File

@ -20,7 +20,7 @@ from gevent.pywsgi import WSGIServer
from mandaye.dispatcher import Dispatcher
from mandaye.handlers.default import MandayeRedirectHandler, MandayeErrorHandler
from mandaye.http import HTTPHeader, HTTPRequest, HTTPResponse
from mandaye.response import _404, _502
from mandaye.response import _404, _502, _500
# Init poster
opener = poster.streaminghttp.register_openers()
@ -77,7 +77,7 @@ class MandayeApp(object):
else:
msg = None
return HTTPRequest(cookies, headers, msg)
return HTTPRequest(cookies, headers, self.env['REQUEST_METHOD'], msg)
def on_request(self, start_response):
mrequest = self._get_request()
@ -90,17 +90,18 @@ class MandayeApp(object):
if not url:
return self.on_response(start_response,
self.dispatcher.get_response(mrequest))
if self.env['REQUEST_METHOD'] == 'POST':
data = self.env['wsgi.input'].read()
req = urllib2.Request(url, data, mrequest.headers.getheaders())
if mrequest.req_method == 'POST':
req = urllib2.Request(url, mrequest.msg, mrequest.headers.getheaders())
else:
req = urllib2.Request(url, headers=mrequest.headers.getheaders())
# Load the cookies
req.add_header('cookie', mrequest.cookies.output(sep=',', header=''))
print req.headers
try:
opener = urllib2.build_opener(MandayeErrorHandler, MandayeRedirectHandler)
response = opener.open(req)
except Exception, e:
response.close()
response = _502(self.env['PATH_INFO'], req.get_host(), e)
return self.on_response(start_response, response)
else:
@ -113,7 +114,7 @@ class MandayeApp(object):
""" start_response: wsgi start_response
response: an instance of HTTPResponse """
response.headers.addsetcookies(response.cookies)
start_response('%d %s' % (response.code, response.status),
start_response('%d %s' % (response.code, response.reason),
response.headers.items())
return [response.msg]

View File

@ -0,0 +1,37 @@
<div class="inner">
<h1 class="title">Associer votre compte</h1>
% if error_msg:
<div class="messages error">
${error_msg}
</div>
% endif
<div class="node" id="node-470">
<div class="content">
<strong><u>N° de carte</u></strong> = les <u>6 derniers numéros </u>de&nbsp; la carte <br>
<strong><u>Mot de passe</u></strong> = lors de la <u>première connexion votre nom de famille</u>
</div>
</div>
<div class="block-connexion-utilisateur clear-block block block-dk_opac15" id="block-dk_opac15-login"><div id="blockContent">
<h2><div>Associer votre compte au compte citoyen</div></h2>
<div class="content"><center>
<form id="dk-opac15-login-form" method="post" accept-charset="UTF-8" action="${action}">
<div><div class="form-item">
<label for="edit-user">Numéro de carte&nbsp;: <span title="Ce champ est obligatoire." class="form-required">*</span></label>
<input type="text" class="form-text required error" value="fdssdf" size="19" id="edit-user" name="username" maxlength="128">
</div>
<div class="form-item">
<label for="edit-password">Mot de passe&nbsp;: <span title="Ce champ est obligatoire." class="form-required">*</span></label>
<input type="password" class="form-text required" size="19" id="edit-password" name="password">
</div>
<input type="submit" class="form-submit" value="Associer" id="edit-submit" name="op">
<input type="hidden" value="dk_opac15_login_form" id="edit-dk-opac15-login-form" name="form_id">
</div></form>
</center></div>
</div></div>