2022-11-01 11:25:21 +00:00
|
|
|
import datetime
|
2024-10-25 07:51:01 +00:00
|
|
|
import secrets
|
2024-04-21 09:47:23 +00:00
|
|
|
from typing import Annotated
|
2024-04-22 18:04:24 +00:00
|
|
|
from typing import ClassVar
|
2022-11-01 11:25:21 +00:00
|
|
|
|
2024-04-06 17:22:47 +00:00
|
|
|
from flask import current_app
|
2024-12-17 13:45:10 +00:00
|
|
|
from pydantic import TypeAdapter
|
2024-04-06 17:22:47 +00:00
|
|
|
|
2024-03-30 17:48:03 +00:00
|
|
|
from canaille.backends.models import Model
|
2024-04-07 15:34:19 +00:00
|
|
|
from canaille.core.configuration import Permission
|
2024-11-18 13:16:38 +00:00
|
|
|
from canaille.core.mails import send_one_time_password_mail
|
2024-11-29 09:32:17 +00:00
|
|
|
from canaille.core.sms import send_one_time_password_sms
|
2020-08-14 11:18:08 +00:00
|
|
|
|
2024-11-14 08:49:19 +00:00
|
|
|
HOTP_LOOK_AHEAD_WINDOW = 10
|
2024-11-18 13:16:38 +00:00
|
|
|
OTP_DIGITS = 6
|
|
|
|
OTP_VALIDITY = 600
|
2024-11-29 09:32:17 +00:00
|
|
|
SEND_NEW_OTP_DELAY = 10
|
2024-11-14 08:49:19 +00:00
|
|
|
|
2024-12-05 10:42:51 +00:00
|
|
|
PASSWORD_MIN_DELAY = 2
|
|
|
|
PASSWORD_MAX_DELAY = 600
|
|
|
|
PASSWORD_FAILURE_COUNT_INTERVAL = 600
|
|
|
|
|
2024-03-30 17:48:03 +00:00
|
|
|
|
|
|
|
class User(Model):
|
2024-04-23 20:12:04 +00:00
|
|
|
"""User model, based on the `SCIM User schema
|
2023-12-14 22:08:17 +00:00
|
|
|
<https://datatracker.ietf.org/doc/html/rfc7643#section-4.1>`_,
|
|
|
|
`Entreprise User Schema Extension
|
|
|
|
<https://datatracker.ietf.org/doc/html/rfc7643#section-4.3>`_
|
|
|
|
and `SCIM Password Management Extension
|
|
|
|
<https://datatracker.ietf.org/doc/html/draft-hunt-scim-password-mgmt-00.html>`_
|
|
|
|
draft.
|
|
|
|
Attribute description is based on SCIM and put there for
|
|
|
|
information purpose. The description may not fit the current
|
|
|
|
implementation in Canaille.
|
2023-08-17 13:55:41 +00:00
|
|
|
"""
|
|
|
|
|
2024-04-22 18:04:24 +00:00
|
|
|
identifier_attribute: ClassVar[str] = "user_name"
|
|
|
|
|
2024-04-01 16:47:42 +00:00
|
|
|
user_name: str
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A service provider's unique identifier for the user, typically used by
|
|
|
|
the user to directly authenticate to the service provider.
|
|
|
|
|
2023-12-14 22:08:17 +00:00
|
|
|
Often displayed to the user as their unique identifier within the
|
|
|
|
system (as opposed to "id" or "externalId", which are generally
|
2023-12-28 17:31:57 +00:00
|
|
|
opaque and not user-friendly identifiers). Each User MUST include a
|
|
|
|
non-empty userName value. This identifier MUST be unique across the
|
|
|
|
service provider's entire set of Users. This attribute is REQUIRED
|
|
|
|
and is case insensitive.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-12-05 10:42:51 +00:00
|
|
|
password_failure_timestamps: list[datetime.datetime] = []
|
|
|
|
"""This attribute stores the timestamps of the user's failed
|
|
|
|
authentications.
|
|
|
|
|
|
|
|
It's currently used by the intruder lockout delay system.
|
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
password: str | None = None
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
This attribute is intended to be used as a means to set, replace,
|
|
|
|
or compare (i.e., filter for equality) a password. The cleartext
|
|
|
|
value or the hashed value of a password SHALL NOT be returnable by
|
|
|
|
a service provider. If a service provider holds the value
|
|
|
|
locally, the value SHOULD be hashed. When a password is set or
|
|
|
|
changed by the client, the cleartext password SHOULD be processed
|
|
|
|
by the service provider as follows:
|
|
|
|
|
|
|
|
* Prepare the cleartext value for international language
|
|
|
|
comparison. See Section 7.8 of [RFC7644].
|
|
|
|
|
|
|
|
* Validate the value against server password policy. Note: The
|
|
|
|
definition and enforcement of password policy are beyond the
|
|
|
|
scope of this document.
|
|
|
|
|
|
|
|
* Ensure that the value is encrypted (e.g., hashed). See
|
|
|
|
Section 9.2 for acceptable hashing and encryption handling when
|
|
|
|
storing or persisting for provisioning workflow reasons.
|
|
|
|
|
|
|
|
A service provider that immediately passes the cleartext value on
|
|
|
|
to another system or programming interface MUST pass the value
|
|
|
|
directly over a secured connection (e.g., Transport Layer Security
|
|
|
|
(TLS)). If the value needs to be temporarily persisted for a
|
|
|
|
period of time (e.g., because of a workflow) before provisioning,
|
|
|
|
then the value MUST be protected by some method, such as
|
|
|
|
encryption.
|
|
|
|
|
|
|
|
Testing for an equality match MAY be supported if there is an
|
|
|
|
existing stored hashed value. When testing for equality, the
|
|
|
|
service provider:
|
|
|
|
|
|
|
|
* Prepares the filter value for international language
|
|
|
|
comparison. See Section 7.8 of [RFC7644].
|
|
|
|
|
|
|
|
* Generates the salted hash of the filter value and tests for a
|
|
|
|
match with the locally held value.
|
|
|
|
|
|
|
|
The mutability of the password attribute is "writeOnly",
|
|
|
|
indicating that the value MUST NOT be returned by a service
|
|
|
|
provider in any form (the attribute characteristic "returned" is
|
|
|
|
"never").
|
|
|
|
"""
|
|
|
|
|
2024-12-17 13:45:10 +00:00
|
|
|
password_last_update: datetime.datetime | None = None
|
2024-12-22 15:36:37 +00:00
|
|
|
"""Specifies the last time the user password was changed.
|
2024-12-17 13:45:10 +00:00
|
|
|
By default, the date of creation of the password is retained.
|
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
preferred_language: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""Indicates the user's preferred written or spoken languages and is
|
|
|
|
generally used for selecting a localized user interface.
|
|
|
|
|
|
|
|
The value indicates the set of natural languages that are preferred.
|
2023-12-14 22:08:17 +00:00
|
|
|
The format of the value is the same as the HTTP Accept-Language
|
2023-12-28 17:31:57 +00:00
|
|
|
header field (not including "Accept-Language:") and is specified in
|
|
|
|
Section 5.3.5 of [RFC7231]. The intent of this value is to enable
|
|
|
|
cloud applications to perform matching of language tags [RFC4647] to
|
|
|
|
the user's language preferences, regardless of what may be indicated
|
|
|
|
by a user agent (which might be shared), or in an interaction that
|
|
|
|
does not involve a user (such as in a delegated OAuth 2.0 [RFC6749]
|
|
|
|
style interaction) where normal HTTP Accept-Language header
|
|
|
|
negotiation cannot take place.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
family_name: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The family name of the User, or last name in most Western languages
|
|
|
|
(e.g., "Jensen" given the full name "Ms. Barbara Jane Jensen, III")."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
given_name: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The given name of the User, or first name in most Western languages
|
|
|
|
(e.g., "Barbara" given the full name "Ms. Barbara Jane Jensen, III")."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
formatted_name: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The full name, including all middle names, titles, and suffixes as
|
|
|
|
appropriate, formatted for display (e.g., "Ms. Barbara Jane Jensen,
|
|
|
|
III")."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
display_name: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The name of the user, suitable for display to end-users.
|
|
|
|
|
|
|
|
Each user returned MAY include a non-empty displayName value. The
|
|
|
|
name SHOULD be the full name of the User being described, if known
|
2023-12-14 22:08:17 +00:00
|
|
|
(e.g., "Babs Jensen" or "Ms. Barbara J Jensen, III") but MAY be a
|
|
|
|
username or handle, if that is all that is available (e.g.,
|
2023-12-28 17:31:57 +00:00
|
|
|
"bjensen"). The value provided SHOULD be the primary textual label
|
|
|
|
by which this User is normally displayed by the service provider
|
|
|
|
when presenting it to end-users.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
emails: list[str] = []
|
2023-12-28 17:31:57 +00:00
|
|
|
"""Email addresses for the User.
|
|
|
|
|
|
|
|
The value SHOULD be specified according to [RFC5321]. Service
|
|
|
|
providers SHOULD canonicalize the value according to [RFC5321],
|
|
|
|
e.g., "bjensen@example.com" instead of "bjensen@EXAMPLE.COM". The
|
|
|
|
"display" sub-attribute MAY be used to return the canonicalized
|
|
|
|
representation of the email value. The "type" sub-attribute is used
|
|
|
|
to provide a classification meaningful to the (human) user. The
|
|
|
|
user interface should encourage the use of basic values of "work",
|
|
|
|
"home", and "other" and MAY allow additional type values to be used
|
|
|
|
at the discretion of SCIM clients.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
phone_numbers: list[str] = []
|
2023-12-28 17:31:57 +00:00
|
|
|
"""Phone numbers for the user.
|
|
|
|
|
|
|
|
The value SHOULD be specified according to the format defined in
|
|
|
|
[RFC3966], e.g., 'tel:+1-201-555-0123'. Service providers SHOULD
|
|
|
|
canonicalize the value according to [RFC3966] format, when
|
|
|
|
appropriate. The "display" sub-attribute MAY be used to return the
|
|
|
|
canonicalized representation of the phone number value. The sub-
|
|
|
|
attribute "type" often has typical values of "work", "home",
|
|
|
|
"mobile", "fax", "pager", and "other" and MAY allow more types to be
|
|
|
|
defined by the SCIM clients.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
formatted_address: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The full mailing address, formatted for display or use with a mailing
|
|
|
|
label.
|
|
|
|
|
|
|
|
This attribute MAY contain newlines.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
street: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The full street address component, which may include house number,
|
|
|
|
street name, P.O.
|
|
|
|
|
|
|
|
box, and multi-line extended street address information. This
|
|
|
|
attribute MAY contain newlines.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
postal_code: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The zip code or postal code component."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
locality: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The city or locality component."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
region: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The state or region component."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
photo: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A URI that is a uniform resource locator (as defined in Section 1.1.3 of
|
|
|
|
[RFC3986]) that points to a resource location representing the user's
|
|
|
|
image.
|
|
|
|
|
|
|
|
The resource MUST be a file (e.g.,
|
2023-12-14 22:08:17 +00:00
|
|
|
a GIF, JPEG, or PNG image file) rather than a web page containing
|
|
|
|
an image. Service providers MAY return the same image in
|
|
|
|
different sizes, although it is recognized that no standard for
|
|
|
|
describing images of various sizes currently exists. Note that
|
|
|
|
this attribute SHOULD NOT be used to send down arbitrary photos
|
|
|
|
taken by this user; instead, profile photos of the user that are
|
|
|
|
suitable for display when describing the user should be sent.
|
|
|
|
Instead of the standard canonical values for type, this attribute
|
|
|
|
defines the following canonical values to represent popular photo
|
|
|
|
sizes: "photo" and "thumbnail".
|
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
profile_url: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A URI that is a uniform resource locator (as defined in Section 1.1.3 of
|
|
|
|
[RFC3986]) and that points to a location representing the user's online
|
|
|
|
profile (e.g., a web page).
|
|
|
|
|
|
|
|
URIs are canonicalized per Section 6.2 of [RFC3986].
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
title: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""The user's title, such as "Vice President"."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
organization: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""Identifies the name of an organization."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
employee_number: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A string identifier, typically numeric or alphanumeric, assigned to a
|
|
|
|
person, typically based on order of hire or association with an
|
|
|
|
organization."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
department: str | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""Identifies the name of a department."""
|
2023-12-14 22:08:17 +00:00
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
groups: list[Annotated["Group", {"backref": "members"}]] = []
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A list of groups to which the user belongs, either through direct
|
|
|
|
membership, through nested groups, or dynamically calculated.
|
|
|
|
|
|
|
|
The values are meant to enable expression of common group-based or
|
|
|
|
role-based access control models, although no explicit authorization
|
|
|
|
model is defined. It is intended that the semantics of group
|
|
|
|
membership and any behavior or authorization granted as a result of
|
|
|
|
membership are defined by the service provider. The canonical types
|
|
|
|
"direct" and "indirect" are defined to describe how the group
|
|
|
|
membership was derived. Direct group membership indicates that the
|
|
|
|
user is directly associated with the group and SHOULD indicate that
|
|
|
|
clients may modify membership through the "Group" resource. Indirect
|
|
|
|
membership indicates that user membership is transitive or dynamic
|
|
|
|
and implies that clients cannot modify indirect group membership
|
|
|
|
through the "Group" resource but MAY modify direct group membership
|
|
|
|
through the "Group" resource, which may influence indirect
|
|
|
|
memberships. If the SCIM service provider exposes a "Group"
|
|
|
|
resource, the "value" sub-attribute MUST be the "id", and the "$ref"
|
|
|
|
sub-attribute must be the URI of the corresponding "Group" resources
|
|
|
|
to which the user belongs. Since this attribute has a mutability of
|
|
|
|
"readOnly", group membership changes MUST be applied via the "Group"
|
|
|
|
Resource (Section 4.2). This attribute has a mutability of
|
|
|
|
"readOnly".
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
lock_date: datetime.datetime | None = None
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A DateTime indicating when the resource was locked."""
|
2023-08-23 13:18:43 +00:00
|
|
|
|
2024-10-25 07:51:01 +00:00
|
|
|
last_otp_login: datetime.datetime | None = None
|
|
|
|
"""A DateTime indicating when the user last logged in with a one-time password.
|
2024-11-18 13:16:38 +00:00
|
|
|
This attribute is currently used to check whether the user has activated one-time password authentication or not."""
|
2024-10-25 07:51:01 +00:00
|
|
|
|
|
|
|
secret_token: str | None = None
|
|
|
|
"""Unique token generated for each user, used for
|
2024-12-12 11:03:32 +00:00
|
|
|
multi-factor authentication."""
|
2024-10-25 07:51:01 +00:00
|
|
|
|
2024-11-14 08:49:19 +00:00
|
|
|
hotp_counter: int | None = None
|
|
|
|
"""HMAC-based One Time Password counter, used for
|
2024-12-12 11:03:32 +00:00
|
|
|
multi-factor authentication."""
|
2024-11-14 08:49:19 +00:00
|
|
|
|
2024-11-18 13:16:38 +00:00
|
|
|
one_time_password: str | None = None
|
2024-12-12 11:03:32 +00:00
|
|
|
"""One time password used for email or sms multi-factor authentication."""
|
2024-11-18 13:16:38 +00:00
|
|
|
|
|
|
|
one_time_password_emission_date: datetime.datetime | None = None
|
2024-11-29 09:32:17 +00:00
|
|
|
"""A DateTime indicating when the user last emitted an email or sms one-time password."""
|
2024-11-18 13:16:38 +00:00
|
|
|
|
2024-04-07 13:21:32 +00:00
|
|
|
_readable_fields = None
|
|
|
|
_writable_fields = None
|
|
|
|
_permissions = None
|
2021-12-02 17:23:14 +00:00
|
|
|
|
2023-08-17 13:55:41 +00:00
|
|
|
def has_password(self) -> bool:
|
2024-09-11 07:33:42 +00:00
|
|
|
"""Check whether a password has been set for the user."""
|
2024-04-07 16:33:43 +00:00
|
|
|
return self.password is not None
|
2023-06-27 15:41:00 +00:00
|
|
|
|
2023-08-17 13:55:41 +00:00
|
|
|
def can_read(self, field: str):
|
2024-09-17 17:58:03 +00:00
|
|
|
return field in self.readable_fields | self.writable_fields
|
2021-12-08 17:06:50 +00:00
|
|
|
|
2023-06-22 13:24:13 +00:00
|
|
|
@property
|
|
|
|
def preferred_email(self):
|
|
|
|
return self.emails[0] if self.emails else None
|
|
|
|
|
2024-04-14 15:30:59 +00:00
|
|
|
def __getattribute__(self, name):
|
2024-04-07 13:21:32 +00:00
|
|
|
prefix = "can_"
|
2021-12-02 17:23:14 +00:00
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
try:
|
|
|
|
return super().__getattribute__(name)
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
if name.startswith(prefix) and name != "can_read":
|
|
|
|
return self.can(name[len(prefix) :])
|
|
|
|
raise
|
2021-12-02 17:23:14 +00:00
|
|
|
|
2024-04-07 15:34:19 +00:00
|
|
|
def can(self, *permissions: Permission):
|
2024-09-11 07:33:42 +00:00
|
|
|
"""Whether or not the user has the
|
2024-04-07 15:34:19 +00:00
|
|
|
:class:`~canaille.core.configuration.Permission` according to the
|
|
|
|
:class:`configuration <canaille.core.configuration.ACLSettings>`."""
|
2024-04-07 13:21:32 +00:00
|
|
|
if self._permissions is None:
|
2024-09-18 08:00:58 +00:00
|
|
|
self._permissions = set()
|
|
|
|
acls = current_app.config["CANAILLE"]["ACL"].values()
|
|
|
|
for details in acls:
|
|
|
|
if self.match_filter(details["FILTER"]):
|
|
|
|
self._permissions |= set(details["PERMISSIONS"])
|
2024-04-07 13:21:32 +00:00
|
|
|
|
|
|
|
return set(permissions).issubset(self._permissions)
|
|
|
|
|
2022-11-01 11:25:21 +00:00
|
|
|
@property
|
2023-08-17 13:55:41 +00:00
|
|
|
def locked(self) -> bool:
|
2024-09-11 07:33:42 +00:00
|
|
|
"""Whether the user account has been locked or has expired."""
|
2022-11-01 11:25:21 +00:00
|
|
|
return bool(self.lock_date) and self.lock_date < datetime.datetime.now(
|
|
|
|
datetime.timezone.utc
|
|
|
|
)
|
|
|
|
|
2024-04-07 13:21:32 +00:00
|
|
|
def reload(self):
|
|
|
|
self._readable = None
|
|
|
|
self._writable = None
|
|
|
|
self._permissions = None
|
2024-04-14 20:51:58 +00:00
|
|
|
yield
|
2024-04-07 13:21:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def readable_fields(self):
|
2024-04-07 15:34:19 +00:00
|
|
|
"""The fields the user can read according to the :class:`configuration
|
|
|
|
<canaille.core.configuration.ACLSettings>` configuration.
|
|
|
|
|
|
|
|
This does not include the :attr:`writable
|
|
|
|
<canaille.core.models.User.writable_fields>` fields.
|
|
|
|
"""
|
2024-04-07 15:43:20 +00:00
|
|
|
if self._readable_fields is None:
|
2024-09-18 08:00:58 +00:00
|
|
|
self._readable_fields = set()
|
|
|
|
acls = current_app.config["CANAILLE"]["ACL"].values()
|
|
|
|
for details in acls:
|
|
|
|
if self.match_filter(details["FILTER"]):
|
|
|
|
self._readable_fields |= set(details["READ"])
|
2024-04-07 13:21:32 +00:00
|
|
|
|
|
|
|
return self._readable_fields
|
|
|
|
|
|
|
|
@property
|
|
|
|
def writable_fields(self):
|
2024-04-07 15:34:19 +00:00
|
|
|
"""The fields the user can write according to the :class:`configuration
|
|
|
|
<canaille.core.configuration.ACLSettings>`."""
|
2024-04-07 13:21:32 +00:00
|
|
|
if self._writable_fields is None:
|
2024-09-18 08:00:58 +00:00
|
|
|
self._writable_fields = set()
|
|
|
|
acls = current_app.config["CANAILLE"]["ACL"].values()
|
|
|
|
for details in acls:
|
|
|
|
if self.match_filter(details["FILTER"]):
|
|
|
|
self._writable_fields |= set(details["WRITE"])
|
2024-04-07 13:21:32 +00:00
|
|
|
return self._writable_fields
|
|
|
|
|
2024-11-14 08:49:19 +00:00
|
|
|
def initialize_otp(self):
|
2024-10-25 07:51:01 +00:00
|
|
|
self.secret_token = secrets.token_hex(32)
|
2024-11-14 08:49:19 +00:00
|
|
|
self.last_otp_login = None
|
|
|
|
if current_app.features.otp_method == "HOTP":
|
|
|
|
self.hotp_counter = 1
|
|
|
|
|
|
|
|
def generate_otp(self, counter_delta=0):
|
2024-11-29 09:32:17 +00:00
|
|
|
import otpauth
|
|
|
|
|
2024-11-14 08:49:19 +00:00
|
|
|
method = current_app.features.otp_method
|
|
|
|
if method == "TOTP":
|
|
|
|
totp = otpauth.TOTP(bytes(self.secret_token, "utf-8"))
|
|
|
|
return totp.string_code(totp.generate())
|
|
|
|
elif method == "HOTP":
|
|
|
|
hotp = otpauth.HOTP(bytes(self.secret_token, "utf-8"))
|
|
|
|
return hotp.string_code(hotp.generate(self.hotp_counter + counter_delta))
|
2024-11-18 13:16:38 +00:00
|
|
|
else: # pragma: no cover
|
|
|
|
raise RuntimeError("Invalid one-time password method")
|
2024-11-14 08:49:19 +00:00
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
def generate_sms_or_mail_otp(self):
|
2024-11-18 13:16:38 +00:00
|
|
|
otp = string_code(secrets.randbelow(10**OTP_DIGITS), OTP_DIGITS)
|
|
|
|
self.one_time_password = otp
|
|
|
|
self.one_time_password_emission_date = datetime.datetime.now(
|
|
|
|
datetime.timezone.utc
|
|
|
|
)
|
|
|
|
return otp
|
2024-11-14 11:12:33 +00:00
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
def generate_and_send_otp_mail(self):
|
|
|
|
otp = self.generate_sms_or_mail_otp()
|
|
|
|
if send_one_time_password_mail(self.preferred_email, otp):
|
|
|
|
return otp
|
|
|
|
return False
|
|
|
|
|
|
|
|
def generate_and_send_otp_sms(self):
|
|
|
|
otp = self.generate_sms_or_mail_otp()
|
|
|
|
if send_one_time_password_sms(self.phone_numbers[0], otp):
|
|
|
|
return otp
|
|
|
|
return False
|
|
|
|
|
2024-11-14 08:49:19 +00:00
|
|
|
def get_otp_authentication_setup_uri(self):
|
2024-11-29 09:32:17 +00:00
|
|
|
import otpauth
|
|
|
|
|
2024-11-14 08:49:19 +00:00
|
|
|
method = current_app.features.otp_method
|
|
|
|
if method == "TOTP":
|
|
|
|
return otpauth.TOTP(bytes(self.secret_token, "utf-8")).to_uri(
|
|
|
|
label=self.user_name, issuer=current_app.config["CANAILLE"]["NAME"]
|
|
|
|
)
|
|
|
|
elif method == "HOTP":
|
|
|
|
return otpauth.HOTP(bytes(self.secret_token, "utf-8")).to_uri(
|
|
|
|
label=self.user_name,
|
|
|
|
issuer=current_app.config["CANAILLE"]["NAME"],
|
|
|
|
counter=self.hotp_counter,
|
|
|
|
)
|
2024-11-18 13:16:38 +00:00
|
|
|
else: # pragma: no cover
|
|
|
|
raise RuntimeError("Invalid one-time password method")
|
2024-11-14 11:12:33 +00:00
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
def is_otp_valid(self, user_otp, method):
|
2024-11-14 08:49:19 +00:00
|
|
|
if method == "TOTP":
|
2024-11-18 13:16:38 +00:00
|
|
|
return self.is_totp_valid(user_otp)
|
2024-11-14 08:49:19 +00:00
|
|
|
elif method == "HOTP":
|
2024-11-18 13:16:38 +00:00
|
|
|
return self.is_hotp_valid(user_otp)
|
2024-11-29 09:32:17 +00:00
|
|
|
elif method == "EMAIL_OTP" or method == "SMS_OTP":
|
|
|
|
return self.is_email_or_sms_otp_valid(user_otp)
|
2024-11-18 13:16:38 +00:00
|
|
|
else: # pragma: no cover
|
|
|
|
raise RuntimeError("Invalid one-time password method")
|
|
|
|
|
|
|
|
def is_totp_valid(self, user_otp):
|
2024-11-29 09:32:17 +00:00
|
|
|
import otpauth
|
|
|
|
|
2024-11-18 13:16:38 +00:00
|
|
|
return otpauth.TOTP(bytes(self.secret_token, "utf-8")).verify(user_otp)
|
|
|
|
|
|
|
|
def is_hotp_valid(self, user_otp):
|
2024-11-29 09:32:17 +00:00
|
|
|
import otpauth
|
|
|
|
|
2024-11-18 13:16:38 +00:00
|
|
|
counter = self.hotp_counter
|
|
|
|
is_valid = False
|
|
|
|
# if user token's counter is ahead of canaille's, try to catch up to it
|
|
|
|
while counter - self.hotp_counter <= HOTP_LOOK_AHEAD_WINDOW:
|
|
|
|
is_valid = otpauth.HOTP(bytes(self.secret_token, "utf-8")).verify(
|
|
|
|
user_otp, counter
|
|
|
|
)
|
|
|
|
counter += 1
|
|
|
|
if is_valid:
|
|
|
|
self.hotp_counter = counter
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
def is_email_or_sms_otp_valid(self, user_otp):
|
|
|
|
return user_otp == self.one_time_password and self.is_otp_still_valid()
|
2024-11-18 13:16:38 +00:00
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
def is_otp_still_valid(self):
|
2024-11-18 13:16:38 +00:00
|
|
|
return datetime.datetime.now(
|
|
|
|
datetime.timezone.utc
|
|
|
|
) - self.one_time_password_emission_date < datetime.timedelta(
|
|
|
|
seconds=OTP_VALIDITY
|
|
|
|
)
|
2024-11-14 11:12:33 +00:00
|
|
|
|
2024-11-29 09:32:17 +00:00
|
|
|
def can_send_new_otp(self):
|
|
|
|
return self.one_time_password_emission_date is None or (
|
|
|
|
datetime.datetime.now(datetime.timezone.utc)
|
|
|
|
- self.one_time_password_emission_date
|
|
|
|
>= datetime.timedelta(seconds=SEND_NEW_OTP_DELAY)
|
|
|
|
)
|
|
|
|
|
2024-12-05 10:42:51 +00:00
|
|
|
def get_intruder_lockout_delay(self):
|
|
|
|
if self.password_failure_timestamps:
|
|
|
|
# discard old attempts
|
|
|
|
self.password_failure_timestamps = [
|
|
|
|
attempt
|
|
|
|
for attempt in self.password_failure_timestamps
|
|
|
|
if attempt
|
|
|
|
> datetime.datetime.now(datetime.timezone.utc)
|
|
|
|
- datetime.timedelta(seconds=PASSWORD_FAILURE_COUNT_INTERVAL)
|
|
|
|
]
|
|
|
|
if not self.password_failure_timestamps:
|
|
|
|
return 0
|
|
|
|
failed_login_count = len(self.password_failure_timestamps)
|
|
|
|
# delay is multiplied by 2 each failed attempt, starting at min delay, limited to max delay
|
|
|
|
calculated_delay = min(
|
|
|
|
PASSWORD_MIN_DELAY * 2 ** (failed_login_count - 1), PASSWORD_MAX_DELAY
|
|
|
|
)
|
|
|
|
time_since_last_failed_bind = (
|
|
|
|
datetime.datetime.now(datetime.timezone.utc)
|
|
|
|
- self.password_failure_timestamps[-1]
|
|
|
|
).total_seconds()
|
|
|
|
return max(calculated_delay - time_since_last_failed_bind, 0)
|
|
|
|
|
2024-12-17 13:45:10 +00:00
|
|
|
def has_expired_password(self):
|
|
|
|
last_update = self.password_last_update or datetime.datetime.now(
|
|
|
|
datetime.timezone.utc
|
|
|
|
)
|
|
|
|
if current_app.config["CANAILLE"]["PASSWORD_LIFETIME"] is None:
|
|
|
|
password_expiration = None
|
|
|
|
else:
|
|
|
|
password_expiration = TypeAdapter(datetime.timedelta).validate_python(
|
|
|
|
current_app.config["CANAILLE"]["PASSWORD_LIFETIME"]
|
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
|
|
|
password_expiration is not None
|
|
|
|
and last_update + password_expiration
|
|
|
|
< datetime.datetime.now(datetime.timezone.utc)
|
|
|
|
)
|
|
|
|
|
2021-06-03 13:00:11 +00:00
|
|
|
|
2024-03-30 17:48:03 +00:00
|
|
|
class Group(Model):
|
2024-04-23 20:12:04 +00:00
|
|
|
"""User model, based on the `SCIM Group schema
|
2023-12-14 22:08:17 +00:00
|
|
|
<https://datatracker.ietf.org/doc/html/rfc7643#section-4.2>`_.
|
2023-08-17 13:55:41 +00:00
|
|
|
"""
|
2023-08-23 13:18:43 +00:00
|
|
|
|
2024-04-22 18:04:24 +00:00
|
|
|
identifier_attribute: ClassVar[str] = "display_name"
|
|
|
|
|
2023-08-23 13:18:43 +00:00
|
|
|
display_name: str
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A human-readable name for the Group.
|
|
|
|
|
|
|
|
REQUIRED.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
members: list[Annotated["User", {"backref": "groups"}]] = []
|
2023-12-28 17:31:57 +00:00
|
|
|
"""A list of members of the Group.
|
|
|
|
|
|
|
|
While values MAY be added or removed, sub-attributes of members are
|
|
|
|
"immutable". The "value" sub-attribute contains the value of an
|
|
|
|
"id" attribute of a SCIM resource, and the "$ref" sub-attribute must
|
|
|
|
be the URI of a SCIM resource such as a "User", or a "Group". The
|
|
|
|
intention of the "Group" type is to allow the service provider to
|
|
|
|
support nested groups. Service providers MAY require clients to
|
|
|
|
provide a non-empty value by setting the "required" attribute
|
|
|
|
characteristic of a sub-attribute of the "members" attribute in the
|
|
|
|
"Group" resource schema.
|
2023-12-14 22:08:17 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
description: str | None = None
|
2024-11-18 13:16:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
def string_code(code: int, digit: int) -> str:
|
|
|
|
"""Add leading 0 if the code length does not match the defined length.
|
|
|
|
|
|
|
|
For instance, parameter ``digit=6``, but ``code=123``, this method would
|
|
|
|
return ``000123``::
|
|
|
|
|
|
|
|
>>> otp.string_code(123)
|
|
|
|
'000123'
|
|
|
|
"""
|
|
|
|
return f"{code:0{digit}}"
|