This document provides a testing plan for the permission changes in the arch-2025 branch, which replaces scope-based authorization with permission-based authorization.
Change Summary: @RequiredScope decorator → @RequiredPermission decorator
Create test users with the following role assignments:
| User | Role | Scope | Purpose |
|---|---|---|---|
| User A | (none) | - | Baseline - should be denied everywhere |
| User B | Viewer | Global | Read access to all resources |
| User C | Editor | Global | Full CRUD access to all resources |
| User D | SurveyTemplateEditor | Org: org-123 |
Survey template management |
| User E | WellnessScoreEditor | Org: org-123 |
Wellness score management |
| User F | RiskClassificationEditor | Org: org-123 |
Risk classification uploads |
| User G | PlatformEditor | Org: org-123 |
Platform management |
| User H | SurveyEditor | Org: org-123 |
Survey management |
| User I | MemberEditor | Org: org-123 |
Member data management |
Role Inheritance:
- All editor roles inherit from
Viewer MemberEditorinherits fromMemberViewer(which inherits fromViewer)
Before testing, verify the migration has been applied. Connect to your database and run:
-- Verify new roles exist
SELECT name FROM role
WHERE name IN (
'RiskClassificationEditor', 'SurveyTemplateEditor', 'WellnessScoreEditor',
'PlatformViewer', 'PlatformEditor', 'SurveyViewer', 'SurveyEditor',
'MemberViewer', 'MemberEditor'
) ORDER BY name;
-- Verify role inheritance
SELECT child.name AS role, parent.name AS inherits_from
FROM role_inheritance ri
JOIN role parent ON ri.inherited_role_id = parent.id
JOIN role child ON ri.role_id = child.id
WHERE child.name LIKE '%Editor' OR child.name LIKE '%Viewer'
ORDER BY child.name;For each endpoint, we list:
- 2XX: Users who should succeed
- 403: Users who should be forbidden
Base Path: /survey-templates
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | /survey-templates |
Anyone (no auth required) | - |
| GET | /survey-templates/:id |
Anyone (no auth required) | - |
| POST | /survey-templates |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
| PUT | /survey-templates/:id |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
| DELETE | /survey-templates/:id |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
Base Path: /survey-templates/:surveyTemplateId/sections
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../sections |
Anyone (no auth required) | - |
| GET | .../sections/:id |
Anyone (no auth required) | - |
| POST | .../sections |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
| PUT | .../sections/:id |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
| DELETE | .../sections/:id |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
Base Path: /survey-templates/:surveyTemplateId/sections/:sectionId/questions
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../questions |
Anyone (no auth required) | - |
| GET | .../questions/:id |
Anyone (no auth required) | - |
| POST | .../questions |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
| PUT | .../questions/:id |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
| DELETE | .../questions/:id |
User C (Editor), User D (SurveyTemplateEditor) | User A, User B (Viewer) |
Base Path: /risk-classifications
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| POST | /risk-classifications/file |
User C (Editor), User F (RiskClassificationEditor) | User A, User B (Viewer) |
Base Path: /wellness-scores/definitions
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | /wellness-scores/definitions |
Anyone (no auth required) | - |
| GET | /wellness-scores/definitions/:id |
Anyone (no auth required) | - |
| POST | /wellness-scores/definitions |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
| PUT | /wellness-scores/definitions/:id |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
| DELETE | /wellness-scores/definitions/:id |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
Base Path: /wellness-scores/definitions/:definitionId/categories
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../categories |
Anyone (no auth required) | - |
| GET | .../categories/:id |
Anyone (no auth required) | - |
| POST | .../categories |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
| PUT | .../categories/:id |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
| DELETE | .../categories/:id |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
Base Path: /wellness-scores/definitions/:definitionId/categories/:categoryId/rubrics
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../rubrics |
Anyone (no auth required) | - |
| GET | .../rubrics/:id |
Anyone (no auth required) | - |
| POST | .../rubrics |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
| PUT | .../rubrics/:id |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
| DELETE | .../rubrics/:id |
User C (Editor), User E (WellnessScoreEditor) | User A, User B (Viewer) |
Base Path: /organizations/:orgId/surveys
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../surveys |
User B (Viewer), User C (Editor), User H (SurveyEditor) | User A |
| GET | .../surveys/:id |
User B (Viewer), User C (Editor), User H (SurveyEditor) | User A |
| POST | .../surveys |
User C (Editor), User H (SurveyEditor) | User A, User B (Viewer) |
| PUT | .../surveys/:id |
User C (Editor), User H (SurveyEditor) | User A, User B (Viewer) |
| DELETE | .../surveys/:id |
User C (Editor), User H (SurveyEditor) | User A, User B (Viewer) |
Base Path: /platforms
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | /platforms |
User B (Viewer), User C (Editor), User G (PlatformEditor) | User A |
| GET | /platforms/:id |
User B (Viewer), User C (Editor), User G (PlatformEditor) | User A |
| POST | /platforms |
User C (Editor), User G (PlatformEditor) | User A, User B (Viewer) |
| PUT | /platforms/:id |
User C (Editor), User G (PlatformEditor) | User A, User B (Viewer) |
| DELETE | /platforms/:id |
User C (Editor), User G (PlatformEditor) | User A, User B (Viewer) |
Base Path: /provider-search
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | /provider-search/members/:id/search-capabilities |
User B (Viewer), User C (Editor) | User A |
| GET | /provider-search/members/:id/specialties |
User B (Viewer), User C (Editor) | User A |
| PUT | /provider-search/members/:id/search |
User B (Viewer), User C (Editor) | User A |
Base Path: /members/:memberId/addresses
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../addresses |
User B (Viewer), User C (Editor), User I (MemberEditor) | User A |
| POST | .../addresses |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
| PUT | .../addresses/:id |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
| DELETE | .../addresses/:id |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
Base Path: /members/:memberId/communication-preferences
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../communication-preferences |
User B (Viewer), User C (Editor), User I (MemberEditor) | User A |
| POST | .../communication-preferences |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
| PUT | .../communication-preferences/:id |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
| DELETE | .../communication-preferences/:id |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
Base Path: /platforms/:platformId/members
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../members |
User B (Viewer), User C (Editor), User I (MemberEditor) | User A |
| GET | .../members/:id |
User B (Viewer), User C (Editor), User I (MemberEditor) | User A |
| POST | .../members |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
| PUT | .../members/:id |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
| DELETE | .../members/:id |
User C (Editor), User I (MemberEditor) | User A, User B (Viewer) |
Base Path: /members/:externalId/mappings
| Method | Endpoint | 2XX (Success) | 403 (Forbidden) |
|---|---|---|---|
| GET | .../mappings |
User B (Viewer), User C (Editor), User I (MemberEditor) | User A |
- User A (no role) is denied on all protected endpoints
- User B (Viewer) can read but not write
- User C (Editor) can read and write everything
- User D (SurveyTemplateEditor) can only write survey templates/sections/questions
- User E (WellnessScoreEditor) can only write wellness scores/categories/rubrics
- User F (RiskClassificationEditor) can only upload risk classification files
- User G (PlatformEditor) can only write platforms
- User H (SurveyEditor) can only write surveys
- User I (MemberEditor) can only write member data
- SurveyTemplateEditor inherits Viewer (can read surveys, platforms, etc.)
- WellnessScoreEditor inherits Viewer
- RiskClassificationEditor inherits Viewer
- PlatformEditor inherits Viewer
- SurveyEditor inherits Viewer
- MemberEditor inherits MemberViewer inherits Viewer
- Expired tokens return 401
- Missing auth header returns 401 (on protected endpoints)
- Invalid tokens return 401
- Non-existent resources return 404 (not 403)
403 when expecting 200:
-- Check user's role assignments
SELECT r.name, ur.is_global, ur.organization_id
FROM user_role ur
JOIN role r ON ur.role_id = r.id
WHERE ur.user_id = '<user-id>';
-- Check role's permissions
SELECT p.name
FROM role_permission rp
JOIN permission p ON rp.permission_id = p.id
WHERE rp.role_id = (SELECT id FROM role WHERE name = '<role-name>');401 errors: Verify token is valid and not expired
500 errors: Check API logs; may indicate migration wasn't applied