Skip to content

Role-Based Access Control (RBAC)

The GenAI IDP Accelerator implements a comprehensive Role-Based Access Control system with server-side enforcement at the API layer, supplemented by UI-level navigation and action controls for a clean user experience. It also supports configuration-profile scoping to restrict non-admin users to specific Configuration Profiles (use cases).

Terminology. What this document used to call a “config version” is now a Configuration Profile; a revision is an immutable snapshot of one profile’s configuration. The stored field is still named allowedConfigVersions for compatibility, and it scopes profiles.

Four roles are defined as Cognito User Pool groups:

RoleCognito GroupDescription
AdminAdminFull access to all operations including user management and pricing
AuthorAuthorRead + write access to documents, configuration, tests, discovery
ReviewerReviewerHITL review operations + limited document visibility
ViewerViewerRead-only access to documents, configuration, agent chat
AnnotatorAnnotatorAnnotates ground truth for assigned test sets only. No access to the document list, configuration, or test sets outside its assignment.

Users can belong to multiple groups. Permissions are the union of all group permissions. For example, a user in both Author and Reviewer groups can both write documents and perform HITL reviews.

Annotator exists so ground-truth labeling can be delegated to people who should not see the rest of the deployment. It is the only role whose access is granted per-object rather than per-feature: an Annotator with no allowedTestSets can do nothing at all.

What an Annotator can reach

OperationPurpose
getAnnotationQueueTheir worst-first review queue for one assigned set
getTestSetDocumentsThe documents and label state of an assigned set
estimateReviewEffortThe “what your review is buying” panel in the workspace
reextractTestSetDocumentRe-run extraction after correcting a document’s class
claimReview / releaseReview / completeSectionReviewClaim a document and save corrected labels

Everything else in Test Studio — creating sets, publishing versions, generating draft labels, resetting labels, deleting — stays Admin/Author. skipAllSectionsReview is deliberately excluded: marking a document reviewed without opening it is the set owner’s decision, not an annotator’s.

Two-layer enforcement. Group membership only reaches the operation; every one of the above additionally asserts the target test set is in the caller’s allowedTestSets. The check is centralized in idp_common/testset_scope.py::assert_can_access_test_set and fails closed — an unreadable users record, a missing attribute, or an empty list all deny. Reaching a test-set operation is never the same as being allowed to see a given set.

For the HITL operations the scope is resolved from the document rather than an argument: a review document carries the TestSetId it came from, and an Annotator attempting a document with no TestSetId (i.e. ordinary production review work) is refused outright.

Scope caching. Lookups are cached briefly per Lambda container. The TTL is asymmetric on purpose: a populated scope is held for 5 minutes (bounding how long a revoked annotator keeps access), while an empty scope is held for only 10 seconds, so granting a new assignment takes effect almost immediately rather than leaving the user locked out for the full TTL.

Feature / API Admin Author Reviewer Viewer
──────────────────────────────────────────────────────────────────────
DOCUMENTS
List documents ✅ ✅† ✅*† ✅†
View document details ✅ ✅† ✅*† ✅†
Upload documents ✅ ✅ ❌ ❌
Delete documents ✅ ✅ ❌ ❌
Reprocess documents ✅ ✅ ❌ ❌
Abort workflows ✅ ✅ ❌ ❌
HITL REVIEW
Claim/Release review ✅ ❌ ✅ ❌
Complete section review ✅ ❌ ✅ ❌
Skip all section reviews ✅ ❌ ✅ ❌
Process changes (edit mode) ✅ ❌ ✅ ❌
CONFIGURATION
View config profiles ✅ ✅† ❌ ✅†
View/Edit configuration ✅ ✅† ❌ ❌
Save as Profile (new profile) ✅ ❌ ❌ ❌
Save as Default ✅ ❌ ❌ ❌
Delete config profile ✅ ❌ ❌ ❌
Set active profile ✅ ✅† ❌ ❌
Sync BDA ✅ ✅† ❌ ❌
CONFIGURATION PROFILE REVISIONS
View/compare revisions ✅ ✅† ❌ ✅†
Restore a revision ✅ ✅† ❌ ❌
Label a revision ✅ ✅† ❌ ❌
Delete a revision ✅ ❌ ❌ ❌
DISCOVERY
List/run discovery jobs ✅ ✅ ❌ ❌
AGENT CHAT & CODE EXPLORER
Chat with agent ✅ ✅ ❌ ✅
Code intelligence ✅ ✅ ❌ ✅
TEST STUDIO
View/run test sets ✅ ✅ ❌ ❌
Create/delete test sets ✅ ✅ ❌ ❌
CUSTOM MODEL FINE-TUNING
List/view fine-tuning jobs ✅ ✅ ❌ ❌
Create fine-tuning jobs ✅ ✅ ❌ ❌
Delete fine-tuning jobs ✅ ✅ ❌ ❌
List available models ✅ ✅ ❌ ❌
CAPACITY PLANNING
Calculate capacity ✅ ✅ ❌ ✅
USER MANAGEMENT
List all users ✅ ❌ ❌ ❌
Create/delete users ✅ ❌ ❌ ❌
Edit user scope ✅ ❌ ❌ ❌
View own profile ✅ ✅ ✅ ✅
PRICING
View pricing ✅ ✅ ❌ ✅
Edit pricing ✅ ❌ ❌ ❌
MODEL LIMITS
View model limits ✅ ✅ ❌ ✅
Edit model limits ✅ ❌ ❌ ❌
✅* = Reviewer sees only HITL-pending docs + their own completed reviews (server-side filtered)
✅† = Scoped by allowedConfigVersions if set (see Configuration-Profile Scoping below)

Configuration-Profile Scoping (Use Case Isolation)

Section titled “Configuration-Profile Scoping (Use Case Isolation)”

Non-admin users can optionally be assigned allowedConfigVersions — a list of Configuration Profile names that restricts their view and access to only those use cases. This enables multi-tenant or multi-use-case deployments where different teams see only their relevant documents and configurations.

  • Admin users: Always unrestricted — allowedConfigVersions is ignored even if set
  • All other roles (Author, Reviewer, Viewer): If allowedConfigVersions is set and non-empty, the user can only:
    • See documents processed with those profiles (server-side filtering)
    • See and select those profiles in all profile dropdowns
    • View/edit configuration — and read, compare, restore, and label revisions — for those profiles only
  • No scope set (empty/null): User sees all profiles and documents (unrestricted)

Scope Is Enforced at the Profile, Never at the Revision

Section titled “Scope Is Enforced at the Profile, Never at the Revision”

A revision is content inside a profile, not an access-control object of its own. Every revision operation resolves its profile first and applies the same scope check used by updateConfiguration, so there is exactly one rule to get right.

This is also why an Author scoped to a profile may restore and label its revisions but still may not create a new profile: moving content inside a profile they already own is ordinary authoring, while minting a profile creates a new access-control object and stays Admin-only. Before revisions existed, keeping a previous configuration required creating a new profile (saveAsVersion), which is why a scoped Author could not iterate without an admin.

Scope entries are matched against the profile name with two deliberate rules:

  • An empty or unset scope means unrestricted. Scoping is opt-in per user.
  • A set scope fails closed. A profile or document with no name to match against is denied, not admitted. In particular, a scoped user does not see documents that carry no ConfigVersion (documents processed before config-version stamping, or whose stamp failed) — an unnamed object cannot be proven in scope.

Entries may be exact names (lending) or glob patterns (lending-*, uc?-prod). Patterns exist for deployments that predate revision history and encode iterations in the name (usecaseA_v1, usecaseA_v2, …), where scoping a user to a use case would otherwise mean re-granting on every iteration. Only an Admin can set a scope entry and only an Admin can create a profile, so a pattern cannot be used to widen one’s own access. New deployments should prefer one profile per use case with revisions for its history, and exact-name scope entries.

The matcher lives in idp_common/config_scope.py. The two document-list resolvers carry no idp_common layer (they are on the hottest UI query and are kept dependency-free), so they vendor that file verbatim; a unit test fails if the copies drift, because a scope matcher that differs between call sites is a privilege-escalation bug.

LayerEnforcement
Document List (server-side)Both listDocuments resolvers filter by the ConfigVersion field using allowedConfigVersions from UsersTable (fails closed on an unstamped document)
Document Chat (server-side)The chat processor resolves the target document’s ConfigVersion and refuses out-of-scope (and unstamped) documents
Config Profile List (server-side)getConfigVersions Lambda resolver filters returned profiles
Config Profile Access (server-side)getConfigVersion Lambda resolver rejects requests for out-of-scope profiles
Revision Operations (server-side)All five *ConfigProfileRevision* operations reject out-of-scope profiles before doing any work
Version Dropdowns (UI)useConfigurationVersions hook filters versions client-side for immediate UX
Default Version Selection (UI)All version pickers auto-select the first available scoped version

All pages with config version selectors automatically respect scope:

PageBehavior
View/Edit ConfigurationShows only scoped versions in Versions panel; loads first scoped version
Upload DocumentsVersion picker shows only scoped versions
DiscoveryVersion picker shows only scoped versions
Test StudioTest runner version picker shows only scoped versions
Capacity PlanningVersion picker shows only scoped versions
Reprocess DocumentDefaults to document’s current ConfigVersion (if in scope)
Document ListServer-side filtered — only shows documents matching scoped versions

Admins can manage user scope via the User Management page:

  1. Create user with scope: When creating a new user, optionally select config versions from the multiselect
  2. Edit user scope: Click “Edit scope” on any non-Admin user row to add/remove config versions
  3. Remove scope: Clear all selections to make a user unrestricted

Admin users’ scope cannot be edited (they are always unrestricted).

All authenticated users can call getMyProfile to retrieve their own profile including allowedConfigVersions. This is used by the UI to apply client-side scope filtering immediately on page load.

query GetMyProfile {
getMyProfile {
userId
email
persona
allowedConfigVersions
}
}
mutation UpdateUser($userId: ID!, $allowedConfigVersions: [String]) {
updateUser(userId: $userId, allowedConfigVersions: $allowedConfigVersions) {
userId
email
allowedConfigVersions
}
}

Layer 1: AppSync Schema Auth Directives (Server-Side)

Section titled “Layer 1: AppSync Schema Auth Directives (Server-Side)”

Every GraphQL mutation and many queries have @aws_cognito_user_pools(cognito_groups: [...]) directives that enforce access at the AppSync level. If a user’s Cognito group is not in the allowed list, AppSync returns an Unauthorized error before any resolver code runs.

⚠️ Do NOT use @aws_auth(cognito_groups: [...]) on this API. The API has an additional authorization provider (AWS_IAM) configured, and on a multi-auth API AppSync silently ignores @aws_auth — every field decorated with it becomes reachable by any authenticated user regardless of group (a Viewer→Admin privilege escalation). Use @aws_cognito_user_pools(cognito_groups: [...]), which AppSync does evaluate on multi-auth APIs. As defense-in-depth, the required group is also enforced server-side in each privileged resolver Lambda (see Layer 2), so an operation is never reachable by an unauthorized caller even if a schema directive regresses.

Key mutations and their allowed roles:

MutationAllowed Roles
deleteConfigVersionAdmin
deleteConfigProfileRevisionAdmin
restoreConfigProfileRevision, labelConfigProfileRevisionAdmin, Author
createUser, updateUser, deleteUserAdmin
updatePricing, restoreDefaultPricingAdmin
updateModelConfigLimits, restoreDefaultModelConfigLimitsAdmin
deleteDocument, updateConfiguration, setActiveVersionAdmin, Author
uploadDocument, reprocessDocument, abortWorkflowAdmin, Author
startTestRun, addTestSet, addTestSetFromUpload, deleteTests, deleteTestSetsAdmin, Author
syncBdaIdp, uploadDiscoveryDocument, deleteDiscoveryJob, autoDetectSectionsAdmin, Author
copyToBaselineAdmin, Author
createFinetuningJob, deleteFinetuningJobAdmin, Author
processChanges, completeSectionReview, claimReview, releaseReview, skipAllSectionsReviewAdmin, Reviewer
sendAgentChatMessageAdmin, Author, Viewer (Reviewer excluded; also IAM for backend)
deleteChatSession, updateChatSessionTitle, deleteAgentJobAll authenticated users (session-scoped; see note below)
updateAgentChatMessageAll authenticated users (also IAM for backend)

