Browse Source

Refactor: Add token management endpoints and new @require annotation

merge-requests/1/head
Drew Short 5 years ago
parent
commit
a87537f168
  1. 53
      server/corvus/api/authentication_api.py
  2. 16
      server/corvus/api/user_api.py
  3. 55
      server/corvus/middleware/authentication_middleware.py
  4. 3
      server/corvus/service/transformation_service.py
  5. 21
      server/corvus/service/user_token_service.py

53
server/corvus/api/authentication_api.py

@ -1,5 +1,5 @@
"""Authentication API blueprint and endpoint definitions.""" """Authentication API blueprint and endpoint definitions."""
from flask import Blueprint, g
from flask import Blueprint, g, abort, request
from corvus.api.decorators import return_json from corvus.api.decorators import return_json
from corvus.api.model import APIMessage, APIResponse from corvus.api.model import APIMessage, APIResponse
@ -7,8 +7,12 @@ from corvus.middleware import authentication_middleware
from corvus.service import ( from corvus.service import (
user_token_service, user_token_service,
authentication_service, authentication_service,
user_service
user_service,
transformation_service
) )
from corvus.middleware.authentication_middleware import Auth
from corvus.service.role_service import Role
from corvus.model import UserToken
AUTH_BLUEPRINT = Blueprint( AUTH_BLUEPRINT = Blueprint(
name='auth', import_name=__name__, url_prefix='/auth') name='auth', import_name=__name__, url_prefix='/auth')
@ -16,7 +20,7 @@ AUTH_BLUEPRINT = Blueprint(
@AUTH_BLUEPRINT.route('/login', methods=['POST']) @AUTH_BLUEPRINT.route('/login', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_basic_auth
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def login() -> APIResponse: def login() -> APIResponse:
""" """
Get a token for continued authentication. Get a token for continued authentication.
@ -29,7 +33,7 @@ def login() -> APIResponse:
@AUTH_BLUEPRINT.route('/bump', methods=['POST']) @AUTH_BLUEPRINT.route('/bump', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def login_bump() -> APIResponse: def login_bump() -> APIResponse:
""" """
Update the user last seen timestamp. Update the user last seen timestamp.
@ -42,7 +46,7 @@ def login_bump() -> APIResponse:
@AUTH_BLUEPRINT.route('/logout', methods=['POST']) @AUTH_BLUEPRINT.route('/logout', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def logout() -> APIResponse: def logout() -> APIResponse:
""" """
Logout and delete a token. Logout and delete a token.
@ -51,3 +55,42 @@ def logout() -> APIResponse:
""" """
authentication_service.logout(g.user_token) authentication_service.logout(g.user_token)
return APIResponse(APIMessage(True, None), 200) return APIResponse(APIMessage(True, None), 200)
@AUTH_BLUEPRINT.route('/token', methods=['GET'])
@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)
@AUTH_BLUEPRINT.route('/token', methods=['POST'])
@return_json
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def create_token():
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)
return APIResponse(user_token, 200)
@AUTH_BLUEPRINT.route('/token/<token>', methods=['GET'])
@return_json
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def get_token(token: str):
user_token = user_token_service.find_by_user_and_token(g.user, token)
if user_token is None:
return abort(404)
return APIResponse(user_token, 200)
@AUTH_BLUEPRINT.route('/token/<token>', methods=['DELETE'])
@return_json
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def delete_token(token: str):
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)

16
server/corvus/api/user_api.py

@ -5,6 +5,7 @@ from flask import Blueprint, abort, request, g
from corvus.api.decorators import return_json from corvus.api.decorators import return_json
from corvus.api.model import APIResponse, APIMessage, APIPage from corvus.api.model import APIResponse, APIMessage, APIPage
from corvus.middleware import authentication_middleware from corvus.middleware import authentication_middleware
from corvus.middleware.authentication_middleware import Auth
from corvus.model import User from corvus.model import User
from corvus.service import ( from corvus.service import (
patch_service, patch_service,
@ -21,8 +22,7 @@ USER_BLUEPRINT = Blueprint(
@USER_BLUEPRINT.route('', methods=['GET']) @USER_BLUEPRINT.route('', methods=['GET'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.USER)
@authentication_middleware.require(required_auth=Auth.TOKEN, required_role=Role.USER)
def get_users() -> APIResponse: def get_users() -> APIResponse:
""" """
Get a list of users. Get a list of users.
@ -38,8 +38,7 @@ def get_users() -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['GET']) @USER_BLUEPRINT.route('/<name>', methods=['GET'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.USER)
@authentication_middleware.require(required_auth=Auth.TOKEN, required_role=Role.USER)
def get_user(name: str) -> APIResponse: def get_user(name: str) -> APIResponse:
""" """
Get a user. Get a user.
@ -54,8 +53,7 @@ def get_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['PATCH']) @USER_BLUEPRINT.route('/<name>', methods=['PATCH'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.USER)
@authentication_middleware.require(required_auth=Auth.TOKEN, required_role=Role.USER)
def patch_user(name: str) -> APIResponse: def patch_user(name: str) -> APIResponse:
""" """
Patch a user. Patch a user.
@ -74,8 +72,7 @@ def patch_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('', methods=['POST']) @USER_BLUEPRINT.route('', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.ADMIN)
@authentication_middleware.require(required_auth=Auth.TOKEN, required_role=Role.ADMIN)
def register_user() -> APIResponse: def register_user() -> APIResponse:
""" """
Register a user with the service. Register a user with the service.
@ -99,8 +96,7 @@ def register_user() -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['DELETE']) @USER_BLUEPRINT.route('/<name>', methods=['DELETE'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.ADMIN)
@authentication_middleware.require(required_auth=Auth.TOKEN, required_role=Role.ADMIN)
def delete_user(name: str) -> APIResponse: def delete_user(name: str) -> APIResponse:
""" """
Delete a user with the service. Delete a user with the service.

55
server/corvus/middleware/authentication_middleware.py

@ -1,6 +1,7 @@
"""Middleware to handle authentication.""" """Middleware to handle authentication."""
import base64 import base64
import binascii import binascii
from enum import Enum
from functools import wraps from functools import wraps
from typing import Optional, Callable, Any from typing import Optional, Callable, Any
@ -8,6 +9,7 @@ from flask import request, Response, g, json
from werkzeug.datastructures import Authorization from werkzeug.datastructures import Authorization
from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes
from corvus.api.model import APIMessage
from corvus.service import ( from corvus.service import (
authentication_service, authentication_service,
user_service, user_service,
@ -16,6 +18,14 @@ from corvus.service import (
from corvus.service.role_service import ROLES, Role from corvus.service.role_service import ROLES, Role
class Auth(Enum):
"""Authentication scheme definitions."""
TOKEN = 'TOKEN'
BASIC = 'BASIC'
NONE = 'NONE'
def authenticate_with_password(name: str, password: str) -> bool: def authenticate_with_password(name: str, password: str) -> bool:
""" """
Authenticate a username and a password. Authenticate a username and a password.
@ -86,13 +96,7 @@ def parse_token_header(
""" """
if not header_value: if not header_value:
return None return None
value = wsgi_to_bytes(header_value)
try:
auth_type, auth_info = value.split(None, 1)
auth_type = auth_type.lower()
except ValueError:
return None
if auth_type == b'token':
auth_info = wsgi_to_bytes(header_value)
try: try:
username, token = base64.b64decode(auth_info).split(b':', 1) username, token = base64.b64decode(auth_info).split(b':', 1)
except binascii.Error: except binascii.Error:
@ -143,7 +147,7 @@ def require_token_auth(func: Callable) -> Callable:
:return: :return:
""" """
token = parse_token_header( token = parse_token_header(
request.headers.get('Authorization', None))
request.headers.get('X-Auth-Token', None))
if token and authenticate_with_token(token.username, token.password): if token and authenticate_with_token(token.username, token.password):
return func(*args, **kwargs) return func(*args, **kwargs)
return authentication_failed('Token') return authentication_failed('Token')
@ -152,7 +156,12 @@ def require_token_auth(func: Callable) -> Callable:
def require_role(required_role: Role) -> Callable: def require_role(required_role: Role) -> Callable:
"""Decorate require user role."""
"""
Decorate require a user role.
:param required_role:
:return:
"""
def required_role_decorator(func: Callable) -> Callable: def required_role_decorator(func: Callable) -> Callable:
"""Decorate the function.""" """Decorate the function."""
@wraps(func) @wraps(func)
@ -163,3 +172,31 @@ def require_role(required_role: Role) -> Callable:
return authorization_failed(required_role.value) return authorization_failed(required_role.value)
return decorate return decorate
return required_role_decorator return required_role_decorator
def require(required_auth: Auth, required_role: Role) -> Callable:
"""
Decorate require Auth and Role
:param required_auth:
:param required_role:
:return:
"""
def require_decorator(func: Callable) -> Callable:
@wraps(func)
def decorate(*args: list, **kwargs: dict) -> Any:
f = require_role(required_role)(func)
if required_auth == Auth.BASIC:
f = require_basic_auth(f)
elif required_auth == Auth.TOKEN:
f = require_token_auth(f)
else:
Response(
response=APIMessage(
message="Unexpected Server Error",
success=False
),
status=500)
return f(*args, **kwargs)
return decorate
return require_decorator

3
server/corvus/service/transformation_service.py

@ -53,8 +53,7 @@ class BaseTransformer:
if value is not None: if value is not None:
factory(self.model, value) factory(self.model, value)
except KeyError as key_error: except KeyError as key_error:
LOGGER.error(
'Unable to transform field: %s %s', key, key_error)
LOGGER.error('Unable to transform field: %s %s', key, key_error)
return self.model return self.model
def _serializers(self) -> Dict[str, Callable[[], Any]]: def _serializers(self) -> Dict[str, Callable[[], Any]]:

21
server/corvus/service/user_token_service.py

@ -1,9 +1,9 @@
"""Service to handle user_token operations.""" """Service to handle user_token operations."""
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, Callable, Any
from typing import Optional, Dict, Callable, Any, List
from iso8601 import iso8601
from iso8601 import iso8601, ParseError
from corvus.db import db from corvus.db import db
from corvus.model import User, UserToken from corvus.model import User, UserToken
@ -11,6 +11,7 @@ from corvus.service.transformation_service import (
BaseTransformer, BaseTransformer,
register_transformer register_transformer
) )
from corvus import errors
@register_transformer @register_transformer
@ -79,7 +80,10 @@ class UserTokenTransformer(BaseTransformer):
def deserialize_expiration_time( def deserialize_expiration_time(
model: UserToken, expiration_time: datetime) -> None: model: UserToken, expiration_time: datetime) -> None:
"""User token expiration time.""" """User token expiration time."""
try:
model.expiration_time = iso8601.parse_date(expiration_time) model.expiration_time = iso8601.parse_date(expiration_time)
except ParseError:
raise errors.ValidationError('Cannot parse datetime from %s' % expiration_time)
def serialize_creation_time(self) -> datetime: def serialize_creation_time(self) -> datetime:
"""User token creation time.""" """User token creation time."""
@ -89,7 +93,10 @@ class UserTokenTransformer(BaseTransformer):
def deserialize_creation_time( def deserialize_creation_time(
model: UserToken, creation_time: datetime) -> None: model: UserToken, creation_time: datetime) -> None:
"""User token creation time.""" """User token creation time."""
try:
model.creation_time = iso8601.parse_date(creation_time) model.creation_time = iso8601.parse_date(creation_time)
except ParseError:
raise errors.ValidationError('Cannot parse datetime from %s' % creation_time)
def serialize_last_usage_time(self) -> datetime: def serialize_last_usage_time(self) -> datetime:
"""User token last usage time.""" """User token last usage time."""
@ -177,3 +184,13 @@ def find_by_user_and_token(user: User, token: str) -> Optional[UserToken]:
:return: :return:
""" """
return UserToken.query.filter_by(user_id=user.id, token=token).first() return UserToken.query.filter_by(user_id=user.id, token=token).first()
def find_by_user(user: User) -> List[UserToken]:
"""
Find all tokens for a user
:param user:
:return:
"""
return UserToken.query.filter_by(user_id=user.id)
Loading…
Cancel
Save