from enum import Enum from pydantic import BaseModel from pydantic import ValidationInfo from pydantic import field_validator class SMTPSettings(BaseModel): """The SMTP configuration. Belong in the ``CANAILLE.SMTP`` namespace. If unset, mail related features will be disabled, such as mail verification or password recovery emails. By default, Canaille will try to send mails from localhost without authentication. """ HOST: str | None = "localhost" """The SMTP host.""" PORT: int | None = 25 """The SMTP port.""" TLS: bool | None = False """Whether to use TLS to connect to the SMTP server.""" SSL: bool | None = False """Whether to use SSL to connect to the SMTP server.""" LOGIN: str | None = None """The SMTP login.""" PASSWORD: str | None = None """The SMTP password.""" FROM_ADDR: str | None = None """The sender for Canaille mails. Some mail provider might require a valid sender address. """ class SMPPSettings(BaseModel): """The SMPP configuration. Belong in the ``CANAILLE.SMPP`` namespace. If not set, sms related features such as sms one-time passwords will be disabled. """ HOST: str | None = "localhost" """The SMPP host.""" PORT: int | None = 2775 """The SMPP port. Use 8775 for SMPP over TLS (recommended).""" LOGIN: str | None = None """The SMPP login.""" PASSWORD: str | None = None """The SMPP password.""" class Permission(str, Enum): """The permissions that can be assigned to users. The permissions are intended to be used in :attr:`ACLSettings `. """ EDIT_SELF = "edit_self" """Allows users to edit their own profile.""" USE_OIDC = "use_oidc" """Allows OpenID Connect authentication.""" MANAGE_OIDC = "manage_oidc" """Allows OpenID Connect client managements.""" MANAGE_USERS = "manage_users" """Allows other users management.""" MANAGE_GROUPS = "manage_groups" """Allows group edition and creation.""" DELETE_ACCOUNT = "delete_account" """Allows users to delete their account. If used with :attr:`~canaille.core.configuration.Permission.MANAGE_USERS`, users can delete any account. """ IMPERSONATE_USERS = "impersonate_users" """Allows users to take the identity of another user.""" class ACLSettings(BaseModel): """Access Control List settings. Belong in the ``CANAILLE.ACL`` namespace. You can define access controls that define what users can do on canaille. An access control consists in a :attr:`FILTER` to match users, a list of :attr:`PERMISSIONS` matched users will be able to perform, and fields users will be able to :attr:`READ` and :attr:`WRITE`. Users matching several filters will cumulate permissions. """ PERMISSIONS: list[Permission] = [Permission.EDIT_SELF, Permission.USE_OIDC] """A list of :class:`Permission` users in the access control will be able to manage. For example: ..code-block:: toml PERMISSIONS = ["manage_users", "manage_groups", "manage_oidc", "delete_account", "impersonate_users"] """ READ: list[str] = [ "user_name", "groups", "lock_date", ] """A list of :class:`~canaille.core.models.User` attributes that users in the ACL will be able to read.""" WRITE: list[str] = [ "photo", "given_name", "family_name", "display_name", "password", "phone_numbers", "emails", "profile_url", "formatted_address", "street", "postal_code", "locality", "region", "preferred_language", "employee_number", "department", "title", "organization", ] """A list of :class:`~canaille.core.models.User` attributes that users in the ACL will be able to edit.""" @field_validator("READ") def validate_read_values( cls, read: list[str], info: ValidationInfo, ) -> list[str]: from canaille.core.models import User assert all(field in User.attributes for field in read) return read @field_validator("WRITE") def validate_write_values( cls, write: list[str], info: ValidationInfo, ) -> list[str]: from canaille.core.models import User assert all(field in User.attributes for field in write) return write FILTER: dict[str, str] | list[dict[str, str]] | None = None """:attr:`FILTER` can be: - :py:data:`None`, in which case all the users will match this access control - a mapping where keys are user attributes name and the values those user attribute values. All the values must be matched for the user to be part of the access control. - a list of those mappings. If a user values match at least one mapping, then the user will be part of the access control Here are some examples:: FILTER = {user_name = 'admin'} FILTER = [ {groups = 'admins}, {groups = 'moderators'}, ] """ class CoreSettings(BaseModel): """The settings from the ``CANAILLE`` namespace. Those are all the configuration parameters that controls the behavior of Canaille. """ NAME: str = "Canaille" """Your organization name. Used for display purpose. """ LOGO: str | None = None """The logo of your organization, this is useful to make your organization recognizable on login screens.""" FAVICON: str | None = None """You favicon. If unset and :attr:`LOGO` is set, then the logo will be used. """ THEME: str = "default" """The name of a theme in the 'theme' directory, or a path to a theme. Defaults to ``default``. Theming is done with `flask-themer `_. """ LANGUAGE: str | None = None """If a language code is set, it will be used for every user. If unset, the language is guessed according to the users browser. """ TIMEZONE: str | None = None """The timezone in which datetimes will be displayed to the users (e.g. ``CEST``). If unset, the server timezone will be used. """ SENTRY_DSN: str | None = None """A `Sentry `_ DSN to collect the exceptions. This is useful for tracking errors in test and production environments. """ JAVASCRIPT: bool = True """Enables Javascript to smooth the user experience.""" HTMX: bool = True """Accelerates webpages loading with asynchronous requests.""" EMAIL_CONFIRMATION: bool = True """If :py:data:`True`, users will need to click on a confirmation link sent by email when they want to add a new email. By default, this is true if ``SMTP`` is configured, else this is false. If explicitly set to true and ``SMTP`` is disabled, the email field will be read-only. """ ENABLE_REGISTRATION: bool = False """If :py:data:`True`, then users can freely create an account at this instance. If email verification is available, users must confirm their email before the account is created. """ HIDE_INVALID_LOGINS: bool = True """If :py:data:`True`, when users try to sign in with an invalid login, a message is shown indicating that the password is wrong, but does not give a clue whether the login exists or not. If :py:data:`False`, when a user tries to sign in with an invalid login, a message is shown indicating that the login does not exist. """ ENABLE_PASSWORD_RECOVERY: bool = True """If :py:data:`False`, then users cannot ask for a password recovery link by email.""" ENABLE_INTRUDER_LOCKOUT: bool = False """If :py:data:`True`, then users will have to wait for an increasingly long time between each failed login attempt.""" OTP_METHOD: str = None """If OTP_METHOD is defined, then users will need to authenticate themselves using a one-time password (OTP) via an authenticator app. If set to ``TOTP``, the application will use time one-time passwords, If set to ``HOTP``, the application will use HMAC-based one-time passwords.""" EMAIL_OTP: bool = False """If :py:data:`True`, then users will need to authenticate themselves via a one-time password sent to their primary email address.""" SMS_OTP: bool = False """If :py:data:`True`, then users will need to authenticate themselves via a one-time password sent to their primary phone number.""" INVITATION_EXPIRATION: int = 172800 """The validity duration of registration invitations, in seconds. Defaults to 2 days. """ LOGGING: str | dict | None = None """Configures the logging output using the python logging configuration format: - If :data:`None`, everything is logged in the standard error output. The log level is :data:`~logging.DEBUG` if the :attr:`~canaille.app.configuration.RootSettings.DEBUG` setting is :py:data:`True`, else this is :py:data:`~logging.INFO`. - If this is a :class:`dict`, it is passed to :func:`logging.config.dictConfig`: - If this is a :class:`str`, it is expected to be a file path that will be passed to :func:`logging.config.fileConfig`. For example: ..code-block:: toml [CANAILLE.LOGGING] version = 1 formatters.default.format = "[%(asctime)s] %(levelname)s in %(module)s: %(message)s" root = {level = "INFO", handlers = ["canaille"]} [CANAILLE.LOGGING.handlers.canaille] class = "logging.handlers.WatchedFileHandler" filename = "/var/log/canaille.log" formatter = "default" """ SMTP: SMTPSettings | None = None """The settings related to SMTP and mail configuration. If unset, mail-related features like password recovery won't be enabled. """ SMPP: SMPPSettings | None = None """The settings related to SMPP configuration. If unset, sms-related features like sms one-time passwords won't be enabled. """ ACL: dict[str, ACLSettings] | None = {"DEFAULT": ACLSettings()} """Mapping of permission groups. See :class:`ACLSettings` for more details. The ACL name can be freely chosen. For example: ..code-block:: toml [CANAILLE.ACL.DEFAULT] PERMISSIONS = ["edit_self", "use_oidc"] READ = ["user_name", "groups"] WRITE = ["given_name", "family_name"] [CANAILLE.ACL.ADMIN] WRITE = ["user_name", "groups"] """ MIN_PASSWORD_LENGTH: int = 8 """User password minimum length. If 0 or :data:`None`, password won't have a minimum length. """ MAX_PASSWORD_LENGTH: int = 1000 """User password maximum length. There is a technical of 4096 characters with the SQL backend. If the value is 0, :data:`None`, or greater than 4096, then 4096 will be retained. """ ADMIN_EMAIL: str | None = None """Administration email contact. In certain special cases (example : questioning about password corruption), it is necessary to provide an administration contact email. """ ENABLE_PASSWORD_COMPROMISSION_CHECK: bool = False """If :py:data:`True`, Canaille will check if passwords appears in compromission databases such as `HIBP `_ when users choose a new one.""" PASSWORD_COMPROMISSION_CHECK_API_URL: str = "https://api.pwnedpasswords.com/range/" """Have i been pwned api url for compromission checks."""