Browse Source

Added a hierarchical Role system and lookup for the authentication.

merge-requests/1/head
Drew Short 6 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.model import User
from atheneum.service import user_service, transformation_service
from atheneum.service.role_service import Role
USER_BLUEPRINT = Blueprint(
name='user', import_name=__name__, url_prefix='/user')
@ -29,7 +30,7 @@ def get_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('/', methods=['POST'])
@return_json
@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:
"""
Register a user with the service.

7
server/atheneum/middleware/authentication_middleware.py

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

7
server/atheneum/model/user_model.py

@ -1,5 +1,8 @@
"""User related SQLALchemy models."""
from sqlalchemy import Enum
from atheneum.db import db
from atheneum.service.role_service import Role
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)
role = db.Column(
'role',
db.Unicode(32),
Enum(Role),
nullable=False,
default=ROLE_USER, )
default=Role.USER, )
password_hash = db.Column('password_hash', db.Unicode(128), nullable=False)
password_revision = db.Column(
'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.db import db
from atheneum.model import User
from atheneum.service.role_service import Role
from atheneum.service.transformation_service import (
BaseTransformer,
register_transformer
@ -83,12 +84,12 @@ class UserTransformer(BaseTransformer):
def serialize_role(self) -> str:
"""User role."""
return self.model.role
return self.model.role.value
@staticmethod
def deserialize_role(model: User, role: str) -> None:
def deserialize_role(model: User, role_value: str) -> None:
"""User role."""
model.role = role
model.role = Role(role_value)
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={
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['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],
'Content-Type': 'application/json'
})
assert result.status_code == 200
assert 200 == result.status_code
assert result.json is not None
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],
'Content-Type': 'application/json'
})
assert result1.status_code == 200
assert 200 == result1.status_code
assert result1.json is not None
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['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