mirror of
https://github.com/bringout/oca-server-auth.git
synced 2026-04-18 19:31:59 +02:00
Initial commit: OCA Server Auth packages (29 packages)
This commit is contained in:
commit
3ed80311c4
1325 changed files with 127292 additions and 0 deletions
|
|
@ -0,0 +1,184 @@
|
|||
# Copyright (C) 2020 GlodoUK <https://www.glodo.uk>
|
||||
# Copyright (C) 2010-2016 XCG Consulting <http://odoo.consulting>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
from typing import Set
|
||||
|
||||
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]:
|
||||
"""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})
|
||||
Loading…
Add table
Add a link
Reference in a new issue