about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFrederick Muriuki Muriithi2023-09-26 02:36:37 +0300
committerFrederick Muriuki Muriithi2023-09-26 03:44:33 +0300
commit9f4e9db223b4e2c052756208ecf035044db0451d (patch)
treea745569a1bb89ae2659b0ad7353dab3815958008
parent196a9399d28e20c55cbb173ce4052845cfad5bf3 (diff)
downloadgn-auth-9f4e9db223b4e2c052756208ecf035044db0451d.tar.gz
Add `public-view` role. Assign it to users.
Add a new `public-view` role to be assigned to all users on all
resources that are defined as publicly viewable.

Update code to make assign `public-view` role to a newly registered
user for all publicly viewable roles.

Update the code to assign/revoke the `public-view` role to/from users
whenever the resource is toggled to and from being publicly viewable.

Ensure that `public-view` is not revoked from system-administrators.

Ensure that `public-view` is not revoked from the group administrators
of the group that owns the resource.
-rw-r--r--gn_auth/auth/authorisation/resources/views.py76
-rw-r--r--gn_auth/auth/authorisation/roles/models.py38
-rw-r--r--migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py61
3 files changed, 159 insertions, 16 deletions
diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py
index 0e44df5..f609303 100644
--- a/gn_auth/auth/authorisation/resources/views.py
+++ b/gn_auth/auth/authorisation/resources/views.py
@@ -18,9 +18,9 @@ from gn_auth.auth.authentication.users import User, user_by_id, user_by_email
 
 from .checks import authorised_for
 from .models import (
-    Resource, save_resource, resource_data, resource_by_id,
-    resource_categories, assign_resource_user, link_data_to_resource,
-    unassign_resource_user, resource_category_by_id, unlink_data_from_resource,
+    Resource, resource_data, resource_by_id, resource_categories,
+    assign_resource_user, link_data_to_resource, unassign_resource_user,
+    resource_category_by_id, unlink_data_from_resource,
     create_resource as _create_resource)
 from .groups.models import Group, GroupRole, resource_owner, group_role_by_id
 
@@ -253,6 +253,54 @@ def unassign_role_to_user(resource_id: uuid.UUID) -> Response:
 
         return jsonify(with_db_connection(__assign__))
 
+def __public_view_params__(cursor, user_id, resource_id):
+    ignore = (str(user_id),)
+    # sys admins
+    cursor.execute(
+        "SELECT ur.user_id FROM user_roles AS ur INNER JOIN roles AS r "
+        "ON ur.role_id=r.role_id WHERE r.role_name='system-administrator'")
+    ignore = ignore + tuple(
+        row["user_id"] for row in cursor.fetchall())
+    # group admins
+    cursor.execute(
+        "SELECT DISTINCT gu.user_id FROM resource_ownership AS ro "
+        "INNER JOIN groups AS g ON ro.group_id=g.group_id "
+        "INNER JOIN group_users AS gu ON g.group_id=gu.group_id "
+        "INNER JOIN user_roles AS ur ON gu.user_id=ur.user_id "
+        "INNER JOIN roles AS r ON ur.role_id=r.role_id "
+        "WHERE ro.resource_id=? AND r.role_name='group-leader'",
+        (str(resource_id),))
+    ignore = tuple(set(
+        ignore + tuple(row["user_id"] for row in cursor.fetchall())))
+
+    cursor.execute(
+        "SELECT user_id FROM users WHERE user_id NOT IN "
+        f"({', '.join(['?'] * len(ignore))})",
+        ignore)
+    user_ids = tuple(row["user_id"] for row in cursor.fetchall())
+    cursor.execute(
+        "SELECT role_id FROM roles WHERE role_name='public-view'")
+    role_id = cursor.fetchone()["role_id"]
+    return tuple({
+        "user_id": user_id,
+        "role_id": role_id,
+        "resource_id": str(resource_id)
+    } for user_id in user_ids)
+
+def __assign_revoke_public_view__(cursor, user_id, resource_id, public):
+    if public:
+        cursor.executemany(
+            "INSERT INTO user_roles(user_id, role_id, resource_id) "
+            "VALUES(:user_id, :role_id, :resource_id) "
+            "ON CONFLICT (user_id, role_id, resource_id) "
+            "DO NOTHING",
+            __public_view_params__(cursor, user_id, resource_id))
+        return
+    cursor.executemany(
+        "DELETE FROM user_roles WHERE user_id=:user_id "
+        "AND role_id=:role_id AND resource_id=:resource_id",
+        __public_view_params__(cursor, user_id, resource_id))
+
 @resources.route("<uuid:resource_id>/toggle-public", methods=["POST"])
 @require_oauth("profile group resource role")
 def toggle_public(resource_id: uuid.UUID) -> Response:
@@ -260,11 +308,23 @@ def toggle_public(resource_id: uuid.UUID) -> Response:
     with require_oauth.acquire("profile group resource") as the_token:
         def __toggle__(conn: db.DbConnection) -> Resource:
             old_rsc = resource_by_id(conn, the_token.user, resource_id)
