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. 63
      server/corvus/middleware/authentication_middleware.py
  4. 3
      server/corvus/service/transformation_service.py
  5. 25
      server/corvus/service/user_token_service.py

53
server/corvus/api/authentication_api.py

@ -1,5 +1,5 @@
"""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.model import APIMessage, APIResponse
@ -7,8 +7,12 @@ from corvus.middleware import authentication_middleware
from corvus.service import (
user_token_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(
name='auth', import_name=__name__, url_prefix='/auth')
@ -16,7 +20,7 @@ AUTH_BLUEPRINT = Blueprint(
@AUTH_BLUEPRINT.route('/login', methods=['POST'])
@return_json
@authentication_middleware.require_basic_auth
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def login() -> APIResponse:
"""
Get a token for continued authentication.
@ -29,7 +33,7 @@ def login() -> APIResponse:
@AUTH_BLUEPRINT.route('/bump', methods=['POST'])
@return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def login_bump() -> APIResponse:
"""
Update the user last seen timestamp.
@ -42,7 +46,7 @@ def login_bump() -> APIResponse:
@AUTH_BLUEPRINT.route('/logout', methods=['POST'])
@return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require(required_auth=Auth.BASIC, required_role=Role.USER)
def logout() -> APIResponse:
"""
Logout and delete a token.
@ -51,3 +55,42 @@ def logout() -> APIResponse:
"""
authentication_service.logout(g.user_token)
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.model import APIResponse, APIMessage, APIPage
from corvus.middleware import authentication_middleware
from corvus.middleware.authentication_middleware import Auth
from corvus.model import User
from corvus.service import (
patch_service,
@ -21,8 +22,7 @@ USER_BLUEPRINT = Blueprint(
@USER_BLUEPRINT.route('', methods=['GET'])
@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:
"""
Get a list of users.
@ -38,8 +38,7 @@ def get_users() -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['GET'])
@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:
"""
Get a user.
@ -54,8 +53,7 @@ def get_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['PATCH'])
@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:
"""
Patch a user.
@ -74,8 +72,7 @@ def patch_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('', methods=['POST'])
@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:
"""
Register a user with the service.
@ -99,8 +96,7 @@ def register_user() -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['DELETE'])
@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:
"""
Delete a user with the service.

63
server/corvus/middleware/authentication_middleware.py

@ -1,6 +1,7 @@
"""Middleware to handle authentication."""
import base64
import binascii
from enum import Enum
from functools import wraps
from typing import Optional, Callable, Any
@ -8,6 +9,7 @@ from flask import request, Response, g, json
from werkzeug.datastructures import Authorization
from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes
from corvus.api.model import APIMessage
from corvus.service import (
authentication_service,
user_service,
@ -16,6 +18,14 @@ from corvus.service import (
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:
"""
Authenticate a username and a password.
@ -86,19 +96,13 @@ def parse_token_header(
"""
if not header_value:
return None
value = wsgi_to_bytes(header_value)
auth_info = wsgi_to_bytes(header_value)
try:
auth_type, auth_info = value.split(None, 1)
auth_type = auth_type.lower()
except ValueError:
username, token = base64.b64decode(auth_info).split(b':', 1)
except binascii.Error:
return None
if auth_type == b'token':
try:
username, token = base64.b64decode(auth_info).split(b':', 1)
except binascii.Error:
return None
return Authorization('token', {'username': bytes_to_wsgi(username),
'password': bytes_to_wsgi(token)})
return Authorization('token', {'username': bytes_to_wsgi(username),
'password': bytes_to_wsgi(token)})
return None
@ -143,7 +147,7 @@ def require_token_auth(func: Callable) -> Callable:
:return:
"""
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):
return func(*args, **kwargs)
return authentication_failed('Token')
@ -152,7 +156,12 @@ def require_token_auth(func: Callable) -> 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:
"""Decorate the function."""
@wraps(func)
@ -163,3 +172,31 @@ def require_role(required_role: Role) -> Callable:
return authorization_failed(required_role.value)
return decorate
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:
factory(self.model, value)
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
def _serializers(self) -> Dict[str, Callable[[], Any]]:

25
server/corvus/service/user_token_service.py

@ -1,9 +1,9 @@
"""Service to handle user_token operations."""
import uuid
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.model import User, UserToken
@ -11,6 +11,7 @@ from corvus.service.transformation_service import (
BaseTransformer,
register_transformer
)
from corvus import errors
@register_transformer
@ -79,7 +80,10 @@ class UserTokenTransformer(BaseTransformer):
def deserialize_expiration_time(
model: UserToken, expiration_time: datetime) -> None:
"""User token expiration time."""
model.expiration_time = iso8601.parse_date(expiration_time)
try:
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:
"""User token creation time."""
@ -89,7 +93,10 @@ class UserTokenTransformer(BaseTransformer):
def deserialize_creation_time(
model: UserToken, creation_time: datetime) -> None:
"""User token creation time."""
model.creation_time = iso8601.parse_date(creation_time)
try:
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:
"""User token last usage time."""
@ -177,3 +184,13 @@ def find_by_user_and_token(user: User, token: str) -> Optional[UserToken]:
:return:
"""
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