Browse Source

Added a hierarchical Role system and lookup for the authentication.

merge-requests/1/head
Drew Short 7 years ago
parent
commit
828ee35096
  1. BIN
      server/.admin_credentials.swp
  2. 3
      server/atheneum/api/user_api.py
  3. 7
      server/atheneum/middleware/authentication_middleware.py
  4. 7
      server/atheneum/model/user_model.py
  5. 88
      server/atheneum/service/role_service.py
  6. 7
      server/atheneum/service/user_service.py
  7. 8
      server/tests/api/test_user_api.py
  8. 19
      server/tests/service/test_role_service.py

BIN
server/.admin_credentials.swp

3
server/atheneum/api/user_api.py

@ -6,6 +6,7 @@ from atheneum.api.model import APIResponse
from atheneum.middleware import authentication_middleware from atheneum.middleware import authentication_middleware
from atheneum.model import User from atheneum.model import User
from atheneum.service import user_service, transformation_service from atheneum.service import user_service, transformation_service
from atheneum.service.role_service import Role
USER_BLUEPRINT = Blueprint( USER_BLUEPRINT = Blueprint(
name='user', import_name=__name__, url_prefix='/user') name='user', import_name=__name__, url_prefix='/user')
@ -29,7 +30,7 @@ def get_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_token_auth
@authentication_middleware.require_role(required_role=User.ROLE_ADMIN)
@authentication_middleware.require_role(required_role=Role.ADMIN)
def register_user() -> APIResponse: def register_user() -> APIResponse:
""" """
Register a user with the service. Register a user with the service.

7
server/atheneum/middleware/authentication_middleware.py

@ -13,6 +13,7 @@ from atheneum.service import (
user_service, user_service,
user_token_service user_token_service
) )
from atheneum.service.role_service import ROLES, Role
def authenticate_with_password(name: str, password: str) -> bool: def authenticate_with_password(name: str, password: str) -> bool:
@ -150,15 +151,15 @@ def require_token_auth(func: Callable) -> Callable:
return decorate return decorate
def require_role(required_role: str) -> Callable:
def require_role(required_role: Role) -> Callable:
"""Decorate require user role.""" """Decorate require user role."""
def required_role_decorator(func: Callable) -> Callable: def required_role_decorator(func: Callable) -> Callable:
"""Decorate the function.""" """Decorate the function."""
@wraps(func) @wraps(func)
def decorate(*args: list, **kwargs: dict) -> Any: def decorate(*args: list, **kwargs: dict) -> Any:
"""Require a user role.""" """Require a user role."""
if g.user.role == required_role:
if g.user.role in ROLES.find_roles_in_hierarchy(required_role):
return func(*args, **kwargs) return func(*args, **kwargs)
return authorization_failed(required_role)
return authorization_failed(required_role.value)
return decorate return decorate
return required_role_decorator return required_role_decorator

7
server/atheneum/model/user_model.py

@ -1,5 +1,8 @@
"""User related SQLALchemy models.""" """User related SQLALchemy models."""
from sqlalchemy import Enum
from atheneum.db import db from atheneum.db import db
from atheneum.service.role_service import Role
class User(db.Model): # pylint: disable=too-few-public-methods class User(db.Model): # pylint: disable=too-few-public-methods
@ -14,9 +17,9 @@ class User(db.Model): # pylint: disable=too-few-public-methods
name = db.Column(db.Unicode(60), unique=True, nullable=False) name = db.Column(db.Unicode(60), unique=True, nullable=False)
role = db.Column( role = db.Column(
'role', 'role',
db.Unicode(32),
Enum(Role),
nullable=False, nullable=False,
default=ROLE_USER, )
default=Role.USER, )
password_hash = db.Column('password_hash', db.Unicode(128), nullable=False) password_hash = db.Column('password_hash', db.Unicode(128), nullable=False)
password_revision = db.Column( password_revision = db.Column(
'password_revision', db.SmallInteger, default=0, nullable=False) 'password_revision', db.SmallInteger, default=0, nullable=False)

88
server/atheneum/service/role_service.py

@ -0,0 +1,88 @@
"""Role service for Atheneum."""
from collections import defaultdict
from enum import Enum
from typing import Optional, List, Set, Dict
class Role(Enum):
"""User role definitions."""
ADMIN = 'ADMIN'
AUDITOR = 'AUDITOR'
MODERATOR = 'MODERATOR'
USER = 'USER'
ANONYMOUS = 'ANONYMOUS'
NONE = 'NONE'
class RoleTree(defaultdict):
"""Simple tree structure to handle hierarchy."""
def __call__(self, data: Role) -> 'RoleTree':
"""Handle direct calls to the tree."""
return RoleTree(self, data)
# def __hash__(self):
def __init__(
self,
parent: Optional['RoleTree'],
data: Role,
**kwargs: dict) -> None:
"""Configure a RoleTree."""
super().__init__(**kwargs)
self.parent: Optional[RoleTree] = parent
self.data: Role = data
self.default_factory = self # type: ignore
self.roles: Dict[Role, List[RoleTree]] = {data: [self]}
def populate(
self, children: Dict[Role, Optional[dict]]) -> List['RoleTree']:
"""Populate a RoleTree from a dictionary of a Role hierarchy."""
role_list: List[RoleTree] = []
for child_role in children.keys():
element = children[child_role]
new_node = self(child_role)
if isinstance(element, dict) and element:
role_list += new_node.populate(element)
self[child_role] = new_node
role_list.append(new_node)
for role_tree in role_list:
if role_tree.data not in self.roles.keys():
self.roles[role_tree.data] = []
self.roles[role_tree.data].append(role_tree)
return role_list
def find_role(self, request_role: Role) -> List['RoleTree']:
"""Identify all instances of a role."""
try:
return [role_tree for role_tree in self.roles[request_role]]
except KeyError:
return []
def get_parent_roles(self) -> List[Role]:
"""Return all the roles from self to the highest parent."""
if self.parent is not None:
return [self.data] + self.parent.get_parent_roles()
return [self.data]
def find_roles_in_hierarchy(self, request_role: Role) -> Set[Role]:
"""Find a set of all roles that fall within the hierarchy."""
roles: List[Role] = []
role_trees = self.find_role(request_role)
for role_tree in role_trees:
roles.extend(role_tree.get_parent_roles())
return set(role for role in roles)
ROLES = RoleTree(None, Role.ADMIN)
ROLES.populate({
Role.MODERATOR: {
Role.USER: {
Role.ANONYMOUS: None
}
},
Role.AUDITOR: {
Role.USER: None
}
})

7
server/atheneum/service/user_service.py

@ -8,6 +8,7 @@ from typing import Optional, Dict, Callable, Any
from atheneum import errors from atheneum import errors
from atheneum.db import db from atheneum.db import db
from atheneum.model import User from atheneum.model import User
from atheneum.service.role_service import Role
from atheneum.service.transformation_service import ( from atheneum.service.transformation_service import (
BaseTransformer, BaseTransformer,
register_transformer register_transformer
@ -83,12 +84,12 @@ class UserTransformer(BaseTransformer):
def serialize_role(self) -> str: def serialize_role(self) -> str:
"""User role.""" """User role."""
return self.model.role
return self.model.role.value
@staticmethod @staticmethod
def deserialize_role(model: User, role: str) -> None:
def deserialize_role(model: User, role_value: str) -> None:
"""User role.""" """User role."""
model.role = role
model.role = Role(role_value)
register_transformer(User.__name__, UserTransformer) register_transformer(User.__name__, UserTransformer)

8
server/tests/api/test_user_api.py

@ -12,7 +12,7 @@ def test_get_user_happy_path(auth: AuthActions, client: FlaskClient):
headers={ headers={
auth_header[0]: auth_header[1] auth_header[0]: auth_header[1]
}) })
assert result.status_code == 200
assert 200 == result.status_code
assert result.json is not None assert result.json is not None
assert result.json['name'] == client.application.config['test_username'] assert result.json['name'] == client.application.config['test_username']
@ -29,7 +29,7 @@ def test_register_user_happy_path(auth: AuthActions, client: FlaskClient):
auth_header[0]: auth_header[1], auth_header[0]: auth_header[1],
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}) })
assert result.status_code == 200
assert 200 == result.status_code
assert result.json is not None assert result.json is not None
assert result.json['name'] == 'test_registered_user' assert result.json['name'] == 'test_registered_user'
@ -55,9 +55,9 @@ def test_register_user_twice_failure(auth: AuthActions, client: FlaskClient):
auth_header[0]: auth_header[1], auth_header[0]: auth_header[1],
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}) })
assert result1.status_code == 200
assert 200 == result1.status_code
assert result1.json is not None assert result1.json is not None
assert result1.json['name'] == 'test_registered_user' assert result1.json['name'] == 'test_registered_user'
assert result2.status_code == 400
assert 400 == result2.status_code
assert result2.json is not None assert result2.json is not None
assert result2.json['message'] == 'User name is already taken.' assert result2.json['message'] == 'User name is already taken.'

19
server/tests/service/test_role_service.py

@ -0,0 +1,19 @@
from atheneum.service.role_service import ROLES, Role
def test_role_tree_find_roles_in_hierarchy():
roles = ROLES.find_roles_in_hierarchy(Role.USER)
assert len(roles) == 4
assert Role.USER in roles
assert Role.MODERATOR in roles
assert Role.AUDITOR in roles
assert Role.ADMIN in roles
roles = ROLES.find_roles_in_hierarchy(Role.AUDITOR)
assert len(roles) == 2
assert Role.AUDITOR in roles
assert Role.ADMIN in roles
def test_role_tree_find_role_key_error():
assert len(ROLES.find_role(Role.NONE)) == 0
Loading…
Cancel
Save