Skip to content

Commit 66ae1ec

Browse files
authored
Remove more class admin things (#493)
Fixes #488 In this PR: - Removes "Administrator" from dropdown in the UI unless the person still has this role - Works towards #488 by updating permissions model to move the `can_manage_assistants` permission over to `supervisor` instead of `admin`. - Allow testing on functions that write / delete permissions with the authz mock server - Allow inspection of permissions operations that occurred during the test on the authz mock server - Add new tests for the group creation endpoint to verify correct permissions setting on public vs. private groups. - We will no longer grant `admin` automatically to the creator of a group. - Note that `admin` role is required to set the API key. Since the creator of the class had to have had `admin` anyway this shouldn't be an issue. - Script for migrating permissions. Migration steps: - [x] Upgrade OpenFGA to 1.6.0 (this is actually optional but a good idea) - [ ] Run `python -m pingpong auth update_model` to update the latest permissions model - [ ] Run script to clean up permissions: `python -m pingpong auth update_group_admin_perms` - [ ] Finally, the permissions model can be updated again after the migration is complete to remove the `class#admin` userset on `can_manage_assistants`. This is needed during the migration in order to check this permission and convert it to supervisor. - [ ] Restart pingpong srv to pick up newest model
1 parent d37c145 commit 66ae1ec

15 files changed

Lines changed: 694 additions & 409 deletions

File tree

conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ async def user(request, config, db):
6969
yield u
7070

7171

72+
@pytest.fixture
73+
async def institution(request, config, db):
74+
if not hasattr(request, "param"):
75+
yield None
76+
else:
77+
from pingpong.models import Institution
78+
79+
async with db.async_session() as session:
80+
i = Institution(**request.param)
81+
session.add(i)
82+
await session.commit()
83+
yield i
84+
85+
7286
@pytest.fixture
7387
async def valid_user_token(user, now):
7488
from pingpong.auth import encode_session_token

docker-compose.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ services:
44

55
authz:
66
platform: linux/amd64
7-
image: openfga/openfga:v1.5.0
7+
image: openfga/openfga:v1.6.0
88
container_name: pingpong-authz
99
command: [run]
1010
expose: [8080, 8081, 3003]
@@ -13,6 +13,7 @@ services:
1313
OPENFGA_DATASTORE_MAX_OPEN_CONNS: 40
1414
OPENFGA_DATASTORE_MAX_IDLE_CONNS: 25
1515
OPENFGA_MAX_CONCURRENT_READS_FOR_CHECK: 100
16+
OPENFGA_EXPERIMENTALS: enable-list-users
1617
secrets:
1718
- source: openfgacfg
1819
mode: 0400

pingpong/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .canvas import canvas_sync_all
1515
from .config import config
1616
from .models import Base, User
17+
from .authz.admin_migration import remove_class_admin_perms
1718

1819
from sqlalchemy import inspect
1920

@@ -81,6 +82,11 @@ async def _update_model() -> None:
8182
asyncio.run(_update_model())
8283

8384

85+
@auth.command("update_group_admin_perms")
86+
def update_group_admin_perms() -> None:
87+
asyncio.run(remove_class_admin_perms())
88+
89+
8490
@auth.command("login")
8591
@click.argument("email")
8692
@click.argument("redirect", default="/")

pingpong/authz/admin_migration.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from ..config import config
2+
from ..models import Class
3+
from .base import AuthzClient
4+
5+
import logging
6+
7+
import tqdm
8+
from sqlalchemy import select
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
async def swap_class_admin_for_teacher(c: AuthzClient, all_classes: list[int]):
14+
"""Find users who are class admins and swap them to teachers."""
15+
logger.info("Swapping group admin role for teacher role ...")
16+
total_migrated = 0
17+
for class_id in tqdm.tqdm(all_classes):
18+
# Use `expand` instead of `list-users` since we want to *ignore* everyone
19+
# who has *inherited* permissions. Expand at level 1 will only return
20+
# the direct assignments within the class.
21+
expand_result = await c.expand(f"class:{class_id}", "admin", max_depth=1)
22+
users_with_admin = [rel.entity for rel in expand_result if not rel.is_group]
23+
await c.write_safe(
24+
grant=[(u, "teacher", f"class:{class_id}") for u in users_with_admin],
25+
revoke=[(u, "admin", f"class:{class_id}") for u in users_with_admin],
26+
)
27+
total_migrated += len(users_with_admin)
28+
logger.info(f"Swapped {total_migrated} user(s) from admin to teacher role.")
29+
30+
31+
async def swap_class_admin_group_for_supervisor_group(
32+
c: AuthzClient, all_classes: list[int]
33+
):
34+
"""Find groups where admins can manage assistants and grant to supervisors."""
35+
logger.info("Swapping group admin group for supervisor group ...")
36+
total_migrated = 0
37+
for class_id in tqdm.tqdm(all_classes):
38+
# NOTE that since supervisor is a supserset of admin, this will always be true even
39+
# after classes are migrated. That's ok, since the update is idempotent.
40+
checks = await c.check(
41+
[(f"class:{class_id}#admin", "can_manage_assistants", f"class:{class_id}")]
42+
)
43+
if checks[0]:
44+
await c.write_safe(
45+
grant=[
46+
(
47+
f"class:{class_id}#supervisor",
48+
"can_manage_assistants",
49+
f"class:{class_id}",
50+
)
51+
],
52+
revoke=[
53+
(
54+
f"class:{class_id}#admin",
55+
"can_manage_assistants",
56+
f"class:{class_id}",
57+
)
58+
],
59+
)
60+
total_migrated += 1
61+
logger.info(f"Swapped {total_migrated} group(s) from admin to supervisor group.")
62+
63+
64+
async def remove_class_admin_perms():
65+
"""Clean up any `admin` permissions assigned on a class level.
66+
67+
1) Swap class `admin` for `teacher`
68+
2) Swap `class:#admin` group for `class:#supervisor` where it is set in `can_manage_assistants`
69+
"""
70+
logger.info("Updating group admin permissions ...")
71+
await config.authz.driver.init()
72+
async with config.db.driver.async_session() as session:
73+
all_classes = await session.execute(select(Class))
74+
all_class_ids = [c.id for c in all_classes.scalars()]
75+
76+
logger.info(f"Found {len(all_class_ids)} groups to migrate.")
77+
78+
async with config.authz.driver.get_client() as c:
79+
await swap_class_admin_for_teacher(c, all_class_ids)
80+
await swap_class_admin_group_for_supervisor_group(c, all_class_ids)
81+
82+
logger.info("Group admin permissions updated.")

pingpong/authz/authz.fga

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type class
3333
define can_manage_users: supervisor
3434
define can_create_thread: member
3535
define can_create_assistants: [class#student] or supervisor
36-
define can_manage_assistants: [class#admin]
36+
define can_manage_assistants: [class#admin, class#supervisor]
3737
define can_publish_assistants: [class#student] or supervisor
3838
define can_manage_threads: [class#supervisor]
3939
define can_publish_threads: [class#student] or can_manage_threads

0 commit comments

Comments
 (0)