-            return save_resource(
-                conn, the_token.user, Resource(
-                    old_rsc.resource_id, old_rsc.resource_name,
-                    old_rsc.resource_category, not old_rsc.public,
-                    old_rsc.resource_data))
+            public = not old_rsc.public
+            new_resource = Resource(
+                old_rsc.resource_id, old_rsc.resource_name,
+                old_rsc.resource_category, public,
+                old_rsc.resource_data)
+            with db.cursor(conn) as cursor:
+                cursor.execute(
+                    "UPDATE resources SET public=:public "
+                    "WHERE resource_id=:resource_id",
+                    {
+                        "public": 1 if public else 0,
+                        "resource_id": str(resource_id)
+                    })
+                __assign_revoke_public_view__(
+                    cursor, the_token.user.user_id, resource_id, public)
+                return new_resource
+            return new_resource
 
         resource = with_db_connection(__toggle__)
         return jsonify({
diff --git a/gn_auth/auth/authorisation/roles/models.py b/gn_auth/auth/authorisation/roles/models.py
index 4281446..7d78eac 100644
--- a/gn_auth/auth/authorisation/roles/models.py
+++ b/gn_auth/auth/authorisation/roles/models.py
@@ -133,18 +133,40 @@ def user_role(conn: db.DbConnection, user: User, role_id: UUID) -> Either:
         return Left(NotFoundError(
             f"Could not find role with id '{role_id}'",))
 
-def assign_default_roles(cursor: db.DbCursor, user: User):
-    """Assign `user` some default roles."""
+def __assign_group_creator_role__(cursor: db.DbCursor, user: User):
     cursor.execute(
         'SELECT role_id FROM roles WHERE role_name IN '
         '("group-creator")')
-    role_ids = cursor.fetchall()
-    str_user_id = str(user.user_id)
-    params = tuple(
-        {"user_id": str_user_id, "role_id": row["role_id"]} for row in role_ids)
+    role_id = cursor.fetchone()["role_id"]
+    cursor.execute(
+        "SELECT resource_id FROM resources AS r "
+        "INNER JOIN resource_categories AS rc "
+        "ON r.resource_category_id=rc.resource_category_id "
+        "WHERE rc.resource_category_key='system'")
+    resource_id = cursor.fetchone()["resource_id"]
+    cursor.execute(
+        ("INSERT INTO user_roles VALUES (:user_id, :role_id, :resource_id)"),
+        {"user_id": str(user.user_id), "role_id": role_id,
+         "resource_id": resource_id})
+
+def __assign_public_view_role__(cursor: db.DbCursor, user: User):
+    cursor.execute("SELECT resource_id FROM resources WHERE public=1")
+    public_resources = tuple(row["resource_id"] for row in cursor.fetchall())
+    cursor.execute("SELECT role_id FROM roles WHERE role_name='public-view'")
+    role_id = cursor.fetchone()["role_id"]
     cursor.executemany(
-        ("INSERT INTO user_roles VALUES (:user_id, :role_id)"),
-        params)
+        "INSERT INTO user_roles(user_id, role_id, resource_id "
+        "VALUES(:user_id, :role_id, :resource_id)",
+        tuple({
+            "user_id": str(user.user_id),
+            "role_id": role_id,
+            "resource_id": resource_id
+        } for resource_id in public_resources))
+
+def assign_default_roles(cursor: db.DbCursor, user: User):
+    """Assign `user` some default roles."""
+    __assign_group_creator_role__(cursor, user)
+    __assign_public_view_role__(cursor, user)
 
 def revoke_user_role_by_name(cursor: db.DbCursor, user: User, role_name: str):
     """Revoke a role from `user` by the role's name"""
diff --git a/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py b/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py
new file mode 100644
index 0000000..1172034
--- /dev/null
+++ b/migrations/auth/20230925_01_TWJuR-add-new-public-view-role.py
@@ -0,0 +1,61 @@
+"""
+Add new "public-view" role
+"""
+
+import sqlite3
+
+from yoyo import step
+
+__depends__ = {'20230912_02_hFmSn-drop-group-id-and-fix-foreign-key-references-on-group-user-roles-on-resources-table'}
+
+def grant_to_all_users_public_view_role(conn):
+    """Grant the `public-view` role to all existing users."""
+    conn.row_factory = sqlite3.Row
+    conn.execute("PRAGMA foreign_keys = ON")
+    cursor = conn.cursor()
+    cursor.execute("SELECT user_id FROM users")
+    user_ids = tuple(row["user_id"] for row in cursor.fetchall())
+
+    cursor.execute("SELECT resource_id FROM resources WHERE public=1")
+    resource_ids = tuple(row["resource_id"] for row in cursor.fetchall())
+
+    params = tuple({
+        "user_id": user_id,
+        "resource_id": resource_id,
+        "role_id": "fd88bfed-d869-4969-87f2-67c4e8446ecb"
+    } for user_id in user_ids for resource_id in resource_ids)
+    cursor.executemany(
+        "INSERT INTO user_roles(user_id, role_id, resource_id) "
+        "VALUES (:user_id, :role_id, :resource_id) ",
+        params)
+
+def revoke_from_all_users_public_view_role(conn):
+    """Revoke the `public-view` role from all existing users."""
+    conn.execute("PRAGMA foreign_keys = ON")
+    conn.execute(
+        "DELETE FROM user_roles "
+        "WHERE role_id='fd88bfed-d869-4969-87f2-67c4e8446ecb'")
+
+steps = [
+    step(
+        """
+        INSERT INTO roles(role_id, role_name, user_editable)
+        VALUES('fd88bfed-d869-4969-87f2-67c4e8446ecb', 'public-view', 0)
+        """,
+        """
+        DELETE FROM roles WHERE role_id='fd88bfed-d869-4969-87f2-67c4e8446ecb'
+        """),
+    step(
+        """
+        INSERT INTO role_privileges(role_id, privilege_id)
+        VALUES(
+          'fd88bfed-d869-4969-87f2-67c4e8446ecb',
+          'group:resource:view-resource')
+        """,
+        """
+        DELETE FROM role_privileges
+        WHERE role_id='fd88bfed-d869-4969-87f2-67c4e8446ecb'
+        """),
+    step(grant_to_all_users_public_view_role,
+         revoke_from_all_users_public_view_role)
+]