From 2d515e8f61d4cbd9d28b43d6fb8d3da677d2b1f7 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Tue, 25 Feb 2014 20:49:57 +0100 Subject: [PATCH 01/93] Initial import --- .hgignore | 12 + auth_saml/__init__.py | 4 + auth_saml/__openerp__.py | 49 ++ auth_saml/auth_saml.py | 65 +++ auth_saml/controllers/__init__.py | 3 + auth_saml/controllers/main.py | 114 +++++ auth_saml/res_config.py | 62 +++ auth_saml/res_users.py | 142 ++++++ auth_saml/security/ir.model.access.csv | 2 + auth_saml_data.xml | 86 ++++ auth_saml_view.xml | 46 ++ res_config.xml | 29 ++ res_users.xml | 23 + static/lib/zocial/README.md | 31 ++ .../lib/zocial/css/zocial-regular-webfont.eot | Bin 0 -> 28471 bytes .../lib/zocial/css/zocial-regular-webfont.svg | 129 ++++++ .../lib/zocial/css/zocial-regular-webfont.ttf | Bin 0 -> 41156 bytes .../zocial/css/zocial-regular-webfont.woff | Bin 0 -> 31320 bytes static/lib/zocial/css/zocial.css | 420 ++++++++++++++++++ static/lib/zocial/sample.html | 265 +++++++++++ static/src/css/auth_saml.css | 34 ++ static/src/js/auth_saml.js | 73 +++ static/src/xml/auth_saml.xml | 8 + 23 files changed, 1597 insertions(+) create mode 100644 .hgignore create mode 100644 auth_saml/__init__.py create mode 100644 auth_saml/__openerp__.py create mode 100644 auth_saml/auth_saml.py create mode 100644 auth_saml/controllers/__init__.py create mode 100644 auth_saml/controllers/main.py create mode 100644 auth_saml/res_config.py create mode 100644 auth_saml/res_users.py create mode 100644 auth_saml/security/ir.model.access.csv create mode 100644 auth_saml_data.xml create mode 100644 auth_saml_view.xml create mode 100644 res_config.xml create mode 100644 res_users.xml create mode 100644 static/lib/zocial/README.md create mode 100644 static/lib/zocial/css/zocial-regular-webfont.eot create mode 100644 static/lib/zocial/css/zocial-regular-webfont.svg create mode 100644 static/lib/zocial/css/zocial-regular-webfont.ttf create mode 100644 static/lib/zocial/css/zocial-regular-webfont.woff create mode 100644 static/lib/zocial/css/zocial.css create mode 100644 static/lib/zocial/sample.html create mode 100644 static/src/css/auth_saml.css create mode 100644 static/src/js/auth_saml.js create mode 100644 static/src/xml/auth_saml.xml diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000000..f46d1844e52 --- /dev/null +++ b/.hgignore @@ -0,0 +1,12 @@ +syntax: glob +*.pyc +**/*.pyc +*.swp +.tmp* +tmp/* +*.egg-info +*.orig +nose* +.rope* +.coverage +cover/* diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py new file mode 100644 index 00000000000..15d0b15a5e6 --- /dev/null +++ b/auth_saml/__init__.py @@ -0,0 +1,4 @@ +import controllers +import auth_saml +import res_users +import res_config diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py new file mode 100644 index 00000000000..8f66b1f76e4 --- /dev/null +++ b/auth_saml/__openerp__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# XCG Consulting Group +# Copyright (C) 2010-2014 XCG Consulting s.a.s +# . +# +############################################################################## + + +{ + 'name': 'Saml2 Authentication', + 'version': '1.0', + 'category': 'Tools', + 'description': """ +Allow users to login through Saml2 Provider. +============================================= +""", + 'author': 'XCG Consulting s.a.s.', + 'maintainer': 'XCG Consulting s.a.s.', + 'website': 'http://www.xcg-consulting.fr', + 'depends': ['base', 'web', 'base_setup'], + 'data': [ + 'auth_saml_data.xml', + 'auth_saml_view.xml', + 'security/ir.model.access.csv' + ], + 'js': ['static/src/js/auth_saml.js'], + 'css': [ + 'static/lib/zocial/css/zocial.css', + 'static/src/css/auth_saml.css', + ], + 'qweb': ['static/src/xml/auth_saml.xml'], + 'installable': True, + 'auto_install': False, +} diff --git a/auth_saml/auth_saml.py b/auth_saml/auth_saml.py new file mode 100644 index 00000000000..d748b5ab599 --- /dev/null +++ b/auth_saml/auth_saml.py @@ -0,0 +1,65 @@ +from openerp.osv import osv, fields +import lasso + + +class auth_saml_provider(osv.osv): + """Class defining the configuration values of an Saml2 provider""" + + _name = 'auth.saml.provider' + _description = 'SAML2 provider' + _order = 'name' + + def _get_lasso_for_provider(self, cr, uid, provider_id, context=None): + print cr, uid, provider_id, context + provider = self.browse(cr, uid, provider_id, context=context) + + # TODO: we should cache those results somewhere because it is + # really costy to always recreate a login variable from buffers + server = lasso.Server.newFromBuffers( + provider.sp_metadata, + provider.sp_pkey + ) + server.addProviderFromBuffer( + lasso.PROVIDER_ROLE_IDP, + provider.idp_metadata + ) + return lasso.Login(server) + + def _get_auth_request(self, cr, uid, ids, name, args, context=None): + """build an authentication request and give it back to our client + WARNING: this method cannot be used for multiple ids + """ + result = {} + login = self._get_lasso_for_provider(cr, uid, ids[0], context=context) + + # ! -- this is the part that MUST be performed on each call and + # cannot be cached + login.initAuthnRequest() + login.request.nameIdPolicy.format = None + login.request.nameIdPolicy.allowCreate = True + login.buildAuthnRequestMsg() + + # msgUrl is a fully encoded url ready for redirect use + result[ids[0]] = login.msgUrl + #print "*" * 35 + #print result + return result + + _columns = { + # Name of the OAuth2 entity, authentic, xcg... + 'name': fields.char('Provider name'), + 'idp_metadata': fields.text('IDP Configuration'), + 'auth_req': fields.function(_get_auth_request), + 'sp_metadata': fields.text('SP Configuration'), + 'sp_pkey': fields.text( + 'Private key of our service provider (this openerpserver)' + ), + 'enabled': fields.boolean('Allowed'), + 'css_class': fields.char('CSS class'), + 'body': fields.char('Body'), + 'sequence': fields.integer(), + } + + _defaults = { + 'enabled': False, + } diff --git a/auth_saml/controllers/__init__.py b/auth_saml/controllers/__init__.py new file mode 100644 index 00000000000..e11f9ba81bb --- /dev/null +++ b/auth_saml/controllers/__init__.py @@ -0,0 +1,3 @@ +import main + +# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py new file mode 100644 index 00000000000..dccbc935679 --- /dev/null +++ b/auth_saml/controllers/main.py @@ -0,0 +1,114 @@ +import functools +import logging + +import simplejson +import werkzeug.utils + +import openerp +from openerp import SUPERUSER_ID +import openerp.addons.web.http as oeweb +from openerp.addons.web.controllers.main import set_cookie_and_redirect +from openerp.addons.web.controllers.main import login_and_redirect +from openerp.modules.registry import RegistryManager + +_logger = logging.getLogger(__name__) + +#---------------------------------------------------------- +# helpers +#---------------------------------------------------------- + + +def fragment_to_query_string(func): + @functools.wraps(func) + def wrapper(self, req, **kw): + if not kw: + return """""" + return func(self, req, **kw) + return wrapper + + +#---------------------------------------------------------- +# Controller +#---------------------------------------------------------- +class SAMLController(oeweb.Controller): + _cp_path = '/auth_saml' + + @oeweb.jsonrequest + def list_providers(self, req, dbname): + try: + registry = RegistryManager.get(dbname) + with registry.cursor() as cr: + providers = registry.get('auth.saml.provider') + l = providers.read( + cr, SUPERUSER_ID, providers.search( + cr, SUPERUSER_ID, [('enabled', '=', True)] + ) + ) + + except Exception, e: + _logger.exception("SAML2: %s" % str(e)) + l = [] + + return l + + @oeweb.httprequest + @fragment_to_query_string + def signin(self, req, **kw): + """JS client obtained a saml token and passed it back + to us... we need to validate it + """ + saml_response = kw.get('SAMLResponse', None) + + state = simplejson.loads(kw['RelayState']) + dbname = state['d'] + provider = state['p'] + context = state.get('c', {}) + registry = RegistryManager.get(dbname) + with registry.cursor() as cr: + try: + u = registry.get('res.users') + credentials = u.auth_saml( + cr, SUPERUSER_ID, provider, saml_response, context=context + ) + cr.commit() + action = state.get('a') + menu = state.get('m') + url = '/' + if action: + url = '/#action=%s' % action + elif menu: + url = '/#menu_id=%s' % menu + return login_and_redirect(req, *credentials, redirect_url=url) + + except AttributeError, e: + print e + # auth_signup is not installed + _logger.error("auth_signup not installed on database %s: saml sign up cancelled." % (dbname,)) + url = "/#action=login&saml_error=1" + + except openerp.exceptions.AccessDenied: + # saml credentials not valid, + # user could be on a temporary session + _logger.info('SAML2: access denied, redirect to main page in case a valid session exists, without setting cookies') + url = "/#action=login&saml_error=3" + redirect = werkzeug.utils.redirect(url, 303) + redirect.autocorrect_location_header = False + return redirect + + except Exception, e: + # signup error + _logger.exception("SAML2: %s" % str(e)) + url = "/#action=login&saml_error=2" + + return set_cookie_and_redirect(req, url) + +# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/auth_saml/res_config.py b/auth_saml/res_config.py new file mode 100644 index 00000000000..75f91ad956c --- /dev/null +++ b/auth_saml/res_config.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# XCG Consulting Group +# Copyright (C) 2010-2014 XCG Consulting s.a.s +# +# +############################################################################## + +from openerp.osv import osv, fields + +#import logging +#_logger = logging.getLogger(__name__) + + +class base_config_settings(osv.TransientModel): + _inherit = 'base.config.settings' + + _columns = { + 'auth_saml_authentic_enabled': fields.boolean( + 'Allow users to sign in with Google' + ), + } + + def get_oauth_providers(self, cr, uid, fields, context=None): + local_id = self.pool.get('ir.model.data').get_object_reference( + cr, uid, 'auth_saml', 'provider_local' + )[1] + + rl = self.pool.get('auth.saml.provider').read( + cr, uid, [local_id], ['enabled'], context=context + ) + + return { + 'auth_oauth_google_enabled': rl[0]['enabled'], + } + + def set_oauth_providers(self, cr, uid, ids, context=None): + local_id = self.pool.get('ir.model.data').get_object_reference( + cr, uid, 'auth_saml', 'provider_local' + )[1] + + config = self.browse(cr, uid, ids[0], context=context) + + rl = { + 'enabled': config.auth_saml_local_enabled, + } + + self.pool.get('auth.saml.provider').write(cr, uid, [local_id], rl) diff --git a/auth_saml/res_users.py b/auth_saml/res_users.py new file mode 100644 index 00000000000..ba24c94e9a5 --- /dev/null +++ b/auth_saml/res_users.py @@ -0,0 +1,142 @@ +import logging +import lasso + +import openerp +from openerp.osv import osv, fields +from openerp import SUPERUSER_ID + +_logger = logging.getLogger(__name__) + + +class res_users(osv.Model): + _inherit = 'res.users' + + _columns = { + 'saml_provider_id': fields.many2one( + 'auth.saml.provider', + string='SAML Provider', + ), + 'saml_uid': fields.char( + 'OAuth User ID', + help="SAML Provider user_id", + ), + } + + _sql_constraints = [ + ( + 'uniq_users_saml_provider_saml_uid', + 'unique(saml_provider_id, saml_uid)', + 'SAML UID must be unique per provider' + ), + ] + + def _auth_saml_validate(self, cr, uid, provider, token, context=None): + """ return the validation data corresponding to the access token """ + + p = self.pool.get('auth.saml.provider') + login = p._get_lasso_for_provider(cr, uid, provider, context=context) + + try: + login.processAuthnResponseMsg(token) + except (lasso.DsError, lasso.ProfileCannotVerifySignatureError): + raise Exception('Lasso Profile cannot verify signature') + except lasso.Error, e: + raise Exception(repr(e)) + + try: + login.acceptSso() + except lasso.Error: + raise Exception('Invalid assertion') + + # TODO use a real token validation from LASSO + validation = {} + + # TODO push into the validation result a real UPN + validation['user_id'] = login.assertion.subject.nameId.content + + """ + if p.data_endpoint: + data = self._auth_oauth_rpc(cr, uid, p.data_endpoint, access_token) + validation.update(data) + """ + + return validation + + def _auth_saml_signin( + self, cr, uid, provider, validation, saml_response, context=None + ): + """ retrieve and sign into openerp the user corresponding to provider + and validated access token + + :param provider: saml provider id (int) + :param validation: result of validation of access token (dict) + :param params: saml parameters (dict) + :return: user login (str) + :raise: openerp.exceptions.AccessDenied if signin failed + + This method can be overridden to add alternative signin methods. + """ + saml_uid = validation['user_id'] + + user_ids = self.search( + cr, uid, + [ + ("saml_uid", "=", saml_uid), + ('saml_provider_id', '=', provider), + ] + ) + + if not user_ids: + raise openerp.exceptions.AccessDenied() + + # TODO replace assert by proper raise... asserts do not execute in + # production code... + assert len(user_ids) == 1 + + user = self.browse(cr, uid, user_ids[0], context=context) + user.write({'saml_access_token': saml_response}) + + return user.login + + def auth_saml(self, cr, uid, provider, saml_response, context=None): + + validation = self._auth_saml_validate( + cr, uid, provider, saml_response + ) + + # required check + if not validation.get('user_id'): + raise openerp.exceptions.AccessDenied() + + # retrieve and sign in user + login = self._auth_saml_signin( + cr, uid, provider, validation, saml_response, context=context + ) + + if not login: + raise openerp.exceptions.AccessDenied() + + # return user credentials + return (cr.dbname, login, saml_response) + + def check_credentials(self, cr, uid, token): + """token can be a password if the user has used the normal form... + but we are more interested in the case when they are tokens + and the interesting code is inside the except clause + """ + try: + return super(res_users, self).check_credentials(cr, uid, token) + + except openerp.exceptions.AccessDenied: + res = self.search( + cr, SUPERUSER_ID, + [ + ('id', '=', uid), + ('saml_access_token', '=', token), + ] + ) + + if not res: + # TODO: maybe raise a defined exception instead of the last + # exception that occured in our execution frame + raise diff --git a/auth_saml/security/ir.model.access.csv b/auth_saml/security/ir.model.access.csv new file mode 100644 index 00000000000..65fd2f54bbf --- /dev/null +++ b/auth_saml/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_auth_oauth_provider,auth_oauth_provider,model_auth_oauth_provider,base.group_system,1,1,1,1 diff --git a/auth_saml_data.xml b/auth_saml_data.xml new file mode 100644 index 00000000000..9fbdd06fd42 --- /dev/null +++ b/auth_saml_data.xml @@ -0,0 +1,86 @@ + + + + + Local Authentic server + + + + + + + MIIDIzCCAgugAwIBAgIJANUBoick1pDpMA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV + BAoTCkVudHJvdXZlcnQwHhcNMTAxMjE0MTUzMzAyWhcNMTEwMTEzMTUzMzAyWjAV + MRMwEQYDVQQKEwpFbnRyb3V2ZXJ0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB + CgKCAQEAvxFkfPdndlGgQPDZgFGXbrNAc/79PULZBuNdWFHDD9P5hNhZn9Kqm4Cp + 06Pe/A6u+g5wLnYvbZQcFCgfQAEzziJtb3J55OOlB7iMEI/T2AX2WzrUH8QT8NGh + ABONKU2Gg4XiyeXNhH5R7zdHlUwcWq3ZwNbtbY0TVc+n665EbrfV/59xihSqsoFr + kmBLH0CoepUXtAzA7WDYn8AzusIuMx3n8844pJwgxhTB7Gjuboptlz9Hri8JRdXi + VT9OS9Wt69ubcNoM6zuKASmtm48UuGnhj8v6XwvbjKZrL9kA+xf8ziazZfvvw/VG + Tm+IVFYB7d1x457jY5zjjXJvNysoowIDAQABo3YwdDAdBgNVHQ4EFgQUeF8ePnu0 + fcAK50iBQDgAhHkOu8kwRQYDVR0jBD4wPIAUeF8ePnu0fcAK50iBQDgAhHkOu8mh + GaQXMBUxEzARBgNVBAoTCkVudHJvdXZlcnSCCQDVAaInJNaQ6TAMBgNVHRMEBTAD + AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAy8l3GhUtpPHx0FxzbRHVaaUSgMwYKGPhE + IdGhqekKUJIx8et4xpEMFBl5XQjBNq/mp5vO3SPb2h2PVSks7xWnG3cvEkqJSOeo + fEEhkqnM45b2MH1S5uxp4i8UilPG6kmQiXU2rEUBdRk9xnRWos7epVivTSIv1Ncp + lG6l41SXp6YgIb2ToT+rOKdIGIQuGDlzeR88fDxWEU0vEujZv/v1PE1YOV0xKjTT + JumlBc6IViKhJeo1wiBBrVRIIkKKevHKQzteK8pWm9CYWculxT26TZ4VWzGbo06j + o2zbumirrLLqnt1gmBDvDvlOwC/zAAyL4chbz66eQHTiIYZZvYgy + + + + + + + + + + + + ]]> + + + + + MIID7jCCA1egAwIBAgIBATANBgkqhkiG9w0BAQUFADCBkzELMAkGA1UEBhMCRlIxDDAKBgNVBAgTA0lERjEOMAwGA1UEBxMFUGFyaXMxDDAKBgNVBAoTA1hDRzEMMAoGA1UECxMDRFNJMQswCQYDVQQDEwJDQTEOMAwGA1UEKRMFWENHQ0ExLTArBgkqhkiG9w0BCQEWHmZsb3JlbnQuYWlkZUB4Y2ctY29uc3VsdGluZy5mcjAgFw0xMzEyMTcxMjExNTJaGA8yMjg3MTAwMTEyMTE1MlowgZUxCzAJBgNVBAYTAkZSMQwwCgYDVQQIEwNJREYxDjAMBgNVBAcTBVBhcmlzMQwwCgYDVQQKEwNYQ0cxDDAKBgNVBAsTA0RTSTEMMAoGA1UEAxMDc3AxMQ8wDQYDVQQpEwZYQ0dTUDExLTArBgkqhkiG9w0BCQEWHmZsb3JlbnQuYWlkZUB4Y2ctY29uc3VsdGluZy5mcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzmzLtPlT6yJOomYstLom6XstTD8E5FQKfPP7UxTcRMXuJynFBSSl6SRVPpUKZQ8maNxOygdeZ5J6vBk2mZEnvk25ZTsXjJDnNZffswi8g/Naxadvh+5kkEx5hjILPTA5jMf7sS/7Am3Kd46vlEIov3OTd2Wh8SII1WgbFLOpy/0CAwEAAaOCAUowggFGMAkGA1UdEwQCMAAwLQYJYIZIAYb4QgENBCAWHkVhc3ktUlNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUPfwm8rjPco3WlV5ntveqm9+Z0iQwgcgGA1UdIwSBwDCBvYAUs++X2JBKjwPYDr7bTZL8ph6fIuShgZmkgZYwgZMxCzAJBgNVBAYTAkZSMQwwCgYDVQQIEwNJREYxDjAMBgNVBAcTBVBhcmlzMQwwCgYDVQQKEwNYQ0cxDDAKBgNVBAsTA0RTSTELMAkGA1UEAxMCQ0ExDjAMBgNVBCkTBVhDR0NBMS0wKwYJKoZIhvcNAQkBFh5mbG9yZW50LmFpZGVAeGNnLWNvbnN1bHRpbmcuZnKCCQCSGwHzqKuCBjATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8EBAMCB4AwDQYJKoZIhvcNAQEFBQADgYEAU7Whu+ZAtIVEtTFCKl/EEWBg7I2m40UqYg/AnU6bPnLkBRXdWdBcP+jwPDh9xjAN07FehWBFGJdaa9p2GBfzkdLF0lJoNda98SYRQKaam53SZ7MIu1newG6joOPJqcliFTAsODGnRwD1dm+p2i6jUI2yiDbXi9ACkKdWfsa88Io= + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + Example SAML 2.0 metadatas + +]]> + + + + zocial saml + Log in with Authentic + + + + diff --git a/auth_saml_view.xml b/auth_saml_view.xml new file mode 100644 index 00000000000..4f013f58586 --- /dev/null +++ b/auth_saml_view.xml @@ -0,0 +1,46 @@ + + + + + + auth.saml.provider.form + auth.saml.provider + +
+ + + + + + + + + + + +
+
+
+ + + auth.saml.provider.list + auth.saml.provider + + + + + + + + + + Providers + auth.saml.provider + form + tree,form + + +
+
diff --git a/res_config.xml b/res_config.xml new file mode 100644 index 00000000000..1e7ce6c64e5 --- /dev/null +++ b/res_config.xml @@ -0,0 +1,29 @@ + + + + + base.config.settings.saml + base.config.settings + + + +
+
+
+ +
+
+
+ You must have an authentic2 server running on + http://localhost:8000
+
+
+
+
+
+
+
+ +
+
diff --git a/res_users.xml b/res_users.xml new file mode 100644 index 00000000000..8e5e872a711 --- /dev/null +++ b/res_users.xml @@ -0,0 +1,23 @@ + + + + + res.users.form + res.users + form + + + + + + + + + + + + + + + + diff --git a/static/lib/zocial/README.md b/static/lib/zocial/README.md new file mode 100644 index 00000000000..213132677af --- /dev/null +++ b/static/lib/zocial/README.md @@ -0,0 +1,31 @@ +# Zocial CSS social buttons + +I basically rewrote this entire set so they are full vector buttons, meaning: + +- @font-face icons +- custom font file for all social icons +- icon font use private unicode spaces for accessibility +- em sizing based on button font-size +- support for about 83 different services +- buttons and icons supported +- no raster images (sweet) +- works splendidly on any browser supporting @font-face +- CSS3 degrades gracefully in IE8 and below etc. +- also includes generic icon-less primary and secondary buttons + +## How to use these buttons + + + +or + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/lib/zocial/css/zocial-regular-webfont.ttf b/static/lib/zocial/css/zocial-regular-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..94809d3a57ffcbfa1f3760420d745b2381574a05 GIT binary patch literal 41156 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 literal 0 HcmV?d00001 diff --git a/static/lib/zocial/css/zocial-regular-webfont.woff b/static/lib/zocial/css/zocial-regular-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..1d6c4abc9d902c0f8662cadf3646810bd03639b7 GIT binary patch literal 31320 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 literal 0 HcmV?d00001 diff --git a/static/lib/zocial/css/zocial.css b/static/lib/zocial/css/zocial.css new file mode 100644 index 00000000000..89fc7450631 --- /dev/null +++ b/static/lib/zocial/css/zocial.css @@ -0,0 +1,420 @@ +@charset "UTF-8"; + +/*! +Code written by Sam Collins (@smcllns) of www.eventasaur.us +You are free to use this work commercially +You are free to extend this work without permissions from the author (just do so tastefully eh?) +Enjoy +*/ + +/* Reference icons from font-files */ + +@font-face { + font-family: 'zocial'; + font-style: normal; + font-weight: normal; + src: url('zocial-regular-webfont.eot'); + src: url('zocial-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('zocial-regular-webfont.woff') format('woff'), + url('zocial-regular-webfont.ttf') format('truetype'), + url('zocial-regular-webfont.svg#ZocialRegular') format('svg'); + unicode-range: U+0-U+10FFFF; +} + +/* Button structure */ + +.zocial, +a.zocial { + border: 1px solid #777; + border-color: rgba(0,0,0,0.2); + border-bottom-color: #333; + border-bottom-color: rgba(0,0,0,0.4); + color: #fff; + -moz-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9); + -webkit-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9); + box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9); + cursor: pointer; + display: inline-block; + font: bold 100%/2.1 "Lucida Grande", Tahoma, sans-serif; + padding: 0 .95em 0 0; + text-align: center; + text-decoration: none; + text-shadow: 0 1px 0 rgba(0,0,0,0.5); + white-space: nowrap; + + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + + position: relative; + + -moz-border-radius: .3em; + -webkit-border-radius: .3em; + border-radius: .3em; +} + +.zocial:before { + content: ""; + border-right: 0.075em solid rgba(0,0,0,0.1); + float: left; + font: 120%/1.65 zocial; + font-style: normal; + font-weight: normal; + margin: 0 0.5em 0 0; + padding: 0 0.5em; + text-align: center; + text-decoration: none; + text-transform: none; + + -moz-box-shadow: 0.075em 0 0 rgba(255,255,255,0.25); + -webkit-box-shadow: 0.075em 0 0 rgba(255,255,255,0.25); + box-shadow: 0.075em 0 0 rgba(255,255,255,0.25); + + -webkit-font-smoothing: antialiased; +} + +.zocial:active { + outline: none; /* outline is visible on :focus */ +} + +/* Buttons can be displayed as standalone icons by adding a class of "icon" */ + +.zocial.icon { + overflow: hidden; + max-width: 2.4em; + padding-left: 0; + padding-right: 0; + max-height: 2.15em; + white-space: nowrap; +} +.zocial.icon:before { + padding: 0; + width: 2em; + height: 2em; + + box-shadow: none; + border: none; +} + +/* Gradients */ + +.zocial { + background-image: -moz-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); + background-image: -ms-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); + background-image: -o-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.1)), color-stop(49%, rgba(255,255,255,.05)), color-stop(51%, rgba(0,0,0,.05)), to(rgba(0,0,0,.1))); + background-image: -webkit-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); + background-image: linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); +} + +.zocial:hover, .zocial:focus { + background-image: -moz-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); + background-image: -ms-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); + background-image: -o-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.15)), color-stop(49%, rgba(255,255,255,.15)), color-stop(51%, rgba(0,0,0,.1)), to(rgba(0,0,0,.15))); + background-image: -webkit-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); + background-image: linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); +} + +.zocial:active { + background-image: -moz-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); + background-image: -ms-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); + background-image: -o-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.1)), color-stop(30%, rgba(255,255,255,0)), color-stop(50%, transparent), to(rgba(0,0,0,.1))); + background-image: -webkit-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); + background-image: linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); +} + +/* Adjustments for light background buttons */ + +.zocial.bitcoin, +.zocial.cloudapp, +.zocial.dropbox, +.zocial.email, +.zocial.eventful, +.zocial.github, +.zocial.gmail, +.zocial.instapaper, +.zocial.itunes, +.zocial.ninetyninedesigns, +.zocial.openid, +.zocial.plancast, +.zocial.posterous, +.zocial.reddit, +.zocial.secondary, +.zocial.viadeo, +.zocial.weibo, +.zocial.wikipedia { + border: 1px solid #aaa; + border-color: rgba(0,0,0,0.3); + border-bottom-color: #777; + border-bottom-color: rgba(0,0,0,0.5); + -moz-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5); + -webkit-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5); + box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5); + text-shadow: 0 1px 0 rgba(255,255,255,0.8); +} + +/* :hover adjustments for light background buttons */ + +.zocial.bitcoin:focus, +.zocial.bitcoin:hover, +.zocial.dropbox:focus, +.zocial.dropbox:hover, +.zocial.email:focus, +.zocial.email:hover, +.zocial.eventful:focus, +.zocial.eventful:hover, +.zocial.github:focus, +.zocial.github:hover, +.zocial.gmail:focus, +.zocial.gmail:hover, +.zocial.instapaper:focus, +.zocial.instapaper:hover, +.zocial.itunes:focus, +.zocial.itunes:hover, +.zocial.ninetyninedesigns:focus, +.zocial.ninetyninedesigns:hover, +.zocial.openid:focus, +.zocial.openid:hover, +.zocial.plancast:focus, +.zocial.plancast:hover, +.zocial.posterous:focus, +.zocial.posterous:hover, +.zocial.reddit:focus, +.zocial.reddit:hover, +.zocial.secondary:focus, +.zocial.secondary:hover, +.zocial.twitter:focus, +.zocial.viadeo:focus, +.zocial.viadeo:hover, +.zocial.weibo:focus, +.zocial.weibo:hover, +.zocial.wikipedia:focus, +.zocial.wikipedia:hover { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.5)), color-stop(49%, rgba(255,255,255,0.2)), color-stop(51%, rgba(0,0,0,0.05)), to(rgba(0,0,0,0.15))); + background-image: -moz-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); + background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); + background-image: -o-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); + background-image: -ms-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); + background-image: linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); +} + +/* :active adjustments for light background buttons */ + +.zocial.bitcoin:active, +.zocial.dropbox:active, +.zocial.email:active, +.zocial.eventful:active, +.zocial.github:active, +.zocial.gmail:active, +.zocial.instapaper:active, +.zocial.itunes:active, +.zocial.ninetyninedesigns:active, +.zocial.openid:active, +.zocial.plancast:active, +.zocial.posterous:active, +.zocial.reddit:active, +.zocial.secondary:active, +.zocial.viadeo:active, +.zocial.weibo:active, +.zocial.wikipedia:active { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0)), color-stop(30%, rgba(255,255,255,0)), color-stop(50%, rgba(0,0,0,0)), to(rgba(0,0,0,0.1))); + background-image: -moz-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); + background-image: -webkit-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); + background-image: -o-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); + background-image: -ms-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); + background-image: linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); +} + +/* Button icon and color */ +/* Icon characters are stored in unicode private area */ +.zocial.amazon:before {content: "\E040";} +.zocial.android:before {content: "\E005";} +.zocial.angellist:before {content: "\E06C";} +.zocial.aol:before {content: "\E001";} +.zocial.appstore:before {content: "\E020";} +.zocial.bitcoin:before {content: "\E011"; color: #f7931a;} +.zocial.blogger:before {content: "\E021";} +.zocial.call:before {content: "\E016";} +.zocial.cal:before {content: "\E00D";} +.zocial.cart:before {content: "\E06A";} +.zocial.chrome:before {content: "\E03A";} +.zocial.cloudapp:before {content: "\E042";} +.zocial.creativecommons:before {content: "\E022";} +.zocial.delicious:before {content: "\E002";} +.zocial.digg:before {content: "\E01A";} +.zocial.disqus:before {content: "\E030";} +.zocial.dribbble:before {content: "\E023";} +.zocial.dropbox:before {content: "\E043"; color: #1f75cc;} +.zocial.email:before {content: "\E03C"; color: #312c2a;} +.zocial.eventasaurus:before {content: "\E055"; color: #9de428;} +.zocial.eventbrite:before {content: "\E05B";} +.zocial.eventful:before {content: "\E006"; color: #0066CC;} +.zocial.evernote:before {content: "\E024";} +.zocial.facebook:before {content: "\E045";} +.zocial.fivehundredpx:before {content: "\E00F"; color: #29b6ff;} +.zocial.flattr:before {content: "\E004";} +.zocial.flickr:before {content: "\E025";} +.zocial.forrst:before {content: "\E019"; color: #50894f;} +.zocial.foursquare:before {content: "\E013";} +.zocial.github:before {content: "\E046";} +.zocial.gmail:before {content: "\E04C"; color: #f00;} +.zocial.google:before {content: "\E026";} +.zocial.googleplay:before {content: "\E05E";} +.zocial.googleplus:before {content: "\E00A";} +.zocial.gowalla:before {content: "\E01F";} +.zocial.grooveshark:before {content: "\E017";} +.zocial.guest:before {content: "\E01E";} +.zocial.html5:before {content: "\E014";} +.zocial.ie:before {content: "\E015";} +.zocial.instagram:before {content: "\E06D";} +.zocial.instapaper:before {content: "\E028";} +.zocial.intensedebate:before {content: "\E05A";} +.zocial.itunes:before {content: "\E048"; color: #1a6dd2;} +.zocial.klout:before {content: "\E02A"; } +.zocial.lanyrd:before {content: "\E00C";} +.zocial.lastfm:before {content: "\E04B";} +.zocial.linkedin:before {content: "\E02B";} +.zocial.macstore:before {content: "\E03D";} +.zocial.meetup:before {content: "\E02C";} +.zocial.myspace:before {content: "\E03E";} +.zocial.ninetyninedesigns:before {content: "\E018"; color: #f50;} +.zocial.openid:before {content: "\E04E"; color: #ff921d;} +.zocial.opentable:before {content: "\E05F";} +.zocial.paypal:before {content: "\E003";} +.zocial.pinboard:before {content: "\E04D";} +.zocial.pinterest:before {content: "\E010";} +.zocial.plancast:before {content: "\E02F";} +.zocial.plurk:before {content: "\E049";} +.zocial.podcast:before {content: "\E03F";} +.zocial.posterous:before {content: "\E05D";} +.zocial.print:before {content: "\E06B";} +.zocial.quora:before {content: "\E050";} +.zocial.reddit:before {content: "\E01D"; color: red;} +.zocial.rss:before {content: "\E031";} +.zocial.scribd:before {content: "\E05C"; color: #00d5ea;} +.zocial.skype:before {content: "\E032";} +.zocial.smashing:before {content: "\E009";} +.zocial.songkick:before {content: "\E04A";} +.zocial.soundcloud:before {content: "\E052";} +.zocial.spotify:before {content: "\E01C";} +.zocial.stumbleupon:before {content: "\E00E";} +.zocial.tumblr:before {content: "\E053";} +.zocial.twitter:before {content: "\E033";} +.zocial.viadeo:before {content: "\E027"; color: #f59b20;} +.zocial.vimeo:before {content: "\E035";} +.zocial.weibo:before {content: "\E029"; color: #e6162d;} +.zocial.wikipedia:before {content: "\E00B";} +.zocial.windows:before {content: "\E036";} +.zocial.xing:before {content: "\E037"} +.zocial.wordpress:before {content: "\E056";} +.zocial.yahoo:before {content: "\E038";} +.zocial.yelp:before {content: "\E058";} +.zocial.youtube:before {content: "\E034";} + +/* Button background and text color */ + +.zocial.amazon {background-color: #ffad1d; color: #030037; text-shadow: 0 1px 0 rgba(255,255,255,0.5);} +.zocial.android {background-color: #a4c639;} +.zocial.angellist {background-color: #000;} +.zocial.aol {background-color: #f00;} +.zocial.appstore {background-color: #000;} +.zocial.bitcoin {background-color: #efefef; color: #4d4d4d;} +.zocial.blogger {background-color: #ee5a22;} +.zocial.call {background-color: #008000;} +.zocial.cal {background-color: #d63538;} +.zocial.cart {background-color: #333;} +.zocial.chrome {background-color: #006cd4;} +.zocial.cloudapp {background-color: #fff; color: #312c2a;} +.zocial.creativecommons {background-color: #000;} +.zocial.delicious {background-color: #3271cb;} +.zocial.digg {background-color: #164673;} +.zocial.disqus {background-color: #5d8aad;} +.zocial.dribbble {background-color: #ea4c89;} +.zocial.dropbox {background-color: #fff; color: #312c2a;} +.zocial.email {background-color: #f0f0eb; color: #312c2a;} +.zocial.eventasaurus {background-color: #192931; color: #fff;} +.zocial.eventbrite {background-color: #ff5616;} +.zocial.eventful {background-color: #fff; color: #47ab15;} +.zocial.evernote {background-color: #6bb130; color: #fff;} +.zocial.facebook {background-color: #4863ae;} +.zocial.fivehundredpx {background-color: #333;} +.zocial.flattr {background-color: #8aba42;} +.zocial.flickr {background-color: #ff0084;} +.zocial.forrst {background-color: #1e360d;} +.zocial.foursquare {background-color: #44a8e0;} +.zocial.github {background-color: #fbfbfb; color: #050505;} +.zocial.gmail {background-color: #efefef; color: #222;} +.zocial.google {background-color: #4e6cf7;} +.zocial.googleplay {background-color: #000;} +.zocial.googleplus {background-color: #dd4b39;} +.zocial.gowalla {background-color: #ff720a;} +.zocial.grooveshark {background-color: #111; color:#eee;} +.zocial.guest {background-color: #1b4d6d;} +.zocial.html5 {background-color: #ff3617;} +.zocial.ie {background-color: #00a1d9;} +.zocial.instapaper {background-color: #eee; color: #222;} +.zocial.instagram {background-color: #3f729b;} +.zocial.intensedebate {background-color: #0099e1;} +.zocial.klout {background-color: #e34a25;} +.zocial.itunes {background-color: #efefeb; color: #312c2a;} +.zocial.lanyrd {background-color: #2e6ac2;} +.zocial.lastfm {background-color: #dc1a23;} +.zocial.linkedin {background-color: #0083a8;} +.zocial.macstore {background-color: #007dcb} +.zocial.meetup {background-color: #ff0026;} +.zocial.myspace {background-color: #000;} +.zocial.ninetyninedesigns {background-color: #fff; color: #072243;} +.zocial.openid {background-color: #f5f5f5; color: #333;} +.zocial.opentable {background-color: #990000;} +.zocial.paypal {background-color: #fff; color: #32689a; text-shadow: 0 1px 0 rgba(255,255,255,0.5);} +.zocial.pinboard {background-color: blue;} +.zocial.pinterest {background-color: #c91618;} +.zocial.plancast {background-color: #e7ebed; color: #333;} +.zocial.plurk {background-color: #cf682f;} +.zocial.podcast {background-color: #9365ce;} +.zocial.posterous {background-color: #ffd959; color: #bc7134;} +.zocial.print {background-color: #f0f0eb; color: #222; text-shadow: 0 1px 0 rgba(255,255,255,0.8);} +.zocial.quora {background-color: #a82400;} +.zocial.reddit {background-color: #fff; color: #222;} +.zocial.rss {background-color: #ff7f25;} +.zocial.scribd {background-color: #231c1a;} +.zocial.skype {background-color: #00a2ed;} +.zocial.smashing {background-color: #ff4f27;} +.zocial.songkick {background-color: #ff0050;} +.zocial.soundcloud {background-color: #ff4500;} +.zocial.spotify {background-color: #60af00;} +.zocial.stumbleupon {background-color: #eb4924;} +.zocial.tumblr {background-color: #374a61;} +.zocial.twitter {background-color: #46c0fb;} +.zocial.viadeo {background-color: #fff; color: #000;} +.zocial.vimeo {background-color: #00a2cd;} +.zocial.weibo {background-color: #faf6f1; color: #000;} +.zocial.wikipedia {background-color: #fff; color: #000;} +.zocial.windows {background-color: #0052a4; color: #fff;} +.zocial.wordpress {background-color: #464646;} +.zocial.xing {background-color: #0A5D5E;} +.zocial.yahoo {background-color: #a200c2;} +.zocial.yelp {background-color: #e60010;} +.zocial.youtube {background-color: #f00;} + +/* +The Miscellaneous Buttons +These button have no icons and can be general purpose buttons while ensuring consistent button style +Credit to @guillermovs for suggesting +*/ + +.zocial.primary, .zocial.secondary {margin: 0.1em 0; padding: 0 1em;} +.zocial.primary:before, .zocial.secondary:before {display: none;} +.zocial.primary {background-color: #333;} +.zocial.secondary {background-color: #f0f0eb; color: #222; text-shadow: 0 1px 0 rgba(255,255,255,0.8);} + +/* Any browser-specific adjustments */ + +button:-moz-focus-inner { + border: 0; + padding: 0; +} + + diff --git a/static/lib/zocial/sample.html b/static/lib/zocial/sample.html new file mode 100644 index 00000000000..98dec554ff1 --- /dev/null +++ b/static/lib/zocial/sample.html @@ -0,0 +1,265 @@ + + + + + Zocial CSS3 Buttons + + + + + + + + + + + + + Sign in with Google+ + + Sign in with LinkedIn + +

The Cool Kids

+ Sync with Dropbox + Clip to Evernote + Follow me on Forrst + Sign in with Dribbble + Sign in to CloudApp + Fork me on Github + Play on Spotify + Read It Later + Follow me on Soundcloud + Follow me on Tumblr + Read on Smashing Magazine + Available on iTunes + Available on the App Store + Available on the Mac App Store + Available on Android Market + Follow me on Pinterest + Follow me on Quora + Attend on Lanyrd + +

The Not-So-Cool But Have-To-Be-Invited Kids

+ Pay with Paypal + Sign in with Amazon + Call me on Skype + Sign in with Last.fm + Write a review on Yelp + Check in with foursquare + Influence with Klout + +

The Older (but Solid) Kids

+ View on Wikipedia + Sign in with Disqus + Sign in with IntenseDebate + Sign in with Google + Sign in with Gmail + Upload to Vimeo + Read more on Scribd + Subscribe on YouTube + Sign in with WordPress + Sign in with Songkick + Sign in with Posterous + Sign in with Eventbrite + Tip with Flattr + Follow me on Plancast + + +

The Kids That Kinda Smell but Some People Don't Mind

+ Submit resume for CEO + Download Internet Explorer 5 + Report bugs with Meetup.com + Learn how-to-use OpenID + Register now for HTML6 + Chat with your parents + +

The Kids That Nobody Normally Notices

+ Sign in as guest + View Creative Commons Licence + Subscribe to RSS + Add to Chrome + +

The He's-My-Son-So-Of-Course-He's-Here Kid

+ Sign up for Eventasaurus + +

Kids By Request

+ Join me on Weibo + Sign in with Plurk + Play on Grooveshark + Post on Blogger + Sign in with Viadeo + Subscribe to this Podcast + View Portfolio on 500px + Bitcoin accepted here + View Portfolio on 99Designs + Bookmark with Pinboard + Stumble! + Find me on Myspace + Sign in with Windows Live + Find Events with Eventful + Sign in with Xing + Upload to Flickr + Sign in with Del.icio.us + Download from Google Play + Reserve with OpenTable + Digg this + Share on Reddit + Fund us on AngelList + Sign-in with Instagram + +

The Multi-Purpose Kids (Credit: Pictos Icons)

+ + Call a phone + + Add to calendar + Add to cart + Print this page + Primary action + Secondary action + + +

Icon versions of the above

+ + Sign in with Google+ + + Sign in with Google + Sign in with LinkedIn + Pay with Paypal + Sign in with Amazon + Sync with Dropbox + Clip to Evernote + Call me on Skype + Sign in as guest + Play on Spotify + Sign in with Last.fm + Sign in with Songkick + Follow me on Forrst + Sign in with Dribbble + Sign in to CloudApp + Fork me on Github + Follow me on Pinterest + Follow me on Quora + Bookmark with Pinboard + Attend on Lanyrd + Download on iTunes + Download on Android + Sign in with Disqus + Sign in with Yahoo + Upload to Vimeo + Add to Chrome + Get a new browser + Made from HTML5 + Read It Later + Read more on Scribd + View on Wikipedia + Tip with Flattr + Follow me on Tumblr + Subscribe to my Posterous + Check in with Gowalla + Check in with foursquare + Write a review on Yelp + Follow me on Soundcloud + Read on Smashing Magazine + Sign in with WordPress + Sign in with IntenseDebate + Sign in with OpenID + Sign in with Gmail + Sign in with Eventbrite + Sign in with Eventasaurus + Sign in with Meetup.com + Sign in with AIM + Follow me on Plancast + Subscribe on YouTube + Available on the Mac App Store + View Creative Commons Licence + Subscribe to RSS + Follow me on Weibo + Follow me on Plurk + Follow me on Grooveshark + Post on Blogger + Sign in with Viadeo + Subscribe to this Podcast + View Portfolio on 500px + Bitcoin accepted here + View Portfolio on 99Designs + Stumble! + Download on iTunes + Find me on Myspace + Sign in with Windows Live + Find Events with Eventful + Influence with Klout + Sign in with Xing + Upload to Flickr + Sign in with Del.icio.us + Download from Google Play + Reserve with OpenTable + Digg this + Share on Reddit + Fund us on AngelList + Sign-in with Instagram + Call a phone + + Add to calendar + Print this page + Add to cart + + + +

Thanks to @guillermovs, @kamens, @vizualover, and @leaverou for code refinements and suggestions.

+ +

Massive thanks to @drewwilson for making the multi-purpose buttons possible with his incredible Pictos icons.

+ +

See code samples at zocial.smcllns.com — ask questions to @smcllns

+ + + + + + + + diff --git a/static/src/css/auth_saml.css b/static/src/css/auth_saml.css new file mode 100644 index 00000000000..475ec7d7b36 --- /dev/null +++ b/static/src/css/auth_saml.css @@ -0,0 +1,34 @@ +.openerp .oe_application .zocial { + font: white; +} + +.openerp .zocial.openerp:before { + content: "\E02E"; + font-style: italic; + text-shadow: 0 1px 1px black; +} + +.openerp a.zocial.openerp { + border: 1px solid #222222; + color: white; + margin: 0; + background-color: #b92020; + background-image: -webkit-gradient(linear, left top, left bottom, from(#b92020), to(#600606)); + background-image: -webkit-linear-gradient(top, #b92020, #600606); + background-image: -moz-linear-gradient(top, #b92020, #600606); + background-image: -ms-linear-gradient(top, #b92020, #600606); + background-image: -o-linear-gradient(top, #b92020, #600606); + background-image: linear-gradient(to bottom, #b92020, #600606); + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; + text-shadow: none; + font-weight: normal; +} + +.openerp .oe_login .oe_saml_provider_login_button { + margin-top: 4px; +} diff --git a/static/src/js/auth_saml.js b/static/src/js/auth_saml.js new file mode 100644 index 00000000000..57437942c53 --- /dev/null +++ b/static/src/js/auth_saml.js @@ -0,0 +1,73 @@ +openerp.auth_saml = function(instance) { + var _t = instance.web._t, + _lt = instance.web._lt; + var QWeb = instance.web.qweb; + + instance.web.Login.include({ + start: function(parent, params) { + var self = this; + var d = this._super.apply(this, arguments); + this.$el.hide(); + this.$el.on('click', 'a.zocial', this.on_saml_sign_in); + this.saml_providers = []; + if(this.params.saml_error === 1) { + this.do_warn(_t("Sign up error"),_t("Sign up is not allowed on this database."), true); + } else if(this.params.saml_error === 2) { + this.do_warn(_t("Authentication error"),_t("Access Denied"), true); + } else if(this.params.saml_error === 3) { + this.do_warn(_t("Authentication error"),_t("You do not have access to this database or your invitation has expired. Please ask for an invitation and be sure to follow the link in your invitation email."), true); + } + return d.done(this.do_saml_load).fail(function() { + self.do_saml_load([]); + }); + }, + on_db_loaded: function(result) { + this._super.apply(this, arguments); + this.$("form [name=db]").change(this.do_saml_load); + }, + do_saml_load: function() { + var db = this.$("form [name=db]").val(); + if (db) { + this.rpc("/auth_saml/list_providers", { dbname: db }).done(this.on_saml_loaded); + } else { + this.$el.show(); + } + }, + on_saml_loaded: function(result) { + this.saml_providers = result; + var params = $.deparam($.param.querystring()); + if (this.saml_providers.length === 1 && params.type === 'signup') { + this.do_saml_sign_in(this.saml_providers[0]); + } else { + this.$el.show(); + this.$('.oe_saml_provider_login_button').remove(); + var buttons = QWeb.render("auth_saml.Login.button",{"widget":this}); + this.$(".oe_login_pane form ul").after(buttons); + } + }, + on_saml_sign_in: function(ev) { + ev.preventDefault(); + var index = $(ev.target).data('index'); + var provider = this.saml_providers[index]; + return this.do_saml_sign_in(provider); + }, + do_saml_sign_in: function(provider) { + var state = this._saml_state(provider); + var params = { + RelayState: JSON.stringify(state), + }; + var url = provider.auth_req + "&" + $.param(params); + // redirect to the saml idp + instance.web.redirect(url); + }, + _saml_state: function(provider) { + // return the state object sent back with the redirected uri + var dbname = this.$("form [name=db]").val(); + return { + d: dbname, + p: provider.id, + }; + }, + }); + +}; diff --git a/static/src/xml/auth_saml.xml b/static/src/xml/auth_saml.xml new file mode 100644 index 00000000000..faca9ea961d --- /dev/null +++ b/static/src/xml/auth_saml.xml @@ -0,0 +1,8 @@ + + + + + + + + From f0d00eba47fd7231794f7145b7b5cc6f989f232a Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 27 Feb 2014 11:14:44 +0100 Subject: [PATCH 02/93] MASSIVE commit. Working POC. This is just a POC. We'll need a lot of cleaning --- auth_saml/__openerp__.py | 2 ++ auth_saml/res_config.py | 10 +++++----- auth_saml/res_users.py | 6 +++++- res_users.xml | 3 +-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 8f66b1f76e4..c01a2d61a84 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -35,6 +35,8 @@ 'depends': ['base', 'web', 'base_setup'], 'data': [ 'auth_saml_data.xml', + 'res_users.xml', + 'res_config.xml', 'auth_saml_view.xml', 'security/ir.model.access.csv' ], diff --git a/auth_saml/res_config.py b/auth_saml/res_config.py index 75f91ad956c..83f41bf4ca9 100644 --- a/auth_saml/res_config.py +++ b/auth_saml/res_config.py @@ -30,12 +30,12 @@ class base_config_settings(osv.TransientModel): _inherit = 'base.config.settings' _columns = { - 'auth_saml_authentic_enabled': fields.boolean( - 'Allow users to sign in with Google' + 'auth_saml_local_enabled': fields.boolean( + 'Allow users to sign in with a Local Authentic' ), } - def get_oauth_providers(self, cr, uid, fields, context=None): + def get_saml_providers(self, cr, uid, fields, context=None): local_id = self.pool.get('ir.model.data').get_object_reference( cr, uid, 'auth_saml', 'provider_local' )[1] @@ -45,10 +45,10 @@ def get_oauth_providers(self, cr, uid, fields, context=None): ) return { - 'auth_oauth_google_enabled': rl[0]['enabled'], + 'auth_saml_local_enabled': rl[0]['enabled'], } - def set_oauth_providers(self, cr, uid, ids, context=None): + def set_saml_providers(self, cr, uid, ids, context=None): local_id = self.pool.get('ir.model.data').get_object_reference( cr, uid, 'auth_saml', 'provider_local' )[1] diff --git a/auth_saml/res_users.py b/auth_saml/res_users.py index ba24c94e9a5..c3604138757 100644 --- a/auth_saml/res_users.py +++ b/auth_saml/res_users.py @@ -17,9 +17,13 @@ class res_users(osv.Model): string='SAML Provider', ), 'saml_uid': fields.char( - 'OAuth User ID', + 'SAML User ID', help="SAML Provider user_id", ), + 'saml_access_token': fields.char( + 'Current SAML token for this user', + help="The current SAML token in use", + ), } _sql_constraints = [ diff --git a/res_users.xml b/res_users.xml index 8e5e872a711..28e905146fb 100644 --- a/res_users.xml +++ b/res_users.xml @@ -10,9 +10,8 @@ - + - From 8f24011596f73e066547132607b320d6b00fa29e Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 27 Feb 2014 15:04:30 +0100 Subject: [PATCH 03/93] Now with proper request encoding!!! we will now test the signature test --- auth_saml/auth_saml.py | 16 +++++++-------- auth_saml/controllers/main.py | 38 ++++++++++++++++++++++++++++++++--- static/src/js/auth_saml.js | 9 ++++----- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/auth_saml/auth_saml.py b/auth_saml/auth_saml.py index d748b5ab599..659ee232d59 100644 --- a/auth_saml/auth_saml.py +++ b/auth_saml/auth_saml.py @@ -1,5 +1,6 @@ from openerp.osv import osv, fields import lasso +import simplejson class auth_saml_provider(osv.osv): @@ -10,7 +11,7 @@ class auth_saml_provider(osv.osv): _order = 'name' def _get_lasso_for_provider(self, cr, uid, provider_id, context=None): - print cr, uid, provider_id, context + #print cr, uid, provider_id, context provider = self.browse(cr, uid, provider_id, context=context) # TODO: we should cache those results somewhere because it is @@ -25,31 +26,28 @@ def _get_lasso_for_provider(self, cr, uid, provider_id, context=None): ) return lasso.Login(server) - def _get_auth_request(self, cr, uid, ids, name, args, context=None): + def _get_auth_request(self, cr, uid, id_, state, context=None): """build an authentication request and give it back to our client WARNING: this method cannot be used for multiple ids """ - result = {} - login = self._get_lasso_for_provider(cr, uid, ids[0], context=context) + login = self._get_lasso_for_provider(cr, uid, id_, context=context) # ! -- this is the part that MUST be performed on each call and # cannot be cached login.initAuthnRequest() login.request.nameIdPolicy.format = None login.request.nameIdPolicy.allowCreate = True + login.msgRelayState = simplejson.dumps(state) login.buildAuthnRequestMsg() # msgUrl is a fully encoded url ready for redirect use - result[ids[0]] = login.msgUrl - #print "*" * 35 - #print result - return result + # obtained after the buildAuthnRequestMsg() call + return login.msgUrl _columns = { # Name of the OAuth2 entity, authentic, xcg... 'name': fields.char('Provider name'), 'idp_metadata': fields.text('IDP Configuration'), - 'auth_req': fields.function(_get_auth_request), 'sp_metadata': fields.text('SP Configuration'), 'sp_pkey': fields.text( 'Private key of our service provider (this openerpserver)' diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index dccbc935679..bc1f32a2792 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -42,8 +42,36 @@ def wrapper(self, req, **kw): class SAMLController(oeweb.Controller): _cp_path = '/auth_saml' + @oeweb.jsonrequest + def get_auth_request(self, req, relaystate): + """state is the JSONified state object and we need to pass + it inside our request as the RelayState argument + """ + state = simplejson.loads(relaystate) + + dbname = state['d'] + provider_id = state['p'] + context = state.get('c', {}) + + registry = RegistryManager.get(dbname) + provider_osv = registry.get('auth.saml.provider') + + auth_request = None + + try: + with registry.cursor() as cr: + auth_request = provider_osv._get_auth_request( + cr, SUPERUSER_ID, provider_id, state, context=context + ) + + except Exception, e: + _logger.exception("SAML2: %s" % str(e)) + + return {'auth_request': auth_request} + @oeweb.jsonrequest def list_providers(self, req, dbname): + l = [] try: registry = RegistryManager.get(dbname) with registry.cursor() as cr: @@ -56,7 +84,6 @@ def list_providers(self, req, dbname): except Exception, e: _logger.exception("SAML2: %s" % str(e)) - l = [] return l @@ -73,6 +100,7 @@ def signin(self, req, **kw): provider = state['p'] context = state.get('c', {}) registry = RegistryManager.get(dbname) + with registry.cursor() as cr: try: u = registry.get('res.users') @@ -92,13 +120,17 @@ def signin(self, req, **kw): except AttributeError, e: print e # auth_signup is not installed - _logger.error("auth_signup not installed on database %s: saml sign up cancelled." % (dbname,)) + _logger.error("auth_signup not installed on database " + "%s: saml sign up cancelled." % (dbname,)) url = "/#action=login&saml_error=1" except openerp.exceptions.AccessDenied: # saml credentials not valid, # user could be on a temporary session - _logger.info('SAML2: access denied, redirect to main page in case a valid session exists, without setting cookies') + _logger.info('SAML2: access denied, redirect to main page ' + 'in case a valid session exists, ' + 'without setting cookies') + url = "/#action=login&saml_error=3" redirect = werkzeug.utils.redirect(url, 303) redirect.autocorrect_location_header = False diff --git a/static/src/js/auth_saml.js b/static/src/js/auth_saml.js index 57437942c53..7fd37458919 100644 --- a/static/src/js/auth_saml.js +++ b/static/src/js/auth_saml.js @@ -53,12 +53,11 @@ openerp.auth_saml = function(instance) { }, do_saml_sign_in: function(provider) { var state = this._saml_state(provider); - var params = { - RelayState: JSON.stringify(state), - }; - var url = provider.auth_req + "&" + $.param(params); + this.rpc("/auth_saml/get_auth_request", { relaystate: JSON.stringify(state) }).done(this.on_request_loaded); + }, + on_request_loaded: function(result) { // redirect to the saml idp - instance.web.redirect(url); + instance.web.redirect(result.auth_request); }, _saml_state: function(provider) { // return the state object sent back with the redirected uri From 5da4193b778302306cc640c066a954b684359141 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 27 Feb 2014 16:21:15 +0100 Subject: [PATCH 04/93] Fixed issue for database that does not have saml providers --- auth_saml/controllers/main.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index bc1f32a2792..29c2be50db0 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -76,11 +76,14 @@ def list_providers(self, req, dbname): registry = RegistryManager.get(dbname) with registry.cursor() as cr: providers = registry.get('auth.saml.provider') - l = providers.read( - cr, SUPERUSER_ID, providers.search( - cr, SUPERUSER_ID, [('enabled', '=', True)] + if providers: + l = providers.read( + cr, SUPERUSER_ID, providers.search( + cr, SUPERUSER_ID, [('enabled', '=', True)] + ) ) - ) + else: + l = [] except Exception, e: _logger.exception("SAML2: %s" % str(e)) From d159ce6f9be86c2b0b2cc5d78ac368b90d27d7ed Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Thu, 27 Feb 2014 16:24:13 +0100 Subject: [PATCH 05/93] [security] Fixed values --- auth_saml/security/ir.model.access.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/security/ir.model.access.csv b/auth_saml/security/ir.model.access.csv index 65fd2f54bbf..a8fbea84b9a 100644 --- a/auth_saml/security/ir.model.access.csv +++ b/auth_saml/security/ir.model.access.csv @@ -1,2 +1,2 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_auth_oauth_provider,auth_oauth_provider,model_auth_oauth_provider,base.group_system,1,1,1,1 +access_auth_saml_provider,auth_saml_provider,model_auth_saml_provider,base.group_system,1,1,1,1 From a43dc3a3c85cf04f9ed790d0808f19e2f0da2fb5 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Tue, 11 Mar 2014 11:48:02 +0100 Subject: [PATCH 06/93] Added a starting point rst documentation... Not finished YET. --- auth_saml/documentation/index.rst | 86 +++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 auth_saml/documentation/index.rst diff --git a/auth_saml/documentation/index.rst b/auth_saml/documentation/index.rst new file mode 100644 index 00000000000..2f2339d7e73 --- /dev/null +++ b/auth_saml/documentation/index.rst @@ -0,0 +1,86 @@ +SAML Authentication with Authentic2 +=================================== + +This is quick howto to help setup a service provider that will be able +to use the IDP from Authentic2 + +We will mostly cover how to setup your rsa keys and certificates + + +Creating the certs +------------------ + +Use easy-rsa from the easy-rsa package (or from the openvpn project) + +Example script below with comment saying what you should do between each +command:: + + #clean your vars + + source ./vars + + ./build-dh + ./pkitool --initca + + #change your vars to math a new client cert + + source ./vars + + ./pkitool myclient + + +Congratulations, you now have a client certificate signed by a shiny new +CA under you own private control. + +Configuring authentic +--------------------- + +We will not describe how to compile requirements nor start an authentic server. + +Just log into your authentic admin panel:: + + https://myauthenticserver/admin + + +and create a new "liberty provider". + +You'll need to create a metadata xml file from a template (TODO) + +You'll need to make sure it is activated and that the default protocol rules +are applied (ie: the requests are signed and signatures are verified) + +Configuring OpenERP +------------------- + +After installing the auth_saml module you should have new configuration +options in the admin panel. + +You'll see a demonstration setup that points to a localhost:8000 +identity provider (IDP). + +DO NOT USE THIS PROVIDER!!! This is a demonstration only setup and contains +a private key that everyone can see in the source code of this module... + +Using a private key when it has been compromised (ie: shared with the world) +is a really bad idea for an authentication system. + +I'll say it again just to make sure you understand:: + + DO NOT USE THE DEMONSTRATION CONFIGURATION AND KEYS + IN ANY SERVER OTHER THAN A DEMO LOCALHOST MACHINE FOR + TESTING PURPOSES. + + DOING SO WILL SURELY LEAD TO YOUR IDENTIY BEING STOLEN, YOUR SERVERS + BEING ROOTED AND MORE SERIOUSLY TO THE END OF THE WORLD AND OTHER + SUCH CALAMITIES YOU DON'T WANT TO EXPERIENCE TOO EARLY... + +Seriously I hope you got the message loud and clear... Don't do that. +Follow the creating certs guide just above. + +Copy the metadata from your identity provider:: + + wget https://myauthenticserver/idp/saml2/metadata + +and make sure the URLs point where they should. Edit the file if necessary. + +Then save its content into the corresponding box in the openerp SAML2 Provider form. From aa92302327cc7413aa14622c217c700d1c9a5c2d Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 13 Mar 2014 16:03:36 +0100 Subject: [PATCH 07/93] Fixed an information leak... This is an important update --- auth_saml/controllers/main.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 29c2be50db0..b22b15cb2fd 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -80,7 +80,15 @@ def list_providers(self, req, dbname): l = providers.read( cr, SUPERUSER_ID, providers.search( cr, SUPERUSER_ID, [('enabled', '=', True)] - ) + ), + [ + "id", + "name", + "enabled", + "css_class", + "body", + "sequence", + ], ) else: l = [] From 932274ad8077945c7ebd9c7dd6960353a4ea5352 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 13 Mar 2014 16:27:55 +0100 Subject: [PATCH 08/93] Added css and body to the default values --- auth_saml/auth_saml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/auth_saml/auth_saml.py b/auth_saml/auth_saml.py index 659ee232d59..e5265a504d5 100644 --- a/auth_saml/auth_saml.py +++ b/auth_saml/auth_saml.py @@ -60,4 +60,6 @@ def _get_auth_request(self, cr, uid, id_, state, context=None): _defaults = { 'enabled': False, + 'css_class': 'zocial saml', + 'body': 'Log in with Authentic', } From 144990c9acb2b0b7a7e41358ff370147091004e2 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Mon, 24 Mar 2014 13:51:34 +0100 Subject: [PATCH 09/93] Try to use a proper url redirect instead of just the barebone window.location assignation done by OE --- static/src/js/auth_saml.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/src/js/auth_saml.js b/static/src/js/auth_saml.js index 7fd37458919..1b3983fda48 100644 --- a/static/src/js/auth_saml.js +++ b/static/src/js/auth_saml.js @@ -57,7 +57,8 @@ openerp.auth_saml = function(instance) { }, on_request_loaded: function(result) { // redirect to the saml idp - instance.web.redirect(result.auth_request); + //instance.web.redirect(result.auth_request); + window.location.replace(result.auth_request); }, _saml_state: function(provider) { // return the state object sent back with the redirected uri From 7f55f249fdfeb86fad49bc2438fb792f55b087d5 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Mon, 24 Mar 2014 14:25:09 +0100 Subject: [PATCH 10/93] Sample local provider is now disabled by default to avoid update issues on production servers when admins have removed the configuration entry instead of disabling it. --- auth_saml_data.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml_data.xml b/auth_saml_data.xml index 9fbdd06fd42..a4af56921e0 100644 --- a/auth_saml_data.xml +++ b/auth_saml_data.xml @@ -80,7 +80,7 @@ sIbngHtWnT36PTZS zocial saml Log in with Authentic - + From fc44b3b084f402d7171f59dfe841bd38760b703a Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Mon, 24 Mar 2014 15:02:37 +0100 Subject: [PATCH 11/93] Added a specific case for clients coming to the singin url without the proper relaystate --- auth_saml/controllers/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index b22b15cb2fd..e8a4f6cc044 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -106,6 +106,17 @@ def signin(self, req, **kw): """ saml_response = kw.get('SAMLResponse', None) + if not kw.get('RelayState', None): + # here we are in front of a client that went through + # some routes that "lost" its relaystate... this can happen + # if the client visited his IDP and successfully logged in + # then the IDP gave him a portal with his available applications + # but the provided link does not include the necessary relaystate + url = "/?type=signup" + redirect = werkzeug.utils.redirect(url, 303) + redirect.autocorrect_location_header = True + return redirect + state = simplejson.loads(kw['RelayState']) dbname = state['d'] provider = state['p'] From 188475099051553cc345f87547f8084306414d87 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Mon, 31 Mar 2014 13:06:11 +0200 Subject: [PATCH 12/93] Added French translation --- auth_saml/i18n/fr.po | 188 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 auth_saml/i18n/fr.po diff --git a/auth_saml/i18n/fr.po b/auth_saml/i18n/fr.po new file mode 100644 index 00000000000..8eb3bb0b052 --- /dev/null +++ b/auth_saml/i18n/fr.po @@ -0,0 +1,188 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * auth_saml +# Vincent Lhote-Hatakeyama , 2014. +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 7.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-03-31 10:54+0000\n" +"PO-Revision-Date: 2014-03-31 13:05+0200\n" +"Last-Translator: Vincent Lhote-Hatakeyama \n" +"Language-Team: XCG Consulting\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Virtaal 0.7.1\n" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_base_config_settings +msgid "base.config.settings" +msgstr "base.config.settings" + +#. module: auth_saml +#: field:res.users,saml_access_token:0 +msgid "Current SAML token for this user" +msgstr "Jeton SAML courant de l’utilisateur" + +#. module: auth_saml +#. openerp-web +#: code:addons/auth_saml/static/src/js/auth_saml.js:14 +#, python-format +msgid "Sign up error" +msgstr "Erreur d’inscription" + +#. module: auth_saml +#: model:ir.ui.menu,name:auth_saml.menu_saml_providers +msgid "SAML Providers" +msgstr "Fournisseurs SAML" + +#. module: auth_saml +#: field:auth.saml.provider,css_class:0 +msgid "CSS class" +msgstr "Classe CSS" + +#. module: auth_saml +#: field:auth.saml.provider,body:0 +msgid "Body" +msgstr "Corps" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users +msgid "Users" +msgstr "Utilisateurs" + +#. module: auth_saml +#: field:auth.saml.provider,sequence:0 +msgid "unknown" +msgstr "inconnu" + +#. module: auth_saml +#. openerp-web +#: code:addons/auth_saml/static/src/js/auth_saml.js:16 +#: code:addons/auth_saml/static/src/js/auth_saml.js:18 +#, python-format +msgid "Authentication error" +msgstr "Erreur d’authentification" + +#. module: auth_saml +#: help:res.users,saml_access_token:0 +msgid "The current SAML token in use" +msgstr "Le jeton SAML courant en cours d’utilisation" + +#. module: auth_saml +#: view:base.config.settings:0 +msgid "http://localhost:8000" +msgstr "http://localhost :8000" + +#. module: auth_saml +#: view:base.config.settings:0 +msgid "running on" +msgstr "en fonctionnement sur" + +#. module: auth_saml +#: view:base.config.settings:0 +msgid "You must have an" +msgstr "Vous devez avoir un" + +#. module: auth_saml +#: view:base.config.settings:0 +msgid "authentic2 server" +msgstr "serveur authentic2" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_provider +msgid "SAML2 provider" +msgstr "Fournisseur SAML2" + +#. module: auth_saml +#. openerp-web +#: code:addons/auth_saml/static/src/js/auth_saml.js:16 +#, python-format +msgid "Access Denied" +msgstr "Accès refusé" + +#. module: auth_saml +#: field:base.config.settings,auth_saml_local_enabled:0 +msgid "Allow users to sign in with a Local Authentic" +msgstr "Autoriser les utilisateurs à s’inscrire avec un Authentic local" + +#. module: auth_saml +#: field:auth.saml.provider,sp_metadata:0 +msgid "SP Configuration" +msgstr "Configuration du FS" + +#. module: auth_saml +#: field:res.users,saml_provider_id:0 +msgid "SAML Provider" +msgstr "Fournisseur SAML" + +#. module: auth_saml +#: field:auth.saml.provider,sp_pkey:0 +msgid "Private key of our service provider (this openerpserver)" +msgstr "Clef privée de notre fournisseur de service (ce serveur OpenERP)" + +#. module: auth_saml +#: field:auth.saml.provider,name:0 +msgid "Provider name" +msgstr "Nom du fournisseur" + +#. module: auth_saml +#: view:auth.saml.provider:0 +msgid "arch" +msgstr "arch" + +#. module: auth_saml +#: sql_constraint:res.users:0 +msgid "SAML UID must be unique per provider" +msgstr "L’identifiant SAML doit être unique par fournisseur" + +#. module: auth_saml +#: field:res.users,saml_uid:0 +msgid "SAML User ID" +msgstr "Identifiant utilisateur SAML" + +#. module: auth_saml +#: model:ir.actions.act_window,name:auth_saml.action_saml_provider +msgid "Providers" +msgstr "Fournisseurs" + +#. module: auth_saml +#: help:res.users,saml_uid:0 +#, fuzzy +msgid "SAML Provider user_id" +msgstr "Fournisseur SAML user_id" + +#. module: auth_saml +#: view:res.users:0 +msgid "SAML" +msgstr "SAML" + +#. module: auth_saml +#: field:auth.saml.provider,idp_metadata:0 +msgid "IDP Configuration" +msgstr "Configuration FI" + +#. module: auth_saml +#: field:auth.saml.provider,enabled:0 +msgid "Allowed" +msgstr "Autorisé" + +#. module: auth_saml +#. openerp-web +#: code:addons/auth_saml/static/src/js/auth_saml.js:14 +#, python-format +msgid "Sign up is not allowed on this database." +msgstr "L’inscription n’est pas autorisée sur cette base de donnée." + +#. module: auth_saml +#. openerp-web +#: code:addons/auth_saml/static/src/js/auth_saml.js:18 +#, python-format +msgid "You do not have access to this database or your invitation has expired. Please ask for an invitation and be sure to follow the link in your invitation email." +msgstr "" +"Vous n’avez pas accès à cette base de donnée ou votre invitation a expirée. " +"Demandez une invitation et assurez-vous de suivre le lien dans le courriel " +"d’invitation." From f54879e9ca7ad2859c09dfee7c26ae40789e401d Mon Sep 17 00:00:00 2001 From: Alexandre Brun Date: Fri, 4 Apr 2014 21:21:28 +0200 Subject: [PATCH 13/93] Bumped version to 1.0(stable) --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index c01a2d61a84..74b9417b206 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -27,7 +27,7 @@ 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. -============================================= +=================================== """, 'author': 'XCG Consulting s.a.s.', 'maintainer': 'XCG Consulting s.a.s.', From ff13d1d946f0eecfc6f992db3c39a3e51a555130 Mon Sep 17 00:00:00 2001 From: Alexandre Brun Date: Fri, 4 Apr 2014 21:21:46 +0200 Subject: [PATCH 14/93] Added tag TAG_1.0 for changeset 0971293545c0 --- .hgtags | 1 + 1 file changed, 1 insertion(+) create mode 100644 .hgtags diff --git a/.hgtags b/.hgtags new file mode 100644 index 00000000000..cd0fe853924 --- /dev/null +++ b/.hgtags @@ -0,0 +1 @@ +0971293545c0e5088ae6a608f9c653d3e687a69e TAG_1.0 From 258cbf87818366fbaaa2f6864c3e3ec98db7cdba Mon Sep 17 00:00:00 2001 From: Alexandre Brun Date: Fri, 4 Apr 2014 21:22:24 +0200 Subject: [PATCH 15/93] Bumped version to 1.0.1(dev) --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 74b9417b206..1122d50569f 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -23,7 +23,7 @@ { 'name': 'Saml2 Authentication', - 'version': '1.0', + 'version': '1.0.1', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. From 9d3ffe9e46a153412f3d44e408aace53aa299e53 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Tue, 15 Apr 2014 10:24:22 +0200 Subject: [PATCH 16/93] Fixes #1 --- auth_saml/__openerp__.py | 1 - auth_saml/res_config.py | 62 ---------------------------------------- res_config.xml | 29 ------------------- 3 files changed, 92 deletions(-) delete mode 100644 auth_saml/res_config.py delete mode 100644 res_config.xml diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 1122d50569f..79016a4b4fc 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -36,7 +36,6 @@ 'data': [ 'auth_saml_data.xml', 'res_users.xml', - 'res_config.xml', 'auth_saml_view.xml', 'security/ir.model.access.csv' ], diff --git a/auth_saml/res_config.py b/auth_saml/res_config.py deleted file mode 100644 index 83f41bf4ca9..00000000000 --- a/auth_saml/res_config.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# XCG Consulting Group -# Copyright (C) 2010-2014 XCG Consulting s.a.s -# -# -############################################################################## - -from openerp.osv import osv, fields - -#import logging -#_logger = logging.getLogger(__name__) - - -class base_config_settings(osv.TransientModel): - _inherit = 'base.config.settings' - - _columns = { - 'auth_saml_local_enabled': fields.boolean( - 'Allow users to sign in with a Local Authentic' - ), - } - - def get_saml_providers(self, cr, uid, fields, context=None): - local_id = self.pool.get('ir.model.data').get_object_reference( - cr, uid, 'auth_saml', 'provider_local' - )[1] - - rl = self.pool.get('auth.saml.provider').read( - cr, uid, [local_id], ['enabled'], context=context - ) - - return { - 'auth_saml_local_enabled': rl[0]['enabled'], - } - - def set_saml_providers(self, cr, uid, ids, context=None): - local_id = self.pool.get('ir.model.data').get_object_reference( - cr, uid, 'auth_saml', 'provider_local' - )[1] - - config = self.browse(cr, uid, ids[0], context=context) - - rl = { - 'enabled': config.auth_saml_local_enabled, - } - - self.pool.get('auth.saml.provider').write(cr, uid, [local_id], rl) diff --git a/res_config.xml b/res_config.xml deleted file mode 100644 index 1e7ce6c64e5..00000000000 --- a/res_config.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - base.config.settings.saml - base.config.settings - - - -
-
-
- -
-
-
- You must have an authentic2 server running on - http://localhost:8000
-
-
-
-
-
-
-
- -
-
From b3ba0e413f816449646fdb9240874efa9fb5dbc6 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Tue, 6 May 2014 11:29:56 +0200 Subject: [PATCH 17/93] updated hgignore --- .hgignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.hgignore b/.hgignore index f46d1844e52..5555daed539 100644 --- a/.hgignore +++ b/.hgignore @@ -10,3 +10,6 @@ nose* .rope* .coverage cover/* +.settings +.pydevproject +.project From ce974380279de2ba615cff6662a55d902f06ea84 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Tue, 6 May 2014 11:30:45 +0200 Subject: [PATCH 18/93] Made body editable --- auth_saml/auth_saml.py | 6 ++++-- auth_saml_view.xml | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/auth_saml/auth_saml.py b/auth_saml/auth_saml.py index e5265a504d5..01120133c2d 100644 --- a/auth_saml/auth_saml.py +++ b/auth_saml/auth_saml.py @@ -54,12 +54,14 @@ def _get_auth_request(self, cr, uid, id_, state, context=None): ), 'enabled': fields.boolean('Allowed'), 'css_class': fields.char('CSS class'), - 'body': fields.char('Body'), + 'body': fields.char('Body', + required=True, + ), 'sequence': fields.integer(), } _defaults = { 'enabled': False, 'css_class': 'zocial saml', - 'body': 'Log in with Authentic', + 'body': 'Authentic', } diff --git a/auth_saml_view.xml b/auth_saml_view.xml index 4f013f58586..52acdb85162 100644 --- a/auth_saml_view.xml +++ b/auth_saml_view.xml @@ -11,6 +11,7 @@ + From 8062faf1e8471c849d99c19bf8300e50de9fd856 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Tue, 6 May 2014 11:36:46 +0200 Subject: [PATCH 19/93] 1.1 --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 79016a4b4fc..80fd4faa623 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -23,7 +23,7 @@ { 'name': 'Saml2 Authentication', - 'version': '1.0.1', + 'version': '1.1', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. From bd5a2cd84ef6cc84f19d87a21a0ce1dfae65f7d6 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Tue, 6 May 2014 11:36:56 +0200 Subject: [PATCH 20/93] Added tag TAG_1.1 for changeset 440c74a3b23e --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index cd0fe853924..8c6a22031a0 100644 --- a/.hgtags +++ b/.hgtags @@ -1 +1,2 @@ 0971293545c0e5088ae6a608f9c653d3e687a69e TAG_1.0 +440c74a3b23eb89c17d0e0d74b31face446f1987 TAG_1.1 From 0795dd4a2745148e136df7edd3111f5d3b9a0cf0 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Tue, 6 May 2014 11:37:16 +0200 Subject: [PATCH 21/93] 1.1.1 --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 80fd4faa623..1e8169740a8 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -23,7 +23,7 @@ { 'name': 'Saml2 Authentication', - 'version': '1.1', + 'version': '1.1.1', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. From c3848a8fcc793150ed670e62c211269c1a123686 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Thu, 15 May 2014 15:44:58 +0200 Subject: [PATCH 22/93] Fixed missing module --- auth_saml/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 15d0b15a5e6..f5c4be48bcc 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,3 @@ import controllers import auth_saml import res_users -import res_config From d7befbf6d76d3946dd81b988a1cc2a26c9d0cbd8 Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Thu, 15 May 2014 15:45:20 +0200 Subject: [PATCH 23/93] 1.2 --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 1e8169740a8..5f762585d5f 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -23,7 +23,7 @@ { 'name': 'Saml2 Authentication', - 'version': '1.1.1', + 'version': '1.2', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. From f6e5ee9eb72d6a33058d8c9b1185edf468ed82dc Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Thu, 15 May 2014 15:45:41 +0200 Subject: [PATCH 24/93] Added tag TAG_1.2 for changeset e0aa862214f8 --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 8c6a22031a0..7784821ef64 100644 --- a/.hgtags +++ b/.hgtags @@ -1,2 +1,3 @@ 0971293545c0e5088ae6a608f9c653d3e687a69e TAG_1.0 440c74a3b23eb89c17d0e0d74b31face446f1987 TAG_1.1 +e0aa862214f82295b0d942bfb53f11d4d7f80e09 TAG_1.2 From 63494c65499302e74e09f51a877f4c3a92acbeea Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Thu, 15 May 2014 15:46:20 +0200 Subject: [PATCH 25/93] dev version --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 5f762585d5f..6470ffcaf68 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -23,7 +23,7 @@ { 'name': 'Saml2 Authentication', - 'version': '1.2', + 'version': '1.2.1', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. From 1e0a21f8d04f3a0bd1695d1899413a88599ca25f Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 16 Oct 2014 10:02:26 +0200 Subject: [PATCH 26/93] Add TODO for multi access --- auth_saml/res_users.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auth_saml/res_users.py b/auth_saml/res_users.py index c3604138757..dabede2064e 100644 --- a/auth_saml/res_users.py +++ b/auth_saml/res_users.py @@ -98,6 +98,10 @@ def _auth_saml_signin( assert len(user_ids) == 1 user = self.browse(cr, uid, user_ids[0], context=context) + # WARNING: writing this means you can only log-in once with a single + # user... + # TODO add a new table with access tokens linked to users + # in order to be able to log multiple times with the same user user.write({'saml_access_token': saml_response}) return user.login From 7615a7113f69027168e5dd71b9c71d9220c9825e Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Mon, 10 Nov 2014 11:47:30 +0100 Subject: [PATCH 27/93] New ignore pattern for pycharm projects --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index 5555daed539..95570ddd2cc 100644 --- a/.hgignore +++ b/.hgignore @@ -9,6 +9,7 @@ tmp/* nose* .rope* .coverage +.idea cover/* .settings .pydevproject From 06b99fecdf95e9918d49597e4acb68bd192deb75 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Mon, 10 Nov 2014 11:48:13 +0100 Subject: [PATCH 28/93] docstring --- auth_saml/res_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/res_users.py b/auth_saml/res_users.py index dabede2064e..83cec66e564 100644 --- a/auth_saml/res_users.py +++ b/auth_saml/res_users.py @@ -74,7 +74,7 @@ def _auth_saml_signin( :param provider: saml provider id (int) :param validation: result of validation of access token (dict) - :param params: saml parameters (dict) + :param saml_response: saml parameters response from the IDP :return: user login (str) :raise: openerp.exceptions.AccessDenied if signin failed From d5322802225ef63feeb40617a4d83eddecb2dc33 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Mon, 10 Nov 2014 12:47:47 +0100 Subject: [PATCH 29/93] Tokens are now in their own table to avoid locks on res.users --- auth_saml/res_users.py | 57 +++++++++++++++++++++++++++++------------- auth_saml/token.py | 31 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 auth_saml/token.py diff --git a/auth_saml/res_users.py b/auth_saml/res_users.py index 83cec66e564..f6f07870465 100644 --- a/auth_saml/res_users.py +++ b/auth_saml/res_users.py @@ -1,3 +1,4 @@ +# -*- encoding: utf-8 -*- import logging import lasso @@ -20,10 +21,6 @@ class res_users(osv.Model): 'SAML User ID', help="SAML Provider user_id", ), - 'saml_access_token': fields.char( - 'Current SAML token for this user', - help="The current SAML token in use", - ), } _sql_constraints = [ @@ -53,10 +50,8 @@ def _auth_saml_validate(self, cr, uid, provider, token, context=None): raise Exception('Invalid assertion') # TODO use a real token validation from LASSO - validation = {} - # TODO push into the validation result a real UPN - validation['user_id'] = login.assertion.subject.nameId.content + validation = {'user_id': login.assertion.subject.nameId.content} """ if p.data_endpoint: @@ -80,6 +75,7 @@ def _auth_saml_signin( This method can be overridden to add alternative signin methods. """ + token_osv = self.pool.get('auth_saml.token') saml_uid = validation['user_id'] user_ids = self.search( @@ -97,12 +93,35 @@ def _auth_saml_signin( # production code... assert len(user_ids) == 1 + # browse the user because we'll need this in the response user = self.browse(cr, uid, user_ids[0], context=context) - # WARNING: writing this means you can only log-in once with a single - # user... - # TODO add a new table with access tokens linked to users - # in order to be able to log multiple times with the same user - user.write({'saml_access_token': saml_response}) + + user_id = user.id + + # now find if a token for this user/provider already exists + token_ids = token_osv.search( + cr, uid, + [ + ('saml_provider_id', '=', provider), + ('user_id', '=', user_id), + ] + ) + if token_ids: + token_osv.write( + cr, uid, token_ids, + {'saml_access_token': saml_response}, + context=context + ) + else: + token_osv.create( + cr, uid, + { + 'saml_access_token': saml_response, + 'saml_provider_id': provider, + 'user_id': user_id, + }, + context=context + ) return user.login @@ -125,26 +144,30 @@ def auth_saml(self, cr, uid, provider, saml_response, context=None): raise openerp.exceptions.AccessDenied() # return user credentials - return (cr.dbname, login, saml_response) + return cr.dbname, login, saml_response def check_credentials(self, cr, uid, token): """token can be a password if the user has used the normal form... but we are more interested in the case when they are tokens and the interesting code is inside the except clause """ + token_osv = self.pool.get('auth_saml.token') try: - return super(res_users, self).check_credentials(cr, uid, token) + super(res_users, self).check_credentials(cr, uid, token) except openerp.exceptions.AccessDenied: - res = self.search( + # since normal auth did not succeed we now try to find if the user + # has an active token attached to his uid + res = token_osv.search( cr, SUPERUSER_ID, [ - ('id', '=', uid), + ('user_id', '=', uid), ('saml_access_token', '=', token), ] ) + # if the user is not found we re-raise the AccessDenied if not res: # TODO: maybe raise a defined exception instead of the last - # exception that occured in our execution frame + # exception that occurred in our execution frame raise diff --git a/auth_saml/token.py b/auth_saml/token.py new file mode 100644 index 00000000000..428713e75d2 --- /dev/null +++ b/auth_saml/token.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +__author__ = 'faide' + + +import logging +from openerp.osv import osv, fields + +_logger = logging.getLogger(__name__) + + +class saml_token(osv.Model): + _name = "auth_saml.token" + _rec_name = "user_id" + + _columns = { + 'saml_provider_id': fields.many2one( + 'auth.saml.provider', + string='SAML Provider that issued the token', + ), + 'user_id': fields.many2one( + 'res.users', + string="User", + # we want the token to be destroyed if the corresponding res.users + # is deleted + ondelete="cascade" + ), + 'saml_access_token': fields.char( + 'Current SAML token for this user', + help="The current SAML token in use", + ), + } From c11d67d742f85ffba886cfd8bd38db0046dbcc6a Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Mon, 10 Nov 2014 16:31:57 +0100 Subject: [PATCH 30/93] Added #security rules, missing import and NEWS and README files --- auth_saml/NEWS | 4 ++++ auth_saml/README | 1 + auth_saml/__init__.py | 1 + auth_saml/__openerp__.py | 2 +- auth_saml/security/ir.model.access.csv | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 auth_saml/NEWS create mode 100644 auth_saml/README diff --git a/auth_saml/NEWS b/auth_saml/NEWS new file mode 100644 index 00000000000..5275c706315 --- /dev/null +++ b/auth_saml/NEWS @@ -0,0 +1,4 @@ +2.0 + +* SAML tokens are not stored in res_users anymore to avoid locks on that table. + diff --git a/auth_saml/README b/auth_saml/README new file mode 100644 index 00000000000..ceffb59ad85 --- /dev/null +++ b/auth_saml/README @@ -0,0 +1 @@ +This is a module for odoo/OpenERP 7. It provides authentification via SAML. diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index f5c4be48bcc..51e06ad1272 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,3 +1,4 @@ import controllers import auth_saml import res_users +import token diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 6470ffcaf68..e5ac8e93da4 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -23,7 +23,7 @@ { 'name': 'Saml2 Authentication', - 'version': '1.2.1', + 'version': '2.0', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. diff --git a/auth_saml/security/ir.model.access.csv b/auth_saml/security/ir.model.access.csv index a8fbea84b9a..7f4cc3a4b6e 100644 --- a/auth_saml/security/ir.model.access.csv +++ b/auth_saml/security/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_auth_saml_provider,auth_saml_provider,model_auth_saml_provider,base.group_system,1,1,1,1 +access_auth_saml_token,access_auth_saml_token,model_auth_saml_token,,0,0,0,0 From e5bd66b1bf2688d3c39fba9608a6e9ca6b3bc35d Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Mon, 10 Nov 2014 16:38:51 +0100 Subject: [PATCH 31/93] Added #script to delete removed table column from DB --- script/2.0-cleanup.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 script/2.0-cleanup.sql diff --git a/script/2.0-cleanup.sql b/script/2.0-cleanup.sql new file mode 100644 index 00000000000..564c887e794 --- /dev/null +++ b/script/2.0-cleanup.sql @@ -0,0 +1,2 @@ +--remove the old column from res.users +ALTER TABLE res_users DROP COLUMN IF EXISTS saml_access_token; From 060d79bfd60a32e0fae554ebd2f47dd9bab9837a Mon Sep 17 00:00:00 2001 From: Vincent Lhote-Hatakeyama Date: Mon, 10 Nov 2014 16:39:14 +0100 Subject: [PATCH 32/93] Added tag TAG_2.0 for changeset ae9585f6103b --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 7784821ef64..83447e267b8 100644 --- a/.hgtags +++ b/.hgtags @@ -1,3 +1,4 @@ 0971293545c0e5088ae6a608f9c653d3e687a69e TAG_1.0 440c74a3b23eb89c17d0e0d74b31face446f1987 TAG_1.1 e0aa862214f82295b0d942bfb53f11d4d7f80e09 TAG_1.2 +ae9585f6103b8271c2b1fba26653b259e472be92 TAG_2.0 From ce6533aa270e0d452918c75c0df44ab2a21455c6 Mon Sep 17 00:00:00 2001 From: jcokimlen Date: Tue, 2 Dec 2014 14:30:33 +0100 Subject: [PATCH 33/93] Renamed token.py into saml_token.py --- auth_saml/__init__.py | 2 +- auth_saml/{token.py => saml_token.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename auth_saml/{token.py => saml_token.py} (100%) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 51e06ad1272..0aa679cec5f 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,4 @@ import controllers import auth_saml import res_users -import token +import saml_token diff --git a/auth_saml/token.py b/auth_saml/saml_token.py similarity index 100% rename from auth_saml/token.py rename to auth_saml/saml_token.py From df54b0d0493e8b65ae59691aceb02893b81d848e Mon Sep 17 00:00:00 2001 From: Alexandre Brun Date: Thu, 15 Jan 2015 17:47:05 +0100 Subject: [PATCH 34/93] Update and Add License (AGPL) and (XCG) --- auth_saml/LICENSE | 661 +++++++++++++++++++++++++++++++++++++++ auth_saml/__openerp__.py | 5 +- 2 files changed, 663 insertions(+), 3 deletions(-) create mode 100644 auth_saml/LICENSE diff --git a/auth_saml/LICENSE b/auth_saml/LICENSE new file mode 100644 index 00000000000..dba13ed2ddf --- /dev/null +++ b/auth_saml/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index e5ac8e93da4..6e048cc41b4 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# XCG Consulting Group -# Copyright (C) 2010-2014 XCG Consulting s.a.s -# Date: Fri, 16 Jan 2015 15:34:29 +0100 Subject: [PATCH 35/93] Improve README --- auth_saml/README | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/auth_saml/README b/auth_saml/README index ceffb59ad85..96ed9d49cf6 100644 --- a/auth_saml/README +++ b/auth_saml/README @@ -1 +1,24 @@ +### README ### + + +### Authentification SAML for Odoo/OpenERP ### + This is a module for odoo/OpenERP 7. It provides authentification via SAML. + + +This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On ) +between Odoo and other applications of your ecosystem. + +Benefits +======== + +* Reducing the time spent typing different passwords for different accounts. + +* Reducing the time spent in IT support for password oversights. + +* Centralizing authentication systems. + +* Securing all input levels / exit / access to multiple systems without prompting users. + +* The centralization of access control information for compliance testing to different standards. From 1ab965c33568555e44f4797de303097ba104c028 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Wed, 28 Jan 2015 15:15:44 +0100 Subject: [PATCH 36/93] Added tag TAG_2.1 for changeset 9dfc2bf569e4 --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 83447e267b8..04fdde3b428 100644 --- a/.hgtags +++ b/.hgtags @@ -2,3 +2,4 @@ 440c74a3b23eb89c17d0e0d74b31face446f1987 TAG_1.1 e0aa862214f82295b0d942bfb53f11d4d7f80e09 TAG_1.2 ae9585f6103b8271c2b1fba26653b259e472be92 TAG_2.0 +9dfc2bf569e44a4fda8a0afc33f6e45d92518d41 TAG_2.1 From b6cc78371f4d7030023d2c53cd2283834c3e8293 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 14:29:35 +0100 Subject: [PATCH 37/93] Copyright update --- auth_saml/__openerp__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 6e048cc41b4..b23858c0dd5 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- ############################################################################## # -# Saml2 Authentication, for OpenERP -# Copyright (C) 2010-2014 XCG Consulting (http://odoo.consulting) +# Saml2 Authentication for Odoo +# Copyright (C) 2010-2015 XCG Consulting # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as From 04552e8500ebe92b545ba75727517be80fd1ece1 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 14:36:11 +0100 Subject: [PATCH 38/93] Move files around --- auth_saml/__init__.py | 4 +--- auth_saml/__openerp__.py | 12 ++++++++---- auth_saml_data.xml => auth_saml/data/auth_saml.xml | 0 auth_saml/model/__init__.py | 5 +++++ auth_saml/{ => model}/auth_saml.py | 0 auth_saml/{ => model}/res_users.py | 0 auth_saml/{ => model}/saml_token.py | 0 {script => auth_saml/scripts}/2.0-cleanup.sql | 0 auth_saml_view.xml => auth_saml/views/auth_saml.xml | 0 res_users.xml => auth_saml/views/res_users.xml | 0 10 files changed, 14 insertions(+), 7 deletions(-) rename auth_saml_data.xml => auth_saml/data/auth_saml.xml (100%) create mode 100644 auth_saml/model/__init__.py rename auth_saml/{ => model}/auth_saml.py (100%) rename auth_saml/{ => model}/res_users.py (100%) rename auth_saml/{ => model}/saml_token.py (100%) rename {script => auth_saml/scripts}/2.0-cleanup.sql (100%) rename auth_saml_view.xml => auth_saml/views/auth_saml.xml (100%) rename res_users.xml => auth_saml/views/res_users.xml (100%) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 0aa679cec5f..4a8832a7537 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,2 @@ import controllers -import auth_saml -import res_users -import saml_token +import model diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index b23858c0dd5..0fc3399d0c1 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -32,12 +32,16 @@ 'maintainer': 'XCG Consulting s.a.s.', 'website': 'http://www.xcg-consulting.fr', 'depends': ['base', 'web', 'base_setup'], + 'data': [ - 'auth_saml_data.xml', - 'res_users.xml', - 'auth_saml_view.xml', - 'security/ir.model.access.csv' + 'data/auth_saml.xml', + + 'security/ir.model.access.csv', + + 'views/auth_saml.xml', + 'views/res_users.xml', ], + 'js': ['static/src/js/auth_saml.js'], 'css': [ 'static/lib/zocial/css/zocial.css', diff --git a/auth_saml_data.xml b/auth_saml/data/auth_saml.xml similarity index 100% rename from auth_saml_data.xml rename to auth_saml/data/auth_saml.xml diff --git a/auth_saml/model/__init__.py b/auth_saml/model/__init__.py new file mode 100644 index 00000000000..547eb9976fd --- /dev/null +++ b/auth_saml/model/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa + +import auth_saml +import res_users +import saml_token diff --git a/auth_saml/auth_saml.py b/auth_saml/model/auth_saml.py similarity index 100% rename from auth_saml/auth_saml.py rename to auth_saml/model/auth_saml.py diff --git a/auth_saml/res_users.py b/auth_saml/model/res_users.py similarity index 100% rename from auth_saml/res_users.py rename to auth_saml/model/res_users.py diff --git a/auth_saml/saml_token.py b/auth_saml/model/saml_token.py similarity index 100% rename from auth_saml/saml_token.py rename to auth_saml/model/saml_token.py diff --git a/script/2.0-cleanup.sql b/auth_saml/scripts/2.0-cleanup.sql similarity index 100% rename from script/2.0-cleanup.sql rename to auth_saml/scripts/2.0-cleanup.sql diff --git a/auth_saml_view.xml b/auth_saml/views/auth_saml.xml similarity index 100% rename from auth_saml_view.xml rename to auth_saml/views/auth_saml.xml diff --git a/res_users.xml b/auth_saml/views/res_users.xml similarity index 100% rename from res_users.xml rename to auth_saml/views/res_users.xml From 2de4d32008717f07f7ee2701c0e240a6fcf2c913 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 14:38:38 +0100 Subject: [PATCH 39/93] Code formatting (flake8 valid) --- auth_saml/__init__.py | 2 ++ auth_saml/controllers/__init__.py | 2 ++ auth_saml/controllers/main.py | 9 +++++---- auth_saml/model/auth_saml.py | 7 ++++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 4a8832a7537..36b6810dbbb 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,2 +1,4 @@ +# flake8: noqa + import controllers import model diff --git a/auth_saml/controllers/__init__.py b/auth_saml/controllers/__init__.py index e11f9ba81bb..e47ae81ca78 100644 --- a/auth_saml/controllers/__init__.py +++ b/auth_saml/controllers/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + import main # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index e8a4f6cc044..df9479f64f7 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -13,9 +13,10 @@ _logger = logging.getLogger(__name__) -#---------------------------------------------------------- + +# ---------------------------------------------------------- # helpers -#---------------------------------------------------------- +# ---------------------------------------------------------- def fragment_to_query_string(func): @@ -36,9 +37,9 @@ def wrapper(self, req, **kw): return wrapper -#---------------------------------------------------------- +# ---------------------------------------------------------- # Controller -#---------------------------------------------------------- +# ---------------------------------------------------------- class SAMLController(oeweb.Controller): _cp_path = '/auth_saml' diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 01120133c2d..0b744f81ac8 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -1,4 +1,5 @@ -from openerp.osv import osv, fields +from openerp.osv import fields +from openerp.osv import osv import lasso import simplejson @@ -11,7 +12,6 @@ class auth_saml_provider(osv.osv): _order = 'name' def _get_lasso_for_provider(self, cr, uid, provider_id, context=None): - #print cr, uid, provider_id, context provider = self.browse(cr, uid, provider_id, context=context) # TODO: we should cache those results somewhere because it is @@ -54,7 +54,8 @@ def _get_auth_request(self, cr, uid, id_, state, context=None): ), 'enabled': fields.boolean('Allowed'), 'css_class': fields.char('CSS class'), - 'body': fields.char('Body', + 'body': fields.char( + 'Body', required=True, ), 'sequence': fields.integer(), From fb9dab30d5a69689c5439f54976c148434eaf006 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 14:39:53 +0100 Subject: [PATCH 40/93] Code formatting --- auth_saml/views/auth_saml.xml | 27 ++++++++++++++------------- auth_saml/views/res_users.xml | 10 ++++++---- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml index 52acdb85162..d37cdab278d 100644 --- a/auth_saml/views/auth_saml.xml +++ b/auth_saml/views/auth_saml.xml @@ -2,6 +2,17 @@ + + auth.saml.provider.list + auth.saml.provider + + + + + + + + auth.saml.provider.form auth.saml.provider @@ -23,17 +34,6 @@ - - auth.saml.provider.list - auth.saml.provider - - - - - - - - Providers auth.saml.provider @@ -41,7 +41,8 @@ tree,form + parent="base.menu_users" sequence="30" action="action_saml_provider" + groups="base.group_no_one" /> + diff --git a/auth_saml/views/res_users.xml b/auth_saml/views/res_users.xml index 28e905146fb..00d0e990679 100644 --- a/auth_saml/views/res_users.xml +++ b/auth_saml/views/res_users.xml @@ -1,22 +1,24 @@ - + + res.users.form res.users form - + - - + + + From 35f500b846fef38ae9d21dbc773414666e7c264a Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 14:42:06 +0100 Subject: [PATCH 41/93] Add some comments --- auth_saml/views/auth_saml.xml | 4 ++++ auth_saml/views/res_users.xml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml index d37cdab278d..f2dc5f00385 100644 --- a/auth_saml/views/auth_saml.xml +++ b/auth_saml/views/auth_saml.xml @@ -2,6 +2,8 @@ + + auth.saml.provider.list auth.saml.provider @@ -34,6 +36,8 @@
+ + Providers auth.saml.provider diff --git a/auth_saml/views/res_users.xml b/auth_saml/views/res_users.xml index 00d0e990679..3f7f19c264c 100644 --- a/auth_saml/views/res_users.xml +++ b/auth_saml/views/res_users.xml @@ -2,6 +2,8 @@ + + res.users.form res.users From aeb5db5ea759d1b834b8edc9f14c79602011eb9a Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 15:07:56 +0100 Subject: [PATCH 42/93] Ensure no Odoo user posesses both an SAML user ID and an Odoo password --- auth_saml/model/res_users.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index f6f07870465..6b8c7720fce 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -23,6 +23,29 @@ class res_users(osv.Model): ), } + def _no_password_with_saml(self, cr, uid, ids, context=None): + """Ensure no Odoo user posesses both an SAML user ID and an Odoo + password. + """ + + users = self.browse(cr, uid, ids, context=context) + for user in users: + if user.password and user.saml_uid: + return False + + return True + + _constraints = [ + ( + _no_password_with_saml, + ( + 'SAML2 authentication: An Odoo user cannot posess both an ' + 'SAML user ID and an Odoo password.' + ), + ['password', 'saml_uid'] + ), + ] + _sql_constraints = [ ( 'uniq_users_saml_provider_saml_uid', From cfd1cca0788c2dd1b8f77ed94193d71894f4ccab Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 15:08:08 +0100 Subject: [PATCH 43/93] Script to clear passwords of Odoo users that already have an SAML user ID --- auth_saml/scripts/clear_passwords.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 auth_saml/scripts/clear_passwords.sql diff --git a/auth_saml/scripts/clear_passwords.sql b/auth_saml/scripts/clear_passwords.sql new file mode 100644 index 00000000000..ea2ec8021e9 --- /dev/null +++ b/auth_saml/scripts/clear_passwords.sql @@ -0,0 +1,5 @@ +-- When migrating to a version of "auth_saml" > 2.0, a constraint (optional) has been added to +-- ensure no Odoo user posesses both an SAML user ID and an Odoo password. +-- Run this script to clear passwords of Odoo users that already have an SAML user ID. + +UPDATE res_users SET password = NULL WHERE password IS NOT NULL AND saml_uid IS NOT NULL; From f4ebdac75800bffdd21e0c4fbe61e56c855f2488 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 15:14:40 +0100 Subject: [PATCH 44/93] Clear out the user's password when setting an SAML user ID --- auth_saml/model/res_users.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 6b8c7720fce..36519984ffa 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -194,3 +194,15 @@ def check_credentials(self, cr, uid, token): # TODO: maybe raise a defined exception instead of the last # exception that occurred in our execution frame raise + + def write(self, cr, uid, ids, vals, context=None): + """Override to clear out the user's password when setting an SAML user + ID (as they can't cohabit). + """ + + if vals and vals.get('saml_uid'): + vals['password'] = False + + return super(res_users, self).write( + cr, uid, ids, vals, context=context + ) From 849797231aa62aac6a3d0444d0bf00e839c24421 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 16:50:51 +0100 Subject: [PATCH 45/93] Setting to allow Odoo users to possess both an SAML ID and an Odoo password --- auth_saml/__openerp__.py | 2 + auth_saml/data/ir_config_parameter.xml | 13 ++++ auth_saml/documentation/index.rst | 2 + auth_saml/model/__init__.py | 1 + auth_saml/model/base_settings.py | 98 ++++++++++++++++++++++++++ auth_saml/model/res_users.py | 13 +++- auth_saml/views/base_settings.xml | 26 +++++++ 7 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 auth_saml/data/ir_config_parameter.xml create mode 100644 auth_saml/model/base_settings.py create mode 100644 auth_saml/views/base_settings.xml diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 0fc3399d0c1..2d1fa8d1d3a 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -35,10 +35,12 @@ 'data': [ 'data/auth_saml.xml', + 'data/ir_config_parameter.xml', 'security/ir.model.access.csv', 'views/auth_saml.xml', + 'views/base_settings.xml', 'views/res_users.xml', ], diff --git a/auth_saml/data/ir_config_parameter.xml b/auth_saml/data/ir_config_parameter.xml new file mode 100644 index 00000000000..ce3c1c16a7b --- /dev/null +++ b/auth_saml/data/ir_config_parameter.xml @@ -0,0 +1,13 @@ + + + + + + + + auth_saml.allow_saml.uid_and_internal_password + 0 + + + + diff --git a/auth_saml/documentation/index.rst b/auth_saml/documentation/index.rst index 2f2339d7e73..cc3e96e6ae0 100644 --- a/auth_saml/documentation/index.rst +++ b/auth_saml/documentation/index.rst @@ -84,3 +84,5 @@ Copy the metadata from your identity provider:: and make sure the URLs point where they should. Edit the file if necessary. Then save its content into the corresponding box in the openerp SAML2 Provider form. + +There are additional SAML-related settings in Configuration > General settings. diff --git a/auth_saml/model/__init__.py b/auth_saml/model/__init__.py index 547eb9976fd..b61318a04a7 100644 --- a/auth_saml/model/__init__.py +++ b/auth_saml/model/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa import auth_saml +import base_settings import res_users import saml_token diff --git a/auth_saml/model/base_settings.py b/auth_saml/model/base_settings.py new file mode 100644 index 00000000000..63bcd362e37 --- /dev/null +++ b/auth_saml/model/base_settings.py @@ -0,0 +1,98 @@ +from openerp import SUPERUSER_ID +from openerp.osv import fields +from openerp.osv import orm + + +_SAML_UID_AND_PASS_SETTING = 'auth_saml.allow_saml.uid_and_internal_password' + + +class base_settings(orm.TransientModel): + """Inherit from base.config.settings to add a setting. This is only here + for easier access; the setting is not actually stored by this (transient) + collection. Instead, it is kept in sync with the + "auth_saml.allow_saml.uid_and_internal_password" global setting. See + comments in the definition of the "res.config.settings" collection for + details. + """ + + _inherit = 'base.config.settings' + + _columns = { + 'allow_saml_uid_and_internal_password': fields.boolean( + ( + 'Allow SAML users to posess an Odoo password (warning: ' + 'decreases security)' + ), + ), + } + + def allow_saml_uid_and_internal_password(self, cr, context=None): + """Read the allow_saml_uid_and_internal_password setting. + Use the admin account to bypass security restrictions. + """ + + uid = SUPERUSER_ID + + config_obj = self.pool['ir.config_parameter'] + + config_ids = config_obj.search( + cr, uid, + [('key', '=', _SAML_UID_AND_PASS_SETTING)], + limit=1, + context=context + ) + if not config_ids: + return False + + config = config_obj.browse( + cr, uid, config_ids, context=context + )[0] + return (True if config.value == '1' else False) + + def get_default_allow_saml_uid_and_internal_password( + self, cr, uid, fields, context=None + ): + """Read the allow_saml_uid_and_internal_password setting. This function + is called when the form is shown. + """ + + ret = {} + + if 'allow_saml_uid_and_internal_password' in fields: + ret['allow_saml_uid_and_internal_password'] = ( + self.allow_saml_uid_and_internal_password(cr, context) + ) + + return ret + + def set_allow_saml_uid_and_internal_password( + self, cr, uid, ids, context=None + ): + """Update the allow_saml_uid_and_internal_password setting. This + function is called when saving the form. + """ + + dlg = self.browse(cr, uid, ids, context=context)[0] + + setting_value = ( + '1' if dlg.allow_saml_uid_and_internal_password else '0' + ) + + config_obj = self.pool['ir.config_parameter'] + config_ids = config_obj.search( + cr, uid, + [('key', '=', _SAML_UID_AND_PASS_SETTING)], + limit=1, + context=context + ) + if config_ids: + config_obj.write( + cr, uid, config_ids, {'value': setting_value}, context=context + ) + else: + # The setting doesn't exist; create it. + config_obj.create( + cr, uid, + {'key': _SAML_UID_AND_PASS_SETTING, 'value': setting_value}, + context=context + ) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 36519984ffa..5351adbc3a3 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -28,6 +28,10 @@ def _no_password_with_saml(self, cr, uid, ids, context=None): password. """ + if self._allow_saml_uid_and_internal_password(cr, context): + # The constraint is a no-op in this case. + return True + users = self.browse(cr, uid, ids, context=context) for user in users: if user.password and user.saml_uid: @@ -201,8 +205,15 @@ def write(self, cr, uid, ids, vals, context=None): """ if vals and vals.get('saml_uid'): - vals['password'] = False + if not self._allow_saml_uid_and_internal_password(cr, context): + vals['password'] = False return super(res_users, self).write( cr, uid, ids, vals, context=context ) + + def _allow_saml_uid_and_internal_password(self, cr, context): + setting_obj = self.pool['base.config.settings'] + return setting_obj.allow_saml_uid_and_internal_password( + cr, context=context + ) diff --git a/auth_saml/views/base_settings.xml b/auth_saml/views/base_settings.xml new file mode 100644 index 00000000000..df9f7318f3e --- /dev/null +++ b/auth_saml/views/base_settings.xml @@ -0,0 +1,26 @@ + + + + + + + + auth_saml_base_settings_form + base.config.settings + + + + +
+ +
+
+ +
+
+ +
+
From 00dd1ee75d0afd485b2047609995158d96d379fe Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 16:56:44 +0100 Subject: [PATCH 46/93] Update French translations --- auth_saml/i18n/fr.po | 159 +++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 73 deletions(-) diff --git a/auth_saml/i18n/fr.po b/auth_saml/i18n/fr.po index 8eb3bb0b052..2942526a969 100644 --- a/auth_saml/i18n/fr.po +++ b/auth_saml/i18n/fr.po @@ -6,16 +6,25 @@ msgid "" msgstr "" "Project-Id-Version: OpenERP Server 7.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-03-31 10:54+0000\n" -"PO-Revision-Date: 2014-03-31 13:05+0200\n" -"Last-Translator: Vincent Lhote-Hatakeyama \n" +"POT-Creation-Date: 2015-03-03 15:51+0000\n" +"PO-Revision-Date: 2015-03-03 16:56+0100\n" +"Last-Translator: Houzéfa Abbasbhay \n" "Language-Team: XCG Consulting\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" +"Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Virtaal 0.7.1\n" +"X-Generator: Poedit 1.5.4\n" + +#. module: auth_saml +#: constraint:res.users:0 +msgid "" +"SAML2 authentication: An Odoo user cannot posess both an SAML user ID and an " +"Odoo password." +msgstr "" +"Authentification SAML2 : Un utilisateur Odoo ne peut pas posséder à la fois " +"un ID utilisateur SAML et un mot de passe Odoo." #. module: auth_saml #: model:ir.model,name:auth_saml.model_base_config_settings @@ -23,7 +32,7 @@ msgid "base.config.settings" msgstr "base.config.settings" #. module: auth_saml -#: field:res.users,saml_access_token:0 +#: field:auth_saml.token,saml_access_token:0 msgid "Current SAML token for this user" msgstr "Jeton SAML courant de l’utilisateur" @@ -35,30 +44,25 @@ msgid "Sign up error" msgstr "Erreur d’inscription" #. module: auth_saml -#: model:ir.ui.menu,name:auth_saml.menu_saml_providers -msgid "SAML Providers" -msgstr "Fournisseurs SAML" +#: model:ir.model,name:auth_saml.model_auth_saml_token +msgid "auth_saml.token" +msgstr "auth_saml.token" #. module: auth_saml -#: field:auth.saml.provider,css_class:0 -msgid "CSS class" -msgstr "Classe CSS" +#: field:auth_saml.token,saml_provider_id:0 +msgid "SAML Provider that issued the token" +msgstr "Fournisseur SAML qui a fourni le token" #. module: auth_saml -#: field:auth.saml.provider,body:0 -msgid "Body" -msgstr "Corps" +#: model:ir.ui.menu,name:auth_saml.menu_saml_providers +msgid "SAML Providers" +msgstr "Fournisseurs SAML" #. module: auth_saml #: model:ir.model,name:auth_saml.model_res_users msgid "Users" msgstr "Utilisateurs" -#. module: auth_saml -#: field:auth.saml.provider,sequence:0 -msgid "unknown" -msgstr "inconnu" - #. module: auth_saml #. openerp-web #: code:addons/auth_saml/static/src/js/auth_saml.js:16 @@ -68,29 +72,14 @@ msgid "Authentication error" msgstr "Erreur d’authentification" #. module: auth_saml -#: help:res.users,saml_access_token:0 +#: help:auth_saml.token,saml_access_token:0 msgid "The current SAML token in use" msgstr "Le jeton SAML courant en cours d’utilisation" #. module: auth_saml -#: view:base.config.settings:0 -msgid "http://localhost:8000" -msgstr "http://localhost :8000" - -#. module: auth_saml -#: view:base.config.settings:0 -msgid "running on" -msgstr "en fonctionnement sur" - -#. module: auth_saml -#: view:base.config.settings:0 -msgid "You must have an" -msgstr "Vous devez avoir un" - -#. module: auth_saml -#: view:base.config.settings:0 -msgid "authentic2 server" -msgstr "serveur authentic2" +#: field:auth_saml.token,user_id:0 +msgid "User" +msgstr "Utilisateur" #. module: auth_saml #: model:ir.model,name:auth_saml.model_auth_saml_provider @@ -105,14 +94,9 @@ msgid "Access Denied" msgstr "Accès refusé" #. module: auth_saml -#: field:base.config.settings,auth_saml_local_enabled:0 -msgid "Allow users to sign in with a Local Authentic" -msgstr "Autoriser les utilisateurs à s’inscrire avec un Authentic local" - -#. module: auth_saml -#: field:auth.saml.provider,sp_metadata:0 -msgid "SP Configuration" -msgstr "Configuration du FS" +#: model:ir.model,name:auth_saml.model_res_config +msgid "res.config" +msgstr "res.config" #. module: auth_saml #: field:res.users,saml_provider_id:0 @@ -120,14 +104,12 @@ msgid "SAML Provider" msgstr "Fournisseur SAML" #. module: auth_saml -#: field:auth.saml.provider,sp_pkey:0 -msgid "Private key of our service provider (this openerpserver)" -msgstr "Clef privée de notre fournisseur de service (ce serveur OpenERP)" - -#. module: auth_saml -#: field:auth.saml.provider,name:0 -msgid "Provider name" -msgstr "Nom du fournisseur" +#: field:base.config.settings,allow_saml_uid_and_internal_password:0 +msgid "" +"Allow SAML users to posess an Odoo password (warning: decreases security)" +msgstr "" +"Autoriser les utilisateurs avec SAML à aussi avoir un mot de passe Odoo " +"(attention : abaisse la sécurité)" #. module: auth_saml #: view:auth.saml.provider:0 @@ -144,31 +126,20 @@ msgstr "L’identifiant SAML doit être unique par fournisseur" msgid "SAML User ID" msgstr "Identifiant utilisateur SAML" -#. module: auth_saml -#: model:ir.actions.act_window,name:auth_saml.action_saml_provider -msgid "Providers" -msgstr "Fournisseurs" - -#. module: auth_saml -#: help:res.users,saml_uid:0 -#, fuzzy -msgid "SAML Provider user_id" -msgstr "Fournisseur SAML user_id" - #. module: auth_saml #: view:res.users:0 msgid "SAML" msgstr "SAML" #. module: auth_saml -#: field:auth.saml.provider,idp_metadata:0 -msgid "IDP Configuration" -msgstr "Configuration FI" +#: help:res.users,saml_uid:0 +msgid "SAML Provider user_id" +msgstr "Fournisseur SAML user_id" #. module: auth_saml -#: field:auth.saml.provider,enabled:0 -msgid "Allowed" -msgstr "Autorisé" +#: model:ir.actions.act_window,name:auth_saml.action_saml_provider +msgid "Providers" +msgstr "Fournisseurs" #. module: auth_saml #. openerp-web @@ -181,8 +152,50 @@ msgstr "L’inscription n’est pas autorisée sur cette base de donnée." #. openerp-web #: code:addons/auth_saml/static/src/js/auth_saml.js:18 #, python-format -msgid "You do not have access to this database or your invitation has expired. Please ask for an invitation and be sure to follow the link in your invitation email." +msgid "" +"You do not have access to this database or your invitation has expired. " +"Please ask for an invitation and be sure to follow the link in your " +"invitation email." msgstr "" "Vous n’avez pas accès à cette base de donnée ou votre invitation a expirée. " "Demandez une invitation et assurez-vous de suivre le lien dans le courriel " "d’invitation." + +#~ msgid "CSS class" +#~ msgstr "Classe CSS" + +#~ msgid "Body" +#~ msgstr "Corps" + +#~ msgid "unknown" +#~ msgstr "inconnu" + +#~ msgid "http://localhost:8000" +#~ msgstr "http://localhost :8000" + +#~ msgid "running on" +#~ msgstr "en fonctionnement sur" + +#~ msgid "You must have an" +#~ msgstr "Vous devez avoir un" + +#~ msgid "authentic2 server" +#~ msgstr "serveur authentic2" + +#~ msgid "Allow users to sign in with a Local Authentic" +#~ msgstr "Autoriser les utilisateurs à s’inscrire avec un Authentic local" + +#~ msgid "SP Configuration" +#~ msgstr "Configuration du FS" + +#~ msgid "Private key of our service provider (this openerpserver)" +#~ msgstr "Clef privée de notre fournisseur de service (ce serveur OpenERP)" + +#~ msgid "Provider name" +#~ msgstr "Nom du fournisseur" + +#~ msgid "IDP Configuration" +#~ msgstr "Configuration FI" + +#~ msgid "Allowed" +#~ msgstr "Autorisé" From bffd42dc4a7cfcc49dbba0cc5336ad175763cd1f Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Tue, 3 Mar 2015 16:57:51 +0100 Subject: [PATCH 47/93] News --- auth_saml/NEWS | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/auth_saml/NEWS b/auth_saml/NEWS index 5275c706315..d39224aad25 100644 --- a/auth_saml/NEWS +++ b/auth_saml/NEWS @@ -1,4 +1,11 @@ +next +==== + +* Checks to ensure no Odoo user with SAML also has an Odoo password. +* Setting to disable that rule. + + 2.0 +=== * SAML tokens are not stored in res_users anymore to avoid locks on that table. - From 4b8ce6dc380342d4713ab9358588f8306ed43fbb Mon Sep 17 00:00:00 2001 From: Alexandre Brun Date: Tue, 17 Mar 2015 11:21:11 +0100 Subject: [PATCH 48/93] Update to TAG_3.0 --- auth_saml/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 2d1fa8d1d3a..619761794a0 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -22,7 +22,7 @@ { 'name': 'Saml2 Authentication', - 'version': '2.0', + 'version': '3.0', 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. From 6573db9759d4ff7de80a516b343d99bf8e10539a Mon Sep 17 00:00:00 2001 From: Alexandre Brun Date: Tue, 17 Mar 2015 11:21:16 +0100 Subject: [PATCH 49/93] Added tag TAG_3.0 for changeset f20470fdf0ef --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 04fdde3b428..a7132924e5b 100644 --- a/.hgtags +++ b/.hgtags @@ -3,3 +3,4 @@ e0aa862214f82295b0d942bfb53f11d4d7f80e09 TAG_1.2 ae9585f6103b8271c2b1fba26653b259e472be92 TAG_2.0 9dfc2bf569e44a4fda8a0afc33f6e45d92518d41 TAG_2.1 +f20470fdf0effdbdd1f87c6222f9b864fa5a7bf9 TAG_3.0 From 95ecc0820e3095bed8be6fb2afad1c251834b915 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Tue, 2 Jun 2015 17:28:47 +0200 Subject: [PATCH 50/93] Changed field description to be more precise --- auth_saml/model/auth_saml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 0b744f81ac8..42822686944 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -52,7 +52,7 @@ def _get_auth_request(self, cr, uid, id_, state, context=None): 'sp_pkey': fields.text( 'Private key of our service provider (this openerpserver)' ), - 'enabled': fields.boolean('Allowed'), + 'enabled': fields.boolean('Enabled'), 'css_class': fields.char('CSS class'), 'body': fields.char( 'Body', From d1c0e55ac667b04d6613340184de64c76153d543 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 03:49:44 +0200 Subject: [PATCH 51/93] WIP, porting to v8 --HG-- branch : odoo8 --- auth_saml/__openerp__.py | 7 +- auth_saml/controllers/main.py | 152 +++++++++++++++++++++------------- auth_saml/model/res_users.py | 6 +- auth_saml/views/auth_saml.xml | 18 ++++ static/src/xml/auth_saml.xml | 8 -- 5 files changed, 121 insertions(+), 70 deletions(-) delete mode 100644 static/src/xml/auth_saml.xml diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 619761794a0..c90f7999429 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -31,7 +31,11 @@ 'author': 'XCG Consulting s.a.s.', 'maintainer': 'XCG Consulting s.a.s.', 'website': 'http://www.xcg-consulting.fr', - 'depends': ['base', 'web', 'base_setup'], + 'depends': [ + 'base', + 'base_setup', + 'web', + ], 'data': [ 'data/auth_saml.xml', @@ -49,7 +53,6 @@ 'static/lib/zocial/css/zocial.css', 'static/src/css/auth_saml.css', ], - 'qweb': ['static/src/xml/auth_saml.xml'], 'installable': True, 'auto_install': False, } diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index df9479f64f7..28f1dc019fd 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -5,11 +5,13 @@ import werkzeug.utils import openerp +from openerp import http +from openerp.http import request from openerp import SUPERUSER_ID -import openerp.addons.web.http as oeweb +# import openerp.addons.web.http as oeweb from openerp.addons.web.controllers.main import set_cookie_and_redirect +from openerp.addons.web.controllers.main import ensure_db from openerp.addons.web.controllers.main import login_and_redirect -from openerp.modules.registry import RegistryManager _logger = logging.getLogger(__name__) @@ -40,74 +42,105 @@ def wrapper(self, req, **kw): # ---------------------------------------------------------- # Controller # ---------------------------------------------------------- -class SAMLController(oeweb.Controller): - _cp_path = '/auth_saml' +class SAMLLogin(openerp.addons.web.controllers.main.Home): - @oeweb.jsonrequest - def get_auth_request(self, req, relaystate): + def list_providers(self): + try: + provider_obj = request.registry.get('auth.saml.provider') + providers = provider_obj.search_read( + request.cr, SUPERUSER_ID, [('enabled', '=', True)] + ) + except Exception, e: + _logger.exception("SAML2: %s" % str(e)) + providers = [] + + return providers + + @http.route() + def web_login(self, *args, **kw): + ensure_db() + if ( + request.httprequest.method == 'GET' and + request.session.uid and + request.params.get('redirect') + ): + + # Redirect if already logged in and redirect param is present + return http.redirect_with_hash(request.params.get('redirect')) + + providers = self.list_providers() + + response = super(SAMLLogin, self).web_login(*args, **kw) + if response.is_qweb: + error = request.params.get('saml_error') + if error == '1': + error = _("Sign up is not allowed on this database.") + elif error == '2': + error = _("Access Denied") + elif error == '3': + error = _( + "You do not have access to this database or your " + "invitation has expired. Please ask for an invitation " + "and be sure to follow the link in your invitation email." + ) + else: + error = None + + response.qcontext['providers'] = providers + + if error: + response.qcontext['error'] = error + + return response + + +class AuthSAMLController(http.Controller): + + @http.route('/auth_saml/get_auth_request', type='http', auth='none') + def get_auth_request(self, pid): """state is the JSONified state object and we need to pass it inside our request as the RelayState argument """ - state = simplejson.loads(relaystate) - - dbname = state['d'] - provider_id = state['p'] - context = state.get('c', {}) - registry = RegistryManager.get(dbname) - provider_osv = registry.get('auth.saml.provider') + provider_id = int(pid) + provider_osv = request.registry.get('auth.saml.provider') auth_request = None try: - with registry.cursor() as cr: + with request.registry.cursor() as cr: auth_request = provider_osv._get_auth_request( - cr, SUPERUSER_ID, provider_id, state, context=context + cr, SUPERUSER_ID, provider_id, pid ) except Exception, e: _logger.exception("SAML2: %s" % str(e)) - return {'auth_request': auth_request} - - @oeweb.jsonrequest - def list_providers(self, req, dbname): - l = [] - try: - registry = RegistryManager.get(dbname) - with registry.cursor() as cr: - providers = registry.get('auth.saml.provider') - if providers: - l = providers.read( - cr, SUPERUSER_ID, providers.search( - cr, SUPERUSER_ID, [('enabled', '=', True)] - ), - [ - "id", - "name", - "enabled", - "css_class", - "body", - "sequence", - ], - ) - else: - l = [] - - except Exception, e: - _logger.exception("SAML2: %s" % str(e)) - - return l - - @oeweb.httprequest + # store a RelayState on the request to our IDP so that the IDP + # can send us back this info alongside the obtained token + params = { + "RelayState": simplejson.dumps({ + "d": request.session.db, + "p": pid, + }), + } + url = auth_request + "&" + werkzeug.url_encode(params) + print "*"*35 + print url + print "*"*35 + redirect = werkzeug.utils.redirect(url, 303) + redirect.autocorrect_location_header = True + return redirect + + @http.route('/auth_saml/signin', type='http', auth='none') @fragment_to_query_string def signin(self, req, **kw): - """JS client obtained a saml token and passed it back + """client obtained a saml token and passed it back to us... we need to validate it """ saml_response = kw.get('SAMLResponse', None) - if not kw.get('RelayState', None): + if kw.get('RelayState', None) is None: # here we are in front of a client that went through # some routes that "lost" its relaystate... this can happen # if the client visited his IDP and successfully logged in @@ -119,16 +152,17 @@ def signin(self, req, **kw): return redirect state = simplejson.loads(kw['RelayState']) - dbname = state['d'] - provider = state['p'] - context = state.get('c', {}) - registry = RegistryManager.get(dbname) + print "*"*35 + print state + print "*"*35 + # THIS IS FALSE TODO TODO + provider = state - with registry.cursor() as cr: + with request.registry.cursor() as cr: try: - u = registry.get('res.users') + u = request.registry.get('res.users') credentials = u.auth_saml( - cr, SUPERUSER_ID, provider, saml_response, context=context + cr, SUPERUSER_ID, provider, saml_response ) cr.commit() action = state.get('a') @@ -141,10 +175,10 @@ def signin(self, req, **kw): return login_and_redirect(req, *credentials, redirect_url=url) except AttributeError, e: - print e + # print e # auth_signup is not installed _logger.error("auth_signup not installed on database " - "%s: saml sign up cancelled." % (dbname,)) + "saml sign up cancelled.") url = "/#action=login&saml_error=1" except openerp.exceptions.AccessDenied: @@ -164,6 +198,6 @@ def signin(self, req, **kw): _logger.exception("SAML2: %s" % str(e)) url = "/#action=login&saml_error=2" - return set_cookie_and_redirect(req, url) + return set_cookie_and_redirect(url) # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 5351adbc3a3..a8da3aaac3a 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -62,7 +62,11 @@ def _auth_saml_validate(self, cr, uid, provider, token, context=None): """ return the validation data corresponding to the access token """ p = self.pool.get('auth.saml.provider') - login = p._get_lasso_for_provider(cr, uid, provider, context=context) + # we are not yet logged in, so the userid cannot have access to the + # fields we need yet + login = p._get_lasso_for_provider( + cr, SUPERUSER_ID, provider, context=context + ) try: login.processAuthnResponseMsg(token) diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml index f2dc5f00385..21f3827b0b8 100644 --- a/auth_saml/views/auth_saml.xml +++ b/auth_saml/views/auth_saml.xml @@ -2,6 +2,24 @@ + + + + + diff --git a/static/src/xml/auth_saml.xml b/static/src/xml/auth_saml.xml deleted file mode 100644 index faca9ea961d..00000000000 --- a/static/src/xml/auth_saml.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - From c73f20aebf4691c1f97b2632d63072b8f48ec5d6 Mon Sep 17 00:00:00 2001 From: Vincent Hatakeyama Date: Thu, 4 Jun 2015 14:42:11 +0200 Subject: [PATCH 52/93] Try to fix odoo8 error by adding missing uid. Also report allow admin to have a password --HG-- branch : odoo8 --- auth_saml/model/base_settings.py | 3 ++- auth_saml/model/res_users.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/auth_saml/model/base_settings.py b/auth_saml/model/base_settings.py index 63bcd362e37..7c9dbb9d7c0 100644 --- a/auth_saml/model/base_settings.py +++ b/auth_saml/model/base_settings.py @@ -26,11 +26,12 @@ class base_settings(orm.TransientModel): ), } - def allow_saml_uid_and_internal_password(self, cr, context=None): + def allow_saml_uid_and_internal_password(self, cr, uid, context=None): """Read the allow_saml_uid_and_internal_password setting. Use the admin account to bypass security restrictions. """ + # ignore the uid, the result of the method does not depend on it uid = SUPERUSER_ID config_obj = self.pool['ir.config_parameter'] diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index a8da3aaac3a..8036f22f7f4 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -216,8 +216,10 @@ def write(self, cr, uid, ids, vals, context=None): cr, uid, ids, vals, context=context ) - def _allow_saml_uid_and_internal_password(self, cr, context): + def _allow_saml_uid_and_internal_password(self, cr, uid, context): + if uid == SUPERUSER_ID: + return true setting_obj = self.pool['base.config.settings'] return setting_obj.allow_saml_uid_and_internal_password( - cr, context=context + cr, uid, context=context ) From 35814be49141d03982d592cd60d16fa56a939959 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 16:07:45 +0200 Subject: [PATCH 53/93] Removed unused assets (those were only for version 7) --HG-- branch : odoo8 --- auth_saml/__openerp__.py | 6 - static/lib/zocial/README.md | 31 -- .../lib/zocial/css/zocial-regular-webfont.eot | Bin 28471 -> 0 bytes .../lib/zocial/css/zocial-regular-webfont.svg | 129 ------ .../lib/zocial/css/zocial-regular-webfont.ttf | Bin 41156 -> 0 bytes .../zocial/css/zocial-regular-webfont.woff | Bin 31320 -> 0 bytes static/lib/zocial/css/zocial.css | 420 ------------------ static/lib/zocial/sample.html | 265 ----------- static/src/css/auth_saml.css | 34 -- static/src/js/auth_saml.js | 73 --- 10 files changed, 958 deletions(-) delete mode 100644 static/lib/zocial/README.md delete mode 100644 static/lib/zocial/css/zocial-regular-webfont.eot delete mode 100644 static/lib/zocial/css/zocial-regular-webfont.svg delete mode 100644 static/lib/zocial/css/zocial-regular-webfont.ttf delete mode 100644 static/lib/zocial/css/zocial-regular-webfont.woff delete mode 100644 static/lib/zocial/css/zocial.css delete mode 100644 static/lib/zocial/sample.html delete mode 100644 static/src/css/auth_saml.css delete mode 100644 static/src/js/auth_saml.js diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index c90f7999429..a0777da2460 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -47,12 +47,6 @@ 'views/base_settings.xml', 'views/res_users.xml', ], - - 'js': ['static/src/js/auth_saml.js'], - 'css': [ - 'static/lib/zocial/css/zocial.css', - 'static/src/css/auth_saml.css', - ], 'installable': True, 'auto_install': False, } diff --git a/static/lib/zocial/README.md b/static/lib/zocial/README.md deleted file mode 100644 index 213132677af..00000000000 --- a/static/lib/zocial/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Zocial CSS social buttons - -I basically rewrote this entire set so they are full vector buttons, meaning: - -- @font-face icons -- custom font file for all social icons -- icon font use private unicode spaces for accessibility -- em sizing based on button font-size -- support for about 83 different services -- buttons and icons supported -- no raster images (sweet) -- works splendidly on any browser supporting @font-face -- CSS3 degrades gracefully in IE8 and below etc. -- also includes generic icon-less primary and secondary buttons - -## How to use these buttons - - - -or - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/static/lib/zocial/css/zocial-regular-webfont.ttf b/static/lib/zocial/css/zocial-regular-webfont.ttf deleted file mode 100644 index 94809d3a57ffcbfa1f3760420d745b2381574a05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41156 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 diff --git a/static/lib/zocial/css/zocial-regular-webfont.woff b/static/lib/zocial/css/zocial-regular-webfont.woff deleted file mode 100644 index 1d6c4abc9d902c0f8662cadf3646810bd03639b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31320 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 diff --git a/static/lib/zocial/css/zocial.css b/static/lib/zocial/css/zocial.css deleted file mode 100644 index 89fc7450631..00000000000 --- a/static/lib/zocial/css/zocial.css +++ /dev/null @@ -1,420 +0,0 @@ -@charset "UTF-8"; - -/*! -Code written by Sam Collins (@smcllns) of www.eventasaur.us -You are free to use this work commercially -You are free to extend this work without permissions from the author (just do so tastefully eh?) -Enjoy -*/ - -/* Reference icons from font-files */ - -@font-face { - font-family: 'zocial'; - font-style: normal; - font-weight: normal; - src: url('zocial-regular-webfont.eot'); - src: url('zocial-regular-webfont.eot?#iefix') format('embedded-opentype'), - url('zocial-regular-webfont.woff') format('woff'), - url('zocial-regular-webfont.ttf') format('truetype'), - url('zocial-regular-webfont.svg#ZocialRegular') format('svg'); - unicode-range: U+0-U+10FFFF; -} - -/* Button structure */ - -.zocial, -a.zocial { - border: 1px solid #777; - border-color: rgba(0,0,0,0.2); - border-bottom-color: #333; - border-bottom-color: rgba(0,0,0,0.4); - color: #fff; - -moz-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9); - -webkit-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9); - box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.4), inset 0 0 0.1em rgba(255,255,255,0.9); - cursor: pointer; - display: inline-block; - font: bold 100%/2.1 "Lucida Grande", Tahoma, sans-serif; - padding: 0 .95em 0 0; - text-align: center; - text-decoration: none; - text-shadow: 0 1px 0 rgba(0,0,0,0.5); - white-space: nowrap; - - -moz-user-select: none; - -webkit-user-select: none; - user-select: none; - - position: relative; - - -moz-border-radius: .3em; - -webkit-border-radius: .3em; - border-radius: .3em; -} - -.zocial:before { - content: ""; - border-right: 0.075em solid rgba(0,0,0,0.1); - float: left; - font: 120%/1.65 zocial; - font-style: normal; - font-weight: normal; - margin: 0 0.5em 0 0; - padding: 0 0.5em; - text-align: center; - text-decoration: none; - text-transform: none; - - -moz-box-shadow: 0.075em 0 0 rgba(255,255,255,0.25); - -webkit-box-shadow: 0.075em 0 0 rgba(255,255,255,0.25); - box-shadow: 0.075em 0 0 rgba(255,255,255,0.25); - - -webkit-font-smoothing: antialiased; -} - -.zocial:active { - outline: none; /* outline is visible on :focus */ -} - -/* Buttons can be displayed as standalone icons by adding a class of "icon" */ - -.zocial.icon { - overflow: hidden; - max-width: 2.4em; - padding-left: 0; - padding-right: 0; - max-height: 2.15em; - white-space: nowrap; -} -.zocial.icon:before { - padding: 0; - width: 2em; - height: 2em; - - box-shadow: none; - border: none; -} - -/* Gradients */ - -.zocial { - background-image: -moz-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); - background-image: -ms-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); - background-image: -o-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); - background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.1)), color-stop(49%, rgba(255,255,255,.05)), color-stop(51%, rgba(0,0,0,.05)), to(rgba(0,0,0,.1))); - background-image: -webkit-linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); - background-image: linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.05) 49%, rgba(0,0,0,.05) 51%, rgba(0,0,0,.1)); -} - -.zocial:hover, .zocial:focus { - background-image: -moz-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: -ms-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: -o-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.15)), color-stop(49%, rgba(255,255,255,.15)), color-stop(51%, rgba(0,0,0,.1)), to(rgba(0,0,0,.15))); - background-image: -webkit-linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); - background-image: linear-gradient(rgba(255,255,255,.15) 49%, rgba(0,0,0,.1) 51%, rgba(0,0,0,.15)); -} - -.zocial:active { - background-image: -moz-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); - background-image: -ms-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); - background-image: -o-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); - background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.1)), color-stop(30%, rgba(255,255,255,0)), color-stop(50%, transparent), to(rgba(0,0,0,.1))); - background-image: -webkit-linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); - background-image: linear-gradient(bottom, rgba(255,255,255,.1), rgba(255,255,255,0) 30%, transparent 50%, rgba(0,0,0,.1)); -} - -/* Adjustments for light background buttons */ - -.zocial.bitcoin, -.zocial.cloudapp, -.zocial.dropbox, -.zocial.email, -.zocial.eventful, -.zocial.github, -.zocial.gmail, -.zocial.instapaper, -.zocial.itunes, -.zocial.ninetyninedesigns, -.zocial.openid, -.zocial.plancast, -.zocial.posterous, -.zocial.reddit, -.zocial.secondary, -.zocial.viadeo, -.zocial.weibo, -.zocial.wikipedia { - border: 1px solid #aaa; - border-color: rgba(0,0,0,0.3); - border-bottom-color: #777; - border-bottom-color: rgba(0,0,0,0.5); - -moz-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5); - -webkit-box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5); - box-shadow: inset 0 0.08em 0 rgba(255,255,255,0.7), inset 0 0 0.08em rgba(255,255,255,0.5); - text-shadow: 0 1px 0 rgba(255,255,255,0.8); -} - -/* :hover adjustments for light background buttons */ - -.zocial.bitcoin:focus, -.zocial.bitcoin:hover, -.zocial.dropbox:focus, -.zocial.dropbox:hover, -.zocial.email:focus, -.zocial.email:hover, -.zocial.eventful:focus, -.zocial.eventful:hover, -.zocial.github:focus, -.zocial.github:hover, -.zocial.gmail:focus, -.zocial.gmail:hover, -.zocial.instapaper:focus, -.zocial.instapaper:hover, -.zocial.itunes:focus, -.zocial.itunes:hover, -.zocial.ninetyninedesigns:focus, -.zocial.ninetyninedesigns:hover, -.zocial.openid:focus, -.zocial.openid:hover, -.zocial.plancast:focus, -.zocial.plancast:hover, -.zocial.posterous:focus, -.zocial.posterous:hover, -.zocial.reddit:focus, -.zocial.reddit:hover, -.zocial.secondary:focus, -.zocial.secondary:hover, -.zocial.twitter:focus, -.zocial.viadeo:focus, -.zocial.viadeo:hover, -.zocial.weibo:focus, -.zocial.weibo:hover, -.zocial.wikipedia:focus, -.zocial.wikipedia:hover { - background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.5)), color-stop(49%, rgba(255,255,255,0.2)), color-stop(51%, rgba(0,0,0,0.05)), to(rgba(0,0,0,0.15))); - background-image: -moz-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); - background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); - background-image: -o-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); - background-image: -ms-linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); - background-image: linear-gradient(top, rgba(255,255,255,0.5), rgba(255,255,255,0.2) 49%, rgba(0,0,0,0.05) 51%, rgba(0,0,0,0.15)); -} - -/* :active adjustments for light background buttons */ - -.zocial.bitcoin:active, -.zocial.dropbox:active, -.zocial.email:active, -.zocial.eventful:active, -.zocial.github:active, -.zocial.gmail:active, -.zocial.instapaper:active, -.zocial.itunes:active, -.zocial.ninetyninedesigns:active, -.zocial.openid:active, -.zocial.plancast:active, -.zocial.posterous:active, -.zocial.reddit:active, -.zocial.secondary:active, -.zocial.viadeo:active, -.zocial.weibo:active, -.zocial.wikipedia:active { - background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0)), color-stop(30%, rgba(255,255,255,0)), color-stop(50%, rgba(0,0,0,0)), to(rgba(0,0,0,0.1))); - background-image: -moz-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); - background-image: -webkit-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); - background-image: -o-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); - background-image: -ms-linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); - background-image: linear-gradient(bottom, rgba(255,255,255,0), rgba(255,255,255,0) 30%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.1)); -} - -/* Button icon and color */ -/* Icon characters are stored in unicode private area */ -.zocial.amazon:before {content: "\E040";} -.zocial.android:before {content: "\E005";} -.zocial.angellist:before {content: "\E06C";} -.zocial.aol:before {content: "\E001";} -.zocial.appstore:before {content: "\E020";} -.zocial.bitcoin:before {content: "\E011"; color: #f7931a;} -.zocial.blogger:before {content: "\E021";} -.zocial.call:before {content: "\E016";} -.zocial.cal:before {content: "\E00D";} -.zocial.cart:before {content: "\E06A";} -.zocial.chrome:before {content: "\E03A";} -.zocial.cloudapp:before {content: "\E042";} -.zocial.creativecommons:before {content: "\E022";} -.zocial.delicious:before {content: "\E002";} -.zocial.digg:before {content: "\E01A";} -.zocial.disqus:before {content: "\E030";} -.zocial.dribbble:before {content: "\E023";} -.zocial.dropbox:before {content: "\E043"; color: #1f75cc;} -.zocial.email:before {content: "\E03C"; color: #312c2a;} -.zocial.eventasaurus:before {content: "\E055"; color: #9de428;} -.zocial.eventbrite:before {content: "\E05B";} -.zocial.eventful:before {content: "\E006"; color: #0066CC;} -.zocial.evernote:before {content: "\E024";} -.zocial.facebook:before {content: "\E045";} -.zocial.fivehundredpx:before {content: "\E00F"; color: #29b6ff;} -.zocial.flattr:before {content: "\E004";} -.zocial.flickr:before {content: "\E025";} -.zocial.forrst:before {content: "\E019"; color: #50894f;} -.zocial.foursquare:before {content: "\E013";} -.zocial.github:before {content: "\E046";} -.zocial.gmail:before {content: "\E04C"; color: #f00;} -.zocial.google:before {content: "\E026";} -.zocial.googleplay:before {content: "\E05E";} -.zocial.googleplus:before {content: "\E00A";} -.zocial.gowalla:before {content: "\E01F";} -.zocial.grooveshark:before {content: "\E017";} -.zocial.guest:before {content: "\E01E";} -.zocial.html5:before {content: "\E014";} -.zocial.ie:before {content: "\E015";} -.zocial.instagram:before {content: "\E06D";} -.zocial.instapaper:before {content: "\E028";} -.zocial.intensedebate:before {content: "\E05A";} -.zocial.itunes:before {content: "\E048"; color: #1a6dd2;} -.zocial.klout:before {content: "\E02A"; } -.zocial.lanyrd:before {content: "\E00C";} -.zocial.lastfm:before {content: "\E04B";} -.zocial.linkedin:before {content: "\E02B";} -.zocial.macstore:before {content: "\E03D";} -.zocial.meetup:before {content: "\E02C";} -.zocial.myspace:before {content: "\E03E";} -.zocial.ninetyninedesigns:before {content: "\E018"; color: #f50;} -.zocial.openid:before {content: "\E04E"; color: #ff921d;} -.zocial.opentable:before {content: "\E05F";} -.zocial.paypal:before {content: "\E003";} -.zocial.pinboard:before {content: "\E04D";} -.zocial.pinterest:before {content: "\E010";} -.zocial.plancast:before {content: "\E02F";} -.zocial.plurk:before {content: "\E049";} -.zocial.podcast:before {content: "\E03F";} -.zocial.posterous:before {content: "\E05D";} -.zocial.print:before {content: "\E06B";} -.zocial.quora:before {content: "\E050";} -.zocial.reddit:before {content: "\E01D"; color: red;} -.zocial.rss:before {content: "\E031";} -.zocial.scribd:before {content: "\E05C"; color: #00d5ea;} -.zocial.skype:before {content: "\E032";} -.zocial.smashing:before {content: "\E009";} -.zocial.songkick:before {content: "\E04A";} -.zocial.soundcloud:before {content: "\E052";} -.zocial.spotify:before {content: "\E01C";} -.zocial.stumbleupon:before {content: "\E00E";} -.zocial.tumblr:before {content: "\E053";} -.zocial.twitter:before {content: "\E033";} -.zocial.viadeo:before {content: "\E027"; color: #f59b20;} -.zocial.vimeo:before {content: "\E035";} -.zocial.weibo:before {content: "\E029"; color: #e6162d;} -.zocial.wikipedia:before {content: "\E00B";} -.zocial.windows:before {content: "\E036";} -.zocial.xing:before {content: "\E037"} -.zocial.wordpress:before {content: "\E056";} -.zocial.yahoo:before {content: "\E038";} -.zocial.yelp:before {content: "\E058";} -.zocial.youtube:before {content: "\E034";} - -/* Button background and text color */ - -.zocial.amazon {background-color: #ffad1d; color: #030037; text-shadow: 0 1px 0 rgba(255,255,255,0.5);} -.zocial.android {background-color: #a4c639;} -.zocial.angellist {background-color: #000;} -.zocial.aol {background-color: #f00;} -.zocial.appstore {background-color: #000;} -.zocial.bitcoin {background-color: #efefef; color: #4d4d4d;} -.zocial.blogger {background-color: #ee5a22;} -.zocial.call {background-color: #008000;} -.zocial.cal {background-color: #d63538;} -.zocial.cart {background-color: #333;} -.zocial.chrome {background-color: #006cd4;} -.zocial.cloudapp {background-color: #fff; color: #312c2a;} -.zocial.creativecommons {background-color: #000;} -.zocial.delicious {background-color: #3271cb;} -.zocial.digg {background-color: #164673;} -.zocial.disqus {background-color: #5d8aad;} -.zocial.dribbble {background-color: #ea4c89;} -.zocial.dropbox {background-color: #fff; color: #312c2a;} -.zocial.email {background-color: #f0f0eb; color: #312c2a;} -.zocial.eventasaurus {background-color: #192931; color: #fff;} -.zocial.eventbrite {background-color: #ff5616;} -.zocial.eventful {background-color: #fff; color: #47ab15;} -.zocial.evernote {background-color: #6bb130; color: #fff;} -.zocial.facebook {background-color: #4863ae;} -.zocial.fivehundredpx {background-color: #333;} -.zocial.flattr {background-color: #8aba42;} -.zocial.flickr {background-color: #ff0084;} -.zocial.forrst {background-color: #1e360d;} -.zocial.foursquare {background-color: #44a8e0;} -.zocial.github {background-color: #fbfbfb; color: #050505;} -.zocial.gmail {background-color: #efefef; color: #222;} -.zocial.google {background-color: #4e6cf7;} -.zocial.googleplay {background-color: #000;} -.zocial.googleplus {background-color: #dd4b39;} -.zocial.gowalla {background-color: #ff720a;} -.zocial.grooveshark {background-color: #111; color:#eee;} -.zocial.guest {background-color: #1b4d6d;} -.zocial.html5 {background-color: #ff3617;} -.zocial.ie {background-color: #00a1d9;} -.zocial.instapaper {background-color: #eee; color: #222;} -.zocial.instagram {background-color: #3f729b;} -.zocial.intensedebate {background-color: #0099e1;} -.zocial.klout {background-color: #e34a25;} -.zocial.itunes {background-color: #efefeb; color: #312c2a;} -.zocial.lanyrd {background-color: #2e6ac2;} -.zocial.lastfm {background-color: #dc1a23;} -.zocial.linkedin {background-color: #0083a8;} -.zocial.macstore {background-color: #007dcb} -.zocial.meetup {background-color: #ff0026;} -.zocial.myspace {background-color: #000;} -.zocial.ninetyninedesigns {background-color: #fff; color: #072243;} -.zocial.openid {background-color: #f5f5f5; color: #333;} -.zocial.opentable {background-color: #990000;} -.zocial.paypal {background-color: #fff; color: #32689a; text-shadow: 0 1px 0 rgba(255,255,255,0.5);} -.zocial.pinboard {background-color: blue;} -.zocial.pinterest {background-color: #c91618;} -.zocial.plancast {background-color: #e7ebed; color: #333;} -.zocial.plurk {background-color: #cf682f;} -.zocial.podcast {background-color: #9365ce;} -.zocial.posterous {background-color: #ffd959; color: #bc7134;} -.zocial.print {background-color: #f0f0eb; color: #222; text-shadow: 0 1px 0 rgba(255,255,255,0.8);} -.zocial.quora {background-color: #a82400;} -.zocial.reddit {background-color: #fff; color: #222;} -.zocial.rss {background-color: #ff7f25;} -.zocial.scribd {background-color: #231c1a;} -.zocial.skype {background-color: #00a2ed;} -.zocial.smashing {background-color: #ff4f27;} -.zocial.songkick {background-color: #ff0050;} -.zocial.soundcloud {background-color: #ff4500;} -.zocial.spotify {background-color: #60af00;} -.zocial.stumbleupon {background-color: #eb4924;} -.zocial.tumblr {background-color: #374a61;} -.zocial.twitter {background-color: #46c0fb;} -.zocial.viadeo {background-color: #fff; color: #000;} -.zocial.vimeo {background-color: #00a2cd;} -.zocial.weibo {background-color: #faf6f1; color: #000;} -.zocial.wikipedia {background-color: #fff; color: #000;} -.zocial.windows {background-color: #0052a4; color: #fff;} -.zocial.wordpress {background-color: #464646;} -.zocial.xing {background-color: #0A5D5E;} -.zocial.yahoo {background-color: #a200c2;} -.zocial.yelp {background-color: #e60010;} -.zocial.youtube {background-color: #f00;} - -/* -The Miscellaneous Buttons -These button have no icons and can be general purpose buttons while ensuring consistent button style -Credit to @guillermovs for suggesting -*/ - -.zocial.primary, .zocial.secondary {margin: 0.1em 0; padding: 0 1em;} -.zocial.primary:before, .zocial.secondary:before {display: none;} -.zocial.primary {background-color: #333;} -.zocial.secondary {background-color: #f0f0eb; color: #222; text-shadow: 0 1px 0 rgba(255,255,255,0.8);} - -/* Any browser-specific adjustments */ - -button:-moz-focus-inner { - border: 0; - padding: 0; -} - - diff --git a/static/lib/zocial/sample.html b/static/lib/zocial/sample.html deleted file mode 100644 index 98dec554ff1..00000000000 --- a/static/lib/zocial/sample.html +++ /dev/null @@ -1,265 +0,0 @@ - - - - - Zocial CSS3 Buttons - - - - - - - - - - - - - Sign in with Google+ - - Sign in with LinkedIn - -

The Cool Kids

- Sync with Dropbox - Clip to Evernote - Follow me on Forrst - Sign in with Dribbble - Sign in to CloudApp - Fork me on Github - Play on Spotify - Read It Later - Follow me on Soundcloud - Follow me on Tumblr - Read on Smashing Magazine - Available on iTunes - Available on the App Store - Available on the Mac App Store - Available on Android Market - Follow me on Pinterest - Follow me on Quora - Attend on Lanyrd - -

The Not-So-Cool But Have-To-Be-Invited Kids

- Pay with Paypal - Sign in with Amazon - Call me on Skype - Sign in with Last.fm - Write a review on Yelp - Check in with foursquare - Influence with Klout - -

The Older (but Solid) Kids

- View on Wikipedia - Sign in with Disqus - Sign in with IntenseDebate - Sign in with Google - Sign in with Gmail - Upload to Vimeo - Read more on Scribd - Subscribe on YouTube - Sign in with WordPress - Sign in with Songkick - Sign in with Posterous - Sign in with Eventbrite - Tip with Flattr - Follow me on Plancast - - -

The Kids That Kinda Smell but Some People Don't Mind

- Submit resume for CEO - Download Internet Explorer 5 - Report bugs with Meetup.com - Learn how-to-use OpenID - Register now for HTML6 - Chat with your parents - -

The Kids That Nobody Normally Notices

- Sign in as guest - View Creative Commons Licence - Subscribe to RSS - Add to Chrome - -

The He's-My-Son-So-Of-Course-He's-Here Kid

- Sign up for Eventasaurus - -

Kids By Request

- Join me on Weibo - Sign in with Plurk - Play on Grooveshark - Post on Blogger - Sign in with Viadeo - Subscribe to this Podcast - View Portfolio on 500px - Bitcoin accepted here - View Portfolio on 99Designs - Bookmark with Pinboard - Stumble! - Find me on Myspace - Sign in with Windows Live - Find Events with Eventful - Sign in with Xing - Upload to Flickr - Sign in with Del.icio.us - Download from Google Play - Reserve with OpenTable - Digg this - Share on Reddit - Fund us on AngelList - Sign-in with Instagram - -

The Multi-Purpose Kids (Credit: Pictos Icons)

- - Call a phone - - Add to calendar - Add to cart - Print this page - Primary action - Secondary action - - -

Icon versions of the above

- - Sign in with Google+ - - Sign in with Google - Sign in with LinkedIn - Pay with Paypal - Sign in with Amazon - Sync with Dropbox - Clip to Evernote - Call me on Skype - Sign in as guest - Play on Spotify - Sign in with Last.fm - Sign in with Songkick - Follow me on Forrst - Sign in with Dribbble - Sign in to CloudApp - Fork me on Github - Follow me on Pinterest - Follow me on Quora - Bookmark with Pinboard - Attend on Lanyrd - Download on iTunes - Download on Android - Sign in with Disqus - Sign in with Yahoo - Upload to Vimeo - Add to Chrome - Get a new browser - Made from HTML5 - Read It Later - Read more on Scribd - View on Wikipedia - Tip with Flattr - Follow me on Tumblr - Subscribe to my Posterous - Check in with Gowalla - Check in with foursquare - Write a review on Yelp - Follow me on Soundcloud - Read on Smashing Magazine - Sign in with WordPress - Sign in with IntenseDebate - Sign in with OpenID - Sign in with Gmail - Sign in with Eventbrite - Sign in with Eventasaurus - Sign in with Meetup.com - Sign in with AIM - Follow me on Plancast - Subscribe on YouTube - Available on the Mac App Store - View Creative Commons Licence - Subscribe to RSS - Follow me on Weibo - Follow me on Plurk - Follow me on Grooveshark - Post on Blogger - Sign in with Viadeo - Subscribe to this Podcast - View Portfolio on 500px - Bitcoin accepted here - View Portfolio on 99Designs - Stumble! - Download on iTunes - Find me on Myspace - Sign in with Windows Live - Find Events with Eventful - Influence with Klout - Sign in with Xing - Upload to Flickr - Sign in with Del.icio.us - Download from Google Play - Reserve with OpenTable - Digg this - Share on Reddit - Fund us on AngelList - Sign-in with Instagram - Call a phone - - Add to calendar - Print this page - Add to cart - - - -

Thanks to @guillermovs, @kamens, @vizualover, and @leaverou for code refinements and suggestions.

- -

Massive thanks to @drewwilson for making the multi-purpose buttons possible with his incredible Pictos icons.

- -

See code samples at zocial.smcllns.com — ask questions to @smcllns

- - - - - - - - diff --git a/static/src/css/auth_saml.css b/static/src/css/auth_saml.css deleted file mode 100644 index 475ec7d7b36..00000000000 --- a/static/src/css/auth_saml.css +++ /dev/null @@ -1,34 +0,0 @@ -.openerp .oe_application .zocial { - font: white; -} - -.openerp .zocial.openerp:before { - content: "\E02E"; - font-style: italic; - text-shadow: 0 1px 1px black; -} - -.openerp a.zocial.openerp { - border: 1px solid #222222; - color: white; - margin: 0; - background-color: #b92020; - background-image: -webkit-gradient(linear, left top, left bottom, from(#b92020), to(#600606)); - background-image: -webkit-linear-gradient(top, #b92020, #600606); - background-image: -moz-linear-gradient(top, #b92020, #600606); - background-image: -ms-linear-gradient(top, #b92020, #600606); - background-image: -o-linear-gradient(top, #b92020, #600606); - background-image: linear-gradient(to bottom, #b92020, #600606); - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - border-radius: 4px; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 1px rgba(155, 155, 155, 0.4) inset; - text-shadow: none; - font-weight: normal; -} - -.openerp .oe_login .oe_saml_provider_login_button { - margin-top: 4px; -} diff --git a/static/src/js/auth_saml.js b/static/src/js/auth_saml.js deleted file mode 100644 index 1b3983fda48..00000000000 --- a/static/src/js/auth_saml.js +++ /dev/null @@ -1,73 +0,0 @@ -openerp.auth_saml = function(instance) { - var _t = instance.web._t, - _lt = instance.web._lt; - var QWeb = instance.web.qweb; - - instance.web.Login.include({ - start: function(parent, params) { - var self = this; - var d = this._super.apply(this, arguments); - this.$el.hide(); - this.$el.on('click', 'a.zocial', this.on_saml_sign_in); - this.saml_providers = []; - if(this.params.saml_error === 1) { - this.do_warn(_t("Sign up error"),_t("Sign up is not allowed on this database."), true); - } else if(this.params.saml_error === 2) { - this.do_warn(_t("Authentication error"),_t("Access Denied"), true); - } else if(this.params.saml_error === 3) { - this.do_warn(_t("Authentication error"),_t("You do not have access to this database or your invitation has expired. Please ask for an invitation and be sure to follow the link in your invitation email."), true); - } - return d.done(this.do_saml_load).fail(function() { - self.do_saml_load([]); - }); - }, - on_db_loaded: function(result) { - this._super.apply(this, arguments); - this.$("form [name=db]").change(this.do_saml_load); - }, - do_saml_load: function() { - var db = this.$("form [name=db]").val(); - if (db) { - this.rpc("/auth_saml/list_providers", { dbname: db }).done(this.on_saml_loaded); - } else { - this.$el.show(); - } - }, - on_saml_loaded: function(result) { - this.saml_providers = result; - var params = $.deparam($.param.querystring()); - if (this.saml_providers.length === 1 && params.type === 'signup') { - this.do_saml_sign_in(this.saml_providers[0]); - } else { - this.$el.show(); - this.$('.oe_saml_provider_login_button').remove(); - var buttons = QWeb.render("auth_saml.Login.button",{"widget":this}); - this.$(".oe_login_pane form ul").after(buttons); - } - }, - on_saml_sign_in: function(ev) { - ev.preventDefault(); - var index = $(ev.target).data('index'); - var provider = this.saml_providers[index]; - return this.do_saml_sign_in(provider); - }, - do_saml_sign_in: function(provider) { - var state = this._saml_state(provider); - this.rpc("/auth_saml/get_auth_request", { relaystate: JSON.stringify(state) }).done(this.on_request_loaded); - }, - on_request_loaded: function(result) { - // redirect to the saml idp - //instance.web.redirect(result.auth_request); - window.location.replace(result.auth_request); - }, - _saml_state: function(provider) { - // return the state object sent back with the redirected uri - var dbname = this.$("form [name=db]").val(); - return { - d: dbname, - p: provider.id, - }; - }, - }); - -}; From b38709b6ca9a2b90db614a4805e8b4d47347b2e7 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 16:08:34 +0200 Subject: [PATCH 54/93] Proper state handling and removed debug prints --HG-- branch : odoo8 --- auth_saml/controllers/main.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 28f1dc019fd..2c9e44c9452 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -96,6 +96,21 @@ def web_login(self, *args, **kw): class AuthSAMLController(http.Controller): + def get_state(self, provider_id): + redirect = request.params.get('redirect') or 'web' + if not redirect.startswith(('//', 'http://', 'https://')): + redirect = '%s%s' % ( + request.httprequest.url_root, + redirect[1:] if redirect[0] == '/' else redirect + ) + + state = { + "d": request.session.db, + "p": provider_id, + "r": werkzeug.url_quote_plus(redirect), + } + return state + @http.route('/auth_saml/get_auth_request', type='http', auth='none') def get_auth_request(self, pid): """state is the JSONified state object and we need to pass @@ -106,11 +121,12 @@ def get_auth_request(self, pid): provider_osv = request.registry.get('auth.saml.provider') auth_request = None + state = self.get_state(provider_id) try: with request.registry.cursor() as cr: auth_request = provider_osv._get_auth_request( - cr, SUPERUSER_ID, provider_id, pid + cr, SUPERUSER_ID, provider_id, state=state ) except Exception, e: @@ -125,9 +141,6 @@ def get_auth_request(self, pid): }), } url = auth_request + "&" + werkzeug.url_encode(params) - print "*"*35 - print url - print "*"*35 redirect = werkzeug.utils.redirect(url, 303) redirect.autocorrect_location_header = True return redirect @@ -152,11 +165,7 @@ def signin(self, req, **kw): return redirect state = simplejson.loads(kw['RelayState']) - print "*"*35 - print state - print "*"*35 - # THIS IS FALSE TODO TODO - provider = state + provider = state['p'] with request.registry.cursor() as cr: try: @@ -172,10 +181,10 @@ def signin(self, req, **kw): url = '/#action=%s' % action elif menu: url = '/#menu_id=%s' % menu - return login_and_redirect(req, *credentials, redirect_url=url) + + return login_and_redirect(*credentials, redirect_url=url) except AttributeError, e: - # print e # auth_signup is not installed _logger.error("auth_signup not installed on database " "saml sign up cancelled.") From 42f1e6ab0e6fce48213aaa5abfea5a55e922469e Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 16:09:10 +0200 Subject: [PATCH 55/93] Fixed Version 8 problems (old API function signatures) --HG-- branch : odoo8 --- auth_saml/model/auth_saml.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 42822686944..c1878769d80 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -2,6 +2,7 @@ from openerp.osv import osv import lasso import simplejson +from openerp import SUPERUSER_ID class auth_saml_provider(osv.osv): @@ -11,8 +12,9 @@ class auth_saml_provider(osv.osv): _description = 'SAML2 provider' _order = 'name' - def _get_lasso_for_provider(self, cr, uid, provider_id, context=None): - provider = self.browse(cr, uid, provider_id, context=context) + def _get_lasso_for_provider(self, cr, uid, pid, context=None): + # user is not connected yet... so use SUPERUSER_ID + provider = self.browse(cr, SUPERUSER_ID, pid, context=context) # TODO: we should cache those results somewhere because it is # really costy to always recreate a login variable from buffers @@ -26,11 +28,11 @@ def _get_lasso_for_provider(self, cr, uid, provider_id, context=None): ) return lasso.Login(server) - def _get_auth_request(self, cr, uid, id_, state, context=None): + def _get_auth_request(self, cr, uid, pid, state, context=None): """build an authentication request and give it back to our client WARNING: this method cannot be used for multiple ids """ - login = self._get_lasso_for_provider(cr, uid, id_, context=context) + login = self._get_lasso_for_provider(cr, uid, pid, context=context) # ! -- this is the part that MUST be performed on each call and # cannot be cached From 3d7c2d1519df10aedcd1564e64a74394252c5a62 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 16:09:40 +0200 Subject: [PATCH 56/93] Fixed version 8 function signature --HG-- branch : odoo8 --- auth_saml/model/res_users.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 8036f22f7f4..334e5b2ebf6 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -1,6 +1,9 @@ # -*- encoding: utf-8 -*- import logging +# this is our very own dependency import lasso +# this is an odoo8 dep so it should be present 'by default' +import passlib import openerp from openerp.osv import osv, fields @@ -28,7 +31,7 @@ def _no_password_with_saml(self, cr, uid, ids, context=None): password. """ - if self._allow_saml_uid_and_internal_password(cr, context): + if self._allow_saml_uid_and_internal_password(cr, uid, context): # The constraint is a no-op in this case. return True @@ -43,7 +46,7 @@ def _no_password_with_saml(self, cr, uid, ids, context=None): ( _no_password_with_saml, ( - 'SAML2 authentication: An Odoo user cannot posess both an ' + 'SAML2 authentication: An Odoo user cannot possess both a ' 'SAML user ID and an Odoo password.' ), ['password', 'saml_uid'] @@ -58,20 +61,22 @@ def _no_password_with_saml(self, cr, uid, ids, context=None): ), ] - def _auth_saml_validate(self, cr, uid, provider, token, context=None): + def _auth_saml_validate(self, cr, uid, provider_id, token, context=None): """ return the validation data corresponding to the access token """ p = self.pool.get('auth.saml.provider') # we are not yet logged in, so the userid cannot have access to the # fields we need yet login = p._get_lasso_for_provider( - cr, SUPERUSER_ID, provider, context=context + cr, SUPERUSER_ID, provider_id, context=context ) try: login.processAuthnResponseMsg(token) except (lasso.DsError, lasso.ProfileCannotVerifySignatureError): raise Exception('Lasso Profile cannot verify signature') + except lasso.ProfileStatusNotSuccessError: + raise Exception('Profile Status Not Success Error') except lasso.Error, e: raise Exception(repr(e)) @@ -186,7 +191,7 @@ def check_credentials(self, cr, uid, token): try: super(res_users, self).check_credentials(cr, uid, token) - except openerp.exceptions.AccessDenied: + except (openerp.exceptions.AccessDenied, passlib.exc.PasswordSizeError): # since normal auth did not succeed we now try to find if the user # has an active token attached to his uid res = token_osv.search( @@ -209,7 +214,7 @@ def write(self, cr, uid, ids, vals, context=None): """ if vals and vals.get('saml_uid'): - if not self._allow_saml_uid_and_internal_password(cr, context): + if not self._allow_saml_uid_and_internal_password(cr, uid, context): vals['password'] = False return super(res_users, self).write( @@ -217,6 +222,10 @@ def write(self, cr, uid, ids, vals, context=None): ) def _allow_saml_uid_and_internal_password(self, cr, uid, context): + + # super user is always allowed to have a password in the database + # as opposed to other users... Doing so avoids being locked out + # of your own instance in case there is an issue with your IDP if uid == SUPERUSER_ID: return true setting_obj = self.pool['base.config.settings'] From c3bf0cff1042ab0fb2f20836874d66c9c3a438a0 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 16:09:48 +0200 Subject: [PATCH 57/93] Fixed bad typo :) --HG-- branch : odoo8 --- auth_saml/model/res_users.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 334e5b2ebf6..b751bfadd64 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -227,7 +227,8 @@ def _allow_saml_uid_and_internal_password(self, cr, uid, context): # as opposed to other users... Doing so avoids being locked out # of your own instance in case there is an issue with your IDP if uid == SUPERUSER_ID: - return true + return True + setting_obj = self.pool['base.config.settings'] return setting_obj.allow_saml_uid_and_internal_password( cr, uid, context=context From cede5d2a2876c46cf832665cb72b0e5757724a49 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Thu, 4 Jun 2015 21:31:13 +0200 Subject: [PATCH 58/93] First v8 API batch --HG-- branch : odoo8 --- auth_saml/controllers/main.py | 9 +-- auth_saml/model/auth_saml.py | 56 ++++++++---------- auth_saml/model/res_users.py | 103 +++++++++++++++------------------- 3 files changed, 73 insertions(+), 95 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 2c9e44c9452..9ab1e0dd1ad 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -124,14 +124,15 @@ def get_auth_request(self, pid): state = self.get_state(provider_id) try: - with request.registry.cursor() as cr: - auth_request = provider_osv._get_auth_request( - cr, SUPERUSER_ID, provider_id, state=state - ) + auth_request = provider_osv._get_auth_request( + request.cr, SUPERUSER_ID, provider_id, state + ) except Exception, e: _logger.exception("SAML2: %s" % str(e)) + # TODO: handle case when auth_request comes back as None + # store a RelayState on the request to our IDP so that the IDP # can send us back this info alongside the obtained token params = { diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index c1878769d80..750ba317831 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -1,38 +1,39 @@ -from openerp.osv import fields -from openerp.osv import osv +from openerp import models +from openerp import api +from openerp import fields import lasso import simplejson -from openerp import SUPERUSER_ID -class auth_saml_provider(osv.osv): +class auth_saml_provider(models.Model): """Class defining the configuration values of an Saml2 provider""" _name = 'auth.saml.provider' _description = 'SAML2 provider' _order = 'name' - def _get_lasso_for_provider(self, cr, uid, pid, context=None): + @api.multi + def _get_lasso_for_provider(self): # user is not connected yet... so use SUPERUSER_ID - provider = self.browse(cr, SUPERUSER_ID, pid, context=context) # TODO: we should cache those results somewhere because it is # really costy to always recreate a login variable from buffers server = lasso.Server.newFromBuffers( - provider.sp_metadata, - provider.sp_pkey + self.sp_metadata, + self.sp_pkey ) server.addProviderFromBuffer( lasso.PROVIDER_ROLE_IDP, - provider.idp_metadata + self.idp_metadata ) return lasso.Login(server) - def _get_auth_request(self, cr, uid, pid, state, context=None): + @api.multi + def _get_auth_request(self, state): """build an authentication request and give it back to our client WARNING: this method cannot be used for multiple ids """ - login = self._get_lasso_for_provider(cr, uid, pid, context=context) + login = self._get_lasso_for_provider() # ! -- this is the part that MUST be performed on each call and # cannot be cached @@ -46,25 +47,14 @@ def _get_auth_request(self, cr, uid, pid, state, context=None): # obtained after the buildAuthnRequestMsg() call return login.msgUrl - _columns = { - # Name of the OAuth2 entity, authentic, xcg... - 'name': fields.char('Provider name'), - 'idp_metadata': fields.text('IDP Configuration'), - 'sp_metadata': fields.text('SP Configuration'), - 'sp_pkey': fields.text( - 'Private key of our service provider (this openerpserver)' - ), - 'enabled': fields.boolean('Enabled'), - 'css_class': fields.char('CSS class'), - 'body': fields.char( - 'Body', - required=True, - ), - 'sequence': fields.integer(), - } - - _defaults = { - 'enabled': False, - 'css_class': 'zocial saml', - 'body': 'Authentic', - } + # Name of the OAuth2 entity, authentic, xcg... + name = fields.Char('Provider name') + idp_metadata = fields.Text('IDP Configuration') + sp_metadata = fields.Text('SP Configuration') + sp_pkey = fields.Text( + 'Private key of our service provider (this openerpserver)' + ) + enabled = fields.Boolean('Enabled', default=False) + sequence = fields.Integer('Sequence') + css_class = fields.Char('CSS Class') + body = fields.Char('Body') diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index b751bfadd64..3663578d775 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -6,37 +6,38 @@ import passlib import openerp -from openerp.osv import osv, fields +from openerp import api +from openerp import models +from openerp import fields from openerp import SUPERUSER_ID +from openerp.addons.base.res.res_users import res_users as baseuser _logger = logging.getLogger(__name__) -class res_users(osv.Model): +class res_users(models.Model): _inherit = 'res.users' - _columns = { - 'saml_provider_id': fields.many2one( - 'auth.saml.provider', - string='SAML Provider', - ), - 'saml_uid': fields.char( - 'SAML User ID', - help="SAML Provider user_id", - ), - } - - def _no_password_with_saml(self, cr, uid, ids, context=None): + saml_provider_id = fields.Many2one( + 'auth.saml.provider', + string='SAML Provider', + ) + saml_uid = fields.Char( + 'SAML User ID', + help="SAML Provider user_id", + ) + + @api.multi + def _no_password_with_saml(self): """Ensure no Odoo user posesses both an SAML user ID and an Odoo password. """ - if self._allow_saml_uid_and_internal_password(cr, uid, context): + if self._allow_saml_uid_and_internal_password(): # The constraint is a no-op in this case. return True - users = self.browse(cr, uid, ids, context=context) - for user in users: + for user in self: if user.password and user.saml_uid: return False @@ -61,15 +62,16 @@ def _no_password_with_saml(self, cr, uid, ids, context=None): ), ] - def _auth_saml_validate(self, cr, uid, provider_id, token, context=None): + @api.multi + def _auth_saml_validate(self, provider_id, token): """ return the validation data corresponding to the access token """ - p = self.pool.get('auth.saml.provider') + pobj = self.env['auth.saml.provider'] + p = pobj.browse(provider_id) + # we are not yet logged in, so the userid cannot have access to the # fields we need yet - login = p._get_lasso_for_provider( - cr, SUPERUSER_ID, provider_id, context=context - ) + login = p.sudo()._get_lasso_for_provider() try: login.processAuthnResponseMsg(token) @@ -87,19 +89,10 @@ def _auth_saml_validate(self, cr, uid, provider_id, token, context=None): # TODO use a real token validation from LASSO # TODO push into the validation result a real UPN - validation = {'user_id': login.assertion.subject.nameId.content} - - """ - if p.data_endpoint: - data = self._auth_oauth_rpc(cr, uid, p.data_endpoint, access_token) - validation.update(data) - """ - - return validation + return {'user_id': login.assertion.subject.nameId.content} - def _auth_saml_signin( - self, cr, uid, provider, validation, saml_response, context=None - ): + @api.multi + def _auth_saml_signin(self, provider, validation, saml_response): """ retrieve and sign into openerp the user corresponding to provider and validated access token @@ -111,11 +104,10 @@ def _auth_saml_signin( This method can be overridden to add alternative signin methods. """ - token_osv = self.pool.get('auth_saml.token') + token_osv = self.env['auth_saml.token'] saml_uid = validation['user_id'] user_ids = self.search( - cr, uid, [ ("saml_uid", "=", saml_uid), ('saml_provider_id', '=', provider), @@ -128,59 +120,48 @@ def _auth_saml_signin( # TODO replace assert by proper raise... asserts do not execute in # production code... assert len(user_ids) == 1 - - # browse the user because we'll need this in the response - user = self.browse(cr, uid, user_ids[0], context=context) - - user_id = user.id + user = user_ids[0] # now find if a token for this user/provider already exists token_ids = token_osv.search( - cr, uid, [ ('saml_provider_id', '=', provider), - ('user_id', '=', user_id), + ('user_id', '=', user.id), ] ) + if token_ids: - token_osv.write( - cr, uid, token_ids, + token_ids.write( {'saml_access_token': saml_response}, - context=context ) else: token_osv.create( - cr, uid, { 'saml_access_token': saml_response, 'saml_provider_id': provider, - 'user_id': user_id, + 'user_id': user.id, }, - context=context ) return user.login - def auth_saml(self, cr, uid, provider, saml_response, context=None): + @api.model + def auth_saml(self, provider, saml_response): - validation = self._auth_saml_validate( - cr, uid, provider, saml_response - ) + validation = self._auth_saml_validate(provider, saml_response) # required check if not validation.get('user_id'): raise openerp.exceptions.AccessDenied() # retrieve and sign in user - login = self._auth_saml_signin( - cr, uid, provider, validation, saml_response, context=context - ) + login = self._auth_saml_signin(provider, validation, saml_response) if not login: raise openerp.exceptions.AccessDenied() # return user credentials - return cr.dbname, login, saml_response + return self.env.cr.dbname, login, saml_response def check_credentials(self, cr, uid, token): """token can be a password if the user has used the normal form... @@ -188,8 +169,14 @@ def check_credentials(self, cr, uid, token): and the interesting code is inside the except clause """ token_osv = self.pool.get('auth_saml.token') + try: - super(res_users, self).check_credentials(cr, uid, token) + baseuser.check_credentials( + self, + cr, + uid, + token + ) except (openerp.exceptions.AccessDenied, passlib.exc.PasswordSizeError): # since normal auth did not succeed we now try to find if the user From 23c658ae24ba63ea44c4d4786a3271690e109d23 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Fri, 5 Jun 2015 16:09:25 +0200 Subject: [PATCH 59/93] Porting to v8 API second round. --HG-- branch : odoo8 --- auth_saml/__openerp__.py | 7 +++- auth_saml/model/base_settings.py | 53 +++++++++--------------- auth_saml/model/res_users.py | 71 ++++++++++++++------------------ 3 files changed, 58 insertions(+), 73 deletions(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index a0777da2460..6329227bcc3 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -26,7 +26,11 @@ 'category': 'Tools', 'description': """ Allow users to login through Saml2 Provider. -=================================== +============================================ + +WARNING: this module requires auth_crypt. This is because you still have the + option if not recommended to allow users to have a password stored in odoo + at the same time as having a SALM provider and id. """, 'author': 'XCG Consulting s.a.s.', 'maintainer': 'XCG Consulting s.a.s.', @@ -35,6 +39,7 @@ 'base', 'base_setup', 'web', + 'auth_crypt', ], 'data': [ diff --git a/auth_saml/model/base_settings.py b/auth_saml/model/base_settings.py index 7c9dbb9d7c0..ccd54d41c37 100644 --- a/auth_saml/model/base_settings.py +++ b/auth_saml/model/base_settings.py @@ -1,6 +1,6 @@ -from openerp import SUPERUSER_ID from openerp.osv import fields from openerp.osv import orm +from openerp import api _SAML_UID_AND_PASS_SETTING = 'auth_saml.allow_saml.uid_and_internal_password' @@ -26,33 +26,27 @@ class base_settings(orm.TransientModel): ), } - def allow_saml_uid_and_internal_password(self, cr, uid, context=None): + # take care to name the function with another name to not clash with column + @api.model + def allow_saml_and_password(self): """Read the allow_saml_uid_and_internal_password setting. Use the admin account to bypass security restrictions. """ - # ignore the uid, the result of the method does not depend on it - uid = SUPERUSER_ID - - config_obj = self.pool['ir.config_parameter'] - - config_ids = config_obj.search( - cr, uid, + config_obj = self.env['ir.config_parameter'] + config_objs = config_obj.sudo().search( [('key', '=', _SAML_UID_AND_PASS_SETTING)], limit=1, - context=context ) - if not config_ids: + + # no configuration found reply with default value + if len(config_objs) == 0: return False - config = config_obj.browse( - cr, uid, config_ids, context=context - )[0] - return (True if config.value == '1' else False) + return (True if config_objs.value == '1' else False) - def get_default_allow_saml_uid_and_internal_password( - self, cr, uid, fields, context=None - ): + @api.multi + def get_default_allow_saml_uid_and_internal_password(self, fields): """Read the allow_saml_uid_and_internal_password setting. This function is called when the form is shown. """ @@ -61,39 +55,32 @@ def get_default_allow_saml_uid_and_internal_password( if 'allow_saml_uid_and_internal_password' in fields: ret['allow_saml_uid_and_internal_password'] = ( - self.allow_saml_uid_and_internal_password(cr, context) + self.allow_saml_uid_and_internal_password() ) return ret - def set_allow_saml_uid_and_internal_password( - self, cr, uid, ids, context=None - ): + @api.multi + def set_allow_saml_uid_and_internal_password(self): """Update the allow_saml_uid_and_internal_password setting. This function is called when saving the form. """ - dlg = self.browse(cr, uid, ids, context=context)[0] - setting_value = ( - '1' if dlg.allow_saml_uid_and_internal_password else '0' + '1' if self.allow_saml_uid_and_internal_password else '0' ) - config_obj = self.pool['ir.config_parameter'] + config_obj = self.env['ir.config_parameter'] config_ids = config_obj.search( - cr, uid, [('key', '=', _SAML_UID_AND_PASS_SETTING)], limit=1, - context=context ) + if config_ids: - config_obj.write( - cr, uid, config_ids, {'value': setting_value}, context=context - ) + config_ids.write({'value': setting_value}) + else: # The setting doesn't exist; create it. config_obj.create( - cr, uid, {'key': _SAML_UID_AND_PASS_SETTING, 'value': setting_value}, - context=context ) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 3663578d775..fcb339c57ad 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -10,7 +10,11 @@ from openerp import models from openerp import fields from openerp import SUPERUSER_ID +from openerp.exceptions import ValidationError +# import real addons name of base.res.res_users in order to call it without +# user the super() call from openerp.addons.base.res.res_users import res_users as baseuser +from openerp import _ _logger = logging.getLogger(__name__) @@ -27,32 +31,28 @@ class res_users(models.Model): help="SAML Provider user_id", ) - @api.multi - def _no_password_with_saml(self): + @api.one + @api.constrains('password_crypt', 'password', 'saml_uid') + def check_no_password_with_saml(self): """Ensure no Odoo user posesses both an SAML user ID and an Odoo - password. + password. Except admin which is not constrained by this rule. """ + if self._allow_saml_and_password(): + pass - if self._allow_saml_uid_and_internal_password(): - # The constraint is a no-op in this case. - return True - - for user in self: - if user.password and user.saml_uid: - return False - - return True - - _constraints = [ - ( - _no_password_with_saml, - ( - 'SAML2 authentication: An Odoo user cannot possess both a ' - 'SAML user ID and an Odoo password.' - ), - ['password', 'saml_uid'] - ), - ] + else: + # Super admin is the only user we allow to have a local password + # in the database + if ( + self.password_crypt and + self.saml_uid and + self.id is not SUPERUSER_ID + ): + raise ValidationError( + _("This database disallows users to have both passwords " + "and SAML IDs. Errors for login %s" % (self.login) + ) + ) _sql_constraints = [ ( @@ -163,6 +163,7 @@ def auth_saml(self, provider, saml_response): # return user credentials return self.env.cr.dbname, login, saml_response + # This method is using the old v7 API because it is called BEFORE the login def check_credentials(self, cr, uid, token): """token can be a password if the user has used the normal form... but we are more interested in the case when they are tokens @@ -195,28 +196,20 @@ def check_credentials(self, cr, uid, token): # exception that occurred in our execution frame raise - def write(self, cr, uid, ids, vals, context=None): + @api.multi + def write(self, vals): """Override to clear out the user's password when setting an SAML user ID (as they can't cohabit). """ if vals and vals.get('saml_uid'): - if not self._allow_saml_uid_and_internal_password(cr, uid, context): + if not self._allow_saml_and_password(): vals['password'] = False - return super(res_users, self).write( - cr, uid, ids, vals, context=context - ) - - def _allow_saml_uid_and_internal_password(self, cr, uid, context): + return super(res_users, self).write(vals) - # super user is always allowed to have a password in the database - # as opposed to other users... Doing so avoids being locked out - # of your own instance in case there is an issue with your IDP - if uid == SUPERUSER_ID: - return True + @api.model + def _allow_saml_and_password(self): - setting_obj = self.pool['base.config.settings'] - return setting_obj.allow_saml_uid_and_internal_password( - cr, uid, context=context - ) + settings_obj = self.env['base.config.settings'] + return settings_obj.allow_saml_and_password() From 2d78354f91f1ca1940c58c13499525359e0a116f Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Fri, 5 Jun 2015 16:40:01 +0200 Subject: [PATCH 60/93] Added more explicit information about LICENSE and download links to the source code in the description. --HG-- branch : odoo8 --- auth_saml/__openerp__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 6329227bcc3..aca41d80b30 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -31,10 +31,20 @@ WARNING: this module requires auth_crypt. This is because you still have the option if not recommended to allow users to have a password stored in odoo at the same time as having a SALM provider and id. + +This module is covered by the Gnu Affero General Public License, AGPLV3 or later + +The full source code and history can always be downloaded, modified +and redistributed from here: + + https://bitbucket.org/xcg/auth_saml/ + or + https://github.com/xcgd/auth_saml + """, 'author': 'XCG Consulting s.a.s.', 'maintainer': 'XCG Consulting s.a.s.', - 'website': 'http://www.xcg-consulting.fr', + 'website': 'http://odoo.consulting', 'depends': [ 'base', 'base_setup', From 3bd9cc2021276439593c2dcea33e70d1b1786c8a Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Fri, 5 Jun 2015 16:40:30 +0200 Subject: [PATCH 61/93] Added a small header to LICENSE file --HG-- branch : odoo8 --- auth_saml/LICENSE | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/auth_saml/LICENSE b/auth_saml/LICENSE index dba13ed2ddf..48a81e39d45 100644 --- a/auth_saml/LICENSE +++ b/auth_saml/LICENSE @@ -1,3 +1,10 @@ +This module is covered by the Gnu Affero General Public License. +It is copyright XGC Consulting SAS. + +A copy of the full licence is provided below. + +====================================================================== + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 From a872c1329d8037a7ef58b125064c52a206e00595 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Fri, 5 Jun 2015 16:41:03 +0200 Subject: [PATCH 62/93] Added a CONTRIBUTORS file with all our committers --HG-- branch : odoo8 --- auth_saml/CONTRIBUTORS | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 auth_saml/CONTRIBUTORS diff --git a/auth_saml/CONTRIBUTORS b/auth_saml/CONTRIBUTORS new file mode 100644 index 00000000000..02e94ba7885 --- /dev/null +++ b/auth_saml/CONTRIBUTORS @@ -0,0 +1,7 @@ +by order of appearance: + + - Florent Aide, + - Vincent Hatakeyama, + - Alexandre Brun, + - Jeremy Co Kim Len, + - Houzefa Abbasbhay From a6ff23e9f033bfc909033265ff3835b26e5d2b1f Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Fri, 5 Jun 2015 16:41:32 +0200 Subject: [PATCH 63/93] Finalized porting to the new v8 API for the whole module --HG-- branch : odoo8 --- auth_saml/model/base_settings.py | 18 +++++++-------- auth_saml/model/saml_token.py | 38 +++++++++++++++----------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/auth_saml/model/base_settings.py b/auth_saml/model/base_settings.py index ccd54d41c37..ac0a599d819 100644 --- a/auth_saml/model/base_settings.py +++ b/auth_saml/model/base_settings.py @@ -1,12 +1,12 @@ -from openerp.osv import fields -from openerp.osv import orm +from openerp import fields +from openerp import models from openerp import api _SAML_UID_AND_PASS_SETTING = 'auth_saml.allow_saml.uid_and_internal_password' -class base_settings(orm.TransientModel): +class base_settings(models.TransientModel): """Inherit from base.config.settings to add a setting. This is only here for easier access; the setting is not actually stored by this (transient) collection. Instead, it is kept in sync with the @@ -17,14 +17,12 @@ class base_settings(orm.TransientModel): _inherit = 'base.config.settings' - _columns = { - 'allow_saml_uid_and_internal_password': fields.boolean( - ( - 'Allow SAML users to posess an Odoo password (warning: ' - 'decreases security)' - ), + allow_saml_uid_and_internal_password = fields.Boolean( + ( + 'Allow SAML users to posess an Odoo password (warning: ' + 'decreases security)' ), - } + ) # take care to name the function with another name to not clash with column @api.model diff --git a/auth_saml/model/saml_token.py b/auth_saml/model/saml_token.py index 428713e75d2..eae2518c147 100644 --- a/auth_saml/model/saml_token.py +++ b/auth_saml/model/saml_token.py @@ -1,31 +1,29 @@ # -*- encoding: utf-8 -*- __author__ = 'faide' - import logging -from openerp.osv import osv, fields +from openerp import fields +from openerp import models _logger = logging.getLogger(__name__) -class saml_token(osv.Model): +class saml_token(models.Model): _name = "auth_saml.token" _rec_name = "user_id" - _columns = { - 'saml_provider_id': fields.many2one( - 'auth.saml.provider', - string='SAML Provider that issued the token', - ), - 'user_id': fields.many2one( - 'res.users', - string="User", - # we want the token to be destroyed if the corresponding res.users - # is deleted - ondelete="cascade" - ), - 'saml_access_token': fields.char( - 'Current SAML token for this user', - help="The current SAML token in use", - ), - } + saml_provider_id = fields.Many2one( + 'auth.saml.provider', + string='SAML Provider that issued the token', + ) + user_id = fields.Many2one( + 'res.users', + string="User", + # we want the token to be destroyed if the corresponding res.users + # is deleted + ondelete="cascade" + ) + saml_access_token = fields.Char( + 'Current SAML token for this user', + help="The current SAML token in use", + ) From 0e26022a6f645d650465b037b5e4dae103ee6f44 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Fri, 5 Jun 2015 17:22:48 +0200 Subject: [PATCH 64/93] Adding python deps to __openerp__.py to make sure lasso is present in the python path --HG-- branch : odoo8 --- auth_saml/__openerp__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index aca41d80b30..05c8725df97 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -64,4 +64,7 @@ ], 'installable': True, 'auto_install': False, + 'external_dependencies': { + 'python': ['lasso'], + }, } From 646cd97ad538e84140d9225be8b41f6ed04ed744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 19 Oct 2015 18:31:34 +0200 Subject: [PATCH 65/93] Code formatting (flake8 valid) and touch-ups before submitting to OCA --- auth_saml/__openerp__.py | 19 ++++++------------- auth_saml/controllers/main.py | 1 + auth_saml/model/res_users.py | 5 ++++- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 05c8725df97..6d37d05a947 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -25,26 +25,19 @@ 'version': '3.0', 'category': 'Tools', 'description': """ -Allow users to login through Saml2 Provider. -============================================ +SAML2 authentication +==================== + +Let users log into Odoo via an SAML2 provider. WARNING: this module requires auth_crypt. This is because you still have the option if not recommended to allow users to have a password stored in odoo at the same time as having a SALM provider and id. - -This module is covered by the Gnu Affero General Public License, AGPLV3 or later - -The full source code and history can always be downloaded, modified -and redistributed from here: - - https://bitbucket.org/xcg/auth_saml/ - or - https://github.com/xcgd/auth_saml - """, - 'author': 'XCG Consulting s.a.s.', + 'author': 'XCG Consulting,Odoo Community Association (OCA)', 'maintainer': 'XCG Consulting s.a.s.', 'website': 'http://odoo.consulting', + 'license': 'AGPL-3', 'depends': [ 'base', 'base_setup', diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 9ab1e0dd1ad..efcfca1b33e 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -5,6 +5,7 @@ import werkzeug.utils import openerp +from openerp import _ from openerp import http from openerp.http import request from openerp import SUPERUSER_ID diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index fcb339c57ad..f731b28c7f6 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -179,7 +179,10 @@ def check_credentials(self, cr, uid, token): token ) - except (openerp.exceptions.AccessDenied, passlib.exc.PasswordSizeError): + except ( + openerp.exceptions.AccessDenied, + passlib.exc.PasswordSizeError, + ): # since normal auth did not succeed we now try to find if the user # has an active token attached to his uid res = token_osv.search( From 6a1de9e70d3240ee2b080b42edc5ee91945142db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 19 Oct 2015 18:37:43 +0200 Subject: [PATCH 66/93] Remove mercurial files --- .hgignore | 16 ---------------- .hgtags | 6 ------ 2 files changed, 22 deletions(-) delete mode 100644 .hgignore delete mode 100644 .hgtags diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 95570ddd2cc..00000000000 --- a/.hgignore +++ /dev/null @@ -1,16 +0,0 @@ -syntax: glob -*.pyc -**/*.pyc -*.swp -.tmp* -tmp/* -*.egg-info -*.orig -nose* -.rope* -.coverage -.idea -cover/* -.settings -.pydevproject -.project diff --git a/.hgtags b/.hgtags deleted file mode 100644 index a7132924e5b..00000000000 --- a/.hgtags +++ /dev/null @@ -1,6 +0,0 @@ -0971293545c0e5088ae6a608f9c653d3e687a69e TAG_1.0 -440c74a3b23eb89c17d0e0d74b31face446f1987 TAG_1.1 -e0aa862214f82295b0d942bfb53f11d4d7f80e09 TAG_1.2 -ae9585f6103b8271c2b1fba26653b259e472be92 TAG_2.0 -9dfc2bf569e44a4fda8a0afc33f6e45d92518d41 TAG_2.1 -f20470fdf0effdbdd1f87c6222f9b864fa5a7bf9 TAG_3.0 From be5ac7550fc42ad6632e4872b846d9179828c26d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 19 Oct 2015 19:01:55 +0200 Subject: [PATCH 67/93] Improve the README & the manifest --- auth_saml/{README => README.rst} | 31 +++++++++++++++++++++--------- auth_saml/__openerp__.py | 33 ++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 11 deletions(-) rename auth_saml/{README => README.rst} (51%) diff --git a/auth_saml/README b/auth_saml/README.rst similarity index 51% rename from auth_saml/README rename to auth_saml/README.rst index 96ed9d49cf6..1e6018d4fe1 100644 --- a/auth_saml/README +++ b/auth_saml/README.rst @@ -1,17 +1,20 @@ -### README ### +SAML2 authentication +==================== +Let users log into Odoo via an SAML2 provider. -### Authentification SAML for Odoo/OpenERP ### +This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On) +between Odoo and other applications of your ecosystem. -This is a module for odoo/OpenERP 7. It provides authentification via SAML. +WARNING: this module requires auth_crypt. This is because you still have the + option if not recommended to allow users to have a password stored in odoo + at the same time as having a SALM provider and id. -This module allows to deport the management of users and passwords in an -external authentication system to provide SSO functionality (Single Sign On ) -between Odoo and other applications of your ecosystem. Benefits -======== +-------- * Reducing the time spent typing different passwords for different accounts. @@ -19,6 +22,16 @@ Benefits * Centralizing authentication systems. -* Securing all input levels / exit / access to multiple systems without prompting users. +* Securing all input levels / exit / access to multiple systems without + prompting users. + +* The centralization of access control information for compliance testing to + different standards. + + +Dependencies +------------ + +This addon requires `lasso`_. -* The centralization of access control information for compliance testing to different standards. +.. _lasso: http://lasso.entrouvert.org diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 6d37d05a947..aa1380e4cdd 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -30,12 +30,41 @@ Let users log into Odoo via an SAML2 provider. +This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On) +between Odoo and other applications of your ecosystem. + + WARNING: this module requires auth_crypt. This is because you still have the option if not recommended to allow users to have a password stored in odoo at the same time as having a SALM provider and id. + + +Benefits +-------- + +* Reducing the time spent typing different passwords for different accounts. + +* Reducing the time spent in IT support for password oversights. + +* Centralizing authentication systems. + +* Securing all input levels / exit / access to multiple systems without + prompting users. + +* The centralization of access control information for compliance testing to + different standards. + + +Dependencies +------------ + +This addon requires `lasso`_. + +.. _lasso: http://lasso.entrouvert.org """, - 'author': 'XCG Consulting,Odoo Community Association (OCA)', - 'maintainer': 'XCG Consulting s.a.s.', + 'author': 'XCG Consulting, Odoo Community Association (OCA)', + 'maintainer': 'XCG Consulting', 'website': 'http://odoo.consulting', 'license': 'AGPL-3', 'depends': [ From 54432a9d49042517be62b071511edf486c49f88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 19 Oct 2015 19:33:09 +0200 Subject: [PATCH 68/93] auth_saml: Code formatting (encoding decl & class names) --- auth_saml/controllers/main.py | 4 ++++ auth_saml/model/auth_saml.py | 4 +++- auth_saml/model/base_settings.py | 4 +++- auth_saml/model/res_users.py | 7 ++++--- auth_saml/model/saml_token.py | 5 ++--- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index efcfca1b33e..c98d9f37dfb 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import functools import logging @@ -43,6 +45,8 @@ def wrapper(self, req, **kw): # ---------------------------------------------------------- # Controller # ---------------------------------------------------------- + + class SAMLLogin(openerp.addons.web.controllers.main.Home): def list_providers(self): diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 750ba317831..c7078ba3af3 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from openerp import models from openerp import api from openerp import fields @@ -5,7 +7,7 @@ import simplejson -class auth_saml_provider(models.Model): +class AuthSamlProvider(models.Model): """Class defining the configuration values of an Saml2 provider""" _name = 'auth.saml.provider' diff --git a/auth_saml/model/base_settings.py b/auth_saml/model/base_settings.py index ac0a599d819..392bedb5a86 100644 --- a/auth_saml/model/base_settings.py +++ b/auth_saml/model/base_settings.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from openerp import fields from openerp import models from openerp import api @@ -6,7 +8,7 @@ _SAML_UID_AND_PASS_SETTING = 'auth_saml.allow_saml.uid_and_internal_password' -class base_settings(models.TransientModel): +class BaseSettings(models.TransientModel): """Inherit from base.config.settings to add a setting. This is only here for easier access; the setting is not actually stored by this (transient) collection. Instead, it is kept in sync with the diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index f731b28c7f6..a5b8c79b102 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -1,4 +1,5 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- + import logging # this is our very own dependency import lasso @@ -19,7 +20,7 @@ _logger = logging.getLogger(__name__) -class res_users(models.Model): +class ResUsers(models.Model): _inherit = 'res.users' saml_provider_id = fields.Many2one( @@ -209,7 +210,7 @@ def write(self, vals): if not self._allow_saml_and_password(): vals['password'] = False - return super(res_users, self).write(vals) + return super(ResUsers, self).write(vals) @api.model def _allow_saml_and_password(self): diff --git a/auth_saml/model/saml_token.py b/auth_saml/model/saml_token.py index eae2518c147..d6bde1b49d8 100644 --- a/auth_saml/model/saml_token.py +++ b/auth_saml/model/saml_token.py @@ -1,5 +1,4 @@ -# -*- encoding: utf-8 -*- -__author__ = 'faide' +# -*- coding: utf-8 -*- import logging from openerp import fields @@ -8,7 +7,7 @@ _logger = logging.getLogger(__name__) -class saml_token(models.Model): +class SamlToken(models.Model): _name = "auth_saml.token" _rec_name = "user_id" From 0e92a95f02874ae273d86d8d4c6284c7b4ccf071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 19 Oct 2015 19:37:10 +0200 Subject: [PATCH 69/93] Add python-lasso into the travis file --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index b7bd8409556..2629dc2eeae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ addons: packages: - expect-dev # provides unbuffer utility - python-lxml # because pip installation is slow + - python-lasso # Used by the "auth_saml" addon. env: global: From 6821d7ea4e5d335e0cc965a4070a265516337bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 19 Oct 2015 20:03:20 +0200 Subject: [PATCH 70/93] auth_saml: Improve the README, add an HTML descr and misc things --- auth_saml/CONTRIBUTORS | 7 --- auth_saml/LICENSE | 7 --- auth_saml/{NEWS => NEWS.rst} | 0 auth_saml/README.rst | 82 ++++++++++++++++++++++++- auth_saml/__openerp__.py | 39 ------------ auth_saml/static/description/index.html | 82 +++++++++++++++++++++++++ 6 files changed, 163 insertions(+), 54 deletions(-) delete mode 100644 auth_saml/CONTRIBUTORS rename auth_saml/{NEWS => NEWS.rst} (100%) create mode 100644 auth_saml/static/description/index.html diff --git a/auth_saml/CONTRIBUTORS b/auth_saml/CONTRIBUTORS deleted file mode 100644 index 02e94ba7885..00000000000 --- a/auth_saml/CONTRIBUTORS +++ /dev/null @@ -1,7 +0,0 @@ -by order of appearance: - - - Florent Aide, - - Vincent Hatakeyama, - - Alexandre Brun, - - Jeremy Co Kim Len, - - Houzefa Abbasbhay diff --git a/auth_saml/LICENSE b/auth_saml/LICENSE index 48a81e39d45..dba13ed2ddf 100644 --- a/auth_saml/LICENSE +++ b/auth_saml/LICENSE @@ -1,10 +1,3 @@ -This module is covered by the Gnu Affero General Public License. -It is copyright XGC Consulting SAS. - -A copy of the full licence is provided below. - -====================================================================== - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/auth_saml/NEWS b/auth_saml/NEWS.rst similarity index 100% rename from auth_saml/NEWS rename to auth_saml/NEWS.rst diff --git a/auth_saml/README.rst b/auth_saml/README.rst index 1e6018d4fe1..b806468a36b 100644 --- a/auth_saml/README.rst +++ b/auth_saml/README.rst @@ -1,3 +1,8 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==================== SAML2 authentication ==================== @@ -14,7 +19,7 @@ WARNING: this module requires auth_crypt. This is because you still have the Benefits --------- +======== * Reducing the time spent typing different passwords for different accounts. @@ -29,9 +34,84 @@ Benefits different standards. +Installation +============ + +Install as you would install any Odoo addon. + Dependencies ------------ This addon requires `lasso`_. .. _lasso: http://lasso.entrouvert.org + + +Configuration +============= + +There are SAML-related settings in Configuration > General settings. + + +Usage +===== + +To use this module, you need an authentic2 server, properly set up. +Read the doc at documentation/index.rst for more information. + + +Demo +==== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/149/8.0 + + +Known issues / Roadmap +====================== + +None for now. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback `here `_. + + +Credits +======= + +Contributors +------------ + +In order of appearance: + + - Florent Aide, + - Vincent Hatakeyama, + - Alexandre Brun, + - Jeremy Co Kim Len, + - Houzéfa Abbasbhay + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index aa1380e4cdd..a9bfd6ace9c 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -24,45 +24,6 @@ 'name': 'Saml2 Authentication', 'version': '3.0', 'category': 'Tools', - 'description': """ -SAML2 authentication -==================== - -Let users log into Odoo via an SAML2 provider. - -This module allows to deport the management of users and passwords in an -external authentication system to provide SSO functionality (Single Sign On) -between Odoo and other applications of your ecosystem. - - -WARNING: this module requires auth_crypt. This is because you still have the - option if not recommended to allow users to have a password stored in odoo - at the same time as having a SALM provider and id. - - -Benefits --------- - -* Reducing the time spent typing different passwords for different accounts. - -* Reducing the time spent in IT support for password oversights. - -* Centralizing authentication systems. - -* Securing all input levels / exit / access to multiple systems without - prompting users. - -* The centralization of access control information for compliance testing to - different standards. - - -Dependencies ------------- - -This addon requires `lasso`_. - -.. _lasso: http://lasso.entrouvert.org -""", 'author': 'XCG Consulting, Odoo Community Association (OCA)', 'maintainer': 'XCG Consulting', 'website': 'http://odoo.consulting', diff --git a/auth_saml/static/description/index.html b/auth_saml/static/description/index.html new file mode 100644 index 00000000000..5f38fe36288 --- /dev/null +++ b/auth_saml/static/description/index.html @@ -0,0 +1,82 @@ +License: AGPL-3 +
+

SAML2 authentication

+

Let users log into Odoo via an SAML2 provider.

+

This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On) +between Odoo and other applications of your ecosystem.

+
+
WARNING: this module requires auth_crypt. This is because you still have the
+
option if not recommended to allow users to have a password stored in odoo +at the same time as having a SALM provider and id.
+
+
+

Benefits

+
    +
  • Reducing the time spent typing different passwords for different accounts.
  • +
  • Reducing the time spent in IT support for password oversights.
  • +
  • Centralizing authentication systems.
  • +
  • Securing all input levels / exit / access to multiple systems without +prompting users.
  • +
  • The centralization of access control information for compliance testing to +different standards.
  • +
+
+
+

Installation

+

Install as you would install any Odoo addon.

+
+

Dependencies

+

This addon requires lasso.

+
+
+
+

Configuration

+

There are SAML-related settings in Configuration > General settings.

+
+
+

Usage

+

To use this module, you need an authentic2 server, properly set up. +Read the doc at documentation/index.rst for more information.

+
+
+

Demo

+Try me on Runbot +
+
+

Known issues / Roadmap

+

None for now.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback here.

+
+
+

Credits

+
+

Contributors

+

In order of appearance:

+
+ +
+
+
+

Maintainer

+Odoo Community Association +

This module is maintained by the OCA.

+

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

To contribute to this module, please visit http://odoo-community.org.

+
+
+
+ From 6b41ee60836a554aa50a4f8c03d2534cfcc62ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Tue, 20 Oct 2015 11:04:18 +0200 Subject: [PATCH 71/93] auth_saml: Relative imports --- auth_saml/__init__.py | 4 ++-- auth_saml/controllers/__init__.py | 4 +--- auth_saml/model/__init__.py | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 36b6810dbbb..5e979d05d06 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa -import controllers -import model +from . import controllers +from . import model diff --git a/auth_saml/controllers/__init__.py b/auth_saml/controllers/__init__.py index e47ae81ca78..6663c573bf0 100644 --- a/auth_saml/controllers/__init__.py +++ b/auth_saml/controllers/__init__.py @@ -1,5 +1,3 @@ # flake8: noqa -import main - -# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: +from . import main diff --git a/auth_saml/model/__init__.py b/auth_saml/model/__init__.py index b61318a04a7..8f75d153e31 100644 --- a/auth_saml/model/__init__.py +++ b/auth_saml/model/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa -import auth_saml -import base_settings -import res_users -import saml_token +from . import auth_saml +from . import base_settings +from . import res_users +from . import saml_token From a30690b3b6b032094a41f8de4c249c2df7428e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Tue, 20 Oct 2015 11:05:31 +0200 Subject: [PATCH 72/93] auth_saml: model/ -> models/ --- auth_saml/__init__.py | 2 +- auth_saml/{model => models}/__init__.py | 0 auth_saml/{model => models}/auth_saml.py | 0 auth_saml/{model => models}/base_settings.py | 0 auth_saml/{model => models}/res_users.py | 0 auth_saml/{model => models}/saml_token.py | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename auth_saml/{model => models}/__init__.py (100%) rename auth_saml/{model => models}/auth_saml.py (100%) rename auth_saml/{model => models}/base_settings.py (100%) rename auth_saml/{model => models}/res_users.py (100%) rename auth_saml/{model => models}/saml_token.py (100%) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 5e979d05d06..66ecd5d6587 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa from . import controllers -from . import model +from . import models diff --git a/auth_saml/model/__init__.py b/auth_saml/models/__init__.py similarity index 100% rename from auth_saml/model/__init__.py rename to auth_saml/models/__init__.py diff --git a/auth_saml/model/auth_saml.py b/auth_saml/models/auth_saml.py similarity index 100% rename from auth_saml/model/auth_saml.py rename to auth_saml/models/auth_saml.py diff --git a/auth_saml/model/base_settings.py b/auth_saml/models/base_settings.py similarity index 100% rename from auth_saml/model/base_settings.py rename to auth_saml/models/base_settings.py diff --git a/auth_saml/model/res_users.py b/auth_saml/models/res_users.py similarity index 100% rename from auth_saml/model/res_users.py rename to auth_saml/models/res_users.py diff --git a/auth_saml/model/saml_token.py b/auth_saml/models/saml_token.py similarity index 100% rename from auth_saml/model/saml_token.py rename to auth_saml/models/saml_token.py From f79f1d4e063a9aa35c61eaa8d23227afe8ceccbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Tue, 20 Oct 2015 11:24:12 +0200 Subject: [PATCH 73/93] Require sudo in the travis file until is accepted (needed to install python-lasso, which is not currently whitelisted) --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2629dc2eeae..1b6550da36e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: python -sudo: false +# Require sudo until +# is resolved (we need sudo to install python-lasso, which is not currently whitelisted. +sudo: required cache: pip python: From 9ecdfd90eb676d495c9db58a829f0f3df42cf3b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 26 Oct 2015 09:20:52 +0100 Subject: [PATCH 74/93] Now that python-lasso has been whitelisted, revert "Require sudo in the travis file until is accepted (needed to install python-lasso, which is not currently whitelisted)" This reverts commit f79f1d4e063a9aa35c61eaa8d23227afe8ceccbb. --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b6550da36e..2629dc2eeae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: python -# Require sudo until -# is resolved (we need sudo to install python-lasso, which is not currently whitelisted. -sudo: required +sudo: false cache: pip python: From 2dc67ae9e4a0c3b43b3808fad1b7b0351ed4ae52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Mon, 22 Aug 2016 18:10:11 +0200 Subject: [PATCH 75/93] Temporarily revert some changes to import updates --- auth_saml/CONTRIBUTORS | 7 ++ auth_saml/LICENSE | 7 ++ auth_saml/{NEWS.rst => NEWS} | 0 auth_saml/README | 24 ++++ auth_saml/README.rst | 117 ------------------- auth_saml/__init__.py | 4 +- auth_saml/__openerp__.py | 23 +++- auth_saml/controllers/__init__.py | 4 +- auth_saml/controllers/main.py | 5 - auth_saml/model/__init__.py | 6 + auth_saml/{models => model}/auth_saml.py | 4 +- auth_saml/{models => model}/base_settings.py | 4 +- auth_saml/{models => model}/res_users.py | 12 +- auth_saml/{models => model}/saml_token.py | 5 +- auth_saml/models/__init__.py | 6 - auth_saml/static/description/index.html | 82 ------------- 16 files changed, 78 insertions(+), 232 deletions(-) create mode 100644 auth_saml/CONTRIBUTORS rename auth_saml/{NEWS.rst => NEWS} (100%) create mode 100644 auth_saml/README delete mode 100644 auth_saml/README.rst create mode 100644 auth_saml/model/__init__.py rename auth_saml/{models => model}/auth_saml.py (96%) rename auth_saml/{models => model}/base_settings.py (97%) rename auth_saml/{models => model}/res_users.py (96%) rename auth_saml/{models => model}/saml_token.py (88%) delete mode 100644 auth_saml/models/__init__.py delete mode 100644 auth_saml/static/description/index.html diff --git a/auth_saml/CONTRIBUTORS b/auth_saml/CONTRIBUTORS new file mode 100644 index 00000000000..02e94ba7885 --- /dev/null +++ b/auth_saml/CONTRIBUTORS @@ -0,0 +1,7 @@ +by order of appearance: + + - Florent Aide, + - Vincent Hatakeyama, + - Alexandre Brun, + - Jeremy Co Kim Len, + - Houzefa Abbasbhay diff --git a/auth_saml/LICENSE b/auth_saml/LICENSE index dba13ed2ddf..48a81e39d45 100644 --- a/auth_saml/LICENSE +++ b/auth_saml/LICENSE @@ -1,3 +1,10 @@ +This module is covered by the Gnu Affero General Public License. +It is copyright XGC Consulting SAS. + +A copy of the full licence is provided below. + +====================================================================== + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/auth_saml/NEWS.rst b/auth_saml/NEWS similarity index 100% rename from auth_saml/NEWS.rst rename to auth_saml/NEWS diff --git a/auth_saml/README b/auth_saml/README new file mode 100644 index 00000000000..96ed9d49cf6 --- /dev/null +++ b/auth_saml/README @@ -0,0 +1,24 @@ +### README ### + + +### Authentification SAML for Odoo/OpenERP ### + +This is a module for odoo/OpenERP 7. It provides authentification via SAML. + + +This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On ) +between Odoo and other applications of your ecosystem. + +Benefits +======== + +* Reducing the time spent typing different passwords for different accounts. + +* Reducing the time spent in IT support for password oversights. + +* Centralizing authentication systems. + +* Securing all input levels / exit / access to multiple systems without prompting users. + +* The centralization of access control information for compliance testing to different standards. diff --git a/auth_saml/README.rst b/auth_saml/README.rst deleted file mode 100644 index b806468a36b..00000000000 --- a/auth_saml/README.rst +++ /dev/null @@ -1,117 +0,0 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - -==================== -SAML2 authentication -==================== - -Let users log into Odoo via an SAML2 provider. - -This module allows to deport the management of users and passwords in an -external authentication system to provide SSO functionality (Single Sign On) -between Odoo and other applications of your ecosystem. - - -WARNING: this module requires auth_crypt. This is because you still have the - option if not recommended to allow users to have a password stored in odoo - at the same time as having a SALM provider and id. - - -Benefits -======== - -* Reducing the time spent typing different passwords for different accounts. - -* Reducing the time spent in IT support for password oversights. - -* Centralizing authentication systems. - -* Securing all input levels / exit / access to multiple systems without - prompting users. - -* The centralization of access control information for compliance testing to - different standards. - - -Installation -============ - -Install as you would install any Odoo addon. - -Dependencies ------------- - -This addon requires `lasso`_. - -.. _lasso: http://lasso.entrouvert.org - - -Configuration -============= - -There are SAML-related settings in Configuration > General settings. - - -Usage -===== - -To use this module, you need an authentic2 server, properly set up. -Read the doc at documentation/index.rst for more information. - - -Demo -==== - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/149/8.0 - - -Known issues / Roadmap -====================== - -None for now. - - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues `_. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed feedback `here `_. - - -Credits -======= - -Contributors ------------- - -In order of appearance: - - - Florent Aide, - - Vincent Hatakeyama, - - Alexandre Brun, - - Jeremy Co Kim Len, - - Houzéfa Abbasbhay - - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. - -To contribute to this module, please visit http://odoo-community.org. diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 66ecd5d6587..36b6810dbbb 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa -from . import controllers -from . import models +import controllers +import model diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index a9bfd6ace9c..05c8725df97 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -24,10 +24,27 @@ 'name': 'Saml2 Authentication', 'version': '3.0', 'category': 'Tools', - 'author': 'XCG Consulting, Odoo Community Association (OCA)', - 'maintainer': 'XCG Consulting', + 'description': """ +Allow users to login through Saml2 Provider. +============================================ + +WARNING: this module requires auth_crypt. This is because you still have the + option if not recommended to allow users to have a password stored in odoo + at the same time as having a SALM provider and id. + +This module is covered by the Gnu Affero General Public License, AGPLV3 or later + +The full source code and history can always be downloaded, modified +and redistributed from here: + + https://bitbucket.org/xcg/auth_saml/ + or + https://github.com/xcgd/auth_saml + +""", + 'author': 'XCG Consulting s.a.s.', + 'maintainer': 'XCG Consulting s.a.s.', 'website': 'http://odoo.consulting', - 'license': 'AGPL-3', 'depends': [ 'base', 'base_setup', diff --git a/auth_saml/controllers/__init__.py b/auth_saml/controllers/__init__.py index 6663c573bf0..e47ae81ca78 100644 --- a/auth_saml/controllers/__init__.py +++ b/auth_saml/controllers/__init__.py @@ -1,3 +1,5 @@ # flake8: noqa -from . import main +import main + +# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index c98d9f37dfb..9ab1e0dd1ad 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import functools import logging @@ -7,7 +5,6 @@ import werkzeug.utils import openerp -from openerp import _ from openerp import http from openerp.http import request from openerp import SUPERUSER_ID @@ -45,8 +42,6 @@ def wrapper(self, req, **kw): # ---------------------------------------------------------- # Controller # ---------------------------------------------------------- - - class SAMLLogin(openerp.addons.web.controllers.main.Home): def list_providers(self): diff --git a/auth_saml/model/__init__.py b/auth_saml/model/__init__.py new file mode 100644 index 00000000000..b61318a04a7 --- /dev/null +++ b/auth_saml/model/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa + +import auth_saml +import base_settings +import res_users +import saml_token diff --git a/auth_saml/models/auth_saml.py b/auth_saml/model/auth_saml.py similarity index 96% rename from auth_saml/models/auth_saml.py rename to auth_saml/model/auth_saml.py index c7078ba3af3..750ba317831 100644 --- a/auth_saml/models/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from openerp import models from openerp import api from openerp import fields @@ -7,7 +5,7 @@ import simplejson -class AuthSamlProvider(models.Model): +class auth_saml_provider(models.Model): """Class defining the configuration values of an Saml2 provider""" _name = 'auth.saml.provider' diff --git a/auth_saml/models/base_settings.py b/auth_saml/model/base_settings.py similarity index 97% rename from auth_saml/models/base_settings.py rename to auth_saml/model/base_settings.py index 392bedb5a86..ac0a599d819 100644 --- a/auth_saml/models/base_settings.py +++ b/auth_saml/model/base_settings.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from openerp import fields from openerp import models from openerp import api @@ -8,7 +6,7 @@ _SAML_UID_AND_PASS_SETTING = 'auth_saml.allow_saml.uid_and_internal_password' -class BaseSettings(models.TransientModel): +class base_settings(models.TransientModel): """Inherit from base.config.settings to add a setting. This is only here for easier access; the setting is not actually stored by this (transient) collection. Instead, it is kept in sync with the diff --git a/auth_saml/models/res_users.py b/auth_saml/model/res_users.py similarity index 96% rename from auth_saml/models/res_users.py rename to auth_saml/model/res_users.py index a5b8c79b102..fcb339c57ad 100644 --- a/auth_saml/models/res_users.py +++ b/auth_saml/model/res_users.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- - +# -*- encoding: utf-8 -*- import logging # this is our very own dependency import lasso @@ -20,7 +19,7 @@ _logger = logging.getLogger(__name__) -class ResUsers(models.Model): +class res_users(models.Model): _inherit = 'res.users' saml_provider_id = fields.Many2one( @@ -180,10 +179,7 @@ def check_credentials(self, cr, uid, token): token ) - except ( - openerp.exceptions.AccessDenied, - passlib.exc.PasswordSizeError, - ): + except (openerp.exceptions.AccessDenied, passlib.exc.PasswordSizeError): # since normal auth did not succeed we now try to find if the user # has an active token attached to his uid res = token_osv.search( @@ -210,7 +206,7 @@ def write(self, vals): if not self._allow_saml_and_password(): vals['password'] = False - return super(ResUsers, self).write(vals) + return super(res_users, self).write(vals) @api.model def _allow_saml_and_password(self): diff --git a/auth_saml/models/saml_token.py b/auth_saml/model/saml_token.py similarity index 88% rename from auth_saml/models/saml_token.py rename to auth_saml/model/saml_token.py index d6bde1b49d8..eae2518c147 100644 --- a/auth_saml/models/saml_token.py +++ b/auth_saml/model/saml_token.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +# -*- encoding: utf-8 -*- +__author__ = 'faide' import logging from openerp import fields @@ -7,7 +8,7 @@ _logger = logging.getLogger(__name__) -class SamlToken(models.Model): +class saml_token(models.Model): _name = "auth_saml.token" _rec_name = "user_id" diff --git a/auth_saml/models/__init__.py b/auth_saml/models/__init__.py deleted file mode 100644 index 8f75d153e31..00000000000 --- a/auth_saml/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# flake8: noqa - -from . import auth_saml -from . import base_settings -from . import res_users -from . import saml_token diff --git a/auth_saml/static/description/index.html b/auth_saml/static/description/index.html deleted file mode 100644 index 5f38fe36288..00000000000 --- a/auth_saml/static/description/index.html +++ /dev/null @@ -1,82 +0,0 @@ -License: AGPL-3 -
-

SAML2 authentication

-

Let users log into Odoo via an SAML2 provider.

-

This module allows to deport the management of users and passwords in an -external authentication system to provide SSO functionality (Single Sign On) -between Odoo and other applications of your ecosystem.

-
-
WARNING: this module requires auth_crypt. This is because you still have the
-
option if not recommended to allow users to have a password stored in odoo -at the same time as having a SALM provider and id.
-
-
-

Benefits

-
    -
  • Reducing the time spent typing different passwords for different accounts.
  • -
  • Reducing the time spent in IT support for password oversights.
  • -
  • Centralizing authentication systems.
  • -
  • Securing all input levels / exit / access to multiple systems without -prompting users.
  • -
  • The centralization of access control information for compliance testing to -different standards.
  • -
-
-
-

Installation

-

Install as you would install any Odoo addon.

-
-

Dependencies

-

This addon requires lasso.

-
-
-
-

Configuration

-

There are SAML-related settings in Configuration > General settings.

-
-
-

Usage

-

To use this module, you need an authentic2 server, properly set up. -Read the doc at documentation/index.rst for more information.

-
-
-

Demo

-Try me on Runbot -
-
-

Known issues / Roadmap

-

None for now.

-
-
-

Bug Tracker

-

Bugs are tracked on GitHub Issues. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed feedback here.

-
-
-

Credits

-
-

Contributors

-

In order of appearance:

-
- -
-
-
-

Maintainer

-Odoo Community Association -

This module is maintained by the OCA.

-

OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use.

-

To contribute to this module, please visit http://odoo-community.org.

-
-
-
- From bf7bf665b7c9ad93e178d8bdac9ff82964e777c9 Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Wed, 10 Feb 2016 17:56:07 +0100 Subject: [PATCH 76/93] docstrings --HG-- branch : odoo8 --- auth_saml/model/auth_saml.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 750ba317831..75bb9a7a0fc 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -1,3 +1,4 @@ +# -*- encoding: utf-8 -*- from openerp import models from openerp import api from openerp import fields @@ -14,10 +15,13 @@ class auth_saml_provider(models.Model): @api.multi def _get_lasso_for_provider(self): + """internal helper to get a configured lasso.Login object for the + given provider id""" + # user is not connected yet... so use SUPERUSER_ID # TODO: we should cache those results somewhere because it is - # really costy to always recreate a login variable from buffers + # really costly to always recreate a login variable from buffers server = lasso.Server.newFromBuffers( self.sp_metadata, self.sp_pkey From ff3d5160033a36358d00f4495ebdded9d13bdbdb Mon Sep 17 00:00:00 2001 From: Florent Aide Date: Wed, 10 Feb 2016 17:57:18 +0100 Subject: [PATCH 77/93] implement real attribute matching instead of using subject.nameId.content for everything --HG-- branch : odoo8 --- auth_saml/model/auth_saml.py | 15 ++++++++ auth_saml/model/res_users.py | 72 ++++++++++++++++++++++++++++++++--- auth_saml/views/auth_saml.xml | 1 + 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 75bb9a7a0fc..4ca6b69d421 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -32,6 +32,16 @@ def _get_lasso_for_provider(self): ) return lasso.Login(server) + @api.multi + def _get_matching_attr_for_provider(self): + """internal helper to fetch the matching attribute for this SAML + provider. Returns a unicode object. + """ + + self.ensure_one() + + return self.matching_attribute + @api.multi def _get_auth_request(self, state): """build an authentication request and give it back to our client @@ -58,6 +68,11 @@ def _get_auth_request(self, state): sp_pkey = fields.Text( 'Private key of our service provider (this openerpserver)' ) + matching_attribute = fields.Text( + string='Matching Attribute', + default='subject.nameId', + required=True, + ), enabled = fields.Boolean('Enabled', default=False) sequence = fields.Integer('Sequence') css_class = fields.Char('CSS Class') diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index fcb339c57ad..73c13930190 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -72,6 +72,7 @@ def _auth_saml_validate(self, provider_id, token): # we are not yet logged in, so the userid cannot have access to the # fields we need yet login = p.sudo()._get_lasso_for_provider() + matching_attribute = p._get_matching_attr_for_provider() try: login.processAuthnResponseMsg(token) @@ -79,17 +80,76 @@ def _auth_saml_validate(self, provider_id, token): raise Exception('Lasso Profile cannot verify signature') except lasso.ProfileStatusNotSuccessError: raise Exception('Profile Status Not Success Error') - except lasso.Error, e: + except lasso.Error as e: raise Exception(repr(e)) try: login.acceptSso() - except lasso.Error: - raise Exception('Invalid assertion') + except lasso.Error as error: + raise Exception( + 'Invalid assertion : %s' % lasso.strError(error[0]) + ) + + attrs = {} + + for att_statement in login.assertion.attributeStatement: + for attribute in att_statement.attribute: + name = None + lformat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC + nickname = None + try: + name = attribute.name.decode('ascii') + except Exception as e: + _logger.warning('sso_after_response: error decoding name of \ + attribute %s' % attribute.dump()) + else: + try: + if attribute.nameFormat: + lformat = attribute.nameFormat.decode('ascii') + if attribute.friendlyName: + nickname = attribute.friendlyName + except Exception as e: + message = 'sso_after_response: name or format of an \ + attribute failed to decode as ascii: %s due to %s' + _logger.warning(message % (attribute.dump(), str(e))) + try: + if name: + if lformat: + if nickname: + key = (name, lformat, nickname) + else: + key = (name, lformat) + else: + key = name + attrs[key] = list() + for value in attribute.attributeValue: + content = [a.exportToXml() for a in value.any] + content = ''.join(content) + attrs[key].append(content.decode('utf8')) + except Exception as e: + message = 'sso_after_response: value of an \ + attribute failed to decode as ascii: %s due to %s' + _logger.warning(message % (attribute.dump(), str(e))) + + matching_value = None + for k in attrs: + if isinstance(k, tuple) and k[0] == matching_attribute: + matching_value = attrs[k][0] + break + + if not matching_value and matching_attribute == "subject.nameId": + matching_value = login.assertion.subject.nameId.content + + elif not matching_value and matching_attribute != "subject.nameId": + raise Exception( + "Matching attribute %s not found in user attrs: %s" % ( + matching_attribute, + attrs, + ) + ) - # TODO use a real token validation from LASSO - # TODO push into the validation result a real UPN - return {'user_id': login.assertion.subject.nameId.content} + validation = {'user_id': matching_value} + return validation @api.multi def _auth_saml_signin(self, provider, validation, saml_response): diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml index 21f3827b0b8..fa4cc468425 100644 --- a/auth_saml/views/auth_saml.xml +++ b/auth_saml/views/auth_saml.xml @@ -43,6 +43,7 @@ + From e5e973057197a6e79337ecc1c9006eead8134aea Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Wed, 2 Mar 2016 17:05:13 +0100 Subject: [PATCH 78/93] Remove a trailing comma introduced during the grafting of rev d67d84c6ebc0 --HG-- branch : odoo8 --- auth_saml/model/auth_saml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 4ca6b69d421..fdda615f557 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -72,7 +72,7 @@ def _get_auth_request(self, state): string='Matching Attribute', default='subject.nameId', required=True, - ), + ) enabled = fields.Boolean('Enabled', default=False) sequence = fields.Integer('Sequence') css_class = fields.Char('CSS Class') From e7845f4ebbbd601b0f59ca58ba0d82854d39f101 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 2 May 2016 16:54:35 +0200 Subject: [PATCH 79/93] Also clear out the "password_crypt" field when clearing out "password" --HG-- branch : odoo8 --- auth_saml/model/res_users.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 73c13930190..7dd7243a815 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -264,7 +264,10 @@ def write(self, vals): if vals and vals.get('saml_uid'): if not self._allow_saml_and_password(): - vals['password'] = False + vals.update({ + 'password': False, + 'password_crypt': False, + }) return super(res_users, self).write(vals) From c60b8e46024b2d95075d3ac3b96259c6cb215800 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 30 May 2016 12:18:30 +0200 Subject: [PATCH 80/93] Security: Let users in the "access rights" group see SAML providers --HG-- branch : odoo8 --- auth_saml/security/ir.model.access.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/auth_saml/security/ir.model.access.csv b/auth_saml/security/ir.model.access.csv index 7f4cc3a4b6e..5d1be902396 100644 --- a/auth_saml/security/ir.model.access.csv +++ b/auth_saml/security/ir.model.access.csv @@ -1,3 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_auth_saml_provider,auth_saml_provider,model_auth_saml_provider,base.group_system,1,1,1,1 access_auth_saml_token,access_auth_saml_token,model_auth_saml_token,,0,0,0,0 +auth_saml_provider_erp_manager_access,auth_saml_provider_erp_manager_access,model_auth_saml_provider,base.group_erp_manager,1,0,0,0 From 1d017d57762604e5444a9bafc5bef6e225733e4e Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 13:57:38 +0200 Subject: [PATCH 81/93] Don't add different states twice to the same URL --HG-- branch : odoo8 --- auth_saml/controllers/main.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 9ab1e0dd1ad..66a38dfab09 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -97,6 +97,12 @@ def web_login(self, *args, **kw): class AuthSAMLController(http.Controller): def get_state(self, provider_id): + """Compute a state to be sent to the IDP so it can forward it back to + us. + + :rtype: Dictionary. + """ + redirect = request.params.get('redirect') or 'web' if not redirect.startswith(('//', 'http://', 'https://')): redirect = '%s%s' % ( @@ -121,6 +127,9 @@ def get_auth_request(self, pid): provider_osv = request.registry.get('auth.saml.provider') auth_request = None + + # store a RelayState on the request to our IDP so that the IDP + # can send us back this info alongside the obtained token state = self.get_state(provider_id) try: @@ -133,16 +142,7 @@ def get_auth_request(self, pid): # TODO: handle case when auth_request comes back as None - # store a RelayState on the request to our IDP so that the IDP - # can send us back this info alongside the obtained token - params = { - "RelayState": simplejson.dumps({ - "d": request.session.db, - "p": pid, - }), - } - url = auth_request + "&" + werkzeug.url_encode(params) - redirect = werkzeug.utils.redirect(url, 303) + redirect = werkzeug.utils.redirect(auth_request, 303) redirect.autocorrect_location_header = True return redirect From e31de2aece870447b68692dbf72cfd071810d171 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 14:23:54 +0200 Subject: [PATCH 82/93] Don't clear out the pass of the Odoo admin. Suggested by petrus-v on . --HG-- branch : odoo8 --- auth_saml/model/res_users.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 7dd7243a815..f22f87ba57e 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -262,8 +262,15 @@ def write(self, vals): ID (as they can't cohabit). """ - if vals and vals.get('saml_uid'): - if not self._allow_saml_and_password(): + # Clear out the pass when: + # - An SAML ID is being set. + # - The user is not the Odoo admin. + # - The "allow both" setting is disabled. + if ( + vals and vals.get('saml_uid') and + self.id is not SUPERUSER_ID and + not self._allow_saml_and_password() + ): vals.update({ 'password': False, 'password_crypt': False, From 9895008c3146f8b5c480484ba984b1e856218a54 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 14:28:21 +0200 Subject: [PATCH 83/93] Add a missing import --HG-- branch : odoo8 --- auth_saml/controllers/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 66a38dfab09..a5e0e95bb9b 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -5,6 +5,7 @@ import werkzeug.utils import openerp +from openerp import _ from openerp import http from openerp.http import request from openerp import SUPERUSER_ID From 0c618ed024e76fc9ce01f1f9ee472ae168a8ef0e Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 14:28:31 +0200 Subject: [PATCH 84/93] Code formatting (flake8 valid) --HG-- branch : odoo8 --- auth_saml/__openerp__.py | 3 ++- auth_saml/model/res_users.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 05c8725df97..6936b934357 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -32,7 +32,8 @@ option if not recommended to allow users to have a password stored in odoo at the same time as having a SALM provider and id. -This module is covered by the Gnu Affero General Public License, AGPLV3 or later +This module is covered by the GNU Affero General Public License, version 3 or +later. The full source code and history can always be downloaded, modified and redistributed from here: diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index f22f87ba57e..84744e8dd3f 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -87,7 +87,7 @@ def _auth_saml_validate(self, provider_id, token): login.acceptSso() except lasso.Error as error: raise Exception( - 'Invalid assertion : %s' % lasso.strError(error[0]) + 'Invalid assertion : %s' % lasso.strError(error[0]) ) attrs = {} @@ -142,10 +142,10 @@ def _auth_saml_validate(self, provider_id, token): elif not matching_value and matching_attribute != "subject.nameId": raise Exception( - "Matching attribute %s not found in user attrs: %s" % ( - matching_attribute, - attrs, - ) + "Matching attribute %s not found in user attrs: %s" % ( + matching_attribute, + attrs, + ) ) validation = {'user_id': matching_value} @@ -239,7 +239,10 @@ def check_credentials(self, cr, uid, token): token ) - except (openerp.exceptions.AccessDenied, passlib.exc.PasswordSizeError): + except ( + openerp.exceptions.AccessDenied, + passlib.exc.PasswordSizeError, + ): # since normal auth did not succeed we now try to find if the user # has an active token attached to his uid res = token_osv.search( From cfd806d9234d4855d2fa6686e86236edb7449e36 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 14:50:14 +0200 Subject: [PATCH 85/93] Don't bork other auth addons when using this one --HG-- branch : odoo8 --- auth_saml/model/res_users.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 84744e8dd3f..b31bc359261 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -6,20 +6,20 @@ import passlib import openerp +from openerp import _ from openerp import api from openerp import models from openerp import fields from openerp import SUPERUSER_ID from openerp.exceptions import ValidationError -# import real addons name of base.res.res_users in order to call it without -# user the super() call -from openerp.addons.base.res.res_users import res_users as baseuser -from openerp import _ _logger = logging.getLogger(__name__) -class res_users(models.Model): +class ResUser(models.Model): + """Add SAML login capabilities to Odoo users. + """ + _inherit = 'res.users' saml_provider_id = fields.Many2one( @@ -232,12 +232,7 @@ def check_credentials(self, cr, uid, token): token_osv = self.pool.get('auth_saml.token') try: - baseuser.check_credentials( - self, - cr, - uid, - token - ) + super(ResUser, self).check_credentials(cr, uid, token) except ( openerp.exceptions.AccessDenied, @@ -279,7 +274,7 @@ def write(self, vals): 'password_crypt': False, }) - return super(res_users, self).write(vals) + return super(ResUser, self).write(vals) @api.model def _allow_saml_and_password(self): From 38bb5f7eb4686357e544d346505a20c6bd18c4d8 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 15:10:58 +0200 Subject: [PATCH 86/93] Odoo 8 style override of "res.users::check_credentials" --HG-- branch : odoo8 --- auth_saml/model/res_users.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index b31bc359261..23eaacbc2cc 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -223,16 +223,18 @@ def auth_saml(self, provider, saml_response): # return user credentials return self.env.cr.dbname, login, saml_response - # This method is using the old v7 API because it is called BEFORE the login - def check_credentials(self, cr, uid, token): - """token can be a password if the user has used the normal form... + @api.model + def check_credentials(self, token): + """Override to handle SAML auths. + + The token can be a password if the user has used the normal form... but we are more interested in the case when they are tokens - and the interesting code is inside the except clause + and the interesting code is inside the "except" clause. """ - token_osv = self.pool.get('auth_saml.token') try: - super(ResUser, self).check_credentials(cr, uid, token) + # Attempt a regular login (via other auth addons) first. + super(ResUser, self).check_credentials(token) except ( openerp.exceptions.AccessDenied, @@ -240,12 +242,11 @@ def check_credentials(self, cr, uid, token): ): # since normal auth did not succeed we now try to find if the user # has an active token attached to his uid - res = token_osv.search( - cr, SUPERUSER_ID, + res = self.env['auth_saml.token'].sudo().search( [ - ('user_id', '=', uid), + ('user_id', '=', self.env.user.id), ('saml_access_token', '=', token), - ] + ], ) # if the user is not found we re-raise the AccessDenied From 3371d443a24ef04c0e204eb12d7523b13c379f2f Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 15:15:22 +0200 Subject: [PATCH 87/93] Remove an old comment --HG-- branch : odoo8 --- auth_saml/model/auth_saml.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index fdda615f557..e5461c61bd1 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -18,8 +18,6 @@ def _get_lasso_for_provider(self): """internal helper to get a configured lasso.Login object for the given provider id""" - # user is not connected yet... so use SUPERUSER_ID - # TODO: we should cache those results somewhere because it is # really costly to always recreate a login variable from buffers server = lasso.Server.newFromBuffers( From 1d6d8f20b3bcdb3e49f1460f4de112e81a203aac Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 15:15:35 +0200 Subject: [PATCH 88/93] Prefer "ensure_one" to a comment --HG-- branch : odoo8 --- auth_saml/model/auth_saml.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index e5461c61bd1..0f31ccb5a16 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -43,8 +43,10 @@ def _get_matching_attr_for_provider(self): @api.multi def _get_auth_request(self, state): """build an authentication request and give it back to our client - WARNING: this method cannot be used for multiple ids """ + + self.ensure_one() + login = self._get_lasso_for_provider() # ! -- this is the part that MUST be performed on each call and From e0c9d124f1508c3a928bc17d575ca2b311706d93 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 15:35:47 +0200 Subject: [PATCH 89/93] Various changes done while submitting to the OCA --HG-- branch : odoo8 --- auth_saml/CONTRIBUTORS | 7 -- auth_saml/LICENSE | 7 -- auth_saml/{NEWS => NEWS.rst} | 0 auth_saml/README | 24 ----- auth_saml/README.rst | 117 ++++++++++++++++++++++++ auth_saml/__init__.py | 4 +- auth_saml/__openerp__.py | 26 +----- auth_saml/controllers/__init__.py | 4 +- auth_saml/controllers/main.py | 4 + auth_saml/model/__init__.py | 8 +- auth_saml/model/auth_saml.py | 12 ++- auth_saml/model/base_settings.py | 4 +- auth_saml/model/res_users.py | 3 +- auth_saml/model/saml_token.py | 5 +- auth_saml/static/description/index.html | 82 +++++++++++++++++ 15 files changed, 228 insertions(+), 79 deletions(-) delete mode 100644 auth_saml/CONTRIBUTORS rename auth_saml/{NEWS => NEWS.rst} (100%) delete mode 100644 auth_saml/README create mode 100644 auth_saml/README.rst create mode 100755 auth_saml/static/description/index.html diff --git a/auth_saml/CONTRIBUTORS b/auth_saml/CONTRIBUTORS deleted file mode 100644 index 02e94ba7885..00000000000 --- a/auth_saml/CONTRIBUTORS +++ /dev/null @@ -1,7 +0,0 @@ -by order of appearance: - - - Florent Aide, - - Vincent Hatakeyama, - - Alexandre Brun, - - Jeremy Co Kim Len, - - Houzefa Abbasbhay diff --git a/auth_saml/LICENSE b/auth_saml/LICENSE index 48a81e39d45..dba13ed2ddf 100644 --- a/auth_saml/LICENSE +++ b/auth_saml/LICENSE @@ -1,10 +1,3 @@ -This module is covered by the Gnu Affero General Public License. -It is copyright XGC Consulting SAS. - -A copy of the full licence is provided below. - -====================================================================== - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/auth_saml/NEWS b/auth_saml/NEWS.rst similarity index 100% rename from auth_saml/NEWS rename to auth_saml/NEWS.rst diff --git a/auth_saml/README b/auth_saml/README deleted file mode 100644 index 96ed9d49cf6..00000000000 --- a/auth_saml/README +++ /dev/null @@ -1,24 +0,0 @@ -### README ### - - -### Authentification SAML for Odoo/OpenERP ### - -This is a module for odoo/OpenERP 7. It provides authentification via SAML. - - -This module allows to deport the management of users and passwords in an -external authentication system to provide SSO functionality (Single Sign On ) -between Odoo and other applications of your ecosystem. - -Benefits -======== - -* Reducing the time spent typing different passwords for different accounts. - -* Reducing the time spent in IT support for password oversights. - -* Centralizing authentication systems. - -* Securing all input levels / exit / access to multiple systems without prompting users. - -* The centralization of access control information for compliance testing to different standards. diff --git a/auth_saml/README.rst b/auth_saml/README.rst new file mode 100644 index 00000000000..b806468a36b --- /dev/null +++ b/auth_saml/README.rst @@ -0,0 +1,117 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +==================== +SAML2 authentication +==================== + +Let users log into Odoo via an SAML2 provider. + +This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On) +between Odoo and other applications of your ecosystem. + + +WARNING: this module requires auth_crypt. This is because you still have the + option if not recommended to allow users to have a password stored in odoo + at the same time as having a SALM provider and id. + + +Benefits +======== + +* Reducing the time spent typing different passwords for different accounts. + +* Reducing the time spent in IT support for password oversights. + +* Centralizing authentication systems. + +* Securing all input levels / exit / access to multiple systems without + prompting users. + +* The centralization of access control information for compliance testing to + different standards. + + +Installation +============ + +Install as you would install any Odoo addon. + +Dependencies +------------ + +This addon requires `lasso`_. + +.. _lasso: http://lasso.entrouvert.org + + +Configuration +============= + +There are SAML-related settings in Configuration > General settings. + + +Usage +===== + +To use this module, you need an authentic2 server, properly set up. +Read the doc at documentation/index.rst for more information. + + +Demo +==== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/149/8.0 + + +Known issues / Roadmap +====================== + +None for now. + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback `here `_. + + +Credits +======= + +Contributors +------------ + +In order of appearance: + + - Florent Aide, + - Vincent Hatakeyama, + - Alexandre Brun, + - Jeremy Co Kim Len, + - Houzéfa Abbasbhay + + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 36b6810dbbb..5e979d05d06 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa -import controllers -import model +from . import controllers +from . import model diff --git a/auth_saml/__openerp__.py b/auth_saml/__openerp__.py index 6936b934357..7300fee2d4f 100644 --- a/auth_saml/__openerp__.py +++ b/auth_saml/__openerp__.py @@ -2,7 +2,7 @@ ############################################################################## # # Saml2 Authentication for Odoo -# Copyright (C) 2010-2015 XCG Consulting +# Copyright (C) 2010-2016 XCG Consulting # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -24,28 +24,10 @@ 'name': 'Saml2 Authentication', 'version': '3.0', 'category': 'Tools', - 'description': """ -Allow users to login through Saml2 Provider. -============================================ - -WARNING: this module requires auth_crypt. This is because you still have the - option if not recommended to allow users to have a password stored in odoo - at the same time as having a SALM provider and id. - -This module is covered by the GNU Affero General Public License, version 3 or -later. - -The full source code and history can always be downloaded, modified -and redistributed from here: - - https://bitbucket.org/xcg/auth_saml/ - or - https://github.com/xcgd/auth_saml - -""", - 'author': 'XCG Consulting s.a.s.', - 'maintainer': 'XCG Consulting s.a.s.', + 'author': 'XCG Consulting, Odoo Community Association (OCA)', + 'maintainer': 'XCG Consulting', 'website': 'http://odoo.consulting', + 'license': 'AGPL-3', 'depends': [ 'base', 'base_setup', diff --git a/auth_saml/controllers/__init__.py b/auth_saml/controllers/__init__.py index e47ae81ca78..6663c573bf0 100644 --- a/auth_saml/controllers/__init__.py +++ b/auth_saml/controllers/__init__.py @@ -1,5 +1,3 @@ # flake8: noqa -import main - -# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: +from . import main diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index a5e0e95bb9b..a96e4f5287e 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import functools import logging @@ -43,6 +45,8 @@ def wrapper(self, req, **kw): # ---------------------------------------------------------- # Controller # ---------------------------------------------------------- + + class SAMLLogin(openerp.addons.web.controllers.main.Home): def list_providers(self): diff --git a/auth_saml/model/__init__.py b/auth_saml/model/__init__.py index b61318a04a7..8f75d153e31 100644 --- a/auth_saml/model/__init__.py +++ b/auth_saml/model/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa -import auth_saml -import base_settings -import res_users -import saml_token +from . import auth_saml +from . import base_settings +from . import res_users +from . import saml_token diff --git a/auth_saml/model/auth_saml.py b/auth_saml/model/auth_saml.py index 0f31ccb5a16..071ce77fb56 100644 --- a/auth_saml/model/auth_saml.py +++ b/auth_saml/model/auth_saml.py @@ -1,12 +1,14 @@ -# -*- encoding: utf-8 -*- -from openerp import models -from openerp import api -from openerp import fields +# -*- coding: utf-8 -*- + import lasso import simplejson +from openerp import api +from openerp import fields +from openerp import models + -class auth_saml_provider(models.Model): +class AuthSamlProvider(models.Model): """Class defining the configuration values of an Saml2 provider""" _name = 'auth.saml.provider' diff --git a/auth_saml/model/base_settings.py b/auth_saml/model/base_settings.py index ac0a599d819..392bedb5a86 100644 --- a/auth_saml/model/base_settings.py +++ b/auth_saml/model/base_settings.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from openerp import fields from openerp import models from openerp import api @@ -6,7 +8,7 @@ _SAML_UID_AND_PASS_SETTING = 'auth_saml.allow_saml.uid_and_internal_password' -class base_settings(models.TransientModel): +class BaseSettings(models.TransientModel): """Inherit from base.config.settings to add a setting. This is only here for easier access; the setting is not actually stored by this (transient) collection. Instead, it is kept in sync with the diff --git a/auth_saml/model/res_users.py b/auth_saml/model/res_users.py index 23eaacbc2cc..afbfc061146 100644 --- a/auth_saml/model/res_users.py +++ b/auth_saml/model/res_users.py @@ -1,4 +1,5 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- + import logging # this is our very own dependency import lasso diff --git a/auth_saml/model/saml_token.py b/auth_saml/model/saml_token.py index eae2518c147..d6bde1b49d8 100644 --- a/auth_saml/model/saml_token.py +++ b/auth_saml/model/saml_token.py @@ -1,5 +1,4 @@ -# -*- encoding: utf-8 -*- -__author__ = 'faide' +# -*- coding: utf-8 -*- import logging from openerp import fields @@ -8,7 +7,7 @@ _logger = logging.getLogger(__name__) -class saml_token(models.Model): +class SamlToken(models.Model): _name = "auth_saml.token" _rec_name = "user_id" diff --git a/auth_saml/static/description/index.html b/auth_saml/static/description/index.html new file mode 100755 index 00000000000..5f38fe36288 --- /dev/null +++ b/auth_saml/static/description/index.html @@ -0,0 +1,82 @@ +License: AGPL-3 +
+

SAML2 authentication

+

Let users log into Odoo via an SAML2 provider.

+

This module allows to deport the management of users and passwords in an +external authentication system to provide SSO functionality (Single Sign On) +between Odoo and other applications of your ecosystem.

+
+
WARNING: this module requires auth_crypt. This is because you still have the
+
option if not recommended to allow users to have a password stored in odoo +at the same time as having a SALM provider and id.
+
+
+

Benefits

+
    +
  • Reducing the time spent typing different passwords for different accounts.
  • +
  • Reducing the time spent in IT support for password oversights.
  • +
  • Centralizing authentication systems.
  • +
  • Securing all input levels / exit / access to multiple systems without +prompting users.
  • +
  • The centralization of access control information for compliance testing to +different standards.
  • +
+
+
+

Installation

+

Install as you would install any Odoo addon.

+
+

Dependencies

+

This addon requires lasso.

+
+
+
+

Configuration

+

There are SAML-related settings in Configuration > General settings.

+
+
+

Usage

+

To use this module, you need an authentic2 server, properly set up. +Read the doc at documentation/index.rst for more information.

+
+
+

Demo

+Try me on Runbot +
+
+

Known issues / Roadmap

+

None for now.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback here.

+
+
+

Credits

+
+

Contributors

+

In order of appearance:

+
+ +
+
+
+

Maintainer

+Odoo Community Association +

This module is maintained by the OCA.

+

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

To contribute to this module, please visit http://odoo-community.org.

+
+
+
+ From e2eb8b29471dc0efd9ec836aa7ce3bbce3d15680 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 15:39:15 +0200 Subject: [PATCH 90/93] model/ -> models/ --HG-- branch : odoo8 --- auth_saml/__init__.py | 2 +- auth_saml/{model => models}/__init__.py | 0 auth_saml/{model => models}/auth_saml.py | 0 auth_saml/{model => models}/base_settings.py | 0 auth_saml/{model => models}/res_users.py | 0 auth_saml/{model => models}/saml_token.py | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename auth_saml/{model => models}/__init__.py (100%) rename auth_saml/{model => models}/auth_saml.py (100%) rename auth_saml/{model => models}/base_settings.py (100%) rename auth_saml/{model => models}/res_users.py (100%) rename auth_saml/{model => models}/saml_token.py (100%) diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py index 5e979d05d06..66ecd5d6587 100644 --- a/auth_saml/__init__.py +++ b/auth_saml/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa from . import controllers -from . import model +from . import models diff --git a/auth_saml/model/__init__.py b/auth_saml/models/__init__.py similarity index 100% rename from auth_saml/model/__init__.py rename to auth_saml/models/__init__.py diff --git a/auth_saml/model/auth_saml.py b/auth_saml/models/auth_saml.py similarity index 100% rename from auth_saml/model/auth_saml.py rename to auth_saml/models/auth_saml.py diff --git a/auth_saml/model/base_settings.py b/auth_saml/models/base_settings.py similarity index 100% rename from auth_saml/model/base_settings.py rename to auth_saml/models/base_settings.py diff --git a/auth_saml/model/res_users.py b/auth_saml/models/res_users.py similarity index 100% rename from auth_saml/model/res_users.py rename to auth_saml/models/res_users.py diff --git a/auth_saml/model/saml_token.py b/auth_saml/models/saml_token.py similarity index 100% rename from auth_saml/model/saml_token.py rename to auth_saml/models/saml_token.py From 93d6fe4527d6b8788c200c5a3437067e14676561 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 17:35:58 +0200 Subject: [PATCH 91/93] Test scaffold (nothing yet) --HG-- branch : odoo8 --- auth_saml/tests/__init__.py | 3 ++ auth_saml/tests/test_auth_saml.py | 21 +++++++++++++ auth_saml/tests/util/__init__.py | 0 auth_saml/tests/util/odoo_tests.py | 47 ++++++++++++++++++++++++++++++ auth_saml/tests/util/singleton.py | 30 +++++++++++++++++++ auth_saml/tests/util/uuidgen.py | 22 ++++++++++++++ 6 files changed, 123 insertions(+) create mode 100644 auth_saml/tests/__init__.py create mode 100644 auth_saml/tests/test_auth_saml.py create mode 100644 auth_saml/tests/util/__init__.py create mode 100644 auth_saml/tests/util/odoo_tests.py create mode 100644 auth_saml/tests/util/singleton.py create mode 100644 auth_saml/tests/util/uuidgen.py diff --git a/auth_saml/tests/__init__.py b/auth_saml/tests/__init__.py new file mode 100644 index 00000000000..85305d3973d --- /dev/null +++ b/auth_saml/tests/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa + +from . import test_auth_saml diff --git a/auth_saml/tests/test_auth_saml.py b/auth_saml/tests/test_auth_saml.py new file mode 100644 index 00000000000..32565d0dea6 --- /dev/null +++ b/auth_saml/tests/test_auth_saml.py @@ -0,0 +1,21 @@ +import openerp.tests + +from .util.odoo_tests import TestBase +from .util.singleton import Singleton +from .util.uuidgen import genUuid + + +class TestMemory(object): + """Keep records in memory across tests.""" + __metaclass__ = Singleton + + +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) +class Test(TestBase): + + def setUp(self): + super(Test, self).setUp() + self.memory = TestMemory() + + # TODO Tests. diff --git a/auth_saml/tests/util/__init__.py b/auth_saml/tests/util/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/auth_saml/tests/util/odoo_tests.py b/auth_saml/tests/util/odoo_tests.py new file mode 100644 index 00000000000..5bf665853c0 --- /dev/null +++ b/auth_saml/tests/util/odoo_tests.py @@ -0,0 +1,47 @@ +"""Utilities useful to Odoo tests. +""" + +import openerp.models +import openerp.tests + + +class TestBase(openerp.tests.SingleTransactionCase): + """Provide some test helpers. + """ + + def createAndTest(self, model, value_list): + """Create records of the specified Odoo model using the specified + values, and ensure afterwards that records have been succesfully + created and that their values are the same as expected. + + :return: The created records. + :rtype: List of openerp.models.BaseModel instances. + """ + + records = [] + + for values in value_list: + + # Maintain a local copy as Odoo calls might modify it... + local_values = values.copy() + + record = self.env[model].create(values) + records.append(record) + + self.assertIsInstance(record, openerp.models.BaseModel) + + for field, value in local_values.iteritems(): + + recorded_value = getattr(record, field) + + # Handle relational fields (Odoo record-sets). + if isinstance(recorded_value, openerp.models.BaseModel): + if isinstance(recorded_value, (tuple, list)): + self.assertEqual(recorded_value.ids, value) + else: + self.assertEqual(recorded_value.id, value) + + else: + self.assertEqual(recorded_value, value) + + return records diff --git a/auth_saml/tests/util/singleton.py b/auth_saml/tests/util/singleton.py new file mode 100644 index 00000000000..e7e1caaf4ab --- /dev/null +++ b/auth_saml/tests/util/singleton.py @@ -0,0 +1,30 @@ +class Singleton(type): + """ + This is a neat singleton pattern. This was found in a comment on this page: + http://www.garyrobinson.net/2004/03/python_singleto.html + + to use this, example : + >>> class C(object): + ... __metaclass__ = Singleton + ... def __init__(self, foo): + ... self.foo = foo + + >>> C('bar').foo + 'bar' + + >>> C().foo + 'bar' + + and your class C is now a singleton, and it is safe to use + the __init__ method as you usually do... + """ + + def __init__(cls, name, bases, dic): + super(Singleton, cls).__init__(name, bases, dic) + cls.instance = None + + def __call__(mcs, *args, **kw): + if mcs.instance is None: + mcs.instance = super(Singleton, mcs).__call__(*args, **kw) + + return mcs.instance diff --git a/auth_saml/tests/util/uuidgen.py b/auth_saml/tests/util/uuidgen.py new file mode 100644 index 00000000000..b9804899fb8 --- /dev/null +++ b/auth_saml/tests/util/uuidgen.py @@ -0,0 +1,22 @@ +"""Utilities to handle unique ID generation. +""" + +import uuid + + +def genUuid(max_chars=None): + """Generate a unique ID and return its hex string representation. + + :param max_chars: Maximum amount of characters to return (might not be a + true UUID then...). + :type max_chars: Integer. + + :rtype: String. + """ + + ret = uuid.uuid4().hex + + if max_chars is not None: + ret = ret[:max_chars] + + return ret From 209a5210f3b6e5ea9f78d13f6d2fecc721521437 Mon Sep 17 00:00:00 2001 From: Houzefa Abbasbhay Date: Mon, 13 Jun 2016 17:51:07 +0200 Subject: [PATCH 92/93] Sphinx doc --HG-- branch : odoo8 --- auth_saml/README.rst | 4 +- auth_saml/doc/Makefile | 223 ++++++++++++ auth_saml/doc/NEWS.rst | 1 + auth_saml/doc/TODO.rst | 7 + auth_saml/doc/autotodo | 69 ++++ auth_saml/doc/autotodo.py | 111 ++++++ auth_saml/doc/conf.py | 332 ++++++++++++++++++ .../index.rst => doc/getting_started.rst} | 4 +- auth_saml/doc/index.rst | 21 ++ auth_saml/doc/models.gv | 7 + auth_saml/doc/models.rst | 7 + auth_saml/doc/requirements | 2 + auth_saml/doc/tests.rst | 10 + 13 files changed, 794 insertions(+), 4 deletions(-) create mode 100644 auth_saml/doc/Makefile create mode 120000 auth_saml/doc/NEWS.rst create mode 100644 auth_saml/doc/TODO.rst create mode 100644 auth_saml/doc/autotodo create mode 100644 auth_saml/doc/autotodo.py create mode 100644 auth_saml/doc/conf.py rename auth_saml/{documentation/index.rst => doc/getting_started.rst} (97%) create mode 100644 auth_saml/doc/index.rst create mode 100644 auth_saml/doc/models.gv create mode 100644 auth_saml/doc/models.rst create mode 100644 auth_saml/doc/requirements create mode 100644 auth_saml/doc/tests.rst diff --git a/auth_saml/README.rst b/auth_saml/README.rst index b806468a36b..c7d4ff16c35 100644 --- a/auth_saml/README.rst +++ b/auth_saml/README.rst @@ -56,8 +56,8 @@ There are SAML-related settings in Configuration > General settings. Usage ===== -To use this module, you need an authentic2 server, properly set up. -Read the doc at documentation/index.rst for more information. +To use this module, you need an IDP server, properly set up. Go through the +"Getting started" section for more information. Demo diff --git a/auth_saml/doc/Makefile b/auth_saml/doc/Makefile new file mode 100644 index 00000000000..03c52bcd1f9 --- /dev/null +++ b/auth_saml/doc/Makefile @@ -0,0 +1,223 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +project:=$(shell basename $(shell readlink -f ..)) +branch:=$(shell hg branch) + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + rm -f autotodo + +.PHONY: html +html: autotodo + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/auth_saml.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/auth_saml.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/auth_saml" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/auth_saml" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +autotodo: + @python autotodo.py ../ .py TODO,FIXME,XXX diff --git a/auth_saml/doc/NEWS.rst b/auth_saml/doc/NEWS.rst new file mode 120000 index 00000000000..e0135754abf --- /dev/null +++ b/auth_saml/doc/NEWS.rst @@ -0,0 +1 @@ +../NEWS.rst \ No newline at end of file diff --git a/auth_saml/doc/TODO.rst b/auth_saml/doc/TODO.rst new file mode 100644 index 00000000000..f569d7f96c4 --- /dev/null +++ b/auth_saml/doc/TODO.rst @@ -0,0 +1,7 @@ +==== +TODO +==== + +.. todolist:: + +.. include:: autotodo diff --git a/auth_saml/doc/autotodo b/auth_saml/doc/autotodo new file mode 100644 index 00000000000..be51a923224 --- /dev/null +++ b/auth_saml/doc/autotodo @@ -0,0 +1,69 @@ +FIXME +===== + +XXX +=== + +TODO +==== + +:class:`main` +------------- + +Line 147 + .. literalinclude:: ../controllers/main.py + :language: python + :lines: 144-151 + :emphasize-lines: 147 + :lineno-start: 144 + +:class:`auth_saml` +------------------ + +Line 22 + .. literalinclude:: ../models/auth_saml.py + :language: python + :lines: 19-26 + :emphasize-lines: 22 + :lineno-start: 19 + +:class:`res_users` +------------------ + +Line 180 + .. literalinclude:: ../models/res_users.py + :language: python + :lines: 177-184 + :emphasize-lines: 180 + :lineno-start: 177 + +:class:`res_users` +------------------ + +Line 254 + .. literalinclude:: ../models/res_users.py + :language: python + :lines: 251-258 + :emphasize-lines: 254 + :lineno-start: 251 + +:class:`test_auth_saml` +----------------------- + +Line 20 + .. literalinclude:: ../tests/test_auth_saml.py + :language: python + :lines: 17-24 + :emphasize-lines: 20 + :lineno-start: 17 + +:class:`conf` +------------- + +Line 315 + .. literalinclude:: ../doc/conf.py + :language: python + :lines: 312-319 + :emphasize-lines: 315 + :lineno-start: 312 + diff --git a/auth_saml/doc/autotodo.py b/auth_saml/doc/autotodo.py new file mode 100644 index 00000000000..0eef07fe919 --- /dev/null +++ b/auth_saml/doc/autotodo.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014 XCG Consulting +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import os +import os.path +import sys + + +def main(): + if len(sys.argv) != 4: + print("usage: autotodo.py ") + sys.exit(1) + + folder = sys.argv[1] + exts = sys.argv[2].split(',') + tags = sys.argv[3].split(',') + todolist = {tag: [] for tag in tags} + + os.path.walk(folder, scan_folder, (exts, tags, todolist)) + create_autotodo(folder, todolist) + + +def write_info(f, infos, folder): + # Check sphinx version for lineno-start support + + import sphinx + + if sphinx.version_info < (1, 3): + lineno_start = False + else: + lineno_start = True + + for i in infos: + path = i[0] + line = i[1] + lines = (line - 3, line + 4) + class_name = ( + ":class:`%s`" % + os.path.basename(os.path.splitext(path)[0]) + ) + f.write( + "%s\n" + "%s\n\n" + "Line %s\n" + "\t.. literalinclude:: %s\n" + "\t\t:language: python\n" + "\t\t:lines: %s-%s\n" + "\t\t:emphasize-lines: %s\n" + % + ( + class_name, + "-" * len(class_name), + line, + path, + lines[0], lines[1], + line, + ) + ) + if lineno_start: + f.write("\t\t:lineno-start: %s\n" % lines[0]) + f.write("\n") + + +def create_autotodo(folder, todolist): + with open('autotodo', 'w+') as f: + for tag, info in todolist.iteritems(): + f.write("%s\n%s\n\n" % (tag, '=' * len(tag))) + write_info(f, info, folder) + + +def scan_folder((exts, tags, res), dirname, names): + file_info = {} + for name in names: + (root, ext) = os.path.splitext(name) + if ext in exts: + file_info = scan_file(os.path.join(dirname, name), tags) + for tag, info in file_info.iteritems(): + if info: + res[tag].extend(info) + + +def scan_file(filename, tags): + res = {tag: [] for tag in tags} + with open(filename, 'r') as f: + for line_num, line in enumerate(f): + for tag in tags: + if tag in line: + res[tag].append((filename, line_num, line[:-1].strip())) + return res + + +if __name__ == "__main__": + main() diff --git a/auth_saml/doc/conf.py b/auth_saml/doc/conf.py new file mode 100644 index 00000000000..909d7874a78 --- /dev/null +++ b/auth_saml/doc/conf.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +# +# SAML2 authentication build configuration file, created by +# sphinx-quickstart on Mon Jun 13 17:24:26 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import ast +import sys +import os + +import openerp + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinxodoo.ext.autodoc', + 'sphinx.ext.graphviz', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'SAML2 authentication' +copyright = u'2016, XCG Consulting' +author = u'XCG Consulting, Odoo Community Association (OCA)' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +with open(os.path.join('..', '__openerp__.py'), 'r') as f: + read_data = f.read() +d = ast.literal_eval(read_data) +# The full version, including alpha/beta/rc tags. +release = d['version'] +# The short X.Y version. +version = '.'.join(release.split('.')[:4]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or +# 32x32 pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'SAML2AuthenticationDoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + + # Latex figure (float) alignment + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'SAML2Authentication.tex', + u'SAML2 authentication documentation', + u'XCG Consulting', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'saml2authentication', u'SAML2 authentication documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'SAML2Authentication', u'SAML2 authentication documentation', + author, 'SAML2Authentication', + 'Let users log into Odoo via an SAML2 provider.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +todo_include_todos = True + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} + + +# +# odoo-sphinx-autodoc +# + +# sphinxodoo_addons : List of addons name to load (if empty, no addon will be loaded) +this_module = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +sphinxodoo_addons = [this_module] +# sphinxodoo_root_path : Path of the Odoo root directory +# sphinxodoo_root_path = os.path.dirname(os.path.dirname(os.path.abspath(openerp.__file__))) +# TODO Fix this. +sphinxodoo_root_path = '/home/habba/Dev/OpenERP/sources/odoo8' + +# sphinxodoo_addons_path : List of paths were Odoo addons to load are located +superproject_path = os.path.dirname(os.path.dirname(os.path.dirname(os.getenv('PWD')))) +with open(os.path.join(superproject_path, 'odoo_type')) as f: + odoo_type = f.read() +sphinxodoo_addons_path = [] +if odoo_type.strip() == 'bzr': + sphinxodoo_addons_path.append(os.path.join(os.getenv('HOME'), 'src', 'openobject-addons')) + sphinxodoo_addons_path.append(os.path.join(os.getenv('HOME'), 'src', 'openerp-web', 'addons')) +else: + sphinxodoo_addons_path.append(os.path.join(sphinxodoo_root_path, 'addons')) + +with open(os.path.join(superproject_path, 'addon_dirs')) as f: + for line in f.read().splitlines(): + sphinxodoo_addons_path.append(os.path.join(superproject_path, line)) diff --git a/auth_saml/documentation/index.rst b/auth_saml/doc/getting_started.rst similarity index 97% rename from auth_saml/documentation/index.rst rename to auth_saml/doc/getting_started.rst index cc3e96e6ae0..f34b759ef84 100644 --- a/auth_saml/documentation/index.rst +++ b/auth_saml/doc/getting_started.rst @@ -1,5 +1,5 @@ -SAML Authentication with Authentic2 -=================================== +Getting started with Authentic2 +=============================== This is quick howto to help setup a service provider that will be able to use the IDP from Authentic2 diff --git a/auth_saml/doc/index.rst b/auth_saml/doc/index.rst new file mode 100644 index 00000000000..018a7090eb4 --- /dev/null +++ b/auth_saml/doc/index.rst @@ -0,0 +1,21 @@ +.. include:: ../README.rst + +Contents: + +.. toctree:: + :maxdepth: 2 + + getting_started + NEWS + models + tests + TODO + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/auth_saml/doc/models.gv b/auth_saml/doc/models.gv new file mode 100644 index 00000000000..722fb78c9c6 --- /dev/null +++ b/auth_saml/doc/models.gv @@ -0,0 +1,7 @@ +digraph model { + + node [ + shape = "record" + ] + +} diff --git a/auth_saml/doc/models.rst b/auth_saml/doc/models.rst new file mode 100644 index 00000000000..5293250d1d3 --- /dev/null +++ b/auth_saml/doc/models.rst @@ -0,0 +1,7 @@ +Models +====== + +.. graphviz:: models.gv + +TODO + diff --git a/auth_saml/doc/requirements b/auth_saml/doc/requirements new file mode 100644 index 00000000000..3c20f90c2dc --- /dev/null +++ b/auth_saml/doc/requirements @@ -0,0 +1,2 @@ +sphinx +git+https://github.com/OCA/odoo-sphinx-autodoc#egg=odoo-sphinx-autodoc diff --git a/auth_saml/doc/tests.rst b/auth_saml/doc/tests.rst new file mode 100644 index 00000000000..5159671d8b0 --- /dev/null +++ b/auth_saml/doc/tests.rst @@ -0,0 +1,10 @@ +Tests +===== + +SAML2 authentication +-------------------- + +.. automodule:: openerp.addons.auth_saml.tests.test_auth_saml + :members: + :undoc-members: + From cff5f27c6aa3fe89f421ba65bbbc9764e5acbf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Houz=C3=A9fa=20Abbasbhay?= Date: Tue, 23 Aug 2016 16:22:34 +0200 Subject: [PATCH 93/93] Code formatting (flake8 valid) --- auth_saml/doc/autotodo.py | 2 +- auth_saml/doc/conf.py | 3 +++ auth_saml/tests/test_auth_saml.py | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/auth_saml/doc/autotodo.py b/auth_saml/doc/autotodo.py index 0eef07fe919..ca404374946 100644 --- a/auth_saml/doc/autotodo.py +++ b/auth_saml/doc/autotodo.py @@ -101,7 +101,7 @@ def scan_file(filename, tags): res = {tag: [] for tag in tags} with open(filename, 'r') as f: for line_num, line in enumerate(f): - for tag in tags: + for tag in tags: if tag in line: res[tag].append((filename, line_num, line[:-1].strip())) return res diff --git a/auth_saml/doc/conf.py b/auth_saml/doc/conf.py index 909d7874a78..aacc89ed8aa 100644 --- a/auth_saml/doc/conf.py +++ b/auth_saml/doc/conf.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- + +# flake8: noqa + # # SAML2 authentication build configuration file, created by # sphinx-quickstart on Mon Jun 13 17:24:26 2016. diff --git a/auth_saml/tests/test_auth_saml.py b/auth_saml/tests/test_auth_saml.py index 32565d0dea6..aff072ae5d0 100644 --- a/auth_saml/tests/test_auth_saml.py +++ b/auth_saml/tests/test_auth_saml.py @@ -2,7 +2,6 @@ from .util.odoo_tests import TestBase from .util.singleton import Singleton -from .util.uuidgen import genUuid class TestMemory(object):