You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tellstone's RBAC policy file (internal/rbac/config.go:33-45) can only express
statically enumerated users (users: with bcrypt passwords) and role definitions.
With OIDC connection auth (#10), identities arrive as validated JWT claims, not
password entries — and there is no way to map those claims to roles without creating
a per-identity user record, which defeats SSO entirely.
A dangerous default lurks in a naive fallback: granting reader (or any role) to an
unmatched token is wrong for a database holding sensitive data. The safe default is fail closed — no matching rule means no role, and no role means no access.
Proposed solution
Add an oauth section to the RBAC policy schema (internal/rbac/config.go) that maps
token claims to roles as a pure function — no identity/user bookkeeping involved.
roles:
- name: adminrules: ["+@all"]
- name: readerrules: ["+@read", "~*"]oauth:
rules: # ordered, first match wins
- claims: # all predicates must match (AND)iss: "https://iam.acess.example"email: "*@saxy.dev"role: admin
- claims:
iss: "https://iam.acess.example"groups: ["dev-*", "qa-*"]role: readerdefault_role: ""# optional; empty = fail closed
Semantics:
First match wins, evaluated top-down; unmatched claims resolve to default_role,
and an empty/unset default_role means no role → connection denied. No implicit
read-only fallback: even reader is a wrong default for sensitive data.
Glob matching on claim values: exact, *-prefix (suffix match), *-suffix
(prefix match); no regex. A missing claim never matches.
Rule validation at load, mirroring existing strictness (config.go:93-96):
unknown target role, duplicate rule, or empty rule list is a load error — a bad file
can never half-apply.
Hot reload for free: the oauth block rides the existing LoadFile/Parse
path (config.go:49-65), so SIGHUP re-evaluates rules without restart — matching the
RBAC hot-swap principle (Zero-Allocation RBAC with Hot-Swap Policy Store #16).
Changes:
Extend fileConfig in internal/rbac/config.go with the oauth block and its
strict validation.
Add ResolveRole(claims map[string][]string) (*Role, bool) in internal/rbac — pure,
allocation-light (connection time, not the per-command hot path), no identity lookup.
Wire it into the OIDC connection-auth story (OIDC/OAuth2 Integration for SSO #10): after signature/iss/exp
verification, feed the token claims to ResolveRole.
Acceptance criteria:
Rule ordering respected; first match wins.
No default_role: an authentic-but-unmatched token is denied, never downgraded.
Unknown target role, duplicate rule, empty rules, and a rule with an undefined
glob → file rejected at load.
SIGHUP with a changed mapping takes effect without restart.
task check passes.
Alternatives considered
Fallback to default_role: reader — rejected: silent over-granting on sensitive data.
users: entries with OAuth subjects — rejected: defeats SSO, no group mapping.
Flat single-claim map ("@saxy.io": admin) — rejected: matches on one claim alone and
can't pin iss/groups, risking privilege escalation on a typo'd claim name.
Area
Security / access control
Problem or motivation
Tellstone's RBAC policy file (
internal/rbac/config.go:33-45) can only expressstatically enumerated users (
users:with bcrypt passwords) and role definitions.With OIDC connection auth (#10), identities arrive as validated JWT claims, not
password entries — and there is no way to map those claims to roles without creating
a per-identity user record, which defeats SSO entirely.
A dangerous default lurks in a naive fallback: granting
reader(or any role) to anunmatched token is wrong for a database holding sensitive data. The safe default is
fail closed — no matching rule means no role, and no role means no access.
Proposed solution
Add an
oauthsection to the RBAC policy schema (internal/rbac/config.go) that mapstoken claims to roles as a pure function — no identity/user bookkeeping involved.
Semantics:
default_role,and an empty/unset
default_rolemeans no role → connection denied. No implicitread-only fallback: even
readeris a wrong default for sensitive data.*-prefix (suffix match),*-suffix(prefix match); no regex. A missing claim never matches.
config.go:93-96):unknown target role, duplicate rule, or empty rule list is a load error — a bad file
can never half-apply.
oauthblock rides the existingLoadFile/Parsepath (
config.go:49-65), so SIGHUP re-evaluates rules without restart — matching theRBAC hot-swap principle (Zero-Allocation RBAC with Hot-Swap Policy Store #16).
Changes:
fileConfigininternal/rbac/config.gowith theoauthblock and itsstrict validation.
ResolveRole(claims map[string][]string) (*Role, bool)ininternal/rbac— pure,allocation-light (connection time, not the per-command hot path), no identity lookup.
iss/expverification, feed the token claims to
ResolveRole.Acceptance criteria:
default_role: an authentic-but-unmatched token is denied, never downgraded.glob → file rejected at load.
task checkpasses.Alternatives considered
default_role: reader— rejected: silent over-granting on sensitive data.users:entries with OAuth subjects — rejected: defeats SSO, no group mapping."@saxy.io": admin) — rejected: matches on one claim alone andcan't pin
iss/groups, risking privilege escalation on a typo'd claim name.Additional context
internal/rbac/config.gointernal/rbac/policy.go,internal/rbac/rbac.gointernal/oauth/oauth.go(empty stub), issue OIDC/OAuth2 Integration for SSO #10