# RBAC System Documentation

## Overview

The RBAC (Role-Based Access Control) system provides a database-driven, industry-standard (NIST model) approach to managing user permissions in Fides. It replaces the legacy hardcoded `ROLES_TO_SCOPES_MAPPING` with a flexible, dynamic system that supports:

- **Hierarchical Roles**: Roles can inherit permissions from parent roles
- **Resource Scoping**: Permissions can be scoped to specific resource types and IDs
- **Temporal Roles**: Role assignments can have validity periods (valid_from/valid_until)
- **Separation of Duties (SoD)**: Constraints to prevent conflicting role combinations
- **Custom Roles**: Administrators can create custom roles beyond the system defaults

## Architecture

### Database Schema

The RBAC system adds 5 new tables to the database:

```
┌─────────────────┐     ┌──────────────────────┐     ┌─────────────────┐
│   rbac_role     │────▶│ rbac_role_permission │◀────│ rbac_permission │
└─────────────────┘     └──────────────────────┘     └─────────────────┘
        │
        │ parent_role_id (self-reference)
        ▼
┌─────────────────┐
│   rbac_role     │
└─────────────────┘
        │
        │
        ▼
┌─────────────────┐     ┌──────────────────────┐
│ rbac_user_role  │     │ rbac_role_constraint │
└─────────────────┘     └──────────────────────┘
```

#### Table Details

| Table | Purpose |
|-------|---------|
| `rbac_role` | Role definitions with hierarchy support via `parent_role_id` |
| `rbac_permission` | Permission definitions (seeded from SCOPE_REGISTRY) |
| `rbac_role_permission` | Many-to-many mapping of roles to permissions |
| `rbac_user_role` | User role assignments with resource scoping and temporal validity |
| `rbac_role_constraint` | Separation of duties and cardinality constraints |

### Code Structure

```
fidesplus/
├── src/fidesplus/
│   ├── api/
│   │   ├── routes/rbac.py              # API endpoints
│   │   ├── schemas/rbac.py             # Pydantic models
│   │   └── plus_scope_registry.py      # RBAC-aware OAuth verification
│   ├── config/
│   │   └── rbac_settings.py            # Configuration settings
│   └── service/rbac/
│       ├── __init__.py
│       ├── rbac_service.py             # Business logic
│       ├── rbac_bootstrap.py           # Startup initialization
│       └── permission_evaluator.py     # Permission checking
└── tests/api/
    ├── routes/test_rbac.py             # API tests
    └── service/test_rbac_service.py    # Service tests

fides/
└── src/fides/api/
    ├── models/rbac/                    # SQLAlchemy models
    │   ├── __init__.py
    │   ├── rbac_role.py
    │   ├── rbac_permission.py
    │   ├── rbac_role_permission.py
    │   ├── rbac_user_role.py
    │   └── rbac_role_constraint.py
    └── alembic/migrations/versions/
        ├── xx_..._add_rbac_tables.py   # Schema migration
        └── xx_..._seed_rbac_defaults.py # Data seeding
```

---

## Configuration

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `FIDESPLUS__RBAC__ENABLED` | `false` | Enable database-driven RBAC. When `false`, uses legacy `ROLES_TO_SCOPES_MAPPING`. Separation of duties constraints and temporal roles are always active when RBAC is enabled. |

### Enabling RBAC

Add to your `.env` file:

```bash
FIDESPLUS__RBAC__ENABLED=true
```

Then restart the fidesplus server. On startup:
1. The RBAC migrations will have created and seeded the tables
2. The bootstrap function assigns the "owner" role to the parent user (fidesadmin)
3. All permission checks will use the database-driven RBAC system

---

## API Endpoints

All RBAC endpoints are under `/api/v1/plus/rbac/` and require appropriate scopes.

### Roles

#### List All Roles
```http
GET /api/v1/plus/rbac/roles
```

**Query Parameters:**
- `include_inactive` (bool, default: false): Include inactive roles

**Required Scope:** `rbac_role:read`

**Response:**
```json
[
  {
    "id": "uuid",
    "name": "Owner",
    "key": "owner",
    "description": "Full system access",
    "parent_role_id": null,
    "is_system_role": true,
    "is_active": true,
    "priority": 100,
    "created_at": "2026-01-31T10:00:00Z",
    "updated_at": "2026-01-31T10:00:00Z",
    "permissions": ["system:read", "system:update", ...]
  }
]
```

#### Get Single Role
```http
GET /api/v1/plus/rbac/roles/{role_id}
```

**Required Scope:** `rbac_role:read`

#### Create Custom Role
```http
POST /api/v1/plus/rbac/roles
```

**Required Scope:** `rbac_role:create`

**Request Body:**
```json
{
  "name": "Data Steward",
  "key": "data_steward",
  "description": "Manages data classification and discovery",
  "parent_role_id": "viewer-role-uuid",  // Optional: inherit from parent
  "priority": 60
}
```

**Notes:**
- `key` must be unique and is used for programmatic references
- `priority` determines precedence when a user has multiple roles (higher = more important)
- Setting `parent_role_id` creates role hierarchy (inherits parent's permissions)

#### Update Role
```http
PUT /api/v1/plus/rbac/roles/{role_id}
```

**Required Scope:** `rbac_role:update`

**Request Body:**
```json
{
  "name": "Updated Name",
  "description": "Updated description",
  "is_active": true
}
```

**Notes:**
- System roles can only have `description` and `is_active` modified
- Custom roles can have all fields modified except `is_system_role`

#### Delete Role
```http
DELETE /api/v1/plus/rbac/roles/{role_id}
```

**Required Scope:** `rbac_role:delete`

**Notes:**
- System roles cannot be deleted
- Roles with active user assignments cannot be deleted

#### Update Role Permissions
```http
PUT /api/v1/plus/rbac/roles/{role_id}/permissions
```

**Required Scope:** `rbac_role:update`

**Request Body:**
```json
{
  "permission_codes": ["system:read", "system:update", "privacy-request:read"]
}
```

**Notes:**
- System role permissions cannot be modified
- Replaces all existing permissions with the provided list

---

### Permissions

#### List All Permissions
```http
GET /api/v1/plus/rbac/permissions
```

**Query Parameters:**
- `resource_type` (string, optional): Filter by resource type (e.g., "system", "privacy-request")

**Required Scope:** `rbac_permission:read`

**Response:**
```json
[
  {
    "id": "uuid",
    "code": "system:read",
    "description": "Read system configurations",
    "resource_type": "system",
    "is_active": true
  }
]
```

---

### User Role Assignments

#### List User's Roles
```http
GET /api/v1/plus/rbac/users/{user_id}/roles
```

**Query Parameters:**
- `include_expired` (bool, default: false): Include expired assignments

**Required Scope:** `rbac_user_role:read`

**Response:**
```json
[
  {
    "id": "uuid",
    "user_id": "user-uuid",
    "role_id": "role-uuid",
    "role": {
      "id": "role-uuid",
      "name": "Contributor",
      "key": "contributor"
    },
    "resource_type": null,
    "resource_id": null,
    "valid_from": null,
    "valid_until": null,
    "assigned_by": "admin-user-uuid",
    "created_at": "2026-01-31T10:00:00Z"
  }
]
```

#### Assign Role to User
```http
POST /api/v1/plus/rbac/users/{user_id}/roles
```

**Required Scope:** `rbac_user_role:create`

**Request Body:**
```json
{
  "role_id": "role-uuid",
  "resource_type": "system",           // Optional: scope to resource type
  "resource_id": "specific-system-id", // Optional: scope to specific resource
  "valid_from": "2026-02-01T00:00:00Z", // Optional: start date
  "valid_until": "2026-12-31T23:59:59Z" // Optional: end date
}
```

**Resource Scoping Examples:**
- Global access: `resource_type=null, resource_id=null`
- All systems: `resource_type="system", resource_id=null`
- Specific system: `resource_type="system", resource_id="my-system-uuid"`

**Temporal Roles:**
- Set `valid_from` to delay when the role becomes active
- Set `valid_until` for temporary access that auto-expires

#### Remove Role from User
```http
DELETE /api/v1/plus/rbac/users/{user_id}/roles/{assignment_id}
```

**Required Scope:** `rbac_user_role:delete`

---

### Constraints (Separation of Duties)

#### List All Constraints
```http
GET /api/v1/plus/rbac/constraints
```

**Required Scope:** `rbac_constraint:read`

**Response:**
```json
[
  {
    "id": "uuid",
    "name": "Privacy Admin / System Admin SoD",
    "description": "Users cannot have both privacy admin and system admin roles",
    "constraint_type": "static_sod",
    "role_id_1": "privacy-admin-uuid",
    "role_id_2": "system-admin-uuid",
    "max_users": null,
    "is_active": true
  }
]
```

#### Create Constraint
```http
POST /api/v1/plus/rbac/constraints
```

**Required Scope:** `rbac_constraint:create`

**Request Body (SoD Constraint):**
```json
{
  "name": "Approver / Requester SoD",
  "description": "Users cannot approve their own requests",
  "constraint_type": "static_sod",
  "role_id_1": "approver-role-uuid",
  "role_id_2": "requester-role-uuid"
}
```

**Request Body (Cardinality Constraint):**
```json
{
  "name": "Max 3 Super Admins",
  "description": "Limit the number of super admin users",
  "constraint_type": "cardinality",
  "role_id_1": "super-admin-role-uuid",
  "max_users": 3
}
```

**Constraint Types:**
- `static_sod`: User cannot have both roles simultaneously (enforced at assignment time)
- `dynamic_sod`: User cannot activate both roles in the same session (schema only - not enforced)
- `cardinality`: Limits how many users can have a role (schema only - not enforced)

#### Delete Constraint
```http
DELETE /api/v1/plus/rbac/constraints/{constraint_id}
```

**Required Scope:** `rbac_constraint:delete`

---

### Permission Evaluation

#### Evaluate User Permission
```http
POST /api/v1/plus/rbac/evaluate
```

**Required Scope:** `rbac:evaluate`

**Request Body:**
```json
{
  "user_id": "user-uuid",
  "permission_code": "system:update",
  "resource_type": "system",
  "resource_id": "specific-system-uuid"
}
```

**Response:**
```json
{
  "has_permission": true,
  "reason": "Permission granted via role 'Contributor'",
  "evaluated_roles": ["contributor", "viewer"],
  "matched_via": "direct"
}
```

**`matched_via` Values:**
- `direct`: Permission is directly assigned to one of user's roles
- `inherited`: Permission is inherited from a parent role
- `null`: Permission not granted

---

## Admin UI

### Accessing RBAC Management

The RBAC UI is available under **Settings > Role Management** when the feature flag is enabled.

**Note:** The UI requires the `alphaRbac` feature flag to be enabled in the admin UI. This is currently set to `development: true, test: false, production: false` in `clients/admin-ui/src/flags.json`.

### UI Components

#### Roles List Page (`/settings/roles`)
- View all roles with their priority and status
- Create new custom roles
- Search and filter roles
- Click a role to view/edit details

#### Role Detail Page (`/settings/roles/[id]`)
- Edit role name, description, and priority
- View and modify assigned permissions via checkbox matrix
- See role hierarchy (parent role)
- View users assigned to this role

#### Permission Matrix
- Organized by resource type
- Check/uncheck permissions for custom roles
- System roles show permissions as read-only

#### User Role Assignment
- Access from User Management or Role Detail pages
- Assign roles with optional resource scoping
- Set validity periods for temporary access
- View active and expired assignments

### UI Limitations

- The UI is behind an alpha feature flag and not production-ready
- Permission matrix loads all ~135 permissions at once; consider pagination or virtualization for performance
- Resource scoping UI is basic (text input for resource_type/resource_id)
- No visual representation of role hierarchy tree
- Constraint management is minimal
- Hardcoded `ROLES_WITHOUT_SYSTEM_ASSIGNMENT` list in RolesForm.tsx; if new roles are added that should not support system assignment, update this list (see code comment for details)

---

## System Roles

The following system roles are seeded on migration and cannot be deleted:

| Role Key | Priority | Description |
|----------|----------|-------------|
| `owner` | 100 | Full system access - all permissions |
| `contributor` | 80 | Can create and modify most resources |
| `viewer_and_approver` | 60 | View access plus approval capabilities |
| `approver` | 50 | Can approve privacy requests |
| `viewer` | 40 | Read-only access |
| `respondent` | 20 | Can respond to manual tasks |
| `external_respondent` | 10 | External user for specific tasks |

---

## Implementation Gaps and Limitations

### Implemented Features

#### Sync and Async Route Support
**Status:** ✅ Implemented

RBAC-aware permission checkers are wired into the application via FastAPI's dependency injection system. The checkers override the default permission functions and are invoked for all routes (sync and async) that use `verify_oauth_client` or `verify_oauth_client_plus`.

**Implementation:**
- The permission checkers are injected via `fides.dependency_overrides` in `fidesplus/main.py`
- The checkers follow a composition pattern: first delegate to default OSS permission logic, then check RBAC tables if needed
- Separate sync (`get_rbac_permission_checker`) and async (`get_rbac_permission_checker_async`) implementations
- Only wired in when `CONFIG.rbac.enabled` is true

**Files:**
- `fidesplus/service/rbac/permission_evaluator.py` - `get_rbac_permission_checker()`, `get_rbac_permission_checker_async()`
- `fides/api/oauth/utils.py` - `get_permission_checker()`, `get_async_permission_checker()`

#### Root User Handling
**Status:** ✅ Implemented

The root user (fidesadmin) is a virtual user created from config, not stored in the `fidesuser` table. Special handling has been added in the permission evaluator to detect root client and fall back to legacy permission checking.

**Implementation:**
- `_has_permissions_unified_impl()` detects root client by matching against `CONFIG.security.oauth_root_client_id`
- Root client uses legacy `_has_direct_scopes()` and `_has_scope_via_role()` checks
- This ensures root user always has access based on config scopes

#### Temporal Roles
**Status:** ✅ Implemented

Role assignments can have `valid_from` and `valid_until` timestamps. The system correctly filters out expired role assignments when evaluating permissions.

---

### Features Not Fully Implemented

| Feature | Status | Notes |
|---------|--------|-------|
| **Dynamic SoD Constraints** | Schema only | `dynamic_sod` constraint type exists but is not enforced at runtime |
| **Cardinality Constraints** | Schema only | `max_users` field stored but not checked when assigning roles |
| **Permission Caching** | Not implemented | Every permission check queries the database; consider N+1 query optimization |
| **Audit Logging** | Not implemented | RBAC changes not logged to audit trail |
| **SoD Validation for Existing Assignments** | Not implemented | No endpoint to check existing role assignments against new constraints |
| **PATCH Endpoint for User Roles** | Not implemented | User role updates require DELETE + POST; consider adding PATCH for partial updates |
| **Permission Table Actions** | Not implemented | Permissions list has no action column (edit/view details) |
| **User Role Update Endpoint** | Not implemented | Scope `rbac_user_role:update` is seeded but no PUT endpoint exists; use DELETE + POST |
| **Constraint Update Endpoint** | Not implemented | Scope `rbac_constraint:update` is seeded but no PUT endpoint exists; constraints are immutable after creation |

---

### Medium Priority Gaps

#### OAuth Token Integration
**Status:** Partial

Currently, RBAC permission checks happen at request time by querying the database. The OAuth token still contains legacy `scopes` and `roles` fields from `ROLES_TO_SCOPES_MAPPING`.

**Impact:**
- Token scopes may not match actual RBAC permissions
- Token-based permission checks (webhooks, callbacks) use legacy scopes if no db session available

**To Fix:**
1. Update token generation to embed RBAC-derived permissions
2. Or remove scope embedding from tokens entirely and always check database

#### No Scope Escalation Prevention

Users with `rbac_role:update` can potentially grant permissions they don't have themselves. Consider implementing "you can only grant what you have" logic.

#### Resource Scoping Limitations

Some permissions are inherently global and cannot be scoped to specific resources. For example:
- `system:create` - Cannot be scoped to a specific system that doesn't exist yet
- `system:read`, `system:update`, `system:delete` - Currently apply globally

The Admin UI displays a warning for system permissions that apply globally. This is by design for permissions that cannot logically be scoped to specific resources. Use `system_manager` permissions for per-system access control where needed.

#### UI Feature Flag
**Status:** Alpha Only

The admin UI for RBAC is behind the `alphaRbac` feature flag set to `production: false`. The UI needs additional polish before production use.

### Migration Considerations

#### From Legacy to RBAC

1. **Data Migration Not Automated**

   The current implementation does not migrate existing `fidesuserPermissions` data to RBAC tables. Users retain their legacy permissions until manually reassigned.

   **Recommended Migration Steps:**
   ```sql
   -- Example: Migrate users with 'owner' legacy role to RBAC
   INSERT INTO rbac_user_role (id, user_id, role_id, created_at, updated_at)
   SELECT
     gen_random_uuid(),
     fup.user_id,
     (SELECT id FROM rbac_role WHERE key = 'owner'),
     NOW(),
     NOW()
   FROM fidesuserPermissions fup
   WHERE fup.roles @> '["owner"]'::jsonb
   AND NOT EXISTS (
     SELECT 1 FROM rbac_user_role rur
     WHERE rur.user_id = fup.user_id
     AND rur.role_id = (SELECT id FROM rbac_role WHERE key = 'owner')
   );
   ```

2. **Dual-Mode Operation Not Supported**

   The system operates in either legacy OR RBAC mode, not both simultaneously. Plan for a maintenance window to:
   - Export current user-role mappings
   - Enable RBAC
   - Import mappings to RBAC tables
   - Verify user access
   - Remove legacy data (optional)

3. **Rollback Strategy**

   To rollback: Set `FIDESPLUS__RBAC__ENABLED=false` and restart. The system immediately reverts to legacy permission checking. RBAC tables remain intact for future use.

### Security Considerations

1. **System Role Protection**

   System roles cannot be deleted or have permissions modified, but custom roles can be created with equivalent permissions. Consider adding additional protections if needed.

2. **Resource Scoping Validation**

   The `resource_type` and `resource_id` fields are not validated against actual resources. Invalid values are accepted and result in permissions that never match.

3. **Self-Permission Visibility (By Design)**

   The `/api/v1/plus/rbac/me/permissions` endpoint requires only authentication (no specific scope). This is intentional - users should be able to see their own effective permissions. The endpoint reveals only the current user's permissions, not the full system permission model or other users' permissions.

---

## Testing

### Running RBAC Tests

```bash
# All RBAC tests (63 tests total)
docker exec fidesplus-slim bash -c "pytest --no-cov tests/api/routes/test_rbac.py tests/api/service/test_rbac_service.py tests/api/service/test_permission_evaluator.py -v"

# API route tests (22 tests)
docker exec fidesplus-slim bash -c "pytest --no-cov tests/api/routes/test_rbac.py -v"

# Service layer tests (28 tests)
docker exec fidesplus-slim bash -c "pytest --no-cov tests/api/service/test_rbac_service.py -v"

# Permission evaluator / callback tests (13 tests)
docker exec fidesplus-slim bash -c "pytest --no-cov tests/api/service/test_permission_evaluator.py -v"

# Integration tests (if available)
docker exec fidesplus-slim bash -c "pytest --no-cov tests/api/service/test_rbac_integration.py -v"
```

### Manual Testing

1. Enable RBAC:
   ```bash
   export FIDESPLUS__RBAC__ENABLED=true
   ```

2. Start fidesplus:
   ```bash
   nox -s "dev(slim)"
   ```

3. Verify tables exist:
   ```bash
   docker exec fidesplus-db psql -U postgres -d fidesplus -c "\dt rbac*"
   ```

4. Test API:
   ```bash
   # Get auth token
   TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/oauth/token \
     -d "client_id=fidesadmin&client_secret=fidesadminsecret" | jq -r '.access_token')

   # List roles
   curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/plus/rbac/roles | jq

   # List permissions
   curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/plus/rbac/permissions | jq
   ```

---

## Future Enhancements

### Planned Features

1. **Permission Sets / Templates**
   - Pre-defined permission bundles for common use cases
   - E.g., "Privacy Analyst", "Security Admin", "Data Engineer"

2. **Role Request Workflow**
   - Users can request role assignments
   - Approvers can approve/deny requests
   - Automatic expiration reminders

3. **Delegated Administration**
   - Allow role admins to manage only specific roles
   - Departmental role management

4. **Integration with External IdP**
   - Map SAML/OIDC groups to RBAC roles
   - Just-in-time role provisioning

5. **Permission Dependencies**
   - Define that permission A requires permission B
   - Automatic inclusion of dependent permissions

### Contributing

When contributing to RBAC:

1. Add new permissions to the seed migration
2. Update `ROLES_TO_SCOPES_MAPPING` for backward compatibility
3. Add corresponding tests
4. Update this documentation

---

## Appendix

### Complete Permission List

The RBAC system seeds permissions from `SCOPE_REGISTRY`. Run this to see all permissions:

```bash
curl -H "Authorization: Bearer $TOKEN" \
  http://localhost:8080/api/v1/plus/rbac/permissions | jq '.[].code' | sort
```

### Database Schema Details

```sql
-- View role with permissions
SELECT r.name, r.key, array_agg(p.code) as permissions
FROM rbac_role r
LEFT JOIN rbac_role_permission rp ON r.id = rp.role_id
LEFT JOIN rbac_permission p ON rp.permission_id = p.id
GROUP BY r.id;

-- View user role assignments
SELECT u.username, r.name as role, ur.resource_type, ur.resource_id,
       ur.valid_from, ur.valid_until
FROM rbac_user_role ur
JOIN fidesuser u ON ur.user_id = u.id
JOIN rbac_role r ON ur.role_id = r.id;

-- View active SoD constraints
SELECT name, constraint_type,
       (SELECT key FROM rbac_role WHERE id = role_id_1) as role1,
       (SELECT key FROM rbac_role WHERE id = role_id_2) as role2
FROM rbac_role_constraint
WHERE is_active = true;
```
