chore: add docformatter pre-commit

This commit is contained in:
Éloi Rivard 2023-12-28 18:31:57 +01:00
parent e8b620588e
commit 395b6ab4f3
No known key found for this signature in database
GPG key ID: 7EDA204EA57DD184
14 changed files with 412 additions and 538 deletions

View file

@ -20,6 +20,10 @@ repos:
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ["--application-directories", "canaille"] args: ["--application-directories", "canaille"]
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.5
hooks:
- id: docformatter
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.0 rev: v3.15.0
hooks: hooks:

View file

@ -26,9 +26,7 @@ def with_backendcontext(func):
@with_appcontext @with_appcontext
@with_backendcontext @with_backendcontext
def check(): def check():
""" """Check the configuration file."""
Check the configuration file.
"""
from canaille.app.configuration import ConfigurationException from canaille.app.configuration import ConfigurationException
from canaille.app.configuration import validate from canaille.app.configuration import validate
@ -42,9 +40,7 @@ def check():
@click.command() @click.command()
@with_appcontext @with_appcontext
def install(): def install():
""" """Installs canaille elements from the configuration."""
Installs canaille elements from the configuration.
"""
from canaille.app.installation import install from canaille.app.installation import install
from canaille.app.configuration import ConfigurationException from canaille.app.configuration import ConfigurationException

View file

@ -15,10 +15,8 @@ class ConfigurationException(Exception):
def parse_file_keys(config): def parse_file_keys(config):
""" """Replaces configuration entries with the '_FILE' suffix with the matching
Replaces configuration entries with the '_FILE' suffix with file content."""
the matching file content.
"""
SUFFIX = "_FILE" SUFFIX = "_FILE"
new_config = {} new_config = {}

View file

@ -32,9 +32,7 @@ def current_user():
def login_user(user): def login_user(user):
""" """Opens a session for the user."""
Opens a session for the user.
"""
g.user = user g.user = user
try: try:
previous = ( previous = (
@ -48,9 +46,7 @@ def login_user(user):
def logout_user(): def logout_user():
""" """Closes the user session."""
Closes the user session.
"""
try: try:
session["user_id"].pop() session["user_id"].pop()
del g.user del g.user

View file

@ -71,9 +71,7 @@ class HTMXFormMixin:
render_field_extra_context = {} render_field_extra_context = {}
def field_from_name(self, field_name): def field_from_name(self, field_name):
""" """Returns a tuple containing a field and its rendering context."""
Returns a tuple containing a field and its rendering context
"""
if self.SEPARATOR not in field_name: if self.SEPARATOR not in field_name:
field = self[field_name] if field_name in self else None field = self[field_name] if field_name in self else None
return field, {} return field, {}
@ -89,10 +87,11 @@ class HTMXFormMixin:
return fieldlist[indice], context return fieldlist[indice], context
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
""" """If the request is a HTMX request, this will only render the field
If the request is a HTMX request, this will only render the field that triggered the request (after having validated the form).
that triggered the request (after having validated the form). This
uses the Flask abort method to interrupt the flow with an exception. This uses the Flask abort method to interrupt the flow with an
exception.
""" """
if not request_is_htmx(): if not request_is_htmx():
return super().validate(*args, **kwargs) return super().validate(*args, **kwargs)
@ -120,10 +119,8 @@ class HTMXFormMixin:
abort(response) abort(response)
def form_control(self): def form_control(self):
""" """Checks wether the current request is the result of the users adding
Checks wether the current request is the result of the users or removing a field from a FieldList."""
adding or removing a field from a FieldList.
"""
FIELDLIST_ADD_BUTTON = "fieldlist_add" FIELDLIST_ADD_BUTTON = "fieldlist_add"
FIELDLIST_REMOVE_BUTTON = "fieldlist_remove" FIELDLIST_REMOVE_BUTTON = "fieldlist_remove"

View file

@ -34,36 +34,29 @@ class BaseBackend:
@classmethod @classmethod
def install(self, config): def install(self, config):
""" """This methods prepares the database to host canaille data."""
This methods prepares the database to host canaille data.
"""
raise NotImplementedError() raise NotImplementedError()
def setup(self): def setup(self):
""" """This method will be called before each http request, it should open
This method will be called before each http request, the connection to the backend."""
it should open the connection to the backend.
"""
def teardown(self): def teardown(self):
""" """This method will be called after each http request, it should close
This method will be called after each http request, the connections to the backend."""
it should close the connections to the backend.
"""
@classmethod @classmethod
def validate(cls, config): def validate(cls, config):
""" """This method should validate the config part dedicated to the
This method should validate the config part dedicated to the backend. backend.
It should raise :class:`~canaille.configuration.ConfigurationError` when It should raise :class:`~canaille.configuration.ConfigurationError` when
errors are met. errors are met.
""" """
raise NotImplementedError() raise NotImplementedError()
def has_account_lockability(self): def has_account_lockability(self):
""" """Indicates wether the backend supports locking user accounts."""
Indicates wether the backend supports locking user accounts.
"""
raise NotImplementedError() raise NotImplementedError()
def register_models(self): def register_models(self):

View file

@ -4,9 +4,7 @@ from canaille.app import classproperty
class Model: class Model:
""" """Model abstract class."""
Model abstract class.
"""
@classproperty @classproperty
def attributes(cls): def attributes(cls):
@ -36,44 +34,37 @@ class Model:
@classmethod @classmethod
def fuzzy(cls, query, attributes=None, **kwargs): def fuzzy(cls, query, attributes=None, **kwargs):
""" """Works like :meth:`~canaille.backends.models.query` but attribute
Works like :meth:`~canaille.backends.models.query` but attribute values values loosely be matched."""
loosely be matched.
"""
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
def get(cls, identifier=None, **kwargs): def get(cls, identifier=None, **kwargs):
""" """Works like :meth:`~canaille.backends.models.query` but return only
Works like :meth:`~canaille.backends.models.query` but return only one one element or :const:`None` if no item is matching."""
element or :const:`None` if no item is matching.
"""
raise NotImplementedError() raise NotImplementedError()
@property @property
def identifier(self): def identifier(self):
""" """Returns a unique value that will be used to identify the model
Returns a unique value that will be used to identify the model instance. instance.
This value will be used in URLs in canaille, so it should be unique and short.
This value will be used in URLs in canaille, so it should be
unique and short.
""" """
raise NotImplementedError() raise NotImplementedError()
def save(self): def save(self):
""" """Validates the current modifications in the database."""
Validates the current modifications in the database.
"""
raise NotImplementedError() raise NotImplementedError()
def delete(self): def delete(self):
""" """Removes the current instance from the database."""
Removes the current instance from the database.
"""
raise NotImplementedError() raise NotImplementedError()
def update(self, **kwargs): def update(self, **kwargs):
""" """Assign a whole dict to the current instance. This is useful to
Assign a whole dict to the current instance. This is useful to update update models based on forms.
models based on forms.
>>> user = User.get(user_name="george") >>> user = User.get(user_name="george")
>>> user.first_name >>> user.first_name
@ -89,8 +80,7 @@ class Model:
setattr(self, attribute, value) setattr(self, attribute, value)
def reload(self): def reload(self):
""" """Cancels the unsaved modifications.
Cancels the unsaved modifications.
>>> user = User.get(user_name="george") >>> user = User.get(user_name="george")
>>> user.display_name >>> user.display_name

View file

@ -13,9 +13,7 @@ except ImportError:
@click.pass_context @click.pass_context
@with_appcontext @with_appcontext
def populate(ctx, nb): def populate(ctx, nb):
""" """Populate the database with generated random data."""
Populate the database with generated random data.
"""
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj["number"] = nb ctx.obj["number"] = nb
@ -26,9 +24,7 @@ def populate(ctx, nb):
@with_appcontext @with_appcontext
@with_backendcontext @with_backendcontext
def users(ctx): def users(ctx):
""" """Populate the database with generated random users."""
Populate the database with generated random users.
"""
from canaille.core.populate import fake_users from canaille.core.populate import fake_users
@ -45,9 +41,7 @@ def users(ctx):
@with_appcontext @with_appcontext
@with_backendcontext @with_backendcontext
def groups(ctx, nb_users_max): def groups(ctx, nb_users_max):
""" """Populate the database with generated random groups."""
Populate the database with generated random groups.
"""
from canaille.core.populate import fake_groups from canaille.core.populate import fake_groups

View file

@ -18,32 +18,32 @@ class User:
""" """
id: str id: str
""" """A unique identifier for a SCIM resource as defined by the service
A unique identifier for a SCIM resource as defined by the service provider.
provider. Each representation of the resource MUST include a
non-empty "id" value. This identifier MUST be unique across the Each representation of the resource MUST include a non-empty "id"
SCIM service provider's entire set of resources. It MUST be a value. This identifier MUST be unique across the SCIM service
stable, non-reassignable identifier that does not change when the provider's entire set of resources. It MUST be a stable, non-
same resource is returned in subsequent requests. The value of reassignable identifier that does not change when the same resource
the "id" attribute is always issued by the service provider and is returned in subsequent requests. The value of the "id" attribute
MUST NOT be specified by the client. The string "bulkId" is a is always issued by the service provider and MUST NOT be specified
reserved keyword and MUST NOT be used within any unique identifier by the client. The string "bulkId" is a reserved keyword and MUST
value. The attribute characteristics are "caseExact" as "true", a NOT be used within any unique identifier value. The attribute
mutability of "readOnly", and a "returned" characteristic of characteristics are "caseExact" as "true", a mutability of
"always". See Section 9 for additional considerations regarding "readOnly", and a "returned" characteristic of "always". See
privacy. Section 9 for additional considerations regarding privacy.
""" """
user_name: Optional[str] user_name: Optional[str]
""" """A service provider's unique identifier for the user, typically used by
A service provider's unique identifier for the user, typically the user to directly authenticate to the service provider.
used by the user to directly authenticate to the service provider.
Often displayed to the user as their unique identifier within the Often displayed to the user as their unique identifier within the
system (as opposed to "id" or "externalId", which are generally system (as opposed to "id" or "externalId", which are generally
opaque and not user-friendly identifiers). Each User MUST include opaque and not user-friendly identifiers). Each User MUST include a
a non-empty userName value. This identifier MUST be unique across non-empty userName value. This identifier MUST be unique across the
the service provider's entire set of Users. This attribute is service provider's entire set of Users. This attribute is REQUIRED
REQUIRED and is case insensitive. and is case insensitive.
""" """
password: Optional[str] password: Optional[str]
@ -92,115 +92,103 @@ class User:
""" """
preferred_language: Optional[str] preferred_language: Optional[str]
""" """Indicates the user's preferred written or spoken languages and is
Indicates the user's preferred written or spoken languages and is generally used for selecting a localized user interface.
generally used for selecting a localized user interface. The
value indicates the set of natural languages that are preferred. The value indicates the set of natural languages that are preferred.
The format of the value is the same as the HTTP Accept-Language The format of the value is the same as the HTTP Accept-Language
header field (not including "Accept-Language:") and is specified header field (not including "Accept-Language:") and is specified in
in Section 5.3.5 of [RFC7231]. The intent of this value is to Section 5.3.5 of [RFC7231]. The intent of this value is to enable
enable cloud applications to perform matching of language tags cloud applications to perform matching of language tags [RFC4647] to
[RFC4647] to the user's language preferences, regardless of what the user's language preferences, regardless of what may be indicated
may be indicated by a user agent (which might be shared), or in an by a user agent (which might be shared), or in an interaction that
interaction that does not involve a user (such as in a delegated does not involve a user (such as in a delegated OAuth 2.0 [RFC6749]
OAuth 2.0 [RFC6749] style interaction) where normal HTTP style interaction) where normal HTTP Accept-Language header
Accept-Language header negotiation cannot take place. negotiation cannot take place.
""" """
family_name: Optional[str] family_name: Optional[str]
""" """The family name of the User, or last name in most Western languages
The family name of the User, or last name in most (e.g., "Jensen" given the full name "Ms. Barbara Jane Jensen, III")."""
Western languages (e.g., "Jensen" given the full name
"Ms. Barbara Jane Jensen, III").
"""
given_name: Optional[str] given_name: Optional[str]
""" """The given name of the User, or first name in most Western languages
The given name of the User, or first name in most (e.g., "Barbara" given the full name "Ms. Barbara Jane Jensen, III")."""
Western languages (e.g., "Barbara" given the full name
"Ms. Barbara Jane Jensen, III").
"""
formatted_name: Optional[str] formatted_name: Optional[str]
""" """The full name, including all middle names, titles, and suffixes as
The full name, including all middle names, titles, and appropriate, formatted for display (e.g., "Ms. Barbara Jane Jensen,
suffixes as appropriate, formatted for display (e.g., III")."""
"Ms. Barbara Jane Jensen, III").
"""
display_name: Optional[str] display_name: Optional[str]
""" """The name of the user, suitable for display to end-users.
The name of the user, suitable for display to end-users. Each
user returned MAY include a non-empty displayName value. The name Each user returned MAY include a non-empty displayName value. The
SHOULD be the full name of the User being described, if known name SHOULD be the full name of the User being described, if known
(e.g., "Babs Jensen" or "Ms. Barbara J Jensen, III") but MAY be a (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., username or handle, if that is all that is available (e.g.,
"bjensen"). The value provided SHOULD be the primary textual "bjensen"). The value provided SHOULD be the primary textual label
label by which this User is normally displayed by the service by which this User is normally displayed by the service provider
provider when presenting it to end-users. when presenting it to end-users.
""" """
emails: List[str] emails: List[str]
""" """Email addresses for the User.
Email addresses for the User. The value SHOULD be specified
according to [RFC5321]. Service providers SHOULD canonicalize the The value SHOULD be specified according to [RFC5321]. Service
value according to [RFC5321], e.g., "bjensen@example.com" instead providers SHOULD canonicalize the value according to [RFC5321],
of "bjensen@EXAMPLE.COM". The "display" sub-attribute MAY be used e.g., "bjensen@example.com" instead of "bjensen@EXAMPLE.COM". The
to return the canonicalized representation of the email value. "display" sub-attribute MAY be used to return the canonicalized
The "type" sub-attribute is used to provide a classification representation of the email value. The "type" sub-attribute is used
meaningful to the (human) user. The user interface should to provide a classification meaningful to the (human) user. The
encourage the use of basic values of "work", "home", and "other" user interface should encourage the use of basic values of "work",
and MAY allow additional type values to be used at the discretion "home", and "other" and MAY allow additional type values to be used
of SCIM clients. at the discretion of SCIM clients.
""" """
phone_numbers: List[str] phone_numbers: List[str]
""" """Phone numbers for the user.
Phone numbers for the user. The value SHOULD be specified
according to the format defined in [RFC3966], e.g., The value SHOULD be specified according to the format defined in
'tel:+1-201-555-0123'. Service providers SHOULD canonicalize the [RFC3966], e.g., 'tel:+1-201-555-0123'. Service providers SHOULD
value according to [RFC3966] format, when appropriate. The canonicalize the value according to [RFC3966] format, when
"display" sub-attribute MAY be used to return the canonicalized appropriate. The "display" sub-attribute MAY be used to return the
representation of the phone number value. The sub-attribute canonicalized representation of the phone number value. The sub-
"type" often has typical values of "work", "home", "mobile", attribute "type" often has typical values of "work", "home",
"fax", "pager", and "other" and MAY allow more types to be defined "mobile", "fax", "pager", and "other" and MAY allow more types to be
by the SCIM clients. defined by the SCIM clients.
""" """
formatted_address: Optional[str] formatted_address: Optional[str]
""" """The full mailing address, formatted for display or use with a mailing
The full mailing address, formatted for display or use label.
with a mailing label. This attribute MAY contain newlines.
This attribute MAY contain newlines.
""" """
street: Optional[str] street: Optional[str]
""" """The full street address component, which may include house number,
The full street address component, which may street name, P.O.
include house number, street name, P.O. box, and multi-line
extended street address information. This attribute MAY box, and multi-line extended street address information. This
contain newlines. attribute MAY contain newlines.
""" """
postal_code: Optional[str] postal_code: Optional[str]
""" """The zip code or postal code component."""
The zip code or postal code component.
"""
locality: Optional[str] locality: Optional[str]
""" """The city or locality component."""
The city or locality component.
"""
region: Optional[str] region: Optional[str]
""" """The state or region component."""
The state or region component.
"""
photo: Optional[str] photo: Optional[str]
""" """A URI that is a uniform resource locator (as defined in Section 1.1.3 of
A URI that is a uniform resource locator (as defined in [RFC3986]) that points to a resource location representing the user's
Section 1.1.3 of [RFC3986]) that points to a resource location image.
representing the user's image. The resource MUST be a file (e.g.,
The resource MUST be a file (e.g.,
a GIF, JPEG, or PNG image file) rather than a web page containing a GIF, JPEG, or PNG image file) rather than a web page containing
an image. Service providers MAY return the same image in an image. Service providers MAY return the same image in
different sizes, although it is recognized that no standard for different sizes, although it is recognized that no standard for
@ -214,74 +202,63 @@ class User:
""" """
profile_url: Optional[str] profile_url: Optional[str]
""" """A URI that is a uniform resource locator (as defined in Section 1.1.3 of
A URI that is a uniform resource locator (as defined in [RFC3986]) and that points to a location representing the user's online
Section 1.1.3 of [RFC3986]) and that points to a location profile (e.g., a web page).
representing the user's online profile (e.g., a web page). URIs
are canonicalized per Section 6.2 of [RFC3986]. URIs are canonicalized per Section 6.2 of [RFC3986].
""" """
title: Optional[str] title: Optional[str]
""" """The user's title, such as "Vice President"."""
The user's title, such as "Vice President".
"""
organization: Optional[str] organization: Optional[str]
""" """Identifies the name of an organization."""
Identifies the name of an organization.
"""
employee_number: Optional[str] employee_number: Optional[str]
""" """A string identifier, typically numeric or alphanumeric, assigned to a
A string identifier, typically numeric or alphanumeric, assigned person, typically based on order of hire or association with an
to a person, typically based on order of hire or association with organization."""
an organization.
"""
department: Optional[str] department: Optional[str]
""" """Identifies the name of a department."""
Identifies the name of a department.
"""
last_modified: Optional[datetime.datetime] last_modified: Optional[datetime.datetime]
""" """The most recent DateTime that the details of this resource were updated
The most recent DateTime that the details of this at the service provider.
resource were updated at the service provider. If this
resource has never been modified since its initial creation, If this resource has never been modified since its initial creation,
the value MUST be the same as the value of "created". the value MUST be the same as the value of "created".
""" """
groups: List["Group"] groups: List["Group"]
""" """A list of groups to which the user belongs, either through direct
A list of groups to which the user belongs, either through direct membership, through nested groups, or dynamically calculated.
membership, through nested groups, or dynamically calculated. The
values are meant to enable expression of common group-based or The values are meant to enable expression of common group-based or
role-based access control models, although no explicit role-based access control models, although no explicit authorization
authorization model is defined. It is intended that the semantics model is defined. It is intended that the semantics of group
of group membership and any behavior or authorization granted as a membership and any behavior or authorization granted as a result of
result of membership are defined by the service provider. The membership are defined by the service provider. The canonical types
canonical types "direct" and "indirect" are defined to describe "direct" and "indirect" are defined to describe how the group
how the group membership was derived. Direct group membership membership was derived. Direct group membership indicates that the
indicates that the user is directly associated with the group and user is directly associated with the group and SHOULD indicate that
SHOULD indicate that clients may modify membership through the clients may modify membership through the "Group" resource. Indirect
"Group" resource. Indirect membership indicates that user membership indicates that user membership is transitive or dynamic
membership is transitive or dynamic and implies that clients and implies that clients cannot modify indirect group membership
cannot modify indirect group membership through the "Group" through the "Group" resource but MAY modify direct group membership
resource but MAY modify direct group membership through the through the "Group" resource, which may influence indirect
"Group" resource, which may influence indirect memberships. If memberships. If the SCIM service provider exposes a "Group"
the SCIM service provider exposes a "Group" resource, the "value" resource, the "value" sub-attribute MUST be the "id", and the "$ref"
sub-attribute MUST be the "id", and the "$ref" sub-attribute must sub-attribute must be the URI of the corresponding "Group" resources
be the URI of the corresponding "Group" resources to which the to which the user belongs. Since this attribute has a mutability of
user belongs. Since this attribute has a mutability of "readOnly", group membership changes MUST be applied via the "Group"
"readOnly", group membership changes MUST be applied via the Resource (Section 4.2). This attribute has a mutability of
"Group" Resource (Section 4.2). This attribute has a mutability "readOnly".
of "readOnly".
""" """
lock_date: Optional[datetime.datetime] lock_date: Optional[datetime.datetime]
""" """A DateTime indicating when the resource was locked."""
A DateTime indicating when the resource was locked.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.read = set() self.read = set()
@ -294,21 +271,15 @@ class User:
raise NotImplementedError() raise NotImplementedError()
def has_password(self) -> bool: def has_password(self) -> bool:
""" """Checks wether a password has been set for the user."""
Checks wether a password has been set for the user.
"""
raise NotImplementedError() raise NotImplementedError()
def check_password(self, password: str) -> bool: def check_password(self, password: str) -> bool:
""" """Checks if the password matches the user password in the database."""
Checks if the password matches the user password in the database.
"""
raise NotImplementedError() raise NotImplementedError()
def set_password(self, password: str): def set_password(self, password: str):
""" """Sets a password for the user."""
Sets a password for the user.
"""
raise NotImplementedError() raise NotImplementedError()
def can_read(self, field: str): def can_read(self, field: str):
@ -327,9 +298,7 @@ class User:
@property @property
def locked(self) -> bool: def locked(self) -> bool:
""" """Wether the user account has been locked or has expired."""
Wether the user account has been locked or has expired.
"""
return bool(self.lock_date) and self.lock_date < datetime.datetime.now( return bool(self.lock_date) and self.lock_date < datetime.datetime.now(
datetime.timezone.utc datetime.timezone.utc
) )
@ -342,39 +311,40 @@ class Group:
""" """
id: str id: str
""" """A unique identifier for a SCIM resource as defined by the service
A unique identifier for a SCIM resource as defined by the service provider.
provider. Each representation of the resource MUST include a
non-empty "id" value. This identifier MUST be unique across the Each representation of the resource MUST include a non-empty "id"
SCIM service provider's entire set of resources. It MUST be a value. This identifier MUST be unique across the SCIM service
stable, non-reassignable identifier that does not change when the provider's entire set of resources. It MUST be a stable, non-
same resource is returned in subsequent requests. The value of reassignable identifier that does not change when the same resource
the "id" attribute is always issued by the service provider and is returned in subsequent requests. The value of the "id" attribute
MUST NOT be specified by the client. The string "bulkId" is a is always issued by the service provider and MUST NOT be specified
reserved keyword and MUST NOT be used within any unique identifier by the client. The string "bulkId" is a reserved keyword and MUST
value. The attribute characteristics are "caseExact" as "true", a NOT be used within any unique identifier value. The attribute
mutability of "readOnly", and a "returned" characteristic of characteristics are "caseExact" as "true", a mutability of
"always". See Section 9 for additional considerations regarding "readOnly", and a "returned" characteristic of "always". See
privacy. Section 9 for additional considerations regarding privacy.
""" """
display_name: str display_name: str
""" """A human-readable name for the Group.
A human-readable name for the Group. REQUIRED.
REQUIRED.
""" """
members: List["User"] members: List["User"]
""" """A list of members of the Group.
A list of members of the Group. While values MAY be added or
removed, sub-attributes of members are "immutable". The "value" While values MAY be added or removed, sub-attributes of members are
sub-attribute contains the value of an "id" attribute of a SCIM "immutable". The "value" sub-attribute contains the value of an
resource, and the "$ref" sub-attribute must be the URI of a SCIM "id" attribute of a SCIM resource, and the "$ref" sub-attribute must
resource such as a "User", or a "Group". The intention of the be the URI of a SCIM resource such as a "User", or a "Group". The
"Group" type is to allow the service provider to support nested intention of the "Group" type is to allow the service provider to
groups. Service providers MAY require clients to provide a support nested groups. Service providers MAY require clients to
non-empty value by setting the "required" attribute characteristic provide a non-empty value by setting the "required" attribute
of a sub-attribute of the "members" attribute in the "Group" characteristic of a sub-attribute of the "members" attribute in the
resource schema. "Group" resource schema.
""" """
description: Optional[str] description: Optional[str]

View file

@ -22,53 +22,54 @@ class Client(Model):
audience: List["Client"] audience: List["Client"]
client_id: Optional[str] client_id: Optional[str]
""" """REQUIRED.
REQUIRED. OAuth 2.0 client identifier string. It SHOULD NOT be
currently valid for any other registered client, though an OAuth 2.0 client identifier string. It SHOULD NOT be currently
authorization server MAY issue the same client identifier to valid for any other registered client, though an authorization
multiple instances of a registered client at its discretion. server MAY issue the same client identifier to multiple instances of
a registered client at its discretion.
""" """
client_secret: Optional[str] client_secret: Optional[str]
""" """OPTIONAL.
OPTIONAL. OAuth 2.0 client secret string. If issued, this MUST
be unique for each "client_id" and SHOULD be unique for multiple OAuth 2.0 client secret string. If issued, this MUST be unique for
instances of a client using the same "client_id". This value is each "client_id" and SHOULD be unique for multiple instances of a
used by confidential clients to authenticate to the token client using the same "client_id". This value is used by
endpoint, as described in OAuth 2.0 [RFC6749], Section 2.3.1. confidential clients to authenticate to the token endpoint, as
described in OAuth 2.0 [RFC6749], Section 2.3.1.
""" """
client_id_issued_at: Optional[datetime.datetime] client_id_issued_at: Optional[datetime.datetime]
""" """OPTIONAL.
OPTIONAL. Time at which the client identifier was issued. The
time is represented as the number of seconds from Time at which the client identifier was issued. The time is
1970-01-01T00:00:00Z as measured in UTC until the date/time of represented as the number of seconds from 1970-01-01T00:00:00Z as
issuance. measured in UTC until the date/time of issuance.
""" """
client_secret_expires_at: Optional[datetime.datetime] client_secret_expires_at: Optional[datetime.datetime]
""" """REQUIRED if "client_secret" is issued.
REQUIRED if "client_secret" is issued. Time at which the client
secret will expire or 0 if it will not expire. The time is Time at which the client secret will expire or 0 if it will not
represented as the number of seconds from 1970-01-01T00:00:00Z as expire. The time is represented as the number of seconds from
measured in UTC until the date/time of expiration. 1970-01-01T00:00:00Z as measured in UTC until the date/time of
expiration.
""" """
redirect_uris: List[str] redirect_uris: List[str]
""" """Array of redirection URI strings for use in redirect-based flows such as
Array of redirection URI strings for use in redirect-based flows the authorization code and implicit flows.
such as the authorization code and implicit flows. As required by
Section 2 of OAuth 2.0 [RFC6749], clients using flows with As required by Section 2 of OAuth 2.0 [RFC6749], clients using flows
redirection MUST register their redirection URI values. with redirection MUST register their redirection URI values.
Authorization servers that support dynamic registration for Authorization servers that support dynamic registration for
redirect-based flows MUST implement support for this metadata redirect-based flows MUST implement support for this metadata value.
value.
""" """
token_endpoint_auth_method: Optional[str] token_endpoint_auth_method: Optional[str]
""" """String indicator of the requested authentication method for the token
String indicator of the requested authentication method for the endpoint. Values defined by this specification are:
token endpoint. Values defined by this specification are:
* "none": The client is a public client as defined in OAuth 2.0, * "none": The client is a public client as defined in OAuth 2.0,
Section 2.1, and does not have a client secret. Section 2.1, and does not have a client secret.
@ -88,9 +89,8 @@ class Client(Model):
""" """
grant_types: List[str] grant_types: List[str]
""" """Array of OAuth 2.0 grant type strings that the client can use at the
Array of OAuth 2.0 grant type strings that the client can use at token endpoint. These grant types are defined as follows:
the token endpoint. These grant types are defined as follows:
* "authorization_code": The authorization code grant type defined * "authorization_code": The authorization code grant type defined
in OAuth 2.0, Section 4.1. in OAuth 2.0, Section 4.1.
@ -146,134 +146,137 @@ class Client(Model):
""" """
client_name: Optional[str] client_name: Optional[str]
""" """Human-readable string name of the client to be presented to the end-user
Human-readable string name of the client to be presented to the during authorization.
end-user during authorization. If omitted, the authorization
server MAY display the raw "client_id" value to the end-user If omitted, the authorization server MAY display the raw "client_id"
instead. It is RECOMMENDED that clients always send this field. value to the end-user instead. It is RECOMMENDED that clients
The value of this field MAY be internationalized, as described in always send this field. The value of this field MAY be
Section 2.2. internationalized, as described in Section 2.2.
""" """
client_uri: Optional[str] client_uri: Optional[str]
""" """URL string of a web page providing information about the client.
URL string of a web page providing information about the client.
If present, the server SHOULD display this URL to the end-user in
a clickable fashion. It is RECOMMENDED that clients always send
this field. The value of this field MUST point to a valid web
page. The value of this field MAY be internationalized, as
described in Section 2.2.
"""
logo_uri: Optional[str] If present, the server SHOULD display this URL to the end-user in a
""" clickable fashion. It is RECOMMENDED that clients always send this
URL string that references a logo for the client. If present, the field. The value of this field MUST point to a valid web page. The
server SHOULD display this image to the end-user during approval.
The value of this field MUST point to a valid image file. The
value of this field MAY be internationalized, as described in value of this field MAY be internationalized, as described in
Section 2.2. Section 2.2.
""" """
scope: List[str] logo_uri: Optional[str]
"""URL string that references a logo for the client.
If present, the server SHOULD display this image to the end-user
during approval. The value of this field MUST point to a valid image
file. The value of this field MAY be internationalized, as
described in Section 2.2.
""" """
String containing a space-separated list of scope values (as
described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client scope: List[str]
can use when requesting access tokens. The semantics of values in """String containing a space-separated list of scope values (as described
this list are service specific. If omitted, an authorization in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when
server MAY register a client with a default set of scopes. requesting access tokens.
The semantics of values in this list are service specific. If
omitted, an authorization server MAY register a client with a
default set of scopes.
""" """
contacts: List[str] contacts: List[str]
""" """Array of strings representing ways to contact people responsible for
Array of strings representing ways to contact people responsible this client, typically email addresses.
for this client, typically email addresses. The authorization
server MAY make these contact addresses available to end-users for The authorization server MAY make these contact addresses available
support requests for the client. See Section 6 for information on to end-users for support requests for the client. See Section 6 for
Privacy Considerations. information on Privacy Considerations.
""" """
tos_uri: Optional[str] tos_uri: Optional[str]
""" """URL string that points to a human-readable terms of service document for
URL string that points to a human-readable terms of service the client that describes a contractual relationship between the end-user
document for the client that describes a contractual relationship and the client that the end-user accepts when authorizing the client.
between the end-user and the client that the end-user accepts when
authorizing the client. The authorization server SHOULD display The authorization server SHOULD display this URL to the end-user if
this URL to the end-user if it is provided. The value of this it is provided. The value of this field MUST point to a valid web
field MUST point to a valid web page. The value of this field MAY page. The value of this field MAY be internationalized, as
be internationalized, as described in Section 2.2. described in Section 2.2.
""" """
policy_uri: Optional[str] policy_uri: Optional[str]
""" """URL string that points to a human-readable privacy policy document that
URL string that points to a human-readable privacy policy document describes how the deployment organization collects, uses, retains, and
that describes how the deployment organization collects, uses, discloses personal data.
retains, and discloses personal data. The authorization server
SHOULD display this URL to the end-user if it is provided. The The authorization server SHOULD display this URL to the end-user if
value of this field MUST point to a valid web page. The value of it is provided. The value of this field MUST point to a valid web
this field MAY be internationalized, as described in Section 2.2. page. The value of this field MAY be internationalized, as
described in Section 2.2.
""" """
jwks_uri: Optional[str] jwks_uri: Optional[str]
""" """URL string referencing the client's JSON Web Key (JWK) Set [RFC7517]
URL string referencing the client's JSON Web Key (JWK) Set document, which contains the client's public keys.
[RFC7517] document, which contains the client's public keys. The
value of this field MUST point to a valid JWK Set document. These The value of this field MUST point to a valid JWK Set document.
keys can be used by higher-level protocols that use signing or These keys can be used by higher-level protocols that use signing or
encryption. For instance, these keys might be used by some encryption. For instance, these keys might be used by some
applications for validating signed requests made to the token applications for validating signed requests made to the token
endpoint when using JWTs for client authentication [RFC7523]. Use endpoint when using JWTs for client authentication [RFC7523]. Use
of this parameter is preferred over the "jwks" parameter, as it of this parameter is preferred over the "jwks" parameter, as it
allows for easier key rotation. The "jwks_uri" and "jwks" allows for easier key rotation. The "jwks_uri" and "jwks"
parameters MUST NOT both be present in the same request or parameters MUST NOT both be present in the same request or response.
response.
""" """
jwk: Optional[str] jwk: Optional[str]
""" """Client's JSON Web Key Set [RFC7517] document value, which contains the
Client's JSON Web Key Set [RFC7517] document value, which contains client's public keys.
the client's public keys. The value of this field MUST be a JSON
object containing a valid JWK Set. These keys can be used by The value of this field MUST be a JSON object containing a valid JWK
higher-level protocols that use signing or encryption. This Set. These keys can be used by higher-level protocols that use
parameter is intended to be used by clients that cannot use the signing or encryption. This parameter is intended to be used by
"jwks_uri" parameter, such as native clients that cannot host clients that cannot use the "jwks_uri" parameter, such as native
public URLs. The "jwks_uri" and "jwks" parameters MUST NOT both clients that cannot host public URLs. The "jwks_uri" and "jwks"
be present in the same request or response. parameters MUST NOT both be present in the same request or response.
""" """
software_id: Optional[str] software_id: Optional[str]
""" """A unique identifier string (e.g., a Universally Unique Identifier
A unique identifier string (e.g., a Universally Unique Identifier (UUID)) assigned by the client developer or software publisher used by
(UUID)) assigned by the client developer or software publisher registration endpoints to identify the client software to be dynamically
used by registration endpoints to identify the client software to registered.
be dynamically registered. Unlike "client_id", which is issued by
the authorization server and SHOULD vary between instances, the Unlike "client_id", which is issued by the authorization server and
"software_id" SHOULD remain the same for all instances of the SHOULD vary between instances, the "software_id" SHOULD remain the
client software. The "software_id" SHOULD remain the same across same for all instances of the client software. The "software_id"
multiple updates or versions of the same piece of software. The SHOULD remain the same across multiple updates or versions of the
value of this field is not intended to be human readable and is same piece of software. The value of this field is not intended to
usually opaque to the client and authorization server. be human readable and is usually opaque to the client and
authorization server.
""" """
software_version: Optional[str] software_version: Optional[str]
""" """A version identifier string for the client software identified by
A version identifier string for the client software identified by "software_id".
"software_id". The value of the "software_version" SHOULD change
on any update to the client software identified by the same The value of the "software_version" SHOULD change on any update to
"software_id". The value of this field is intended to be compared the client software identified by the same "software_id". The value
using string equality matching and no other comparison semantics of this field is intended to be compared using string equality
are defined by this specification. The value of this field is matching and no other comparison semantics are defined by this
outside the scope of this specification, but it is not intended to specification. The value of this field is outside the scope of this
be human readable and is usually opaque to the client and specification, but it is not intended to be human readable and is
authorization server. The definition of what constitutes an usually opaque to the client and authorization server. The
update to client software that would trigger a change to this definition of what constitutes an update to client software that
value is specific to the software itself and is outside the scope would trigger a change to this value is specific to the software
of this specification. itself and is outside the scope of this specification.
""" """
post_logout_redirect_uris: List[str] post_logout_redirect_uris: List[str]
""" """OPTIONAL.
OPTIONAL. Array of URLs supplied by the RP to which it MAY request
that the End-User's User Agent be redirected using the Array of URLs supplied by the RP to which it MAY request that the
End-User's User Agent be redirected using the
post_logout_redirect_uri parameter after a logout has been post_logout_redirect_uri parameter after a logout has been
performed. These URLs SHOULD use the https scheme and MAY contain performed. These URLs SHOULD use the https scheme and MAY contain
port, path, and query parameter components; however, they MAY use port, path, and query parameter components; however, they MAY use
@ -284,9 +287,7 @@ class Client(Model):
class AuthorizationCode(Model): class AuthorizationCode(Model):
""" """OpenID Connect temporary authorization code definition."""
OpenID Connect temporary authorization code definition.
"""
id: str id: str
authorization_code_id: str authorization_code_id: str
@ -305,9 +306,7 @@ class AuthorizationCode(Model):
class Token(Model): class Token(Model):
""" """OpenID Connect token definition."""
OpenID Connect token definition.
"""
id: str id: str
token_id: str token_id: str
@ -324,9 +323,7 @@ class Token(Model):
class Consent(Model): class Consent(Model):
""" """Long-term user consent to an application."""
Long-term user consent to an application.
"""
id: str id: str
consent_id: str consent_id: str

View file

@ -8,9 +8,7 @@ from flask.cli import with_appcontext
@with_appcontext @with_appcontext
@with_backendcontext @with_backendcontext
def clean(): def clean():
""" """Remove expired tokens and authorization codes."""
Remove expired tokens and authorization codes.
"""
for t in models.Token.query(): for t in models.Token.query():
if t.is_expired(): if t.is_expired():
t.delete() t.delete()

View file

@ -8,10 +8,8 @@ from flask import url_for
def test_confirmation_disabled_email_editable(testclient, backend, logged_user): def test_confirmation_disabled_email_editable(testclient, backend, logged_user):
""" """If email confirmation is disabled, users should be able to pick any
If email confirmation is disabled, users should be able to pick email."""
any email.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = False testclient.app.config["EMAIL_CONFIRMATION"] = False
res = testclient.get("/profile/user") res = testclient.get("/profile/user")
@ -34,11 +32,9 @@ def test_confirmation_disabled_email_editable(testclient, backend, logged_user):
def test_confirmation_unset_smtp_disabled_email_editable( def test_confirmation_unset_smtp_disabled_email_editable(
testclient, backend, logged_admin, user testclient, backend, logged_admin, user
): ):
""" """If email confirmation is unset and no SMTP server has been configured,
If email confirmation is unset and no SMTP server has then email confirmation cannot be enabled, thus users must be able to pick
been configured, then email confirmation cannot be enabled, any email."""
thus users must be able to pick any email.
"""
del testclient.app.config["SMTP"] del testclient.app.config["SMTP"]
testclient.app.config["EMAIL_CONFIRMATION"] = None testclient.app.config["EMAIL_CONFIRMATION"] = None
@ -59,9 +55,9 @@ def test_confirmation_unset_smtp_disabled_email_editable(
def test_confirmation_enabled_smtp_disabled_readonly(testclient, backend, logged_user): def test_confirmation_enabled_smtp_disabled_readonly(testclient, backend, logged_user):
""" """If email confirmation is enabled and no SMTP server is configured, this
If email confirmation is enabled and no SMTP server is configured, might be a misconfiguration, or a temporary SMTP disabling.
this might be a misconfiguration, or a temporary SMTP disabling.
In doubt, users cannot edit their emails. In doubt, users cannot edit their emails.
""" """
del testclient.app.config["SMTP"] del testclient.app.config["SMTP"]
@ -78,10 +74,8 @@ def test_confirmation_enabled_smtp_disabled_readonly(testclient, backend, logged
def test_confirmation_unset_smtp_enabled_email_admin_editable( def test_confirmation_unset_smtp_enabled_email_admin_editable(
testclient, backend, logged_admin, user testclient, backend, logged_admin, user
): ):
""" """Administrators should be able to edit user email addresses, even when
Administrators should be able to edit user email addresses, email confirmation is unset and SMTP is configured."""
even when email confirmation is unset and SMTP is configured.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = None testclient.app.config["EMAIL_CONFIRMATION"] = None
res = testclient.get("/profile/user") res = testclient.get("/profile/user")
@ -103,10 +97,8 @@ def test_confirmation_unset_smtp_enabled_email_admin_editable(
def test_confirmation_enabled_smtp_disabled_admin_editable( def test_confirmation_enabled_smtp_disabled_admin_editable(
testclient, backend, logged_admin, user testclient, backend, logged_admin, user
): ):
""" """Administrators should be able to edit user email addresses, even when
Administrators should be able to edit user email addresses, email confirmation is enabled and SMTP is disabled."""
even when email confirmation is enabled and SMTP is disabled.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = True testclient.app.config["EMAIL_CONFIRMATION"] = True
del testclient.app.config["SMTP"] del testclient.app.config["SMTP"]
@ -129,11 +121,8 @@ def test_confirmation_enabled_smtp_disabled_admin_editable(
def test_confirmation_unset_smtp_enabled_email_user_validation( def test_confirmation_unset_smtp_enabled_email_user_validation(
smtpd, testclient, backend, user smtpd, testclient, backend, user
): ):
""" """If email confirmation is unset and there is a SMTP server configured,
If email confirmation is unset and there is a SMTP server then users emails should be validated by sending a confirmation email."""
configured, then users emails should be validated by sending
a confirmation email.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = None testclient.app.config["EMAIL_CONFIRMATION"] = None
with freezegun.freeze_time("2020-01-01 01:00:00"): with freezegun.freeze_time("2020-01-01 01:00:00"):
@ -188,9 +177,7 @@ def test_confirmation_unset_smtp_enabled_email_user_validation(
def test_confirmation_invalid_link(testclient, backend, user): def test_confirmation_invalid_link(testclient, backend, user):
""" """Random confirmation links should fail."""
Random confirmation links should fail.
"""
res = testclient.get("/email-confirmation/invalid/invalid") res = testclient.get("/email-confirmation/invalid/invalid")
assert ( assert (
"error", "error",
@ -199,9 +186,7 @@ def test_confirmation_invalid_link(testclient, backend, user):
def test_confirmation_mail_form_failed(testclient, backend, user): def test_confirmation_mail_form_failed(testclient, backend, user):
""" """Tests when an error happens during the mail sending."""
Tests when an error happens during the mail sending.
"""
with freezegun.freeze_time("2020-01-01 01:00:00"): with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get("/login") res = testclient.get("/login")
res.form["login"] = "user" res.form["login"] = "user"
@ -227,9 +212,7 @@ def test_confirmation_mail_form_failed(testclient, backend, user):
@mock.patch("smtplib.SMTP") @mock.patch("smtplib.SMTP")
def test_confirmation_mail_send_failed(SMTP, smtpd, testclient, backend, user): def test_confirmation_mail_send_failed(SMTP, smtpd, testclient, backend, user):
""" """Tests when an error happens during the mail sending."""
Tests when an error happens during the mail sending.
"""
SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error")) SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error"))
with freezegun.freeze_time("2020-01-01 01:00:00"): with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get("/login") res = testclient.get("/login")
@ -255,9 +238,7 @@ def test_confirmation_mail_send_failed(SMTP, smtpd, testclient, backend, user):
def test_confirmation_expired_link(testclient, backend, user): def test_confirmation_expired_link(testclient, backend, user):
""" """Expired valid confirmation links should fail."""
Expired valid confirmation links should fail.
"""
email_confirmation = EmailConfirmationPayload( email_confirmation = EmailConfirmationPayload(
"2020-01-01T01:00:00+00:00", "2020-01-01T01:00:00+00:00",
"user", "user",
@ -282,9 +263,7 @@ def test_confirmation_expired_link(testclient, backend, user):
def test_confirmation_invalid_hash_link(testclient, backend, user): def test_confirmation_invalid_hash_link(testclient, backend, user):
""" """Confirmation link with invalid hashes should fail."""
Confirmation link with invalid hashes should fail.
"""
email_confirmation = EmailConfirmationPayload( email_confirmation = EmailConfirmationPayload(
"2020-01-01T01:00:00+00:00", "2020-01-01T01:00:00+00:00",
"user", "user",
@ -309,10 +288,10 @@ def test_confirmation_invalid_hash_link(testclient, backend, user):
def test_confirmation_invalid_user_link(testclient, backend, user): def test_confirmation_invalid_user_link(testclient, backend, user):
""" """Confirmation link about an unexisting user should fail.
Confirmation link about an unexisting user should fail.
For instance, when the user account has been deleted between For instance, when the user account has been deleted between the
the mail is sent and the link is clicked. mail is sent and the link is clicked.
""" """
email_confirmation = EmailConfirmationPayload( email_confirmation = EmailConfirmationPayload(
"2020-01-01T01:00:00+00:00", "2020-01-01T01:00:00+00:00",
@ -338,9 +317,7 @@ def test_confirmation_invalid_user_link(testclient, backend, user):
def test_confirmation_email_already_confirmed_link(testclient, backend, user, admin): def test_confirmation_email_already_confirmed_link(testclient, backend, user, admin):
""" """Clicking twice on a confirmation link should fail."""
Clicking twice on a confirmation link should fail.
"""
email_confirmation = EmailConfirmationPayload( email_confirmation = EmailConfirmationPayload(
"2020-01-01T01:00:00+00:00", "2020-01-01T01:00:00+00:00",
"user", "user",
@ -365,10 +342,11 @@ def test_confirmation_email_already_confirmed_link(testclient, backend, user, ad
def test_confirmation_email_already_used_link(testclient, backend, user, admin): def test_confirmation_email_already_used_link(testclient, backend, user, admin):
""" """Confirmation link should fail if the target email is already associated
Confirmation link should fail if the target email is already associated to another account.
to another account. For instance, if an administrator already put
this email to someone else's profile. For instance, if an administrator already put this email to someone
else's profile.
""" """
email_confirmation = EmailConfirmationPayload( email_confirmation = EmailConfirmationPayload(
"2020-01-01T01:00:00+00:00", "2020-01-01T01:00:00+00:00",
@ -394,10 +372,8 @@ def test_confirmation_email_already_used_link(testclient, backend, user, admin):
def test_delete_email(testclient, logged_user): def test_delete_email(testclient, logged_user):
""" """Tests that user can deletes its emails unless they have only one
Tests that user can deletes its emails unless they have only left."""
one left.
"""
res = testclient.get("/profile/user") res = testclient.get("/profile/user")
assert "email_remove" not in res.forms["emailconfirmationform"].fields assert "email_remove" not in res.forms["emailconfirmationform"].fields
@ -416,10 +392,7 @@ def test_delete_email(testclient, logged_user):
def test_delete_wrong_email(testclient, logged_user): def test_delete_wrong_email(testclient, logged_user):
""" """Tests that removing an already removed email do not produce anything."""
Tests that removing an already removed email do not
produce anything.
"""
logged_user.emails = logged_user.emails + ["new@email.com"] logged_user.emails = logged_user.emails + ["new@email.com"]
logged_user.save() logged_user.save()
@ -440,9 +413,7 @@ def test_delete_wrong_email(testclient, logged_user):
def test_delete_last_email(testclient, logged_user): def test_delete_last_email(testclient, logged_user):
""" """Tests that users cannot remove their last email address."""
Tests that users cannot remove their last email address.
"""
logged_user.emails = logged_user.emails + ["new@email.com"] logged_user.emails = logged_user.emails + ["new@email.com"]
logged_user.save() logged_user.save()
@ -463,10 +434,8 @@ def test_delete_last_email(testclient, logged_user):
def test_edition_forced_mail(testclient, logged_user): def test_edition_forced_mail(testclient, logged_user):
""" """Tests that users that must perform email verification cannot force the
Tests that users that must perform email verification profile form."""
cannot force the profile form.
"""
res = testclient.get("/profile/user", status=200) res = testclient.get("/profile/user", status=200)
form = res.forms["baseform"] form = res.forms["baseform"]
testclient.post( testclient.post(
@ -483,10 +452,8 @@ def test_edition_forced_mail(testclient, logged_user):
def test_invitation_form_mail_field_readonly(testclient): def test_invitation_form_mail_field_readonly(testclient):
""" """Tests that the email field is readonly in the invitation form creation
Tests that the email field is readonly in the invitation if email confirmation is enabled."""
form creation if email confirmation is enabled.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = True testclient.app.config["EMAIL_CONFIRMATION"] = True
payload = RegistrationPayload( payload = RegistrationPayload(
@ -504,10 +471,8 @@ def test_invitation_form_mail_field_readonly(testclient):
def test_invitation_form_mail_field_writable(testclient): def test_invitation_form_mail_field_writable(testclient):
""" """Tests that the email field is writable in the invitation form creation
Tests that the email field is writable in the invitation if email confirmation is disabled."""
form creation if email confirmation is disabled.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = False testclient.app.config["EMAIL_CONFIRMATION"] = False
payload = RegistrationPayload( payload = RegistrationPayload(

View file

@ -7,9 +7,7 @@ from flask import url_for
def test_registration_without_email_validation(testclient, backend, foo_group): def test_registration_without_email_validation(testclient, backend, foo_group):
""" """Tests a nominal registration without email validation."""
Tests a nominal registration without email validation.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
testclient.app.config["EMAIL_CONFIRMATION"] = False testclient.app.config["EMAIL_CONFIRMATION"] = False
@ -29,9 +27,7 @@ def test_registration_without_email_validation(testclient, backend, foo_group):
def test_registration_with_email_validation(testclient, backend, smtpd, foo_group): def test_registration_with_email_validation(testclient, backend, smtpd, foo_group):
""" """Tests a nominal registration with email validation."""
Tests a nominal registration with email validation.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
with freezegun.freeze_time("2020-01-01 02:00:00"): with freezegun.freeze_time("2020-01-01 02:00:00"):
@ -84,9 +80,7 @@ def test_registration_with_email_validation(testclient, backend, smtpd, foo_grou
def test_registration_with_email_already_taken( def test_registration_with_email_already_taken(
testclient, backend, smtpd, user, foo_group testclient, backend, smtpd, user, foo_group
): ):
""" """Be sure to not leak email existence if HIDE_INVALID_LOGINS is true."""
Be sure to not leak email existence if HIDE_INVALID_LOGINS is true.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
testclient.app.config["HIDE_INVALID_LOGINS"] = True testclient.app.config["HIDE_INVALID_LOGINS"] = True
@ -111,26 +105,20 @@ def test_registration_with_email_already_taken(
def test_registration_with_email_validation_needs_a_valid_link( def test_registration_with_email_validation_needs_a_valid_link(
testclient, backend, smtpd, foo_group testclient, backend, smtpd, foo_group
): ):
""" """Tests a nominal registration without email validation."""
Tests a nominal registration without email validation.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
testclient.get(url_for("core.account.registration"), status=403) testclient.get(url_for("core.account.registration"), status=403)
def test_join_page_registration_disabled(testclient, backend, smtpd, foo_group): def test_join_page_registration_disabled(testclient, backend, smtpd, foo_group):
""" """The join page should not be available if registration is disabled."""
The join page should not be available if registration is disabled.
"""
testclient.app.config["ENABLE_REGISTRATION"] = False testclient.app.config["ENABLE_REGISTRATION"] = False
testclient.get(url_for("core.account.join"), status=404) testclient.get(url_for("core.account.join"), status=404)
def test_join_page_email_confirmation_disabled(testclient, backend, smtpd, foo_group): def test_join_page_email_confirmation_disabled(testclient, backend, smtpd, foo_group):
""" """The join page should directly redirect to the registration page if email
The join page should directly redirect to the registration page if confirmation is disabled."""
email confirmation is disabled.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
testclient.app.config["EMAIL_CONFIRMATION"] = False testclient.app.config["EMAIL_CONFIRMATION"] = False
res = testclient.get(url_for("core.account.join"), status=302) res = testclient.get(url_for("core.account.join"), status=302)
@ -138,18 +126,14 @@ def test_join_page_email_confirmation_disabled(testclient, backend, smtpd, foo_g
def test_join_page_already_logged_in(testclient, backend, logged_user, foo_group): def test_join_page_already_logged_in(testclient, backend, logged_user, foo_group):
""" """The join page should not be accessible for logged users."""
The join page should not be accessible for logged users.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
testclient.get(url_for("core.account.join"), status=403) testclient.get(url_for("core.account.join"), status=403)
@mock.patch("smtplib.SMTP") @mock.patch("smtplib.SMTP")
def test_registration_mail_error(SMTP, testclient, backend, smtpd, foo_group): def test_registration_mail_error(SMTP, testclient, backend, smtpd, foo_group):
""" """Display an error message if the registration mail could not be sent."""
Display an error message if the registration mail could not be sent.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error")) SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error"))
res = testclient.get(url_for("core.account.join")) res = testclient.get(url_for("core.account.join"))

View file

@ -1,5 +1,5 @@
""" """Tests the behavior of Canaille depending on the OIDC 'prompt' parameter.
Tests the behavior of Canaille depending on the OIDC 'prompt' parameter.
https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
""" """
import datetime import datetime
@ -13,9 +13,7 @@ from flask import url_for
def test_prompt_none(testclient, logged_user, client): def test_prompt_none(testclient, logged_user, client):
""" """Nominal case with prompt=none."""
Nominal case with prompt=none
"""
consent = models.Consent( consent = models.Consent(
consent_id=str(uuid.uuid4()), consent_id=str(uuid.uuid4()),
client=client, client=client,
@ -43,16 +41,14 @@ def test_prompt_none(testclient, logged_user, client):
def test_prompt_not_logged(testclient, user, client): def test_prompt_not_logged(testclient, user, client):
""" """Prompt=none should return a login_required error when no user is logged
prompt=none should return a login_required error when no in.
user is logged in.
login_required login_required The Authorization Server requires End-User
The Authorization Server requires End-User authentication. authentication. This error MAY be returned when the prompt
This error MAY be returned when the prompt parameter value in the parameter value in the Authentication Request is none, but the
Authentication Request is none, but the Authentication Request Authentication Request cannot be completed without displaying a
cannot be completed without displaying a user interface for End-User user interface for End-User authentication.
authentication.
""" """
consent = models.Consent( consent = models.Consent(
consent_id=str(uuid.uuid4()), consent_id=str(uuid.uuid4()),
@ -79,15 +75,14 @@ def test_prompt_not_logged(testclient, user, client):
def test_prompt_no_consent(testclient, logged_user, client): def test_prompt_no_consent(testclient, logged_user, client):
""" """Prompt=none should return a consent_required error when user are logged
prompt=none should return a consent_required error when user in but have not granted their consent.
are logged in but have not granted their consent.
consent_required consent_required The Authorization Server requires End-User consent.
The Authorization Server requires End-User consent. This error MAY be This error MAY be returned when the prompt parameter value in the
returned when the prompt parameter value in the Authentication Request Authentication Request is none, but the Authentication Request
is none, but the Authentication Request cannot be completed without cannot be completed without displaying a user interface for End-User
displaying a user interface for End-User consent. consent.
""" """
res = testclient.get( res = testclient.get(
"/oauth/authorize", "/oauth/authorize",
@ -104,10 +99,8 @@ def test_prompt_no_consent(testclient, logged_user, client):
def test_prompt_create_logged(testclient, logged_user, client): def test_prompt_create_logged(testclient, logged_user, client):
""" """If prompt=create and user is already logged in, then go straight to the
If prompt=create and user is already logged in, consent page."""
then go straight to the consent page.
"""
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True
consent = models.Consent( consent = models.Consent(
@ -135,16 +128,15 @@ def test_prompt_create_logged(testclient, logged_user, client):
def test_prompt_create_registration_disabled(testclient, trusted_client, smtpd): def test_prompt_create_registration_disabled(testclient, trusted_client, smtpd):
""" """If prompt=create but Canaille registration is disabled, an error
If prompt=create but Canaille registration is disabled, response should be returned.
an error response should be returned.
If the OpenID Provider receives a prompt value that it does If the OpenID Provider receives a prompt value that it does not
not support (not declared in the prompt_values_supported support (not declared in the prompt_values_supported metadata field)
metadata field) the OP SHOULD respond with an HTTP 400 (Bad the OP SHOULD respond with an HTTP 400 (Bad Request) status code and
Request) status code and an error value of invalid_request. an error value of invalid_request. It is RECOMMENDED that the OP
It is RECOMMENDED that the OP return an error_description return an error_description value identifying the invalid parameter
value identifying the invalid parameter value. value.
""" """
res = testclient.get( res = testclient.get(
"/oauth/authorize", "/oauth/authorize",
@ -164,11 +156,11 @@ def test_prompt_create_registration_disabled(testclient, trusted_client, smtpd):
def test_prompt_create_not_logged(testclient, trusted_client, smtpd): def test_prompt_create_not_logged(testclient, trusted_client, smtpd):
""" """If prompt=create and user is not logged in, then display the
If prompt=create and user is not logged in, registration form.
then display the registration form.
Check that the user is correctly redirected to Check that the user is correctly redirected to the client page after
the client page after the registration process. the registration process.
""" """
testclient.app.config["ENABLE_REGISTRATION"] = True testclient.app.config["ENABLE_REGISTRATION"] = True