Skip to content

Commit

Permalink
Added an endpoint to make other users admin
Browse files Browse the repository at this point in the history
  • Loading branch information
terazus committed Jul 29, 2023
1 parent ed62937 commit a9d1ae2
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 9 deletions.
1 change: 1 addition & 0 deletions ptmd/api/queries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
enable_account, validate_account,
send_reset_email, reset_password,
get_users,
change_role
)
from .files import (
validate_file,
Expand Down
23 changes: 22 additions & 1 deletion ptmd/api/queries/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from json import loads

from flask import jsonify, request, Response
from flask_jwt_extended import get_jwt
from flask_jwt_extended import get_jwt, get_current_user
from sqlalchemy.exc import IntegrityError
from jsonschema import Draft4Validator as Validator

Expand Down Expand Up @@ -192,3 +192,24 @@ def reset_password(token: str) -> tuple[Response, int]:
return jsonify({"msg": "Password changed successfully"}), 200
except Exception as e:
return jsonify({"msg": str(e)}), 400


@check_role(role='admin')
def change_role(user_id: int, role: str) -> tuple[Response, int]:
""" Change the role of a user. Admin only
:param user_id: ID of the user to make admin
:param role: role to change to
:return: tuple containing a JSON response and a status code
"""
user: User = User.query.filter(User.id == user_id).first()
if not user:
return jsonify(msg="User not found"), 404
current_user: User = get_current_user()
if current_user.id == user.id:
return jsonify(msg="Cannot change your own role"), 400
try:
user.set_role(role)
return jsonify(msg=f"User {user_id} role has been changed to {role}"), 200
except ValueError:
return jsonify(msg="Invalid role"), 400
6 changes: 3 additions & 3 deletions ptmd/api/queries/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from flask_jwt_extended.exceptions import NoAuthorizationError

from ptmd.config import jwt
from ptmd.const import ROLES
from ptmd.database import User


Expand All @@ -37,7 +38,7 @@ def decorator(f):
def decorator_function(*args, **kwargs):
""" wrapper logic """
verify_jwt_in_request()
current_user = get_current_user()
current_user: User = get_current_user()
allowed: bool = is_allowed(user_role=current_user.role, level=role)
if not allowed:
raise NoAuthorizationError("You are not authorized to access this route")
Expand All @@ -54,11 +55,10 @@ def is_allowed(user_role: str, level: str = 'enabled') -> bool:
:return: True if the user is allowed to access the route, False otherwise
"""
levels: list[str] = ['banned', 'disabled', 'enabled', 'user', 'admin']
if user_role == 'admin':
return True
if user_role == 'banned':
return False
if levels.index(user_role) >= levels.index(level):
if ROLES.index(user_role) >= ROLES.index(level):
return True
return False
14 changes: 13 additions & 1 deletion ptmd/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
get_sample, get_samples,
ship_data, receive_data,
convert_to_isa,
send_reset_email, reset_password
send_reset_email, reset_password,
change_role
)
from ptmd.api.const import SWAGGER_DATA_PATH, FILES_DOC_PATH, USERS_DOC_PATH, CHEMICALS_DOC_PATH, SAMPLES_DOC_PATH

Expand Down Expand Up @@ -116,6 +117,17 @@ def reset_pwd(token: str) -> tuple[Response, int]:
return reset_password(token)


@app.route("/api/users/<user_id>/make_admin", methods=["GET"])
@jwt_required()
def make_admin_(user_id: int) -> tuple[Response, int]:
""" Route to make a user an admin. This is an admin only route
:param user_id: the id of the user to make admin
:return: the response and the status code
"""
return change_role(user_id=user_id, role='admin')


###########################################################
# CHEMICALS #
###########################################################
Expand Down
2 changes: 2 additions & 0 deletions ptmd/const/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@
EMPTY_FIELDS_VALUES
)
from .site import SITE_URL, ADMIN_EMAIL, ADMIN_USERNAME, ADMIN_PASSWORD

ROLES: list[str] = ['banned', 'disabled', 'enabled', 'user', 'admin'] # careful with the order
11 changes: 9 additions & 2 deletions ptmd/database/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from passlib.hash import bcrypt

from ptmd.config import Base, db, session
from ptmd.const import ROLES
from ptmd.database.models.token import Token
from ptmd.lib.email import send_validation_mail, send_validated_account_mail

Expand Down Expand Up @@ -101,14 +102,20 @@ def set_password(self, password: str) -> None:
def set_role(self, role: str) -> None:
""" Set the user role. Helper function to avoid code repetition.
"""
{'enabled': self.__enable_account, 'user': self.__activate_account, 'admin': self.__make_admin}[role]()
if role not in ROLES:
raise ValueError(f"Invalid role: {role}")
if role == 'banned':
self.role = role
else:
{'enabled': self.__enable_account, 'user': self.__activate_account, 'admin': self.__make_admin}[role]()
session.commit()

def __enable_account(self) -> None:
""" Changed the role to 'enabled' when the user confirms the email.
"""
self.role = 'enabled'
session.delete(self.activation_token) # type: ignore
if self.activation_token:
session.delete(self.activation_token) # type: ignore
send_validation_mail(self)

def __activate_account(self) -> None:
Expand Down
69 changes: 67 additions & 2 deletions tests/test_api/test_queries/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@

from ptmd.api import app


HEADERS = {'Content-Type': 'application/json'}


class MockedUser:
def __init__(self) -> None:
self.role = "enabled"
self.id = 2

def set_role(self, role):
self.role = role


@patch('ptmd.api.queries.utils.verify_jwt_in_request', return_value=None)
@patch('flask_jwt_extended.view_decorators.verify_jwt_in_request')
@patch('ptmd.api.queries.utils.get_current_user')
Expand Down Expand Up @@ -254,13 +264,13 @@ def test_reset_password_error(self, mock_token,
def test_reset_password_success(self, mock_session, mock_token,
mock_get_current_user, mock_verify_jwt, mock_verify_in_request):
class MockedUser:
def __init__(self):
def __init__(self) -> None:
self.pwd = None

def set_password(self, pwd):
self.pwd = pwd

mocked_user = MockedUser()
mocked_user: MockedUser = MockedUser()
mock_token.return_value.user_reset = [mocked_user]
headers = {'Authorization': f'Bearer {123}', **HEADERS}
with app.test_client() as client:
Expand All @@ -269,3 +279,58 @@ def set_password(self, pwd):
self.assertEqual(response.status_code, 200)
mock_session.delete.assert_called_with(mock_token.return_value)
self.assertEqual(mocked_user.pwd, "None")

@patch('ptmd.api.queries.users.User')
@patch('ptmd.api.queries.users.session')
@patch('ptmd.api.queries.users.get_current_user')
def test_make_admin_success(self, mock_current_user, mock_session, mock_user,
mock_get_current_user_utils, mock_verify_jwt, mock_verify_in_request):

mock_get_current_user_utils.return_value.role = 'admin'
mock_user.query.filter().return_value = MockedUser()
mock_current_user.return_value = MockedUser()
headers = {'Authorization': f'Bearer {123}', **HEADERS}
with app.test_client() as client:
response = client.get('/api/users/2/make_admin', headers=headers)
self.assertEqual(response.json, {"msg": "User 2 role has been changed to admin"})
self.assertEqual(response.status_code, 200)

@patch('ptmd.api.queries.users.User')
def test_make_admin_failed_404(self, mock_user, mock_get_current_user, mock_verify_jwt, mock_verify_in_request):
mock_user.query.filter().first.return_value = None
mock_get_current_user.return_value.role = 'admin'
headers = {'Authorization': f'Bearer {123}', **HEADERS}
with app.test_client() as client:
response = client.get('/api/users/100/make_admin', headers=headers)
self.assertEqual(response.json, {"msg": "User not found"})
self.assertEqual(response.status_code, 404)

@patch('ptmd.api.queries.users.User')
@patch('ptmd.api.queries.users.get_current_user')
def test_make_admin_failed_change_self(self, mock_get_current_user, mock_user,
mock_get_current_user_utils, mock_verify_jwt, mock_verify_in_request):
mock_user.query.filter().first.return_value.id = 1
mock_get_current_user_utils.return_value.role = 'admin'
mock_get_current_user.return_value.id = 1
headers = {'Authorization': f'Bearer {123}', **HEADERS}
with app.test_client() as client:
response = client.get('/api/users/1/make_admin', headers=headers)
self.assertEqual(response.json, {"msg": "Cannot change your own role"})
self.assertEqual(response.status_code, 400)

@patch('ptmd.api.queries.users.User')
@patch('ptmd.api.queries.users.session')
@patch('ptmd.api.queries.users.get_current_user')
def test_make_admin_failed_invalid_role(self, mock_current_user, mock_session, mock_user,
mock_get_current_user_utils, mock_verify_jwt, mock_verify_in_request):
mock_get_current_user_utils.return_value.role = 'admin'
mock_get_current_user_utils.return_value.role = 'admin'
mock_user.query.filter().return_value = MockedUser()
mock_current_user.return_value = MockedUser()
mock_user.query.filter().first.return_value.set_role.side_effect = ValueError()

headers = {'Authorization': f'Bearer {123}', **HEADERS}
with app.test_client() as client:
response = client.get('/api/users/2/make_admin', headers=headers)
self.assertEqual(response.json, {'msg': 'Invalid role'})
self.assertEqual(response.status_code, 400)
16 changes: 16 additions & 0 deletions tests/test_database/test_models/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ def test_user_with_organisation(self, mock_send_mail, mock_create_access_token):
self.assertEqual(user.role, 'disabled')
self.assertEqual(user.username, user_input['username'])
self.assertEqual(user.organisation_id, user_input['organisation_id'])

@patch('ptmd.database.models.user.session')
@patch('ptmd.database.models.token.send_confirmation_mail', return_value=True)
def test_set_role_success(self, mock_send_confirmation_mail, mock_session):
user = User('test', 'test', 'test', 'disabled')
user.set_role('banned')
self.assertEqual(user.role, 'banned')
mock_session.commit.assert_called_once()

@patch('ptmd.database.models.user.session')
@patch('ptmd.database.models.token.send_confirmation_mail', return_value=True)
def test_set_role_invalid_role(self, mock_send_confirmation_mail, mock_session):
user = User('test', 'test', 'test', 'disabled')
with self.assertRaises(ValueError) as context:
user.set_role('invalid role')
self.assertEqual(str(context.exception), "Invalid role: invalid role")

0 comments on commit a9d1ae2

Please sign in to comment.