gh_UserManager/docs/IDENTITY_MODEL.md
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

347 lines
13 KiB
Markdown

# Identity Model
## Purpose
This document describes the central identity model managed by the Identity Platform. It is the single source of truth for **who a user is** and **whether they are authenticated**.
UserManager answers:
- **WHO ARE YOU?** → User identity (UUID, email, phone)
- **ARE YOU AUTHENTICATED?** → Session + token validity
- **IS YOUR IDENTITY VERIFIED?** → email_verified, phone_verified, mfa_enabled
- **WHICH ORGANIZATIONS?** → Membership (user ↔ Organization)
- **WHICH PRODUCTS?** → Product catalog + active context in JWT
- **WHAT SESSIONS?** → Session tracking (browser/api/device)
Products answer:
- **WHAT CAN YOU DO HERE?** → Product-domain permissions (roles, policies) within a specific context
---
## Entity: User
**Definition:** The central identity entity. A User is a person whose identity is managed by the Identity Platform. The User's UUID is **shared across all products** — no product creates a separate user for the same person.
**Key Principle:** A User's identity is **immutable** and **product-agnostic**. A User can be an Owner in one org, an Employee in another, and a Specialist in Hamsoo — all as the same User entity.
### User Schema
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK, immutable | Globally unique user identifier. Shared across all products. |
| `email` | EmailField | unique, nullable | Primary authentication channel. Verified. |
| `phone` | CharField | unique, nullable | Secondary authentication channel. Verified. |
| `username` | SlugField | unique, nullable | Human-readable identifier. |
| `full_name` | CharField | blank | Display name |
| `given_name` | CharField | blank | First name |
| `family_name` | CharField | blank | Last name |
| `avatar_url` | URLField | blank | Profile picture URL |
| `status` | CharField | choices | Pending / Active / Suspended / Banned |
| `email_verified` | BooleanField | default False | Is email verified |
| `phone_verified` | BooleanField | default False | Is phone verified |
| `locale` | CharField | default "fa" | Default locale (fa/en) |
| `timezone` | CharField | default "UTC" | User's timezone |
| `mfa_enabled` | BooleanField | default False | Is MFA enrolled |
| `totp_secret` | CharField | blank | TOTP secret (encrypted) |
| `password_reset_token` | CharField | blank | Hash of password reset token |
| `password_reset_expires` | DateTimeField | nullable | Reset token expiry |
| `last_login_at` | DateTimeField | nullable | Last login timestamp |
| `is_active` | BooleanField | default True | Django auth active flag |
| `is_staff` | BooleanField | default False | Django admin access |
| `created_at` | DateTimeField | auto_now_add | Creation timestamp |
| `updated_at` | DateTimeField | auto_now | Update timestamp |
**Removed (previously product-specific):**
- `employer` / `job_seeker` global roles → These are **product-specific roles**, managed by Bermooda/Hamsoo, NOT identity layer.
- Any product-specific profile fields → Managed by Product layers (BusinessProfile, Workspace, etc.)
### Computed Properties
| Property | Type | Description |
|----------|------|-------------|
| `user_id` | UUID | Alias for `id` — the canonical user identifier |
| `identity_key` | str | `usr_{id_hex}` — prefixed identifier for external systems |
### User Lifecycle
```
1. Created (status=pending) → User registered, not yet active
2. Verified (status=active) → Email/phone verified, user can authenticate
3. Active → User is fully operational
4. Suspended → Temporarily blocked (self-serve or admin action)
5. Banned → Permanently blocked (admin action)
```
**Key Rules:**
- UUID never changes — even if email/phone changes
- `employer`/`job_seeker` roles are NOT on the User model
- Product-specific roles come from Membership → Organization → Product mapping
- A suspended/banned user cannot authenticate (LoginView rejects them)
---
## Entity: Organization
**Definition:** A generic organization (company, team, workspace). NOT tied to any specific product.
**Key Principle:** Organization is a generic container. `Bermooda.Workspace` and `Hamsoo.BusinessProfile` are product-layer entities that **map to** an identity.Organization.
### Organization Schema
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK | Organization identifier |
| `name` | CharField | required | Display name |
| `slug` | SlugField | unique, auto-generated | URL-safe identifier |
| `description` | TextField | blank | Description |
| `logo_url` | URLField | blank | Logo |
| `website_url` | URLField | blank | Website |
| `status` | CharField | choices (active/inactive) | Active/Inactive |
| `created_by` | FK→User | nullable | User who created org |
| `created_at` / `updated_at` | DateTimeField | auto | Timestamps |
**No product-specific fields.** A Bermooda Workspace adds fields like `industry` via a product-layer FK to Organization.
### Example: Bermooda → Workspace mapping
```python
# In Bermooda product layer
class Workspace(models.Model):
org = models.ForeignKey("identity.Organization", on_delete=models.CASCADE)
industry = models.CharField(max_length=100)
hiring_settings = JSONField()
...
```
The Workspace is NOT an Organization — it extends one.
---
## Entity: Membership
**Definition:** The relationship between a User and an Organization. Carries role context.
**Key Principle:** Roles are **contextual** — a role on the Membership (org-level owner/admin/member), not a global role on the User. Product-specific roles are in the Product layer.
### Membership Schema
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK | Membership identifier |
| `user` | FK→User | required | User identity |
| `organization` | FK→Organization | required | Organization context |
| `role` | CharField | choices (owner/admin/member) | Org-level role |
| `status` | CharField | choices (active/invited/pending/deactivated) | Membership state |
| `invited_by` | FK→User | nullable | Who invited |
| `joined_at` | DateTimeField | nullable | When accepted |
| `created_at` / `updated_at` | DateTimeField | auto | Timestamps |
**Constraints:**
- `UniqueConstraint(fields=["user", "organization"])` — a user can have only one membership per organization
### Role Context (Key Concept)
```
User: Ali
Org: ACME
Membership: role=owner → Ali is Owner of ACME (identity-level)
Bermooda: maps ACME → Workspace
Bermooda role: Ali's profile in Bermooda's Workspace (e.g., HR Manager) — product-layer
Hamsoo: not connected to ACME
Hamsoo profile: Ali's specialist rating, bio — product-layer
```
Products read the Membership to know **if** the user has access to an org, then apply **their own** product-specific logic for **what they can do** within it.
---
## Entity: Product
**Definition:** A registered product in the identity ecosystem. Identity uses this to know which products exist and are active.
**Key Principle:** Identity knows that `bermooda` and `hamsoo` are products. It does NOT store product-domain data (BusinessProfile, projects, payroll). It only stores the product's identity metadata.
### Product Schema
| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | UUID | PK | Product identifier |
| `key` | SlugField | unique | Unique product key (bermooda, hamsoo, ...) |
| `name` | CharField | required | Human-readable name |
| `description` | TextField | blank | Description |
| `logo_url` | URLField | blank | Product logo |
| `website_url` | URLField | blank | Product website |
| `is_active` | BooleanField | default True | Can authenticate against this product |
| `created_by` | FK→User | nullable | Who registered |
| `created_at` / `updated_at` | DateTimeField | auto | Timestamps |
---
## Entity: Application (OAuth2 Client)
**Definition:** An OAuth2 client registered against a specific Product. Used for authentication flows.
### Relationship to Product
Each Application has a `product_key` that identifies which product it belongs to. This is for routing/context — not for storing product domain data.
---
## Entity: Session
**Definition:** A user's authenticated session. Tracks device, IP, and lifecycle.
### Session Lifecycle
```
Created (active) → User logs in
→ Session created, refresh token issued
Active (active) → User uses platform
Expired (expired) → Refresh token lifetime (30 days) exceeded
Revoked (revoked) → User logs out, password change, admin action
```
Session is identity-layer. It tracks **that** a user authenticated — not what they did in a product.
---
## Entity: SecurityEvent
**Definition:** An immutable audit event. Every auth action, state change, and security-relevant action is logged.
### Event Types
| Event Type | Severity | Description |
|------------|----------|-------------|
| `login_success` | info | Successful login |
| `login_failed` | medium | Failed login attempt |
| `login_locked` | high | Account locked due to brute-force |
| `logout` | low | User logged out |
| `password_change` | medium | Password changed |
| `password_reset` | medium | Password reset completed |
| `email_verified` | low | Email verified |
| `phone_verified` | low | Phone verified |
| `mfa_enabled` | low | MFA enrolled |
| `mfa_disabled` | low | MFA removed |
| `session_revoked` | medium | Session revoked |
| `session_expired` | low | Session expired |
| `session_created` | low | New session created |
| `device_added` | low | New device registered |
| `application_created` | medium | OAuth2 app registered |
| `application_revoked` | high | OAuth2 app revoked |
### Severity Levels
- `info` — Normal operation
- `low` — Minor event
- `medium` — Worth noting, potential investigation
- `high` — Security-relevant, requires action
- `critical` — Immediate action required
---
## JWT Token Structure
### Access Token (RS256, 15 min lifetime)
```json
{
"user_id": "uuid",
"user_status": "active",
"identity_key": "usr_abc123...",
"product_key": "bermooda", // optional — active context
"organization_id": "uuid", // optional — active context
"token_type": "access",
"exp": 1700000000,
"iat": 1699999100,
"iss": "https://id.example.com",
"jti": "unique-token-id"
}
```
**Claims policy:**
- Always present: `user_id`, `user_status`, `identity_key`, `exp`, `iat`, `iss`, `jti`
- Optional (context): `product_key`, `organization_id` — only included if login established a context
- **Never present**: product-specific roles, permissions, business data
### Refresh Token (RS256, 30 day lifetime)
```json
{
"user_id": "uuid",
"session_id": "uuid",
"token_type": "refresh",
"exp": 1702591100,
"iat": 1699999100,
"iss": "https://id.example.com",
"jti": "unique-token-id"
}
```
**Token rotation:** Each refresh issues a new refresh token; the old one is revoked and stored in `replaced_by`. Products must discard old refresh tokens immediately.
### ID Token (OIDC)
```json
{
"sub": "uuid",
"name": "User Name",
"email": "user@acme.com",
"email_verified": true,
"preferred_username": "user",
"locale": "fa",
"iat": 1699999100,
"exp": 1700000000,
"iss": "https://id.example.com",
"aud": "bermooda-client-id"
}
```
---
## Verification
**Definition:** Separate from authentication, verification confirms the authenticity of identity claims.
```
Authentication: "You have a password to the account."
Verification: "This email/phone belongs to you. This identity is real."
```
### Verification States
| Channel | Verified? | Verified At |
|---------|-----------|-------------|
| Email | boolean | timestamp |
| Phone | boolean | timestamp |
`Verification` is identity-layer. Future extensions:
- Email verification → Identity Platform
- Phone verification → Identity Platform
- Shahkar (Iran national ID verification) → Identity Platform (verification provider)
- Manual Verification → Identity Platform (admin override)
In the future, products may request additional verification (e.g., Hamsoo needs business license verified) — but that's a Product-layer concern.
---
## User ID Consistency
**Guarantee:** A user's UUID is the same across Bermooda, Hamsoo, and all future products.
```
UserManager: user_id = UUID(550e8400-e29b-41d4-a716-446655440000)
Bermooda: user_id = UUID(550e8400-e29b-41d4-a716-446655440000) ← same
Hamsoo: user_id = UUID(550e8400-e29b-41d4-a716-446655440000) ← same
No product creates its own user record.
No product duplicates user identity.
```
Products reference the identity.User UUID via FK. They never store passwords, sessions, or tokens.