Skip to content

Commit 51845df

Browse files
authored
Merge add multi tenancy support in taskflow api and fix org switcher pull request #22 from mjunaidca/009-multi-tenancy
add multi tenancy #20
2 parents 1771277 + 322dcac commit 51845df

21 files changed

Lines changed: 2321 additions & 102 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
id: "0001"
3+
title: "Multi-Tenancy Specification"
4+
stage: spec
5+
date: "2025-12-10"
6+
surface: agent
7+
model: claude-opus-4-5-20251101
8+
feature: multi-tenancy
9+
branch: 009-multi-tenancy
10+
user: system
11+
command: /sp.specify multi-tenancy
12+
labels: ["multi-tenancy", "api", "security", "project-isolation"]
13+
links:
14+
spec: specs/009-multi-tenancy/spec.md
15+
ticket: null
16+
adr: null
17+
pr: null
18+
files:
19+
- specs/009-multi-tenancy/spec.md
20+
- specs/009-multi-tenancy/checklists/requirements.md
21+
tests: []
22+
---
23+
24+
# Multi-Tenancy Specification
25+
26+
## Prompt
27+
28+
User requested via /sp.orchestrate to implement multi-tenancy using the SDD-RI loop. The objective is to add tenant_id scoping to the TaskFlow API so projects are isolated by organization, using a default tenant ("taskflow") for all users to avoid SSO changes.
29+
30+
Key requirements specified:
31+
1. Add tenant_id field to Project model with default "taskflow"
32+
2. Update schemas for tenant_id visibility in create/read
33+
3. Add get_tenant_id() helper function for tenant extraction
34+
4. Filter all project queries by tenant_id
35+
5. Handle edge cases: cross-tenant 404 (not 403), slug uniqueness per tenant, dev mode header override
36+
37+
## Response
38+
39+
Created comprehensive specification at `specs/009-multi-tenancy/spec.md` with:
40+
41+
**User Stories (4 total)**:
42+
- P1: Project Isolation by Tenant (core security boundary)
43+
- P1: Default Tenant for Existing Users (backward compatibility)
44+
- P2: Unique Project Slugs Per Tenant (UX improvement)
45+
- P3: Dev Mode Tenant Override (developer tooling)
46+
47+
**Functional Requirements (10 total)**:
48+
- FR-001 through FR-010 covering model changes, query filtering, tenant extraction, security responses, and audit logging
49+
50+
**Edge Cases (6 total)**:
51+
- 404 vs 403 for cross-tenant access
52+
- JWT vs header tenant priority
53+
- Empty string handling
54+
- Existing data migration
55+
- Production mode header blocking
56+
- Audit log inheritance
57+
58+
**Validation Results**:
59+
- spec-architect verdict: READY FOR PLANNING
60+
- Quality score: 9.5/10
61+
- All 5 formal invariants verified
62+
- No clarifications needed
63+
64+
## Outcome
65+
66+
SPECIFICATION COMPLETE - Ready for /sp.plan
67+
68+
## Evaluation
69+
70+
| Criterion | Status |
71+
|-----------|--------|
72+
| Spec created | PASS |
73+
| Checklist generated | PASS |
74+
| Formal verification | PASS |
75+
| Evals-first pattern | PASS |
76+
| Ready for planning | PASS |
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
id: "0002"
3+
title: "Multi-Tenancy Implementation Planning"
4+
stage: plan
5+
date: "2025-12-10"
6+
surface: agent
7+
model: claude-opus-4-5-20251101
8+
feature: multi-tenancy
9+
branch: 009-multi-tenancy
10+
user: system
11+
command: /sp.plan multi-tenancy
12+
labels: ["multi-tenancy", "api", "architecture", "planning"]
13+
links:
14+
spec: specs/009-multi-tenancy/spec.md
15+
plan: specs/009-multi-tenancy/plan.md
16+
ticket: null
17+
adr: null
18+
pr: null
19+
files:
20+
- specs/009-multi-tenancy/plan.md
21+
tests: []
22+
---
23+
24+
# Multi-Tenancy Implementation Planning
25+
26+
## Prompt
27+
28+
Execute implementation planning for the multi-tenancy feature following the SDD-RI workflow. Work type classified as ENGINEERING (API feature with data model changes). Route to general-purpose subagent for architectural decomposition.
29+
30+
## Response
31+
32+
Created comprehensive implementation plan at `specs/009-multi-tenancy/plan.md` with:
33+
34+
**Constitutional Compliance** (All 5 Checks Passed):
35+
1. Audit Check: All project operations include tenant context in audit entries
36+
2. Agent Parity: Agents inherit tenant filtering through API layer
37+
3. Recursive Tasks: Tasks inherit tenant through project relationship
38+
4. Spec-Driven: Spec exists and validated
39+
5. Phase Continuity: Data model designed for permanence across all phases
40+
41+
**6 Key Technical Decisions**:
42+
1. Tenant ID Format: String identifier (VARCHAR(100)) for human-readability
43+
2. Storage Strategy: Embedded tenant_id (denormalized, no JOINs)
44+
3. Slug Uniqueness: Per-tenant UNIQUE(tenant_id, slug)
45+
4. Dev Mode Override: X-Tenant-ID header only in dev mode
46+
5. Audit Context: tenant_id in JSONB details field
47+
6. Task Scoping: Derived from Project relationship
48+
49+
**5 Implementation Components**:
50+
1. Data Model Changes (Project.tenant_id field)
51+
2. Tenant Context Extraction (get_tenant_id() function)
52+
3. Schema Updates (ProjectRead.tenant_id)
53+
4. Project Router Filtering (5 endpoints)
54+
5. Audit Log Integration
55+
56+
**4 Implementation Phases** (2 hours total):
57+
- Phase 1: Foundation (30 min) - Data model + tenant extraction
58+
- Phase 2: Schema Updates (15 min) - API response changes
59+
- Phase 3: Tenant Filtering (45 min) - All 5 endpoints
60+
- Phase 4: Audit & Testing (30 min) - Verification
61+
62+
**Security Model**:
63+
- 404 (not 403) for cross-tenant access prevents enumeration
64+
- Indexed tenant_id for query performance
65+
- Dev mode flag required for header override
66+
67+
## Outcome
68+
69+
PLANNING COMPLETE - Ready for /sp.tasks
70+
71+
## Evaluation
72+
73+
| Criterion | Status |
74+
|-----------|--------|
75+
| Plan created | PASS |
76+
| Constitutional compliance | PASS |
77+
| Component breakdown | PASS |
78+
| Implementation sequence | PASS |
79+
| Risk analysis | PASS |
80+
| Deployment checklist | PASS |
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
id: "0003"
3+
title: "Multi-Tenancy Task Breakdown"
4+
stage: tasks
5+
date: "2025-12-10"
6+
surface: agent
7+
model: claude-opus-4-5-20251101
8+
feature: multi-tenancy
9+
branch: 009-multi-tenancy
10+
user: system
11+
command: /sp.tasks multi-tenancy
12+
labels: ["multi-tenancy", "tasks", "breakdown", "implementation"]
13+
links:
14+
spec: specs/009-multi-tenancy/spec.md
15+
plan: specs/009-multi-tenancy/plan.md
16+
tasks: specs/009-multi-tenancy/tasks.md
17+
ticket: null
18+
adr: null
19+
pr: null
20+
files:
21+
- specs/009-multi-tenancy/tasks.md
22+
tests: []
23+
---
24+
25+
# Multi-Tenancy Task Breakdown
26+
27+
## Prompt
28+
29+
Generate actionable tasks from the multi-tenancy spec and plan. Tasks should be organized by user story to enable independent implementation and testing.
30+
31+
## Response
32+
33+
Created comprehensive task breakdown at `specs/009-multi-tenancy/tasks.md` with:
34+
35+
**7 Phases**:
36+
1. Setup (2 tasks) - Prepare for implementation
37+
2. Foundation (4 tasks) - Data model + tenant extraction (BLOCKING)
38+
3. US1: Tenant Isolation (8 tasks) - Core feature
39+
4. US2: Default Tenant (3 tasks) - Backward compatibility
40+
5. US3: Slug Uniqueness (2 tasks) - Per-tenant naming
41+
6. US4: Dev Mode (2 tasks) - Testing workflow
42+
7. Polish (4 tasks) - Audit trail + comprehensive testing
43+
44+
**Task Summary**:
45+
- Total Tasks: 25
46+
- Parallel Opportunities: 8 tasks
47+
- MVP Scope: Phases 1-4 (17 tasks)
48+
49+
**User Story Task Distribution**:
50+
- US1 (Tenant Isolation): 8 tasks including tests
51+
- US2 (Default Tenant): 3 tasks including tests
52+
- US3 (Slug Uniqueness): 2 tasks including tests
53+
- US4 (Dev Mode): 2 tasks including tests
54+
55+
**Key Files Modified**:
56+
- `packages/api/src/taskflow_api/models/project.py` - tenant_id field
57+
- `packages/api/src/taskflow_api/auth.py` - get_tenant_id()
58+
- `packages/api/src/taskflow_api/schemas/project.py` - ProjectRead update
59+
- `packages/api/src/taskflow_api/routers/projects.py` - all endpoint filtering
60+
- `packages/api/tests/test_multitenancy.py` - new test file
61+
62+
## Outcome
63+
64+
TASK BREAKDOWN COMPLETE - Ready for /sp.implement
65+
66+
## Evaluation
67+
68+
| Criterion | Status |
69+
|-----------|--------|
70+
| Tasks created | PASS |
71+
| Checklist format | PASS |
72+
| User story organization | PASS |
73+
| Parallel opportunities | PASS |
74+
| MVP scope defined | PASS |
75+
| Dependencies documented | PASS |
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
id: 0004
3+
title: Multi-Tenancy Implementation Complete
4+
stage: green
5+
date: 2025-12-10
6+
surface: agent
7+
model: claude-opus-4-5-20251101
8+
feature: multi-tenancy
9+
branch: 009-multi-tenancy
10+
user: system
11+
command: /sp.implement
12+
labels: [multi-tenancy, tenant-isolation, api, security, implementation]
13+
links:
14+
spec: specs/009-multi-tenancy/spec.md
15+
ticket: null
16+
adr: null
17+
pr: null
18+
files:
19+
- packages/api/src/taskflow_api/models/project.py
20+
- packages/api/src/taskflow_api/auth.py
21+
- packages/api/src/taskflow_api/schemas/project.py
22+
- packages/api/src/taskflow_api/routers/projects.py
23+
- packages/api/src/taskflow_api/tests/test_multitenancy.py
24+
tests:
25+
- test_list_projects_tenant_isolation
26+
- test_get_project_wrong_tenant_returns_404
27+
- test_create_project_sets_tenant
28+
- test_default_tenant_fallback
29+
- test_slug_unique_per_tenant
30+
---
31+
32+
## Prompt
33+
34+
Implementation of multi-tenancy feature following the SDD-RI Loop workflow. Execute tasks T003-T025 from specs/009-multi-tenancy/tasks.md to implement tenant isolation for projects.
35+
36+
Key implementation requirements:
37+
1. Add tenant_id field to Project model with default "taskflow"
38+
2. Update CurrentUser to extract tenant_id from JWT claims
39+
3. Create get_tenant_id() function with priority: JWT → X-Tenant-ID header (dev mode) → default
40+
4. Add tenant_id to ProjectRead schema
41+
5. Update all 5 project endpoints with tenant filtering
42+
6. Return 404 (not 403) for cross-tenant access
43+
7. Per-tenant slug uniqueness (not global)
44+
8. Include tenant_id in all audit log entries
45+
46+
## Response snapshot
47+
48+
Successfully implemented all multi-tenancy features across 5 files:
49+
50+
**Model Changes (project.py:21-26)**
51+
- Added `tenant_id: str` field with default "taskflow", max_length=100, indexed
52+
- Removed `unique=True` from slug field (now per-tenant unique)
53+
54+
**Auth Changes (auth.py:152-155, 192-223)**
55+
- Updated `CurrentUser` class to extract tenant_id from `tenant_id` or `organization_id` JWT claims
56+
- Added `get_tenant_id()` function implementing priority: JWT claim → X-Tenant-ID header (dev mode only) → default "taskflow"
57+
- Empty string handling via `.strip()` check
58+
59+
**Schema Changes (project.py:37)**
60+
- Added `tenant_id: str` to `ProjectRead` response schema
61+
62+
**Router Changes (projects.py)**
63+
- Added imports for `Request` and `get_tenant_id`
64+
- `list_projects()`: Added tenant filter to join query (lines 34, 42-43)
65+
- `create_project()`: Set tenant on creation, per-tenant slug check (lines 102, 105-114, 118)
66+
- `get_project()`: Tenant filter returns 404 for cross-tenant (lines 179, 182-191)
67+
- `update_project()`: Tenant filter in query (lines 240, 243-251)
68+
- `delete_project()`: Tenant filter in query (lines 322, 325-333)
69+
- All audit logs include tenant_id in details (lines 145, 278, 381)
70+
71+
**Tests (test_multitenancy.py)**
72+
- 5 comprehensive tests covering all user stories
73+
- All tests pass using X-Tenant-ID header in dev mode
74+
75+
## Outcome
76+
77+
- ✅ Impact: Multi-tenancy fully operational - projects isolated by tenant with backward compatibility
78+
- 🧪 Tests: 61 passed (5 new + 56 existing, no regressions)
79+
- 📁 Files: 5 files modified/created
80+
- 🔁 Next prompts: Commit changes, create PR
81+
- 🧠 Reflection: Clean implementation following spec precisely. The dev mode header override enabled testing without JWT manipulation.
82+
83+
## Evaluation notes (flywheel)
84+
85+
- Failure modes observed: None - implementation followed spec exactly
86+
- Graders run and results (PASS/FAIL): All 61 tests PASS
87+
- Prompt variant (if applicable): SDD-RI Loop with spec-architect validation
88+
- Next experiment (smallest change to try): Consider adding index on (tenant_id, slug) composite for query performance

