Skip to content

Oauth Claim Mapping for RBAC System #34

Description

@Saxy

Area

Security / access control

Problem or motivation

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: admin
    rules: ["+@all"]
  - name: reader
    rules: ["+@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: reader
  default_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.

Additional context

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions