authz

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

authz

CI Go Reference Go Report Card License

Experimental — internal use. This library is developed for infodancer and matthewjhunter projects. The API is unstable and may change without notice.

A small authorization layer for Go web applications. authz provides a role store keyed on (issuer, subject, module, role) plus a resolver that maps an authenticated identity into a list of granted roles. It is deliberately decoupled from authentication: it never reads roles from JWT claims, federated or otherwise.

The trust model

+----------+      +---------------+      +------------------+
| OIDC IdP | ---> | host auth-N   | ---> | authz.Resolver   |
| (any)    |      | (cookie/JWT)  |      | (this library)   |
+----------+      +---------------+      +------------------+
                          |                       |
                          v                       v
                 identity (iss, sub,        roles, looked up
                  email, name)              in local store
  • Identity is whatever the host's auth-N layer produces. The host validates the request and constructs an authz.Identity (issuer, subject, email, name) — from OIDC, mTLS, API keys, any scheme. authz never sees the request; it has no HTTP dependency.
  • Roles are looked up in authz's own authz_user_roles table. They are never read from claims. Even for IdPs you control. Federated IdPs (Google, GitHub, ...) are treated identically to your own.
  • Grants happen out-of-band via authzctl or an admin UI in the consuming application. There is no JIT seeding from claims and no trust-policy configuration.

This costs you centralized role management from the IdP. It buys immediate revocation, a crisp audit trail, and a federation story that works trivially.

Schema

One table, two backends (PostgreSQL and SQLite). The shape is the same:

CREATE TABLE authz_user_roles (
    issuer      TEXT NOT NULL,
    subject     TEXT NOT NULL,
    module      TEXT NOT NULL DEFAULT '',  -- '' = global, otherwise module name
    role        TEXT NOT NULL,
    granted_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    granted_by  TEXT NOT NULL,
    PRIMARY KEY (issuer, subject, module, role)
);
CREATE INDEX authz_user_roles_lookup_idx ON authz_user_roles (issuer, subject);

Migrations ship in migrations/{postgres,sqlite}/ and run via the module's own goose tree (table authz_goose_db_version) so they don't collide with the host application's migration history. They are applied through the separate authz/migrate package — import it only if you want authz to manage its own schema; the root package stays free of goose.

Use

import (
    "github.com/infodancer/authz"
    "github.com/infodancer/authz/migrate"
)

// db is a *sql.DB opened with any PostgreSQL driver. A pgxpool host
// can wrap its pool with stdlib.OpenDBFromPool(pool).
if err := migrate.Postgres(ctx, db); err != nil {
    return err
}
store := authz.NewPostgresStore(db)

resolver := &authz.Resolver{
    Store:  store,
    Module: "faq",          // "" if this resolver checks global roles
}

// The host turns the request into an identity (anonymous requests are
// handled before this point), then asks authz for that identity's roles.
id := authz.Identity{Issuer: iss, Subject: sub, DisplayName: name, Email: email}
principal, err := resolver.Resolve(ctx, id)

Consumers that run their own migration tooling can skip authz/migrate entirely and apply the SQL under migrations/ themselves; importing only github.com/infodancer/authz keeps the dependency footprint to the standard library plus a database driver.

principal.Roles carries both global roles (module = '') and roles granted with module = "faq", deduped. principal.HasRole("admin") is the typical check.

Bootstrap

authzctl --driver postgres --dsn $DATABASE_URL grant \
    https://auth.example.com sub-uuid admin --module faq --by matthew

The CLI is also the canonical way to grant the first global admin before any UI exists.

See also

License

Apache-2.0. See LICENSE.

Documentation

Overview

Package authz is a small authorization layer for Go web applications.

It provides a role store keyed on (issuer, subject, module, role) and a resolver that maps an authenticated identity into the roles granted to it. authz is deliberately decoupled from authentication: roles are never read from JWT claims, and the package has no knowledge of HTTP. The host validates each request and constructs an Identity; authz answers what that identity may do.

See DESIGN.md at the repository root for the full rationale.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Grant

type Grant struct {
	Issuer    string
	Subject   string
	Module    string
	Role      string
	GrantedBy string
}

Grant is a single role assignment to be written by Store.Grant.

Module is the URL-safe name of the consuming module ("faq", "blog") or the empty string for a global grant that applies across every module. GrantedBy is free-text attribution, conventionally "cli:<operator>" or "host:<app>:<endpoint>".

type Holding

type Holding struct {
	Issuer    string
	Subject   string
	Module    string
	Role      string
	GrantedAt time.Time
	GrantedBy string
}

Holding is one row of the role table as returned by Store.Holders, used for audit and the authzctl holders command.

type Identity

type Identity struct {
	Issuer        string
	Subject       string
	DisplayName   string
	Email         string
	EmailVerified bool
}

Identity is an authenticated subject, as resolved by the host from whatever auth-N mechanism it runs (an OIDC cookie/JWT, mutual TLS, an API key, ...). It carries identity only — never roles. (issuer, subject) is the stable key into the role store; DisplayName and Email are presentation data the host may surface.

authz does not extract Identity from a request — it has no knowledge of HTTP. The host validates the request and constructs the Identity, then asks authz what that identity may do.

EmailVerified is an identity-assurance fact, not a role: it reports whether the auth-N layer (the IdP's email_verified claim, or a verification flow the host ran itself) has confirmed control of Email. authz never sets or stores it — the host supplies it and authz carries it through so callers can gate on it alongside roles.

type Principal

type Principal struct {
	Issuer        string
	Subject       string
	DisplayName   string
	Email         string
	EmailVerified bool
	Roles         []string
}

Principal is an authenticated identity together with the roles granted to it, scoped to the resolver's module.

func (*Principal) HasRole

func (p *Principal) HasRole(role string) bool

HasRole reports whether the principal holds role.

type Resolver

type Resolver struct {
	Store  Store
	Module string
}

Resolver loads the roles for an Identity from a Store, scoped to a module. A resolver with Module "faq" sees grants made with module "faq" plus all global grants (module ""). An empty Module yields a global-only checker.

The host is responsible for authentication: turn a request into an Identity (anonymous requests never reach Resolve), then call Resolve to load that identity's roles. Keeping the request out of authz is what lets the package stay free of any HTTP or auth-N dependency.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, id Identity) (*Principal, error)

Resolve loads id's roles, scoped to the resolver's module, and returns a Principal. An authenticated identity with no grants yields a Principal with an empty Roles slice, not an error.

type Store

type Store interface {
	// Roles returns every role granted to (issuer, subject) whose module
	// is either "" (global) or equal to the requested module. The result
	// is deduplicated and sorted. An authenticated user with no grants
	// yields an empty slice, not an error.
	Roles(ctx context.Context, issuer, subject, module string) ([]string, error)

	// Grant inserts a role assignment. Idempotent: re-granting an existing
	// (issuer, subject, module, role) tuple is a no-op and preserves the
	// original granted_at.
	Grant(ctx context.Context, g Grant) error

	// Revoke hard-deletes a specific (issuer, subject, module, role) tuple.
	// Revoking a tuple that does not exist is a no-op, not an error.
	Revoke(ctx context.Context, issuer, subject, module, role string) error

	// Holders lists every grant of role across all modules, ordered by
	// (issuer, subject, module). At most holdersLimit rows are returned;
	// capped reports whether more rows existed than were returned.
	Holders(ctx context.Context, role string) (holdings []Holding, capped bool, err error)
}

Store is the persistence contract for role assignments. Implementations for PostgreSQL and SQLite ship with the module; both satisfy the same behavioral test suite.

func NewPostgresStore

func NewPostgresStore(db *sql.DB) Store

NewPostgresStore returns a Store backed by a PostgreSQL database. The caller opens db with a PostgreSQL driver (typically the pgx stdlib driver) and owns its lifecycle. A pgxpool-based host can adapt its pool with stdlib.OpenDBFromPool.

func NewSQLiteStore

func NewSQLiteStore(db *sql.DB) Store

NewSQLiteStore returns a Store backed by a SQLite database. The caller opens db with a SQLite driver (typically modernc.org/sqlite) and owns its lifecycle.

Directories

Path Synopsis
cmd
authzctl command
Command authzctl manages the authz role store from the shell.
Command authzctl manages the authz role store from the shell.
Package migrate applies authz's embedded schema migrations.
Package migrate applies authz's embedded schema migrations.
migrations
postgres
Package postgres embeds the PostgreSQL migration files for authz.
Package postgres embeds the PostgreSQL migration files for authz.
sqlite
Package sqlite embeds the SQLite migration files for authz.
Package sqlite embeds the SQLite migration files for authz.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL