From d020cee00edd785dc6fb12da4efde825032a5382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Fri, 23 Oct 2020 11:31:16 +0200 Subject: [PATCH] Added a command to clean tokens and codes. Fixes #17 --- README.md | 9 +++++++ canaille/__init__.py | 20 ++++++++++----- canaille/commands.py | 22 ++++++++++++++++ canaille/models.py | 7 +++++ setup.cfg | 1 + tests/test_command_clean.py | 51 +++++++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 canaille/commands.py create mode 100644 tests/test_command_clean.py diff --git a/README.md b/README.md index ebed9970..b31da777 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,15 @@ pip install gunicorn gunicorn "canaille:create_app()" ``` +## Recurrent jobs + +You might want to clean up your database to avoid it growing too much. You can regularly delete +expired tokens and authorization codes with: + +``` + flask clean +``` + ## Contribute Contributions are welcome! diff --git a/canaille/__init__.py b/canaille/__init__.py index 6306934d..82d3bac5 100644 --- a/canaille/__init__.py +++ b/canaille/__init__.py @@ -7,6 +7,7 @@ import canaille.admin.tokens import canaille.admin.authorizations import canaille.admin.clients import canaille.consents +import canaille.commands import canaille.oauth import canaille.account import canaille.tokens @@ -106,6 +107,16 @@ def setup_ldap_tree(app): conn.unbind_s() +def setup_ldap(app): + g.ldap = ldap.initialize(app.config["LDAP"]["URI"]) + g.ldap.simple_bind_s(app.config["LDAP"]["BIND_DN"], app.config["LDAP"]["BIND_PW"]) + + +def teardown_ldap(app): + if "ldap" in g: + g.ldap.unbind_s() + + def setup_app(app): if SENTRY and app.config.get("SENTRY_DSN"): sentry_sdk.init(dsn=app.config["SENTRY_DSN"], integrations=[FlaskIntegration()]) @@ -123,6 +134,7 @@ def setup_app(app): setup_ldap_tree(app) app.register_blueprint(canaille.account.bp) app.register_blueprint(canaille.oauth.bp, url_prefix="/oauth") + app.register_blueprint(canaille.commands.bp) app.register_blueprint(canaille.consents.bp, url_prefix="/consent") app.register_blueprint(canaille.tokens.bp, url_prefix="/token") app.register_blueprint(canaille.well_known.bp, url_prefix="/.well-known") @@ -153,15 +165,11 @@ def setup_app(app): @app.before_request def before_request(): - g.ldap = ldap.initialize(app.config["LDAP"]["URI"]) - g.ldap.simple_bind_s( - app.config["LDAP"]["BIND_DN"], app.config["LDAP"]["BIND_PW"] - ) + setup_ldap(app) @app.after_request def after_request(response): - if "ldap" in g: - g.ldap.unbind_s() + teardown_ldap(app) return response @app.context_processor diff --git a/canaille/commands.py b/canaille/commands.py new file mode 100644 index 00000000..8323fa24 --- /dev/null +++ b/canaille/commands.py @@ -0,0 +1,22 @@ +from flask import Blueprint, current_app +from canaille.models import AuthorizationCode, Token + + +bp = Blueprint("commands", __name__) + + +@bp.cli.command("clean") +def clean(): + from canaille import setup_ldap + + setup_ldap(current_app) + + for t in Token.filter(): + if t.is_expired(): + t.delete() + + for a in AuthorizationCode.filter(): + if a.is_expired(): + a.delete() + + teardown_ldap() diff --git a/canaille/models.py b/canaille/models.py index d40fcf0d..472b9be4 100644 --- a/canaille/models.py +++ b/canaille/models.py @@ -234,6 +234,13 @@ class Token(LDAPObject, TokenMixin): return self.expire_date >= datetime.datetime.now() + def is_expired(self): + return ( + datetime.datetime.strptime(self.oauthIssueDate, "%Y%m%d%H%M%SZ") + + datetime.timedelta(seconds=int(self.oauthTokenLifetime)) + < datetime.datetime.now() + ) + class Consent(LDAPObject): object_class = ["oauthConsent"] diff --git a/setup.cfg b/setup.cfg index f4fefae2..b2b38dba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ include_package_data = true python_requires = >= 3.6 install_requires = authlib + click email_validator flask flask-babel diff --git a/tests/test_command_clean.py b/tests/test_command_clean.py new file mode 100644 index 00000000..27b52d6d --- /dev/null +++ b/tests/test_command_clean.py @@ -0,0 +1,51 @@ +import datetime +from canaille.commands import clean +from canaille.models import AuthorizationCode, Token +from werkzeug.security import gen_salt + + +def test_clean_command(testclient, slapd_connection, client, user): + AuthorizationCode.ocs_by_name(slapd_connection) + code = AuthorizationCode( + oauthCode="my-code", + oauthClient=client.dn, + oauthSubject=user.dn, + oauthRedirectURI="https://foo.bar/callback", + oauthResponseType="code", + oauthScope="openid profile", + oauthNonce="nonce", + oauthAuthorizationDate=( + datetime.datetime.now() - datetime.timedelta(days=1) + ).strftime("%Y%m%d%H%M%SZ"), + oauthAuthorizationLifetime="3600", + oauthCodeChallenge="challenge", + oauthCodeChallengeMethod="method", + oauthRevokation="", + ) + code.save(slapd_connection) + + Token.ocs_by_name(slapd_connection) + token = Token( + oauthAccessToken="my-token", + oauthClient=client.dn, + oauthSubject=user.dn, + oauthTokenType=None, + oauthRefreshToken=gen_salt(48), + oauthScope="openid profile", + oauthIssueDate=(datetime.datetime.now() - datetime.timedelta(days=1)).strftime( + "%Y%m%d%H%M%SZ" + ), + oauthTokenLifetime=str(3600), + ) + token.save(slapd_connection) + + assert AuthorizationCode.get("my-code", conn=slapd_connection) + assert Token.get("my-token", conn=slapd_connection) + assert code.is_expired() + assert token.is_expired() + + runner = testclient.app.test_cli_runner() + res = runner.invoke(clean) + + assert not AuthorizationCode.get("my-code", conn=slapd_connection) + assert not Token.get("my-token", conn=slapd_connection)