3333from sqlalchemy .exc import IntegrityError
3434from sqlalchemy .ext .asyncio import AsyncSession
3535
36- from ..auth import require_admin_key
36+ from ..auth_framework import Operation , Principal , require_operation
3737from ..db import get_async_db
3838from ..errors import (
3939 APIValidationError ,
@@ -443,9 +443,11 @@ async def _validate_control_definition(
443443 summary = "Render a control template preview" ,
444444 response_description = "Rendered control preview" ,
445445)
446+ # Rendering is part of the authoring flow, so require create access.
446447async def render_control_template (
447448 request : RenderControlTemplateRequest ,
448449 db : AsyncSession = Depends (get_async_db ),
450+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_CREATE )),
449451) -> RenderControlTemplateResponse :
450452 """Render a template-backed control without persisting it."""
451453 control_def = await _render_and_validate_template_input (
@@ -461,13 +463,14 @@ async def render_control_template(
461463
462464@router .put (
463465 "" ,
464- dependencies = [Depends (require_admin_key )],
465466 response_model = CreateControlResponse ,
466467 summary = "Create a new control" ,
467468 response_description = "Created control ID" ,
468469)
469470async def create_control (
470- request : CreateControlRequest , db : AsyncSession = Depends (get_async_db )
471+ request : CreateControlRequest ,
472+ db : AsyncSession = Depends (get_async_db ),
473+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_CREATE )),
471474) -> CreateControlResponse :
472475 """
473476 Create a new control with a unique name.
@@ -549,6 +552,7 @@ async def create_control(
549552 summary = "Get control definition JSON schema" ,
550553 response_description = "JSON schema for ControlDefinition" ,
551554)
555+ # Public schema metadata: no tenant state, no auth operation.
552556async def get_control_schema () -> GetControlSchemaResponse :
553557 """Return the canonical JSON schema for ControlDefinition."""
554558 return GetControlSchemaResponse (
@@ -563,7 +567,9 @@ async def get_control_schema() -> GetControlSchemaResponse:
563567 response_description = "Control metadata and configuration" ,
564568)
565569async def get_control (
566- control_id : int , db : AsyncSession = Depends (get_async_db )
570+ control_id : int ,
571+ db : AsyncSession = Depends (get_async_db ),
572+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_READ )),
567573) -> GetControlResponse :
568574 """
569575 Retrieve a control by ID including its name and configuration data.
@@ -600,7 +606,9 @@ async def get_control(
600606 response_description = "Control data payload" ,
601607)
602608async def get_control_data (
603- control_id : int , db : AsyncSession = Depends (get_async_db )
609+ control_id : int ,
610+ db : AsyncSession = Depends (get_async_db ),
611+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_READ )),
604612) -> GetControlDataResponse :
605613 """
606614 Retrieve the configuration data for a control.
@@ -640,6 +648,7 @@ async def list_control_versions(
640648 ),
641649 limit : int = Query (_DEFAULT_PAGINATION_LIMIT , ge = 1 , le = _MAX_PAGINATION_LIMIT ),
642650 db : AsyncSession = Depends (get_async_db ),
651+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_READ )),
643652) -> ListControlVersionsResponse :
644653 """List control versions ordered newest-first using cursor-based pagination."""
645654 page = await ControlService (db ).list_versions (control_id , cursor = cursor , limit = limit )
@@ -673,6 +682,7 @@ async def get_control_version(
673682 control_id : int ,
674683 version_num : int ,
675684 db : AsyncSession = Depends (get_async_db ),
685+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_READ )),
676686) -> GetControlVersionResponse :
677687 """Return a specific control version, including its raw persisted snapshot."""
678688 version = await ControlService (db ).get_version_or_404 (control_id , version_num )
@@ -687,7 +697,6 @@ async def get_control_version(
687697
688698@router .put (
689699 "/{control_id}/data" ,
690- dependencies = [Depends (require_admin_key )],
691700 response_model = SetControlDataResponse ,
692701 summary = "Update control configuration data" ,
693702 response_description = "Success confirmation" ,
@@ -696,6 +705,7 @@ async def set_control_data(
696705 control_id : int ,
697706 request : SetControlDataRequest ,
698707 db : AsyncSession = Depends (get_async_db ),
708+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_UPDATE )),
699709) -> SetControlDataResponse :
700710 """
701711 Update the configuration data for a control.
@@ -757,8 +767,11 @@ async def set_control_data(
757767 summary = "Validate control configuration" ,
758768 response_description = "Validation result" ,
759769)
770+ # Validation uses the authoring path, so require create access.
760771async def validate_control_data (
761- request : ValidateControlDataRequest , db : AsyncSession = Depends (get_async_db )
772+ request : ValidateControlDataRequest ,
773+ db : AsyncSession = Depends (get_async_db ),
774+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_CREATE )),
762775) -> ValidateControlDataResponse :
763776 """
764777 Validate control configuration data without saving it.
@@ -798,6 +811,7 @@ async def list_controls(
798811 execution : str | None = Query (None , description = "Filter by execution ('server' or 'sdk')" ),
799812 tag : str | None = Query (None , description = "Filter by tag" ),
800813 db : AsyncSession = Depends (get_async_db ),
814+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_READ )),
801815) -> ListControlsResponse :
802816 """
803817 List all controls with optional filtering and cursor-based pagination.
@@ -884,7 +898,6 @@ async def list_controls(
884898
885899@router .delete (
886900 "/{control_id}" ,
887- dependencies = [Depends (require_admin_key )],
888901 response_model = DeleteControlResponse ,
889902 summary = "Delete a control" ,
890903 response_description = "Deletion confirmation with dissociation info" ,
@@ -897,6 +910,7 @@ async def delete_control(
897910 "If false, fail if control is associated with any policy or agent." ,
898911 ),
899912 db : AsyncSession = Depends (get_async_db ),
913+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_DELETE )),
900914) -> DeleteControlResponse :
901915 """
902916 Delete a control by ID.
@@ -1035,7 +1049,6 @@ async def delete_control(
10351049
10361050@router .patch (
10371051 "/{control_id}" ,
1038- dependencies = [Depends (require_admin_key )],
10391052 response_model = PatchControlResponse ,
10401053 summary = "Update control metadata" ,
10411054 response_description = "Updated control information" ,
@@ -1044,6 +1057,7 @@ async def patch_control(
10441057 control_id : int ,
10451058 request : PatchControlRequest ,
10461059 db : AsyncSession = Depends (get_async_db ),
1060+ _principal : Principal = Depends (require_operation (Operation .CONTROLS_UPDATE )),
10471061) -> PatchControlResponse :
10481062 """
10491063 Update control metadata (name and/or enabled status).
0 commit comments