diff --git a/README.rst b/README.rst index 5e9a8dd..2e6c43d 100644 --- a/README.rst +++ b/README.rst @@ -52,32 +52,29 @@ You must install the following packages to use Mandaye * sqlalchemy-migrate:: http://pypi.python.org/pypi/sqlalchemy-migrate -You can install all those dependencies quickly using pip:: - - pip install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback - -or easy_install:: - - easy_install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback - -or apt-get (Debian based distributions):: - - apt-get install gunicorn python-poster python-sqlalchemy python-beaker python-mako python-lxml python-setuptools - -It's recommanded to install the following modules - - * PyCrypto >= 2.3:: http://pypi.python.org/pypi/pycrypto - * Static >= 0.4:: http://pypi.python.org/pypi/static - - You can install this Python modules with pip:: - - pip install pycrypto static - Quick installation ------------------ -Install at least Python >=2.5 and setuptools or distribute and enter this command in a shell:: +Install at least Python >=2.5 and pip in your system. +For example with Debian or a Debian based distribution:: + sudo apt-get install python python-pip + +Then install virtualenv :: + + pip install virtualenv + +Create your virtualenv activate it:: + + virtualenv mandaye + source mandaye/bin/activate + pip install -U pip + +Install mandaye:: + + $ tar xfvz mandaye-VERSION.tar.gz + $ cd mandaye-VERSION + $ pip install -r requirements.txt $ python setup.py install If you want to develop use this command line:: @@ -88,12 +85,9 @@ If you want to develop use this command line:: Quick Start ----------- -Configure MANDAYE_PATH/mandaye/config.py with your own preferences. -You must configure the database uri and the log file. +First step is to create a mandaye project:: -First create your database:: - - $ mandaye_admin.py --createdb + $ mandaye_admin.py --newproject Launch mandaye server:: diff --git a/mandaye/auth/authform.py b/mandaye/auth/authform.py index 3b73c15..1ec5efd 100644 --- a/mandaye/auth/authform.py +++ b/mandaye/auth/authform.py @@ -16,19 +16,20 @@ from lxml.html import fromstring from urlparse import parse_qs from mandaye import config, VERSION -from mandaye.db import sql_session from mandaye.exceptions import MandayeException -from mandaye.models import Site, ExtUser, LocalUser from mandaye.log import logger from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest from mandaye.response import _500, _302, _401 from mandaye.response import template_response from mandaye.server import get_response +from mandaye.config.backend import ManagerIDPUser, ManagerSPUser,\ + ManagerServiceProvider + try: from Crypto.Cipher import AES except ImportError: - config.encrypt_ext_password = False + config.encrypt_sp_password = False class AuthForm(object): @@ -72,7 +73,7 @@ a password_field key if you want to encode a password.") def _encrypt_pwd(self, post_values): """ This method allows you to encrypt a password - To use this feature you muste set encrypt_ext_password to True + To use this feature you muste set encrypt_sp_password to True in your configuration and set a secret in encrypt_secret post_values: containt the post values return None and modify post_values @@ -98,7 +99,7 @@ a password_field key if you want to encode a password.") def _decrypt_pwd(self, post_values): """ This method allows you to dencrypt a password encrypt with _encrypt_pwd method. To use this feature you muste set - encrypt_ext_password to True in your configuration and + encrypt_sp_password to True in your configuration and set a secret in encrypt_secret post_values: containt the post values return None and modify post_values @@ -120,7 +121,7 @@ a password_field key if you want to encode a password.") def _get_password(self, post_values): if self.form_values.has_key('password_field'): - if config.encrypt_ext_password: + if config.encrypt_sp_password: return self._encrypt_pwd( post_values[self.form_values['password_field']] ) @@ -182,54 +183,36 @@ a password_field key if you want to encode a password.") request = HTTPRequest(cookies, headers, "POST", params) return get_response(env, request, action, cj) - def _save_association(self, env, local_login, post_values): + def _save_association(self, env, unique_id, post_values): """ save an association in the database env: wsgi environment - local_login: the Mandaye login + unique_id: idp uinique id post_values: dict with the post values """ - ext_username = post_values[self.form_values['username_field']] - if config.encrypt_ext_password: + sp_login = post_values[self.form_values['username_field']] + if config.encrypt_sp_password: self._encrypt_pwd(post_values) - site = sql_session().query(Site).\ - filter_by(name=self.site_name).first() - if not site: - logger.info('Add %s site in the database' % self.site_name) - site = Site(self.site_name) - sql_session().add(site) - local_user = sql_session().query(LocalUser).\ - filter_by(login=local_login).first() - if not local_user: - logger.debug('Add user %s in the database' % local_login) - local_user = LocalUser(login=local_login) - sql_session().add(local_user) - ext_user = sql_session().query(ExtUser).\ - join(LocalUser).\ - filter(LocalUser.login==local_login).\ - filter(ExtUser.login==ext_username).\ - first() - if not ext_user: - ext_user = ExtUser() - sql_session().add(ext_user) - logger.info('New association: %s with %s on site %s' % \ - (ext_username, local_login, self.site_name)) - ext_user.login = ext_username - ext_user.post_values = post_values - ext_user.local_user = local_user - ext_user.last_connection = datetime.now() - ext_user.site = site - sql_session().commit() - env['beaker.session']['login'] = local_login - env['beaker.session'][self.site_name] = ext_user.id + service_provider = ManagerServiceProvider.get_or_create(self.site_name) + idp_user = ManagerIDPUser.get_or_create(unique_id) + sp_user = ManagerSPUser.get_or_create(sp_login, post_values, + idp_user, service_provider) + sp_user.login = sp_login + sp_user.post_values = post_values + sp_user.idp_user = idp_user + sp_user.last_connection = datetime.now() + sp_user.service_provider = service_provider + ManagerSPUser.save() + env['beaker.session']['unique_id'] = unique_id + env['beaker.session'][self.site_name] = sp_user.id env['beaker.session'].save() def associate_submit(self, env, values, condition, request, response): """ Associate your login / password into your database """ logger.debug("Trying to associate a user") - login = env['beaker.session'].get('login') + unique_id = env['beaker.session'].get('unique_id') if request.msg: - if not login: + if not unique_id: logger.warning("Association failed: user isn't login on Mandaye") return _302(values.get('connection_url')) post = parse_qs(request.msg.read(), request) @@ -247,7 +230,7 @@ a password_field key if you want to encode a password.") response = self.replay(env, post_values) if eval(condition): logger.debug("Replay works: save the association") - self._save_association(env, login, post_values) + self._save_association(env, unique_id, post_values) if qs.has_key('next_url'): return _302(qs['next_url'], response.cookies) return response @@ -256,20 +239,20 @@ a password_field key if you want to encode a password.") qs['type'] = 'badlogin' return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs)) - def _login_ext_user(self, ext_user, env, condition, values): - """ Log in an external user + def _login_sp_user(self, sp_user, env, condition, values): + """ Log in sp user """ - if not ext_user.login: + if not sp_user.login: return _500(env['PATH_INFO'], 'Invalid values for AuthFormDispatcher.login') - post_values = copy.deepcopy(ext_user.post_values) - if config.encrypt_ext_password: + post_values = copy.deepcopy(sp_user.post_values) + if config.encrypt_sp_password: self._decrypt_pwd(post_values) response = self.replay(env, post_values) if condition and eval(condition): - ext_user.last_connection = datetime.now() - sql_session().commit() - env['beaker.session'][self.site_name] = ext_user.id + sp_user.last_connection = datetime.now() + ManagerSPUser.save() + env['beaker.session'][self.site_name] = sp_user.id env['beaker.session'].save() return response else: @@ -279,29 +262,27 @@ a password_field key if you want to encode a password.") """ Automatic login on a site with a form """ logger.debug('Trying to login on Mandaye') - login = self.get_current_login(env) - if not login: + # Specific method to get current idp unique id + unique_id = self.get_current_unique_id(env) + if not unique_id: return _401('Access denied: invalid token') # FIXME: hack to force beaker to generate an id # somtimes beaker doesn't do it by himself env['beaker.session'].regenerate_id() - env['beaker.session']['login'] = login + env['beaker.session']['unique_id'] = unique_id env['beaker.session'].save() - logger.debug('User %s successfully login' % env['beaker.session']['login']) - ext_user = sql_session().query(ExtUser).\ - join(LocalUser).\ - join(Site).\ - filter(LocalUser.login==login).\ - filter(Site.name==self.site_name).\ - order_by(ExtUser.last_connection.desc()).\ - first() - if not ext_user: - logger.debug('User %s is not associate' % env['beaker.session']['login']) + logger.debug('User %s successfully login' % env['beaker.session']['unique_id']) + + idp_user = ManagerIDPUser.get(unique_id) + service_provider = ManagerServiceProvider.get(self.site_name) + sp_user = ManagerSPUser.get_last_connected(idp_user, service_provider) + if not sp_user: + logger.debug('User %s is not associate' % env['beaker.session']['unique_id']) return _302(values.get('associate_url') + "?type=first") - return self._login_ext_user(ext_user, env, condition, values) + return self._login_sp_user(sp_user, env, condition, values) def logout(self, env, values, request, response): """ Destroy the Beaker session @@ -317,19 +298,17 @@ a password_field key if you want to encode a password.") This method must have a query string with a username parameter """ # TODO: need to logout the first - login = env['beaker.session']['login'] + unique_id = env['beaker.session']['unique_id'] qs = parse_qs(env['QUERY_STRING']) if not login or not qs.has_key('id'): return _401('Access denied: beaker session invalid or not qs id') id = qs['id'][0] - ext_user = sql_session().query(ExtUser).\ - join(LocalUser).\ - filter(LocalUser.login==login).\ - filter(ExtUser.id==id).\ - first() - if not ext_user: + service_provider = ManagerServiceProvider.get(self.site_name) + idp_user = ManagerServiceProvider.get(unique_id) + sp_user = ManagerSPUser.get_last_connected(idp_user, service_provider) + if not sp_user: return _302(values.get('associate_url')) - return self._login_ext_user(ext_user, env, 'response.code==302', values) + return self._login_sp_user(sp_user, env, 'response.code==302', values) def disassociate(self, env, values, request, response): """ Multi accounts feature @@ -345,15 +324,9 @@ a password_field key if you want to encode a password.") if not login or not qs.has_key('id'): return _401('Access denied: beaker session invalid or not id') id = qs['id'][0] - ext_user = sql_session().query(ExtUser).\ - join(LocalUser).\ - filter(LocalUser.login==login).\ - filter(ExtUser.id==id).\ - first() - if ext_user: - logger.debug('Disassociate account %s' % ext_user.login) - sql_session().delete(ext_user) - sql_session().commit() + sp_user = ManagerSPUser.get_by_id(id) + if sp_user: + ManagerSPUser.delete(sp_user) if qs.has_key('logout'): self.logout(env, values, request, response) return _302(values.get('next_url')) diff --git a/mandaye/auth/vincennes.py b/mandaye/auth/vincennes.py index c3ce06a..f588917 100644 --- a/mandaye/auth/vincennes.py +++ b/mandaye/auth/vincennes.py @@ -38,8 +38,8 @@ class VincennesAuth(AuthForm): res[keyvalue[0]] = keyvalue[1] return res - def get_current_login(self, env): - """ Return the current Vincennes pseudo + def get_current_unique_id(self, env): + """ Return the current Vincennes unique id """ from mandaye import config # TODO: test time validity @@ -97,7 +97,7 @@ class VincennesAuth(AuthForm): if not login: logger.debug('Auto login failed because the user is not connected on vincennes.fr') return _302(path, request.cookies) - env['beaker.session']['login'] = login + env['beaker.session']['unique_id'] = unique_id env['beaker.session'].save() ext_user = sql_session().query(ExtUser).\ join(LocalUser).\ diff --git a/mandaye/backends/__init__.py b/mandaye/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mandaye/backends/default.py b/mandaye/backends/default.py new file mode 100644 index 0000000..d2baa1d --- /dev/null +++ b/mandaye/backends/default.py @@ -0,0 +1,76 @@ + +class DefaultManagerIDPUser: + + @staticmethod + def get(unique_id, idp_id='default'): + pass + + @staticmethod + def create(unique_id, idp_id='default'): + pass + + @staticmethod + def get_or_create(unique_id, idp_id='default'): + pass + + @staticmethod + def delete(idp_user): + pass + + @staticmethod + def save(idp_user): + pass + +class DefaultManagerSPUser: + + @staticmethod + def get(login, idp_user, service_provider): + pass + + @staticmethod + def get_by_id(id): + pass + + @staticmethod + def get_last_connected(idp_user, service_provider): + pass + + @staticmethod + def create(login, post_values, idp_user, service_provider): + pass + + @staticmethod + def get_or_create(login, post_values, idp_user, service_provider): + pass + + @staticmethod + def delete(sp_user): + pass + + @staticmethod + def save(sp_user): + pass + +class DefaultServiceProvider: + + @staticmethod + def get(name): + pass + + @staticmethod + def create(name): + pass + + @staticmethod + def get_or_create(name): + pass + + @staticmethod + def delete(service_provider): + pass + + @staticmethod + def save(service_provider): + pass + + diff --git a/mandaye/backends/sql.py b/mandaye/backends/sql.py new file mode 100644 index 0000000..c9150ba --- /dev/null +++ b/mandaye/backends/sql.py @@ -0,0 +1,162 @@ + +from datetime import datetime + +from mandaye.db import sql_session +from mandaye.models import IDPUser, SPUser, ServiceProvider + +class ManagerIDPUserSQL: + + @staticmethod + def get(unique_id, idp_id='default'): + idp_user = sql_session().query(IDPUser).\ + filter_by(unique_id=unique_id, + idp_id='default') + if len(idp_user) > 1: + logger.critical('ManagerIDPUserSQL.get %s not unique' % unique_id) + raise MandayeException( + 'ManagerIDPUserSQL.get : %s is not unique' % unique_id) + if idp_user: + return idp_user.first() + else: + return None + + @staticmethod + def create(unique_id, idp_id='default'): + idp_user = IDPUser( + unique_id=unique_id, + idp_id=idp_id) + sql_session().add(idp_user) + return idp_user + + @staticmethod + def get_or_create(unique_id, idp_id='default'): + if ManagerIDPUserSQL.get(**kwargs): + return user + else: + return ManagerIDPUserSQL.create(**kwargs) + + @staticmethod + def delete(idp_user): + sql_session().delete(idp_user) + sql_session().commit() + + @staticmethod + def save(): + sql_session().commit() + +class ManagerSPUserSQL: + + @staticmethod + def get(login, idp_user, service_provider): + sp_user = sql_session().query(SPPUser).\ + join(IDPUser).\ + join(ServiceProvider).\ + filter_by(login=login, + idp_user=idp_user, + service_provider=service_provider) + if sp_user: + return sp_user.first() + else: + return None + + @staticmethod + def get_by_id(id): + return sql_session().query(SPUser).\ + filter(id==id).first() + + @staticmethod + def get_last_connected(idp_user, service_provider): + return sql_session().query(SPPUser).\ + join(IDPUser).\ + join(ServiceProvider).\ + filter(idp_user=idp_user).\ + filer(service_provider=service_provider).\ + order_by(SPUser.last_connection.desc()).\ + first() + @staticmethod + def get_sp_users(idp_unique_id, service_provider_name): + return sql_session().query(SPUser).\ + join(IDPUser).\ + join(ServiceProvider).\ + filter(IDPUser.unique_id==idp_unique_id).\ + filter(ServiceProvider.name==service_provider_name).\ + order_by(SPUser.last_connection.desc()).\ + all() + + @staticmethod + def create(login, post_values, idp_user, service_provider): + sp_user = SPUser( + login=login, + post_values=post_values, + idp_id=idp_id, + service_provider = service_provider + ) + logger.info('New association: %s with %s on site %s' % \ + (login, idp_user.unique_id, service_provider.name)) + sql_session().add(sp_user) + sql_session().commit() + return idp_user + + @staticmethod + def get_or_create(login, post_values, idp_user, service_provider): + sp_user = ManagerSPUserSQL.get(login, idp_user, service_provider) + if sp_user: + return sp_user + else: + return ManagerSPUserSQL.create(login, post_values, + idp_user, service_provider) + @staticmethod + def all(): + return sql_session().query(SPUser).all() + + @staticmethod + def delete(sp_user): + logger.debug('Disassociate account %s' % sp_user.login) + sql_session().delete(sp_user) + sql_session().commit() + + @staticmethod + def save(): + sql_session().commit() + +class ManagerServiceProviderSQL: + + @staticmethod + def get(name): + sp = sql_session().query(ServiceProvider).\ + filter_by(name=name) + if sp: + return sp.first() + else: + return None + + @staticmethod + def create(name): + logger.info('Add %s service provider into the database' % name) + sp = ServiceProvider(name=name) + sql_session().add(sp) + sql_session().commit() + return sp + + @staticmethod + def get_or_create(name): + sp = ManagerServiceProviderSQL.get(name) + if sp: + return sp + else: + return ManagerServiceProviderSQL.create(name) + + @staticmethod + def delete(service_provider): + logger.debug('Delete service provider %s' % service_provider.name) + sql_session().delete(service_provider) + sql_session().commit() + + @staticmethod + def save(): + sql_session().commit() + +ManagerServiceProvider = ManagerServiceProviderSQL +ManagerSPUser = ManagerSPUserSQL +ManagerServiceProvider = ManagerServiceProviderSQL + diff --git a/mandaye/config.py b/mandaye/config.py index 85e67eb..a5cb55f 100644 --- a/mandaye/config.py +++ b/mandaye/config.py @@ -1,6 +1,14 @@ import logging from mandaye.exceptions import ImproperlyConfigured +# Database configuration +# rfc 1738 http://rfc.net/rfc1738.html +db_url = 'sqlite:///test.db' + +# Default local backend +import mandaye.backends.sql +backend = mandaye.backends.sql + # Needed if ssl is activated ssl = False keyfile = '' @@ -11,6 +19,7 @@ debug = False syslog = False log_file = '/var/log/mandaye/mandaye.log' log_level = logging.INFO + # Log rotation # W[0-6] : weekly (0: Monday), D: day, ... (python doc) log_when = 'W6' @@ -24,9 +33,6 @@ template_directory = 'mandaye/templates' # Static folder static_root = 'mandaye/static' -# Database configuration -# rfc 1738 http://rfc.net/rfc1738.html -db_url = 'sqlite:///test.db' # Email notification configuration email_notification = False @@ -42,9 +48,9 @@ use_long_trace = True # Decompress response only if you load a filter auto_decompress = True -# Encrypt external passwords with a secret +# Encrypt service provider passwords with a secret # You should install pycypto to use this feature -encrypt_ext_password = False +encrypt_sp_password = False # Must be a 16, 24, or 32 bytes long encrypt_secret = '' diff --git a/mandaye/exceptions.py b/mandaye/exceptions.py index 5d5df3a..e2df73f 100644 --- a/mandaye/exceptions.py +++ b/mandaye/exceptions.py @@ -9,3 +9,4 @@ class ImproperlyConfigured(Exception): class MandayeException(Exception): "Mandaye generic exception" pass + diff --git a/mandaye/filters/vincennes.py b/mandaye/filters/vincennes.py index bbe0ab9..dc065c7 100644 --- a/mandaye/filters/vincennes.py +++ b/mandaye/filters/vincennes.py @@ -6,12 +6,13 @@ from urlparse import parse_qs from BeautifulSoup import BeautifulSoup import lxml.html -from mandaye.db import sql_session from mandaye.log import logger from mandaye.models import Site, ExtUser, LocalUser from mandaye.response import _302, _401 from mandaye.template import serve_template +from mandaye.config.backend import ManagerSPUser + def get_associate_form(env, values): """ Return association template content """ @@ -37,8 +38,7 @@ def get_current_account(env, values): """ Return the current Mandaye user """ site_name = values.get('site_name') if env['beaker.session'].get(site_name): - return sql_session().query(ExtUser).\ - get(env['beaker.session'].get(site_name)) + return ManagerSPUser.get_by_id(env['beaker.session'].get(site_name)) else: return None @@ -46,18 +46,13 @@ def get_current_account(env, values): def get_multi_template(env, values, current_account): """ return the content of the multi account template """ - login = env['beaker.session'].get('login') + unique_id = env['beaker.session'].get('unique_id') if login: - ext_users = sql_session().query(ExtUser).\ - join(LocalUser).\ - join(Site).\ - filter(LocalUser.login==login).\ - filter(Site.name==values.get('site_name')).\ - order_by(ExtUser.last_connection.desc()).\ - all() accounts = {} - for ext_user in ext_users: - accounts[ext_user.id] = ext_user.login + sp_users = ManagerSPUser.get_sp_users(unique_id, + values.get('site_name')) + for sp_user in sp_users: + accounts[sp_user.id] = sp_user.login if current_account: current_login = current_account.login else: @@ -113,7 +108,7 @@ class Biblio: """ Modify response html to support multi accounts """ if response.msg and '