# NexusCRM Permission System — Implementation Map

> **⚠️ HISTORICAL (archived 2026-04-23).** This map describes the
> pre-refactor implementation (`_System_ProfilePermission` as live storage,
> `AllSystemPermissions()` Go catalog, 26 bitmap columns, 3-layer read
> resolution). All of that was eliminated by PR #1831 — see
> `docs/design/permissions-metadata-refactor-outcome.md` for the current
> implementation. Kept for the reasoning chain.

**Date:** 2026-04-03
**Method:** 4 parallel explore agents scanning the codebase
**Purpose:** Complete technical reference of the PRE-REFACTOR permission implementation (HISTORICAL — see banner)

---

## Architecture Overview

```
Request Flow:
                                                              
  Client Request                                              
       │                                                      
       ▼                                                      
  ┌─────────────────┐                                         
  │  RequireAuth    │  JWT 验证 + DB session revocation 检查    
  │  auth.go:15-55  │  → ctx.Set("user", UserSession)         
  └────────┬────────┘                                         
           │                                                  
           ▼                                                  
  ┌──────────────────────┐                                    
  │  PermissionLoader    │  Profile 动态刷新 (60s TTL)          
  │  perm_loader:324-398 │  System/Custom perms 加载 (5min TTL) 
  │                      │  → ctx.Set("system_permissions")    
  │                      │  → 后台异步 WarmUser (obj+field)     
  └────────┬─────────────┘                                    
           │                                                  
           ▼                                                  
  ┌──────────────────────┐                                    
  │ RequirePermission    │  可选：路由级系统权限守卫              
  │ perm_loader:402-442  │  RequireAny / RequireAll / Custom   
  └────────┬─────────────┘                                    
           │                                                  
           ▼                                                  
  ┌──────────────────────┐                                    
  │     Handler          │                                    
  │  ┌────────────────┐  │                                    
  │  │ Object Perms   │  │  CheckObjectPermissionWithUser()    
  │  │ service:152    │  │                                    
  │  ├────────────────┤  │                                    
  │  │ Field Perms    │  │  CheckFieldVisibility/Editability() 
  │  │ service:228    │  │                                    
  │  ├────────────────┤  │                                    
  │  │ Record Access  │  │  CheckRecordAccess() - 5 层检查     
  │  │ record_access  │  │                                    
  │  ├────────────────┤  │                                    
  │  │ SQL RLS        │  │  SecurityValidator.ValidateAndRewrite
  │  │ sec_validator  │  │  → applyRLS (owner_id 注入)         
  │  └────────────────┘  │                                    
  └──────────────────────┘                                    
```

---

## 1. Authentication Layer

### JWT Token Structure

**File:** `backend/pkg/auth/jwt.go:19-27`

```
UserSession {
  ID          string    // 用户 UUID
  Name        string    // 显示名称
  Email       string    // 邮箱
  ProfileId   string    // "system_admin" | "standard_user" | etc.
  ProfileName *string   // 可选显示名
  RoleId      *string   // 角色 UUID（可选）
  RoleName    *string   // 角色显示名（可选）
}
```

- **签名**: HS256 + JWT_SECRET 环境变量
- **过期**: 24h 硬过期
- **Session ID**: JWT `jti` 字段 = UUID，存入 DB 支持撤销
- **验证**: `ValidateToken()` 验证签名 → `ValidateSession()` 检查 DB revocation

### Login Flow

```
POST /services/oauth2/token
  → AuthService.Login(email, password)
    → VerifyPassword (bcrypt)
    → GenerateToken(UserSession) → JWT
    → InsertSession(jti, IP, UserAgent) → DB
    → Return {token, expires_at}
```

### Session Validation (per-request)

```
RequireAuth middleware (auth.go:15-55):
  1. Extract "Bearer <token>" from Authorization header
  2. ValidateToken() → verify HS256 signature + expiry
  3. ValidateSession(jti) → DB check: exists + !IsRevoked
  4. ctx.Set("user", UserSession)
  5. TouchSession(jti) → async update last_activity
```

---

## 2. Permission Loading & Caching

### PermissionLoader Middleware

**File:** `backend/internal/interfaces/middleware/permission_loader.go:324-398`

```
Per-request flow:
  1. Get user from ctx (RequireAuth must run first)
  2. Resolve current ProfileId (60s cache, handles re-assignment)
  3. If SuperUser → set empty perms map, skip DB
  4. Load system + custom permissions (5min cache)
  5. Background: WarmUser() for object + field perms (singleflight)
  6. ctx.Set("system_permissions", map[string]bool)
  7. ctx.Set("custom_permissions", map[string]bool)
```

### Cache Architecture

**File:** `permission_loader.go:19-85`

| Cache | Key | TTL | Data |
|-------|-----|-----|------|
| System/Custom Perms | `"userID\|profileID"` | 5min | `map[string]bool` |
| Object Perms | `"userID\|profileID"` | 5min | `map[string]*ObjectPermissions` |
| Field Perms | `"userID\|profileID"` | 5min | `map[string]*FieldPermissions` |
| Profile ID | `"userID"` | 60s | `string` (current profileID) |

- **Singleflight**: `golang.org/x/sync/singleflight` prevents thundering herd
- **WarmUser**: 3 concurrent goroutines load system+custom, object, field perms
- **Invalidation**: `cache.Invalidate()` clears all entries (called on perm changes)

---

## 3. Permission Guard Middleware

**File:** `permission_loader.go:402-580`

| Middleware | Signature | Logic |
|-----------|-----------|-------|
| `RequirePermission(perm)` | L402-442 | SuperUser bypass → check `perms[perm]` → 403 |
| `RequireCustomPermission(name)` | L445-485 | Same, checks custom_permissions |
| `RequireAnyPermission(perms...)` | L488-530 | SuperUser bypass → any perm true → pass |
| `RequireAllPermissions(perms...)` | L533-580 | SuperUser bypass → all perms true → pass; lists missing |

### Route Guard Application

**File:** `backend/cmd/server/routes.go`

| Route Group | Guard | Line |
|-------------|-------|------|
| `/services/data/:version/*` | `requireAuth + permLoader` | L56 |
| Metadata write (POST/PATCH/DELETE objects, fields) | `RequirePermission(PermModifyMetadata)` | L134 |
| Flow execution | `RequirePermission(PermRunFlow)` | L191 |
| Tooling sObjects write | `RequirePermission(PermCustomizeApplication)` | L200 |
| Layout management | `RequirePermission(PermCustomizeApplication)` | L239 |
| App management | `RequirePermission(PermCustomizeApplication)` | L256 |
| **Analytics (reports)** | **None (only requireAuth+permLoader)** | **L159** |

---

## 4. System Permissions Resolution

### 4-layer additive + 1-layer subtractive

**File:** `backend/internal/infrastructure/persistence/perm_system.go:67-122`

```
GetEffectiveSystemPermissions(ctx, userID, profileID) → map[string]bool

Layer 1: Profile baseline
  └─ _System_ProfilePermission WHERE ProfileId = ? AND IsEnabled = 1

Layer 2: PermissionSet boolean columns (Profile-owned + direct + PSG component)
  └─ SELECT Permissions* FROM _System_PermissionSet
     WHERE (ProfileID=? AND IsOwnedByProfile=1)
        OR Id IN (direct assignments)
        OR Id IN (PSG component assignments)

Layer 3: PermissionSet permission rows (normalized)
  └─ _System_PermissionSetPermission WHERE PermissionSetID IN (direct assignments)

Layer 4: PSG component permissions
  └─ _System_PermissionSetPermission JOIN PSG components JOIN PSG assignments

Layer 5: Muting (SUBTRACT)
  └─ _System_MutingPermissionSetPermission JOIN MutingPS JOIN PSG assignments
  └─ for _, mutedPerm := range mutedPerms { delete(effective, mutedPerm) }
```

**Merge logic**: OR (additive) across all layers, then DELETE for muted perms.

---

## 5. Object Permissions

### CheckObjectPermissionWithUser

**File:** `backend/internal/features/permissions/service.go:152-195`

```
CheckObjectPermissionWithUser(ctx, objectAPIName, operation, user) → bool

  1. SuperUser → true
  2. Setup object → check required system perms:
     - User/Profile/PermSet → ManageUsers
     - UserRole → ManageRoles
     - Group/Queue/SharingRule → ManageSharing
  3. Load effective object perms (cache or DB)
  4. Check: Read/Create/Edit/Delete → corresponding bool field
```

### Effective Object Permission Query

**File:** `backend/internal/infrastructure/persistence/perm_object_field.go:92-144`

```sql
SELECT MAX(PermissionsRead), MAX(PermissionsCreate),
       MAX(PermissionsEdit), MAX(PermissionsDelete),
       MAX(PermissionsViewAllRecords), MAX(PermissionsModifyAllRecords)
FROM _System_ObjectPermissions
WHERE SobjectType = ?
AND ParentID IN (
    SELECT Id FROM _System_PermissionSet
    WHERE ProfileID = ? AND IsOwnedByProfile = 1
    UNION
    SELECT PermissionSetID FROM _System_PermissionSetAssignment
    WHERE AssigneeID = ?
)
```

`MAX()` = additive OR — any PermissionSet granting true → final true.

### GrantInitialPermissions (new object bootstrap)

**File:** `service.go:410-438`

| Profile | R | C | E | D | ViewAll | ModifyAll |
|---------|---|---|---|---|---------|-----------|
| system_admin | T | T | T | T | **T** | **T** |
| All others | T | T | T | T | **F** | **F** |

---

## 6. Field-Level Security

### CheckFieldVisibilityWithUser

**File:** `service.go:259-282`

```
1. SuperUser → true
2. Load effective field perm (cache or DB)
3. If explicit FLS row exists → return PermissionsRead
4. Else fallback → CheckObjectPermissionWithUser(..., "Read")
```

### CheckFieldEditabilityWithUser

**File:** `service.go:228-256`

```
1. System read-only field (Id, CreatedDate, etc.) → false
2. SuperUser → true
3. Load effective field perm (cache or DB)
4. If explicit FLS row exists → return PermissionsEdit
5. Else fallback → CheckObjectPermissionWithUser(..., "Edit")
```

### FieldAccessResolver (combined capability)

**File:** `field_access_resolver.go:16-81`

```
EffectiveCreateable = intrinsic capability AND object Create AND field Edit (with Create fallback)
EffectiveUpdateable = intrinsic capability AND object Edit AND field Edit
```

---

## 7. Record-Level Security (Single Record)

### CheckRecordAccess — 5-layer cascade

**File:** `backend/internal/features/permissions/record_access.go:15-104`

```
CheckRecordAccess(ctx, schema, record, operation, user) → bool

Layer 1: SuperUser → true (line 21)

Layer 2: Ownership (line 50-64)
  ├─ record.OwnerId == user.ID → true
  └─ IsUserInGroup(record.OwnerId, user.ID) → true (queue ownership)

Layer 3: Role Hierarchy — READ ONLY (line 69-75)
  └─ isUserAboveInHierarchy(user.RoleID, owner.RoleID) → true
  └─ LIMITATION: only for PermRead, not Edit/Delete

Layer 4: Sharing Rules (line 77-86)
  └─ For each rule in schema.SharingRules:
     └─ checkSharingRuleAccess():
        ├─ Identity: isUserInRoleOrAbove() OR IsUserInGroup()
        ├─ Operation: AccessLevel="Edit" allows Read+Edit
        └─ Criteria: evaluateSharingCriteria() via formula engine

Layer 5: Manual Share (line 88-93)
  └─ GetManualShareAccessLevels() → query _System_RecordShare
     WHERE (share_with_user_id=? OR share_with_group_id IN user_groups)

Layer 6: Team Member (line 95-100)
  └─ GetTeamMemberAccessLevel() → query _System_TeamMember
     WHERE sobject_type=? AND record_id=? AND user_id=?

Default: DENY (line 103)
```

---

## 8. Role Hierarchy

### Memory Cache

**File:** `backend/internal/features/permissions/role_hierarchy.go`

```go
roleHierarchyCache map[string]*string  // role_id → parent_role_id (single parent)
roleHierarchyMu    sync.RWMutex
```

- **refreshRoleHierarchy** (L11-29): Load all roles from DB → build map → atomic replace
- **GetRoleAncestors** (L33-57): Walk parent pointers → return ancestor list. O(depth), cycle detection via visited set
- **isUserAboveInHierarchy** (L61-83): Check if userRole is in targetRole's ancestor chain
- **Init**: Called at `NewPermissionService()` startup; also on role CRUD operations

---

## 9. Sharing Rules

**File:** `backend/internal/features/permissions/sharing_rules.go`

### checkSharingRuleAccess (L32-86)

```
1. Identity match (one of):
   ├─ Role-based: isUserInRoleOrAbove(user.RoleID, rule.ShareWithRoleID)
   └─ Group-based: IsUserInGroup(rule.ShareWithGroupID, user.ID)

2. Operation level:
   ├─ "Edit" → allows Read + Edit
   └─ "Read" → allows Read only

3. Criteria evaluation (if present):
   └─ evaluateSharingCriteria(record, criteria)
      └─ formula.Engine.Evaluate(criteria, {Record: record.Fields}) → bool
```

### Limitations
- No Owner-based sharing rules (only role/group + criteria)
- Formula engine errors silently return false
- No criteria caching (re-evaluates each time)

---

## 10. SQL RLS (Analytics Queries)

### SecurityValidator.ValidateAndRewrite

**File:** `backend/internal/features/analytics/security_validator.go:44-160`

```
ValidateAndRewrite(ctx, sql, params, user, contextVars) → (rewrittenSQL, params, error)

  1. Parse SQL (TiDB parser) → AST
  2. Only SELECT/UNION allowed
  3. SecurityVisitor.Enter():
     ├─ Table permission: CheckObjectPermissionWithUser(table, "Read")
     ├─ Field permission: CheckFieldVisibilityWithUser(table, column)
     └─ Variable substitution: @user_id → user.ID, @user_email → user.Email
  4. If NOT SuperUser → applyRLS()
  5. Restore AST → SQL string
  6. Return rewritten SQL
```

### applyRLS

**File:** `security_validator.go:162-233`

```
applyRLS(ctx, stmt, user) → error

  1. No FROM clause → skip
  2. JOIN detected (TableRefs.Right != nil) → SKIP (return nil) ← BUG
  3. Get table name from AST
  4. Check if table has owner_id field
  5. Build AST node: owner_id = 'user.ID'
  6. Inject into WHERE: existing AND owner_id = 'user.ID'
     or set as new WHERE clause
```

### Known Issues
- **JOIN queries bypass RLS entirely** (L173: `return nil`)
- **No ViewAllRecords check** — only IsSuperUser bypass
- **No Role Hierarchy subordinates** — only `owner_id = user.ID`
- **Subqueries unprotected** — inner SELECT not validated
- **No OWD** — implicitly assumes all objects are Private

---

## 11. Data Model

### Permission Tables

| Table | Purpose | Key Fields |
|-------|---------|------------|
| `_System_Profile` | Profile definitions | Id, Name, IsSystem |
| `_System_PermissionSet` | Permission sets (+ 20 boolean perm columns) | Id, ProfileId, IsOwnedByProfile, Permissions* |
| `_System_PermissionSetAssignment` | User ↔ PermSet mapping | AssigneeId, PermissionSetId |
| `_System_ProfilePermission` | Profile system perms (normalized) | ProfileId, PermissionApiName, IsEnabled |
| `_System_PermissionSetPermission` | PermSet system perms (normalized) | PermissionSetId, PermissionApiName, IsEnabled |
| `_System_ObjectPermissions` | Object CRUD perms | ParentId(PermSetId), SobjectType, R/C/E/D/ViewAll/ModifyAll |
| `_System_FieldPermissions` | Field R/E perms | ParentId(PermSetId), SobjectType, Field, Read/Edit |
| `_System_PermissionSetGroup` | PSG definitions | Id, ApiName, Label |
| `_System_PermissionSetGroupComponent` | PSG ↔ PermSet membership | GroupId, PermissionSetId |
| `_System_PermissionSetGroupAssignment` | User ↔ PSG mapping | AssigneeId, GroupId |
| `_System_MutingPermissionSet` | Muting PS definitions | Id, GroupId |
| `_System_MutingPermissionSetPermission` | Muted permissions | MutingPermissionSetId, PermissionApiName |
| `_System_UserRole` | Role hierarchy | Id, Name, ParentRoleId (self-ref FK) |
| `_System_SharingRule` | Sharing rules | ObjectApiName, Criteria, AccessLevel, ShareWithRoleId/GroupId |
| `_System_RecordShare` | Manual record shares | SobjectType, RecordId, ShareWithUserId/GroupId, AccessLevel |
| `_System_TeamMember` | Team member access | SobjectType, RecordId, UserId, AccessLevel |
| `_System_CustomPermission` | Custom permission definitions | ApiName, Label, IsActive |
| `_System_Permission` | System permission catalog | ApiName, Label, Category, RequiresPermission |

---

## 12. System Permission Constants

**File:** `shared/pkg/constants/permissions.go`

60+ permissions across 12 categories:

| Category | Count | Examples |
|----------|-------|---------|
| Administrative | 8 | ApiEnabled, ViewAllData, ModifyAllData, ManageUsers, ViewSetup |
| User Management | 3 | ManageExternalUsers, PasswordNeverExpires |
| Data | 8 | BulkApiHardDelete, MassInlineEdit, TransferAnyEntity |
| Reports & Dashboards | 8 | RunReports, ExportReport, ManageReportsInPubFolders |
| Flow & Automation | 7 | RunFlow, ManageFlows, DebugFlows |
| Schema & Metadata | 3 | ModifyMetadata, ManageCustomPermissions |
| Email | 3 | EmailSingle, EmailMass |
| List Views | 2 | CreateCustomizeFilters, EditPublicFilters |
| Content & Files | 3 | ManageContentPermissions |
| Audit & Compliance | 4 | ViewSetupAuditTrail, ViewFieldAuditTrail |
| Platform | 3 | LightningExperienceUser, SalesConsole |
| Integration | 5 | QueryAllFiles, ViewPlatformEvents |

### Profile Constants

**File:** `shared/pkg/constants/profiles.go`

```
ProfileSystemAdmin  = "system_admin"     → "System Administrator"
ProfileStandardUser = "standard_user"    → "Standard User"
ProfileSalesUser    = "sales_user"       → "Sales User"
ProfileDealUser     = "deal_user"        → "Deal User"
ProfileLegalUser    = "legal_user"       → "Legal User"

IsSuperUser(profileId) = profileId == "system_admin"
```

### Bootstrap Permission Matrix

**File:** `backend/internal/bootstrap/permissions.go`

| Profile | System tables (R/C/E/D) | Business tables (R/C/E/D) | System Perms |
|---------|------------------------|--------------------------|--------------|
| system_admin | TTTТ | TTTT + ViewAll + ModifyAll | All 60+ |
| standard_user | T--- | TTT- | ApiEnabled, RunReports, ExportReport, EmailSingle, CreateCustomizeFilters, LightningExperienceUser |
| sales_user | T--- | TTT- | Same as standard |
| deal_user | T--- | TTTT | Same as standard |
| legal_user | T--- | T--- | Same as standard |

---

## 13. UserInfo Endpoint

**File:** `backend/internal/interfaces/rest/oauth_handler.go:259-313`

```json
GET /services/oauth2/userinfo

Response:
{
  "sub": "userId",
  "user_id": "userId",
  "organization_id": "defaultOrgId",
  "preferred_username": "user@example.com",
  "name": "John Doe",
  "email": "user@example.com",
  "profile_id": "standard_user",
  "is_active": true,
  "permissions": {
    "ApiEnabled": true,
    "RunReports": true,
    "ViewAllData": false,
    ...
  }
}

Note: Missing role_id and role_name in response
```

---

## 14. Frontend Permission Consumption

**File:** `frontend/src/core/query/permissionQueries.ts`

```typescript
// Object permissions derived from DescribeGlobal CRUD flags
// (backend returns per-user values via CheckObjectPermissionWithUser)
{
  PermissionsRead: schema.retrieveable,
  PermissionsCreate: schema.createable,
  PermissionsEdit: schema.updateable,
  PermissionsDelete: schema.deletable,
  PermissionsViewAllRecords: false,    // HARDCODED — known limitation
  PermissionsModifyAllRecords: false,  // HARDCODED — known limitation
  fieldPermissions: [],                // EMPTY — backend API gap
}
```

**File:** `frontend/src/types/permissions.ts`

```typescript
interface UserSession {
  Id: string;
  ProfileId: string;
  UserRoleId?: string;
  systemPermissions?: Record<string, boolean>;
}
```

---

## 15. Gap Summary

| Area | Single Record (CRUD) | Bulk Query (SQL/SOQL) |
|------|---------------------|----------------------|
| SuperUser bypass | OK | OK |
| Ownership | OK | OK (owner_id = ?) |
| Role Hierarchy | Read only | **Missing** |
| ViewAllRecords | **Not checked** | **Not checked** |
| Sharing Rules | OK (formula engine) | **Missing** |
| Manual Share | OK | **Missing** |
| Team Member | OK | **Missing** |
| JOIN queries | N/A | **Bypasses RLS** |
| OWD | **Missing** | **Missing** |

The fundamental gap: **single-record access has 5-layer protection, but bulk queries only have owner_id filtering**. This creates an inconsistency where users see different results depending on access pattern.
