# How OpenWork Handles Organization Role-Based Access Control: A Deep Dive into RBAC Implementation

> OpenWork implements robust RBAC, validating organization roles across database, API, and UI layers to block unauthorized access with HTTP 403 errors. Discover the deep dive into its RBAC implementation.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: deep-dive
- Published: 2026-08-01

---

**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:

```ts
// 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:

```ts
// 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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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:

```ts
// 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

```ts
// 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`](https://github.com/different-ai/openwork/blob/main/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.

```ts
// 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/me` endpoint 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`](https://github.com/different-ai/openwork/blob/main/dev/packages/types/src/den/mcp-connection-action.ts) |
| Desktop policy messages | [`dev/packages/types/src/den/desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/dev/packages/types/src/den/desktop-policies.ts) |
| Server-side approval logic | [`dev/apps/server/src/approvals.ts`](https://github.com/different-ai/openwork/blob/main/dev/apps/server/src/approvals.ts) |
| End-to-end role editor tests | [`dev/evals/flows/cloud-super-admins.flow.ts`](https://github.com/different-ai/openwork/blob/main/dev/evals/flows/cloud-super-admins.flow.ts) |
| Database schema verification | [`dev/evals/runner/kube-stack.ts`](https://github.com/different-ai/openwork/blob/main/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.ts`](https://github.com/different-ai/openwork/blob/main/approvals.ts) returns explicit `PROVIDER_ACL_DENIED` errors 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`](https://github.com/different-ai/openwork/blob/main/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.