CLI - add command to clean blacklisted tokens
This commit is contained in:
parent
e39fc3d211
commit
9b377c08e4
@ -65,6 +65,23 @@ Remove tokens expired for more than provided number of days
|
|||||||
Users
|
Users
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
|
``ftcli users clean_tokens``
|
||||||
|
""""""""""""""""""""""""""""
|
||||||
|
.. versionadded:: 0.7.0
|
||||||
|
|
||||||
|
Remove blacklisted tokens expired for more than provided number of days.
|
||||||
|
|
||||||
|
.. cssclass:: table-bordered
|
||||||
|
.. list-table::
|
||||||
|
:widths: 25 50
|
||||||
|
:header-rows: 1
|
||||||
|
|
||||||
|
* - Options
|
||||||
|
- Description
|
||||||
|
* - ``--days``
|
||||||
|
- Number of days.
|
||||||
|
|
||||||
|
|
||||||
``ftcli users update``
|
``ftcli users update``
|
||||||
""""""""""""""""""""""
|
""""""""""""""""""""""
|
||||||
.. versionadded:: 0.6.5
|
.. versionadded:: 0.6.5
|
||||||
|
@ -71,6 +71,7 @@ def upgrade():
|
|||||||
op.create_table('blacklisted_tokens',
|
op.create_table('blacklisted_tokens',
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('token', sa.String(length=500), nullable=False),
|
sa.Column('token', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('expired_at', sa.Integer(), nullable=False),
|
||||||
sa.Column('blacklisted_on', sa.DateTime(), nullable=False),
|
sa.Column('blacklisted_on', sa.DateTime(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('token')
|
sa.UniqueConstraint('token')
|
@ -1,13 +1,9 @@
|
|||||||
import time
|
from fittrackee.utils import clean
|
||||||
|
|
||||||
from fittrackee import db
|
|
||||||
|
|
||||||
|
|
||||||
def clean_tokens(days: int) -> int:
|
def clean_tokens(days: int) -> int:
|
||||||
limit = int(time.time()) - (days * 86400)
|
|
||||||
sql = """
|
sql = """
|
||||||
DELETE FROM oauth2_token
|
DELETE FROM oauth2_token
|
||||||
WHERE oauth2_token.issued_at + oauth2_token.expires_in < %(limit)s;
|
WHERE oauth2_token.issued_at + oauth2_token.expires_in < %(limit)s;
|
||||||
"""
|
"""
|
||||||
result = db.engine.execute(sql, {'limit': limit})
|
return clean(sql, days)
|
||||||
return result.rowcount
|
|
||||||
|
@ -7,7 +7,7 @@ from fittrackee.cli.app import app
|
|||||||
from .clean import clean_tokens
|
from .clean import clean_tokens
|
||||||
|
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
logger = logging.getLogger('fittrackee_clean_tokens')
|
logger = logging.getLogger('fittrackee_clean_oauth2_tokens')
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from random import randint
|
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
@ -18,7 +17,12 @@ from .custom_asserts import (
|
|||||||
assert_errored_response,
|
assert_errored_response,
|
||||||
assert_oauth_errored_response,
|
assert_oauth_errored_response,
|
||||||
)
|
)
|
||||||
from .utils import TEST_OAUTH_CLIENT_METADATA, random_email, random_string
|
from .utils import (
|
||||||
|
TEST_OAUTH_CLIENT_METADATA,
|
||||||
|
random_email,
|
||||||
|
random_int,
|
||||||
|
random_string,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RandomMixin:
|
class RandomMixin:
|
||||||
@ -40,7 +44,7 @@ class RandomMixin:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_int(min_val: int = 0, max_val: int = 999999) -> int:
|
def random_int(min_val: int = 0, max_val: int = 999999) -> int:
|
||||||
return randint(min_val, max_val)
|
return random_int(min_val, max_val)
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Mixin(RandomMixin):
|
class OAuth2Mixin(RandomMixin):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import time
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict
|
from typing import Dict, Optional
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
@ -9,13 +10,12 @@ from cryptography.hazmat.primitives import serialization
|
|||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
from fittrackee import bcrypt
|
from fittrackee import bcrypt, db
|
||||||
from fittrackee.tests.utils import random_string
|
|
||||||
from fittrackee.users.exceptions import (
|
from fittrackee.users.exceptions import (
|
||||||
InvalidEmailException,
|
InvalidEmailException,
|
||||||
UserNotFoundException,
|
UserNotFoundException,
|
||||||
)
|
)
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import BlacklistedToken, User
|
||||||
from fittrackee.users.utils.admin import UserManagerService
|
from fittrackee.users.utils.admin import UserManagerService
|
||||||
from fittrackee.users.utils.controls import (
|
from fittrackee.users.utils.controls import (
|
||||||
check_password,
|
check_password,
|
||||||
@ -23,9 +23,13 @@ from fittrackee.users.utils.controls import (
|
|||||||
is_valid_email,
|
is_valid_email,
|
||||||
register_controls,
|
register_controls,
|
||||||
)
|
)
|
||||||
from fittrackee.users.utils.token import decode_user_token, get_user_token
|
from fittrackee.users.utils.token import (
|
||||||
|
clean_blacklisted_tokens,
|
||||||
|
decode_user_token,
|
||||||
|
get_user_token,
|
||||||
|
)
|
||||||
|
|
||||||
from ..utils import random_email
|
from ..utils import random_email, random_int, random_string
|
||||||
|
|
||||||
|
|
||||||
class TestUserManagerService:
|
class TestUserManagerService:
|
||||||
@ -511,3 +515,67 @@ class TestDecodeUserToken:
|
|||||||
user_id = decode_user_token(token)
|
user_id = decode_user_token(token)
|
||||||
|
|
||||||
assert user_id == expected_user_id
|
assert user_id == expected_user_id
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlacklistedTokensCleanup:
|
||||||
|
@staticmethod
|
||||||
|
def blacklisted_token(expiration_days: Optional[int] = None) -> str:
|
||||||
|
token = get_user_token(user_id=random_int())
|
||||||
|
blacklisted_token = BlacklistedToken(token=token)
|
||||||
|
if expiration_days is not None:
|
||||||
|
blacklisted_token.expired_at = int(time.time()) - (
|
||||||
|
expiration_days * 86400
|
||||||
|
)
|
||||||
|
db.session.add(blacklisted_token)
|
||||||
|
db.session.commit()
|
||||||
|
return token
|
||||||
|
|
||||||
|
def test_it_returns_0_as_count_when_no_blacklisted_token_deleted(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
count = clean_blacklisted_tokens(days=30)
|
||||||
|
|
||||||
|
assert count == 0
|
||||||
|
|
||||||
|
def test_it_does_not_delete_blacklisted_token_when_not_expired(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
token = self.blacklisted_token()
|
||||||
|
|
||||||
|
clean_blacklisted_tokens(days=10)
|
||||||
|
|
||||||
|
existing_token = BlacklistedToken.query.filter_by(token=token).first()
|
||||||
|
assert existing_token is not None
|
||||||
|
|
||||||
|
def test_it_deletes_blacklisted_token_when_expired_more_then_provided_days(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
token = self.blacklisted_token(expiration_days=40)
|
||||||
|
|
||||||
|
clean_blacklisted_tokens(days=30)
|
||||||
|
|
||||||
|
existing_token = BlacklistedToken.query.filter_by(token=token).first()
|
||||||
|
assert existing_token is None
|
||||||
|
|
||||||
|
def test_it_does_not_delete_blacklisted_token_when_expired_below_provided_days( # noqa
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
token = self.blacklisted_token(expiration_days=30)
|
||||||
|
|
||||||
|
clean_blacklisted_tokens(days=40)
|
||||||
|
|
||||||
|
existing_token = BlacklistedToken.query.filter_by(token=token).first()
|
||||||
|
assert existing_token is not None
|
||||||
|
|
||||||
|
def test_it_returns_deleted_rows_count(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
self.blacklisted_token()
|
||||||
|
for _ in range(3):
|
||||||
|
self.blacklisted_token(expiration_days=30)
|
||||||
|
|
||||||
|
count = clean_blacklisted_tokens(
|
||||||
|
days=app.config['TOKEN_EXPIRATION_DAYS']
|
||||||
|
)
|
||||||
|
|
||||||
|
assert count == 3
|
||||||
|
@ -35,6 +35,10 @@ def random_email() -> str:
|
|||||||
return random_string(suffix='@example.com')
|
return random_string(suffix='@example.com')
|
||||||
|
|
||||||
|
|
||||||
|
def random_int(min_val: int = 0, max_val: int = 999999) -> int:
|
||||||
|
return random.randint(min_val, max_val)
|
||||||
|
|
||||||
|
|
||||||
def random_short_id() -> str:
|
def random_short_id() -> str:
|
||||||
return encode_uuid(uuid4())
|
return encode_uuid(uuid4())
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@ -5,6 +6,12 @@ import click
|
|||||||
from fittrackee.cli.app import app
|
from fittrackee.cli.app import app
|
||||||
from fittrackee.users.exceptions import UserNotFoundException
|
from fittrackee.users.exceptions import UserNotFoundException
|
||||||
from fittrackee.users.utils.admin import UserManagerService
|
from fittrackee.users.utils.admin import UserManagerService
|
||||||
|
from fittrackee.users.utils.token import clean_blacklisted_tokens
|
||||||
|
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
logger = logging.getLogger('fittrackee_clean_blacklisted_tokens')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
@click.group(name='users')
|
@click.group(name='users')
|
||||||
@ -60,3 +67,16 @@ def manage_user(
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f'An error occurred: {e}', err=True)
|
click.echo(f'An error occurred: {e}', err=True)
|
||||||
|
|
||||||
|
|
||||||
|
@users_cli.command('clean_tokens')
|
||||||
|
@click.option('--days', type=int, required=True, help='Number of days.')
|
||||||
|
def clean(
|
||||||
|
days: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Clean blacklisted tokens expired for more than provided number of days.
|
||||||
|
"""
|
||||||
|
with app.app_context():
|
||||||
|
deleted_rows = clean_blacklisted_tokens(days)
|
||||||
|
logger.info(f'Blacklisted tokens deleted: {deleted_rows}.')
|
||||||
|
@ -244,11 +244,22 @@ class BlacklistedToken(BaseModel):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
token = db.Column(db.String(500), unique=True, nullable=False)
|
token = db.Column(db.String(500), unique=True, nullable=False)
|
||||||
|
expired_at = db.Column(db.Integer, nullable=False)
|
||||||
blacklisted_on = db.Column(db.DateTime, nullable=False)
|
blacklisted_on = db.Column(db.DateTime, nullable=False)
|
||||||
|
|
||||||
def __init__(self, token: str) -> None:
|
def __init__(
|
||||||
|
self, token: str, blacklisted_on: Optional[datetime] = None
|
||||||
|
) -> None:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token,
|
||||||
|
current_app.config['SECRET_KEY'],
|
||||||
|
algorithms=['HS256'],
|
||||||
|
)
|
||||||
self.token = token
|
self.token = token
|
||||||
self.blacklisted_on = datetime.utcnow()
|
self.expired_at = payload['exp']
|
||||||
|
self.blacklisted_on = (
|
||||||
|
blacklisted_on if blacklisted_on else datetime.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check(cls, auth_token: str) -> bool:
|
def check(cls, auth_token: str) -> bool:
|
||||||
|
@ -4,6 +4,8 @@ from typing import Optional
|
|||||||
import jwt
|
import jwt
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
from fittrackee.utils import clean
|
||||||
|
|
||||||
|
|
||||||
def get_user_token(
|
def get_user_token(
|
||||||
user_id: int, password_reset: Optional[bool] = False
|
user_id: int, password_reset: Optional[bool] = False
|
||||||
@ -45,3 +47,14 @@ def decode_user_token(auth_token: str) -> int:
|
|||||||
algorithms=['HS256'],
|
algorithms=['HS256'],
|
||||||
)
|
)
|
||||||
return payload['sub']
|
return payload['sub']
|
||||||
|
|
||||||
|
|
||||||
|
def clean_blacklisted_tokens(days: int) -> int:
|
||||||
|
"""
|
||||||
|
Delete blacklisted tokens expired for more than provided number of days
|
||||||
|
"""
|
||||||
|
sql = """
|
||||||
|
DELETE FROM blacklisted_tokens
|
||||||
|
WHERE blacklisted_tokens.expired_at < %(limit)s;
|
||||||
|
"""
|
||||||
|
return clean(sql, days)
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import humanize
|
import humanize
|
||||||
|
|
||||||
|
from fittrackee import db
|
||||||
|
|
||||||
|
|
||||||
def get_readable_duration(duration: int, locale: Optional[str] = None) -> str:
|
def get_readable_duration(duration: int, locale: Optional[str] = None) -> str:
|
||||||
"""
|
"""
|
||||||
@ -19,3 +22,9 @@ def get_readable_duration(duration: int, locale: Optional[str] = None) -> str:
|
|||||||
if locale != 'en':
|
if locale != 'en':
|
||||||
humanize.i18n.deactivate()
|
humanize.i18n.deactivate()
|
||||||
return readable_duration
|
return readable_duration
|
||||||
|
|
||||||
|
|
||||||
|
def clean(sql: str, days: int) -> int:
|
||||||
|
limit = int(time.time()) - (days * 86400)
|
||||||
|
result = db.engine.execute(sql, {'limit': limit})
|
||||||
|
return result.rowcount
|
||||||
|
Loading…
Reference in New Issue
Block a user