From 3494b828624a2677834d037b95ac31e837f1848d Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Tue, 22 Oct 2024 14:38:02 +0000 Subject: [PATCH 1/9] Initial commit --- auth_saml/README.rst | 171 +++++ auth_saml/__init__.py | 1 + auth_saml/__manifest__.py | 31 + auth_saml/controllers/__init__.py | 3 + auth_saml/controllers/main.py | 293 ++++++++ auth_saml/data/ir_config_parameter.xml | 7 + auth_saml/i18n/auth_saml.pot | 525 ++++++++++++++ auth_saml/i18n/es.po | 569 +++++++++++++++ auth_saml/i18n/fr.po | 679 ++++++++++++++++++ auth_saml/i18n/it.po | 564 +++++++++++++++ auth_saml/models/__init__.py | 11 + .../models/auth_saml_attribute_mapping.py | 37 + auth_saml/models/auth_saml_provider.py | 372 ++++++++++ auth_saml/models/auth_saml_request.py | 21 + auth_saml/models/ir_config_parameter.py | 39 + auth_saml/models/res_config_settings.py | 15 + auth_saml/models/res_users.py | 184 +++++ auth_saml/models/res_users_saml.py | 36 + auth_saml/pyproject.toml | 3 + auth_saml/readme/CONFIGURE.md | 20 + auth_saml/readme/CONTRIBUTORS.md | 15 + auth_saml/readme/DESCRIPTION.md | 16 + auth_saml/readme/HISTORY.md | 3 + auth_saml/readme/INSTALL.md | 4 + auth_saml/readme/ROADMAP.md | 1 + auth_saml/readme/USAGE.md | 2 + auth_saml/security/ir.model.access.csv | 5 + auth_saml/static/description/index.html | 183 +++++ auth_saml/tests/__init__.py | 1 + auth_saml/tests/data/idp.pem | 49 ++ auth_saml/tests/data/sp.key | 28 + auth_saml/tests/data/sp.pem | 21 + auth_saml/tests/fake_idp.py | 179 +++++ .../test_models_saml_attribute_mapping.py | 27 + auth_saml/tests/test_pysaml.py | 424 +++++++++++ auth_saml/views/auth_saml.xml | 203 ++++++ auth_saml/views/res_config_settings.xml | 20 + auth_saml/views/res_users.xml | 24 + 38 files changed, 4786 insertions(+) create mode 100644 auth_saml/README.rst create mode 100644 auth_saml/__init__.py create mode 100644 auth_saml/__manifest__.py create mode 100644 auth_saml/controllers/__init__.py create mode 100644 auth_saml/controllers/main.py create mode 100644 auth_saml/data/ir_config_parameter.xml create mode 100644 auth_saml/i18n/auth_saml.pot create mode 100644 auth_saml/i18n/es.po create mode 100644 auth_saml/i18n/fr.po create mode 100644 auth_saml/i18n/it.po create mode 100644 auth_saml/models/__init__.py create mode 100644 auth_saml/models/auth_saml_attribute_mapping.py create mode 100644 auth_saml/models/auth_saml_provider.py create mode 100644 auth_saml/models/auth_saml_request.py create mode 100644 auth_saml/models/ir_config_parameter.py create mode 100644 auth_saml/models/res_config_settings.py create mode 100644 auth_saml/models/res_users.py create mode 100644 auth_saml/models/res_users_saml.py create mode 100644 auth_saml/pyproject.toml create mode 100644 auth_saml/readme/CONFIGURE.md create mode 100644 auth_saml/readme/CONTRIBUTORS.md create mode 100644 auth_saml/readme/DESCRIPTION.md create mode 100644 auth_saml/readme/HISTORY.md create mode 100644 auth_saml/readme/INSTALL.md create mode 100644 auth_saml/readme/ROADMAP.md create mode 100644 auth_saml/readme/USAGE.md create mode 100644 auth_saml/security/ir.model.access.csv create mode 100755 auth_saml/static/description/index.html create mode 100644 auth_saml/tests/__init__.py create mode 100644 auth_saml/tests/data/idp.pem create mode 100644 auth_saml/tests/data/sp.key create mode 100644 auth_saml/tests/data/sp.pem create mode 100644 auth_saml/tests/fake_idp.py create mode 100644 auth_saml/tests/test_models_saml_attribute_mapping.py create mode 100644 auth_saml/tests/test_pysaml.py create mode 100644 auth_saml/views/auth_saml.xml create mode 100644 auth_saml/views/res_config_settings.xml create mode 100644 auth_saml/views/res_users.xml diff --git a/auth_saml/README.rst b/auth_saml/README.rst new file mode 100644 index 0000000000..a53a8f65af --- /dev/null +++ b/auth_saml/README.rst @@ -0,0 +1,171 @@ +==================== +SAML2 Authentication +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:29773025a7d79e9696be8e0a1b65361642ef6bc8b6fb8f9cb13a4b4719017c71 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/17.0/auth_saml + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-17-0/server-auth-17-0-auth_saml + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Let users log into Odoo via an SAML2 identity 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. + +**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. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This addon requires the python module ``pysaml2``. + +``pysaml2`` requires the binary ``xmlsec1`` (on Debian or Ubuntu you can +install it with ``apt-get install xmlsec1``) + +Configuration +============= + +To use this module, you need an IDP server, properly set up. + +1. Configure the module according to your IdP’s instructions (Settings > + Users & Companies > SAML Providers). +2. Pre-create your users and set the SAML information against the user. + +By default, the module let users have both a password and SAML ids. To +increase security, disable passwords by using the option in Settings. +Note that the admin account can still have a password, even if the +option is activated. Setting the option immediately remove all password +from users with a configured SAML ids. + +If all the users have a SAML id in a single provider, you can set +automatic redirection in the provider settings. The autoredirection will +only be done on the active provider with the highest priority. It is +still possible to access the login without redirection by using the +query parameter ``disable_autoredirect``, as in +``https://example.com/web/login?disable_autoredirect=`` The login is +also displayed if there is an error with SAML login, in order to display +any error message. + +Usage +===== + +Users can login with the configured SAML IdP with buttons added in the +login screen. + +Known issues / Roadmap +====================== + +- clean up ``auth_saml.request`` + +Changelog +========= + +16.0.1.0.0 +---------- + +Initial migration for 16.0. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* XCG Consulting + +Contributors +------------ + +- `XCG Consulting `__: + + - Florent Aide + - Vincent Hatakeyama + - Alexandre Brun + - Houzéfa Abbasbhay + - Szeka Wong + +- Jeremy Co Kim Len +- Jeffery Chen Fan +- Bhavesh Odedra +- `Tecnativa `__: + + - Jairo Llopis + +- `GlodoUK `__: + + - Karl Southern + +- `TAKOBI `__: + + - Lorenzo Battistini + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-vincent-hatakeyama| image:: https://github.com/vincent-hatakeyama.png?size=40px + :target: https://github.com/vincent-hatakeyama + :alt: vincent-hatakeyama + +Current `maintainer `__: + +|maintainer-vincent-hatakeyama| + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/auth_saml/__init__.py b/auth_saml/__init__.py new file mode 100644 index 0000000000..72d3ea60a8 --- /dev/null +++ b/auth_saml/__init__.py @@ -0,0 +1 @@ +from . import controllers, models diff --git a/auth_saml/__manifest__.py b/auth_saml/__manifest__.py new file mode 100644 index 0000000000..981ee9f011 --- /dev/null +++ b/auth_saml/__manifest__.py @@ -0,0 +1,31 @@ +# Copyright (C) 2020 GlodoUK +# Copyright (C) 2010-2016, 2022 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "SAML2 Authentication", + "version": "18.0.0.0.0", + "category": "Tools", + "author": "XCG Consulting, Odoo Community Association (OCA)", + "maintainers": ["vincent-hatakeyama"], + "website": "https://github.com/OCA/server-auth", + "license": "AGPL-3", + "depends": ["base_setup", "web"], + "external_dependencies": { + "python": ["pysaml2"], + "bin": ["xmlsec1"], + # special definition used by OCA to install packages + "deb": ["xmlsec1"], + }, + "demo": [], + "data": [ + "data/ir_config_parameter.xml", + "security/ir.model.access.csv", + "views/auth_saml.xml", + "views/res_config_settings.xml", + "views/res_users.xml", + ], + "installable": True, + "auto_install": False, + "development_status": "Beta", +} diff --git a/auth_saml/controllers/__init__.py b/auth_saml/controllers/__init__.py new file mode 100644 index 0000000000..2a3e5d5654 --- /dev/null +++ b/auth_saml/controllers/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import main diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py new file mode 100644 index 0000000000..fb635d3a72 --- /dev/null +++ b/auth_saml/controllers/main.py @@ -0,0 +1,293 @@ +# Copyright (C) 2020 GlodoUK +# Copyright (C) 2010-2016, 2022-2023 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import functools +import json +import logging + +import werkzeug.utils +from werkzeug.exceptions import BadRequest +from werkzeug.urls import url_quote_plus + +from odoo import ( + SUPERUSER_ID, + _, + api, + exceptions, + http, + models, +) +from odoo import ( + registry as registry_get, +) +from odoo.http import request +from odoo.tools.misc import clean_context + +from odoo.addons.web.controllers.home import Home +from odoo.addons.web.controllers.utils import _get_login_redirect_url, ensure_db + +_logger = logging.getLogger(__name__) + + +# ---------------------------------------------------------- +# helpers +# ---------------------------------------------------------- + + +def fragment_to_query_string(func): + @functools.wraps(func) + def wrapper(self, **kw): + if not kw: + return """""" + return func(self, **kw) + + return wrapper + + +# ---------------------------------------------------------- +# Controller +# ---------------------------------------------------------- + + +class SAMLLogin(Home): + # Disable pylint self use as the method is meant to be reused in other modules + def _list_saml_providers_domain(self): # pylint: disable=no-self-use + return [] + + def list_saml_providers(self, with_autoredirect: bool = False) -> models.Model: + """Return available providers + + :param with_autoredirect: True to only list providers with automatic redirection + :return: a recordset of providers + """ + domain = self._list_saml_providers_domain() + if with_autoredirect: + domain.append(("autoredirect", "=", True)) + providers = request.env["auth.saml.provider"].sudo().search_read(domain) + for provider in providers: + provider["auth_link"] = self._auth_saml_request_link(provider) + return providers + + def _saml_autoredirect(self): + # automatically redirect if any provider is set up to do that + autoredirect_providers = self.list_saml_providers(True) + # do not redirect if asked too or if a SAML error has been found + disable_autoredirect = ( + "disable_autoredirect" in request.params or "error" in request.params + ) + if autoredirect_providers and not disable_autoredirect: + return werkzeug.utils.redirect( + self._auth_saml_request_link(autoredirect_providers[0]), + 303, + ) + return None + + def _auth_saml_request_link(self, provider: models.Model): + """Return the auth request link for the provided provider""" + params = { + "pid": provider["id"], + } + redirect = request.params.get("redirect") + if redirect: + params["redirect"] = redirect + return "/auth_saml/get_auth_request?%s" % werkzeug.urls.url_encode(params) + + @http.route() + def web_client(self, s_action=None, **kw): + ensure_db() + if not request.session.uid: + result = self._saml_autoredirect() + if result: + return result + return super().web_client(s_action, **kw) + + @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 request.redirect(request.params.get("redirect")) + + if request.httprequest.method == "GET": + result = self._saml_autoredirect() + if result: + return result + + providers = self.list_saml_providers() + + response = super().web_login(*args, **kw) + if response.is_qweb: + error = request.params.get("saml_error") + if error == "no-signup": + error = _("Sign up is not allowed on this database.") + elif error == "access-denied": + error = _("Access Denied") + elif error == "expired": + error = _( + "You do not have access to this database. Please contact" + " support." + ) + else: + error = None + + response.qcontext["saml_providers"] = providers + + if error: + response.qcontext["error"] = error + + return response + + +class AuthSAMLController(http.Controller): + def _get_saml_extra_relaystate(self): + """ + Compute any additional extra state to be sent to the IDP so it can + forward it back to us. This is called RelayState. + + The provider will automatically set things like the dbname, provider + id, etc. + """ + redirect = request.params.get("redirect") or "web" + if not redirect.startswith(("//", "http://", "https://")): + redirect = "{}{}".format( + request.httprequest.url_root, + redirect[1:] if redirect[0] == "/" else redirect, + ) + + state = { + "r": url_quote_plus(redirect), + } + return state + + @http.route("/auth_saml/get_auth_request", type="http", auth="none") + def get_auth_request(self, pid): + provider_id = int(pid) + + provider = request.env["auth.saml.provider"].sudo().browse(provider_id) + redirect_url = provider._get_auth_request( + self._get_saml_extra_relaystate(), request.httprequest.url_root.rstrip("/") + ) + if not redirect_url: + raise Exception( + "Failed to get auth request from provider. " + "Either misconfigured SAML provider or unknown provider." + ) + + redirect = werkzeug.utils.redirect(redirect_url, 303) + redirect.autocorrect_location_header = True + return redirect + + @http.route("/auth_saml/signin", type="http", auth="none", csrf=False) + @fragment_to_query_string + def signin(self, **kw): + """ + Client obtained a saml token and passed it back + to us... we need to validate it + """ + saml_response = kw.get("SAMLResponse") + + if not kw.get("RelayState"): + # 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 = json.loads(kw["RelayState"]) + provider = state["p"] + dbname = state["d"] + if not http.db_filter([dbname]): + return BadRequest() + ensure_db(db=dbname) + + request.update_context(**clean_context(state.get("c", {}))) + try: + credentials = ( + request.env["res.users"] + .with_user(SUPERUSER_ID) + .auth_saml( + provider, + saml_response, + request.httprequest.url_root.rstrip("/"), + ) + ) + action = state.get("a") + menu = state.get("m") + redirect = ( + werkzeug.urls.url_unquote_plus(state["r"]) if state.get("r") else False + ) + url = "/web" + if redirect: + url = redirect + elif action: + url = "/#action=%s" % action + elif menu: + url = "/#menu_id=%s" % menu + pre_uid = request.session.authenticate(*credentials) + resp = request.redirect(_get_login_redirect_url(pre_uid, url), 303) + resp.autocorrect_location_header = False + return resp + + except exceptions.AccessDenied: + # saml credentials not valid, + # user could be on a temporary session + _logger.info("SAML2: access denied") + + url = "/web/login?saml_error=expired" + redirect = werkzeug.utils.redirect(url, 303) + redirect.autocorrect_location_header = False + return redirect + + except Exception as e: + # signup error + _logger.exception("SAML2: failure - %s", str(e)) + url = "/web/login?saml_error=access-denied" + + redirect = request.redirect(url, 303) + redirect.autocorrect_location_header = False + return redirect + + @http.route("/auth_saml/metadata", type="http", auth="none", csrf=False) + def saml_metadata(self, **kw): + provider = kw.get("p") + dbname = kw.get("d") + valid = kw.get("valid", None) + + if not dbname or not provider: + _logger.debug("Metadata page asked without database name or provider id") + return request.not_found(_("Missing parameters")) + + provider = int(provider) + + registry = registry_get(dbname) + + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + client = env["auth.saml.provider"].sudo().browse(provider) + if not client.exists(): + return request.not_found(_("Unknown provider")) + + return request.make_response( + client._metadata_string( + valid, request.httprequest.url_root.rstrip("/") + ), + [("Content-Type", "text/xml")], + ) diff --git a/auth_saml/data/ir_config_parameter.xml b/auth_saml/data/ir_config_parameter.xml new file mode 100644 index 0000000000..c65089f451 --- /dev/null +++ b/auth_saml/data/ir_config_parameter.xml @@ -0,0 +1,7 @@ + + + + auth_saml.allow_saml_uid_and_internal_password + True + + diff --git a/auth_saml/i18n/auth_saml.pot b/auth_saml/i18n/auth_saml.pot new file mode 100644 index 0000000000..2d729324af --- /dev/null +++ b/auth_saml/i18n/auth_saml.pot @@ -0,0 +1,525 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_saml +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.providers +msgid "- or -" +msgstr "" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__active +msgid "Active" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__css_class +msgid "Add a CSS class that serves you to style the login button." +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Algorithm used to sign requests." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_config_settings__allow_saml_uid_and_internal_password +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +msgid "" +"Allow SAML users to possess an Odoo password (warning: decreases security)" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Archived" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__attribute_mapping_ids +msgid "Attribute Mapping" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute +msgid "" +"Attribute to look for in the returned IDP response to match against an Odoo " +"user." +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Attribute to match the user in Odoo with against the IDP (Identity " +"Provider). You may use the special case \"subject.nameId\" to match against " +"the nameId in the IDP response." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "Authn Requests Signed" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__autoredirect +msgid "Automatic Redirection" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Available after first save. The URL will change if the provider is deleted " +"& recreated or the database is renamed." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "" +"Base URL sent to Odoo with this, rather than automatically\n" +" detecting from request or system parameter web.base.url" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__css_class +msgid "Button Icon CSS class" +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata +msgid "" +"Configuration for this Identity Provider. Supplied by the provider, in XML " +"format." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_uid +msgid "Created by" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_date +msgid "Created on" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_request_id +msgid "Current Request ID" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_access_token +msgid "Current SAML token for this user" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__display_name +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__display_name +msgid "Display Name" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Display Settings" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__entity_id +msgid "Entity ID" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Entity Identifier sent to the IDP. Often this would be the metadata URL, but" +" it can be any string." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__entity_id +msgid "EntityID passed to IDP, used to identify the Odoo" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Force matching_attribute to lower case before passing back to Odoo." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__id +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__id +msgid "ID" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__attribute_name +msgid "IDP Response Attribute" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata +msgid "Identity Provider Metadata" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Identity Provider Settings" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute +msgid "Identity Provider matching attribute" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "" +"Indicates if the Authentication Requests sent by this SP should be signed by" +" default." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Indicates if this SP wants the IdP to send the assertions signed." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "" +"Indicates if this entity will sign the Logout Requests originated from it." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Indicates that Authentication Responses to this SP must be signed." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "" +"Indicates that either the Authentication Response or the assertions " +"contained within the response to this SP must be signed." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_date +msgid "Last Updated on" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__body +msgid "Link text in Login Dialog" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__body +msgid "Login button label" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "Logout Requests Signed" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Lowercase IDP Matching Attribute" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Mapped attributes are copied from the SAML response at every logon, if " +"available. If multiple values are returned (i.e. a list) then the first " +"value is used." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_metadata_url +msgid "Metadata URL" +msgstr "" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Missing parameters" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__field_name +msgid "Odoo Field" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private +msgid "Odoo Private Key" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private_filename +msgid "Odoo Private Key File Name" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public +msgid "Odoo Public Certificate" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public_filename +msgid "Odoo Public Certificate File Name" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Odoo Settings" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__autoredirect +msgid "" +"Only the provider with the higher priority will be automatically redirected" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "Override Base URL" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__provider_id +msgid "Provider" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__name +msgid "Provider Name" +msgstr "" + +#. module: auth_saml +#: model:ir.actions.act_window,name:auth_saml.action_saml_provider +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +msgid "Providers" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +#: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form +msgid "SAML" +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_request +msgid "SAML Outstanding Requests" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_provider_id +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "SAML Provider" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_provider_id +msgid "SAML Provider that issued the token" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_uid +msgid "SAML Provider user_id" +msgstr "" + +#. module: auth_saml +#: model:ir.ui.menu,name:auth_saml.menu_saml_providers +msgid "SAML Providers" +msgstr "" + +#. module: auth_saml +#: model:ir.model.constraint,message:auth_saml.constraint_res_users_saml_uniq_users_saml_provider_saml_uid +msgid "SAML UID must be unique per provider" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_uid +msgid "SAML User ID" +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_provider +msgid "SAML2 Provider" +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_attribute_mapping +msgid "SAML2 attribute mapping" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users__saml_ids +msgid "Saml" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sequence +msgid "Sequence" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Sign Authenticate Requests" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Sign Metadata" +msgstr "" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Sign up is not allowed on this database." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sig_alg +msgid "Signature Algorithm" +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_ir_config_parameter +msgid "System Parameter" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The URL configured for the ACS must exactly match what is sent. If you have " +"odoo responding on multiple URLs you can use this to force it to send a " +"specific address rather than rely on automatically detecting." +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_access_token +msgid "The current SAML token in use" +msgstr "" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/models/res_users.py:0 +#, python-format +msgid "" +"This database disallows users to have both passwords and SAML IDs. Error for" +" logins %s" +msgstr "" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Unknown provider" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Used to sign requests sent to the IDP. You can use openssl to generate a " +"certificate and key." +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__user_id +msgid "User" +msgstr "" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users_saml +msgid "User to SAML Provider Mapping" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "Want Assertions Or Response Signed" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Want Assertions Signed" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Want Response Signed" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Whether metadata should be signed or not" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Whether the request should be signed or not" +msgstr "" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "You do not have access to this database. Please contact support." +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your ACS url will be base_url + /auth_saml/signin" +msgstr "" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your provider will give you this XML once configured." +msgstr "" diff --git a/auth_saml/i18n/es.po b/auth_saml/i18n/es.po new file mode 100644 index 0000000000..6dc88be8d5 --- /dev/null +++ b/auth_saml/i18n/es.po @@ -0,0 +1,569 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_saml +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-09-02 19:25+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\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: Weblate 4.17\n" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.providers +msgid "- or -" +msgstr "- o -" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "Acceso Denegado" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__active +msgid "Active" +msgstr "Activo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__css_class +msgid "Add a CSS class that serves you to style the login button." +msgstr "" +"Añade una clase CSS que te sirva para dar estilo al botón de inicio de " +"sesión." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Algorithm used to sign requests." +msgstr "Algoritmo utilizado para firmar las solicitudes." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_config_settings__allow_saml_uid_and_internal_password +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +msgid "" +"Allow SAML users to possess an Odoo password (warning: decreases security)" +msgstr "" +"Permitir a los usuarios SAML poseer una contraseña Odoo (advertencia: " +"disminuye la seguridad)" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Archived" +msgstr "Archivado" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__attribute_mapping_ids +msgid "Attribute Mapping" +msgstr "Mapeo de atributos" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute +msgid "" +"Attribute to look for in the returned IDP response to match against an Odoo " +"user." +msgstr "" +"Atributo a buscar en la respuesta IDP devuelta para comparar con un usuario " +"Odoo." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Attribute to match the user in Odoo with against the IDP (Identity " +"Provider). You may use the special case \"subject.nameId\" to match against " +"the nameId in the IDP response." +msgstr "" +"Atributo para comparar el usuario en Odoo con el IDP (Proveedor de " +"Identidad). Puede usar el caso especial \"subject.nameId\" para comparar con " +"el nameId en la respuesta IDP." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "Authn Requests Signed" +msgstr "Solicitudes de autenticación firmadas" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__autoredirect +msgid "Automatic Redirection" +msgstr "Redireccionamiento automático" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Available after first save. The URL will change if the provider is deleted " +"& recreated or the database is renamed." +msgstr "" +"Disponible después de guardar por primera vez. La URL cambiará si se elimina " +"el proveedor & se vuelve a crear o se cambia el nombre de la base de " +"datos." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "" +"Base URL sent to Odoo with this, rather than automatically\n" +" detecting from request or system parameter web.base.url" +msgstr "" +"URL base enviada a Odoo con esto, en lugar de automáticamente\n" +" detectando desde la petición o el parámetro del sistema web.base.url" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__css_class +msgid "Button Icon CSS class" +msgstr "Clase CSS de icono de botón" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de Configuración" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata +msgid "" +"Configuration for this Identity Provider. Supplied by the provider, in XML " +"format." +msgstr "" +"Configuración de este proveedor de identidades. Proporcionada por el " +"proveedor, en formato XML." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_request_id +msgid "Current Request ID" +msgstr "ID de solicitud actual" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_access_token +msgid "Current SAML token for this user" +msgstr "Ficha SAML actual para este usuario" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__display_name +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Display Settings" +msgstr "Configuración de la pantalla" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__entity_id +msgid "Entity ID" +msgstr "ID de entidad" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Entity Identifier sent to the IDP. Often this would be the metadata URL, but " +"it can be any string." +msgstr "" +"Identificador de entidad enviado al IDP. Suele ser la URL de los metadatos, " +"pero puede ser cualquier cadena." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__entity_id +msgid "EntityID passed to IDP, used to identify the Odoo" +msgstr "EntityID pasado a IDP, utilizado para identificar el Odoo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Force matching_attribute to lower case before passing back to Odoo." +msgstr "Forzar matching_attribute a minúsculas antes de pasar de nuevo a Odoo." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__id +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__attribute_name +msgid "IDP Response Attribute" +msgstr "Atributo de la respuesta del PDI" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata +msgid "Identity Provider Metadata" +msgstr "Metadatos del proveedor de identidad" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Identity Provider Settings" +msgstr "Configuración del proveedor de identidades" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute +msgid "Identity Provider matching attribute" +msgstr "Atributo de coincidencia del proveedor de identidad" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "" +"Indicates if the Authentication Requests sent by this SP should be signed by " +"default." +msgstr "" +"Indica si las Solicitudes de Autenticación enviadas por este SP deben ser " +"firmadas por defecto." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Indicates if this SP wants the IdP to send the assertions signed." +msgstr "Indica si este SP desea que el IdP envíe las aserciones firmadas." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "" +"Indicates if this entity will sign the Logout Requests originated from it." +msgstr "" +"Indica si esta entidad firmará las Solicitudes de Cierre de Sesión " +"originadas desde ella." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Indicates that Authentication Responses to this SP must be signed." +msgstr "" +"Indica que las respuestas de autenticación a este SP deben estar firmadas." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "" +"Indicates that either the Authentication Response or the assertions " +"contained within the response to this SP must be signed." +msgstr "" +"Indica que la respuesta de autenticación o las afirmaciones contenidas en la " +"respuesta a este SP deben estar firmadas." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__body +msgid "Link text in Login Dialog" +msgstr "Texto de enlace en el diálogo de inicio de sesión" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__body +msgid "Login button label" +msgstr "Texto de enlace en el diálogo de inicio de sesión" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "Logout Requests Signed" +msgstr "Solicitudes de cierre de sesión firmadas" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Lowercase IDP Matching Attribute" +msgstr "Atributo de coincidencia de IDP en minúsculas" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Mapped attributes are copied from the SAML response at every logon, if " +"available. If multiple values are returned (i.e. a list) then the first " +"value is used." +msgstr "" +"Los atributos asignados se copian de la respuesta SAML en cada inicio de " +"sesión, si están disponibles. Si se devuelven varios valores (es decir, una " +"lista), se utiliza el primero." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_metadata_url +msgid "Metadata URL" +msgstr "URL de metadatos" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Missing parameters" +msgstr "Parámetros que faltan" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__field_name +msgid "Odoo Field" +msgstr "Campo de Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private +msgid "Odoo Private Key" +msgstr "Clave privada de Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private_filename +msgid "Odoo Private Key File Name" +msgstr "Nombre de archivo de la clave privada de Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public +msgid "Odoo Public Certificate" +msgstr "Certificado público de Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public_filename +msgid "Odoo Public Certificate File Name" +msgstr "Nombre de archivo del certificado público de Odoo" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Odoo Settings" +msgstr "Ajustes Odoo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__autoredirect +msgid "" +"Only the provider with the higher priority will be automatically redirected" +msgstr "Sólo se redirigirá automáticamente al proveedor con mayor prioridad" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "Override Base URL" +msgstr "Anular URL base" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__provider_id +msgid "Provider" +msgstr "Proveedor" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__name +msgid "Provider Name" +msgstr "Nombre de Proveedor" + +#. module: auth_saml +#: model:ir.actions.act_window,name:auth_saml.action_saml_provider +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +msgid "Providers" +msgstr "Proveedores" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +#: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form +msgid "SAML" +msgstr "SAML" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_request +msgid "SAML Outstanding Requests" +msgstr "Solicitudes pendientes SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_provider_id +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "SAML Provider" +msgstr "Proveedor SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_provider_id +msgid "SAML Provider that issued the token" +msgstr "Proveedor SAML que emitió el código" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_uid +msgid "SAML Provider user_id" +msgstr "ID de usuario del proveedor SAML" + +#. module: auth_saml +#: model:ir.ui.menu,name:auth_saml.menu_saml_providers +msgid "SAML Providers" +msgstr "Proveedores SAML" + +#. module: auth_saml +#: model:ir.model.constraint,message:auth_saml.constraint_res_users_saml_uniq_users_saml_provider_saml_uid +msgid "SAML UID must be unique per provider" +msgstr "El UID de SAML debe ser único por proveedor" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_uid +msgid "SAML User ID" +msgstr "ID de usuario SAML" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_provider +msgid "SAML2 Provider" +msgstr "Proveedor de SAML2" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_attribute_mapping +msgid "SAML2 attribute mapping" +msgstr "Asignación de atributos SAML2" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users__saml_ids +msgid "Saml" +msgstr "Saml" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Sign Authenticate Requests" +msgstr "Firmar Solicitudes de Autenticación" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Sign Metadata" +msgstr "Firmar metadatos" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Sign up is not allowed on this database." +msgstr "No está permitido registrarse en esta base de datos." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sig_alg +msgid "Signature Algorithm" +msgstr "Algoritmo de firma" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_ir_config_parameter +msgid "System Parameter" +msgstr "Parámetro del Sistema" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The URL configured for the ACS must exactly match what is sent. If you have " +"odoo responding on multiple URLs you can use this to force it to send a " +"specific address rather than rely on automatically detecting." +msgstr "" +"La URL configurada para el ACS debe coincidir exactamente con lo que se " +"envía. Si tiene odoo respondiendo en múltiples URLs puede usar esto para " +"forzarlo a enviar una dirección específica en lugar de confiar en la " +"detección automática." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_access_token +msgid "The current SAML token in use" +msgstr "El código SAML actual en uso" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/models/res_users.py:0 +#, python-format +msgid "" +"This database disallows users to have both passwords and SAML IDs. Error for " +"logins %s" +msgstr "" +"Esta base de datos no permite que los usuarios tengan contraseñas ni ID de " +"SAML. Error al iniciar sesión %s" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Unknown provider" +msgstr "Proveedor desconocido" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Used to sign requests sent to the IDP. You can use openssl to generate a " +"certificate and key." +msgstr "" +"Se utiliza para firmar las solicitudes enviadas al IDP. Puede utilizar " +"openssl para generar un certificado y una clave." + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__user_id +msgid "User" +msgstr "Usuario" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users_saml +msgid "User to SAML Provider Mapping" +msgstr "Asignación de usuario a proveedor SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "Want Assertions Or Response Signed" +msgstr "Quiere afirmaciones o respuestas firmadas" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Want Assertions Signed" +msgstr "Desea que se firmen las afirmaciones" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Want Response Signed" +msgstr "Quiere respuesta firmada" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Whether metadata should be signed or not" +msgstr "Si los metadatos deben firmarse o no" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Whether the request should be signed or not" +msgstr "Si la solicitud debe firmarse o no" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "You do not have access to this database. Please contact support." +msgstr "" +"No tiene acceso a esta base de datos. Póngase en contacto con el servicio de " +"asistencia." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your ACS url will be base_url + /auth_saml/signin" +msgstr "Su url ACS será base_url + /auth_saml/signin" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your provider will give you this XML once configured." +msgstr "Su proveedor le proporcionará este XML una vez configurado." + +#~ msgid "Last Modified on" +#~ msgstr "Última Modificación el" diff --git a/auth_saml/i18n/fr.po b/auth_saml/i18n/fr.po new file mode 100644 index 0000000000..c49097ce52 --- /dev/null +++ b/auth_saml/i18n/fr.po @@ -0,0 +1,679 @@ +# 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: 2015-03-03 15:51+0000\n" +"PO-Revision-Date: 2022-04-04 08:56+0000\n" +"Last-Translator: Vincent 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: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.3.2\n" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.providers +msgid "- or -" +msgstr "- ou -" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "Accès refusé" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__active +msgid "Active" +msgstr "Actif" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__css_class +msgid "Add a CSS class that serves you to style the login button." +msgstr "" +"Ajoute une classe CSS utile pour mettre en forme le bouton d’identification." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Algorithm used to sign requests." +msgstr "Algorithme utilisé pour signer les requêtes." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_config_settings__allow_saml_uid_and_internal_password +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +msgid "" +"Allow SAML users to possess 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 +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Archived" +msgstr "Archivé" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__attribute_mapping_ids +msgid "Attribute Mapping" +msgstr "Correspondance des attributs" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute +msgid "" +"Attribute to look for in the returned IDP response to match against an Odoo " +"user." +msgstr "" +"Attribut retourné par le fournisseur d’identité à utiliser pour la " +"correspondance avec un utilisateur Odoo." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Attribute to match the user in Odoo with against the IDP (Identity " +"Provider). You may use the special case \"subject.nameId\" to match against " +"the nameId in the IDP response." +msgstr "" +"Attribut retourné par le fournisseur d’identité à utiliser pour la " +"correspondance avec un utilisateur Odoo. La valeur spéciale « subject." +"nameId » peut être utilisée pour une correspondance avec le nameId de la " +"réponse du fournisseur d’identité." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "Authn Requests Signed" +msgstr "Signature des requêtes Authn" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__autoredirect +msgid "Automatic Redirection" +msgstr "Redirection automatique" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Available after first save. The URL will change if the provider is deleted " +"& recreated or the database is renamed." +msgstr "" +"Disponible après enregistrement. L’adresse universelle changera si le " +"fournisseur est supprimé et recréée ou bien si la base de donnée est " +"renommée." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "" +"Base URL sent to Odoo with this, rather than automatically\n" +" detecting from request or system parameter web.base.url" +msgstr "" +"Base de l’adresse universelle envoyé à Odoo avec ceci, plutôt que de la " +"détecter depuis la requête ou le paramètre système « web.base.url »" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__css_class +msgid "Button Icon CSS class" +msgstr "Classe CSS pour l’icône du bouton" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de configuration" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata +msgid "" +"Configuration for this Identity Provider. Supplied by the provider, in XML " +"format." +msgstr "" +"Configuration pour ce fournisseur d’identité. Fourni par le fournisseur, au " +"format XML." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_request_id +msgid "Current Request ID" +msgstr "Identifiant de la requête courrante" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_access_token +msgid "Current SAML token for this user" +msgstr "Jeton SAML courant de l’utilisateur" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__display_name +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Display Settings" +msgstr "Paramètres d’affichage" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__entity_id +msgid "Entity ID" +msgstr "Identifiant de l’entité" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Entity Identifier sent to the IDP. Often this would be the metadata URL, but " +"it can be any string." +msgstr "" +"Identifiant de l’entité envoyé au fournisseur d’identité. C’est souvent " +"l’URL des métadonnées, mais peut être toute chaîne." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__entity_id +msgid "EntityID passed to IDP, used to identify the Odoo" +msgstr "" +"EntityID envoyé au fournisseur d’identité, utilisé pour identifier cet Odoo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Force matching_attribute to lower case before passing back to Odoo." +msgstr "" +"Forcer le bas de casse pour les correspondances d’attribut avant de les " +"envoyer à Odoo." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__id +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__id +msgid "ID" +msgstr "Identifiant" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__attribute_name +msgid "IDP Response Attribute" +msgstr "Attribut de réponse du FI" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata +msgid "Identity Provider Metadata" +msgstr "Métadonnées du fournisseur d’identité" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Identity Provider Settings" +msgstr "Paramètres du fournisseur d’identité" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute +msgid "Identity Provider matching attribute" +msgstr "Attribut de correspondance du fournisseur d’identité" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "" +"Indicates if the Authentication Requests sent by this SP should be signed by " +"default." +msgstr "" +"Indique si les requêtes d’authentification envoyé par ce fournisseur de " +"service doivent être signées par défaut." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Indicates if this SP wants the IdP to send the assertions signed." +msgstr "" +"Indique si le fournisseur de service veut que le fournisseur d’identité " +"signe les assertions." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "" +"Indicates if this entity will sign the Logout Requests originated from it." +msgstr "" +"Indique si cet entité signera les requêtes de déconnexion dont il est " +"l’origine." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Indicates that Authentication Responses to this SP must be signed." +msgstr "" +"Indique que les réponses d’authentification de ce fournisseur de service " +"doivent être signées." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "" +"Indicates that either the Authentication Response or the assertions " +"contained within the response to this SP must be signed." +msgstr "" +"Indique que soit les réponses d’authentification soit les assertions contenu " +"dans la réponse envoyé à ce fournisseur de service doivent être signées." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_uid +msgid "Last Updated by" +msgstr "Dernière modification par" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_date +msgid "Last Updated on" +msgstr "Dernière modification le" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__body +msgid "Link text in Login Dialog" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__body +msgid "Login button label" +msgstr "" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "Logout Requests Signed" +msgstr "Signature des requêtes de déconnexion" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Lowercase IDP Matching Attribute" +msgstr "Bas de casse de l’attribut de correspondance du FI" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Mapped attributes are copied from the SAML response at every logon, if " +"available. If multiple values are returned (i.e. a list) then the first " +"value is used." +msgstr "" +"Les attributs, si disponibles, seront copiés depuis la réponse SAML à chaque " +"connexion. Si plusieurs valeurs sont retournées, dans le cas d’une liste par " +"exemple, alors seulement la première valeur sera utilisé." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_metadata_url +msgid "Metadata URL" +msgstr "Adresse universelle des métadonnées" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Missing parameters" +msgstr "Paramètres manquants" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__field_name +msgid "Odoo Field" +msgstr "Champ Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private +msgid "Odoo Private Key" +msgstr "Clé privée Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private_filename +msgid "Odoo Private Key File Name" +msgstr "Nom de la clé privée d’Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public +msgid "Odoo Public Certificate" +msgstr "Certificat public d’Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public_filename +msgid "Odoo Public Certificate File Name" +msgstr "Nom de fichier du certificat public d’Odoo" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Odoo Settings" +msgstr "Paramètres Odoo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__autoredirect +msgid "" +"Only the provider with the higher priority will be automatically redirected" +msgstr "" +"Seul le fournisseur avec la plus haute priorité sera automatiquement redirigé" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "Override Base URL" +msgstr "Outrepasser l’adresse universelle de base" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__provider_id +msgid "Provider" +msgstr "Fournisseur" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__name +msgid "Provider Name" +msgstr "Nom du fournisseur" + +#. module: auth_saml +#: model:ir.actions.act_window,name:auth_saml.action_saml_provider +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +msgid "Providers" +msgstr "Fournisseurs" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +#: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form +msgid "SAML" +msgstr "SAML" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_request +msgid "SAML Outstanding Requests" +msgstr "Requêtes en attente SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_provider_id +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "SAML Provider" +msgstr "Fournisseur SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_provider_id +msgid "SAML Provider that issued the token" +msgstr "Fournisseur SAML qui a fourni le jeton" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_uid +msgid "SAML Provider user_id" +msgstr "Identifiant de l’utilisateur pour ce fournisseur SAML" + +#. module: auth_saml +#: model:ir.ui.menu,name:auth_saml.menu_saml_providers +msgid "SAML Providers" +msgstr "Fournisseurs SAML" + +#. module: auth_saml +#: model:ir.model.constraint,message:auth_saml.constraint_res_users_saml_uniq_users_saml_provider_saml_uid +msgid "SAML UID must be unique per provider" +msgstr "L’identifiant SAML doit être unique par fournisseur" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_uid +msgid "SAML User ID" +msgstr "Identifiant utilisateur SAML" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_provider +msgid "SAML2 Provider" +msgstr "Fournisseur SAML2" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_attribute_mapping +msgid "SAML2 attribute mapping" +msgstr "Correspondance d’attribut SAML2" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users__saml_ids +msgid "Saml" +msgstr "SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sequence +msgid "Sequence" +msgstr "Séquence" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Sign Authenticate Requests" +msgstr "Signer les requêtes d’authentification" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Sign Metadata" +msgstr "Signer les métadonnées" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, 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 +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sig_alg +msgid "Signature Algorithm" +msgstr "Algorithme de signature" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_ir_config_parameter +msgid "System Parameter" +msgstr "Paramètre système" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The URL configured for the ACS must exactly match what is sent. If you have " +"odoo responding on multiple URLs you can use this to force it to send a " +"specific address rather than rely on automatically detecting." +msgstr "" +"L’adresse universelle configuré pour le SCA doit correspondre exactement à " +"ce qui est envoyé. Si Odoo répond sur plusieurs adresses universelles, vous " +"pouvez utiliser ceci pour le forcer à envoyer une adresse spécifique plutôt " +"que de dépendre sur la détection automatique." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_access_token +msgid "The current SAML token in use" +msgstr "Le jeton SAML en cours d’utilisation" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/models/res_users.py:0 +#, python-format +msgid "" +"This database disallows users to have both passwords and SAML IDs. Error for " +"logins %s" +msgstr "" +"Cette base de données interdit aux utilisateurs d’avoir à la fois un mot de " +"passe et un identifiant SAML. Erreur pour les identifiants %s" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Unknown provider" +msgstr "Fournisseur inconnu" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Used to sign requests sent to the IDP. You can use openssl to generate a " +"certificate and key." +msgstr "" +"Utilisé pour signer des requêtes envoyées au fournisseur d’identité. Vous " +"pouvez utiliser openssl pour générer le certificat et la clé." + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__user_id +msgid "User" +msgstr "Utilisateur" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users_saml +msgid "User to SAML Provider Mapping" +msgstr "Mappage des utilisateurs aux fournisseurs SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "Want Assertions Or Response Signed" +msgstr "Désire une signature des réponses ou des assertions" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Want Assertions Signed" +msgstr "Désire une signature des assertions" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Want Response Signed" +msgstr "Désire une signature des réponses" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Whether metadata should be signed or not" +msgstr "Signature ou non des métadonnées" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Whether the request should be signed or not" +msgstr "Signature ou non des requêtes" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "You do not have access to this database. Please contact support." +msgstr "" +"Vous n’avez pas accès à cette base de donnée. Veuillez contacter votre " +"support." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your ACS url will be base_url + /auth_saml/signin" +msgstr "" +"Votre adresse universelle SCA sera celle de base suivi de /auth_saml/signin" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your provider will give you this XML once configured." +msgstr "Votre fournisseur vous donnera ce XML une fois configuré." + +#~ msgid "Last Modified on" +#~ msgstr "Dernière modification le" + +#~ msgid "Button Description" +#~ msgstr "Description du bouton" + +#~ msgid "Users" +#~ msgstr "Utilisateurs" + +#~ 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é)" + +#~ msgid "Body" +#~ msgstr "Corps" + +#~ msgid "CSS Class" +#~ msgstr "Classe CSS" + +#~ msgid "Enabled" +#~ msgstr "Activé" + +#~ msgid "IDP Configuration" +#~ msgstr "Configuration FI" + +#~ msgid "Matching Attribute" +#~ msgstr "Attributs correspondants" + +#~ msgid "Private key of our service provider (this openerpserver)" +#~ msgstr "Clé privée de notre fournisseur de service (ce serveur Odoo)" + +#~ msgid "Provider name" +#~ msgstr "Nom du fournisseur" + +#~ msgid "SAML2 provider" +#~ msgstr "Fournisseur SAML2" + +#~ msgid "SP Configuration" +#~ msgstr "Configuration du fournisseur de service" + +#~ msgid "" +#~ "This database disallows users to have both passwords and SAML IDs. Errors " +#~ "for login %s" +#~ msgstr "" +#~ "Cette base de données interdit aux utilisateurs d’avoir à la fois un mot " +#~ "de passe et un identifiant SAML. Erreurs pour l’identifiant %s" + +#~ 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 "arch" +#~ msgstr "arch" + +#~ msgid "auth_saml.token" +#~ msgstr "auth_saml.token" + +#~ msgid "res.config.settings" +#~ msgstr "res.config.settings" + +#~ 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." + +#~ msgid "Sign up error" +#~ msgstr "Erreur d’inscription" + +#~ msgid "Authentication error" +#~ msgstr "Erreur d’authentification" + +#~ msgid "res.config" +#~ msgstr "res.config" + +#~ 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 "Allowed" +#~ msgstr "Autorisé" diff --git a/auth_saml/i18n/it.po b/auth_saml/i18n/it.po new file mode 100644 index 0000000000..565cab3e35 --- /dev/null +++ b/auth_saml/i18n/it.po @@ -0,0 +1,564 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_saml +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-05 10:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\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: Weblate 4.17\n" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.providers +msgid "- or -" +msgstr "- o -" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Access Denied" +msgstr "Accesso negato" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__active +msgid "Active" +msgstr "Attivo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__css_class +msgid "Add a CSS class that serves you to style the login button." +msgstr "Aggiungere una classe CSS utile a definire il pulsante di accesso." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Algorithm used to sign requests." +msgstr "Algoritmo utilizzato per firmare le richieste." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_config_settings__allow_saml_uid_and_internal_password +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +msgid "" +"Allow SAML users to possess an Odoo password (warning: decreases security)" +msgstr "" +"Consente agli utenti SAML di avere una password Odoo (attenzione: diminuisce " +"la sicurezza)" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Archived" +msgstr "In archivio" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__attribute_mapping_ids +msgid "Attribute Mapping" +msgstr "Mappatura attributo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute +msgid "" +"Attribute to look for in the returned IDP response to match against an Odoo " +"user." +msgstr "" +"Attributo da cercare nella risposta IDP restituita da corrispondere con un " +"utente Odoo." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Attribute to match the user in Odoo with against the IDP (Identity " +"Provider). You may use the special case \"subject.nameId\" to match against " +"the nameId in the IDP response." +msgstr "" +"Attributo per corrispondere l'utente in Odoo con quello dell'IDP (provider " +"di identità). Si può utilizzare l'opzione speciale \"subject.nameId\" per " +"corrispondere al nameId nella risposta IDP." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "Authn Requests Signed" +msgstr "Richieste atenticazione firmate" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__autoredirect +msgid "Automatic Redirection" +msgstr "Inoltro automatico" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Available after first save. The URL will change if the provider is deleted " +"& recreated or the database is renamed." +msgstr "" +"Disponibile dopo il primo salvataggio. L'URL cambierà se il forntore è " +"cancellato e ricreato il database rinominato." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "" +"Base URL sent to Odoo with this, rather than automatically\n" +" detecting from request or system parameter web.base.url" +msgstr "" +"URL base inviato a Odoo con questo, anziché rilevarlo \n" +" automaticamente dalla richiesta o parametro di sistema web.base.url" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__css_class +msgid "Button Icon CSS class" +msgstr "Pulsante icona classe CSS" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__idp_metadata +msgid "" +"Configuration for this Identity Provider. Supplied by the provider, in XML " +"format." +msgstr "" +"Configurazione per questo provider di identità. Fornito dal provider, in " +"formato XML." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__create_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__create_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_request_id +msgid "Current Request ID" +msgstr "ID richiesta attuale" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_access_token +msgid "Current SAML token for this user" +msgstr "Token SAML attuale per questo utente" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__display_name +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__display_name +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Display Settings" +msgstr "Visualizza impostazioni" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__entity_id +msgid "Entity ID" +msgstr "ID entità" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Entity Identifier sent to the IDP. Often this would be the metadata URL, but " +"it can be any string." +msgstr "" +"Identificatore entità inviato al IDP. Spesso è l'URL dei metadati, ma può " +"essere qualsiasi stringa." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__entity_id +msgid "EntityID passed to IDP, used to identify the Odoo" +msgstr "EntityID passato all'IDP, utilizzato per identificate Odoo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Force matching_attribute to lower case before passing back to Odoo." +msgstr "Forza matching_attribute a minuscolo prima di restituirlo ad Odoo." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__id +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__id +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__id +msgid "ID" +msgstr "ID" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__attribute_name +msgid "IDP Response Attribute" +msgstr "Attributo risposta IDP" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__idp_metadata +msgid "Identity Provider Metadata" +msgstr "Metadati provider identità" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Identity Provider Settings" +msgstr "Impostazioni provider identità" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute +msgid "Identity Provider matching attribute" +msgstr "Attributo corrsipondenza provider identità" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__authn_requests_signed +msgid "" +"Indicates if the Authentication Requests sent by this SP should be signed by " +"default." +msgstr "" +"ndica se la richiesta di atenticazione inviata da questo SP deve essere " +"firmata in modo predefinito." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Indicates if this SP wants the IdP to send the assertions signed." +msgstr "Indica se questo SP richiede che l'IDP invii la conferma firmata." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "" +"Indicates if this entity will sign the Logout Requests originated from it." +msgstr "" +"Indica se questa entità firmerà la richiesta di uscita da esso generata." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Indicates that Authentication Responses to this SP must be signed." +msgstr "" +"Indica che le risposte di autenticazione a questo SP devono essere firmate." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "" +"Indicates that either the Authentication Response or the assertions " +"contained within the response to this SP must be signed." +msgstr "" +"Indica che la risposta di autenticazione o le dichiarazioni contenute nelle " +"risposte a questo SP deve essere firmata." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_uid +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__write_date +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__write_date +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__body +msgid "Link text in Login Dialog" +msgstr "Collega il testo nella maschera di accesso" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__body +msgid "Login button label" +msgstr "Etichetta pulsante accesso" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__logout_requests_signed +msgid "Logout Requests Signed" +msgstr "Richiesta uscita firmata" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__matching_attribute_to_lower +msgid "Lowercase IDP Matching Attribute" +msgstr "Attributo corrispondenza IDP minuscolo" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Mapped attributes are copied from the SAML response at every logon, if " +"available. If multiple values are returned (i.e. a list) then the first " +"value is used." +msgstr "" +"Gli attributi mappati sono copiati dalla risposta SAML ad ogni accesso, se " +"disponibile. Se vengono restituiti valori multipli (cioè una lista) allora " +"viene usato il primo valore." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_metadata_url +msgid "Metadata URL" +msgstr "URL metadati" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Missing parameters" +msgstr "Parametri mancanti" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__field_name +msgid "Odoo Field" +msgstr "Campo Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private +msgid "Odoo Private Key" +msgstr "Chiave privata Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_private_filename +msgid "Odoo Private Key File Name" +msgstr "Nome file chiave privata Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public +msgid "Odoo Public Certificate" +msgstr "Certificato pubblico Odoo" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_pem_public_filename +msgid "Odoo Public Certificate File Name" +msgstr "Nome file certificato pubblico Odoo" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Odoo Settings" +msgstr "Impostazioni Odoo" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__autoredirect +msgid "" +"Only the provider with the higher priority will be automatically redirected" +msgstr "" +"Solo il provider con la priorità maggiore verrà inoltrato automaticamente" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sp_baseurl +msgid "Override Base URL" +msgstr "Ignora URL base" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_attribute_mapping__provider_id +msgid "Provider" +msgstr "Provider" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__name +msgid "Provider Name" +msgstr "Nome provider" + +#. module: auth_saml +#: model:ir.actions.act_window,name:auth_saml.action_saml_provider +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_provider_view_search +msgid "Providers" +msgstr "Provider" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.auth_saml_base_settings_form +#: model_terms:ir.ui.view,arch_db:auth_saml.view_users_form +msgid "SAML" +msgstr "SAML" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_request +msgid "SAML Outstanding Requests" +msgstr "Richiesta straordinaria SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_provider_id +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "SAML Provider" +msgstr "Provider SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_request__saml_provider_id +msgid "SAML Provider that issued the token" +msgstr "Provider SAML che ha emesso il token" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_uid +msgid "SAML Provider user_id" +msgstr "user_id provider SAML" + +#. module: auth_saml +#: model:ir.ui.menu,name:auth_saml.menu_saml_providers +msgid "SAML Providers" +msgstr "Provider SAML" + +#. module: auth_saml +#: model:ir.model.constraint,message:auth_saml.constraint_res_users_saml_uniq_users_saml_provider_saml_uid +msgid "SAML UID must be unique per provider" +msgstr "UID SAML deve essere univoco per provider" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__saml_uid +msgid "SAML User ID" +msgstr "ID utente SAML" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_provider +msgid "SAML2 Provider" +msgstr "Provider SAML2" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_auth_saml_attribute_mapping +msgid "SAML2 attribute mapping" +msgstr "Mappatura attributo SAML2" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_res_users__saml_ids +msgid "Saml" +msgstr "SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Sign Authenticate Requests" +msgstr "Firma richieste autenticazione" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Sign Metadata" +msgstr "Frma metadati" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Sign up is not allowed on this database." +msgstr "Non è consentito iscriversi a questo database." + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__sig_alg +msgid "Signature Algorithm" +msgstr "Algoritmo firma" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_ir_config_parameter +msgid "System Parameter" +msgstr "Parametro di sistema" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"The URL configured for the ACS must exactly match what is sent. If you have " +"odoo responding on multiple URLs you can use this to force it to send a " +"specific address rather than rely on automatically detecting." +msgstr "" +"L'URL configurato per l'ACS deve corrispondere esattamente a quando inviato. " +"Se si ha Odoo che risponde da URL multipli si può utilizzare per forzarlo ad " +"inviare un indirizzo specifico invece che rispondere al rilevamento " +"automatico." + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_res_users_saml__saml_access_token +msgid "The current SAML token in use" +msgstr "Token SAML attualmente in uso" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/models/res_users.py:0 +#, python-format +msgid "" +"This database disallows users to have both passwords and SAML IDs. Error for " +"logins %s" +msgstr "" +"Questo database nn consente agli utenti di avere sia password che ID SAML. " +"Errore per accessi %s" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "Unknown provider" +msgstr "Provider sconosciuto" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "" +"Used to sign requests sent to the IDP. You can use openssl to generate a " +"certificate and key." +msgstr "" +"Usato per firmare le richieste inviate all'IDP. Si può utilizzare OpenSSL " +"per generare un certificato e una chiave." + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users +#: model:ir.model.fields,field_description:auth_saml.field_res_users_saml__user_id +msgid "User" +msgstr "Utente" + +#. module: auth_saml +#: model:ir.model,name:auth_saml.model_res_users_saml +msgid "User to SAML Provider Mapping" +msgstr "Mappatura tra utente e provider SAML" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_or_response_signed +msgid "Want Assertions Or Response Signed" +msgstr "Richiede conferma o risposta firmate" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_assertions_signed +msgid "Want Assertions Signed" +msgstr "Richiede conferme firmate" + +#. module: auth_saml +#: model:ir.model.fields,field_description:auth_saml.field_auth_saml_provider__want_response_signed +msgid "Want Response Signed" +msgstr "Richiede risposta firmata" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_metadata +msgid "Whether metadata should be signed or not" +msgstr "Se i metadata devno essere firmati o meno" + +#. module: auth_saml +#: model:ir.model.fields,help:auth_saml.field_auth_saml_provider__sign_authenticate_requests +msgid "Whether the request should be signed or not" +msgstr "Se la richiesta deve essere firmata o meno" + +#. module: auth_saml +#. odoo-python +#: code:addons/auth_saml/controllers/main.py:0 +#, python-format +msgid "You do not have access to this database. Please contact support." +msgstr "Non si ha accesso a questo database. Contattare il supporto." + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your ACS url will be base_url + /auth_saml/signin" +msgstr "Il suo URL ACS sarà base_url + /auth_saml/signin" + +#. module: auth_saml +#: model_terms:ir.ui.view,arch_db:auth_saml.view_saml_provider_form +msgid "Your provider will give you this XML once configured." +msgstr "Il suo provider fornirà questo XML una volta configurato." + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" diff --git a/auth_saml/models/__init__.py b/auth_saml/models/__init__.py new file mode 100644 index 0000000000..b6825a4d6e --- /dev/null +++ b/auth_saml/models/__init__.py @@ -0,0 +1,11 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ( + auth_saml_attribute_mapping, + auth_saml_provider, + auth_saml_request, + ir_config_parameter, + res_config_settings, + res_users, + res_users_saml, +) diff --git a/auth_saml/models/auth_saml_attribute_mapping.py b/auth_saml/models/auth_saml_attribute_mapping.py new file mode 100644 index 0000000000..6fb6190538 --- /dev/null +++ b/auth_saml/models/auth_saml_attribute_mapping.py @@ -0,0 +1,37 @@ +from odoo import api, fields, models + + +class AuthSamlAttributeMapping(models.Model): + """ + Attributes to copy from SAML provider on logon, into Odoo + """ + + _name = "auth.saml.attribute.mapping" + _description = "SAML2 attribute mapping" + + provider_id = fields.Many2one( + "auth.saml.provider", + index=True, + required=True, + ) + attribute_name = fields.Char( + string="IDP Response Attribute", + required=True, + ) + field_name = fields.Selection( + string="Odoo Field", + selection="_field_name_selection", + required=True, + ) + + @api.model + def _field_name_selection(self): + user_fields = self.env["res.users"].fields_get().items() + + def valid_field(f, d): + return d.get("type") == "char" and not d.get("readonly") + + result = [(f, d.get("string")) for f, d in user_fields if valid_field(f, d)] + result.sort(key=lambda r: r[1]) + + return result diff --git a/auth_saml/models/auth_saml_provider.py b/auth_saml/models/auth_saml_provider.py new file mode 100644 index 0000000000..4b323b7c26 --- /dev/null +++ b/auth_saml/models/auth_saml_provider.py @@ -0,0 +1,372 @@ +# Copyright (C) 2020 Glodo UK +# Copyright (C) 2010-2016, 2022 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import json +import logging +import os +import tempfile +import urllib.parse + +# dependency name is pysaml2 # pylint: disable=W7936 +import saml2 +import saml2.xmldsig as ds +from saml2.client import Saml2Client +from saml2.config import Config as Saml2Config + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class AuthSamlProvider(models.Model): + """Configuration values of a SAML2 provider""" + + _name = "auth.saml.provider" + _description = "SAML2 Provider" + _order = "sequence, name" + + name = fields.Char("Provider Name", required=True, index="trigram") + entity_id = fields.Char( + "Entity ID", + help="EntityID passed to IDP, used to identify the Odoo", + required=True, + default="odoo", + ) + idp_metadata = fields.Text( + string="Identity Provider Metadata", + help=( + "Configuration for this Identity Provider. Supplied by the" + " provider, in XML format." + ), + required=True, + ) + sp_baseurl = fields.Text( + string="Override Base URL", + help="""Base URL sent to Odoo with this, rather than automatically + detecting from request or system parameter web.base.url""", + ) + sp_pem_public = fields.Binary( + string="Odoo Public Certificate", + attachment=True, + required=True, + ) + sp_pem_public_filename = fields.Char("Odoo Public Certificate File Name") + sp_pem_private = fields.Binary( + string="Odoo Private Key", + attachment=True, + required=True, + ) + sp_pem_private_filename = fields.Char("Odoo Private Key File Name") + sp_metadata_url = fields.Char( + compute="_compute_sp_metadata_url", + string="Metadata URL", + readonly=True, + ) + matching_attribute = fields.Char( + string="Identity Provider matching attribute", + default="subject.nameId", + required=True, + help=( + "Attribute to look for in the returned IDP response to match" + " against an Odoo user." + ), + ) + matching_attribute_to_lower = fields.Boolean( + string="Lowercase IDP Matching Attribute", + help="Force matching_attribute to lower case before passing back to Odoo.", + ) + attribute_mapping_ids = fields.One2many( + "auth.saml.attribute.mapping", + "provider_id", + string="Attribute Mapping", + ) + active = fields.Boolean(default=True) + sequence = fields.Integer(index=True) + css_class = fields.Char( + string="Button Icon CSS class", + help="Add a CSS class that serves you to style the login button.", + default="fa fa-fw fa-sign-in text-primary", + ) + body = fields.Char( + string="Login button label", help="Link text in Login Dialog", translate=True + ) + autoredirect = fields.Boolean( + "Automatic Redirection", + default=False, + help="Only the provider with the higher priority will be automatically " + "redirected", + ) + sig_alg = fields.Selection( + selection=lambda s: s._sig_alg_selection(), + required=True, + string="Signature Algorithm", + ) + # help string is from pysaml2 documentation + authn_requests_signed = fields.Boolean( + default=True, + help="Indicates if the Authentication Requests sent by this SP should be signed" + " by default.", + ) + logout_requests_signed = fields.Boolean( + default=True, + help="Indicates if this entity will sign the Logout Requests originated from it" + ".", + ) + want_assertions_signed = fields.Boolean( + default=True, + help="Indicates if this SP wants the IdP to send the assertions signed.", + ) + want_response_signed = fields.Boolean( + default=True, + help="Indicates that Authentication Responses to this SP must be signed.", + ) + want_assertions_or_response_signed = fields.Boolean( + default=True, + help="Indicates that either the Authentication Response or the assertions " + "contained within the response to this SP must be signed.", + ) + # this one is used in Saml2Client.prepare_for_authenticate + sign_authenticate_requests = fields.Boolean( + default=True, + help="Whether the request should be signed or not", + ) + sign_metadata = fields.Boolean( + default=True, + help="Whether metadata should be signed or not", + ) + + @api.model + def _sig_alg_selection(self): + return [(sig[0], sig[0]) for sig in ds.SIG_ALLOWED_ALG] + + @api.onchange("name") + def _onchange_name(self): + if not self.body: + self.body = self.name + + @api.depends("sp_baseurl") + def _compute_sp_metadata_url(self): + icp_base_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url", "") + ) + + for record in self: + if isinstance(record.id, models.NewId): + record.sp_metadata_url = False + continue + + base_url = icp_base_url + if record.sp_baseurl: + base_url = record.sp_baseurl + + qs = urllib.parse.urlencode({"p": record.id, "d": self.env.cr.dbname}) + + record.sp_metadata_url = urllib.parse.urljoin( + base_url, ("/auth_saml/metadata?%s" % qs) + ) + + def _get_cert_key_path(self, field="sp_pem_public"): + self.ensure_one() + + model_attachment = self.env["ir.attachment"].sudo() + keys = model_attachment.search( + [ + ("res_model", "=", self._name), + ("res_field", "=", field), + ("res_id", "=", self.id), + ], + limit=1, + ) + + if model_attachment._storage() != "file": + # For non-file locations we need to create a temp file to pass to pysaml. + fd, keys_path = tempfile.mkstemp() + with open(keys_path, "wb") as f: + f.write(base64.b64decode(keys.datas)) + os.close(fd) + else: + keys_path = model_attachment._full_path(keys.store_fname) + + return keys_path + + def _get_config_for_provider(self, base_url: str = None) -> Saml2Config: + """ + Internal helper to get a configured Saml2Client + """ + self.ensure_one() + + if self.sp_baseurl: + base_url = self.sp_baseurl + + if not base_url: + base_url = ( + self.env["ir.config_parameter"].sudo().get_param("web.base.url", "") + ) + + acs_url = urllib.parse.urljoin(base_url, "/auth_saml/signin") + settings = { + "metadata": {"inline": [self.idp_metadata]}, + "entityid": self.entity_id, + "service": { + "sp": { + "endpoints": { + "assertion_consumer_service": [ + (acs_url, saml2.BINDING_HTTP_REDIRECT), + (acs_url, saml2.BINDING_HTTP_POST), + (acs_url, saml2.BINDING_HTTP_REDIRECT), + (acs_url, saml2.BINDING_HTTP_POST), + ], + }, + "allow_unsolicited": False, + "authn_requests_signed": self.authn_requests_signed, + "logout_requests_signed": self.logout_requests_signed, + "want_assertions_signed": self.want_assertions_signed, + "want_response_signed": self.want_response_signed, + "want_assertions_or_response_signed": ( + self.want_assertions_or_response_signed + ), + }, + }, + "cert_file": self._get_cert_key_path("sp_pem_public"), + "key_file": self._get_cert_key_path("sp_pem_private"), + } + sp_config = Saml2Config() + sp_config.load(settings) + sp_config.allow_unknown_attributes = True + return sp_config + + def _get_client_for_provider(self, base_url: str = None) -> Saml2Client: + sp_config = self._get_config_for_provider(base_url) + saml_client = Saml2Client(config=sp_config) + return saml_client + + def _get_auth_request(self, extra_state=None, url_root=None): + """ + build an authentication request and give it back to our client + """ + self.ensure_one() + + if extra_state is None: + extra_state = {} + state = { + "d": self.env.cr.dbname, + "p": self.id, + } + state.update(extra_state) + + sig_alg = ds.SIG_RSA_SHA1 + if self.sig_alg: + sig_alg = getattr(ds, self.sig_alg) + + saml_client = self._get_client_for_provider(url_root) + reqid, info = saml_client.prepare_for_authenticate( + sign=self.sign_authenticate_requests, + relay_state=json.dumps(state), + sigalg=sig_alg, + ) + + redirect_url = None + # Select the IdP URL to send the AuthN request to + for key, value in info["headers"]: + if key == "Location": + redirect_url = value + + self._store_outstanding_request(reqid) + + return redirect_url + + def _validate_auth_response(self, token: str, base_url: str = None): + """return the validation data corresponding to the access token""" + self.ensure_one() + + client = self._get_client_for_provider(base_url) + response = client.parse_authn_request_response( + token, + saml2.entity.BINDING_HTTP_POST, + self._get_outstanding_requests_dict(), + ) + matching_value = None + + if self.matching_attribute == "subject.nameId": + matching_value = response.name_id.text + else: + attrs = response.get_identity() + + for k, v in attrs.items(): + if k == self.matching_attribute: + matching_value = v + break + + if not matching_value: + raise Exception( + f"Matching attribute {self.matching_attribute} not found " + f"in user attrs: {attrs}" + ) + + if matching_value and isinstance(matching_value, list): + matching_value = next(iter(matching_value), None) + + if isinstance(matching_value, str) and self.matching_attribute_to_lower: + matching_value = matching_value.lower() + + vals = {"user_id": matching_value} + + post_vals = self._hook_validate_auth_response(response, matching_value) + if post_vals: + vals.update(post_vals) + + return vals + + def _get_outstanding_requests_dict(self): + self.ensure_one() + + requests = ( + self.env["auth_saml.request"] + .sudo() + .search([("saml_provider_id", "=", self.id)]) + ) + return {record.saml_request_id: record.id for record in requests} + + def _store_outstanding_request(self, reqid): + self.ensure_one() + + self.env["auth_saml.request"].sudo().create( + {"saml_provider_id": self.id, "saml_request_id": reqid} + ) + + def _metadata_string(self, valid=None, base_url: str = None): + self.ensure_one() + + sp_config = self._get_config_for_provider(base_url) + return saml2.metadata.create_metadata_string( + None, + config=sp_config, + valid=valid, + cert=self._get_cert_key_path("sp_pem_public"), + keyfile=self._get_cert_key_path("sp_pem_private"), + sign=self.sign_metadata, + ) + + def _hook_validate_auth_response(self, response, matching_value): + self.ensure_one() + vals = {} + attrs = response.get_identity() + + for attribute in self.attribute_mapping_ids: + if attribute.attribute_name not in attrs: + _logger.debug( + "SAML attribute '%s' found in response %s", + attribute.attribute_name, + attrs, + ) + continue + + attribute_value = attrs[attribute.attribute_name] + if isinstance(attribute_value, list): + attribute_value = attribute_value[0] + + vals[attribute.field_name] = attribute_value + + return {"mapped_attrs": vals} diff --git a/auth_saml/models/auth_saml_request.py b/auth_saml/models/auth_saml_request.py new file mode 100644 index 0000000000..ef8a7f2b74 --- /dev/null +++ b/auth_saml/models/auth_saml_request.py @@ -0,0 +1,21 @@ +# Copyright (C) 2020 GlodoUK +# Copyright (C) 2022 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AuthSamlRequest(models.TransientModel): + _name = "auth_saml.request" + _description = "SAML Outstanding Requests" + _rec_name = "saml_request_id" + + saml_provider_id = fields.Many2one( + "auth.saml.provider", + string="SAML Provider that issued the token", + required=True, + ) + saml_request_id = fields.Char( + "Current Request ID", + required=True, + ) diff --git a/auth_saml/models/ir_config_parameter.py b/auth_saml/models/ir_config_parameter.py new file mode 100644 index 0000000000..dd8677d0fa --- /dev/null +++ b/auth_saml/models/ir_config_parameter.py @@ -0,0 +1,39 @@ +# Copyright (C) 2022 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import api, models + +_logger = logging.getLogger(__name__) +ALLOW_SAML_UID_AND_PASSWORD = "auth_saml.allow_saml_uid_and_internal_password" + + +class IrConfigParameter(models.Model): + """Redefined to update users when our parameter is changed.""" + + _inherit = "ir.config_parameter" + + @api.model_create_multi + def create(self, vals_list): + """Redefined to update users when our parameter is changed.""" + result = super().create(vals_list) + if result.filtered(lambda param: param.key == ALLOW_SAML_UID_AND_PASSWORD): + self.env["res.users"].allow_saml_and_password_changed() + return result + + def write(self, vals): + """Redefined to update users when our parameter is changed.""" + result = super().write(vals) + if self.filtered(lambda param: param.key == ALLOW_SAML_UID_AND_PASSWORD): + self.env["res.users"].allow_saml_and_password_changed() + return result + + def unlink(self): + """Redefined to update users when our parameter is deleted.""" + param_saml = self.filtered( + lambda param: param.key == ALLOW_SAML_UID_AND_PASSWORD + ) + result = super().unlink() + if result and param_saml: + self.env["res.users"].allow_saml_and_password_changed() + return result diff --git a/auth_saml/models/res_config_settings.py b/auth_saml/models/res_config_settings.py new file mode 100644 index 0000000000..36b78d959e --- /dev/null +++ b/auth_saml/models/res_config_settings.py @@ -0,0 +1,15 @@ +# Copyright (C) 2010-2016, 2022 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from .ir_config_parameter import ALLOW_SAML_UID_AND_PASSWORD + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + allow_saml_uid_and_internal_password = fields.Boolean( + "Allow SAML users to possess an Odoo password (warning: decreases security)", + config_parameter=ALLOW_SAML_UID_AND_PASSWORD, + ) diff --git a/auth_saml/models/res_users.py b/auth_saml/models/res_users.py new file mode 100644 index 0000000000..412b5c6994 --- /dev/null +++ b/auth_saml/models/res_users.py @@ -0,0 +1,184 @@ +# Copyright (C) 2020 GlodoUK +# Copyright (C) 2010-2016 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from typing import Set # noqa + +import passlib + +from odoo import SUPERUSER_ID, _, api, fields, models, registry, tools +from odoo.exceptions import AccessDenied, ValidationError + +from .ir_config_parameter import ALLOW_SAML_UID_AND_PASSWORD + +_logger = logging.getLogger(__name__) + + +class ResUser(models.Model): + """ + Add SAML login capabilities to Odoo users. + """ + + _inherit = "res.users" + + saml_ids = fields.One2many("res.users.saml", "user_id") + + def _auth_saml_validate(self, provider_id: int, token: str, base_url: str = None): + provider = self.env["auth.saml.provider"].sudo().browse(provider_id) + return provider._validate_auth_response(token, base_url) + + def _auth_saml_signin(self, provider: int, validation: dict, saml_response) -> str: + """Sign in Odoo user corresponding to provider and validated access token. + + :param provider: SAML provider id + :param validation: result of validation of access token + :param saml_response: saml parameters response from the IDP + :return: user login + :raise: odoo.exceptions.AccessDenied if signin failed + + This method can be overridden to add alternative signin methods. + """ + saml_uid = validation["user_id"] + user_saml = self.env["res.users.saml"].search( + [("saml_uid", "=", saml_uid), ("saml_provider_id", "=", provider)], + limit=1, + ) + user = user_saml.user_id + if len(user) != 1: + raise AccessDenied() + + with registry(self.env.cr.dbname).cursor() as new_cr: + new_env = api.Environment(new_cr, self.env.uid, self.env.context) + # Update the token. Need to be committed, otherwise the token is not visible + # to other envs, like the one used in login_and_redirect + user_saml.with_env(new_env).write({"saml_access_token": saml_response}) + + if validation.get("mapped_attrs", {}): + user.write(validation.get("mapped_attrs", {})) + + return user.login + + @api.model + def auth_saml(self, provider: int, saml_response: str, base_url: str = None): + validation = self._auth_saml_validate(provider, saml_response, base_url) + + # required check + if not validation.get("user_id"): + raise AccessDenied() + + # retrieve and sign in user + login = self._auth_saml_signin(provider, validation, saml_response) + + if not login: + raise AccessDenied() + + # return user credentials + return self.env.cr.dbname, login, saml_response + + def _check_credentials(self, password, env): + """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. + """ + try: + # Attempt a regular login (via other auth addons) first. + return super()._check_credentials(password, env) + + except (AccessDenied, passlib.exc.PasswordSizeError): + passwd_allowed = ( + env["interactive"] or not self.env.user._rpc_api_keys_only() + ) + if passwd_allowed and self.env.user.active: + # since normal auth did not succeed we now try to find if the user + # has an active token attached to his uid + token = ( + self.env["res.users.saml"] + .sudo() + .search( + [ + ("user_id", "=", self.env.user.id), + ("saml_access_token", "=", password), + ] + ) + ) + if token: + return + raise AccessDenied() from None + + @api.model + def _saml_allowed_user_ids(self) -> Set[int]: # noqa + """Users that can have a password even if the option to disallow it is set. + + It includes superuser and the admin if it exists. + """ + allowed_users = {SUPERUSER_ID} + user_admin = self.env.ref("base.user_admin", False) + if user_admin: + allowed_users.add(user_admin.id) + return allowed_users + + @api.model + def allow_saml_and_password(self) -> bool: + """Can both SAML and local password auth methods coexist.""" + return tools.str2bool( + self.env["ir.config_parameter"] + .sudo() + .get_param(ALLOW_SAML_UID_AND_PASSWORD) + ) + + def _set_password(self): + """Inverse method of the password field.""" + # Redefine base method to block setting password on users with SAML ids + # And also to be able to set password to a blank value + if not self.allow_saml_and_password(): + saml_users = self.filtered( + lambda user: user.sudo().saml_ids + and user.id not in self._saml_allowed_user_ids() + and user.password + ) + if saml_users: + # same error as an api.constrains because it is a constraint. + # a standard constrains would require the use of SQL as the password + # field is obfuscated by the base module. + raise ValidationError( + _( + "This database disallows users to " + "have both passwords and SAML IDs. " + "Error for logins %s" + ) + % saml_users.mapped("login") + ) + # handle setting password to NULL + blank_password_users = self.filtered(lambda user: user.password is False) + non_blank_password_users = self - blank_password_users + if non_blank_password_users: + # pylint: disable=protected-access + super(ResUser, non_blank_password_users)._set_password() + if blank_password_users: + # similar to what Odoo does in Users._set_encrypted_password + self.env.cr.execute( + "UPDATE res_users SET password = NULL WHERE id IN %s", + (tuple(blank_password_users.ids),), + ) + blank_password_users.invalidate_recordset(fnames=["password"]) + return + + def allow_saml_and_password_changed(self): + """Called after the parameter is changed.""" + if not self.allow_saml_and_password(): + # sudo because the user doing the parameter change might not have the right + # to search or write users + users_to_blank_password = self.sudo().search( + [ + "&", + ("saml_ids", "!=", False), + ("id", "not in", list(self._saml_allowed_user_ids())), + ] + ) + _logger.debug( + "Removing password from %s user(s)", len(users_to_blank_password) + ) + users_to_blank_password.write({"password": False}) diff --git a/auth_saml/models/res_users_saml.py b/auth_saml/models/res_users_saml.py new file mode 100644 index 0000000000..d7cbd308d3 --- /dev/null +++ b/auth_saml/models/res_users_saml.py @@ -0,0 +1,36 @@ +# Copyright (C) 2022 XCG Consulting +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class ResUserSaml(models.Model): + _name = "res.users.saml" + _description = "User to SAML Provider Mapping" + + user_id = fields.Many2one("res.users", index=True, required=True) + saml_provider_id = fields.Many2one( + "auth.saml.provider", string="SAML Provider", index=True + ) + saml_uid = fields.Char("SAML User ID", help="SAML Provider user_id", required=True) + saml_access_token = fields.Char( + "Current SAML token for this user", + required=False, + help="The current SAML token in use", + ) + + _sql_constraints = [ + ( + "uniq_users_saml_provider_saml_uid", + "unique(saml_provider_id, saml_uid)", + "SAML UID must be unique per provider", + ) + ] + + @api.model_create_multi + def create(self, vals_list): + """Creates new records for the res.users.saml model""" + # Redefined to remove password if necessary + result = super().create(vals_list) + if not self.env["res.users"].allow_saml_and_password(): + result.mapped("user_id").write({"password": False}) + return result diff --git a/auth_saml/pyproject.toml b/auth_saml/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/auth_saml/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/auth_saml/readme/CONFIGURE.md b/auth_saml/readme/CONFIGURE.md new file mode 100644 index 0000000000..68072d142c --- /dev/null +++ b/auth_saml/readme/CONFIGURE.md @@ -0,0 +1,20 @@ +To use this module, you need an IDP server, properly set up. + +1. Configure the module according to your IdP’s instructions (Settings + \> Users & Companies \> SAML Providers). +2. Pre-create your users and set the SAML information against the user. + +By default, the module let users have both a password and SAML ids. To +increase security, disable passwords by using the option in Settings. +Note that the admin account can still have a password, even if the +option is activated. Setting the option immediately remove all password +from users with a configured SAML ids. + +If all the users have a SAML id in a single provider, you can set +automatic redirection in the provider settings. The autoredirection will +only be done on the active provider with the highest priority. It is +still possible to access the login without redirection by using the +query parameter `disable_autoredirect`, as in +`https://example.com/web/login?disable_autoredirect=` The login is also +displayed if there is an error with SAML login, in order to display any +error message. diff --git a/auth_saml/readme/CONTRIBUTORS.md b/auth_saml/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..27967be136 --- /dev/null +++ b/auth_saml/readme/CONTRIBUTORS.md @@ -0,0 +1,15 @@ +- [XCG Consulting](https://xcg-consulting.fr/): + - Florent Aide \<\> + - Vincent Hatakeyama \<\> + - Alexandre Brun + - Houzéfa Abbasbhay \<\> + - Szeka Wong \<\> +- Jeremy Co Kim Len \<\> +- Jeffery Chen Fan \<\> +- Bhavesh Odedra \<\> +- [Tecnativa](https://www.tecnativa.com/): + - Jairo Llopis +- [GlodoUK](https://www.glodo.uk/): + - Karl Southern +- [TAKOBI](https://takobi.online/): + - Lorenzo Battistini diff --git a/auth_saml/readme/DESCRIPTION.md b/auth_saml/readme/DESCRIPTION.md new file mode 100644 index 0000000000..e48c71bd45 --- /dev/null +++ b/auth_saml/readme/DESCRIPTION.md @@ -0,0 +1,16 @@ +Let users log into Odoo via an SAML2 identity 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. + +**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/HISTORY.md b/auth_saml/readme/HISTORY.md new file mode 100644 index 0000000000..89020f8c3c --- /dev/null +++ b/auth_saml/readme/HISTORY.md @@ -0,0 +1,3 @@ +## 16.0.1.0.0 + +Initial migration for 16.0. diff --git a/auth_saml/readme/INSTALL.md b/auth_saml/readme/INSTALL.md new file mode 100644 index 0000000000..42b963af3f --- /dev/null +++ b/auth_saml/readme/INSTALL.md @@ -0,0 +1,4 @@ +This addon requires the python module `pysaml2`. + +`pysaml2` requires the binary `xmlsec1` (on Debian or Ubuntu you can +install it with `apt-get install xmlsec1`) diff --git a/auth_saml/readme/ROADMAP.md b/auth_saml/readme/ROADMAP.md new file mode 100644 index 0000000000..ce302e1d98 --- /dev/null +++ b/auth_saml/readme/ROADMAP.md @@ -0,0 +1 @@ +- clean up `auth_saml.request` diff --git a/auth_saml/readme/USAGE.md b/auth_saml/readme/USAGE.md new file mode 100644 index 0000000000..7b724b8cf2 --- /dev/null +++ b/auth_saml/readme/USAGE.md @@ -0,0 +1,2 @@ +Users can login with the configured SAML IdP with buttons added in the +login screen. diff --git a/auth_saml/security/ir.model.access.csv b/auth_saml/security/ir.model.access.csv new file mode 100644 index 0000000000..6c699bed06 --- /dev/null +++ b/auth_saml/security/ir.model.access.csv @@ -0,0 +1,5 @@ +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_res_users_saml,auth_res_users_saml,model_res_users_saml,base.group_erp_manager,1,1,1,1 +access_auth_saml_attribute_mapping,auth_saml_attribute_mapping,model_auth_saml_attribute_mapping,base.group_system,1,1,1,1 +access_auth_saml_request,access_auth_saml_request,model_auth_saml_request,,0,0,0,0 diff --git a/auth_saml/static/description/index.html b/auth_saml/static/description/index.html new file mode 100755 index 0000000000..5e9ade6d3d --- /dev/null +++ b/auth_saml/static/description/index.html @@ -0,0 +1,183 @@ + + License: AGPL-3 + +
+

SAML2 authentication

+

Let users log into Odoo via an SAML2 identity 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. +

+
+

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 pysaml2 and xmlsec1.

+
+
+
+

Configuration

+

To use this module, you need an IDP server, properly set up.

+ +
    +
  1. + Configure the module according to your IdP’s instructions (Settings > Users & + Companies > SAML Providers). +
  2. +
  3. Pre-create your users and set the SAML information against the user.
  4. +
+ +

+ By default, the module let users have both a password and SAML ids. To increase + security, disable passwords by using the option in Settings. Note that the admin + account can still have a password, even if the option is activated. +

+ +

+ If all the users have a SAML id in a single provider, you can set automatic + redirection in the provider settings. It is still possible to access the login + without redirection by using the query parameter + disable_autoredirect, as in + https://example.com/web/login?disable_autoredirect= The login is also + displayed if there is an error with SAML login, in order to display any error + message. +

+
+
+

Usage

+

+ Users can login with the configured SAML IdP with buttons added in the login + screen. +

+
+
+

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 smash 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. +

+
+
+
diff --git a/auth_saml/tests/__init__.py b/auth_saml/tests/__init__.py new file mode 100644 index 0000000000..af2e0f3ed2 --- /dev/null +++ b/auth_saml/tests/__init__.py @@ -0,0 +1 @@ +from . import fake_idp, test_pysaml, test_models_saml_attribute_mapping diff --git a/auth_saml/tests/data/idp.pem b/auth_saml/tests/data/idp.pem new file mode 100644 index 0000000000..ff222d96ed --- /dev/null +++ b/auth_saml/tests/data/idp.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAOOSWfQt/sCwMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMjEwMzAyMjE0ODMwWhcNMjEwNDAxMjE0ODMwWjBF +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAtBIWiINhfAGdNHiNoG9xzz/PPQvgPKKz6RirHpcx7RAaCUcnsTcwZILH +8aQ+lUbP62lIhCQ6o7/EuJt7AGxaOclM6E117o4eDQTcNPekhlqcUpFmL21V2dZg +fL0tfwtu6ny6h/pJsgy66LF1YPgUrHA6e4TFWZOIXL5KHZivuveyeyu+hb14CwzB +uvRMPgI7GeyxrhXlBsfntxKnD9cz4brnM2Eznuy8jSufvh3urCLphek9z4Bnfdsa +7I7bCTAHjkUegWWXiAcRgd+Uluhcu1xQB1h4135dehVKnoaBTgFVUl8IDsqZN1yg +/s7JzCA4iI23JEI0lHIJHTQ2GhpxtQIDAQABo1AwTjAdBgNVHQ4EFgQUXXIizCA8 +7eJpzk2fI2od4Inq4CQwHwYDVR0jBBgwFoAUXXIizCA87eJpzk2fI2od4Inq4CQw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAIohOqEpKjkLBySQYTOdO +ZkO31B3YnVXX9o2rN8ulVzfQNKcvKtGiME8ubcmrFg49sP+T3Y14hG4KfYFJ6zcC +qveitFcmQ8NXHgByTqPGi9ZDdwEBKeHCHIym0R+24hEHw59iH42f6IVtfOZOR02P +UyH1AKY2rktXNatOLysNYigf3JN6WlwahEaQ4XOrx+l0ez8H0mqR51rRUhdxuyBJ +RPhqpWqrWhm6Hmcgv17/PwEQRHsFBF/qWb6iT9Cv0IR8v5gnlimaYyF1gacHKbkb +gpit88248SSh1joHnhyoM+yTVnZiL5xAcuf5HicOmPOMLaKv6pJ/pqNz9jy0V3/U +iw== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0EhaIg2F8AZ00 +eI2gb3HPP889C+A8orPpGKselzHtEBoJRyexNzBkgsfxpD6VRs/raUiEJDqjv8S4 +m3sAbFo5yUzoTXXujh4NBNw096SGWpxSkWYvbVXZ1mB8vS1/C27qfLqH+kmyDLro +sXVg+BSscDp7hMVZk4hcvkodmK+697J7K76FvXgLDMG69Ew+AjsZ7LGuFeUGx+e3 +EqcP1zPhuuczYTOe7LyNK5++He6sIumF6T3PgGd92xrsjtsJMAeORR6BZZeIBxGB +35SW6Fy7XFAHWHjXfl16FUqehoFOAVVSXwgOypk3XKD+zsnMIDiIjbckQjSUcgkd +NDYaGnG1AgMBAAECggEAFpPCAYG/gkXNiRuoXjo64cpVWIkZp2CbABnYsrAwUVHY +gdtLDbwmtCN1oEWAl0TWouSDdBX6yDcuGhtcc7QiJ+amXuX/aFanS+iVF4sJNNM9 +kFisoDusLPDlDh7GCozLblkPJidqgAl6kdxWJD9WkDxOCNifydhmm4I8VrOjLOTV +w5gF0kjSRJmkNbPXiSOmaZzJEuxJYR9tqpanf2Fh/6Xf50Y5Hd+25mqbKUg21lZy +AmHk5DYFZMugPFZgPzcZayr0LotlmfiExrMtrKoI2UX9J//MGrVil1bp8iVTJElJ +fDPAivpcTjO5AlHRl2dfLM4ZUub6E4bw+DDGUX04DQKBgQDYRd0jp8S44763+6sA +/F8jKNzxn5nCOkFUxMalh9+wzd433MsXj50QTVy3BGbm+mmo8ybqcQ2PVDI9mo3n +73g4eG/BeSeYSDfk1gqIP5pb9gaAYKmKrNFphCqvrbJsCwzQ+bgrd3WKAv/isIlT +756B/cl+RP6doDcTsFcBV9VjHwKBgQDVJdRnBAXgeeY8XP2sbd8Mr8Ev8Wf4IpTh +dNN3A+ekcYyBkSdXeB8j+ToAcBWM5T69gofG61V6RHvAUPTf3AU9hWs1K2Xm5+Ln +SEstDwDQ+DdcASCkgC48j2anvlla17Tf5Tcl6DaksxGHYNhT1U+hCiaDDU03mrDF +aVIFbSNEqwKBgDZ0GMLiefindxyx5BOCd53Nqxu3OKqbqlliljWVaXAF1Z6xG/2Z +rk0tfVujYxljEXl1h2XeAzEEXQX/xR0RwW5OfKz1CVAhVtlqPwqhIQdogaiPLgD5 +lFyB55GGJXdorNhtF77x/Ak8yhrUoi8dFQbb1IDTdFxRu6xcaPuwlsy3AoGBAKdG +5hfmz1npMOiErkzpeVhygnHGyiqxsRfzYJYRyXSD7Jouuapqyj2oNX3seO03aHLA +AyD4xf+LyXcX0eXxvWcX0xhKM9HwgGG0mdMF6EUX2BJrjButwRukCxNwTp39laT1 +Nb+ZK3E8W3Bcb8nzKWggGDNXeBdAXqS/UDCUA067AoGAUhJg9//JkziNYImCjInX +nsYOH1ojfxtm5pr9+yiobKGdtUe36MQZmW6JMfiqJVjw5NYBgykbDXwrOkK97IeR +2xtx0jcNLyadhzoWIHU/OvJRC1tsdOV30PdsiIRYbpuZjoBiDd2wODeylm9WG9Xz +N6TBKKvJflx4Sw2o7+4EfZU= +-----END PRIVATE KEY----- diff --git a/auth_saml/tests/data/sp.key b/auth_saml/tests/data/sp.key new file mode 100644 index 0000000000..372cdff052 --- /dev/null +++ b/auth_saml/tests/data/sp.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0EhaIg2F8AZ00 +eI2gb3HPP889C+A8orPpGKselzHtEBoJRyexNzBkgsfxpD6VRs/raUiEJDqjv8S4 +m3sAbFo5yUzoTXXujh4NBNw096SGWpxSkWYvbVXZ1mB8vS1/C27qfLqH+kmyDLro +sXVg+BSscDp7hMVZk4hcvkodmK+697J7K76FvXgLDMG69Ew+AjsZ7LGuFeUGx+e3 +EqcP1zPhuuczYTOe7LyNK5++He6sIumF6T3PgGd92xrsjtsJMAeORR6BZZeIBxGB +35SW6Fy7XFAHWHjXfl16FUqehoFOAVVSXwgOypk3XKD+zsnMIDiIjbckQjSUcgkd +NDYaGnG1AgMBAAECggEAFpPCAYG/gkXNiRuoXjo64cpVWIkZp2CbABnYsrAwUVHY +gdtLDbwmtCN1oEWAl0TWouSDdBX6yDcuGhtcc7QiJ+amXuX/aFanS+iVF4sJNNM9 +kFisoDusLPDlDh7GCozLblkPJidqgAl6kdxWJD9WkDxOCNifydhmm4I8VrOjLOTV +w5gF0kjSRJmkNbPXiSOmaZzJEuxJYR9tqpanf2Fh/6Xf50Y5Hd+25mqbKUg21lZy +AmHk5DYFZMugPFZgPzcZayr0LotlmfiExrMtrKoI2UX9J//MGrVil1bp8iVTJElJ +fDPAivpcTjO5AlHRl2dfLM4ZUub6E4bw+DDGUX04DQKBgQDYRd0jp8S44763+6sA +/F8jKNzxn5nCOkFUxMalh9+wzd433MsXj50QTVy3BGbm+mmo8ybqcQ2PVDI9mo3n +73g4eG/BeSeYSDfk1gqIP5pb9gaAYKmKrNFphCqvrbJsCwzQ+bgrd3WKAv/isIlT +756B/cl+RP6doDcTsFcBV9VjHwKBgQDVJdRnBAXgeeY8XP2sbd8Mr8Ev8Wf4IpTh +dNN3A+ekcYyBkSdXeB8j+ToAcBWM5T69gofG61V6RHvAUPTf3AU9hWs1K2Xm5+Ln +SEstDwDQ+DdcASCkgC48j2anvlla17Tf5Tcl6DaksxGHYNhT1U+hCiaDDU03mrDF +aVIFbSNEqwKBgDZ0GMLiefindxyx5BOCd53Nqxu3OKqbqlliljWVaXAF1Z6xG/2Z +rk0tfVujYxljEXl1h2XeAzEEXQX/xR0RwW5OfKz1CVAhVtlqPwqhIQdogaiPLgD5 +lFyB55GGJXdorNhtF77x/Ak8yhrUoi8dFQbb1IDTdFxRu6xcaPuwlsy3AoGBAKdG +5hfmz1npMOiErkzpeVhygnHGyiqxsRfzYJYRyXSD7Jouuapqyj2oNX3seO03aHLA +AyD4xf+LyXcX0eXxvWcX0xhKM9HwgGG0mdMF6EUX2BJrjButwRukCxNwTp39laT1 +Nb+ZK3E8W3Bcb8nzKWggGDNXeBdAXqS/UDCUA067AoGAUhJg9//JkziNYImCjInX +nsYOH1ojfxtm5pr9+yiobKGdtUe36MQZmW6JMfiqJVjw5NYBgykbDXwrOkK97IeR +2xtx0jcNLyadhzoWIHU/OvJRC1tsdOV30PdsiIRYbpuZjoBiDd2wODeylm9WG9Xz +N6TBKKvJflx4Sw2o7+4EfZU= +-----END PRIVATE KEY----- diff --git a/auth_saml/tests/data/sp.pem b/auth_saml/tests/data/sp.pem new file mode 100644 index 0000000000..af2ef9ad3f --- /dev/null +++ b/auth_saml/tests/data/sp.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAOOSWfQt/sCwMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMjEwMzAyMjE0ODMwWhcNMjEwNDAxMjE0ODMwWjBF +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAtBIWiINhfAGdNHiNoG9xzz/PPQvgPKKz6RirHpcx7RAaCUcnsTcwZILH +8aQ+lUbP62lIhCQ6o7/EuJt7AGxaOclM6E117o4eDQTcNPekhlqcUpFmL21V2dZg +fL0tfwtu6ny6h/pJsgy66LF1YPgUrHA6e4TFWZOIXL5KHZivuveyeyu+hb14CwzB +uvRMPgI7GeyxrhXlBsfntxKnD9cz4brnM2Eznuy8jSufvh3urCLphek9z4Bnfdsa +7I7bCTAHjkUegWWXiAcRgd+Uluhcu1xQB1h4135dehVKnoaBTgFVUl8IDsqZN1yg +/s7JzCA4iI23JEI0lHIJHTQ2GhpxtQIDAQABo1AwTjAdBgNVHQ4EFgQUXXIizCA8 +7eJpzk2fI2od4Inq4CQwHwYDVR0jBBgwFoAUXXIizCA87eJpzk2fI2od4Inq4CQw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAIohOqEpKjkLBySQYTOdO +ZkO31B3YnVXX9o2rN8ulVzfQNKcvKtGiME8ubcmrFg49sP+T3Y14hG4KfYFJ6zcC +qveitFcmQ8NXHgByTqPGi9ZDdwEBKeHCHIym0R+24hEHw59iH42f6IVtfOZOR02P +UyH1AKY2rktXNatOLysNYigf3JN6WlwahEaQ4XOrx+l0ez8H0mqR51rRUhdxuyBJ +RPhqpWqrWhm6Hmcgv17/PwEQRHsFBF/qWb6iT9Cv0IR8v5gnlimaYyF1gacHKbkb +gpit88248SSh1joHnhyoM+yTVnZiL5xAcuf5HicOmPOMLaKv6pJ/pqNz9jy0V3/U +iw== +-----END CERTIFICATE----- diff --git a/auth_saml/tests/fake_idp.py b/auth_saml/tests/fake_idp.py new file mode 100644 index 0000000000..f2865b403d --- /dev/null +++ b/auth_saml/tests/fake_idp.py @@ -0,0 +1,179 @@ +import os +from urllib.parse import parse_qs, urlparse + +from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT, pack +from saml2.authn_context import INTERNETPROTOCOLPASSWORD +from saml2.config import Config as Saml2Config +from saml2.metadata import create_metadata_string +from saml2.saml import NAME_FORMAT_URI, NAMEID_FORMAT_PERSISTENT +from saml2.server import Server + +TYP = {"GET": [BINDING_HTTP_REDIRECT], "POST": [BINDING_HTTP_POST]} + + +AUTHN = { + "class_ref": INTERNETPROTOCOLPASSWORD, + "authn_auth": "http://www.example.com/login", +} + + +BASE = "http://localhost:8000" +CONFIG = { + "entityid": "urn:mace:example.com:saml:example:idp", + "name": "Rolands IdP", + "service": { + "aa": { + "endpoints": { + "attribute_service": [ + ("%s/aap" % BASE, BINDING_HTTP_POST), + ] + }, + }, + "aq": { + "endpoints": { + "authn_query_service": [("%s/aqs" % BASE, BINDING_HTTP_POST)] + }, + }, + "idp": { + "endpoints": { + "single_sign_on_service": [ + ("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT), + ("%s/sso/post" % BASE, BINDING_HTTP_POST), + ], + }, + "policy": { + "default": { + "lifetime": {"minutes": 15}, + "attribute_restrictions": None, + "name_form": NAME_FORMAT_URI, + }, + "urn:mace:example.com:saml:example:sp": { + "lifetime": {"minutes": 5}, + "nameid_format": NAMEID_FORMAT_PERSISTENT, + }, + }, + }, + }, + "debug": 1, + "key_file": os.path.join(os.path.dirname(__file__), "data", "idp.pem"), + "cert_file": os.path.join(os.path.dirname(__file__), "data", "idp.pem"), + "organization": { + "name": "Example", + "display_name": [("Example", "uk")], + "url": "http://www.example.com/", + }, + "contact_person": [ + { + "given_name": "Admin", + "sur_name": "Admin", + "email_address": ["admin@example.com"], + "contact_type": "technical", + }, + ], +} + + +class DummyResponse: + def __init__(self, status, data, headers=None): + self.status_code = status + self.text = data + self.headers = headers or [] + self.content = data + self._identity = {} + + def _unpack(self, ver="SAMLResponse"): + """ + Unpack the response form + """ + _str = self.text + + sr_str = 'name="%s" value="' % ver + rs_str = 'name="RelayState" value="' + + i = _str.find(sr_str) + i += len(sr_str) + j = _str.find('"', i) + + sr = _str[i:j] + + start = _str.find(rs_str, j) + start += len(rs_str) + end = _str.find('"', start) + + rs = _str[start:end] + + return {ver: sr, "RelayState": rs} + + def get_identity(self): + """ + Return the identity attributes + """ + return self._identity + + def set_identity(self, identity): + """ + Set the identity attributes + """ + self._identity = identity + + +class FakeIDP(Server): + def __init__(self, metadatas=None): + settings = CONFIG + if metadatas: + settings.update({"metadata": {"inline": metadatas}}) + + config = Saml2Config() + config.load(settings) + config.allow_unknown_attributes = True + Server.__init__(self, config=config) + + def get_metadata(self): + return create_metadata_string( + None, + config=self.config, + sign=True, + valid=True, + cert=CONFIG.get("cert_file"), + keyfile=CONFIG.get("key_file"), + ) + + def fake_login(self, url): + # Assumes GET query and HTTP_REDIRECT only. + # This is all that auth_pysaml currently supports. + parsed_url = urlparse(url) + qs_dict = parse_qs(parsed_url.query) + + samlreq = qs_dict["SAMLRequest"][0] + rstate = qs_dict["RelayState"][0] + + # process the logon request, and automatically "login" + return self.authn_request_endpoint(samlreq, BINDING_HTTP_REDIRECT, rstate) + + def authn_request_endpoint(self, req, binding, relay_state): + req = self.parse_authn_request(req, binding) + if req.message.protocol_binding == BINDING_HTTP_REDIRECT: + _binding = BINDING_HTTP_POST + else: + _binding = req.message.protocol_binding + + resp_args = self.response_args(req.message, [_binding]) + + identity = { + "surName": "Example", + "givenName": "Test", + "title": "Ind", + "mail": "test@example.com", + } + + resp_args.update({"sign_assertion": True, "sign_response": True}) + + authn_resp = self.create_authn_response( + identity, userid=identity.get("mail"), authn=AUTHN, **resp_args + ) + + _dict = pack.factory( + _binding, authn_resp, resp_args["destination"], relay_state, "SAMLResponse" + ) + + return DummyResponse(**_dict) diff --git a/auth_saml/tests/test_models_saml_attribute_mapping.py b/auth_saml/tests/test_models_saml_attribute_mapping.py new file mode 100644 index 0000000000..41aeb402bb --- /dev/null +++ b/auth_saml/tests/test_models_saml_attribute_mapping.py @@ -0,0 +1,27 @@ +from odoo.tests.common import TransactionCase + + +class TestAuthSamlModelsSamlAttributeMapping(TransactionCase): + def setUp(self): + super().setUp() + self.SamlAttributeMapping = self.env["auth.saml.attribute.mapping"] + + def test_field_name_selection(self): + # Call the method + field_name_selection = self.SamlAttributeMapping._field_name_selection() + + # Fetch all user fields + user_fields = self.env["res.users"].fields_get().items() + + # Filter out valid fields (char and not readonly) + valid_fields = [ + (f, d.get("string")) + for f, d in user_fields + if d.get("type") == "char" and not d.get("readonly") + ] + + # Sort the valid fields by their name + valid_fields.sort(key=lambda r: r[1]) + + # Assert that the selections match the valid fields + self.assertEqual(field_name_selection, valid_fields) diff --git a/auth_saml/tests/test_pysaml.py b/auth_saml/tests/test_pysaml.py new file mode 100644 index 0000000000..9eedaa5405 --- /dev/null +++ b/auth_saml/tests/test_pysaml.py @@ -0,0 +1,424 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 +import html +import os +import urllib +from unittest.mock import patch + +from odoo.exceptions import AccessDenied, UserError, ValidationError +from odoo.tests import HttpCase, tagged + +from .fake_idp import DummyResponse, FakeIDP + + +@tagged("saml", "post_install", "-at_install") +class TestPySaml(HttpCase): + def setUp(self): + super().setUp() + + with open( + os.path.join(os.path.dirname(__file__), "data", "sp.pem"), + encoding="UTF-8", + ) as file: + sp_pem_public = file.read() + + with open( + os.path.join(os.path.dirname(__file__), "data", "sp.key"), + encoding="UTF-8", + ) as file: + sp_pem_private = file.read() + + self.saml_provider = self.env["auth.saml.provider"].create( + { + "name": "SAML Provider Demo", + "idp_metadata": FakeIDP().get_metadata(), + "sp_pem_public": base64.b64encode(sp_pem_public.encode()), + "sp_pem_private": base64.b64encode(sp_pem_private.encode()), + "body": "Login with Authentic", + "active": True, + "sig_alg": "SIG_RSA_SHA1", + "matching_attribute": "mail", + } + ) + self.url_saml_request = ( + "/auth_saml/get_auth_request?pid=%d" % self.saml_provider.id + ) + + self.idp = FakeIDP([self.saml_provider._metadata_string()]) + + # Create a user with only password, and another with both password and saml id + self.user, self.user2 = ( + self.env["res.users"] + .with_context(no_reset_password=True, tracking_disable=True) + .create( + [ + { + "name": "User", + "email": "test@example.com", + "login": "test@example.com", + "password": "Lu,ums-7vRU>0i]=YDLa", + }, + { + "name": "User with SAML", + "email": "user@example.com", + "login": "user@example.com", + "password": "NesTNSte9340D720te>/-A", + "saml_ids": [ + ( + 0, + 0, + { + "saml_provider_id": self.saml_provider.id, + "saml_uid": "user@example.com", + }, + ) + ], + }, + ] + ) + ) + + def test_ensure_provider_appears_on_login(self): + # SAML provider should be listed in the login page + response = self.url_open("/web/login") + self.assertIn("Login with Authentic", response.text) + self.assertIn(self.url_saml_request, response.text) + + def test_ensure_provider_appears_on_login_with_redirect_param(self): + """Test that SAML provider is listed in the login page keeping the redirect""" + response = self.url_open( + "/web/login?redirect=%2Fweb%23action%3D37%26model%3Dir.module.module%26view" + "_type%3Dkanban%26menu_id%3D5" + ) + self.assertIn("Login with Authentic", response.text) + self.assertIn( + "/auth_saml/get_auth_request?pid={}&redirect=%2Fweb%23action%3D37%26mod" + "el%3Dir.module.module%26view_type%3Dkanban%26menu_id%3D5".format( + self.saml_provider.id + ), + response.text, + ) + + def test__onchange_name(self): + temp = self.saml_provider.body + self.saml_provider.body = "" + r = self.saml_provider._onchange_name() + self.assertEqual(r, None) + self.assertEqual(self.saml_provider.body, self.saml_provider.name) + self.saml_provider.body = temp + + def test__compute_sp_metadata_url__new_provider(self): + # Create a new unsaved record + new_provider = self.env["auth.saml.provider"].new( + {"name": "New SAML Provider", "sp_baseurl": "http://example.com"} + ) + # Compute the metadata URL + new_provider._compute_sp_metadata_url() + # Assert that sp_metadata_url is False for the new record + self.assertFalse(new_provider.sp_metadata_url) + new_provider.unlink() + + def test__compute_sp_metadata_url__provider_has_sp_baseurl(self): + # Create a new saved record with sp_baseurl set + temp = self.saml_provider.sp_baseurl + self.saml_provider.sp_baseurl = "http://example.com" + self.saml_provider._compute_sp_metadata_url() + expected_qs = urllib.parse.urlencode( + {"p": self.saml_provider.id, "d": self.env.cr.dbname} + ) + expected_url = urllib.parse.urljoin( + "http://example.com", ("/auth_saml/metadata?%s" % expected_qs) + ) + # Assert that sp_metadata_url is set correctly + self.assertEqual(self.saml_provider.sp_metadata_url, expected_url) + self.saml_provider.sp_baseurl = temp + + def test__hook_validate_auth_response(self): + # Create a fake response with attributes + fake_response = DummyResponse(200, "fake_data") + fake_response.set_identity( + {"email": "new_user@example.com", "first_name": "New", "last_name": "User"} + ) + + # Add attribute mappings to the provider + self.saml_provider.attribute_mapping_ids = [ + (0, 0, {"attribute_name": "email", "field_name": "login"}), + (0, 0, {"attribute_name": "first_name", "field_name": "name"}), + ( + 0, + 0, + {"attribute_name": "nick_name", "field_name": "name"}, + ), # This attribute is not in attrs + ] + + # Call the method + result = self.saml_provider._hook_validate_auth_response( + fake_response, "test@example.com" + ) + + # Check the result + self.assertIn("mapped_attrs", result) + self.assertEqual(result["mapped_attrs"]["login"], "new_user@example.com") + self.assertEqual(result["mapped_attrs"]["name"], "New") + self.assertNotIn("middle_name", result["mapped_attrs"]) + + def test_get_config_for_provider(self): + temp = self.saml_provider.sp_baseurl + self.saml_provider.sp_baseurl = "http://example.com" + self.saml_provider._get_config_for_provider(None) + self.saml_provider.sp_baseurl = temp + + def test_ensure_metadata_present(self): + response = self.url_open( + "/auth_saml/metadata?p=%d&d=%s" + % (self.saml_provider.id, self.env.cr.dbname) + ) + + self.assertTrue(response.ok) + self.assertTrue("xml" in response.headers.get("Content-Type")) + + def test_ensure_get_auth_request_redirects(self): + response = self.url_open( + "/auth_saml/get_auth_request?pid=%d" % self.saml_provider.id, + allow_redirects=False, + ) + self.assertTrue(response.ok) + self.assertEqual(response.status_code, 303) + self.assertIn( + "http://localhost:8000/sso/redirect?SAMLRequest=", + response.headers.get("Location"), + ) + + def test_login_no_saml(self): + """ + Login with a user account, but without any SAML provider setup + against the user + """ + # Standard login using password + self.authenticate(user="test@example.com", password="Lu,ums-7vRU>0i]=YDLa") + self.assertEqual(self.session.uid, self.user.id) + + self.logout() + + # Try to log in with a non-existing SAML token + with self.assertRaises(AccessDenied): + self.authenticate(user="test@example.com", password="test_saml_token") + + redirect_url = self.saml_provider._get_auth_request() + self.assertIn("http://localhost:8000/sso/redirect?SAMLRequest=", redirect_url) + + response = self.idp.fake_login(redirect_url) + self.assertEqual(200, response.status_code) + unpacked_response = response._unpack() + + with self.assertRaises(AccessDenied): + self.env["res.users"].sudo().auth_saml( + self.saml_provider.id, unpacked_response.get("SAMLResponse"), None + ) + + def add_provider_to_user(self): + """Add a provider to self.user""" + self.user.write( + { + "saml_ids": [ + ( + 0, + 0, + { + "saml_provider_id": self.saml_provider.id, + "saml_uid": "test@example.com", + }, + ) + ] + } + ) + + def test_login_with_saml(self): + self.add_provider_to_user() + + redirect_url = self.saml_provider._get_auth_request() + self.assertIn("http://localhost:8000/sso/redirect?SAMLRequest=", redirect_url) + + response = self.idp.fake_login(redirect_url) + self.assertEqual(200, response.status_code) + unpacked_response = response._unpack() + + (database, login, token) = ( + self.env["res.users"] + .sudo() + .auth_saml( + self.saml_provider.id, unpacked_response.get("SAMLResponse"), None + ) + ) + + self.assertEqual(database, self.env.cr.dbname) + self.assertEqual(login, self.user.login) + + # We should not be able to log in with the wrong token + with self.assertRaises(AccessDenied): + self.authenticate(user="test@example.com", password=f"{token}-WRONG") + + # User should now be able to log in with the token + self.authenticate(user="test@example.com", password=token) + + def test_disallow_user_password_when_changing_ir_config_parameter(self): + """Test that disabling users from having both a password and SAML ids remove + users password.""" + # change the option + self.browse_ref( + "auth_saml.allow_saml_uid_and_internal_password" + ).value = "False" + # The password should be blank and the user should not be able to connect + with self.assertRaises(AccessDenied): + self.authenticate( + user="user@example.com", password="NesTNSte9340D720te>/-A" + ) + + def test_disallow_user_password_new_user(self): + """Test that a new user can not be set up with both password and SAML ids when + the disallow option is set.""" + # change the option + self.browse_ref( + "auth_saml.allow_saml_uid_and_internal_password" + ).value = "False" + with self.assertRaises(UserError): + self.env["res.users"].with_context(no_reset_password=True).create( + { + "name": "New user with SAML", + "email": "user2@example.com", + "login": "user2@example.com", + "password": "NesTNSte9340D720te>/-A", + "saml_ids": [ + ( + 0, + 0, + { + "saml_provider_id": self.saml_provider.id, + "saml_uid": "user2", + }, + ) + ], + } + ) + + def test_disallow_user_password_no_password_set(self): + """Test that a new user with SAML ids can not have its password set up when the + disallow option is set.""" + # change the option + self.browse_ref( + "auth_saml.allow_saml_uid_and_internal_password" + ).value = "False" + # Create a new user with only SAML ids + user = ( + self.env["res.users"] + .with_context(no_reset_password=True, tracking_disable=True) + .create( + { + "name": "New user with SAML", + "email": "user2@example.com", + "login": "user2@example.com", + "saml_ids": [ + ( + 0, + 0, + { + "saml_provider_id": self.saml_provider.id, + "saml_uid": "unused", + }, + ) + ], + } + ) + ) + # Assert that the user password can not be set + with self.assertRaises(ValidationError): + user.password = "new password" + + def test_disallow_user_password(self): + """Test that existing user password is deleted when adding an SAML provider when + the disallow option is set.""" + # change the option + self.browse_ref( + "auth_saml.allow_saml_uid_and_internal_password" + ).value = "False" + # Test that existing user password is deleted when adding an SAML provider + self.authenticate(user="test@example.com", password="Lu,ums-7vRU>0i]=YDLa") + self.add_provider_to_user() + with self.assertRaises(AccessDenied): + self.authenticate(user="test@example.com", password="Lu,ums-7vRU>0i]=YDLa") + + def test_disallow_user_admin_can_have_password(self): + """Test that admin can have its password set + even if the disallow option is set.""" + # change the option + self.browse_ref( + "auth_saml.allow_saml_uid_and_internal_password" + ).value = "False" + # Test base.user_admin exception + self.env.ref("base.user_admin").password = "nNRST4j*->sEatNGg._!" + + def test_db_filtering(self): + # change filter to only allow our db. + with patch("odoo.http.db_filter", new=lambda *args, **kwargs: []): + self.add_provider_to_user() + + redirect_url = self.saml_provider._get_auth_request() + response = self.idp.fake_login(redirect_url) + unpacked_response = response._unpack() + + for key in unpacked_response: + unpacked_response[key] = html.unescape(unpacked_response[key]) + response = self.url_open("/auth_saml/signin", data=unpacked_response) + self.assertFalse(response.ok) + self.assertIn(response.status_code, [400, 404]) + + def test_redirect_after_login(self): + """Test that providing a redirect will be kept after SAML login.""" + self.add_provider_to_user() + + redirect_url = self.saml_provider._get_auth_request( + { + "r": "%2Fweb%23action%3D37%26model%3Dir.module.module%26view_type%3Dkan" + "ban%26menu_id%3D5" + } + ) + response = self.idp.fake_login(redirect_url) + unpacked_response = response._unpack() + + for key in unpacked_response: + unpacked_response[key] = html.unescape(unpacked_response[key]) + response = self.url_open( + "/auth_saml/signin", + data=unpacked_response, + allow_redirects=True, + timeout=300, + ) + self.assertTrue(response.ok) + self.assertEqual( + response.url, + self.base_url() + + "/web#action=37&model=ir.module.module&view_type=kanban&menu_id=5", + ) + + def test_disallow_user_password_when_changing_settings(self): + """Test that disabling the setting will remove passwords from related users""" + # We activate the settings to allow password login + self.env["res.config.settings"].create( + { + "allow_saml_uid_and_internal_password": True, + } + ).execute() + + # Test the user can login with the password + self.authenticate(user="user@example.com", password="NesTNSte9340D720te>/-A") + + self.env["res.config.settings"].create( + { + "allow_saml_uid_and_internal_password": False, + } + ).execute() + + with self.assertRaises(AccessDenied): + self.authenticate( + user="user@example.com", password="NesTNSte9340D720te>/-A" + ) diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml new file mode 100644 index 0000000000..9ee7dc0335 --- /dev/null +++ b/auth_saml/views/auth_saml.xml @@ -0,0 +1,203 @@ + + + + + + + + auth.saml.provider.search + auth.saml.provider + + + + + + + + + + + auth.saml.provider.list + auth.saml.provider + + + + + + + + + + + auth.saml.provider.form + auth.saml.provider + +
+ + +
+
+ + + + + + + + + + + +
+
+
+
+ + + Providers + auth.saml.provider + tree,form + + +
diff --git a/auth_saml/views/res_config_settings.xml b/auth_saml/views/res_config_settings.xml new file mode 100644 index 0000000000..7e27733c27 --- /dev/null +++ b/auth_saml/views/res_config_settings.xml @@ -0,0 +1,20 @@ + + + + + auth_saml_base_settings_form + res.config.settings + + + + + + + + + + diff --git a/auth_saml/views/res_users.xml b/auth_saml/views/res_users.xml new file mode 100644 index 0000000000..00ac6c057c --- /dev/null +++ b/auth_saml/views/res_users.xml @@ -0,0 +1,24 @@ + + + + + res.users.form + res.users + form + + + + + + + + + + + + + + + + + From a4daaa4604be7ab9b9697c0c215251d24d88dba2 Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Tue, 22 Oct 2024 14:53:00 +0000 Subject: [PATCH 2/9] Tree to list --- auth_saml/views/auth_saml.xml | 10 +++++----- auth_saml/views/res_users.xml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/auth_saml/views/auth_saml.xml b/auth_saml/views/auth_saml.xml index 9ee7dc0335..e9ed50207a 100644 --- a/auth_saml/views/auth_saml.xml +++ b/auth_saml/views/auth_saml.xml @@ -48,12 +48,12 @@ auth.saml.provider.list auth.saml.provider - + - + @@ -168,10 +168,10 @@ name="attribute_mapping_ids" context="{'default_provider_id': id}" > - + - +

