Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e0a0c70
feat: add RBAC contracts and persistence
ColeMurray Aug 31, 2026
46b620d
feat: enforce workspace permissions at the HTTP boundary
ColeMurray Aug 31, 2026
68789f0
feat: enforce session authorization and revoke stale sockets
ColeMurray Aug 31, 2026
9c9403d
feat: enforce automation ownership and execution authority
ColeMurray Aug 31, 2026
1333903
feat: add workspace access administration
ColeMurray Aug 31, 2026
3f39e3a
feat: gate session and automation UI by permission
ColeMurray Aug 31, 2026
ade1647
chore: preserve original RBAC patch bytes
ColeMurray Aug 31, 2026
4866d41
fix(rbac): address foundation review feedback
ColeMurray Aug 31, 2026
ab4dc1b
Merge branch 'rbac-foundation' into rbac-http-enforcement
ColeMurray Aug 31, 2026
34e6d03
Merge branch 'rbac-http-enforcement' into rbac-session-authorization
ColeMurray Aug 31, 2026
ba3774c
Merge branch 'rbac-session-authorization' into rbac-automation-author…
ColeMurray Aug 31, 2026
9f6f939
Merge branch 'rbac-automation-authorization' into rbac-workspace-sett…
ColeMurray Aug 31, 2026
c20ee34
Merge branch 'rbac-workspace-settings' into rbac-permission-aware-ui
ColeMurray Aug 31, 2026
675a554
fix(rbac): preserve merge batch result contract
ColeMurray Aug 31, 2026
cf5e5e5
Merge branch 'rbac-foundation' into rbac-http-enforcement
ColeMurray Aug 31, 2026
0c33bb8
Merge branch 'rbac-http-enforcement' into rbac-session-authorization
ColeMurray Aug 31, 2026
2644c82
Merge branch 'rbac-session-authorization' into rbac-automation-author…
ColeMurray Aug 31, 2026
45fd9fe
Merge branch 'rbac-automation-authorization' into rbac-workspace-sett…
ColeMurray Aug 31, 2026
cc4edf4
Merge branch 'rbac-workspace-settings' into rbac-permission-aware-ui
ColeMurray Aug 31, 2026
7edc1f5
Merge commit 'cc4edf4cbcc725da1428fd7a79d0c19d945141b0' into HEAD
ColeMurray Aug 31, 2026
82f7264
feat: gate session and automation UI by permission
ColeMurray Aug 31, 2026
a1a5858
fix: address permission-aware UI review feedback
ColeMurray Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ The system uses a shared GitHub App installation for git operations (clone, fetc
control plane mints short-lived installation tokens server-side and brokers them to sandboxes
through the git credential helper on demand. This means:

- **All users share the same GitHub App credentials** - The GitHub App must be installed on your
organization's repositories, and any user of the system can access any repo the App has access to
- **Authorized users share the same GitHub App credentials** - The GitHub App must be installed on
your organization's repositories, and active users whose role permits repository use can access
any repo the App has access to
- **No per-user repository access validation** - The system does not verify that a user has
permission to access a specific repository before creating a session
- **GitHub users' OAuth tokens are used for PR creation** - For GitHub logins, PRs are created using
Expand Down Expand Up @@ -70,6 +71,9 @@ built for internal use where all employees are trusted and have access to compan
4. **Use GitHub's repository selection** - When installing the App, select specific repositories
rather than "All repositories"

See [Authentication and Authorization](docs/AUTH.md) for workspace roles, session access, automation
ownership, bots, and member suspension.

## Architecture

```
Expand Down
206 changes: 206 additions & 0 deletions docs/AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# Authentication and Authorization

Open-Inspect uses authentication to establish who you are and workspace authorization to decide what
you can do. This guide explains the behavior users and workspace administrators will see.

> **Important:** Open-Inspect is designed for a single trusted organization. A deployment is one
> workspace, and the source-control App installation defines the repositories available to that
> workspace. Roles control which Open-Inspect features a person can use; they are not per-repository
> access lists.

---

## Signing In

A deployment can offer GitHub sign-in, Google sign-in, or both. The sign-in page shows only the
providers configured by the deployment operator.

Signing in has two stages:

1. Your identity provider verifies your identity and email address.
2. The deployment's admission rules determine whether you may join the workspace.

Depending on the deployment configuration, admission can be limited by:

- GitHub username
- Verified email address
- Verified email domain
- Active membership in an allowed GitHub organization

These rules are checked when you sign in. Removing someone from an allowlist or GitHub organization
does not end an existing browser session; an Administrator or Owner can suspend the member when
access must be revoked immediately.

Authentication does not make someone an Owner or Administrator. Every admitted user has exactly one
workspace role, and new users receive the Member role by default.

## Workspace Roles

Open-Inspect includes four built-in roles.

| Capability | Owner | Administrator | Member | Viewer |
| ------------------------------------------------- | :---: | :-----------: | :----: | :----: |
| View repositories and environments | Yes | Yes | Yes | Yes |
| Use repositories and environments in sessions | Yes | Yes | Yes | No |
| Manage shared settings, integrations, and secrets | Yes | Yes | No | No |
| Create sessions | Yes | Yes | Yes | No |
| View every session | Yes | Yes | Yes | Yes |
| Collaborate in and manage sessions | Yes | Yes | Yes | No |
| View automations | Yes | Yes | Yes | Yes |
| Create automations | Yes | Yes | Yes | No |
| Manage and trigger own automations | Yes | Yes | Yes | No |
| Manage and trigger any automation | Yes | Yes | No | No |
| View and manage workspace members | Yes | Yes | No | No |
| Transfer workspace ownership | Yes | No | No | No |
| View analytics | Yes | Yes | Yes | Yes |
| View provider accounts | Yes | Yes | Yes | No |
| View image-build history | Yes | Yes | Yes | Yes |
| Manage personal skill profiles | Yes | Yes | Yes | No |

### Owner

Owners have full access to the workspace. Only Owners can grant or remove the Owner role or suspend
and restore another Owner. Open-Inspect also prevents the final active Owner from being suspended or
demoted, so the workspace cannot accidentally lose all ownership.

### Administrator

Administrators can operate the workspace day to day. They can manage members, sessions, automations,
repositories, environments, provider accounts, integrations, and secrets. They cannot transfer
ownership, change who holds the Owner role, or suspend and restore an Owner.

### Member

Members can create and use sessions, collaborate in existing sessions, use shared repositories and
environments, and create automations. They can manage and manually trigger automations they own but
cannot modify another person's automation or administer shared configuration. They can view
workspace analytics.

### Viewer

Viewers have read-only access to shared workspace resources. They can inspect sessions, automations,
analytics, repositories, environments, skills, and MCP servers. They cannot create or prompt
sessions, access sandboxes, manage personal skill profiles, trigger automations, or change shared
configuration.

## How Session Access Works

Sessions are workspace resources rather than private resources owned by their creator.

- Anyone with session read access can view every session in the workspace.
- Anyone with collaboration access can prompt and contribute to every session.
- Anyone with lifecycle access can stop, retry, archive, unarchive, and otherwise manage every
session.
- Anyone with sandbox access can use supported sandbox tools for every session.
- Anyone with delete access can delete every session.

The creator shown on a session records attribution; it is not an access list. Likewise, participant
labels identify who contributed to a session but do not grant or remove workspace permissions. The
**Mine** filter is a convenience for finding sessions you created, not a security boundary.

Creating a session also requires permission to use its selected repository or environment. A role
may therefore be able to view an existing session without being allowed to create a new one.

New HTTP requests reflect role changes and suspension immediately. Live browser connections to a
session are rechecked at least every five minutes, so a connection may remain open for up to five
minutes after access changes. Recreating the session is not required.

## How Automation Access Works

Automation definitions and run history are visible workspace-wide to roles with automation read
access. Creating, changing, and manually triggering automations use ownership rules.

- Members can manage and manually trigger automations they own.
- Administrators and Owners can manage and manually trigger any automation.
- Viewers can inspect automations but cannot create, change, or run them.

Automation ownership follows the signed-in account that created it, not a display name or external
provider username.

### Scheduled and Event Runs

Scheduled and event-driven runs execute under the automation owner's authority. At run time, the
owner must still be active and allowed to create sessions and use every selected repository or
environment. If those permissions have been removed, the run does not start.

### Manual Runs

A manual run executes under the authority of the person who clicked **Run**, even when an
Administrator or Owner triggers someone else's automation. The requester must be allowed both to
trigger that automation and to create the resulting session with its selected resources. Their
identity and linked source-control credentials are used for that run.

See [Automations](AUTOMATIONS.md) for trigger setup and run behavior.

## Bots and Integrations

Slack, GitHub, and Linear integrations act on behalf of a workspace user when they handle a user
request. Their effective access is limited by both:

- The acting user's current role
- The integration's fixed set of allowed operations

This means an integration cannot bypass a suspended user or perform workspace administration simply
because the acting user is an Owner. Calls that do not identify an acting user are denied unless a
specific integration route explicitly permits that operation.

Some integrations also apply their own ingress rules. For example, the GitHub integration may
require an allowed trigger user or sufficient repository collaborator access before it sends a
request to Open-Inspect.

## Suspension

Suspending a member disables their workspace access without deleting their account or historical
attribution.

After suspension:

- New browser and bot operations are denied.
- Existing browser sign-in sessions are invalidated.
- Live browser session connections close within five minutes.
- Scheduled and event-driven automations owned by the member no longer pass run authorization.
- Existing session history and authorship remain intact.

Suspension does not automatically stop a sandbox that is already executing. An Administrator or
Owner can manage that session separately.

## Repository and Credential Boundaries

Open-Inspect uses a shared source-control App installation for clone, fetch, and push operations.
The App should be installed only on repositories intended for the workspace.

A user's role determines whether they may read or use workspace repositories, but Open-Inspect does
not compare that role with the user's personal GitHub access for each repository. Linked GitHub
credentials can be used for actions such as attributed pull-request creation; when no suitable user
credential is available, supported operations may use the shared App identity.

Secrets and provider credentials are not made visible through role-based read access. Administrative
permissions control who can configure them, and saved secret values are not returned to the browser.
See [Secrets Management](SECRETS.md) for details.

## Workspace Administration

Owners and Administrators can manage members from **Settings > Workspace access**. Depending on
their own role, they can:

- Review workspace members and assigned roles
- Change a member's role
- Suspend or restore a member

Only an Owner can assign or remove the Owner role or suspend and restore another Owner. The final
active Owner cannot be suspended or demoted.

### Initial Owner Setup

The first person who signs in receives the default Member role and is not promoted to Owner
automatically. On a new deployment, the intended Owner must sign in once, after which a deployment
operator runs the Owner bootstrap command using that person's Open-Inspect user ID. See
[Getting Started](GETTING_STARTED.md#step-7a-bootstrap-the-workspace-owner) for the deployment
steps.

## Related Guides

- [Getting Started](GETTING_STARTED.md)
- [Automations](AUTOMATIONS.md)
- [Secrets Management](SECRETS.md)
- [How Open-Inspect Works](HOW_IT_WORKS.md)
62 changes: 54 additions & 8 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,11 @@ GitHub OAuth sign-in, but its client pair is optional when Google is the only si
> **Keep "User-to-server token expiration" active** (GitHub App → **Optional Features**; it is
> the default for newly created Apps, but activate it if yours predates that default). Expiring
> user tokens are what make GitHub return a **refresh token** at sign-in, and Open-Inspect stores
> that per-user credential so sessions clone, commit, and push **as the signed-in user**. With
> expiration deactivated — or on an **OAuth App**, which never issues a refresh token — no
> per-user credential is captured and sessions fall back to the shared GitHub App **bot**
> identity for repository access.
> that per-user credential for attributed GitHub operations such as pull-request creation. Clone,
> fetch, and push authentication still use the shared GitHub App installation. With expiration
> deactivated — or on an **OAuth App**, which never issues a refresh token — no per-user
> credential is captured, so supported attributed operations fall back to the shared GitHub App
> **bot** identity.

5. Set **Repository permissions**:
- Actions: **Read-only** _(required for GitHub workflow-run automations)_
Expand Down Expand Up @@ -651,10 +652,9 @@ configurations because they authorize repository operations; they do not enable

### Enable Google Login (Optional)

Google login lets non-developer users (PMs, support agents) sign in without a GitHub account. They
get the same flat access as everyone else; git operations still use the shared GitHub App, and their
PRs fall back to the App bot (no personal GitHub attribution unless the same verified email is also
a linked GitHub identity).
Google login lets non-developer users (PMs, support agents) sign in without a GitHub account. Git
operations still use the shared GitHub App, and their PRs fall back to the App bot (no personal
GitHub attribution unless the same verified email is also a linked GitHub identity).

1. In the [Google Cloud Console](https://console.cloud.google.com/apis/credentials), create an
**OAuth client ID** of type **Web application**.
Expand Down Expand Up @@ -726,6 +726,52 @@ Terraform will update the workers with the required bindings.

---

## Step 7a: Bootstrap the Workspace Owner

Owner assignment is an explicit operator action. After both deployment phases complete:

1. Have the intended Owner sign in to the deployed web application once. This creates their
canonical user and default role assignment.
2. While signed in, open `/api/auth/get-session` on the web application origin and record the
32-character lowercase hexadecimal `user.id`. The bootstrap command accepts this canonical ID,
never an email address.
3. Obtain the D1 database name with `terraform output -raw d1_database_name` from
`terraform/environments/production`.
4. From the repository root, run the remote dry run (the default):

```bash
npm run rbac:bootstrap-owner -- \
--database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
--user "<canonical-user-id>"
```

5. Confirm the preflight result is `ready` (or `no-op` when the target is already the current
unsuspended Owner), then execute the same command with `--execute`:

```bash
npm run rbac:bootstrap-owner -- \
--database "$(terraform -chdir=terraform/environments/production output -raw d1_database_name)" \
--user "<canonical-user-id>" \
--execute
```

The command uses Wrangler credentials (`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`, or
`wrangler login`) and targets remote D1. It refuses a suspended/missing user, a missing or ambiguous
assignment, or another unsuspended Owner. There is no force option. Execution is one atomic Wrangler
SQL file: it writes one redacted `workspace.owner_bootstrapped` service audit event and replaces the
target's assignment. A no-op writes nothing.

6. Verify the control-plane health response contains `"rbac":{"ownerAssignment":"present"}`:

```bash
curl "$(terraform -chdir=terraform/environments/production output -raw control_plane_url)/health"
```

This health value reports current state: `present` means at least one Owner assignment belongs to an
unsuspended user.

---

## Step 7b: Complete Slack Setup (If Using Slack)

Now that the Slack bot worker is deployed, configure the agent experience, App Home, and event
Expand Down
6 changes: 1 addition & 5 deletions packages/control-plane/src/router.policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,7 @@ describe("route policy table", () => {
});
expect(routeFor("POST", "/sessions/session-1/ws-token")?.authorization).toMatchObject({
kind: "active-user",
allOf: [
{ kind: "permission", permission: "sessions.read" },
{ kind: "permission", permission: "sessions.collaborate" },
{ kind: "permission", permission: "sessions.lifecycle" },
],
allOf: [{ kind: "permission", permission: "sessions.read" }],
});
expect(routeFor("POST", "/sessions/session-1/stop")?.authorization).toMatchObject({
service: { kind: "actor", actorlessGrants: [{ service: "linear-bot" }] },
Expand Down
14 changes: 10 additions & 4 deletions packages/control-plane/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import { UserStore } from "./db/user-store";
import { AutomationStore } from "./db/automation-store";
import { AuthorizationError, AuthorizationService } from "./authorization/service";
import { serviceAllowsPermission } from "./authorization/service-permissions";
import { SCOPED_PERMISSION_PAIRS, resolveScopedPermission } from "@open-inspect/shared/rbac";
import {
SCOPED_PERMISSION_PAIRS,
hasScopedPermission,
resolveScopedPermission,
} from "@open-inspect/shared/rbac";
import { createLogger } from "./logger";
import type { BackgroundTasks } from "./platform-ports";
import {
Expand Down Expand Up @@ -478,11 +482,13 @@ async function enforceAutomationRequirement(
const automation = await store.resolveCanonicalOwner(storedAutomation);

const permissionStem = `automations.${requirement.operation}` as const;
const permissionScope = resolveScopedPermission(permissionStem, authorization.permissions);
const ownPermission = SCOPED_PERMISSION_PAIRS[permissionStem].own;
if (
!permissionScope ||
(permissionScope === "own" && automation.user_id !== ctx.principal.userId)
!hasScopedPermission(
permissionStem,
authorization.permissions,
automation.user_id === ctx.principal.userId
)
) {
return json(
{ error: "Forbidden", code: "permission_required", permission: ownPermission },
Expand Down
Loading
Loading