uffd issueshttps://git.cccv.de/uffd/uffd/-/issues2023-10-20T13:14:15Zhttps://git.cccv.de/uffd/uffd/-/issues/162Deprecation of crypt2023-10-20T13:14:15ZJulianDeprecation of cryptThe `crypt` module is going to be removed from the standard library in python3.13.
We still use it for recovery codes. We should probably migrate recovery codes to `PasswordHash` and a saner algorithm. However, we cannot break existing ...The `crypt` module is going to be removed from the standard library in python3.13.
We still use it for recovery codes. We should probably migrate recovery codes to `PasswordHash` and a saner algorithm. However, we cannot break existing recovery codes.
## Option 1: Switch to passlib
To quote [Python's crypt deprecation notice](https://docs.python.org/3.11/library/crypt.html):
> The passlib package can replace all use cases of this module.
Technically this is not true. While the `crypt` module calls the crypt function of the system libc, passlib contains pure-python implementations of the most common crypt methods. So there might be some libc-supported methods that passlib does not implement.
The project seems pretty dormant. Last commit was 3 years ago. Maybe it is simply "done".
## Option 2: Manually call libc's crypt
```python
>>> import ctypes
>>> crypt = ctypes.CDLL("libcrypt.so")
>>> crypt.restype = ctypes.c_char_p
>>> crypt(b'foobar', b'$6$EoC2R3kckYop9Zz0$QWdAmKAp5JaWThjPVThqmO1XvN.Gh7cEY9l51PQgEP0f9rL6luTMriif09x9TEJheq3KWc04W.ZsOCbnxRcvL.')
b'$6$EoC2R3kckYop9Zz0$QWdAmKAp5JaWThjPVThqmO1XvN.Gh7cEY9l51PQgEP0f9rL6luTMriif09x9TEJheq3KWc04W.ZsOCbnxRcvL.'
```
Not sure if library name "libcrypt.so" works in all cases.
## Option 3: Implement most relevant methods ourselves
Find out which methods are the default/available on our target platform (Debian Buster and Bullseye). On our production system it is `6`, i.e. salted sha512 without any rounds. Then implement those methods. This approach has similar problems as using passlib.
Sadly we cannot convert crypt hashes to OpenLDAP-/NIS-style password hashes: E.g. crypt's `6` method hashes `<password><salt><password>` instead of `<password><salt>`.
## Option 4: Switch algorithm and prompt users to generate new recovery codes
We will still not be able to drop crypt support anytime soon, but we might be able to do that at some point.
Not sure if potentially getting rid of this technical dept justifies the user impact.
---
I guess we will use some combination, like use `crypt` python module if available, fall back to loading `libcrypt` manually and gradually get rid of crypt hashes by prompting users to generate new recovery codes.https://git.cccv.de/uffd/uffd/-/issues/163Migrate recovery codes to PasswordHash2023-10-20T13:12:08ZJulianMigrate recovery codes to PasswordHashAdd migration to prefix `recovery_hash` field in `mfa_method` table with `{crypt}`. Adapt `RecoveryCodeMethod` model to rely on `PasswordHash` instead of using `crypt` manually.
Maybe also change method for new recovery codes to somethi...Add migration to prefix `recovery_hash` field in `mfa_method` table with `{crypt}`. Adapt `RecoveryCodeMethod` model to rely on `PasswordHash` instead of using `crypt` manually.
Maybe also change method for new recovery codes to something different.
Would be a good opportunity to remove the unused field `recovery_salt`.https://git.cccv.de/uffd/uffd/-/issues/161Set caching headers2023-01-14T22:31:05ZndSet caching headersWe should include the `Cache-Control: no-store` in all responses to ensure no upstream lb ever caches uffd content.
Caching uffd pages could result in leaking of data from one user to another.
This would be a precaution against a misscon...We should include the `Cache-Control: no-store` in all responses to ensure no upstream lb ever caches uffd content.
Caching uffd pages could result in leaking of data from one user to another.
This would be a precaution against a missconfigured reverse proxy in front of uffdhttps://git.cccv.de/uffd/uffd/-/issues/128User impersonation2022-10-12T12:19:20ZJulianUser impersonationAdmin-only function to temporarily *become* another user.
This could be realised by setting `session['impersonated_user_id']`. `set_request_user()` could then set `request.user` to the impersonated user as long as the cookie value is pr...Admin-only function to temporarily *become* another user.
This could be realised by setting `session['impersonated_user_id']`. `set_request_user()` could then set `request.user` to the impersonated user as long as the cookie value is present. The logout button could be replaced with a "Stop impersonation" button.v2.x.xhttps://git.cccv.de/uffd/uffd/-/issues/159Extended metrics2022-08-28T11:26:43ZndExtended metricsWe want metrics for:
* [ ] histogram call times for each endpoint
* [ ] python interpreter data (gc runs / memory ussage) (pid label)
* [ ] ratelimit events
Special care musst be take to merge those over uwsgi processes / threads. We co...We want metrics for:
* [ ] histogram call times for each endpoint
* [ ] python interpreter data (gc runs / memory ussage) (pid label)
* [ ] ratelimit events
Special care musst be take to merge those over uwsgi processes / threads. We could look how `prometheus_flask_exporter` does that, or create our own solution via shared memory.
This will probably result in a mandatory dependency on `prometheus-client`, that's why it's targeting 3.0.0.v3.0.0https://git.cccv.de/uffd/uffd/-/issues/158Service-bound Resources and Permissions2022-08-24T11:38:48ZJulianService-bound Resources and PermissionsCurrently permissions are represented by groups. This applies to both uffd-internal permissions (e.g. admin rights in uffd) and service permissions (e.g. admin rights in rocketchat). For service permissions this is somewhat limited:
- T...Currently permissions are represented by groups. This applies to both uffd-internal permissions (e.g. admin rights in uffd) and service permissions (e.g. admin rights in rocketchat). For service permissions this is somewhat limited:
- The groups of a user are global, so every service sees all permissions a user has
- Services with complex resources and permissions either require
- local configuration (e.g. encoded in custom sync scripts) that map uffd groups to actual permissions on resources.
- a complex encoding scheme for group names. This is barely possible, since they are length-limited.
I propose a service-bound resource and permission model (example: mailman3):
- Resource types: Admin-configured types of resources with unique *names* (e.g. "list" for mailinglists)
- Resources: Instances of resource types with a unique *value* (e.g. for every mailinglist with the list name as the unique value)
- Permission types: Admin-configured types of permissions, either related to a resource type or freestanding (e.g. "member" for the "list" resource type and "admin" as a freestanding permission for the service)
- Permission: Instance of permission types. For resource-type-bound permission types, there is a permission instance for every resource of that type (e.g. a "member" permission object for every mailinglist). For each freestanding permission types there is just one instance.
- Similar to groups, permissions can be added to roles
- Similar to groups, users have the permissions of their effective roles
- getusers API endpoint returns an additional "permissions" field with the names of the freestanding permissions a user has and a "resources" field with a value similar to `{"RESOURCE_TYPE1": {"RESOURCE_VALUE1": {"permissions": [names of permissions]}, ...}, ...}`
- New API endpoint to retrieve all resources
- Resources can have extra admin-configured data/setting (free-form JSON), e.g. for a mailinglist whether or not members are auto-unsubscribed if they don't have the "member" permission for that list. This extra data would be available in both the getusers field and the new resources endpointhttps://git.cccv.de/uffd/uffd/-/issues/140Incremental sync for services2022-08-24T11:01:14ZJulianIncremental sync for servicesIn most cases we use LDAP (or the getusers/getgroups API endpoints) to keep local users accounts in a service updated and create new accounts for newly created uffd users.
Both (standard) LDAP and the getusers/getgroups API endpoints do ...In most cases we use LDAP (or the getusers/getgroups API endpoints) to keep local users accounts in a service updated and create new accounts for newly created uffd users.
Both (standard) LDAP and the getusers/getgroups API endpoints do not offer a way to get recent changes. So this is realised by regularly (once every 15 minutes, in some cases once per day) performing full syncs of all users.
This has a few problems:
1. Unnecessary performance impact on both the service and uffd: The current syncs with ~550 users usually run serveral minutes with significant load, limiting the frequency in which we can run the syncs.
2. Latency: Even running a sync every 15 minutes can be annoying for newly registered users or if someone is granted a role to get urgently needed access to something. Running syncs at a lower frequency is a pretty bad user experience.
3. Since the service syncs by performing queries, uffd has no idea of the state the service is in. There are also no metrics available to detect whether some number users is not synced correctly.
My proposal is a new incremental sync API for services, i.e. a new API endpoint that service can call to get a list of changed users and a mechansim to confirm when the service has updated the corresponding local accounts.
Since the build-in LDAP support of most services is already insufficient (or horribly broken) and we already use custom sync scripts, we can rewrite those scripts to use the new sync API.
Such an API could also allow deletion of user accounts in services, which is neccessary for deletion of uffd user accounts.v2.x.xhttps://git.cccv.de/uffd/uffd/-/issues/79Service-specific User Passwords2022-08-20T22:40:26ZJulianService-specific User PasswordsSome services require password authentification and cannot be integrated into the Single-Sign-On, e.g. mail or mumble. Since entering the SSO password anywhere else than the SSO login page contradicts the idea of a SSO and also contradic...Some services require password authentification and cannot be integrated into the Single-Sign-On, e.g. mail or mumble. Since entering the SSO password anywhere else than the SSO login page contradicts the idea of a SSO and also contradicts what we tell users about password security, those passwords should be separate from the SSO password.
Requires removal of LDAP support, so v2.x.x feature.v2.x.xhttps://git.cccv.de/uffd/uffd/-/issues/85Use Post for any Endpoints Which Change Data2022-03-24T21:50:20ZndUse Post for any Endpoints Which Change Data* [ ] `mail.delete GET /mail/<uid>/del`
* [ ] `mfa.admin_disable GET /mfa/admin/<int:uid>/disable`
* [ ] `mfa.delete_totp GET /mfa/setup/totp/<int:id>/delete`
* [ ] `mfa.de...* [ ] `mail.delete GET /mail/<uid>/del`
* [ ] `mfa.admin_disable GET /mfa/admin/<int:uid>/disable`
* [ ] `mfa.delete_totp GET /mfa/setup/totp/<int:id>/delete`
* [ ] `mfa.delete_webauthn GET /mfa/setup/webauthn/<int:id>/delete`
* [ ] `role.delete GET /role/<int:roleid>/del`
* [ ] `role.set_default GET /role/<int:roleid>/setdefault`
* [ ] `role.unset_default GET /role/<int:roleid>/unsetdefault`
* [ ] `role.unlock GET /role/<int:roleid>/unlock`
* [ ] `rolemod.delete_member GET /rolemod/<int:role_id>/delete_member/<member_dn>`
* [ ] `selfservice.token_mail GET /self/token/mail_verification/<token>`
* [ ] `user.delete GET /user/<int:uid>/del`
Special case:
* [ ] `session.logout GET /logout`https://git.cccv.de/uffd/uffd/-/issues/118Set Referrer-Policy header2022-03-24T21:49:28ZJulianSet Referrer-Policy headeruffd does not rely on the Referer header at all, so we should disable it altogether (i.e. set `Referrer-Policy: no-referrer`). Since this is application-specfic and not deployment-specific, the application should IMHO set this policy (in...uffd does not rely on the Referer header at all, so we should disable it altogether (i.e. set `Referrer-Policy: no-referrer`). Since this is application-specfic and not deployment-specific, the application should IMHO set this policy (in contrast to HSTS).https://git.cccv.de/uffd/uffd/-/issues/119Set CSP header2022-03-24T21:49:21ZJulianSet CSP headerSimilar to #118 I would argue that it is uffd's responsibility to set a `Content-Security-Policy` header that is as strict as possible.
* `default-src 'none';`
* `img-src 'self';`: We embed an SVG image on the TOTP setup page. Not sure ...Similar to #118 I would argue that it is uffd's responsibility to set a `Content-Security-Policy` header that is as strict as possible.
* `default-src 'none';`
* `img-src 'self';`: We embed an SVG image on the TOTP setup page. Not sure if this requires a more permissive policy.
* `style-src 'self' 'unsafe-inline';`: We use inline-styles in a few places. This should be easy to refactor.
* `script-src 'self' 'unsafe-inline';`: We use a lot of inline Javascript on WebAuthn and SLO pages and on a few button/forms to display "Are you sure?" alerts. While I'm not sure what to do with the WebAuthn/SLO pages, the other stuff should be easy to refactor.
* `connect-src 'self';`: This is difficult, because the Single-Log-Out (SLO) relies on calling logout endpoints on other websites (with different domains). Since the SLO view knows the logout URLs that the frontend code is going to call, we could generate a minimal `connect-src` directive dynamically. In all other cases `'self'` should be sufficient.
* `form-action 'self';`
* `frame-ancestors 'none';`https://git.cccv.de/uffd/uffd/-/issues/139TOTP Key Encryption2022-03-24T21:44:54ZndTOTP Key EncryptionWe could encrypt totp keys in the datebase with a symetric key from the config file.
This would protect against a leaked database and mitigate some dangers of sql injections.
We should think about if we want to do this.
An example for a...We could encrypt totp keys in the datebase with a symetric key from the config file.
This would protect against a leaked database and mitigate some dangers of sql injections.
We should think about if we want to do this.
An example for an application doing this already would be gitlab. See for https://docs.gitlab.com/ee/raketasks/user_management.html#rotate-two-factor-authentication-encryption-keyhttps://git.cccv.de/uffd/uffd/-/issues/153Make recovery keys optional2022-03-22T22:47:13ZdavidcMake recovery keys optionalIt would be good if the creation of recovery keys before setting up 2FA is made optional. Maybe it can give a warning if you try to set up 2FA without them, but allow you to proceed anyway.
Reason being that we can always reset accounts...It would be good if the creation of recovery keys before setting up 2FA is made optional. Maybe it can give a warning if you try to set up 2FA without them, but allow you to proceed anyway.
Reason being that we can always reset accounts for people via out of band communication. And storing recovery keys (even if the user never saves them) could potentially be a risk.https://git.cccv.de/uffd/uffd/-/issues/149uffd silently fails without TLS2022-03-22T12:54:12Zdavidcuffd silently fails without TLSThe cookie is sent with the Secure flag, so it doesn't work if not using https (e.g. testing). So, login silently fails and it is hard to track down why.
Some possible solutions:
1. don't send it with Secure flag - maybe make this a con...The cookie is sent with the Secure flag, so it doesn't work if not using https (e.g. testing). So, login silently fails and it is hard to track down why.
Some possible solutions:
1. don't send it with Secure flag - maybe make this a config option for the admin
2. if the login form is POSTed over http, catch this and display an error
3. or just document in the README that TLS is required.
Thanks!
-davidchttps://git.cccv.de/uffd/uffd/-/issues/108Admin API2021-12-04T14:15:04ZJulianAdmin APIWith v2 (i.e. without LDAP) we can no longer create groups via Ansible with `ldap_entry`. We currently use a custom Ansible module for creating roles that imports the DB models and directly writes to the DB. A proper API for all of that ...With v2 (i.e. without LDAP) we can no longer create groups via Ansible with `ldap_entry`. We currently use a custom Ansible module for creating roles that imports the DB models and directly writes to the DB. A proper API for all of that would be cleaner and more stable.
* CLI command to manage API credentials
* CRUD-like REST-API for
* User (+ role memberships)
* Group
* Role (+ included role and group)
* OAuth2Client (#107)
* APIClient (#107)
* Service (#107)
* Loginname blocklist entries (#107)
* Simplistic access control for now (either full permissions or none), maybe more powerful ACLs later
Depends on #107v2.x.xhttps://git.cccv.de/uffd/uffd/-/issues/129Fix migration to support Postgres database2021-12-03T02:27:16Zc-timFix migration to support Postgres databaseCurrently the `constraint name fixes` migration does not run with the `postgresql+psycopg2` driver.
Instead it outputs
```
INFO [alembic.runtime.migration] Running upgrade 5a07d4a63b64 -> cbca20cf64d9, constraint name fixes
Traceback (...Currently the `constraint name fixes` migration does not run with the `postgresql+psycopg2` driver.
Instead it outputs
```
INFO [alembic.runtime.migration] Running upgrade 5a07d4a63b64 -> cbca20cf64d9, constraint name fixes
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/base.py", line 1276, in _execute_context
self.dialect.do_execute(
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/default.py", line 609, in do_execute
cursor.execute(statement, parameters)
psycopg2.errors.UndefinedObject: constraint "ck_invite_used" of relation "invite" does not exist
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/bin/flask", line 33, in <module>
sys.exit(load_entry_point('Flask==1.1.2', 'console_scripts', 'flask')())
File "/usr/lib/python3/dist-packages/flask/cli.py", line 967, in main
cli.main(args=sys.argv[1:], prog_name="python -m flask" if as_module else None)
File "/usr/lib/python3/dist-packages/flask/cli.py", line 586, in main
return super(FlaskGroup, self).main(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/core.py", line 782, in main
rv = self.invoke(ctx)
File "/usr/lib/python3/dist-packages/click/core.py", line 1259, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 1259, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 1066, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/usr/lib/python3/dist-packages/click/core.py", line 610, in invoke
return callback(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/decorators.py", line 21, in new_func
return f(get_current_context(), *args, **kwargs)
File "/usr/lib/python3/dist-packages/flask/cli.py", line 426, in decorator
return __ctx.invoke(f, *args, **kwargs)
File "/usr/lib/python3/dist-packages/click/core.py", line 610, in invoke
return callback(*args, **kwargs)
File "/usr/lib/python3/dist-packages/flask_migrate/cli.py", line 136, in upgrade
_upgrade(directory, revision, sql, tag, x_arg)
File "/usr/lib/python3/dist-packages/flask_migrate/__init__.py", line 96, in wrapped
f(*args, **kwargs)
File "/usr/lib/python3/dist-packages/flask_migrate/__init__.py", line 271, in upgrade
command.upgrade(config, revision, sql=sql, tag=tag)
File "/usr/lib/python3/dist-packages/alembic/command.py", line 298, in upgrade
script.run_env()
File "/usr/lib/python3/dist-packages/alembic/script/base.py", line 489, in run_env
util.load_python_file(self.dir, "env.py")
File "/usr/lib/python3/dist-packages/alembic/util/pyfiles.py", line 98, in load_python_file
module = load_module_py(module_id, path)
File "/usr/lib/python3/dist-packages/alembic/util/compat.py", line 184, in load_module_py
spec.loader.exec_module(module)
File "<frozen importlib._bootstrap_external>", line 790, in exec_module
File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
File "/usr/share/uffd/uffd/migrations/env.py", line 87, in <module>
run_migrations_online()
File "/usr/share/uffd/uffd/migrations/env.py", line 80, in run_migrations_online
context.run_migrations()
File "<string>", line 8, in run_migrations
File "/usr/lib/python3/dist-packages/alembic/runtime/environment.py", line 846, in run_migrations
self.get_context().run_migrations(**kw)
File "/usr/lib/python3/dist-packages/alembic/runtime/migration.py", line 522, in run_migrations
step.migration_fn(**kw)
File "/usr/share/uffd/uffd/migrations/versions/cbca20cf64d9_constraint_name_fixes.py", line 90, in upgrade
pass
File "/usr/lib/python3.9/contextlib.py", line 124, in __exit__
next(self.gen)
File "/usr/lib/python3/dist-packages/alembic/operations/base.py", line 353, in batch_alter_table
impl.flush()
File "/usr/lib/python3/dist-packages/alembic/operations/batch.py", line 116, in flush
batch_impl._create(self.impl)
File "/usr/lib/python3/dist-packages/alembic/operations/batch.py", line 357, in _create
op_impl.prep_table_for_batch(self.table)
File "/usr/lib/python3/dist-packages/alembic/ddl/postgresql.py", line 48, in prep_table_for_batch
self.drop_constraint(constraint)
File "/usr/lib/python3/dist-packages/alembic/ddl/impl.py", line 248, in drop_constraint
self._exec(schema.DropConstraint(const))
File "/usr/lib/python3/dist-packages/alembic/ddl/impl.py", line 141, in _exec
return conn.execute(construct, *multiparams, **params)
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/base.py", line 1011, in execute
return meth(self, multiparams, params)
File "/usr/lib/python3/dist-packages/sqlalchemy/sql/ddl.py", line 72, in _execute_on_connection
return connection._execute_ddl(self, multiparams, params)
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/base.py", line 1068, in _execute_ddl
ret = self._execute_context(
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/base.py", line 1316, in _execute_context
self._handle_dbapi_exception(
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/base.py", line 1510, in _handle_dbapi_exception
util.raise_(
File "/usr/lib/python3/dist-packages/sqlalchemy/util/compat.py", line 182, in raise_
raise exception
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/base.py", line 1276, in _execute_context
self.dialect.do_execute(
File "/usr/lib/python3/dist-packages/sqlalchemy/engine/default.py", line 609, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedObject) constraint "ck_invite_used" of relation "invite" does not exist
[SQL: ALTER TABLE invite DROP CONSTRAINT ck_invite_used]
(Background on this error at: http://sqlalche.me/e/13/f405)
```
while the database outputs
```
2021-10-09 00:34:04.819 UTC [65] ERROR: constraint "ck_invite_used" of relation "invite" does not exist
2021-10-09 00:34:04.819 UTC [65] STATEMENT: ALTER TABLE invite DROP CONSTRAINT ck_invite_used
```https://git.cccv.de/uffd/uffd/-/issues/136Device login with full SSO session2021-12-03T01:55:49ZJulianDevice login with full SSO sessionCurrently device login only allows authenticating with an OAuth2 service. It cannot be used to get a full SSO session that allows changing e.g. the password or logging into multiple services.
Since 2FA is sometimes difficult to use on m...Currently device login only allows authenticating with an OAuth2 service. It cannot be used to get a full SSO session that allows changing e.g. the password or logging into multiple services.
Since 2FA is sometimes difficult to use on mobile device, having the option to get a full session would be nice.
Split off #135https://git.cccv.de/uffd/uffd/-/issues/32Single-Log-Out support2021-10-03T15:33:02ZJulianSingle-Log-Out supportAfter a user logged into multiple services using uffd as a SSO identity provider, he might eventually click the logout button in one of the services or uffd. The users probably expects to be logged out of uffd as well as all services he ...After a user logged into multiple services using uffd as a SSO identity provider, he might eventually click the logout button in one of the services or uffd. The users probably expects to be logged out of uffd as well as all services he logged in to.
SAML would provide a Single-Log-Out (SLO) mechanism, but OAuth2 does not. We could simply revoke all OAuth2 access tokens related to the current session, but the services will not regularly check if their access token is still valid. Other solutions provide services with a session_id attached to the access token that the IdP sends to the service in a server-to-server request on logout. This is not widely supported by services and rather complex to implement.
Probably the simplest way is to make the browser call logout urls for all services.
Logout-URLs for different services:
* Gitlab: POST-Request to /users/sign_out
* Nextcloud: /logout requires CSRF token. Doesn't seem to be possible directly. Maybe implement cookie-clearing in oauthproxy.
* Dokuwiki: GET-Request to /start?do=logouthttps://git.cccv.de/uffd/uffd/-/issues/124Consistent variable/argument/route parameter naming for database ids2021-10-01T16:49:47ZJulianConsistent variable/argument/route parameter naming for database idsWe sometimes use `id` (which collides with a builtin), names like `roleid` (for values of `Role.id`) and in other cases names like `invide_id` (for values of `Invite.id`). We should decide on one scheme and apply it consistently.
Afterw...We sometimes use `id` (which collides with a builtin), names like `roleid` (for values of `Role.id`) and in other cases names like `invide_id` (for values of `Invite.id`). We should decide on one scheme and apply it consistently.
Afterwards: Reenable pylints `redefined-builtin` check.https://git.cccv.de/uffd/uffd/-/issues/123Configurable mail verification and password reset link lifetime2021-10-01T16:44:03ZJulianConfigurable mail verification and password reset link lifetimeCurrently the lifetime of mail verification (signup, changing mail address in selfservice) and password reset links is hard-coded to 48 hours. This should be configurable and consistent.Currently the lifetime of mail verification (signup, changing mail address in selfservice) and password reset links is hard-coded to 48 hours. This should be configurable and consistent.