diff options
author | Frederick Muriuki Muriithi | 2023-03-06 14:57:53 +0300 |
---|---|---|
committer | Frederick Muriuki Muriithi | 2023-03-06 14:57:53 +0300 |
commit | 98e93be1b8e5353656e18f1452026db6f2902e6c (patch) | |
tree | 2547ab9284e1a1718b35faf92d8aa68e9d42b283 | |
parent | 4fc72af7e851f12a9f4edc98b0a55c66c9bf1b13 (diff) | |
download | genenetwork3-98e93be1b8e5353656e18f1452026db6f2902e6c.tar.gz |
auth: resources: Enable assigning a user roles on resources
-rw-r--r-- | gn3/auth/authentication/oauth2/grants/password_grant.py | 10 | ||||
-rw-r--r-- | gn3/auth/authentication/users.py | 7 | ||||
-rw-r--r-- | gn3/auth/authorisation/resources/models.py | 30 | ||||
-rw-r--r-- | gn3/auth/authorisation/resources/views.py | 38 | ||||
-rw-r--r-- | gn3/auth/authorisation/users/models.py | 19 | ||||
-rw-r--r-- | gn3/auth/authorisation/users/views.py | 9 | ||||
-rw-r--r-- | main.py | 3 | ||||
-rw-r--r-- | migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py | 26 | ||||
-rw-r--r-- | migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py | 42 |
9 files changed, 169 insertions, 15 deletions
diff --git a/gn3/auth/authentication/oauth2/grants/password_grant.py b/gn3/auth/authentication/oauth2/grants/password_grant.py index 3ec7384..3233877 100644 --- a/gn3/auth/authentication/oauth2/grants/password_grant.py +++ b/gn3/auth/authentication/oauth2/grants/password_grant.py @@ -6,6 +6,8 @@ from authlib.oauth2.rfc6749 import grants from gn3.auth import db from gn3.auth.authentication.users import valid_login, user_by_email +from gn3.auth.authorisation.errors import NotFoundError + class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): """Implement the 'Password' grant.""" TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"] @@ -13,6 +15,8 @@ class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): def authenticate_user(self, username, password): "Authenticate the user with their username and password." with db.connection(app.config["AUTH_DB"]) as conn: - return user_by_email(conn, username).maybe( - None, - lambda user: valid_login(conn, user, password) and user) + try: + user = user_by_email(conn, username) + return user if valid_login(conn, user, password) else None + except NotFoundError as _nfe: + return None diff --git a/gn3/auth/authentication/users.py b/gn3/auth/authentication/users.py index ce01805..e65938e 100644 --- a/gn3/auth/authentication/users.py +++ b/gn3/auth/authentication/users.py @@ -6,6 +6,7 @@ import bcrypt from pymonad.maybe import Just, Maybe, Nothing from gn3.auth import db +from gn3.auth.authorisation.errors import NotFoundError class User(NamedTuple): """Class representing a user.""" @@ -25,16 +26,16 @@ DUMMY_USER = User(user_id=UUID("a391cf60-e8b7-4294-bd22-ddbbda4b3530"), email="gn3@dummy.user", name="Dummy user to use as placeholder") -def user_by_email(conn: db.DbConnection, email: str) -> Maybe: +def user_by_email(conn: db.DbConnection, email: str) -> User: """Retrieve user from database by their email address""" with db.cursor(conn) as cursor: cursor.execute("SELECT * FROM users WHERE email=?", (email,)) row = cursor.fetchone() if row: - return Just(User(UUID(row["user_id"]), row["email"], row["name"])) + return User(UUID(row["user_id"]), row["email"], row["name"]) - return Nothing + raise NotFoundError(f"Could not find user with email {email}") def user_by_id(conn: db.DbConnection, user_id: UUID) -> Maybe: """Retrieve user from database by their user id""" diff --git a/gn3/auth/authorisation/resources/models.py b/gn3/auth/authorisation/resources/models.py index be1bc17..0a5b1ec 100644 --- a/gn3/auth/authorisation/resources/models.py +++ b/gn3/auth/authorisation/resources/models.py @@ -14,7 +14,8 @@ from .checks import authorised_for from ..checks import authorised_p from ..errors import NotFoundError, AuthorisationError -from ..groups.models import Group, user_group, group_by_id, is_group_leader +from ..groups.models import ( + Group, GroupRole, user_group, group_by_id, is_group_leader) class MissingGroupError(AuthorisationError): """Raised for any resource operation without a group.""" @@ -477,3 +478,30 @@ def attach_resources_data( cursor, rscs) for category, rscs in organised.items()) for resource in categories) + +@authorised_p( + ("group:user:assign-role",), + "You cannot assign roles to users for this group.", + oauth2_scope="profile group role resource") +def assign_resource_user( + conn: db.DbConnection, resource: Resource, user: User, + role: GroupRole) -> dict: + """Assign `role` to `user` for the specific `resource`.""" + with db.cursor(conn) as cursor: + cursor.execute( + "INSERT INTO " + "group_user_roles_on_resources(group_id, user_id, role_id, " + "resource_id) " + "VALUES (?, ?, ?, ?) " + "ON CONFLICT (group_id, user_id, role_id, resource_id) " + "DO NOTHING", + (str(resource.group.group_id), str(user.user_id), + str(role.role.role_id), str(resource.resource_id))) + return { + "resource": dictify(resource), + "user": dictify(user), + "role": dictify(role), + "description": ( + f"The user '{user.name}'({user.email}) was assigned the " + f"'{role.role.role_name}' role on resource with ID " + f"'{resource.resource_id}'.")} diff --git a/gn3/auth/authorisation/resources/views.py b/gn3/auth/authorisation/resources/views.py index e1386ab..6d4098a 100644 --- a/gn3/auth/authorisation/resources/views.py +++ b/gn3/auth/authorisation/resources/views.py @@ -9,17 +9,17 @@ from gn3.auth.db_utils import with_db_connection from .checks import authorised_for from .models import ( - resource_by_id, resource_categories, link_data_to_resource, - resource_category_by_id, unlink_data_from_resource, + resource_by_id, resource_categories, assign_resource_user, + link_data_to_resource, resource_category_by_id, unlink_data_from_resource, create_resource as _create_resource) from ..roles import Role -from ..groups.models import Group, GroupRole from ..errors import InvalidData, AuthorisationError +from ..groups.models import Group, GroupRole, group_role_by_id from ... import db from ...dictify import dictify -from ...authentication.users import User +from ...authentication.users import User, user_by_email from ...authentication.oauth2.resource_server import require_oauth resources = Blueprint("resources", __name__) @@ -101,7 +101,7 @@ def unlink_data(): except AssertionError as aserr: raise InvalidData(aserr.args[0]) from aserr -@resources.route("<uuid:resource_id>/users", methods=["GET"]) +@resources.route("<uuid:resource_id>/user/list", methods=["GET"]) @require_oauth("profile group resource") def resource_users(resource_id: uuid.UUID): """Retrieve all users with access to the given resource.""" @@ -115,8 +115,8 @@ def resource_users(resource_id: uuid.UUID): with db.cursor(conn) as cursor: def __organise_users_n_roles__(users_n_roles, row): user_id = uuid.UUID(row["user_id"]) - user = users_n_roles.get( - user_id, User(user_id, row["email"], row["name"])) + user = users_n_roles.get(user_id, {}).get( + "user", User(user_id, row["email"], row["name"])) role = GroupRole( uuid.UUID(row["group_role_id"]), resource.group, @@ -157,3 +157,27 @@ def resource_users(resource_id: uuid.UUID): user_row for user_id, user_row in with_db_connection(__the_users__).items())) return jsonify(tuple(results)) + +@resources.route("<uuid:resource_id>/user/assign", methods=["POST"]) +@require_oauth("profile group resource role") +def assign_role_to_user(resource_id: uuid.UUID) -> Response: + """Assign a role on the specified resource to a user.""" + with require_oauth.acquire("profile group resource role") as the_token: + try: + form = request.form + group_role_id = form.get("group_role_id", "") + user_email = form.get("user_email", "") + assert bool(group_role_id), "The role must be provided." + assert bool(user_email), "The user email must be provided." + + def __assign__(conn: db.DbConnection) -> dict: + resource = resource_by_id(conn, the_token.user, resource_id) + user = user_by_email(conn, user_email) + return assign_resource_user( + conn, resource, user, + group_role_by_id(conn, resource.group, + uuid.UUID(group_role_id))) + except AssertionError as aserr: + raise AuthorisationError(aserr.args[0]) from aserr + + return jsonify(with_db_connection(__assign__)) diff --git a/gn3/auth/authorisation/users/models.py b/gn3/auth/authorisation/users/models.py new file mode 100644 index 0000000..844a8a9 --- /dev/null +++ b/gn3/auth/authorisation/users/models.py @@ -0,0 +1,19 @@ +"""Functions for acting on users.""" +import uuid + +from gn3.auth import db +from gn3.auth.authorisation.checks import authorised_p + +from gn3.auth.authentication.users import User + +@authorised_p( + ("system:user:list",), + "You do not have the appropriate privileges to list users.", + oauth2_scope="profile user") +def list_users(conn: db.DbConnection) -> tuple[User, ...]: + """List out all users.""" + with db.cursor(conn) as cursor: + cursor.execute("SELECT * FROM users") + return tuple( + User(uuid.UUID(row["user_id"]), row["email"], row["name"]) + for row in cursor.fetchall()) diff --git a/gn3/auth/authorisation/users/views.py b/gn3/auth/authorisation/users/views.py index 2219440..5015cac 100644 --- a/gn3/auth/authorisation/users/views.py +++ b/gn3/auth/authorisation/users/views.py @@ -11,6 +11,7 @@ from gn3.auth import db from gn3.auth.dictify import dictify from gn3.auth.db_utils import with_db_connection +from ..users.models import list_users from ..groups.models import user_group as _user_group from ..resources.models import user_resources as _user_resources from ..roles.models import assign_default_roles, user_roles as _user_roles @@ -158,3 +159,11 @@ def user_join_request_exists(): with require_oauth.acquire("profile group") as the_token: return jsonify(with_db_connection(partial( __request_exists__, user=the_token.user))) + +@users.route("/list", methods=["GET"]) +@require_oauth("profile user") +def list_all_users() -> Response: + """List all the users.""" + with require_oauth.acquire("profile group") as _the_token: + return jsonify(tuple( + dictify(user) for user in with_db_connection(list_users))) @@ -81,7 +81,8 @@ def init_dev_clients(): "default_redirect_uri": "http://localhost:5033/oauth2/code", "redirect_uris": ["http://localhost:5033/oauth2/code"], "response_type": "token", # choices: ["code", "token"] - "scope": ["profile", "group", "role", "resource", "register-client"] + "scope": ["profile", "group", "role", "resource", "register-client", + "user"] }), "user_id": "0ad1917c-57da-46dc-b79e-c81c91e5b928"},) diff --git a/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py b/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py new file mode 100644 index 0000000..0393cd3 --- /dev/null +++ b/migrations/auth/20230306_01_pRfxl-add-system-user-list-privilege.py @@ -0,0 +1,26 @@ +""" +Add system:user:list privilege +""" +import contextlib + +from yoyo import step + +__depends__ = {'20230216_02_0ZHSl-make-dataset-id-and-trait-id-foreign-keys-in-tables'} + +def insert_users_list_priv(conn): + """Create a new 'system:user:list' privilege.""" + with contextlib.closing(conn.cursor()) as cursor: + cursor.execute( + "INSERT INTO privileges(privilege_id, privilege_description) " + "VALUES('system:user:list', 'List users in the system.') " + "ON CONFLICT (privilege_id) DO NOTHING") + +def delete_users_list_priv(conn): + """Delete the new 'system:user:list' privilege.""" + with contextlib.closing(conn.cursor()) as cursor: + cursor.execute( + "DELETE FROM privileges WHERE privilege_id='system:user:list'") + +steps = [ + step(insert_users_list_priv, delete_users_list_priv) +] diff --git a/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py b/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py new file mode 100644 index 0000000..4cfd068 --- /dev/null +++ b/migrations/auth/20230306_02_7GnRY-add-system-user-list-privilege-to-system-administrator-and-group-leader-roles.py @@ -0,0 +1,42 @@ +""" +Add system:user:list privilege to system-administrator and group-leader roles. +""" +import uuid +import contextlib + +from yoyo import step + +__depends__ = {'20230306_01_pRfxl-add-system-user-list-privilege'} + +def role_ids(cursor): + """Get role ids from names""" + cursor.execute( + "SELECT * FROM roles WHERE role_name IN " + "('system-administrator', 'group-leader')") + return (uuid.UUID(row[0]) for row in cursor.fetchall()) + +def add_privilege_to_roles(conn): + """ + Add 'system:user:list' privilege to 'system-administrator' and + 'group-leader' roles.""" + with contextlib.closing(conn.cursor()) as cursor: + cursor.executemany( + "INSERT INTO role_privileges(role_id,privilege_id) " + "VALUES(?, ?)", + tuple((str(role_id), "system:user:list") + for role_id in role_ids(cursor))) + +def del_privilege_from_roles(conn): + """ + Delete 'system:user:list' privilege to 'system-administrator' and + 'group-leader' roles. + """ + with contextlib.closing(conn.cursor()) as cursor: + cursor.execute( + "DELETE FROM role_privileges WHERE " + "role_id IN (?, ?) AND privilege_id='system:user:list'", + tuple(role_ids(cursor))) + +steps = [ + step(add_privilege_to_roles, del_privilege_from_roles) +] |