# NexusCRM Permission System — Design & Gap Analysis

> **⚠️ HISTORICAL (archived 2026-04-23).** This directory captures the
> pre-refactor design work from 2026-04-03 to 2026-04-11 that motivated
> PRs #766 / #768 / #790 / #1831. The architecture it describes —
> `_System_ProfilePermission` as live storage, the three-layer read path,
> the `AllSystemPermissions()` Go catalog, and 26 `Permissions<X>` bitmap
> columns on `PermissionSet` — **is no longer current.** For what actually
> shipped, read `docs/design/permissions-metadata-refactor-outcome.md`.
>
> Kept for the reasoning chain (45-vulnerability audit, 4-expert review
> panel, SF research); **do not use as a guide to current code.** Symbols
> referenced here (`AllSystemPermissions`, `PermissionDefinition`,
> `PermCat*`, `permissionSetSystemPermissionColumns`,
> `BuildPermissionVirtualFields`, `PermissionVirtualFieldPrefix`, etc.)
> were deleted by the refactor.
>
> For the current explanation of the `#1970` ownership-model direction, read
> `docs/design/permission-system/access-ownership-model.md`.

**Date:** 2026-04-03
**Status:** Design proposal (HISTORICAL — see banner above)
**Related docs:**
- [SF Permission Model Research](salesforce-permission-model.md) — SF design philosophy and architecture
- [Current Implementation Map](permission-implementation-map.md) — Complete blueprint of code implementation
- [Refactor Analysis](permission-refactor-analysis.md) — Three-party agent review conclusions

---

## Part 1: Ideal Design

### Design Philosophy

Following the five principles validated by Salesforce over 25 years, adapted to NexusCRM's 127-user scale:

| Principle | Meaning | NexusCRM Application |
|------|------|--------------|
| **Default Deny** | No permissions by default, explicitly granted | OWD = Private as baseline, opened up layer by layer |
| **Orthogonal Three Dimensions** | Object / Field / Record checked independently | Three dimensions are each independent; all must pass for access |
| **Additive Model** | Permissions can only stack, never revoke (except Muting) | Profile ∪ PermSet₁ ∪ PermSet₂ ∪ ... |
| **Declarative** | Permissions are metadata (data), not if/else | Admins configure at runtime, no code changes needed |
| **Composition over Inheritance** | PermSets stack freely, not a role inheritance tree | PermSet-first; Profile serves only as minimal baseline |

### Target Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│                     Authentication                              │
│  JWT (identity only) → RequireAuth → Session validation         │
│  + PermApiEnabled gate                                          │
└───────────────────────────────┬─────────────────────────────────┘
                                │
┌───────────────────────────────▼─────────────────────────────────┐
│                  Permission Resolution                          │
│                                                                 │
│  ┌─────────────┐   ┌──────────────┐   ┌───────────────────┐    │
│  │ Minimum     │   │ Permission   │   │ Permission Set    │    │
│  │ Access      │──▶│ Sets         │──▶│ Groups + Muting   │    │
│  │ Profile     │   │ (atomic perm │   │ (Persona bundling)│    │
│  │ (5-8)       │   │  packages)   │   │                   │    │
│  │             │   │ (~15)        │   │                   │    │
│  └─────────────┘   └──────────────┘   └───────────────────┘    │
│                                                                 │
│  Resolution: Profile ∪ PermSets ∪ PSG components - Muting      │
│  Cache: PermissionLoader (5min TTL, singleflight)               │
└───────────────────────────────┬─────────────────────────────────┘
                                │
        ┌───────────────────────┼───────────────────────┐
        │                      │                        │
        ▼                      ▼                        ▼
┌──────────────┐     ┌──────────────┐          ┌──────────────┐
│ Object Perms │     │ Field Perms  │          │ System Perms │
│              │     │              │          │              │
│ Per-object:  │     │ Per-field:   │          │ 60+ boolean  │
│ R/C/E/D      │     │ Visible     │          │ flags        │
│ ViewAll      │     │ Editable    │          │              │
│ ModifyAll    │     │              │          │              │
└──────┬───────┘     └──────┬───────┘          └──────┬───────┘
       │                    │                         │
       │  All three must    │                         │
       │  pass simultaneously│                        │
       └────────────┬───────┘                         │
                    │                                 │
                    ▼                                 │
┌─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│                  Record-Level Security                          │
│                                                                 │
│  ┌──────────────────────────────────────────┐                   │
│  │         OWD (Organization-Wide Defaults)  │                  │
│  │         Per-object configuration          │                  │
│  │                                           │                  │
│  │  Private ──────────▶ Full RLS check       │                  │
│  │  Public Read Only ─▶ Edit/Delete check only│                 │
│  │  Public Read/Write ▶ Skip RLS             │                  │
│  └──────────────────────────────────────────┘                   │
│                         │                                       │
│                    OWD ≠ Public R/W                              │
│                         │                                       │
│                         ▼                                       │
│  ┌─────────────────────────────────────────────────────┐        │
│  │ Record Access Check (ordered, short-circuit)        │        │
│  │                                                     │        │
│  │  1. ViewAllRecords / ModifyAllRecords → bypass      │        │
│  │  2. Record Ownership (user or group/queue)          │        │
│  │  3. Role Hierarchy (configurable: R / R+E+D)        │        │
│  │  4. Sharing Rules (owner-based + criteria-based)    │        │
│  │  5. Manual Share (_System_RecordShare)               │        │
│  │  6. Team Member (_System_TeamMember)                 │        │
│  │  7. Default: DENY                                   │        │
│  └─────────────────────────────────────────────────────┘        │
│                         │                                       │
│         ┌───────────────┴───────────────┐                       │
│         │                               │                       │
│         ▼                               ▼                       │
│  ┌──────────────┐              ┌──────────────────┐             │
│  │ Single Record│              │ Bulk Query (SQL) │             │
│  │ (CRUD API)   │              │ (Reports/SOQL)   │             │
│  │              │              │                  │             │
│  │ Per-record   │              │ WHERE clause     │             │
│  │ check        │              │ injection:       │             │
│  │ 5-layer      │              │ owner_id IN (    │             │
│  │ waterfall    │              │   self ∪ reports │             │
│  │ (implemented)│              │   ∪ sharing rules│             │
│  │              │              │   ∪ manual share │             │
│  │              │              │ )                │             │
│  │              │              │ + JOIN support   │             │
│  └──────────────┘              └──────────────────┘             │
└─────────────────────────────────────────────────────────────────┘
```

### Key Design Decisions

#### 1. JWT Stays Lean — Permissions Not Stored in Token

```
JWT payload = { ID, Name, Email, ProfileId, RoleId }  ← identity information only

Permission loading = PermissionLoader middleware, reads from DB/Cache per-request
  - Permission changes take effect within 5min, no re-login required
  - No JWT bloat problem
  - No security window where permissions change but token hasn't expired
```

#### 2. OWD as the Switch for RLS

OWD determines whether a given object requires record-level security checks. This is the most critical design in the SF model — not every object needs RLS.

```
_System_ObjectMetadata table new fields:
  DefaultInternalAccess VARCHAR(20) DEFAULT 'Private'
  GrantAccessUsingHierarchies BOOLEAN DEFAULT true

Values:
  'Private'          → Full RLS check
  'PublicReadOnly'   → Read exempt, Edit/Delete go through RLS
  'PublicReadWrite'  → Fully exempt

Typical configuration:
  Account      → PublicReadOnly   (everyone can read, editing restricted)
  Opportunity  → Private          (strict by ownership and sharing)
  Contact      → PublicReadOnly
  Lead         → Private
  Case         → Private
  Report       → Controlled by Folder permissions, not OWD
```

#### 3. RLS Bulk Query: Real-Time Subquery Instead of Materialized Table

127-user scale does not need SF's `<Object>Share` materialized table; real-time computation is sufficient:

```sql
-- WHERE condition injected by applyRLS (replaces current owner_id = ?)
WHERE (
    -- OWD check (if PublicReadWrite, this condition is not injected)
    t.owner_id = @user_id                                    -- 1. owned by self
    OR t.owner_id IN (                                       -- 2. owned by role subordinates
        SELECT id FROM _System_User
        WHERE user_role_id IN (@subordinate_role_ids)
    )
    OR EXISTS (                                              -- 3. sharing rule match
        SELECT 1 FROM _System_RecordShare rs
        WHERE rs.record_id = t.id AND rs.sobject_type = @obj
        AND rs.user_or_group_id IN (@user_groups)
    )
    OR @has_view_all_records = true                          -- 4. ViewAllRecords
)
```

For JOIN queries, RLS conditions are injected independently for each table.

#### 4. PermSet-first Persona Model

```
Instead of creating different Profiles for each role, use PSG combinations:

Persona "Sales Rep APAC" = PSG {
  PermSets: [Base CRM Access, Report Viewer, APAC Data Access]
  Muting: [none]
}

Persona "Sales Manager NA" = PSG {
  PermSets: [Base CRM Access, Report Builder, Team Management, NA Data Access]
  Muting: [none]
}

Persona "Finance Analyst" = PSG {
  PermSets: [Base CRM Access, Report Builder, Export Data]
  Muting: [Delete Records]  ← precise subtraction
}
```

All users use the Minimum Access Profile (contains only login settings and layout assignments); all specific permissions are granted through PermSet/PSG.

#### 5. Consistency Between Single-Record and Bulk Query

**Core principle: Both access paths must return the same results.**

```
Records a user retrieves via SOQL ⊆ Records the user can access via CRUD API

Implementation:
- CRUD API: CheckRecordAccess() per-record check (already implemented)
- SQL/SOQL: applyRLS() injects an equivalent WHERE condition
- Both share the same permission data source (OWD + Ownership + Hierarchy + Sharing + Manual)
```

---

## Part 2: Current Implementation Gaps

### Gap Overview

| Dimension | Ideal Design | Current Implementation | Gap |
|------|---------|---------|------|
| **OWD** | Per-object configuration (Private/PublicRO/PublicRW) | Does not exist | Completely missing |
| **System Perms** | Profile → PermSet → PSG → Muting | Fully implemented | **No gap** |
| **Object Perms** | Profile + PermSet merged (MAX) | Fully implemented | **No gap** |
| **Field Perms** | Profile + PermSet merged | Fully implemented | **No gap** |
| **Record Access (single record)** | 7-layer check | 5-layer check (missing OWD + ViewAll) | Medium |
| **Record Access (bulk)** | Equivalent WHERE clause injection | Only owner_id = ? | **Critical** |
| **ViewAllRecords** | Stored in ObjPerms + checked in RLS | Stored correctly, not checked in RLS | High |
| **Role Hierarchy** | Configurable propagation R/E/D | Propagates Read only | Medium |
| **Sharing Rules** | Owner-based + Criteria-based | Criteria-based (missing Owner-based) | Low |
| **Route Guards** | All sensitive routes have permission guards | Analytics/API routes missing | High |
| **UserInfo** | Returns complete user context | Missing role_id, role_name | Low |
| **Frontend ViewAll** | Fetches real value from API | Hardcoded false | Medium |

### Specific Gaps Ordered by Severity

#### CRITICAL — Security Vulnerabilities

**C1: SQL RLS JOIN Query Bypass**
```
Location: security_validator.go:173
Current: if stmt.From.TableRefs.Right != nil { return nil }
Impact: Any JOIN query has completely no record-level security
Fix: Inject RLS conditions independently for each table in the JOIN
```

**C2: applyRLS Does Not Check ViewAllRecords**
```
Location: security_validator.go:132-147
Current: Only checks IsSuperUser(), does not check object-level ViewAllRecords
Impact: Users granted ViewAllRecords are still restricted by owner_id filter
Fix: Query user's ViewAllRecords for target object → if true, skip filter
```

#### HIGH — Missing Functionality

**H1: Missing OWD Configuration**
```
Current: No per-object default access level
Impact: Cannot distinguish "Account readable by everyone" vs "Opportunity strictly private"
       All objects implicitly Private, but frontend/reports are unaware of this setting
Fix: Add DefaultInternalAccess field to _System_ObjectMetadata
     applyRLS reads OWD, skips injection when PublicReadWrite
```

**H2: applyRLS Does Not Include Role Hierarchy Subordinates**
```
Location: security_validator.go:204-219
Current: Only injects owner_id = user.ID
Impact: Managers cannot see subordinate records via reports/SOQL (but single-record GET works)
Fix: Use roleHierarchyCache to compute subordinate role IDs
     → Collect user IDs under those roles
     → owner_id IN (...) replaces owner_id = ?
```

**H3: Analytics Routes Missing RunReports Guard**
```
Location: routes.go:159-161
Current: analyticsGroup only has requireAuth + permLoader
Impact: Any authenticated user can run reports
Fix: analyticsGroup.Use(middleware.RequirePermission(constants.PermRunReports))
```

**H4: PermApiEnabled Not Checked at Route Layer**
```
Location: routes.go (global)
Current: PermApiEnabled is defined (perm_system.go:20) but never checked
Impact: Any authenticated user can call all APIs
Fix: Add PermApiEnabled middleware to api group
```

#### MEDIUM — Incomplete Functionality

**M1: Role Hierarchy Propagates Read Only**
```
Location: record_access.go:69
Current: if operation == constants.PermRead && ...
Impact: Managers cannot edit/delete subordinate records via role hierarchy
Fix: Make configurable (per-object GrantAccessUsingHierarchies)
     SF standard behavior: standard objects propagate R+E+D, custom objects configurable
```

**M2: Frontend ViewAll/ModifyAll Hardcoded false**
```
Location: frontend/src/core/query/permissionQueries.ts:35-36
Current: PermissionsViewAllRecords: false (hardcoded)
Impact: Frontend cannot adjust UI based on actual permissions
Fix: Add REST endpoint to expose ListEffectiveObjectPermissionsForUser
     or return per-object ViewAll/ModifyAll in userinfo
```

**M3: UserInfo Missing role_id**
```
Location: oauth_handler.go:291-312
Current: Response has no role_id, role_name fields
Impact: Frontend cannot obtain user role information
Fix: Add "role_id" and "role_name" to userinfo response (1 line of code)
```

**M4: Single-Record vs Bulk Query Results Inconsistent**
```
Current: CheckRecordAccess has 5-layer check, applyRLS only owner_id
Impact: Same user sees different data through different access paths
Fix: Upgrade applyRLS to equivalent subquery (see Ideal Design point 3)
```

#### LOW — Optimization Items

**L1: Sharing Rules Missing Owner-based Type**
```
Current: Only supports role/group + criteria-based
Impact: Cannot configure "records owned by one role's members shared to another role"
       127 users can use criteria-based as substitute
```

**L2: Sharing Rules Have No Cache**
```
Current: GetSharingRules queries DB every time
Impact: Performance (but impact is small at 127 users)
Fix: 5min TTL cache
```

**L3: PermSet Data Cleanup**
```
Current: 139 PermSets contain many third-party tool integrations
Impact: Administrative complexity
Fix: Clean up to ~15 core business PSets
```

---

## Part 3: Implementation Roadmap

### Phase 0: Security Fixes (This Week, 2 Days)

| # | Work Item | Change Scope | Effort |
|---|--------|---------|------|
| 0.1 | applyRLS checks ViewAllRecords | security_validator.go | 0.5d |
| 0.2 | Add RequirePermission(PermRunReports) to Analytics routes | routes.go + Router.tsx | 0.5d |
| 0.3 | applyRLS supports JOIN (inject RLS per table) | security_validator.go | 1d |

### Phase 1: OWD + RLS Upgrade (2-3 Weeks)

| # | Work Item | Change Scope | Effort |
|---|--------|---------|------|
| 1.1 | OWD fields + configuration API | _System_ObjectMetadata + REST | 2d |
| 1.2 | applyRLS reads OWD (skip when PublicRW) | security_validator.go | 1d |
| 1.3 | applyRLS adds Role Hierarchy subordinates | security_validator.go + role_hierarchy.go | 2d |
| 1.4 | applyRLS adds Sharing Rules + Manual Share | security_validator.go | 2d |
| 1.5 | SOQL query handler reuses SecurityValidator | query handler | 2d |

### Phase 2: Feature Completion (2-3 Weeks)

| # | Work Item | Change Scope | Effort |
|---|--------|---------|------|
| 2.1 | PermApiEnabled route guard | routes.go | 0.5d |
| 2.2 | Role Hierarchy Edit/Delete propagation (configurable) | record_access.go + OWD config | 2d |
| 2.3 | UserInfo add role_id/role_name | oauth_handler.go | 0.5d |
| 2.4 | Frontend ViewAll/ModifyAll real data source | permissionQueries.ts + new endpoint | 2d |
| 2.5 | PermSet data cleanup (keep ~15) | data migration | 1d |
| 2.6 | Sharing Rules cache | permission service | 1d |

### Phase 3: PermSet-first Migration (4-6 Weeks, Optional)

| # | Work Item | Change Scope |
|---|--------|---------|
| 3.1 | Design Persona PSG model (5-8 Personas) | Design document |
| 3.2 | Create Minimum Access Profile template | bootstrap data |
| 3.3 | Migrate existing 46 Profiles to PSG model | data migration |
| 3.4 | Setup UI supports PSG management | frontend |

---

## File Index

```
docs/design/permission-system/
├── README.md                        ← This file (comprehensive design + gap analysis)
├── salesforce-permission-model.md   ← SF permission model design philosophy research
├── permission-implementation-map.md ← Complete blueprint of current code implementation
└── permission-refactor-analysis.md  ← Research report three-party review conclusions
```