packages/api/src/taskflow_api/auth.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Any
1313

1414
import httpx
15-
from fastapi import Depends, HTTPException, status
15+
from fastapi import Depends, HTTPException, Request, status
1616
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
1717
from jose import JWTError, jwt
1818

@@ -141,14 +141,18 @@ class CurrentUser:
141141
- name: Display name
142142
- role: "user" | "admin"
143143
- tenant_id: Primary organization (optional)
144+
- organization_id: Alternative tenant claim (optional)
144145
"""
145146

146147
def __init__(self, payload: dict[str, Any]) -> None:
147148
self.id: str = payload.get("sub", "")
148149
self.email: str = payload.get("email", "")
149150
self.name: str = payload.get("name", "")
150151
self.role: str = payload.get("role", "user")
151-
self.tenant_id: str | None = payload.get("tenant_id")
152+
# Extract tenant from multiple possible JWT claims
153+
self.tenant_id: str | None = (
154+
payload.get("tenant_id") or payload.get("organization_id") or None
155+
)
152156

153157
def __repr__(self) -> str:
154158
return f"CurrentUser(id={self.id!r}, email={self.email!r})"
@@ -183,3 +187,37 @@ async def list_projects(user: CurrentUser = Depends(get_current_user)):
183187
user = CurrentUser(payload)
184188
logger.info("[AUTH] Authenticated user: %s", user)
185189
return user
190+
191+
192+
def get_tenant_id(user: CurrentUser, request: Request | None = None) -> str:
193+
"""Extract tenant context from JWT or request headers.
194+
195+
Priority:
196+
1. JWT claim: tenant_id or organization_id
197+
2. X-Tenant-ID header (dev mode only)
198+
3. Default: "taskflow"
199+
200+
Args:
201+
user: Authenticated user from JWT
202+
request: FastAPI request (for header access in dev mode)
203+
204+
Returns:
205+
Tenant identifier string (never empty)
206+
"""
207+
# Priority 1: JWT claim
208+
if user.tenant_id:
209+
tenant = user.tenant_id.strip()
210+
if tenant:
211+
logger.debug("[TENANT] Using JWT tenant_id: %s", tenant)
212+
return tenant
213+
214+
# Priority 2: Dev mode header override
215+
if request and settings.dev_mode:
216+
header_tenant = request.headers.get("X-Tenant-ID", "").strip()
217+
if header_tenant:
218+
logger.debug("[TENANT] Using dev mode header: %s", header_tenant)
219+
return header_tenant
220+
221+
# Priority 3: Default tenant
222+
logger.debug("[TENANT] Using default tenant: taskflow-default-org-id")
223+
return "taskflow-default-org-id"

0 commit comments

Comments
 (0)