Mapped attributes are copied from the SAML response at every logon, if available. If multiple values are returned (i.e. a list) then the first value is used. @@ -191,7 +191,7 @@ Providers auth.saml.provider - tree,form + list,form - + - + From 56e47e2444374560f17f0cdd8a80744d47c4e370 Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Sat, 26 Oct 2024 07:04:05 +0000 Subject: [PATCH 3/9] improve --- auth_saml/controllers/main.py | 9 ++++++++- auth_saml/models/res_users.py | 10 ++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index fb635d3a72..e6752db8d5 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -241,7 +241,14 @@ def signin(self, **kw): url = "/#action=%s" % action elif menu: url = "/#menu_id=%s" % menu - pre_uid = request.session.authenticate(*credentials) + + credentials_dict = = { + 'login': credentials[1], + 'token': credentials[2], + 'type' : 'saml_token', + } + + pre_uid = request.session.authenticate(dbname, credentials_dict) resp = request.redirect(_get_login_redirect_url(pre_uid, url), 303) resp.autocorrect_location_header = False return resp diff --git a/auth_saml/models/res_users.py b/auth_saml/models/res_users.py index 412b5c6994..b05a571c08 100644 --- a/auth_saml/models/res_users.py +++ b/auth_saml/models/res_users.py @@ -76,7 +76,7 @@ def auth_saml(self, provider: int, saml_response: str, base_url: str = None): # return user credentials return self.env.cr.dbname, login, saml_response - def _check_credentials(self, password, env): + def _check_credentials(self, credential, env): """Override to handle SAML auths. The token can be a password if the user has used the normal form... @@ -85,9 +85,11 @@ def _check_credentials(self, password, env): """ try: # Attempt a regular login (via other auth addons) first. - return super()._check_credentials(password, env) + return super()._check_credentials(credential, env) except (AccessDenied, passlib.exc.PasswordSizeError): + if not (credential['type'] == 'saml_token' and credential['token']): + raise passwd_allowed = ( env["interactive"] or not self.env.user._rpc_api_keys_only() ) @@ -100,14 +102,14 @@ def _check_credentials(self, password, env): .search( [ ("user_id", "=", self.env.user.id), - ("saml_access_token", "=", password), + ("saml_access_token", "=", credential["token"]), ] ) ) if token: return raise AccessDenied() from None - + @api.model def _saml_allowed_user_ids(self) -> Set[int]: # noqa """Users that can have a password even if the option to disallow it is set. From f8f0bd5b48ce9e46242865f8e65cbf91abc809fd Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Sat, 26 Oct 2024 07:05:40 +0000 Subject: [PATCH 4/9] improve --- auth_saml/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index e6752db8d5..3bc0336fd6 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -242,7 +242,7 @@ def signin(self, **kw): elif menu: url = "/#menu_id=%s" % menu - credentials_dict = = { + credentials_dict = { 'login': credentials[1], 'token': credentials[2], 'type' : 'saml_token', From 7343fc383cd1a1250c3e42ebb71f8f18d9e7000c Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Sat, 26 Oct 2024 07:42:06 +0000 Subject: [PATCH 5/9] Fix return of _check_credentials --- auth_saml/controllers/main.py | 1 - auth_saml/models/res_users.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 3bc0336fd6..4ab8c032b9 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -247,7 +247,6 @@ def signin(self, **kw): 'token': credentials[2], 'type' : 'saml_token', } - pre_uid = request.session.authenticate(dbname, credentials_dict) resp = request.redirect(_get_login_redirect_url(pre_uid, url), 303) resp.autocorrect_location_header = False diff --git a/auth_saml/models/res_users.py b/auth_saml/models/res_users.py index b05a571c08..36fa1747dd 100644 --- a/auth_saml/models/res_users.py +++ b/auth_saml/models/res_users.py @@ -107,7 +107,11 @@ def _check_credentials(self, credential, env): ) ) if token: - return + return { + 'uid': self.env.user.id, + 'auth_method': 'saml', + 'mfa': 'default', + } raise AccessDenied() from None @api.model From 9860db15905cb5d39d20a37258c9bfc825e43551 Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Sat, 26 Oct 2024 12:58:22 +0400 Subject: [PATCH 6/9] Update README.rst --- auth_saml/README.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/auth_saml/README.rst b/auth_saml/README.rst index a53a8f65af..1954b8b96b 100644 --- a/auth_saml/README.rst +++ b/auth_saml/README.rst @@ -96,10 +96,10 @@ Known issues / Roadmap Changelog ========= -16.0.1.0.0 +18.0.1.0.0 ---------- -Initial migration for 16.0. +Initial migration for 18.0. Bug Tracker =========== @@ -145,6 +145,10 @@ Contributors - Lorenzo Battistini +- `Letzdoo `__: + + - Jérôme Sonnet + Maintainers ----------- @@ -166,6 +170,6 @@ Current `maintainer `__: |maintainer-vincent-hatakeyama| -This module is part of the `OCA/server-auth `_ project on GitHub. +This module is part of the `OCA/server-auth `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. From afaf4e9df7ce439add36abfc97a83cb0b2fa501b Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Sat, 26 Oct 2024 12:58:40 +0400 Subject: [PATCH 7/9] Update __manifest__.py --- auth_saml/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_saml/__manifest__.py b/auth_saml/__manifest__.py index 981ee9f011..bf6d3cc792 100644 --- a/auth_saml/__manifest__.py +++ b/auth_saml/__manifest__.py @@ -4,7 +4,7 @@ { "name": "SAML2 Authentication", - "version": "18.0.0.0.0", + "version": "18.0.1.0.0", "category": "Tools", "author": "XCG Consulting, Odoo Community Association (OCA)", "maintainers": ["vincent-hatakeyama"], From 6534866fa16183f8f78d753842b8150c96ab7f86 Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Mon, 28 Oct 2024 12:44:31 +0000 Subject: [PATCH 8/9] Fix test_disallow_user_password --- auth_saml/README.rst | 64 ++++++++++++++++------------------- auth_saml/models/res_users.py | 18 ++++++---- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/auth_saml/README.rst b/auth_saml/README.rst index 1954b8b96b..d2efa97090 100644 --- a/auth_saml/README.rst +++ b/auth_saml/README.rst @@ -17,13 +17,13 @@ SAML2 Authentication :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github - :target: https://github.com/OCA/server-auth/tree/17.0/auth_saml + :target: https://github.com/OCA/server-auth/tree/18.0/auth_saml :alt: OCA/server-auth .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-auth-17-0/server-auth-17-0-auth_saml + :target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-auth_saml :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=17.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -36,14 +36,14 @@ 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. +- 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. **Table of contents** @@ -91,15 +91,15 @@ login screen. Known issues / Roadmap ====================== -- clean up ``auth_saml.request`` +- clean up ``auth_saml.request`` Changelog ========= -18.0.1.0.0 +16.0.1.0.0 ---------- -Initial migration for 18.0. +Initial migration for 16.0. Bug Tracker =========== @@ -107,7 +107,7 @@ 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 to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -122,32 +122,28 @@ Authors Contributors ------------ -- `XCG Consulting `__: +- `XCG Consulting `__: - - Florent Aide - - Vincent Hatakeyama - - Alexandre Brun - - Houzéfa Abbasbhay - - Szeka Wong + - Florent Aide + - Vincent Hatakeyama + - Alexandre Brun + - Houzéfa Abbasbhay + - Szeka Wong -- Jeremy Co Kim Len -- Jeffery Chen Fan -- Bhavesh Odedra -- `Tecnativa `__: +- Jeremy Co Kim Len +- Jeffery Chen Fan +- Bhavesh Odedra +- `Tecnativa `__: - - Jairo Llopis + - Jairo Llopis -- `GlodoUK `__: +- `GlodoUK `__: - - Karl Southern + - Karl Southern -- `TAKOBI `__: +- `TAKOBI `__: - - Lorenzo Battistini - -- `Letzdoo `__: - - - Jérôme Sonnet + - Lorenzo Battistini Maintainers ----------- diff --git a/auth_saml/models/res_users.py b/auth_saml/models/res_users.py index 36fa1747dd..8e5f4877f6 100644 --- a/auth_saml/models/res_users.py +++ b/auth_saml/models/res_users.py @@ -84,11 +84,15 @@ def _check_credentials(self, credential, env): and the interesting code is inside the "except" clause. """ try: - # Attempt a regular login (via other auth addons) first. - return super()._check_credentials(credential, env) + if self.allow_saml_and_password(): + # If both SAML and password are allowed we can try first the normal auth + return super()._check_credentials(credential, env) + else: + # If only SAML we go to the except clause + raise AccessDenied() from None except (AccessDenied, passlib.exc.PasswordSizeError): - if not (credential['type'] == 'saml_token' and credential['token']): + if not (credential["type"] == "saml_token" and credential["token"]): raise passwd_allowed = ( env["interactive"] or not self.env.user._rpc_api_keys_only() @@ -108,12 +112,12 @@ def _check_credentials(self, credential, env): ) if token: return { - 'uid': self.env.user.id, - 'auth_method': 'saml', - 'mfa': 'default', + "uid": self.env.user.id, + "auth_method": "saml", + "mfa": "default", } raise AccessDenied() from None - + @api.model def _saml_allowed_user_ids(self) -> Set[int]: # noqa """Users that can have a password even if the option to disallow it is set. From 3a89c8b2c427ffc3bfbeb5b40336b36a1384077e Mon Sep 17 00:00:00 2001 From: "Jerome Sonnet (letzdoo)" Date: Mon, 28 Oct 2024 12:56:57 +0000 Subject: [PATCH 9/9] Fix ruff errros --- auth_saml/controllers/main.py | 12 ++++++------ auth_saml/models/auth_saml_provider.py | 2 +- auth_saml/tests/fake_idp.py | 12 +++++------- auth_saml/tests/test_pysaml.py | 8 +++----- requirements.txt | 1 + 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/auth_saml/controllers/main.py b/auth_saml/controllers/main.py index 4ab8c032b9..343104b248 100644 --- a/auth_saml/controllers/main.py +++ b/auth_saml/controllers/main.py @@ -100,7 +100,7 @@ def _auth_saml_request_link(self, provider: models.Model): redirect = request.params.get("redirect") if redirect: params["redirect"] = redirect - return "/auth_saml/get_auth_request?%s" % werkzeug.urls.url_encode(params) + return f"/auth_saml/get_auth_request?{werkzeug.urls.url_encode(params)}" @http.route() def web_client(self, s_action=None, **kw): @@ -238,14 +238,14 @@ def signin(self, **kw): if redirect: url = redirect elif action: - url = "/#action=%s" % action + url = f"/#action={action}" elif menu: - url = "/#menu_id=%s" % menu + url = f"/#menu_id={menu}" credentials_dict = { - 'login': credentials[1], - 'token': credentials[2], - 'type' : 'saml_token', + "login": credentials[1], + "token": credentials[2], + "type": "saml_token", } pre_uid = request.session.authenticate(dbname, credentials_dict) resp = request.redirect(_get_login_redirect_url(pre_uid, url), 303) diff --git a/auth_saml/models/auth_saml_provider.py b/auth_saml/models/auth_saml_provider.py index 4b323b7c26..05f26ccfd4 100644 --- a/auth_saml/models/auth_saml_provider.py +++ b/auth_saml/models/auth_saml_provider.py @@ -164,7 +164,7 @@ def _compute_sp_metadata_url(self): qs = urllib.parse.urlencode({"p": record.id, "d": self.env.cr.dbname}) record.sp_metadata_url = urllib.parse.urljoin( - base_url, ("/auth_saml/metadata?%s" % qs) + base_url, (f"/auth_saml/metadata?{qs}") ) def _get_cert_key_path(self, field="sp_pem_public"): diff --git a/auth_saml/tests/fake_idp.py b/auth_saml/tests/fake_idp.py index f2865b403d..8383dd0b28 100644 --- a/auth_saml/tests/fake_idp.py +++ b/auth_saml/tests/fake_idp.py @@ -25,20 +25,18 @@ "aa": { "endpoints": { "attribute_service": [ - ("%s/aap" % BASE, BINDING_HTTP_POST), + (f"{BASE}/aap", BINDING_HTTP_POST), ] }, }, "aq": { - "endpoints": { - "authn_query_service": [("%s/aqs" % BASE, BINDING_HTTP_POST)] - }, + "endpoints": {"authn_query_service": [(f"{BASE}/aqs", BINDING_HTTP_POST)]}, }, "idp": { "endpoints": { "single_sign_on_service": [ - ("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT), - ("%s/sso/post" % BASE, BINDING_HTTP_POST), + (f"{BASE}/sso/redirect", BINDING_HTTP_REDIRECT), + (f"{BASE}/sso/post", BINDING_HTTP_POST), ], }, "policy": { @@ -87,7 +85,7 @@ def _unpack(self, ver="SAMLResponse"): """ _str = self.text - sr_str = 'name="%s" value="' % ver + sr_str = f'name="{ver}" value="' rs_str = 'name="RelayState" value="' i = _str.find(sr_str) diff --git a/auth_saml/tests/test_pysaml.py b/auth_saml/tests/test_pysaml.py index 9eedaa5405..4af51d253f 100644 --- a/auth_saml/tests/test_pysaml.py +++ b/auth_saml/tests/test_pysaml.py @@ -92,10 +92,8 @@ def test_ensure_provider_appears_on_login_with_redirect_param(self): ) self.assertIn("Login with Authentic", response.text) self.assertIn( - "/auth_saml/get_auth_request?pid={}&redirect=%2Fweb%23action%3D37%26mod" - "el%3Dir.module.module%26view_type%3Dkanban%26menu_id%3D5".format( - self.saml_provider.id - ), + f"/auth_saml/get_auth_request?pid={self.saml_provider.id}&redirect=%2Fweb%23action%3D37%26mod" + "el%3Dir.module.module%26view_type%3Dkanban%26menu_id%3D5", response.text, ) @@ -127,7 +125,7 @@ def test__compute_sp_metadata_url__provider_has_sp_baseurl(self): {"p": self.saml_provider.id, "d": self.env.cr.dbname} ) expected_url = urllib.parse.urljoin( - "http://example.com", ("/auth_saml/metadata?%s" % expected_qs) + "http://example.com", (f"/auth_saml/metadata?{expected_qs}") ) # Assert that sp_metadata_url is set correctly self.assertEqual(self.saml_provider.sp_metadata_url, expected_url) diff --git a/requirements.txt b/requirements.txt index 13c8fbb434..8e7bf891df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ # generated from manifests external_dependencies +pysaml2 python-jose