Drew Short
7 years ago
8 changed files with 264 additions and 1 deletions
-
BINserver/.admin_credentials.swp
-
1server/atheneum/__init__.py
-
2server/atheneum/api/authentication_api.py
-
0server/atheneum/middleware/__init__.py
-
84server/atheneum/middleware/authentication_middleware.py
-
52server/atheneum/service/user_token_service.py
-
4server/test_settings.py
-
122server/tests/conftest.py
@ -0,0 +1,84 @@ |
|||||
|
import base64 |
||||
|
from functools import wraps |
||||
|
from typing import Optional, Callable |
||||
|
|
||||
|
from flask import request, Response, g |
||||
|
from werkzeug.datastructures import Authorization |
||||
|
from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes |
||||
|
|
||||
|
from atheneum.service import ( |
||||
|
authentication_service, |
||||
|
user_service, |
||||
|
user_token_service |
||||
|
) |
||||
|
|
||||
|
|
||||
|
def authenticate_with_password(name: str, password: str) -> bool: |
||||
|
user = user_service.find_by_name(name) |
||||
|
if user is not None \ |
||||
|
and authentication_service.is_valid_password(user, password): |
||||
|
g.user = user |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def authenticate_with_token(name: str, token: str) -> bool: |
||||
|
user = user_service.find_by_name(name) |
||||
|
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): |
||||
|
g.user = user |
||||
|
g.user_token = user_token |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def authentication_failed(auth_type: str) -> Response: |
||||
|
return Response( |
||||
|
status=401, |
||||
|
headers={ |
||||
|
'WWW-Authenticate': '%s realm="Login Required"' % auth_type |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
def parse_token_authorization_header(header_value) -> Optional[Authorization]: |
||||
|
if not header_value: |
||||
|
return |
||||
|
value = wsgi_to_bytes(header_value) |
||||
|
try: |
||||
|
auth_type, auth_info = value.split(None, 1) |
||||
|
auth_type = auth_type.lower() |
||||
|
except ValueError: |
||||
|
return |
||||
|
if auth_type == b'token': |
||||
|
try: |
||||
|
username, token = base64.b64decode(auth_info).split(b':', 1) |
||||
|
except Exception: |
||||
|
return |
||||
|
return Authorization('token', {'username': bytes_to_wsgi(username), |
||||
|
'password': bytes_to_wsgi(token)}) |
||||
|
|
||||
|
|
||||
|
def require_basic_auth(func: Callable) -> Callable: |
||||
|
@wraps(func) |
||||
|
def decorate(*args, **kwargs): |
||||
|
auth = request.authorization |
||||
|
if auth and authenticate_with_password(auth.username, auth.password): |
||||
|
return func(*args, **kwargs) |
||||
|
else: |
||||
|
return authentication_failed('Basic') |
||||
|
|
||||
|
return decorate |
||||
|
|
||||
|
|
||||
|
def require_token_auth(func: Callable) -> Callable: |
||||
|
@wraps(func) |
||||
|
def decorate(*args, **kwargs): |
||||
|
token = parse_token_authorization_header( |
||||
|
request.headers.get('Authorization', None)) |
||||
|
if token and authenticate_with_token(token.username, token.password): |
||||
|
return func(*args, **kwargs) |
||||
|
else: |
||||
|
return authentication_failed('Bearer') |
||||
|
|
||||
|
return decorate |
@ -0,0 +1,52 @@ |
|||||
|
from datetime import datetime |
||||
|
from typing import Optional |
||||
|
|
||||
|
from atheneum import db |
||||
|
from atheneum.model import User, UserToken |
||||
|
from atheneum.service import authentication_service |
||||
|
|
||||
|
|
||||
|
def create( |
||||
|
user: User, |
||||
|
note: Optional[str] = None, |
||||
|
enabled: bool = True, |
||||
|
expiration_time: Optional[datetime] = None) -> UserToken: |
||||
|
""" |
||||
|
Create and save a UserToken |
||||
|
|
||||
|
:param user: The User object to bind the token to |
||||
|
:param note: An optional field to store additional information about a token |
||||
|
:param enabled: A boolean to indicate whether a token can be considered |
||||
|
eligible for authentication |
||||
|
:param expiration_time: An optional argument to determine when the token |
||||
|
becomes invalid as a means of authentication. Defaults to None, which means |
||||
|
no expiration |
||||
|
:return: |
||||
|
""" |
||||
|
token = authentication_service.generate_token() |
||||
|
user_token = UserToken( |
||||
|
user_id=user.id, |
||||
|
token=token.__str__(), |
||||
|
note=note, |
||||
|
enabled=enabled, |
||||
|
creation_time=datetime.now(), |
||||
|
expiration_time=expiration_time, |
||||
|
version=0) |
||||
|
|
||||
|
db.session.add(user_token) |
||||
|
db.session.commit() |
||||
|
|
||||
|
return user_token |
||||
|
|
||||
|
|
||||
|
def delete(user_token: UserToken) -> bool: |
||||
|
existing_user_token = db.session.delete(user_token) |
||||
|
if existing_user_token is None: |
||||
|
db.session.commit() |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def find_by_user_and_token(user: User, token: str) -> Optional[UserToken]: |
||||
|
return UserToken.query.filter_by(user_id=user.id, token=token).first() |
||||
|
|
@ -0,0 +1,4 @@ |
|||||
|
DEBUG = False |
||||
|
SECRET_KEY = b'\xb4\x89\x0f\x0f\xe5\x88\x97\xfe\x8d<\x0b@d\xe9\xa5\x87%' \ |
||||
|
b'\xc6\xf0@l1\xe3\x90g\xfaA.?u=s' # CHANGE ME IN REAL CONFIG |
||||
|
SQLALCHEMY_TRACK_MODIFICATIONS=False |
@ -0,0 +1,122 @@ |
|||||
|
import base64 |
||||
|
import os |
||||
|
import random |
||||
|
import string |
||||
|
import tempfile |
||||
|
from typing import Tuple, Any |
||||
|
|
||||
|
import pytest |
||||
|
from werkzeug.test import Client |
||||
|
|
||||
|
from atheneum import create_app, init_db, register_blueprints |
||||
|
from atheneum.model import User |
||||
|
from atheneum.service import user_service |
||||
|
|
||||
|
|
||||
|
def add_test_user() -> Tuple[str, str]: |
||||
|
test_username = 'test_' + ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=17)).strip() |
||||
|
test_password = ''.join( |
||||
|
random.choices(string.ascii_letters + string.digits, k=32)).strip() |
||||
|
user_service.register(test_username, test_password, User.ROLE_ADMIN) |
||||
|
return test_username, test_password |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def app(): |
||||
|
"""Create and configure a new app instance for each test.""" |
||||
|
# create a temporary file to isolate the database for each test |
||||
|
db_fd, db_path = tempfile.mkstemp() |
||||
|
# create the app with common test config |
||||
|
app = create_app({ |
||||
|
'TESTING': True, |
||||
|
'DATABASE': db_path, |
||||
|
}) |
||||
|
register_blueprints(app) |
||||
|
|
||||
|
# create the database and load test data |
||||
|
with app.app_context(): |
||||
|
init_db() |
||||
|
test_username, test_password = add_test_user() |
||||
|
app.config['test_username'] = test_username |
||||
|
app.config['test_password'] = test_password |
||||
|
# get_db().executescript(_data_sql) |
||||
|
|
||||
|
yield app |
||||
|
|
||||
|
# close and remove the temporary database |
||||
|
os.close(db_fd) |
||||
|
os.unlink(db_path) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def client(app): |
||||
|
"""A test client for the app.""" |
||||
|
return app.test_client() |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def runner(app): |
||||
|
"""A test runner for the app's Click commands.""" |
||||
|
return app.test_cli_runner() |
||||
|
|
||||
|
|
||||
|
class AuthActions(object): |
||||
|
def __init__(self, client: Client, username: str = "", password: str = ""): |
||||
|
self._client = client |
||||
|
self.username: str = username |
||||
|
self.password: str = password |
||||
|
self.token: str = "" |
||||
|
|
||||
|
def configure(self, username, password) -> Any: |
||||
|
self.username = username |
||||
|
self.password = password |
||||
|
return self |
||||
|
|
||||
|
def login(self): |
||||
|
auth_header = self.get_authorization_header_basic() |
||||
|
result = self._client.post( |
||||
|
'/auth/login', |
||||
|
headers={ |
||||
|
auth_header[0]: auth_header[1] |
||||
|
} |
||||
|
) |
||||
|
self.token = result.json['token'] |
||||
|
return result |
||||
|
|
||||
|
def bump(self): |
||||
|
auth_header = self.get_authorization_header_token() |
||||
|
return self._client.post( |
||||
|
'/auth/bump', |
||||
|
headers={ |
||||
|
auth_header[0]: auth_header[1] |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
def logout(self): |
||||
|
auth_header = self.get_authorization_header_token() |
||||
|
return self._client.post( |
||||
|
'/auth/logout', |
||||
|
headers={ |
||||
|
auth_header[0]: auth_header[1] |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
def get_authorization_header_basic(self) -> Tuple[str, str]: |
||||
|
credentials = base64.b64encode( |
||||
|
'{}:{}'.format(self.username, self.password).encode('utf8')) \ |
||||
|
.decode('utf8').strip() |
||||
|
return 'Authorization', 'Basic {}'.format(credentials) |
||||
|
|
||||
|
def get_authorization_header_token(self) -> Tuple[str, str]: |
||||
|
credentials = base64.b64encode( |
||||
|
'{}:{}'.format(self.username, self.token).encode('utf8')) \ |
||||
|
.decode('utf8').strip() |
||||
|
return 'Authorization', 'Token {}'.format(credentials) |
||||
|
|
||||
|
|
||||
|
@pytest.fixture |
||||
|
def auth(client: Client): |
||||
|
return AuthActions(client, |
||||
|
client.application.config.get('test_username'), |
||||
|
client.application.config.get('test_password')) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue