From 5532c61d4ead4e2cbf8811497dc257ef3fe09057 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 6 Jun 2026 23:17:17 +0100 Subject: [PATCH 1/2] Auto-grant full RBAC access to cue type creator on creation When a user creates a new cue type, immediately grant them READ, WRITE, and EXECUTE roles on that resource so they have per-instance access without requiring a separate admin grant. Also sends a GET_CURRENT_RBAC WebSocket message to the creator's active connections so both frontends refresh their RBAC state without a reload. Co-Authored-By: Claude Sonnet 4.6 --- server/controllers/api/show/cues.py | 10 +++ server/test/controllers/api/show/test_cues.py | 74 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/server/controllers/api/show/cues.py b/server/controllers/api/show/cues.py index 2dca6fb2..3bc28afe 100644 --- a/server/controllers/api/show/cues.py +++ b/server/controllers/api/show/cues.py @@ -21,6 +21,7 @@ from models.cue import Cue, CueAssociation, CueType from models.script import Script, ScriptLine, ScriptLineType, ScriptRevision from models.show import Show +from models.user import User from rbac.role import Role from schemas.schemas import CueSchema, CueTypeSchema from utils.web.base_controller import BaseAPIController @@ -81,6 +82,15 @@ async def post(self): session.add(new_cuetype) session.commit() + user = session.get(User, self.current_user["id"]) + self.application.rbac.give_role( + user, new_cuetype, Role.READ | Role.WRITE | Role.EXECUTE + ) + for socket in self.application.get_all_ws(user.id): + await socket.write_message( + {"OP": "NOOP", "DATA": {}, "ACTION": "GET_CURRENT_RBAC"} + ) + self.set_status(200) await self.finish( {"id": new_cuetype.id, "message": "Successfully added cue type"} diff --git a/server/test/controllers/api/show/test_cues.py b/server/test/controllers/api/show/test_cues.py index be510b48..47b90ba1 100644 --- a/server/test/controllers/api/show/test_cues.py +++ b/server/test/controllers/api/show/test_cues.py @@ -11,6 +11,7 @@ ) from models.show import Act, Scene, Show, ShowScriptType from models.user import User +from rbac.role import Role from test.conftest import DigiScriptTestCase @@ -936,3 +937,76 @@ def test_get_import_returns_correct_structure(self): self.assertIn("name", group) self.assertIn("cue_types", group) self.assertIsInstance(group["cue_types"], list) + + +class TestCueTypesController(DigiScriptTestCase): + """Test suite for POST /api/v1/show/cues/types endpoint.""" + + def setUp(self): + super().setUp() + with self._app.get_db().sessionmaker() as session: + show = Show(name="Test Show", script_mode=ShowScriptType.FULL) + session.add(show) + session.flush() + self.show_id = show.id + session.commit() + + self._app.digi_settings.settings["current_show"].set_value(self.show_id) + self.admin_token = self._create_and_login_admin() + self.user_token = self._create_and_login_user(self.admin_token) + + with self._app.get_db().sessionmaker() as session: + admin = session.scalars( + select(User).where(User.username == "admin") + ).first() + self.admin_id = admin.id + user = session.scalars( + select(User).where(User.username == "user") + ).first() + self.user_id = user.id + show = session.get(Show, self.show_id) + self._app.rbac.give_role(user, show, Role.WRITE) + + def test_post_cue_type_grants_all_roles_to_creator(self): + """Creating a cue type grants READ|WRITE|EXECUTE to the creating user.""" + response = self.fetch( + "/api/v1/show/cues/types", + method="POST", + body=tornado.escape.json_encode( + {"prefix": "LX", "description": "Lighting", "colour": "#ff0000"} + ), + headers={"Authorization": f"Bearer {self.user_token}"}, + ) + self.assertEqual(200, response.code) + cue_type_id = tornado.escape.json_decode(response.body)["id"] + + with self._app.get_db().sessionmaker() as session: + user = session.get(User, self.user_id) + cue_type = session.get(CueType, cue_type_id) + self.assertTrue( + self._app.rbac.has_role( + user, cue_type, Role.READ | Role.WRITE | Role.EXECUTE + ) + ) + + def test_post_cue_type_admin_also_gets_grant(self): + """Creating a cue type as an admin still writes the RBAC grant.""" + response = self.fetch( + "/api/v1/show/cues/types", + method="POST", + body=tornado.escape.json_encode( + {"prefix": "SQ", "description": "Sound", "colour": "#00ff00"} + ), + headers={"Authorization": f"Bearer {self.admin_token}"}, + ) + self.assertEqual(200, response.code) + cue_type_id = tornado.escape.json_decode(response.body)["id"] + + with self._app.get_db().sessionmaker() as session: + admin = session.get(User, self.admin_id) + cue_type = session.get(CueType, cue_type_id) + self.assertTrue( + self._app.rbac.has_role( + admin, cue_type, Role.READ | Role.WRITE | Role.EXECUTE + ) + ) From 2b6bbe6ccec97757af37be74c94ea0b4285c87b6 Mon Sep 17 00:00:00 2001 From: Tim Bradgate Date: Sat, 6 Jun 2026 23:21:17 +0100 Subject: [PATCH 2/2] Fix ruff formatting in test_cues.py Co-Authored-By: Claude Sonnet 4.6 --- server/test/controllers/api/show/test_cues.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/test/controllers/api/show/test_cues.py b/server/test/controllers/api/show/test_cues.py index 47b90ba1..6b9412c1 100644 --- a/server/test/controllers/api/show/test_cues.py +++ b/server/test/controllers/api/show/test_cues.py @@ -960,9 +960,7 @@ def setUp(self): select(User).where(User.username == "admin") ).first() self.admin_id = admin.id - user = session.scalars( - select(User).where(User.username == "user") - ).first() + user = session.scalars(select(User).where(User.username == "user")).first() self.user_id = user.id show = session.get(Show, self.show_id) self._app.rbac.give_role(user, show, Role.WRITE)