234 lines
7.2 KiB
Markdown
234 lines
7.2 KiB
Markdown
# Migration Guide
|
|
|
|
## Overview
|
|
|
|
This document describes how products (Bermooda, Hamsoo, new products) connect to the Identity Platform, and how the platform evolved from a product-coupled UserManager to an independent identity service.
|
|
|
|
## From UserManager → Central Identity Platform
|
|
|
|
### Phase 1: Architecture Audit (DONE)
|
|
|
|
The audit confirmed:
|
|
|
|
1. **No global roles on User**: The `identity.User` model has NO `employer`/`job_seeker` fields. Roles are contextual via `Membership.role` (owner/admin/member).
|
|
2. **No product-domain models in identity layer**: No `Workspace`, `BusinessProfile`, `Employee`, `Payroll`, `Project`, `Resume` tables in the identity database.
|
|
3. **No product-specific fields in JWT**: The JWT contains only `user_id`, `user_status`, `identity_key`, and optional `product_key`/`organization_id` (context, not domain).
|
|
4. **Organization is generic**: `identity.Organization` has no Bermooda-specific fields (like industry, hiring settings).
|
|
|
|
The project was **already architected correctly** — it was built as an independent platform rather than a product-coupled service.
|
|
|
|
### Phase 2-5: Domain Separation (ALREADY DONE)
|
|
|
|
All product-specific concepts were **never tightly coupled** — the platform was designed with clear boundaries from the start. No destructive changes are needed.
|
|
|
|
### Phase 6: JWT Payload (DONE)
|
|
|
|
JWT contains only identity claims:
|
|
- Always: `user_id`, `user_status`, `identity_key`, `exp`, `iat`, `iss`, `jti`
|
|
- Optional context: `product_key`, `organization_id` (if context established at login)
|
|
|
|
No product-specific roles or permissions are embedded.
|
|
|
|
### Phase 7-10: Versioning, Contracts, Security, Docs (IN PROGRESS)
|
|
|
|
- API versioning: `/v1/` namespace added (parallel to `/api/v1/`)
|
|
- Product model added to identity layer
|
|
- Active Context support in JWT
|
|
- This documentation
|
|
|
|
## How a Product Integrates
|
|
|
|
### Step 1: Register Product
|
|
|
|
Products register themselves with the Identity Platform:
|
|
|
|
```bash
|
|
POST /v1/products/
|
|
Authorization: Bearer <admin_token>
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"key": "bermooda",
|
|
"name": "Bermooda",
|
|
"description": "ERP platform for teams"
|
|
}
|
|
```
|
|
|
|
Response: `{ "id": "uuid", "key": "bermooda", "name": "Bermooda", ... }`
|
|
|
|
### Step 2: Register Application (OAuth2 Client)
|
|
|
|
Products register OAuth2 clients:
|
|
|
|
```bash
|
|
POST /v1/applications/
|
|
Authorization: Bearer <admin_token>
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"product_key": "bermooda",
|
|
"name": "Bermooda Web",
|
|
"redirect_uris": ["https://bermooda.example.com/callback"],
|
|
"grant_types": ["authorization_code", "refresh_token"],
|
|
"response_types": ["code"],
|
|
"scopes": ["openid", "profile", "email", "org:read"]
|
|
}
|
|
```
|
|
|
|
Response: `{ "client_id": "...", "client_secret": "..." }` (secret shown once)
|
|
|
|
### Step 3: Authenticate Users
|
|
|
|
Products redirect users to the Identity Platform login:
|
|
|
|
```
|
|
https://id.example.com/login?product_key=bermooda&organization_id=<uuid>
|
|
```
|
|
|
|
After login, the user is redirected back with a code. The product exchanges the code for tokens:
|
|
|
|
```bash
|
|
POST /v1/auth/token
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"grant_type": "authorization_code",
|
|
"code": "auth_code_from_platform",
|
|
"client_id": "bermooda-client-id",
|
|
"client_secret": "bermooda-client-secret",
|
|
"redirect_uri": "https://bermooda.example.com/callback",
|
|
"code_verifier": "..."
|
|
}
|
|
```
|
|
|
|
Response: `{ "access_token": "...", "refresh_token": "...", ... }`
|
|
|
|
### Step 4: Verify JWTs
|
|
|
|
Products verify access tokens using the platform's public key:
|
|
|
|
```python
|
|
import jwt
|
|
import requests
|
|
|
|
jwks = requests.get("https://id.example.com/.well-known/jwks.json").json()
|
|
# Cache JWKS, extract public key by kid
|
|
decoded = jwt.decode(
|
|
token,
|
|
public_key,
|
|
algorithms=["RS256"],
|
|
audience="bermooda-client-id",
|
|
issuer="https://id.example.com",
|
|
)
|
|
# decoded = {"user_id": "uuid", "user_status": "active", "identity_key": "usr_...", ...}
|
|
```
|
|
|
|
**Important:** The JWT `sub`/`user_id` is the SAME UUID across all products. Products use this UUID as their FK to identity.User — never creating their own user record.
|
|
|
|
### Step 5: Resolve User Context
|
|
|
|
Products resolve the user's identity and context:
|
|
|
|
```bash
|
|
GET /v1/identity/
|
|
Authorization: Bearer <access_token>
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"user": {
|
|
"id": "uuid",
|
|
"email": "user@example.com",
|
|
"phone": "+989123456789",
|
|
"status": "active",
|
|
"email_verified": true
|
|
},
|
|
"memberships": [
|
|
{ "organization": "org-uuid", "role": "admin", "status": "active" }
|
|
],
|
|
"organizations": [
|
|
{ "id": "org-uuid", "name": "ACME", "slug": "acme" }
|
|
],
|
|
"products": [
|
|
{ "key": "bermooda", "name": "Bermooda" }
|
|
]
|
|
}
|
|
```
|
|
|
|
The product then maps `organization_id` (identity) to its own concept:
|
|
- Bermooda: `Workspace.objects.get(org=org_uuid)`
|
|
- Hamsoo: `BusinessProfile.objects.get(user=user_uuid)`
|
|
|
|
### Step 6: Subscribe to Events
|
|
|
|
Products subscribe to identity events via Redis Streams:
|
|
|
|
```python
|
|
import redis
|
|
|
|
r = redis.Redis(host="redis", port=6379, db=0)
|
|
# Create consumer group (one per product service)
|
|
r.xgroup_create("events.user", "bermooda-sync", mkstream=True)
|
|
|
|
# Consume events
|
|
while True:
|
|
events = r.xread(
|
|
{"events.user": "$"},
|
|
count=10,
|
|
block=1000,
|
|
)
|
|
for stream, messages in events:
|
|
for msg_id, msg in messages:
|
|
process_event(msg)
|
|
r.xack("events.user", "bermooda-sync", msg_id)
|
|
```
|
|
|
|
## Backward Compatibility
|
|
|
|
The platform maintains backward compatibility:
|
|
|
|
- **Old endpoints** (`/api/v1/...`) remain functional — they are NOT removed
|
|
- **New endpoints** (`/v1/...`) mirror the same functionality with improved naming
|
|
- **Old tokens** remain valid until expiry
|
|
- **Existing users** retain their UUIDs — no data migration needed
|
|
|
|
| Old Path | New Path | Status |
|
|
|----------|----------|--------|
|
|
| `/api/v1/auth/login` | `/v1/auth/login` | Both work |
|
|
| `/api/v1/users/` | `/v1/users/` | Both work |
|
|
| `/api/v1/organizations/` | `/v1/organizations/` | Both work |
|
|
| `/api/v1/applications/` | `/v1/applications/` | Both work |
|
|
| `/api/v1/sessions/` | `/v1/sessions/` | Both work |
|
|
| `/api/v1/security/events/` | `/v1/security/events/` | Both work |
|
|
| (NEW) | `/v1/products/` | New — product catalog |
|
|
| (NEW) | `/v1/health/` | New — health check |
|
|
|
|
## Migration Checklist
|
|
|
|
- [x] Architecture audit complete
|
|
- [x] No product-domain models in identity layer
|
|
- [x] No global roles on User model
|
|
- [x] JWT contains only identity claims
|
|
- [x] Organization is generic (no product-specific fields)
|
|
- [x] Product model added to identity layer
|
|
- [ ] Add Active Context to admin dashboard
|
|
- [ ] Migrate product integrations to new endpoints (gradual)
|
|
- [ ] Deprecation timeline for old endpoints (6+ months)
|
|
- [ ] Full test coverage of migration scenarios
|
|
|
|
## Gotchas
|
|
|
|
1. **Do NOT recreate users in products**: Use the identity User UUID as FK
|
|
2. **Do NOT store passwords/tokens in products**: Delegate to Identity Platform
|
|
3. **Do NOT embed product roles in JWT**: Read from Membership API
|
|
4. **Do NOT hardcode product names**: Use `product_key` from Product registration
|
|
5. **Do NOT assume user data is complete**: Products extend with their own profiles
|
|
6. **UUIDs are immutable**: Never change a user's UUID; use `identity_key` for external references
|
|
|
|
## Support
|
|
|
|
- **API Issues**: Internal ticket system
|
|
- **Security**: `security@identity-platform.internal`
|
|
- **Documentation**: `/api/docs/` (Swagger UI)
|