Agent Chat authorization: sendAgentChatMessage and listAvailableAgents restrict Agent Chat to Admin, Author, Viewer (Reviewer excluded). The restriction is declared in schema.graphql and enforced server-side in each resolver via a _caller_in_groups check — the single REST route’s Cognito authorizer only authenticates, so the group gate lives in the resolver. The IAM backend publish path has no Cognito identity and bypasses the check. The session-scoped operations (deleteChatSession, getChatMessages, listChatSessions, etc.) remain open to any authenticated user, bounded by session scoping (each user only sees their own sessions).

(Previously the Reviewer exclusion was UI-only — tracked as accepted-risk gap GAP-03 — because AppSync could not combine a cognito_groups restriction with @aws_iam on one field. AppSync has since been removed, so the real groups are now enforced.)

Key queries and their allowed roles:

QueryAllowed Roles
getDocument, listDocuments, listDocumentsByDateRange, etc.All authenticated (server-side filtering in resolvers)
getFileContents, getStepFunctionExecutionAll authenticated
getConfigVersions, getConfigVersion, getPricing, getModelConfigLimits, calculateCapacityAdmin, Author, Viewer
listConfigProfileRevisions, getConfigProfileRevisionAdmin, Author, Viewer
listAvailableAgentsAdmin, Author, Viewer (Reviewer excluded; enforced server-side — see Agent Chat note above)
listChatSessions, getChatMessages, getAgentChatMessagesAll authenticated (session-scoped)
submitAgentQuery, getAgentJobStatus, listAgentJobsAdmin, Author, Viewer
listConfigurationLibrary, getConfigurationLibraryFileAdmin, Author, Viewer
listDiscoveryJobsAdmin, Author
getTestRun, getTestRuns, getTestRunStatus, compareTestRuns, getTestSets, listBucketFiles, validateTestFileNameAdmin, Author
listFinetuningJobs, getFinetuningJob, validateTestSetForFinetuning, listAvailableModelsAll authenticated (UI limited to Admin, Author)
queryKnowledgeBaseAll authenticated
sendChatDocumentMessage (mutation), onChatDocumentMessageUpdate (subscription)All authenticated; resolver enforces per-session ownership and processor enforces allowedConfigVersions scope on the target document
listUsersAll authenticated (non-admin sees only self in resolver)
getMyProfileAll authenticated

Note: The updateConfiguration mutation is schema-level restricted to Admin+Author, but the resolver additionally enforces that saveAsVersion and saveAsDefault operations within that mutation are Admin-only.

Layer 2: Server-Side Resolver Group Checks & Filtering

Section titled “Layer 2: Server-Side Resolver Group Checks & Filtering”

Defense-in-depth group enforcement: In addition to the Layer 1 schema directives, each privileged resolver Lambda re-checks the caller’s cognito:groups claim at its entrypoint and rejects the request if the caller is not in an allowed group. This ensures a privileged operation is never reachable by an unauthorized caller even if a schema directive is missing or misconfigured (for example, a regression back to the silently-ignored @aws_auth directive). The required groups mirror the Layer 1 tables above (Admin, Admin+Author, or Admin+Reviewer per operation).

Identity-based filtering: Lambda resolvers also apply finer-grained filtering based on the caller’s identity:

Document Filtering:

  • Admin: See all documents
  • Author/Viewer: See all documents, filtered by allowedConfigVersions if scope is set
  • Reviewer-only: See only HITL-pending documents + their own completed reviews, plus config-version scope

Configuration Filtering:

  • getConfigVersions: Returns only profiles in user’s scope (or all if unrestricted)
  • getConfigVersion: Rejects request if the profile is not in user’s scope
  • listConfigProfileRevisions / getConfigProfileRevision / restoreConfigProfileRevision / labelConfigProfileRevision / deleteConfigProfileRevision: Reject the request if the profile is not in user’s scope

User Management Filtering:

  • listUsers: Admin sees all users; non-admin sees only their own profile
  • getMyProfile: Returns the calling user’s own profile (including allowedConfigVersions)

The UI adapts based on the user’s role and scope:

  • Navigation sidebar shows only relevant features per role
  • Action buttons (delete, reprocess, upload, save, import) are hidden for roles that can’t perform those actions
  • Version dropdowns are automatically filtered to show only scoped versions
  • The top navigation badge shows the user’s role with color coding (blue=Admin, green=Author, grey=Reviewer/Viewer)
  • Admin-only buttons: “Save as Profile”, “Save as Default” in Configuration; Import/Restore/Save in Pricing and Model Limits
  • Pricing page: Shows “View Pricing” (read-only) for non-admin; “Pricing Configuration” (editable) for admin
  • Model Limits page: Shows “View Model Limits” (read-only) for non-admin; “Model Limits Configuration” (editable) for admin

This layer is NOT a security boundary — it’s purely for user experience. Security is enforced at Layers 1 & 2.

Admins can create users with any of the four roles via the User Management page. Each user is:

  1. Created in DynamoDB (source of truth)
  2. Synced to Cognito (for authentication)
  3. Added to the appropriate Cognito group (for authorization)
  4. Optionally assigned allowedConfigVersions for config-version scoping
FieldDescription
userIdUnique identifier (UUID)
emailUser’s email address (used as Cognito username)
personaRole: Admin, Author, Reviewer, Viewer, or Annotator
statusUser status (active)
allowedConfigVersionsOptional list of Configuration Profile names (or glob patterns) for scoping
allowedTestSetsOptional list of test set ids an Annotator may read and annotate. A separate scope axis from allowedConfigVersions, not a replacement — a user may carry both.
createdAtCreation timestamp
┌─────────────────────────────────┐
│ Browser (UI) │ Layer 3: Navigation/button hiding + scope filtering (UX only)
│ useUserRole + getMyProfile │
│ useConfigurationVersions │ ← Filters versions by allowedConfigVersions
└────────────┬────────────────────┘
│ GraphQL
┌────────────▼────────────────────┐
│ REST API + schema directives │ Layer 1: @aws_cognito_user_pools(cognito_groups) directives (DENY if wrong group)
│ Schema Directives │
└────────────┬────────────────────┘
┌────────────▼────────────────────┐
│ Lambda Resolvers │ Layer 2: Server-side group checks (defense-in-depth) + filtering
│ • listDocuments: ConfigVersion │ ← Filters by allowedConfigVersions from UsersTable
│ • getConfigVersions: scope │ ← Filters profile list
│ • getConfigVersion: scope │ ← Rejects out-of-scope access
│ • *ConfigProfileRevision* │ ← Scope checked at the profile
│ • listUsers: self-only │ ← Non-admin sees only own profile
└────────────┬────────────────────┘
┌────────────▼────────────────────┐
│ DynamoDB │
│ TrackingTable (documents) │
│ ConfigurationTable (versions) │
│ UsersTable (scope data) │
└─────────────────────────────────┘

To add a new role:

  1. Add a AWS::Cognito::UserPoolGroup in template.yaml
  2. Add the group name to relevant @aws_cognito_user_pools(cognito_groups: [...]) directives in schema.graphql (do not use @aws_auth — see Layer 1 warning), and update the corresponding server-side group check in the resolver Lambda
  3. Update the VALID_PERSONAS dict in src/lambda/user_management/index.py
  4. Add role detection in src/ui/src/hooks/use-user-role.ts
  5. Add navigation items in src/ui/src/components/genaiidp-layout/navigation.tsx
  6. Pass the new group as an environment variable to the UserManagement Lambda
  • Knowledge Base queries do not currently enforce config-version scope. KB results may include documents from out-of-scope config versions.
  • Agent Companion Chat analytics queries (Athena) do not filter by config-version scope.
  • GetDocument API (direct document access by URL) does not enforce config-version scope at the resolver level. UI navigation hides out-of-scope documents, but direct API access is not blocked.
  • Documents with no ConfigVersion are now hidden from scoped users rather than shown (the filters fail closed). If a scoped user reports documents disappearing after an upgrade, those documents were processed before config-version stamping; reprocessing them under a profile in that user’s scope restores visibility.
  • Custom Model Fine-tuning jobs are global — not scoped by allowedConfigVersions. A scoped Author can see all fine-tuning jobs and create jobs from any test set. However, when applying a custom model to a configuration version (via the “Create Config Version” modal), the config-version scope IS enforced — the Author can only target versions within their scope.
  • These limitations are tracked for Phase 3 implementation.