From 1450fa596d2d8c9de74d98d3436ca0b0126f42d1 Mon Sep 17 00:00:00 2001 From: Drew Short Date: Mon, 7 Oct 2019 15:54:07 -0500 Subject: [PATCH] Refactor: Moved token validation, made token list a page, documentation --- server/corvus/api/authentication_api.py | 40 ++++++++++++++++--- .../middleware/authentication_middleware.py | 2 +- .../corvus/service/authentication_service.py | 21 ---------- server/corvus/service/user_token_service.py | 34 ++++++++++++++-- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/server/corvus/api/authentication_api.py b/server/corvus/api/authentication_api.py index 64cd454..ec565c1 100644 --- a/server/corvus/api/authentication_api.py +++ b/server/corvus/api/authentication_api.py @@ -2,7 +2,7 @@ from flask import Blueprint, g, abort, request from corvus.api.decorators import return_json -from corvus.api.model import APIMessage, APIResponse +from corvus.api.model import APIMessage, APIResponse, APIPage from corvus.middleware import authentication_middleware from corvus.service import ( user_token_service, @@ -13,6 +13,7 @@ from corvus.service import ( from corvus.middleware.authentication_middleware import Auth from corvus.service.role_service import Role from corvus.model import UserToken +from corvus.utility.pagination_utility import get_pagination_params AUTH_BLUEPRINT = Blueprint( name='auth', import_name=__name__, url_prefix='/auth') @@ -61,17 +62,34 @@ def logout() -> APIResponse: @return_json @authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER) def get_tokens() -> APIResponse: - user_tokens = user_token_service.find_by_user(g.user) - return APIResponse(user_tokens, 200) + """ + Get a list of all tokens for the current user + + :return: a paginated list of user tokens + """ + page, per_page = get_pagination_params(request.args) + user_token_page = user_token_service.find_by_user(g.user, page, per_page) + if user_token_page is not None: + return APIResponse(APIPage.from_page(user_token_page), 200) + return abort(404) @AUTH_BLUEPRINT.route('/token', methods=['POST']) @return_json @authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER) def create_token(): + """ + Create a new token with optional parameters + note: String + enabled: Boolean + expirationTime: DateTime + + :return: The new token with the optional parameters + """ requested_token: UserToken = transformation_service.deserialize_model( UserToken, request.json, options=['note', 'enabled', 'expirationTime']) - user_token = user_token_service.create(g.user, requested_token.note, requested_token.enabled, requested_token.expiration_time) + user_token = user_token_service.create( + g.user, requested_token.note, requested_token.enabled, requested_token.expiration_time) return APIResponse(user_token, 200) @@ -79,6 +97,12 @@ def create_token(): @return_json @authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER) def get_token(token: str): + """ + Retrieve a specific token for this user + + :param token: The token to retrieve for this user + :return: The token if it exists + """ user_token = user_token_service.find_by_user_and_token(g.user, token) if user_token is None: return abort(404) @@ -89,8 +113,14 @@ def get_token(token: str): @return_json @authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER) def delete_token(token: str): + """ + Delete a specific token for this user + + :param token: The token to delete for this user + :return: Nothing on success + """ user_token = user_token_service.find_by_user_and_token(g.user, token) if user_token is None: return abort(404) user_token_service.delete(user_token) - return APIResponse(None, 200) + return APIResponse(APIMessage(True, None), 200) diff --git a/server/corvus/middleware/authentication_middleware.py b/server/corvus/middleware/authentication_middleware.py index 29ef868..7419823 100644 --- a/server/corvus/middleware/authentication_middleware.py +++ b/server/corvus/middleware/authentication_middleware.py @@ -54,7 +54,7 @@ def authenticate_with_token(name: str, token: str) -> bool: if user is not None: user_token = user_token_service.find_by_user_and_token(user, token) if user is not None \ - and authentication_service.is_valid_token(user_token): + and user_token_service.is_valid_token(user_token): g.user = user g.user_token = user_token return True diff --git a/server/corvus/service/authentication_service.py b/server/corvus/service/authentication_service.py index 55b1487..78c2c8b 100644 --- a/server/corvus/service/authentication_service.py +++ b/server/corvus/service/authentication_service.py @@ -1,6 +1,5 @@ """Service to handle authentication.""" import re -from datetime import datetime from typing import Optional from nacl import pwhash @@ -53,26 +52,6 @@ def is_valid_password(user: User, password: str) -> bool: return False -def is_valid_token(user_token: Optional[UserToken]) -> bool: - """ - Validate a token. - - Token must be enabled and if it has an expiration, it must be greater - than now. - - :param user_token: - :return: - """ - if user_token is None: - return False - if not user_token.enabled: - return False - if (user_token.expiration_time is not None - and user_token.expiration_time < datetime.utcnow()): - return False - return True - - def logout(user_token: Optional[UserToken] = None) -> None: """ Remove a user_token associated with a client session. diff --git a/server/corvus/service/user_token_service.py b/server/corvus/service/user_token_service.py index 72aee9b..17aaa45 100644 --- a/server/corvus/service/user_token_service.py +++ b/server/corvus/service/user_token_service.py @@ -3,6 +3,7 @@ import uuid from datetime import datetime from typing import Optional, Dict, Callable, Any, List +from flask_sqlalchemy import Pagination from iso8601 import iso8601, ParseError from corvus.db import db @@ -42,6 +43,7 @@ class UserTokenTransformer(BaseTransformer): 'expirationTime': self.serialize_expiration_time, 'creationTime': self.serialize_creation_time, 'lastUsageTime': self.serialize_last_usage_time, + 'isValid': self.serialize_is_valid, 'version': self.serialize_version } @@ -108,6 +110,9 @@ class UserTokenTransformer(BaseTransformer): """User token last usage time.""" model.last_usage_time = iso8601.parse_date(last_usage_time) + def serialize_is_valid(self): + return is_valid_token(self.model) + def serialize_version(self) -> int: """User token version.""" return self.model.version @@ -161,6 +166,26 @@ def create( return user_token +def is_valid_token(user_token: Optional[UserToken]) -> bool: + """ + Validate a token. + + Token must be enabled and if it has an expiration, it must be greater + than now. + + :param user_token: + :return: + """ + if user_token is None: + return False + if not user_token.enabled: + return False + if (user_token.expiration_time is not None + and user_token.expiration_time < datetime.utcnow()): + return False + return True + + def delete(user_token: UserToken) -> bool: """ Delete a user_token. @@ -186,11 +211,14 @@ def find_by_user_and_token(user: User, token: str) -> Optional[UserToken]: return UserToken.query.filter_by(user_id=user.id, token=token).first() -def find_by_user(user: User) -> List[UserToken]: +def find_by_user(user: User, page: int, per_page: int = 20, max_per_page: int = 100) -> Pagination: """ Find all tokens for a user - :param user: + :param user: The user to find tokens for + :param page: The page to request + :param per_page: The number to get per page + :param max_per_page: :return: """ - return UserToken.query.filter_by(user_id=user.id) + return UserToken.query.filter_by(user_id=user.id).paginate(page, per_page, True, max_per_page)