Skip to content

Commit 2d8aa44

Browse files
committed
Multi-tenancy with Role-Based Access Control (RBAC)
1 parent 142cf5c commit 2d8aa44

44 files changed

Lines changed: 1707 additions & 320 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ SPDX-License-Identifier: MIT-0
77

88
### Added
99

10+
11+
- **Multi-tenancy with Role-Based Access Control (RBAC)** — 4-role model (Admin, Author, Reviewer, Viewer) with server-side AppSync auth directives, server-side Reviewer document filtering, and UI adaptation. Admin has full access; Author can edit config and process documents but cannot manage users or delete config versions; Viewer has read-only access (editors, save buttons, and edit mode all disabled); Reviewer sees only HITL-pending documents. Non-admin roles can be scoped to specific use cases via `allowedConfigVersions`. See `docs/rbac.md`.
12+
1013
- **Standard Class Catalog** — When adding a new document class in the Schema Builder, users can now choose between **Custom Class** (define from scratch) and **Standard Class** (import from a catalog of 35 pre-built document types). Standard classes are derived from AWS BDA standard blueprints and include common document types like Invoice, Receipt, W-2, Bank Statement, Payslip, US Driver License, US Passport, various tax forms (1040, 941, 940, W-9, 1098, 1099), insurance cards, birth/death/marriage certificates, and more. Each standard class comes with a complete extraction schema including attributes, descriptions, and nested types. Imported classes are fully editable. Run `make classes-from-bda` to refresh the catalog from the BDA API.
1114

1215
- **Documentation Site** — Added a hosted documentation site built with [Astro Starlight](https://starlight.astro.build/), auto-deployed to GitHub Pages. Provides full-text search (Pagefind), sidebar navigation organized by topic, dark/light mode, and a professional landing page — all sourced directly from the existing `docs/` markdown files with zero content duplication. Browse at [aws-solutions-library-samples.github.io/accelerated-intelligent-document-processing-on-aws](https://aws-solutions-library-samples.github.io/accelerated-intelligent-document-processing-on-aws/).

docs/rbac.md

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# Role-Based Access Control (RBAC)
2+
3+
## Overview
4+
5+
The GenAI IDP Accelerator implements a comprehensive Role-Based Access Control system with **server-side enforcement** at the AppSync API layer, supplemented by UI-level navigation and action controls for a clean user experience. It also supports **config-version scoping** to restrict non-admin users to specific configuration versions (use cases).
6+
7+
## Roles
8+
9+
Four roles are defined as Cognito User Pool groups:
10+
11+
| Role | Cognito Group | Description |
12+
|------|--------------|-------------|
13+
| **Admin** | `Admin` | Full access to all operations including user management and pricing |
14+
| **Author** | `Author` | Read + write access to documents, configuration, tests, discovery |
15+
| **Reviewer** | `Reviewer` | HITL review operations + limited document visibility |
16+
| **Viewer** | `Viewer` | Read-only access to documents, configuration, agent chat |
17+
18+
### Multi-Group Support
19+
20+
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.
21+
22+
## Permission Matrix
23+
24+
```
25+
Feature / API Admin Author Reviewer Viewer
26+
──────────────────────────────────────────────────────────────────────
27+
DOCUMENTS
28+
List documents ✅ ✅† ✅*† ✅†
29+
View document details ✅ ✅† ✅*† ✅†
30+
Upload documents ✅ ✅ ❌ ❌
31+
Delete documents ✅ ✅ ❌ ❌
32+
Reprocess documents ✅ ✅ ❌ ❌
33+
Abort workflows ✅ ✅ ❌ ❌
34+
35+
HITL REVIEW
36+
Claim/Release review ✅ ❌ ✅ ❌
37+
Complete section review ✅ ❌ ✅ ❌
38+
Skip all section reviews ✅ ❌ ✅ ❌
39+
Process changes (edit mode) ✅ ❌ ✅ ❌
40+
41+
CONFIGURATION
42+
View config versions ✅ ✅† ❌ ✅†
43+
View/Edit configuration ✅ ✅ ❌ ❌
44+
Save as Version (new) ✅ ❌ ❌ ❌
45+
Save as Default ✅ ❌ ❌ ❌
46+
Delete config version ✅ ❌ ❌ ❌
47+
Set active version ✅ ✅ ❌ ❌
48+
Sync BDA ✅ ✅ ❌ ❌
49+
50+
DISCOVERY
51+
List/run discovery jobs ✅ ✅ ❌ ❌
52+
53+
AGENT CHAT & CODE EXPLORER
54+
Chat with agent ✅ ✅ ❌ ✅
55+
Code intelligence ✅ ✅ ❌ ✅
56+
57+
TEST STUDIO
58+
View/run test sets ✅ ✅ ❌ ❌
59+
Create/delete test sets ✅ ✅ ❌ ❌
60+
61+
CAPACITY PLANNING
62+
Calculate capacity ✅ ✅ ❌ ✅
63+
64+
USER MANAGEMENT
65+
List all users ✅ ❌ ❌ ❌
66+
Create/delete users ✅ ❌ ❌ ❌
67+
Edit user scope ✅ ❌ ❌ ❌
68+
View own profile ✅ ✅ ✅ ✅
69+
70+
PRICING
71+
View pricing ✅ ✅ ❌ ✅
72+
Edit pricing ✅ ❌ ❌ ❌
73+
74+
✅* = Reviewer sees only HITL-pending docs + their own completed reviews (server-side filtered)
75+
✅† = Scoped by allowedConfigVersions if set (see Config-Version Scoping below)
76+
```
77+
78+
## Config-Version Scoping (Use Case Isolation)
79+
80+
### Overview
81+
82+
Non-admin users can optionally be assigned **allowedConfigVersions** — a list of configuration version 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.
83+
84+
### How It Works
85+
86+
- **Admin users**: Always unrestricted — `allowedConfigVersions` is ignored even if set
87+
- **All other roles** (Author, Reviewer, Viewer): If `allowedConfigVersions` is set and non-empty, the user can only:
88+
- See documents processed with those config versions (server-side filtering)
89+
- See and select those config versions in all version dropdowns
90+
- View/edit configuration for those versions only
91+
- **No scope set** (empty/null): User sees all versions and documents (unrestricted)
92+
93+
### Scope Enforcement Points
94+
95+
| Layer | Enforcement |
96+
|-------|-------------|
97+
| **Document List** (server-side) | `listDocuments` Lambda resolver filters by `ConfigVersion` field using `allowedConfigVersions` from UsersTable |
98+
| **Config Versions List** (server-side) | `getConfigVersions` Lambda resolver filters returned versions |
99+
| **Config Version Access** (server-side) | `getConfigVersion` Lambda resolver rejects requests for out-of-scope versions |
100+
| **Version Dropdowns** (UI) | `useConfigurationVersions` hook filters versions client-side for immediate UX |
101+
| **Default Version Selection** (UI) | All version pickers auto-select the first available scoped version |
102+
103+
### Affected UI Components
104+
105+
All pages with config version selectors automatically respect scope:
106+
107+
| Page | Behavior |
108+
|------|----------|
109+
| **View/Edit Configuration** | Shows only scoped versions in Versions panel; loads first scoped version |
110+
| **Upload Documents** | Version picker shows only scoped versions |
111+
| **Discovery** | Version picker shows only scoped versions |
112+
| **Test Studio** | Test runner version picker shows only scoped versions |
113+
| **Capacity Planning** | Version picker shows only scoped versions |
114+
| **Reprocess Document** | Defaults to document's current ConfigVersion (if in scope) |
115+
| **Document List** | Server-side filtered — only shows documents matching scoped versions |
116+
117+
### Managing User Scope
118+
119+
Admins can manage user scope via the **User Management** page:
120+
121+
1. **Create user with scope**: When creating a new user, optionally select config versions from the multiselect
122+
2. **Edit user scope**: Click "Edit scope" on any non-Admin user row to add/remove config versions
123+
3. **Remove scope**: Clear all selections to make a user unrestricted
124+
125+
Admin users' scope cannot be edited (they are always unrestricted).
126+
127+
### API: `getMyProfile`
128+
129+
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.
130+
131+
```graphql
132+
query GetMyProfile {
133+
getMyProfile {
134+
userId
135+
email
136+
persona
137+
allowedConfigVersions
138+
}
139+
}
140+
```
141+
142+
### API: `updateUser` (Admin-only)
143+
144+
```graphql
145+
mutation UpdateUser($userId: ID!, $allowedConfigVersions: [String]) {
146+
updateUser(userId: $userId, allowedConfigVersions: $allowedConfigVersions) {
147+
userId
148+
email
149+
allowedConfigVersions
150+
}
151+
}
152+
```
153+
154+
## Enforcement Layers
155+
156+
### Layer 1: AppSync Schema Auth Directives (Server-Side)
157+
158+
Every GraphQL **mutation** has `@aws_auth(cognito_groups: [...])` directives that enforce write 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.
159+
160+
**Key mutations and their allowed roles:**
161+
162+
| Mutation | Allowed Roles |
163+
|----------|---------------|
164+
| `deleteConfigVersion` | Admin |
165+
| `createUser`, `updateUser`, `deleteUser` | Admin |
166+
| `updatePricing`, `restoreDefaultPricing` | Admin |
167+
| `deleteDocument`, `updateConfiguration`, `setActiveVersion` | Admin, Author |
168+
| `uploadDocument`, `reprocessDocument`, `abortWorkflow` | Admin, Author |
169+
| `startTestRun`, `addTestSet`, `deleteTests` | Admin, Author |
170+
| `syncBdaIdp`, `uploadDiscoveryDocument` | Admin, Author |
171+
| `processChanges`, `completeSectionReview`, `claimReview` | Admin, Reviewer |
172+
| `sendAgentChatMessage`, `deleteChatSession` | All authenticated users |
173+
174+
**Important**: `@aws_auth` directives are applied to **Mutations only**, not Queries. Read access for Queries is controlled by:
175+
- **UI navigation** (which features are visible per role)
176+
- **Server-side resolver filtering** (e.g., reviewer document filtering, config-version scoping)
177+
178+
### Layer 2: Server-Side Resolver Filtering
179+
180+
Lambda resolvers apply additional filtering based on the caller's identity:
181+
182+
**Document Filtering:**
183+
- **Admin**: See all documents
184+
- **Author/Viewer**: See all documents, filtered by `allowedConfigVersions` if scope is set
185+
- **Reviewer-only**: See only HITL-pending documents + their own completed reviews, plus config-version scope
186+
187+
**Configuration Filtering:**
188+
- `getConfigVersions`: Returns only versions in user's scope (or all if unrestricted)
189+
- `getConfigVersion`: Rejects request if version is not in user's scope
190+
191+
**User Management Filtering:**
192+
- `listUsers`: Admin sees all users; non-admin sees only their own profile
193+
- `getMyProfile`: Returns the calling user's own profile (including `allowedConfigVersions`)
194+
195+
### Layer 3: UI Adaptation (UX Convenience)
196+
197+
The UI adapts based on the user's role and scope:
198+
- Navigation sidebar shows only relevant features per role
199+
- Action buttons (delete, reprocess, upload, save, import) are hidden for roles that can't perform those actions
200+
- Version dropdowns are automatically filtered to show only scoped versions
201+
- The top navigation badge shows the user's role with color coding (blue=Admin, green=Author, grey=Reviewer/Viewer)
202+
- **Admin-only buttons**: "Save as Version", "Save as Default" in Configuration; Import/Restore/Save in Pricing
203+
- **Pricing page**: Shows "View Pricing" (read-only) for non-admin; "Pricing Configuration" (editable) for admin
204+
205+
**This layer is NOT a security boundary** — it's purely for user experience. Security is enforced at Layers 1 & 2.
206+
207+
## User Management
208+
209+
Admins can create users with any of the four roles via the User Management page. Each user is:
210+
1. Created in DynamoDB (source of truth)
211+
2. Synced to Cognito (for authentication)
212+
3. Added to the appropriate Cognito group (for authorization)
213+
4. Optionally assigned `allowedConfigVersions` for config-version scoping
214+
215+
### User Table Fields
216+
217+
| Field | Description |
218+
|-------|-------------|
219+
| `userId` | Unique identifier (UUID) |
220+
| `email` | User's email address (used as Cognito username) |
221+
| `persona` | Role: Admin, Author, Reviewer, or Viewer |
222+
| `status` | User status (active) |
223+
| `allowedConfigVersions` | Optional list of config version names for scoping |
224+
| `createdAt` | Creation timestamp |
225+
226+
## Architecture
227+
228+
```
229+
┌─────────────────────────────────┐
230+
│ Browser (UI) │ Layer 3: Navigation/button hiding + scope filtering (UX only)
231+
│ useUserRole + getMyProfile │
232+
│ useConfigurationVersions │ ← Filters versions by allowedConfigVersions
233+
└────────────┬────────────────────┘
234+
│ GraphQL
235+
┌────────────▼────────────────────┐
236+
│ AppSync API │ Layer 1: @aws_auth directives (DENY if wrong group)
237+
│ Schema Directives │
238+
└────────────┬────────────────────┘
239+
240+
┌────────────▼────────────────────┐
241+
│ Lambda Resolvers │ Layer 2: Server-side filtering
242+
│ • listDocuments: ConfigVersion │ ← Filters by allowedConfigVersions from UsersTable
243+
│ • getConfigVersions: scope │ ← Filters version list
244+
│ • getConfigVersion: scope │ ← Rejects out-of-scope access
245+
│ • listUsers: self-only │ ← Non-admin sees only own profile
246+
└────────────┬────────────────────┘
247+
248+
┌────────────▼────────────────────┐
249+
│ DynamoDB │
250+
│ TrackingTable (documents) │
251+
│ ConfigurationTable (versions) │
252+
│ UsersTable (scope data) │
253+
└─────────────────────────────────┘
254+
```
255+
256+
## Adding New Roles
257+
258+
To add a new role:
259+
1. Add a `AWS::Cognito::UserPoolGroup` in `template.yaml`
260+
2. Add the group name to relevant `@aws_auth` directives in `schema.graphql`
261+
3. Update the `VALID_PERSONAS` dict in `src/lambda/user_management/index.py`
262+
4. Add role detection in `src/ui/src/hooks/use-user-role.ts`
263+
5. Add navigation items in `src/ui/src/components/genaiidp-layout/navigation.tsx`
264+
6. Pass the new group as an environment variable to the UserManagement Lambda
265+
266+
## Known Limitations
267+
268+
- **Knowledge Base queries** do not currently enforce config-version scope. KB results may include documents from out-of-scope config versions.
269+
- **Agent Companion Chat** analytics queries (Athena) do not filter by config-version scope.
270+
- **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.
271+
- These limitations are tracked for Phase 3 implementation.

lib/idp_common_pkg/idp_common/appsync/mutations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
HITLReviewURL
5858
ConfidenceAlertCount
5959
TraceId
60+
ConfigVersion
6061
}
6162
}
6263
"""

lib/idp_common_pkg/idp_common/appsync/service.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,8 @@ def _document_to_create_input(
6969
"ExpiresAfter": expires_after,
7070
}
7171

72-
# Add confidence alert count if available
73-
if document.confidence_alert_count > 0:
74-
input_data["ConfidenceAlertCount"] = document.confidence_alert_count
72+
# Note: ConfidenceAlertCount is NOT in CreateDocumentInput schema —
73+
# it gets persisted via the update path in processresults after assessment.
7574

7675
# Add trace_id if available
7776
if document.trace_id:
@@ -238,6 +237,9 @@ def _document_to_update_input(self, document: Document) -> Dict[str, Any]:
238237
if document.hitl_sections_completed:
239238
input_data["HITLSectionsCompleted"] = document.hitl_sections_completed
240239

240+
# Always include ConfidenceAlertCount so it persists to DynamoDB GSI
241+
input_data["ConfidenceAlertCount"] = document.confidence_alert_count
242+
241243
# Add trace_id if available
242244
if document.trace_id:
243245
input_data["TraceId"] = document.trace_id
@@ -282,8 +284,9 @@ def _appsync_to_document(self, appsync_data: Dict[str, Any]) -> Document:
282284
request_id=doc.id or "", output_uri=rule_validation_uri
283285
)
284286

285-
# Handle HITL fields - create HITL metadata if HITL fields are present
287+
# Set Review Status fields from AppSync response
286288
hitl_status = appsync_data.get("HITLStatus")
289+
doc.hitl_status = hitl_status
287290
hitl_review_url = appsync_data.get("HITLReviewURL")
288291
hitl_triggered = appsync_data.get("HITLTriggered")
289292
hitl_completed = appsync_data.get("HITLCompleted")

lib/idp_common_pkg/idp_common/dynamodb/service.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,10 @@ def _document_to_update_expressions(
318318
document.hitl_sections_completed
319319
)
320320

321-
# Add confidence alert count if available
322-
if document.confidence_alert_count > 0:
323-
set_expressions.append("#ConfidenceAlertCount = :ConfidenceAlertCount")
324-
expression_names["#ConfidenceAlertCount"] = "ConfidenceAlertCount"
325-
expression_values[":ConfidenceAlertCount"] = document.confidence_alert_count
321+
# Always persist confidence alert count (even 0) so GSI has it for listDocuments
322+
set_expressions.append("#ConfidenceAlertCount = :ConfidenceAlertCount")
323+
expression_names["#ConfidenceAlertCount"] = "ConfidenceAlertCount"
324+
expression_values[":ConfidenceAlertCount"] = document.confidence_alert_count
326325

327326
# Build update expression with optional REMOVE clause
328327
update_expression = "SET " + ", ".join(set_expressions)

lib/idp_common_pkg/idp_common/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def to_dict(self) -> Dict[str, Any]:
308308
"metering": self.metering,
309309
"trace_id": self.trace_id,
310310
"config_version": self.config_version,
311+
"confidence_alert_count": self.confidence_alert_count,
311312
# We don't include evaluation_result or summarization_result in the dict since they're objects
312313
}
313314

@@ -445,6 +446,9 @@ def from_dict(cls, data: Dict[str, Any]) -> "Document":
445446
document.hitl_sections_pending = data.get("hitl_sections_pending", [])
446447
document.hitl_sections_completed = data.get("hitl_sections_completed", [])
447448

449+
# Restore confidence alert count
450+
document.confidence_alert_count = int(data.get("confidence_alert_count", 0))
451+
448452
# Convert rule_validation_result if present (optional)
449453
if "rule_validation_result" in data:
450454
rv_data = data["rule_validation_result"]

0 commit comments

Comments
 (0)