Roles & Entitlements System¶
Overview¶
The Roles & Entitlements system enables Floh to provision and deprovision external resources (entitlements) when users are granted or revoked business roles. Roles are distinct from RBAC system roles — they represent business-level access such as "Project X Participant" and map to concrete resources in external systems like Authifi groups and Active Directory accounts.
Key Concepts¶
Entitlement Definition (Shared)¶
An independent, reusable template for a single external resource tied to a specific connector. Entitlement definitions are first-class entities that exist independently of roles and can be shared across multiple roles via a many-to-many relationship.
Each entitlement definition specifies:
- Provision config: connector command and parameters to create/grant the resource
- Deprovision config: connector command and parameters to remove the resource
- Reconciliation config (optional): policy that controls what happens when the system detects a mismatch between the entitlement and the upstream resource
For example, a single "AD Group X Membership" entitlement can be linked to both "Project X Participant" and "Project X Administrator" roles.
Role Definition¶
A reusable template that defines what a role means and which entitlements it provisions. Each role definition:
- Has a unique name, description, and status (active/inactive)
- Can specify a default
expires_after_daysTTL for automatic expiry - Is linked to one or more shared entitlement definitions via a join table
- Supports soft-delete for deactivation without data loss
Role Assignment¶
An instance of a role granted to a specific user, tracking:
- Who granted it and when
- Which workflow run triggered it (if any)
- Expiry date (from the role definition's TTL or an explicit value)
- Status:
provisioning,active,partially_provisioned,expired,revoked
Entitlement Instance¶
A tracked provisioned resource within a role assignment. Each instance records:
- The external system's resource ID (
external_id) - Provisioning/deprovisioning timestamps
- Reconciliation status (
ok,missing,error)
Data Model¶
entitlement_definition (independent, shared)
├── connector_id → connector_definition
├── provision_config, deprovision_config, reconciliation_config
└── linked to roles via role_entitlement join table
role_definition
└── linked to entitlements via role_entitlement (many-to-many)
role_entitlement (join table)
├── role_definition_id → role_definition
└── entitlement_definition_id → entitlement_definition
role_assignment (instance of a granted role)
├── role_definition_id → role_definition
├── user_id → user
└── entitlement_instance[] (provisioned resources)
entitlement_instance
├── entitlement_definition_id → entitlement_definition
├── role_assignment_id → role_assignment
└── status, external_id, timestamps
Architecture¶
Workflow Run
└─ role_grant step ──→ RoleService.grantRole()
├─ Creates RoleAssignment
└─ For each linked EntitlementDefinition:
├─ Creates EntitlementInstance (pending)
├─ Calls connector with provision_config
└─ Updates EntitlementInstance (provisioned/failed)
Scheduled Jobs
├─ role-expiry-check (hourly) ──→ Revokes expired assignments
├─ document-expiry-check (hourly) ──→ Expires documents, revokes linked roles
└─ entitlement-reconciliation (daily at 02:00 UTC) ──→ Checks upstream systems
API Endpoints¶
Entitlement Definitions (/api/entitlements)¶
| Method | Path | Description | Permission |
|---|---|---|---|
| GET | / |
List entitlement definitions (paginated, filterable by connector/search) | entitlement:read |
| GET | /:id |
Get single entitlement definition | entitlement:read |
| POST | / |
Create entitlement definition | entitlement:manage |
| PUT | /:id |
Update entitlement definition | entitlement:manage |
| DELETE | /:id |
Delete (rejects if active instances exist) | entitlement:manage |
Role Definitions (/api/roles)¶
| Method | Path | Description | Permission |
|---|---|---|---|
| GET | / |
List role definitions (paginated, filterable) | role_definition:read |
| POST | / |
Create with optional entitlementIds to link |
role_definition:manage |
| GET | /:id |
Get with linked entitlements | role_definition:read |
| PUT | /:id |
Update name, description, status, expiry | role_definition:manage |
| DELETE | /:id |
Soft-delete | role_definition:manage |
| POST | /:id/restore |
Restore soft-deleted | role_definition:manage |
| POST | /:id/entitlements |
Link an existing entitlement definition | role_definition:manage |
| DELETE | /:id/entitlements/:eid |
Unlink an entitlement definition | role_definition:manage |
Role Assignments (/api/role-assignments)¶
| Method | Path | Description | Permission |
|---|---|---|---|
| GET | / |
List (filter by userId, roleDefinitionId, status; ?include=entitlements) |
entitlement:read |
| GET | /:id |
Get with entitlement instances | entitlement:read |
| POST | / |
Manual grant outside workflow | entitlement:manage |
| POST | /:id/revoke |
Revoke and deprovision | entitlement:manage |
| POST | /:id/reprovision |
Re-provision failed/orphaned | entitlement:manage |
POST / body supports an onDuplicate field (skip, error, renew, update) to control behavior when the user already has an active assignment for the role. Defaults to skip. Returns 201 for new assignments and 200 when an existing assignment was skipped, renewed, or updated.
Reconciliation (/api/reconciliation)¶
| Method | Path | Description | Permission |
|---|---|---|---|
| POST | /run |
Trigger manual reconciliation | entitlement:manage |
| GET | /status |
Last reconciliation results | entitlement:read |
Webhook (/api/entitlements/webhook)¶
| Method | Path | Description |
|---|---|---|
| POST | /:connectorId |
Receive upstream change events |
Documents (/api/documents)¶
Enhanced with new query parameters:
userId— filter by subject userstatus— filter by document status (uploaded, approved, rejected, expired)expiringBefore— ISO date, show documents expiring before this dateexpiringSoon— boolean, shorthand for documents expiring within 30 days
Enriched response includes uploader_name, workflow_name, and role_assignments[].
Workflow Step Types¶
role_grant¶
Grants a role to a user during workflow execution. Supports configurable duplicate handling for cases where the user already has an active assignment for the same role.
Config:
{
"roleDefinitionId": "uuid-of-role-definition",
"userId": "uuid-of-target-user",
"expiresAt": "2025-12-31T00:00:00Z",
"failOnPartial": false,
"onDuplicate": "skip"
}
onDuplicate strategies:
| Strategy | Behavior |
|---|---|
skip (default) |
If the user already has an active assignment, return success without changes. Ideal for idempotent workflows. |
error |
If the user already has an active assignment, fail the step. Forces explicit handling of duplicates. |
renew |
Update the expires_at on the existing assignment. Use for recertification flows where the goal is to extend access. |
update |
Reconcile entitlements on the existing assignment against the current role definition (provision new, deprovision removed) and optionally update expires_at. Use when the role's entitlements may have changed since the original grant. |
Output variables:
- roleAssignmentId — ID of the created or existing assignment
- roleProvisioned — boolean, true if all entitlements provisioned
- provisionedCount — number of successfully provisioned entitlements
- failedCount — number of failed entitlements
- roleGrantAction — what actually happened: created, skipped, renewed, or updated
role_revoke¶
Revokes all active assignments for a role/user combination.
Config:
{
"roleDefinitionId": "uuid-of-role-definition",
"userId": "uuid-of-target-user",
"reason": "Access no longer required"
}
Output variables:
- revokedCount — number of assignments revoked
document_submission (enhanced)¶
New config field:
- expiresAfterDays — integer, auto-calculates expires_at when a document is uploaded
Document Lifecycle¶
Documents now track expiry and subject user:
- Upload:
expires_atcalculated from step config'sexpiresAfterDaysor explicit field - Approval: Document becomes
approved - Expiry: Scheduled job detects
expires_at < now()for approved docs - Revocation: Expired documents trigger revocation of linked role assignments (via
run_id)
Connector Extensions¶
Authifi¶
New commands:
- checkGroupMembership — verifies user is still a member of a specified group
- Config: { "command": "checkGroupMembership", "groupId": "...", "userId": "..." }
- Returns: { "isMember": boolean }
Test Active Directory¶
New commands:
- checkGroupMembership — checks if user is in an AD group
- checkAccountExists — checks if an AD account exists
- createAccount — creates a new AD account (simulated)
Reconciliation¶
Reconciliation Policies¶
Each entitlement definition can specify a reconciliation policy that controls what happens when the system detects a mismatch between the entitlement and the upstream resource. The policy is stored as { "policy": "<value>" } in the reconciliation_config column.
| Policy | Label (UI) | Behavior |
|---|---|---|
null |
None | No reconciliation checks are performed for this entitlement. |
log_only |
Log only | Run the check and log any discrepancy to the audit log, but don't change entitlement status. Useful for monitoring without automated remediation. |
flag |
Flag as out of sync | Mark the entitlement instance as orphaned when the upstream resource is missing. This was the only behavior prior to the policy system. |
sync |
Keep synced | Automatically re-provision the resource via the connector if it is missing upstream. |
Auto-derived Check Commands¶
The system automatically derives which connector command to run during reconciliation. Each provision command in a connector schema can declare a reconcilesWith field that names the corresponding check command. For example:
{
"addToGroup": {
"params": ["groupId", "userId"],
"reconcilesWith": "checkGroupMembership"
},
"checkGroupMembership": {
"params": ["groupId", "userId"]
}
}
When reconciling, the system:
1. Reads the policy from the entitlement's reconciliation_config
2. Looks up reconcilesWith on the provision command in the connector schema
3. Copies matching parameter values from the entitlement's provision_config to build the check command
4. Executes the check command via the connector
5. Applies the policy if a mismatch is detected
Backward Compatibility¶
Legacy reconciliation_config values that contain a command field (e.g., { "command": "checkGroupMembership", "groupId": "..." }) are treated as flag policy and the config is used directly as the check command. No data migration is required.
Scheduled Reconciliation¶
Runs daily at 02:00 UTC. For each provisioned entitlement instance with a reconciliation_config:
- Resolves the reconciliation policy and derives the check command
- Executes the check command via the connector
- If the response confirms existence → updates
last_reconciled_atand status took - If the response indicates the resource is missing → applies the policy (log, flag, or sync)
- If an error occurs → marks
reconciliation_statusaserror
Webhook Support¶
External systems can notify Floh of resource changes via POST /api/entitlements/webhook/:connectorId:
The system correlates resourceId with entitlement_instance.external_id and marks matching instances as orphaned.
Permissions¶
| Permission | Description | Roles |
|---|---|---|
role_definition:read |
View role definitions | admin, approver, resource_manager |
role_definition:manage |
Create/update/delete role definitions, link/unlink entitlements | admin, resource_manager |
entitlement:read |
View entitlement definitions and assignment status | admin, approver, resource_manager |
entitlement:manage |
Create/update/delete entitlement definitions, grant/revoke roles, trigger reconciliation | admin, resource_manager |
Database Schema¶
Tables (migrations 021–022)¶
role_definition— business role templates with soft-deleteentitlement_definition— shared resource templates tied to connectors (independent of roles)role_entitlement— join table linking roles to entitlements (many-to-many, composite PK)role_assignment— user-role grants with lifecycle trackingentitlement_instance— provisioned resource tracking with reconciliation statusdocument— addedexpires_at(timestamp) andsubject_user_id(FK to user)
Example: Complete Flow¶
- Admin creates entitlement definitions on the Entitlements page:
Genomics Portal Access— Authifi group membership (provision:addToGroup, deprovision:removeFromGroup, reconciliation policy:flag)-
Research File Share Access— AD account (provision:createAccount, deprovision:disableAccount, reconciliation policy:sync) -
Admin creates role "Project X Participant" on the Role Definitions page and links both entitlements
-
Workflow:
document_submission(withexpiresAfterDays: 365) →approval→role_grant -
User submits document, gets approved,
role_grantstep executes: - Creates role assignment
- Provisions Authifi group membership via connector
- Provisions AD account via connector
-
Assignment status →
active -
Daily reconciliation confirms entitlements exist upstream
-
One year later: document expires →
document-expiry-checkjob detects it → role revoked → Authifi membership removed → AD account disabled -
If admin manually removes user from Authifi group: reconciliation detects orphan → entitlement instance marked as
orphaned→ admin can reprovision or revoke via UI
UI Pages¶
- Entitlements (
/entitlements) — shared entitlement definition management (CRUD), with connector selection, config editors for provision/deprovision, and a reconciliation policy dropdown - Role Definitions (
/roles) — list and detail views; link/unlink shared entitlements to roles - Role Assignments (
/role-assignments) — global view with status filters, revoke/reprovision actions - User Entitlements (
/users/:id/entitlements) — per-user view of role assignments and entitlement instances, with active/history toggle - Documents (
/documents) — enriched document browser with expiry badges and role linkage - Workflow Designer —
role_grantandrole_revokestep types with role lookup dialog - Sidebar — "Access" section with Entitlements, Role Definitions, Role Assignments, and Documents links