fix(04-rbac): revise plans based on checker feedback
This commit is contained in:
@@ -16,6 +16,7 @@ files_modified:
|
|||||||
- migrations/versions/006_rbac_roles.py
|
- migrations/versions/006_rbac_roles.py
|
||||||
- tests/unit/test_rbac_guards.py
|
- tests/unit/test_rbac_guards.py
|
||||||
- tests/unit/test_invitations.py
|
- tests/unit/test_invitations.py
|
||||||
|
- tests/unit/test_portal_auth.py
|
||||||
autonomous: true
|
autonomous: true
|
||||||
requirements:
|
requirements:
|
||||||
- RBAC-01
|
- RBAC-01
|
||||||
@@ -54,6 +55,9 @@ must_haves:
|
|||||||
- path: "tests/unit/test_invitations.py"
|
- path: "tests/unit/test_invitations.py"
|
||||||
provides: "Unit tests for HMAC token and invitation flow"
|
provides: "Unit tests for HMAC token and invitation flow"
|
||||||
min_lines: 40
|
min_lines: 40
|
||||||
|
- path: "tests/unit/test_portal_auth.py"
|
||||||
|
provides: "Unit tests for auth/verify endpoint returning role + tenant_ids claims"
|
||||||
|
min_lines: 30
|
||||||
key_links:
|
key_links:
|
||||||
- from: "packages/shared/shared/api/rbac.py"
|
- from: "packages/shared/shared/api/rbac.py"
|
||||||
to: "packages/shared/shared/models/auth.py"
|
to: "packages/shared/shared/models/auth.py"
|
||||||
@@ -266,10 +270,11 @@ class AuditEvent(Base): # Reuse for impersonation logging in Plan 03
|
|||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto" tdd="true">
|
<task type="auto" tdd="true">
|
||||||
<name>Task 3: Unit tests for RBAC guards and invitation system</name>
|
<name>Task 3: Unit tests for RBAC guards, invitation system, and portal auth</name>
|
||||||
<files>
|
<files>
|
||||||
tests/unit/test_rbac_guards.py,
|
tests/unit/test_rbac_guards.py,
|
||||||
tests/unit/test_invitations.py
|
tests/unit/test_invitations.py,
|
||||||
|
tests/unit/test_portal_auth.py
|
||||||
</files>
|
</files>
|
||||||
<behavior>
|
<behavior>
|
||||||
- test_platform_admin_passes: platform_admin caller gets through require_platform_admin
|
- test_platform_admin_passes: platform_admin caller gets through require_platform_admin
|
||||||
@@ -285,6 +290,9 @@ class AuditEvent(Base): # Reuse for impersonation logging in Plan 03
|
|||||||
- test_invite_accept_creates_user: accepting invite creates PortalUser + UserTenantRole
|
- test_invite_accept_creates_user: accepting invite creates PortalUser + UserTenantRole
|
||||||
- test_invite_accept_rejects_expired: expired invitation returns error
|
- test_invite_accept_rejects_expired: expired invitation returns error
|
||||||
- test_invite_resend_updates_token: resend generates new token_hash and extends expires_at
|
- test_invite_resend_updates_token: resend generates new token_hash and extends expires_at
|
||||||
|
- test_auth_verify_returns_role: auth/verify response contains role field (not is_admin)
|
||||||
|
- test_auth_verify_returns_tenant_ids: auth/verify response contains tenant_ids list
|
||||||
|
- test_auth_verify_returns_active_tenant: auth/verify response contains active_tenant_id
|
||||||
</behavior>
|
</behavior>
|
||||||
<action>
|
<action>
|
||||||
Create `tests/unit/test_rbac_guards.py`:
|
Create `tests/unit/test_rbac_guards.py`:
|
||||||
@@ -309,18 +317,27 @@ class AuditEvent(Base): # Reuse for impersonation logging in Plan 03
|
|||||||
- Test invitation acceptance via POST /api/portal/invitations/accept (mock DB session with pending invitation)
|
- Test invitation acceptance via POST /api/portal/invitations/accept (mock DB session with pending invitation)
|
||||||
- Test resend updates token_hash and extends expires_at
|
- Test resend updates token_hash and extends expires_at
|
||||||
|
|
||||||
|
Create `tests/unit/test_portal_auth.py`:
|
||||||
|
- Test the updated auth/verify endpoint returns role (not is_admin) in the response
|
||||||
|
- Test auth/verify returns tenant_ids as a list of UUID strings for a user with UserTenantRole memberships
|
||||||
|
- Test auth/verify returns active_tenant_id as the first tenant ID (or None for users with no memberships)
|
||||||
|
- Test auth/verify for platform_admin returns all tenant IDs from the tenants table
|
||||||
|
- Test auth/verify for customer_admin returns only tenant IDs from their UserTenantRole rows
|
||||||
|
- Mock the DB session with appropriate PortalUser (role field) and UserTenantRole rows
|
||||||
|
- Use httpx.AsyncClient with the app, following existing test patterns (make_app(session) factory)
|
||||||
|
|
||||||
Follow existing test patterns: use `make_app(session)` factory from tests/ or direct httpx.AsyncClient with app.
|
Follow existing test patterns: use `make_app(session)` factory from tests/ or direct httpx.AsyncClient with app.
|
||||||
</action>
|
</action>
|
||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/unit/test_rbac_guards.py tests/unit/test_invitations.py -x -v</automated>
|
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/unit/test_rbac_guards.py tests/unit/test_invitations.py tests/unit/test_portal_auth.py -x -v</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<done>All RBAC guard unit tests pass. All invitation token and API unit tests pass. Coverage includes platform_admin bypass, tenant membership checks, token tampering, and expiry validation.</done>
|
<done>All RBAC guard unit tests pass. All invitation token and API unit tests pass. All portal auth unit tests pass verifying role + tenant_ids claims. Coverage includes platform_admin bypass, tenant membership checks, token tampering, expiry validation, and auth/verify response shape.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
|
|
||||||
<verification>
|
<verification>
|
||||||
- `pytest tests/unit/test_rbac_guards.py tests/unit/test_invitations.py -x -v` — all pass
|
- `pytest tests/unit/test_rbac_guards.py tests/unit/test_invitations.py tests/unit/test_portal_auth.py -x -v` — all pass
|
||||||
- `python -c "from shared.models.auth import UserRole; assert len(UserRole) == 3"` — enum has 3 values
|
- `python -c "from shared.models.auth import UserRole; assert len(UserRole) == 3"` — enum has 3 values
|
||||||
- `python -c "from shared.api.rbac import require_platform_admin"` — guard imports clean
|
- `python -c "from shared.api.rbac import require_platform_admin"` — guard imports clean
|
||||||
- `python -c "from shared.invite_token import generate_invite_token, validate_invite_token"` — token utils import clean
|
- `python -c "from shared.invite_token import generate_invite_token, validate_invite_token"` — token utils import clean
|
||||||
@@ -335,7 +352,7 @@ class AuditEvent(Base): # Reuse for impersonation logging in Plan 03
|
|||||||
- HMAC invite tokens generate and validate with 48h TTL
|
- HMAC invite tokens generate and validate with 48h TTL
|
||||||
- SMTP email utility exists (sync, for Celery)
|
- SMTP email utility exists (sync, for Celery)
|
||||||
- Auth/verify returns role + tenant_ids
|
- Auth/verify returns role + tenant_ids
|
||||||
- All unit tests pass
|
- All unit tests pass (including test_portal_auth.py for JWT callback claims)
|
||||||
</success_criteria>
|
</success_criteria>
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ files_modified:
|
|||||||
- packages/portal/components/nav.tsx
|
- packages/portal/components/nav.tsx
|
||||||
- packages/portal/components/tenant-switcher.tsx
|
- packages/portal/components/tenant-switcher.tsx
|
||||||
- packages/portal/components/impersonation-banner.tsx
|
- packages/portal/components/impersonation-banner.tsx
|
||||||
- packages/portal/app/(dashboard)/invite/[token]/page.tsx
|
- packages/portal/app/invite/[token]/page.tsx
|
||||||
- packages/portal/app/(dashboard)/users/page.tsx
|
- packages/portal/app/(dashboard)/users/page.tsx
|
||||||
- packages/portal/app/(dashboard)/admin/users/page.tsx
|
- packages/portal/app/(dashboard)/admin/users/page.tsx
|
||||||
- packages/portal/app/(dashboard)/layout.tsx
|
- packages/portal/app/(dashboard)/layout.tsx
|
||||||
@@ -52,8 +52,8 @@ must_haves:
|
|||||||
- path: "packages/portal/components/impersonation-banner.tsx"
|
- path: "packages/portal/components/impersonation-banner.tsx"
|
||||||
provides: "Impersonation indicator banner"
|
provides: "Impersonation indicator banner"
|
||||||
min_lines: 15
|
min_lines: 15
|
||||||
- path: "packages/portal/app/(dashboard)/invite/[token]/page.tsx"
|
- path: "packages/portal/app/invite/[token]/page.tsx"
|
||||||
provides: "Invite acceptance page with password form"
|
provides: "Invite acceptance page with password form (outside dashboard layout — no auth required)"
|
||||||
min_lines: 40
|
min_lines: 40
|
||||||
- path: "packages/portal/app/(dashboard)/users/page.tsx"
|
- path: "packages/portal/app/(dashboard)/users/page.tsx"
|
||||||
provides: "Per-tenant user management page"
|
provides: "Per-tenant user management page"
|
||||||
@@ -253,13 +253,13 @@ const navItems = [ /* dashboard, tenants, agents, usage, billing, api-keys */ ];
|
|||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 3: Invite acceptance page + user management pages</name>
|
<name>Task 3: Invite acceptance page + user management pages</name>
|
||||||
<files>
|
<files>
|
||||||
packages/portal/app/(dashboard)/invite/[token]/page.tsx,
|
packages/portal/app/invite/[token]/page.tsx,
|
||||||
packages/portal/app/(dashboard)/users/page.tsx,
|
packages/portal/app/(dashboard)/users/page.tsx,
|
||||||
packages/portal/app/(dashboard)/admin/users/page.tsx
|
packages/portal/app/(dashboard)/admin/users/page.tsx
|
||||||
</files>
|
</files>
|
||||||
<action>
|
<action>
|
||||||
Create `packages/portal/app/(dashboard)/invite/[token]/page.tsx`:
|
Create `packages/portal/app/invite/[token]/page.tsx`:
|
||||||
- NOTE: This page must be accessible WITHOUT authentication. If the (dashboard) layout requires auth, create this at `packages/portal/app/invite/[token]/page.tsx` instead (outside the dashboard layout group). Check existing route structure.
|
- IMPORTANT: This page is created OUTSIDE the (dashboard) route group because the (dashboard) layout enforces authentication. Invite acceptance must be accessible to unauthenticated users who are creating their account.
|
||||||
- Reads `token` from URL params
|
- Reads `token` from URL params
|
||||||
- On load, validates token client-side by calling a GET endpoint or just displays the form (server validates on submit)
|
- On load, validates token client-side by calling a GET endpoint or just displays the form (server validates on submit)
|
||||||
- Form fields: Password (min 8 chars), Confirm Password
|
- Form fields: Password (min 8 chars), Confirm Password
|
||||||
@@ -289,7 +289,7 @@ const navItems = [ /* dashboard, tenants, agents, usage, billing, api-keys */ ];
|
|||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx tsc --noEmit 2>&1 | head -30</automated>
|
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx tsc --noEmit 2>&1 | head -30</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<done>Invite acceptance page renders password form and submits to accept endpoint. Per-tenant users page lists users with invite/resend capability. Platform admin users page shows cross-tenant user list with filters. All pages compile without TypeScript errors.</done>
|
<done>Invite acceptance page renders password form and submits to accept endpoint. Page is outside (dashboard) group so unauthenticated users can access it. Per-tenant users page lists users with invite/resend capability. Platform admin users page shows cross-tenant user list with filters. All pages compile without TypeScript errors.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
@@ -299,6 +299,7 @@ const navItems = [ /* dashboard, tenants, agents, usage, billing, api-keys */ ];
|
|||||||
- `cd packages/portal && npx next build` — build succeeds
|
- `cd packages/portal && npx next build` — build succeeds
|
||||||
- Login as platform_admin: JWT contains role="platform_admin", sees all nav items
|
- Login as platform_admin: JWT contains role="platform_admin", sees all nav items
|
||||||
- Login as customer_operator: does not see Billing/API Keys/Users in nav, /billing redirects to /agents
|
- Login as customer_operator: does not see Billing/API Keys/Users in nav, /billing redirects to /agents
|
||||||
|
- Visit /invite/{token} while logged out: page renders without auth redirect
|
||||||
</verification>
|
</verification>
|
||||||
|
|
||||||
<success_criteria>
|
<success_criteria>
|
||||||
@@ -307,7 +308,7 @@ const navItems = [ /* dashboard, tenants, agents, usage, billing, api-keys */ ];
|
|||||||
- Nav hides restricted items based on role
|
- Nav hides restricted items based on role
|
||||||
- Tenant switcher works for multi-tenant users (no page reload)
|
- Tenant switcher works for multi-tenant users (no page reload)
|
||||||
- Impersonation banner renders when impersonating
|
- Impersonation banner renders when impersonating
|
||||||
- Invite acceptance page accepts token and creates account
|
- Invite acceptance page at /invite/[token] (outside dashboard layout) accepts token and creates account without requiring auth
|
||||||
- User management pages exist for tenant admin and platform admin
|
- User management pages exist for tenant admin and platform admin
|
||||||
- Portal builds and TypeScript compiles clean
|
- Portal builds and TypeScript compiles clean
|
||||||
</success_criteria>
|
</success_criteria>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ must_haves:
|
|||||||
- "Every tenant-scoped endpoint returns 403 for customer_admin accessing a different tenant"
|
- "Every tenant-scoped endpoint returns 403 for customer_admin accessing a different tenant"
|
||||||
- "Platform admin gets 200 on any tenant's endpoints regardless of membership"
|
- "Platform admin gets 200 on any tenant's endpoints regardless of membership"
|
||||||
- "Customer operator gets 200 on read-only endpoints (GET agents, GET usage) for their tenant"
|
- "Customer operator gets 200 on read-only endpoints (GET agents, GET usage) for their tenant"
|
||||||
|
- "Customer operator can POST a test message to an agent (POST /tenants/{tid}/agents/{aid}/test) and get 200"
|
||||||
- "Impersonation actions are logged in audit_events with platform admin user_id"
|
- "Impersonation actions are logged in audit_events with platform admin user_id"
|
||||||
- "Full invite flow works end-to-end: create invitation -> accept -> login -> correct role"
|
- "Full invite flow works end-to-end: create invitation -> accept -> login -> correct role"
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -40,8 +41,8 @@ must_haves:
|
|||||||
key_links:
|
key_links:
|
||||||
- from: "packages/shared/shared/api/portal.py"
|
- from: "packages/shared/shared/api/portal.py"
|
||||||
to: "packages/shared/shared/api/rbac.py"
|
to: "packages/shared/shared/api/rbac.py"
|
||||||
via: "Depends(require_tenant_admin) on mutating endpoints"
|
via: "Depends(require_tenant_admin) on mutating endpoints, Depends(require_tenant_member) on test-message endpoint"
|
||||||
pattern: "Depends\\(require_tenant_admin\\)|Depends\\(require_platform_admin\\)"
|
pattern: "Depends\\(require_tenant_admin\\)|Depends\\(require_platform_admin\\)|Depends\\(require_tenant_member\\)"
|
||||||
- from: "packages/shared/shared/api/billing.py"
|
- from: "packages/shared/shared/api/billing.py"
|
||||||
to: "packages/shared/shared/api/rbac.py"
|
to: "packages/shared/shared/api/rbac.py"
|
||||||
via: "Depends(require_tenant_admin) on billing endpoints"
|
via: "Depends(require_tenant_admin) on billing endpoints"
|
||||||
@@ -53,10 +54,10 @@ must_haves:
|
|||||||
---
|
---
|
||||||
|
|
||||||
<objective>
|
<objective>
|
||||||
Wire RBAC guards to ALL existing portal API endpoints, add impersonation audit logging, add user listing endpoints, and create comprehensive integration tests proving every endpoint enforces role-based authorization.
|
Wire RBAC guards to ALL existing portal API endpoints, add test-message endpoint for operators, add impersonation audit logging, add user listing endpoints, and create comprehensive integration tests proving every endpoint enforces role-based authorization.
|
||||||
|
|
||||||
Purpose: Defense in depth — the UI hides things, but the API MUST enforce authorization. This plan completes the server-side enforcement layer and validates the entire RBAC system end-to-end.
|
Purpose: Defense in depth — the UI hides things, but the API MUST enforce authorization. This plan completes the server-side enforcement layer and validates the entire RBAC system end-to-end.
|
||||||
Output: All portal endpoints guarded, impersonation logged, integration tests for RBAC + invite flow.
|
Output: All portal endpoints guarded, test-message endpoint for operators, impersonation logged, integration tests for RBAC + invite flow.
|
||||||
</objective>
|
</objective>
|
||||||
|
|
||||||
<execution_context>
|
<execution_context>
|
||||||
@@ -104,6 +105,9 @@ POST /api/portal/tenants/{tid}/agents # require_tenant_admin
|
|||||||
GET /api/portal/tenants/{tid}/agents/{aid} # require_tenant_member
|
GET /api/portal/tenants/{tid}/agents/{aid} # require_tenant_member
|
||||||
PUT /api/portal/tenants/{tid}/agents/{aid} # require_tenant_admin
|
PUT /api/portal/tenants/{tid}/agents/{aid} # require_tenant_admin
|
||||||
DELETE /api/portal/tenants/{tid}/agents/{aid} # require_tenant_admin
|
DELETE /api/portal/tenants/{tid}/agents/{aid} # require_tenant_admin
|
||||||
|
|
||||||
|
# NEW — Test message (per locked decision: operators can send test messages)
|
||||||
|
POST /api/portal/tenants/{tid}/agents/{aid}/test # require_tenant_member (operators included)
|
||||||
```
|
```
|
||||||
|
|
||||||
From packages/shared/shared/api/billing.py, channels.py, llm_keys.py, usage.py:
|
From packages/shared/shared/api/billing.py, channels.py, llm_keys.py, usage.py:
|
||||||
@@ -126,7 +130,7 @@ class AuditEvent(Base):
|
|||||||
<tasks>
|
<tasks>
|
||||||
|
|
||||||
<task type="auto">
|
<task type="auto">
|
||||||
<name>Task 1: Wire RBAC guards to all existing API endpoints + impersonation + user listing</name>
|
<name>Task 1: Wire RBAC guards to all existing API endpoints + test-message endpoint + impersonation + user listing</name>
|
||||||
<files>
|
<files>
|
||||||
packages/shared/shared/api/portal.py,
|
packages/shared/shared/api/portal.py,
|
||||||
packages/shared/shared/api/billing.py,
|
packages/shared/shared/api/billing.py,
|
||||||
@@ -149,6 +153,7 @@ class AuditEvent(Base):
|
|||||||
- `GET /tenants/{tenant_id}/agents/{agent_id}` — add `Depends(require_tenant_member)`.
|
- `GET /tenants/{tenant_id}/agents/{agent_id}` — add `Depends(require_tenant_member)`.
|
||||||
- `PUT /tenants/{tenant_id}/agents/{agent_id}` — add `Depends(require_tenant_admin)`.
|
- `PUT /tenants/{tenant_id}/agents/{agent_id}` — add `Depends(require_tenant_admin)`.
|
||||||
- `DELETE /tenants/{tenant_id}/agents/{agent_id}` — add `Depends(require_tenant_admin)`.
|
- `DELETE /tenants/{tenant_id}/agents/{agent_id}` — add `Depends(require_tenant_admin)`.
|
||||||
|
- ADD new endpoint: `POST /tenants/{tenant_id}/agents/{agent_id}/test` — requires `Depends(require_tenant_member)` (NOT require_tenant_admin). This is the test-message endpoint per locked decision: "operators can send test messages to agents." Accepts `{message: str}` body. Dispatches the message through the agent orchestrator pipeline (or a lightweight test handler) for the specified agent, returns the agent's response. This allows operators to QA agent behavior without agent CRUD access.
|
||||||
- ADD new endpoint: `GET /tenants/{tenant_id}/users` — requires require_tenant_admin. Queries UserTenantRole JOIN PortalUser WHERE tenant_id matches. Returns list of {id, name, email, role, created_at}. Also queries PortalInvitation WHERE tenant_id AND status='pending' to include pending invites.
|
- ADD new endpoint: `GET /tenants/{tenant_id}/users` — requires require_tenant_admin. Queries UserTenantRole JOIN PortalUser WHERE tenant_id matches. Returns list of {id, name, email, role, created_at}. Also queries PortalInvitation WHERE tenant_id AND status='pending' to include pending invites.
|
||||||
- ADD new endpoint: `GET /admin/users` — requires require_platform_admin. Queries ALL PortalUser with their UserTenantRole associations. Supports optional query params: tenant_id filter, role filter. Returns list with tenant membership info.
|
- ADD new endpoint: `GET /admin/users` — requires require_platform_admin. Queries ALL PortalUser with their UserTenantRole associations. Supports optional query params: tenant_id filter, role filter. Returns list with tenant membership info.
|
||||||
|
|
||||||
@@ -189,8 +194,11 @@ from shared.api.billing import billing_router
|
|||||||
from shared.api.channels import channels_router
|
from shared.api.channels import channels_router
|
||||||
routes = [r.path for r in portal_router.routes]
|
routes = [r.path for r in portal_router.routes]
|
||||||
print(f'Portal routes: {len(routes)}')
|
print(f'Portal routes: {len(routes)}')
|
||||||
|
# Verify test-message endpoint exists
|
||||||
|
test_routes = [r.path for r in portal_router.routes if 'test' in r.path]
|
||||||
|
assert test_routes, 'Missing /test endpoint for agent test messages'
|
||||||
|
print(f'Test-message route: {test_routes}')
|
||||||
# Verify at least one route has dependencies
|
# Verify at least one route has dependencies
|
||||||
import inspect
|
|
||||||
for r in portal_router.routes:
|
for r in portal_router.routes:
|
||||||
if hasattr(r, 'dependant') and r.dependant.dependencies:
|
if hasattr(r, 'dependant') and r.dependant.dependencies:
|
||||||
print(f' {r.path} has {len(r.dependant.dependencies)} dependencies')
|
print(f' {r.path} has {len(r.dependant.dependencies)} dependencies')
|
||||||
@@ -199,7 +207,7 @@ else:
|
|||||||
print('WARNING: No routes have dependencies')
|
print('WARNING: No routes have dependencies')
|
||||||
"</automated>
|
"</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<done>Every portal API endpoint has an RBAC guard. Mutating endpoints require tenant_admin or platform_admin. Read-only tenant endpoints allow tenant_member. Global endpoints require platform_admin. Impersonation endpoint logs to audit trail. User listing endpoints exist for both per-tenant and global views.</done>
|
<done>Every portal API endpoint has an RBAC guard. Mutating endpoints require tenant_admin or platform_admin. Read-only tenant endpoints allow tenant_member. Test-message endpoint (POST /tenants/{tid}/agents/{aid}/test) allows tenant_member including operators. Global endpoints require platform_admin. Impersonation endpoint logs to audit trail. User listing endpoints exist for both per-tenant and global views.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="auto" tdd="true">
|
<task type="auto" tdd="true">
|
||||||
@@ -212,6 +220,7 @@ else:
|
|||||||
- Platform admin with correct headers gets 200 on all endpoints
|
- Platform admin with correct headers gets 200 on all endpoints
|
||||||
- Customer admin gets 200 on own-tenant endpoints, 403 on other tenants
|
- Customer admin gets 200 on own-tenant endpoints, 403 on other tenants
|
||||||
- Customer operator gets 200 on GET endpoints, 403 on POST/PUT/DELETE
|
- Customer operator gets 200 on GET endpoints, 403 on POST/PUT/DELETE
|
||||||
|
- Customer operator gets 200 on POST /tenants/{tid}/agents/{aid}/test (test message — exception to POST restriction)
|
||||||
- Missing role headers return 401/422 (FastAPI Header() validation)
|
- Missing role headers return 401/422 (FastAPI Header() validation)
|
||||||
- Impersonation endpoint logs AuditEvent row
|
- Impersonation endpoint logs AuditEvent row
|
||||||
- Full invite flow: admin creates invite -> token generated -> accept with password -> new user can login -> new user has correct role and tenant membership
|
- Full invite flow: admin creates invite -> token generated -> accept with password -> new user can login -> new user has correct role and tenant membership
|
||||||
@@ -237,6 +246,7 @@ else:
|
|||||||
| POST /tenants/{tid}/agents | 201 | 201 | 403 | 403 |
|
| POST /tenants/{tid}/agents | 201 | 201 | 403 | 403 |
|
||||||
| PUT /tenants/{tid}/agents/{aid} | 200 | 200 | 403 | 403 |
|
| PUT /tenants/{tid}/agents/{aid} | 200 | 200 | 403 | 403 |
|
||||||
| DELETE /tenants/{tid}/agents/{aid} | 204 | 204 | 403 | 403 |
|
| DELETE /tenants/{tid}/agents/{aid} | 204 | 204 | 403 | 403 |
|
||||||
|
| POST /tenants/{tid}/agents/{aid}/test | 200 | 200 | 403 | 200 |
|
||||||
| GET /tenants/{tid}/users | 200 | 200 | 403 | 403 |
|
| GET /tenants/{tid}/users | 200 | 200 | 403 | 403 |
|
||||||
| GET /admin/users | 200 | 403 | 403 | 403 |
|
| GET /admin/users | 200 | 403 | 403 | 403 |
|
||||||
|
|
||||||
@@ -244,6 +254,7 @@ else:
|
|||||||
- Request with NO role headers -> 422 (missing required header)
|
- Request with NO role headers -> 422 (missing required header)
|
||||||
- Impersonation endpoint creates AuditEvent row
|
- Impersonation endpoint creates AuditEvent row
|
||||||
- Billing, channels, llm_keys, usage endpoints follow same pattern (at least one representative test per router)
|
- Billing, channels, llm_keys, usage endpoints follow same pattern (at least one representative test per router)
|
||||||
|
- Specific test: customer_operator can POST to /test endpoint but NOT to agent CRUD POST
|
||||||
|
|
||||||
Create `tests/integration/test_invite_flow.py`:
|
Create `tests/integration/test_invite_flow.py`:
|
||||||
- Set up: create a tenant, create a customer_admin user with membership
|
- Set up: create a tenant, create a customer_admin user with membership
|
||||||
@@ -263,7 +274,7 @@ else:
|
|||||||
<verify>
|
<verify>
|
||||||
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/integration/test_portal_rbac.py tests/integration/test_invite_flow.py -x -v</automated>
|
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/integration/test_portal_rbac.py tests/integration/test_invite_flow.py -x -v</automated>
|
||||||
</verify>
|
</verify>
|
||||||
<done>All RBAC integration tests pass — every endpoint returns correct status code for each role. Full invite flow works end-to-end. Expired invites are rejected. Resend works. Double-accept prevented.</done>
|
<done>All RBAC integration tests pass — every endpoint returns correct status code for each role. Operator test-message endpoint returns 200. Full invite flow works end-to-end. Expired invites are rejected. Resend works. Double-accept prevented.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
<task type="checkpoint:human-verify" gate="blocking">
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
@@ -279,8 +290,9 @@ else:
|
|||||||
- Verify: can access /admin/users (global user management)
|
- Verify: can access /admin/users (global user management)
|
||||||
- Verify: can impersonate a tenant (banner appears, can exit)
|
- Verify: can impersonate a tenant (banner appears, can exit)
|
||||||
4. Create a customer_admin invite from the Users page
|
4. Create a customer_admin invite from the Users page
|
||||||
5. Open the invite link in an incognito window
|
5. Open the invite link in an incognito window (URL will be /invite/{token} — NOT inside dashboard)
|
||||||
- Verify: activation page shows, can set password
|
- Verify: activation page shows without requiring login
|
||||||
|
- Verify: can set password and complete account creation
|
||||||
- Verify: after activation, redirected to login
|
- Verify: after activation, redirected to login
|
||||||
6. Login as the new customer admin:
|
6. Login as the new customer admin:
|
||||||
- Verify: sees Dashboard, Employees, Usage, Billing, API Keys, Users (no Tenants, no Platform)
|
- Verify: sees Dashboard, Employees, Usage, Billing, API Keys, Users (no Tenants, no Platform)
|
||||||
@@ -290,11 +302,12 @@ else:
|
|||||||
- Verify: sees only Employees and Usage in nav
|
- Verify: sees only Employees and Usage in nav
|
||||||
- Verify: navigating to /billing redirects to /agents
|
- Verify: navigating to /billing redirects to /agents
|
||||||
- Verify: cannot see Billing, API Keys, Users in sidebar
|
- Verify: cannot see Billing, API Keys, Users in sidebar
|
||||||
|
- Verify: can click "Test" on an agent to send a test message (per locked decision)
|
||||||
9. If user has multiple tenants, verify tenant switcher appears and switches context
|
9. If user has multiple tenants, verify tenant switcher appears and switches context
|
||||||
10. Run: `pytest tests/ -x` — all tests pass
|
10. Run: `pytest tests/ -x` — all tests pass
|
||||||
</action>
|
</action>
|
||||||
<verify>Human confirms all verification steps pass or reports issues</verify>
|
<verify>Human confirms all verification steps pass or reports issues</verify>
|
||||||
<done>All three roles behave correctly in portal UI and API. Invitation flow works end-to-end. Full test suite green.</done>
|
<done>All three roles behave correctly in portal UI and API. Operators can send test messages but not edit agents. Invitation flow works end-to-end. Full test suite green.</done>
|
||||||
</task>
|
</task>
|
||||||
|
|
||||||
</tasks>
|
</tasks>
|
||||||
@@ -305,16 +318,18 @@ else:
|
|||||||
- `pytest tests/ -x` — entire test suite green (no regressions)
|
- `pytest tests/ -x` — entire test suite green (no regressions)
|
||||||
- Every mutating endpoint returns 403 without proper role headers
|
- Every mutating endpoint returns 403 without proper role headers
|
||||||
- Platform admin bypasses all tenant membership checks
|
- Platform admin bypasses all tenant membership checks
|
||||||
|
- Operator gets 200 on POST /tenants/{tid}/agents/{aid}/test
|
||||||
</verification>
|
</verification>
|
||||||
|
|
||||||
<success_criteria>
|
<success_criteria>
|
||||||
- All portal API endpoints enforce role-based authorization via Depends() guards
|
- All portal API endpoints enforce role-based authorization via Depends() guards
|
||||||
- Customer operators cannot mutate any data via API (403 on POST/PUT/DELETE)
|
- Customer operators cannot mutate any data via API (403 on POST/PUT/DELETE) EXCEPT test-message endpoint
|
||||||
|
- Customer operators CAN send test messages to agents (POST /tenants/{tid}/agents/{aid}/test returns 200)
|
||||||
- Customer admins can only access their own tenant's data (403 on other tenants)
|
- Customer admins can only access their own tenant's data (403 on other tenants)
|
||||||
- Platform admin has unrestricted access to all endpoints
|
- Platform admin has unrestricted access to all endpoints
|
||||||
- Impersonation actions logged in audit_events table
|
- Impersonation actions logged in audit_events table
|
||||||
- User listing endpoints exist for per-tenant and global views
|
- User listing endpoints exist for per-tenant and global views
|
||||||
- Integration tests comprehensively cover the RBAC matrix
|
- Integration tests comprehensively cover the RBAC matrix including test-message operator access
|
||||||
- Full invite flow works end-to-end in integration tests
|
- Full invite flow works end-to-end in integration tests
|
||||||
- Human verification confirms visual role-based behavior in portal
|
- Human verification confirms visual role-based behavior in portal
|
||||||
</success_criteria>
|
</success_criteria>
|
||||||
|
|||||||
Reference in New Issue
Block a user