RBAC roles & bindings
Nanosync RBAC has three concepts: permissions (a verb on a resource type), roles (a named bag of permissions), and bindings (subject + role + scope).
A subject is a user or service account. A scope is one of org, workspace, or project. A binding grants a role at a scope, and the grant applies to every descendant (scope containment).
Permission matrix
5 verbs × 14 resource types. The cells marked ✓ are the only meaningful combinations; the others have no semantic meaning (e.g. you can’t run a workspace).
| Resource | read | write | delete | run | admin |
|---|---|---|---|---|---|
| org | ✓ | ✓ | ✓ | ||
| workspace | ✓ | ✓ | ✓ | ✓ | |
| project | ✓ | ✓ | ✓ | ✓ | |
| pipeline | ✓ | ✓ | ✓ | ✓ | ✓ |
| connection | ✓ | ✓ | ✓ | ✓ | |
| run | ✓ | ✓ (cancel) | |||
| log | ✓ | ||||
| audit | ✓ | ||||
| role | ✓ | ✓ | ✓ | ✓ (bind) | |
| user | ✓ | ✓ | ✓ | ✓ | |
| token | ✓ | ✓ | ✓ | ||
| plugin | ✓ | ✓ | ✓ | ||
| idp | ✓ | ✓ | ✓ | ||
| secret | ✓ | ✓ | ✓ |
Why run exists: starting a pipeline is operationally distinct from editing its YAML. On-call engineers should be able to restart a stuck pipeline at 3am without holding the keys to change its config.
Why admin is a meta-verb: (admin, pipeline) grants every other verb on pipelines. (admin, org) is god-mode at that scope. Avoids modeling an explicit role hierarchy.
Built-in roles
Four roles ship seeded into every database. They’re immutable — the API rejects modifications.
admin
- Scope-type:
org - Permissions: every (verb, resource) tuple in the matrix
- Use for: org owners, platform engineers, break-glass accounts
editor
- Scope-type:
workspace - Permissions: r/w/d/run on
pipeline; r/w/d onconnection; r onrun,log,project,workspace,org - Use for: data engineers who design and operate pipelines within a workspace
operator
- Scope-type:
workspace - Permissions: r on
pipeline,connection; run onpipeline; r/d onrun; r onlog,audit - Use for: on-call engineers. Can restart pipelines and read audit logs; can’t change config or invite users.
viewer
- Scope-type:
workspace - Permissions: r on
pipeline,connection,run,log,project,workspace - Use for: stakeholders, support staff, anyone who needs to see what’s running without touching it
audit access is intentionally NOT in editor or viewer. Reading audit logs is a separate decision from reading pipeline config — give it to operator and to explicit auditor bindings only.
Scope containment
A binding at a broader scope applies to everything inside it.
Alice ── editor @ workspace=analytics
│
├─ project=prod ← Alice can edit pipelines here
└─ project=staging ← and here
Bob ── editor @ project=prod
← Bob can edit pipelines here only
(not staging — different project)
The reverse does not hold. A binding at a narrow scope does NOT escalate to its parents. An admin binding at project=prod does not let you create new workspaces in the org.
This matches the GCP IAM mental model. It’s intentional: prevents accidental escalation, makes “what can Alice do?” computable from her binding set without traversing a graph.
Binding roles via the CLI
Most bindings use one of the four built-in roles:
# Make alice@acme.com an admin at the org level
ns role bind admin \
--user=alice@acme.com \
--scope=org \
--scope-id=00000000-0000-0000-0000-000000000001
# Make bob@acme.com an editor in the analytics workspace
ns role bind editor \
--user=bob@acme.com \
--scope=workspace \
--scope-id=<workspace-uuid>
# Make oncall-svc (a service account) operator on the prod project
ns role bind operator \
--service-account=oncall-svc \
--scope=project \
--scope-id=<project-uuid> # List all bindings for a user
ns role bindings --user=alice@acme.com
# Show one binding
ns role binding show <binding-id>
# Remove a binding
ns role unbind <binding-id> Custom role recipes
When the four built-ins don’t fit, create a custom role with exactly the verbs you need.
Data engineer for one project
A senior engineer who owns pipelines in a single project end-to-end:
ns role create \
--name=data-engineer \
--scope=project \
--description='Owns pipelines + connections in this project' \
--permission=read:pipeline \
--permission=write:pipeline \
--permission=delete:pipeline \
--permission=run:pipeline \
--permission=read:connection \
--permission=write:connection \
--permission=delete:connection \
--permission=read:run \
--permission=delete:run \
--permission=read:log \
--permission=read:audit
# Then bind to a specific user + project
ns role bind data-engineer \
--user=charlie@acme.com \
--scope=project \
--scope-id=<project-uuid>
Audit reader
For SOC 2 / SOX reviewers who only need read-only audit access workspace-wide:
ns role create \
--name=audit-reader \
--scope=workspace \
--description='Read-only access to audit logs and pipeline state' \
--permission=read:audit \
--permission=read:log \
--permission=read:run \
--permission=read:pipeline
Connector author
For someone managing the plugin marketplace:
ns role create \
--name=connector-author \
--scope=org \
--description='Publish + manage marketplace plugins' \
--permission=read:plugin \
--permission=write:plugin \
--permission=admin:plugin
Token manager
For an automated system that mints API tokens on behalf of users:
ns role create \
--name=token-manager \
--scope=org \
--description='Mint and revoke API tokens for other users' \
--permission=read:token \
--permission=write:token \
--permission=delete:token \
--permission=read:user
How Enforce resolves a request
For request POST /v1/pipelines from Alice in project prod:
- The middleware resolves Alice’s
Principalfrom her session cookie. - The handler resolves the target scope from
X-Project-ID(or defaults to the seeded project). rbac.Enforce(ctx, alice.UserID, VerbWrite, ResourceRef{Pipeline, scope})runs.- The enforcer walks Alice’s bindings, keeps the ones whose
scope_idis an ancestor (or equal to) the request scope. - For each kept binding, it loads the role’s permissions.
- Allow if any role has
(write, pipeline)OR(admin, pipeline)OR(admin, org). - Deny otherwise → 403 Forbidden, audit event recorded.
The enforcer’s permission cache is per-subject, TTL 30s, bust-on-write. Single-node behavior is correct; multi-node cache invalidation lands in v2.
Reading effective permissions
GET /v1/auth/me returns the principal’s resolved permission set for the active scope:
curl -fsSL https://nanosync.example.com/v1/auth/me \
-H "Authorization: Bearer $TOKEN"
{
"user_id": "…",
"email": "alice@acme.com",
"active_workspace": "analytics",
"permissions": [
{"verb": "read", "resource": "pipeline"},
{"verb": "write", "resource": "pipeline"},
{"verb": "delete", "resource": "pipeline"},
{"verb": "run", "resource": "pipeline"},
{"verb": "read", "resource": "connection"}
]
}
The web UI uses this to gate menu items via the <PermissionGate> component — buttons grey out instead of returning 403 on click.
What’s next
- Bootstrap the first admin — set up the initial account before binding more users.
- SSO setup — back the roles with IdP-managed users instead of local accounts.
- Audit logging — see every binding change in your SIEM.