How OpenWork Handles Organization Role-Based Access Control: A Deep Dive into RBAC Implementation
OpenWork enforces organization-wide permissions through a role-based access control (RBAC) model that validates roles at the database, API, and UI layers, rejecting unauthorized requests with HTTP 403 errors.
The different-ai/openwork repository implements a comprehensive RBAC system where each organization member receives a specific role—owner, super-admin, admin, or member—that determines what actions they can perform. This guide explains how role-based access control works across OpenWork's architecture, from type definitions to server-side enforcement and UI conditional rendering.
Role Definitions and Type Safety
OpenWork defines organization roles in type-safe TypeScript files shared across the cloud (Den) and desktop environments. These definitions serve as the foundation for all permission checks.
Den role identifiers specify available organization actions:
// dev/packages/types/src/den/mcp-connection-action.ts
export type OrgAction = "organization_admin" | /* … */;
Desktop policy messages provide user-facing explanations when organization administrators restrict functionality:
// dev/packages/types/src/den/desktop-policies.ts
"Your organization administrator has disabled adding custom providers."
These type definitions ensure role values are validated at compile time throughout the codebase.
Database Layer: Role Persistence
Roles are stored persistently in the organization_member table. The database schema verification in dev/evals/runner/kube-stack.ts confirms the presence of organization tables with proper role columns, establishing the data foundation for all subsequent permission checks.
When a member joins an organization, their role is written to this table and propagated to authentication tokens used by both API and UI layers.
Server-Side Enforcement in Approvals.ts
The core RBAC logic resides in dev/apps/server/src/approvals.ts. This module validates role changes and returns structured errors when permissions are insufficient.
Permission Denial Pattern
When a caller lacks required permissions, the server returns a 403 Forbidden response:
// dev/apps/server/src/approvals.ts
if (!callerHasPermission) {
return toolError(
context.state.issueOpaque("provider-request"),
"PROVIDER_ACL_DENIED",
"The synthetic provider denied this operation through roles or ACLs",
403,
"provider_acl",
false
);
}
The PROVIDER_ACL_DENIED error code specifically identifies role-based rejections, enabling clients to handle permission failures appropriately.
Protected Operations
Three critical protections are hardcoded in the approval logic:
- Owner protection — Owner accounts cannot be demoted or deleted; attempts return 400 or 403
- Admin limits — Admins can view member lists but cannot modify roles through any API endpoint
- Super-admin privileges — Only super-admins receive authorization for role-changing operations
UI Layer: Conditional Role Editor Rendering
OpenWork's frontend mirrors server permissions to prevent privilege escalation through the interface. The role editor component is conditionally rendered based on the authenticated user's role.
Implementation Pattern
// Simplified React component pattern
const { role } = useCurrentUser();
const showEditor = role === "super-admin";
return (
<>
{showEditor ? <RoleEditor /> : <NoEditorMessage />}
</>
);
Flow Test Verification
The end-to-end tests in dev/evals/flows/cloud-super-admins.flow.ts explicitly verify this behavior:
-
Admin view — The role editor is absent
claim: "An admin can open Members … but the role editor is absent"(line 237) -
Super-admin view — The editor is visible and functional
ctx.assert(roleEditor.visible, "Super‑admin should see the member role editor.")(line 331)
This dual-layer protection ensures that even if a client bypasses UI restrictions, the server remains the authoritative permission gatekeeper.
Practical Example: Updating Member Roles
Below is a complete client implementation that respects OpenWork's RBAC model. It validates the caller's role before attempting server operations and handles the expected error responses.
// client.ts — role update with RBAC awareness
import { fetchJson } from "./utils";
async function updateMemberRole(
memberId: string,
newRole: "owner" | "admin" | "member" | "super-admin"
) {
// Retrieve current user's role from authenticated endpoint
const { role: callerRole } = await fetchJson("/v1/me");
// Client-side guard: prevent requests that would certainly fail
if (callerRole !== "super-admin") {
throw new Error("Only super-admin can change roles");
}
const resp = await fetchJson(`/v1/organization/members/${memberId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ role: newRole }),
});
if (!resp.ok) {
// Server returns 403 PROVIDER_ACL_DENIED if any check fails
const err = await resp.json();
throw new Error(`Failed (${resp.status}): ${err.message}`);
}
return await resp.json();
}
Key implementation details:
- The
/v1/meendpoint provides the caller's role from their JWT token - The
PATCH /v1/organization/members/{id}endpoint performs the actual update - Server-side re-validation is mandatory; client checks are for UX optimization only
- Non-super-admins receive 403 errors even if they construct requests manually
Key Files in the RBAC Implementation
| Purpose | File Path |
|---|---|
| Den role type definitions | dev/packages/types/src/den/mcp-connection-action.ts |
| Desktop policy messages | dev/packages/types/src/den/desktop-policies.ts |
| Server-side approval logic | dev/apps/server/src/approvals.ts |
| End-to-end role editor tests | dev/evals/flows/cloud-super-admins.flow.ts |
| Database schema verification | dev/evals/runner/kube-stack.ts |
Summary
OpenWork's organization role-based access control operates through three synchronized layers:
- Type-safe role definitions prevent invalid role values at compile time
- Database persistence ensures roles survive sessions and propagate to authentication tokens
- Server-side enforcement in
approvals.tsreturns explicitPROVIDER_ACL_DENIEDerrors with HTTP 403 status - UI conditional rendering prevents users from seeing controls they cannot use
- Owner immutability guarantees organization ownership cannot be transferred through standard role changes
The system prioritizes fail-closed behavior: any ambiguity in permissions results in denial, with clear error codes for debugging and user communication.
Frequently Asked Questions
What roles exist in OpenWork organizations?
OpenWork defines four primary roles: owner (sole, immutable), super-admin (full management rights), admin (view-only for member management), and member (standard participation). These are encoded in OrgAction types and enforced throughout the stack.
Why does OpenWork return 403 instead of 401 for role violations?
HTTP 403 indicates the server understood the request but refuses to authorize it, which matches RBAC semantics—the user is authenticated but lacks the specific role required. The PROVIDER_ACL_DENIED error code in the response body provides additional context for client handling.
Can admins ever change member roles in OpenWork?
No. According to the flow tests in cloud-super-admins.flow.ts, admins see no role editor in the UI and receive 403 errors for any direct API attempts. Only super-admins and owners possess role modification capabilities.
How does OpenWork prevent owners from being removed?
The approval logic explicitly protects owner accounts. Attempts to demote or delete an owner return 400 or 403 status codes without modifying the database, ensuring organizational continuity and preventing accidental or malicious ownership transfer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →