api

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2026 License: MIT Imports: 14 Imported by: 0

README

api

Русская версия

Embeddable M2M access management for Go: API token issuance, rotation, revocation, integration bans, and permission checks. Each project has its own access source, PostgreSQL schema, and cache instances.

Go >= 1.27.3 (security patch floor), toolchain 1.27.3. Tested PostgreSQL version: 18.6. Module: github.com/assurrussa/api. User sessions, OAuth, an administrative HTTP API, and a goadmin interface are outside the scope of the first version.

Installation and license

Specify an exact published version:

go get github.com/assurrussa/api@v0.1.0

License: MIT. Supported import paths: the root package github.com/assurrussa/api, postgres, httpapi, cache/arc, and cache/rcu under the same prefix. Versions v0.x allow refinements to the public API before the compatibility commitments of v1; contract changes are described in release notes.

Access model

  • Client: a service or integration, its state, and its allowed scopes.
  • Credential: a named client credential with a permanent ID and its own scopes. One client can have several independent credentials.
  • SecretVersion: a secret version with an explicit lifetime. Rotation preserves the Credential, replaces the secret, and increments the generation.
  • Principal: verified identifiers and the intersection of the client's and Credential's scopes. The host project checks ownership of orders, files, and other objects.

Scopes are exact strings, with no wildcards or role inheritance. The catalog is defined through Config.Scopes; an empty set allows authentication, but not operations that require scopes. Middleware requires all listed scopes. The catalog restricts new administrative assignments. Changing the configured catalog does not revoke permissions already stored: to revoke them, change the client's scopes through SetClientScopes or revoke the Credential. During a rollout with mixed configuration versions, this rule applies equally to all replicas.

Tokens contain 256 random bits and are opaque to clients. The database and caches store SHA-256 verification values of random secrets. Secret comparison is constant-time. The full token is returned only through Issued at issuance and rotation; it cannot be recovered or displayed again. Do not log Issued.Token, Authorization, or issuance response bodies.

DefaultTTL and MaxTTL are mandatory and set by the project. A zero operation TTL uses DefaultTTL; values above MaxTTL are rejected. Explicit permanent issuance requires WithNonExpiring() on the Service and NonExpiring: true, TTL: 0 in IssueRequest or RotateRequest. A nonzero TTL with that flag is invalid. Finite defaults and limits remain mandatory. Permanent results expose NonExpiring: true and a zero time.Time in ExpiresAt; JSON adds non_expiring while preserving the existing time fields. Existing permanent credentials authenticate even without the issuance option. This v0.x addition changes positional struct literals for the extended request and result types; consumers must use keyed literals. The PostgreSQL migration 003_non_expiring.sql makes expires_at nullable: SQL NULL means permanent. Roll out in this order: expand the schema with the migration, upgrade all readers, writers, cache builders, and administrative processes to the new API version, then enable permanent issuance. Old binaries cannot read NULL expiries. Rolling back to an old binary is prohibited while any NULL-expiry rows remain; an operator must explicitly convert those versions to finite lifetimes or reissue credentials before rollback. There is no automatic down migration or automatic conversion of permanent credentials. Adapters without NonExpiringStore.SupportsNonExpiring() bool are rejected with ErrUnsupported when opting in. Optional TimedStore signatures are unchanged: permanent writes pass zero TTL together with SecretVersion.NonExpiring. The PostgreSQL Store sets the issuance time and grace start after acquiring the required locks; Issued.ExpiresAt contains the expiration time actually stored. External Store implementations can implement the optional TimedStore for the same semantics. A slow write or commit still consumes part of TTL/grace before the response; choose durations with enough margin for the operation budget. An expired Credential can be renewed through administrative rotation if it has not been revoked and the client is not banned.

Rotation accepts ExpectedGeneration to protect against concurrent replacements and GracePeriod for the transition. Zero invalidates the previously current secret. A positive period is capped by that secret's original expiration time; a permanent previous version becomes finite at the grace deadline. Rotations support every combination of finite and permanent versions. Repeated rotations do not extend previously assigned transition deadlines; several unexpired transition versions may remain valid at the same time.

A ban is reversible: after unbanning, valid, unrevoked credentials work again. Credential revocation is irreversible and covers all of its versions. RevokeAll revokes all existing client credentials; subsequent issuance of new credentials is allowed. In a compromise, combine a ban with revocation if you also need to prevent new issuance. Requests already admitted are not canceled retroactively.

Integration

The project owns the database pool, configuration, migration execution, TLS, rate limiting, and administrative authorization. All Service management methods are a trusted Go API; there is no public issuance route using an ordinary bearer token.

store, err := postgres.New(pool, "orders_access")
if err != nil { return err }

// Call in an explicit migration step, not on every web replica startup.
if err := store.Migrate(ctx); err != nil { return err }

service, err := api.New(store, api.Config{
    Scopes:     []string{"orders:read", "orders:write"},
    DefaultTTL: 24 * time.Hour,
    MaxTTL:     30 * 24 * time.Hour,
})
if err != nil { return err }
defer service.Close()

// Start is optional in strict mode; for caches, it starts their lifecycle.
if err := service.Start(ctx); err != nil { return err }

mux.Handle("GET /orders",
    httpapi.Middleware(service, "orders:read")(ordersHandler))

Supported imports: the root api package, postgres, httpapi, cache/arc, and cache/rcu. Public Store/Source/Cache interfaces support custom adapters; consistency contracts are documented in the interfaces' Go doc. Caches return data to a shared validator and do not grant permissions themselves.

New does not access the network or start goroutines. Start is called once; canceling its context stops service operation. Close is idempotent, cancels and waits for workers and operations, including an unfinished Start. The Service cannot be restarted after Close. Finish incoming HTTP requests first, then close the Service and database pool. Errors() also exposes rate-limited reports of LISTEN failures and background RCU refresh failures; the channel is not a counter of every attempt.

Each project uses a separate schema. The schema name is passed explicitly to postgres.New; names with the pg_ prefix are forbidden. Migrations are embedded in the adapter, versioned, and applied transactionally under an advisory lock. All verifier reads must go to the primary; a pool with read replicas whose lag is unbounded does not satisfy the contract. State writes go through Store/Service.

Administrative methods: CreateClient, Client, Clients, SetClientScopes, SetClientBanned, Issue, Rotate, Credential, Credentials, Revoke, RevokeAll. Lists use ID-based keyset pagination with after and a limit of 1–1000. For goadmin, wrap these methods in its administrative authorization; business tokens do not replace operator authorization.

PostgreSQL retains secret version history. For the retention period chosen by the project, call Store.PruneExpiredVersions(ctx, cutoff, batchSize) from an administrative job in repeated small batches. The method deletes only expired historical versions; it preserves the current Credential version even after expiration so that administrative rotation remains possible. Define the audit retention period before enabling cleanup.

Management, receipts, audit, and usage

Optional capabilities extend Store without changing the existing Store, Client, Credential, Principal, or request types. Custom adapters implement only the capabilities they support; unsupported features return ErrUnsupported. CredentialDetails reads current state from the primary, including effective scopes, ban status, safe current-version metadata, unexpired-version count, and approximate last use. SecretVersions(ctx, id, beforeGeneration, limit) returns newest generations first (zero starts the list, limit 1–1000). RetireVersion expires a historical version immediately; retiring the current version returns ErrConflict. AuthenticateDetails adds verified version ID, generation, and expiry without exposing a secret or digest.

IssueOnce and RotateOnce accept a stable operation ID scoped to the schema. The receipt and mutation commit atomically. Repeating the same normalized request returns the receipt without a token; a different request or audited actor with the same ID returns ErrConflict. Receipts do not expire automatically and are not deleted by secret-version pruning. They describe the committed operation, not current credential state. After a lost response, query Operation or retry with the same ID and request; ErrNotFound is not proof of rollback. A lost plaintext token cannot be recovered from a receipt.

WithAudit() requires transactional audit support. Every administrative write requires WithActor(ctx, Actor{Kind: "human" or "service", ID: ...}); the host must verify that identity before attaching it. Never derive the actor from an untrusted header or business token. Successful changes and their safe audit metadata commit in the same transaction; failures and receipt replays add no success event. AuditEvents(ctx, beforeID, limit) lists newest first, with zero starting the list; PruneAuditEvents applies the host's explicit retention policy. Run the explicit PostgreSQL migration step before enabling these features, and configure every administrative writer to audit: an older or unaudited writer cannot provide complete audit coverage.

WithUsageTracking(UsageConfig{}) enables bounded best-effort LastUsedAt collection for authenticated versions, including 403 scope failures, excluding 401 failures. Defaults are an approximately 30-second flush interval, 10,000 pending versions, batches of 1,000, and five-second flush/close timeouts. Times merge monotonically; usage writes do not send authorization NOTIFY events. Drops, crashes, other replicas, and persistence failures can leave observations missing, so last use cannot prove inactivity. Usage failures do not deny access; monitor Errors() and UsageStats(), drain HTTP first, then check the error from Close() for the final flush.

Strict verification, ARC, and RCU

By default, every request reads consistent state from PostgreSQL. A new verification after confirmed revocation observes it. A storage error does not grant access.

Enable caching explicitly:

// Up to 10,000 frequently used entries, with at most 10 seconds of staleness.
api.WithCache(arc.Factory(10_000), 10*time.Second)

// Full projection: refresh every 30 seconds, with at most 2 minutes of staleness.
api.WithCache(rcu.Factory(30*time.Second), 2*time.Minute)

// Same interval, with a separate 60-second budget for initial and background loads.
api.WithCache(rcu.FactoryWithTimeout(30*time.Second, 60*time.Second), 2*time.Minute)

These are options for api.New. One adapter is selected at a time; if several WithCache options are supplied, the last one applies. Uses gocache v0.2.1.

  • ARC limits capacity and loads entries on a miss. TTL jitter is disabled.
  • RCU loads a complete immutable snapshot; the refresh interval must be positive and below MaxStaleness. For compatibility, Factory also uses that interval as the load timeout; FactoryWithTimeout sets a separate positive timeout below MaxStaleness. After a failure, the scheduler delays event-driven and periodic retries using backoff with jitter, without losing one pending invalidation. An initial load failure aborts Start.
  • MaxStaleness is set by the project and can be seconds or minutes. Age is measured from the start of the read, not from publication to the cache.
  • Cache hits, TTL refreshes, and notifications do not extend trust in the data. Stale state requires a primary read; a failure returns an infrastructure error. Secret expiration is checked independently of cache expiration.
  • Local invalidation increments the generation. Old loads and snapshots cannot grant access after a change has already been processed.

Changes send PostgreSQL NOTIFY transactionally. Each cached Service opens one additional dedicated connection for LISTEN, outside the MaxConns limit of the supplied pool. Its BeforeConnect and AfterConnect also apply to LISTEN. The projection is invalidated when the connection is restored. Notifications are best-effort: a missed event is bounded by MaxStaleness, rather than a promise of immediate revocation. Strict mode does not open a listener.

In the first version, any change invalidates the entire local projection. ARC then loads entries as needed, while RCU rebuilds its snapshot. An unknown token in a fresh RCU snapshot does not trigger a full reload; negative responses are not cached. Frequent changes can eliminate the benefit of caching. The application must limit incoming invalid tokens; the package is not a rate limiter.

HTTP accepts exactly one Authorization: Bearer … header. Tokens from URLs, cookies, or request bodies are not supported. Responses: 401 for invalid access, 403 for insufficient scopes, and 503 when reliable verification is impossible. Responses do not expose internal database errors or ban details. The scheme is case insensitive and accepts one or more ASCII spaces before the token, within a total header limit of 263 bytes; tabs, duplicate headers, and token whitespace are rejected. Missing credentials receive Bearer realm="api"; invalid supplied credentials add error="invalid_token", and 403 adds error="insufficient_scope". MiddlewareWithOptions(service, httpapi.Options{ErrorHandler: httpapi.JSONErrorHandler}, scopes...) selects safe JSON error bodies. Custom handlers receive only httpapi.Error{Status, Code}; the middleware sets the mandatory challenge before invoking the handler. Middleware keeps text bodies.

Working example

Use a separate local database; sslmode=disable below is intended only for the local example. External APIs must be served over HTTPS.

export API_DATABASE_URL='postgres://USER:PASSWORD@127.0.0.1:5432/DATABASE?sslmode=disable'
export API_SCHEMA='api_demo'
go run ./examples/access migrate
go run ./examples/access create-client -name warehouse -scopes orders:read
go run ./examples/access issue -client CLIENT_ID -name reader -scopes orders:read -ttl 24h
go run ./examples/access serve -cache arc -max-staleness 10s

For RCU, the example accepts -refresh and -snapshot-timeout. A zero -snapshot-timeout preserves the previous mode, where the load budget equals -refresh; with a separate timeout, both values must be below -max-staleness.

Save the issued token in the integration's secret store. The example returns it in JSON only once. GET /orders checks orders:read and returns the Principal. There are no administrative HTTP endpoints; the default bind address is 127.0.0.1:8080.

go run ./examples/access rotate -credential CREDENTIAL_ID -generation 1 -grace 10m
go run ./examples/access ban -client CLIENT_ID
go run ./examples/access unban -client CLIENT_ID
go run ./examples/access revoke -credential CREDENTIAL_ID
go run ./examples/access revoke-all -client CLIENT_ID
go run ./examples/access scopes -client CLIENT_ID -scopes orders:read
go run ./examples/access clients -limit 100
go run ./examples/access credentials -client CLIENT_ID -limit 100
go run ./examples/access detail -credential CREDENTIAL_ID
go run ./examples/access versions -credential CREDENTIAL_ID -before-generation 0 -limit 100
go run ./examples/access retire-version -credential CREDENTIAL_ID -version HISTORICAL_VERSION_ID
go run ./examples/access issue -client CLIENT_ID -name reader -scopes orders:read -operation-id ISSUE_ID -audit -actor operator -actor-kind human
go run ./examples/access operation -operation-id ISSUE_ID
go run ./examples/access audit-events -before-event 0 -limit 100
go run ./examples/access serve -usage

-operation-id also selects RotateOnce for rotate; without it, issue/rotate retain their original output. -audit requires -actor and accepts -actor-kind human|service (default service). These are trusted CLI inputs, not authentication. -usage is accepted only by serve; all features are opt-in.

The example's TTL policy is 24 hours by default, with a maximum of 30 days. This is example configuration, not hidden library defaults. If the issuance response is lost after commit, the secret cannot be recovered: rotate the corresponding credential, or revoke it and issue a new one. Lists return metadata, without secrets or hashes.

Checks

Requires the golangci-lint version from .golangci-lint-version (2.14.0). The gate and make lint / make lint-fix check for an exact version match: they first use bin/golangci-lint, then the binary from PATH. To install the pinned official binary from the repository root:

api_lint_version=$(cat .golangci-lint-version)
api_lint_installer=$(mktemp)
curl -fsSL "https://raw.githubusercontent.com/golangci/golangci-lint/v${api_lint_version}/install.sh" -o "$api_lint_installer"
sh "$api_lint_installer" -b ./bin "v${api_lint_version}"
rm "$api_lint_installer"
bash scripts/lint.sh version

The installer verifies the release archive checksum. Regular checks use the installed binary without downloading it again.

make check

The gate checks release-script regressions, the pinned golangci-lint, formatting, vet, all race tests with PostgreSQL, the external consumer probe, and process E2E. If API_TEST_DATABASE_URL is not set, the script starts an isolated postgres:18.6-alpine container with a temporary port and removes it after the check. With an explicit URL, use a dedicated test database: tests create and remove only their own unique schemas.

make unit without a URL skips PostgreSQL tests and does not replace make check. The GitHub workflow installs the same golangci-lint version, runs the full gate and pinned govulncheck for PRs targeting main and pushes to main; separately, it runs a one-minute token parser fuzz test once a week. Required merge status is configured in branch settings. The consumer probe uses a temporary module with a local replace; it proves the public surface of the current checkout. To check a published version:

make consumer-release API_VERSION=v0.1.0

Requires Git and curl. For tagged versions, the command first checks the tag in the public Git repository. An unpublished tag fails immediately, before querying Go services. Canonical pseudo-versions remain supported for pre-tag checks.

The command automatically waits for the version's metadata, manifest, archive, and checksum record to become available in the public Go services. It uses lightweight HEAD requests, reports progress, and honors cache lifetimes and Retry-After. The wait is bounded to 35 minutes by default; set API_RELEASE_WAIT_TIMEOUT to a limit in seconds (1..86400) to change it. An early request made before a tag exists can leave a cached 404 for up to 30 minutes. Only after availability is confirmed does the clean consumer probe start. Checksum, compatibility, and test failures stop immediately rather than being retried.

This command uses Go 1.27.3, a separate temporary module, and a fresh dependency cache, and disables the workspace, local Go settings, and private module overrides. Downloads use proxy.golang.org with verification through sum.golang.org. The probe checks the exact version, absence of active replacements in the consumer graph and replace directives in api's downloaded go.mod, checksums, supported imports, Service creation in strict, ARC, and RCU modes, tests, and builds. go list -m all does not show directives inside dependencies: Go ignores them. The published manifest is therefore read separately through go mod edit -json without changing the file. Access to public Go services is required; latest, branch names, and an empty version are rejected. The temporary module and its cache are removed on exit.

Releasing a version
  1. Prepare changes on a branch and run make check. Create a PR targeting main, complete the required review, and wait for green CI after merge.
  2. Before publishing the tag, check the release probe against the exact canonical pseudo-version of the public commit, obtained through go list -m -f '{{.Version}}' github.com/assurrussa/api@<commit-sha>.
  3. Create an annotated tag for the chosen version on the verified merge commit, publish it, and verify that it points to the correct commit.
  4. Run make consumer-release API_VERSION=<version>. The command automatically waits for public Go services and then checks the version with a fresh cache. A timeout or a checksum, compatibility, or test failure stops the release.
  5. After successful verification, publish a GitHub Release with the commit SHA, requirements, and checks actually performed.

Published Go versions are immutable: tags must not be moved or recreated. Fixes to a published release receive a new version. The release probe confirms external consumption of the module; a production pilot in a real service is verified separately.

Process E2E can be run separately:

make e2e

Requires Go, Bash, and a working Docker installation. The script creates its own PostgreSQL 18.6 instance with a temporary loopback port, builds the CLI with the race detector, and starts two HTTP replicas for each mode: strict, ARC, and RCU. API_DATABASE_URL, API_SCHEMA, API_TEST_DATABASE_URL, and the local demo file do not determine the database used by this setup: E2E always uses its own container. Tokens remain in test memory; the report contains scenario names and results.

Checks cover issuance and administrative rejections, HTTP 200/401/403/503, scope intersection, bans/unbans, rotation with zero and positive grace, TTL and its independence from the cache, concurrent rotation, irreversible revocation, revoke-all followed by issuance, pagination, and schema isolation. Additional failures: authoritative state changes without NOTIFY, termination of only the Service's own LISTEN backend, PostgreSQL shutdown, initial RCU snapshot failure, and recovery of verification without restarting HTTP processes.

Waits are bounded by timeouts; HTTP processes are stopped during test cleanup. The shell trap removes the container and its anonymous volume and terminates remaining processes in the test run's process group, including after a go test failure or timeout. E2E data is disposable. The check uses local HTTP; TLS, an external reverse proxy, network partitions, and sustained load are outside this suite. The E2E matrix describes criteria and limits.

API_TEST_DATABASE_URL='postgres://…' make bench

Benchmarks use real PostgreSQL queries and 1/10/100 thousand credentials, a working set of 128 tokens, ARC capacity of 1024, and one sequential stream. They compare reads without changes and one mutation per 100 verifications. The report includes mean time, sampled verification p50/p95, allocations, database reads, and database snapshots. Mutation time is included in ns/op, but not in sampled verification p50/p95. init-allocated-B is the total allocation during initialization and warmup, not retained heap/RSS. BenchmarkPostgresSnapshot measures the full database load separately. This compares scenarios; it is not a production SLO or a sustainable load limit. Measured results and limitations: benchmark report.

The transitive dependency golang.org/x/text is pinned to v0.41.0 to fix GO-2026-5970, GO-2026-6629. Vulnerability checks must be repeated when dependencies change. Token expiration requires synchronized server clocks; the monotonic age of the local cache is unaffected by clock adjustments.

Documentation

Overview

Package api manages project-local machine-to-machine API credentials. Administrative methods are trusted host operations, never bearer capabilities.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalid      = errors.New("api: invalid input")
	ErrNotFound     = errors.New("api: not found")
	ErrConflict     = errors.New("api: conflict")
	ErrUnauthorized = errors.New("api: unauthorized")
	ErrForbidden    = errors.New("api: forbidden")
	ErrUnavailable  = errors.New("api: verification unavailable")
	ErrClosed       = errors.New("api: closed")
)
View Source
var ErrUnsupported = errors.New("api: unsupported capability")

ErrUnsupported means the configured adapter lacks an optional capability.

Functions

func WithActor added in v0.2.0

func WithActor(ctx context.Context, actor Actor) context.Context

Types

type Actor added in v0.2.0

type Actor struct {
	Kind string `json:"kind"`
	ID   string `json:"id"`
}

Actor is an identity verified by the trusted host, never by a business token.

func ActorFromContext added in v0.2.0

func ActorFromContext(ctx context.Context) (Actor, bool)

func (Actor) Valid added in v0.2.0

func (a Actor) Valid() bool

type AuditEvent added in v0.2.0

type AuditEvent struct {
	ID           int64     `json:"id"`
	Actor        Actor     `json:"actor"`
	Action       string    `json:"action"`
	ClientID     string    `json:"client_id,omitempty"`
	CredentialID string    `json:"credential_id,omitempty"`
	VersionID    string    `json:"version_id,omitempty"`
	OperationID  string    `json:"operation_id,omitempty"`
	OccurredAt   time.Time `json:"occurred_at"`
	// Details is a whitelist of safe result metadata, never secrets or digests.
	Details map[string]any `json:"details,omitempty"`
}

type AuditReader added in v0.2.0

type AuditReader interface {
	AuditEvents(ctx context.Context, before int64, limit int) ([]AuditEvent, error)
	PruneAuditEvents(ctx context.Context, before time.Time, limit int) (int64, error)
}

type AuditedOperationStore added in v0.2.0

type AuditedOperationStore interface {
	IssueOnceAudited(
		ctx context.Context, actor Actor, spec OperationSpec, credential Credential, version SecretVersion, ttl time.Duration,
	) (OperationReceipt, bool, error)
	RotateOnceAudited(
		ctx context.Context, actor Actor, spec OperationSpec, id string, generation int64,
		version SecretVersion, ttl, grace time.Duration,
	) (OperationReceipt, bool, error)
}

type AuditedRetirementStore added in v0.2.0

type AuditedRetirementStore interface {
	RetireVersionAudited(ctx context.Context, actor Actor, credentialID, versionID string) error
}

type AuditedStore added in v0.2.0

type AuditedStore interface {
	CreateClientAudited(ctx context.Context, actor Actor, client Client) error
	SetClientScopesAudited(ctx context.Context, actor Actor, id string, scopes []string) error
	SetClientBannedAudited(ctx context.Context, actor Actor, id string, banned bool) error
	IssueAudited(ctx context.Context, actor Actor, credential Credential, version SecretVersion) error
	RotateAudited(
		ctx context.Context, actor Actor, id string, generation int64, version SecretVersion, grace time.Duration, at time.Time,
	) (Credential, error)
	RevokeAudited(ctx context.Context, actor Actor, id string, at time.Time) error
	RevokeAllAudited(ctx context.Context, actor Actor, id string, at time.Time) error
}

AuditedStore implementations record every successful administrative command in the same transaction as its mutation, and reject invalid actors.

type AuditedTimedStore added in v0.2.0

type AuditedTimedStore interface {
	IssueWithLifetimeAudited(
		ctx context.Context, actor Actor, credential Credential, version SecretVersion, ttl time.Duration,
	) (Credential, SecretVersion, error)
	RotateWithLifetimeAudited(
		ctx context.Context, actor Actor, id string, generation int64, version SecretVersion, ttl, grace time.Duration,
	) (Credential, SecretVersion, error)
}

type AuthenticationDetails added in v0.2.0

type AuthenticationDetails struct {
	NonExpiring     bool      `json:"non_expiring"`
	Principal       Principal `json:"principal"`
	SecretVersionID string    `json:"secret_version_id"`
	Generation      int64     `json:"generation"`
	ExpiresAt       time.Time `json:"expires_at"`
}

type Cache

type Cache interface {
	Start(ctx context.Context) error
	Lookup(ctx context.Context, key string, change uint64) (ReadResult, error)
	Invalidate()
	Close() error
}

Cache implementations never decide authorization. Start owns workers until Close; constructors must be side-effect free. Returned values are immutable.

type CacheFactory

type CacheFactory func(Source) (Cache, error)

type Client

type Client struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Scopes    []string  `json:"scopes"`
	Banned    bool      `json:"banned"`
	CreatedAt time.Time `json:"created_at"`
}

type Config

type Config struct {
	Scopes     []string
	DefaultTTL time.Duration
	MaxTTL     time.Duration
}

type Credential

type Credential struct {
	ID         string     `json:"id"`
	ClientID   string     `json:"client_id"`
	Name       string     `json:"name"`
	Scopes     []string   `json:"scopes"`
	Generation int64      `json:"generation"`
	RevokedAt  *time.Time `json:"revoked_at,omitempty"`
	CreatedAt  time.Time  `json:"created_at"`
}

Credential is a stable access grant. Rotation changes its secret, not its ID.

type CredentialDetails added in v0.2.0

type CredentialDetails struct {
	Credential        Credential        `json:"credential"`
	ClientBanned      bool              `json:"client_banned"`
	EffectiveScopes   []string          `json:"effective_scopes"`
	CurrentVersion    SecretVersionInfo `json:"current_version"`
	UnexpiredVersions int               `json:"unexpired_versions"`
	LastUsedAt        *time.Time        `json:"last_used_at,omitempty"`
}

type IssueRequest

type IssueRequest struct {
	NonExpiring bool
	ClientID    string
	Name        string
	Scopes      []string
	TTL         time.Duration // Zero uses Config.DefaultTTL unless NonExpiring is true.
}

type Issued

type Issued struct {
	NonExpiring bool       `json:"non_expiring"`
	Credential  Credential `json:"credential"`
	Token       string     `json:"token"`
	ExpiresAt   time.Time  `json:"expires_at"`
}

Issued is the only administrative result containing the plaintext token. Do not log or persist Token. It cannot be recovered from the store.

type ManagementStore added in v0.2.0

type ManagementStore interface {
	CredentialDetails(ctx context.Context, id string) (CredentialDetails, error)
	SecretVersions(ctx context.Context, id string, before int64, limit int) ([]SecretVersionInfo, error)
	RetireVersion(ctx context.Context, credentialID, versionID string) error
}

type NonExpiringStore added in v0.2.0

type NonExpiringStore interface {
	SupportsNonExpiring() bool
}

NonExpiringStore advertises explicit support for zero-expiry permanent secrets. Stores must preserve the flag and enforce finite retirement during rotation.

type OperationReceipt added in v0.2.0

type OperationReceipt struct {
	Fingerprint [32]byte          `json:"-"`
	ID          string            `json:"id"`
	Kind        string            `json:"kind"`
	Credential  Credential        `json:"credential"`
	Version     SecretVersionInfo `json:"version"`
	CommittedAt time.Time         `json:"committed_at"`
}

OperationReceipt is immutable proof of a committed operation, not current state. It never contains a token or verification digest. Retain it without expiration.

type OperationResult added in v0.2.0

type OperationResult struct {
	Receipt OperationReceipt `json:"receipt"`
	Issued  *Issued          `json:"issued,omitempty"`
}

type OperationSpec added in v0.2.0

type OperationSpec struct {
	ID          string
	Fingerprint [32]byte
}

OperationSpec identifies a logical request. Fingerprint is a SHA-256 of its normalized requested parameters and actor (when auditing is enabled).

type OperationStore added in v0.2.0

type OperationStore interface {
	Operation(ctx context.Context, id string) (OperationReceipt, error)
	IssueOnce(
		ctx context.Context, spec OperationSpec, credential Credential, version SecretVersion, ttl time.Duration,
	) (OperationReceipt, bool, error)
	RotateOnce(
		ctx context.Context, spec OperationSpec, id string, generation int64, version SecretVersion, ttl, grace time.Duration,
	) (OperationReceipt, bool, error)
}

OperationStore commits receipt and mutation atomically. A matching replay is checked before current client/generation policy; it returns applied=false. ErrNotFound from Operation means not observed, not proof of rollback.

type Option

type Option func(*settings) error

func WithAudit added in v0.2.0

func WithAudit() Option

WithAudit requires transactional audit support and a host-verified Actor for writes.

func WithCache

func WithCache(factory CacheFactory, maxStaleness time.Duration) Option

WithCache opts into bounded-staleness verification. The duration is mandatory and may be seconds or minutes. Expiry of the actual secret is always enforced.

func WithNonExpiring added in v0.2.0

func WithNonExpiring() Option

WithNonExpiring permits explicit permanent issuance. Finite defaults remain unchanged. Existing permanent credentials authenticate without this option.

func WithUsageTracking added in v0.2.0

func WithUsageTracking(cfg UsageConfig) Option

WithUsageTracking enables bounded, best-effort LastUsedAt collection. Zero fields use defaults; failure to persist statistics never denies access.

type Principal

type Principal struct {
	ClientID     string   `json:"client_id"`
	CredentialID string   `json:"credential_id"`
	Scopes       []string `json:"scopes"`
}

type ReadResult

type ReadResult struct {
	Record     Record
	ReadAt     time.Time
	Generation uint64
}

ReadResult and Snapshot carry conservative monotonic freshness metadata.

type Record

type Record struct {
	Client     Client
	Credential Credential
	Secret     SecretVersion
}

Record must come from one consistent database statement/snapshot.

type RotateRequest

type RotateRequest struct {
	NonExpiring        bool
	CredentialID       string
	ExpectedGeneration int64
	TTL                time.Duration // Zero uses Config.DefaultTTL unless NonExpiring is true.
	GracePeriod        time.Duration // Zero immediately retires the current secret.
}

type SecretVersion

type SecretVersion struct {
	ID           string
	CredentialID string
	Generation   int64
	Digest       [32]byte
	ExpiresAt    time.Time
	NonExpiring  bool `json:"non_expiring"`
	CreatedAt    time.Time
}

SecretVersion contains verification material, never a plaintext secret. ExpiresAt includes any earlier retirement deadline introduced by rotation. NonExpiring requires a zero ExpiresAt; finite versions require a nonzero expiry.

type SecretVersionInfo added in v0.2.0

type SecretVersionInfo struct {
	NonExpiring  bool       `json:"non_expiring"`
	ID           string     `json:"id"`
	CredentialID string     `json:"credential_id"`
	Generation   int64      `json:"generation"`
	CreatedAt    time.Time  `json:"created_at"`
	ExpiresAt    time.Time  `json:"expires_at"`
	LastUsedAt   *time.Time `json:"last_used_at,omitempty"`
}

SecretVersionInfo contains safe administrative metadata only.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service owns verification and optional workers, but never the Store's pool. New is side-effect free. Strict verification needs no Start; caches do.

func New

func New(store Store, cfg Config, options ...Option) (*Service, error)

func (*Service) AuditEvents added in v0.2.0

func (s *Service) AuditEvents(ctx context.Context, before int64, limit int) ([]AuditEvent, error)

AuditEvents lists newest events first; before=0 starts at the newest event.

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, token string, required ...string) (Principal, error)

Authenticate verifies the secret and every requested scope. No returned slice aliases the Store or cache. Errors never include the supplied token.

func (*Service) AuthenticateDetails added in v0.2.0

func (s *Service) AuthenticateDetails(ctx context.Context, token string, required ...string) (AuthenticationDetails, error)

AuthenticateDetails verifies access and returns safe secret-version metadata.

func (*Service) Client

func (s *Service) Client(ctx context.Context, id string) (Client, error)

func (*Service) Clients

func (s *Service) Clients(ctx context.Context, after string, limit int) ([]Client, error)

func (*Service) Close

func (s *Service) Close() error

Close cancels workers and in-flight operations and waits for their completion.

func (*Service) CreateClient

func (s *Service) CreateClient(ctx context.Context, name string, scopes []string) (Client, error)

func (*Service) Credential

func (s *Service) Credential(ctx context.Context, id string) (Credential, error)

func (*Service) CredentialDetails added in v0.2.0

func (s *Service) CredentialDetails(ctx context.Context, id string) (CredentialDetails, error)

CredentialDetails reads consistent current management state from the primary.

func (*Service) Credentials

func (s *Service) Credentials(ctx context.Context, clientID, after string, limit int) ([]Credential, error)

func (*Service) Errors

func (s *Service) Errors() <-chan error

Errors reports recoverable cache/notification failures and unexpected watcher termination. Delivery is nonblocking and rate limited; use separate metrics for counting every failure. Freshness deadlines still apply.

func (*Service) Issue

func (s *Service) Issue(ctx context.Context, req IssueRequest) (Issued, error)

func (*Service) IssueOnce added in v0.2.0

func (s *Service) IssueOnce(ctx context.Context, id string, req IssueRequest) (OperationResult, error)

IssueOnce creates at most one committed credential per schema and operation ID. A replay returns a receipt without the unrecoverable plaintext token.

func (*Service) Operation added in v0.2.0

func (s *Service) Operation(ctx context.Context, id string) (OperationReceipt, error)

Operation returns a committed receipt. ErrNotFound does not prove rollback.

func (*Service) PruneAuditEvents added in v0.2.0

func (s *Service) PruneAuditEvents(ctx context.Context, before time.Time, limit int) (int64, error)

PruneAuditEvents removes only the host-selected historical window in small batches.

func (*Service) RetireVersion added in v0.2.0

func (s *Service) RetireVersion(ctx context.Context, credentialID, versionID string) error

RetireVersion shortens only a historical version's deadline, never the current one.

func (*Service) Revoke

func (s *Service) Revoke(ctx context.Context, id string) error

func (*Service) RevokeAll

func (s *Service) RevokeAll(ctx context.Context, clientID string) error

func (*Service) Rotate

func (s *Service) Rotate(ctx context.Context, req RotateRequest) (Issued, error)

func (*Service) RotateOnce added in v0.2.0

func (s *Service) RotateOnce(ctx context.Context, id string, req RotateRequest) (OperationResult, error)

RotateOnce makes a generation-checked rotation safe to retry with the same ID.

func (*Service) SecretVersions added in v0.2.0

func (s *Service) SecretVersions(ctx context.Context, id string, before int64, limit int) ([]SecretVersionInfo, error)

SecretVersions lists newest generations first; zero before starts at the newest.

func (*Service) SetClientBanned

func (s *Service) SetClientBanned(ctx context.Context, id string, banned bool) error

func (*Service) SetClientScopes

func (s *Service) SetClientScopes(ctx context.Context, id string, scopes []string) error

func (*Service) Start

func (s *Service) Start(ctx context.Context) error

Start begins optional cache and notification workers. It may be called once. Canceling ctx stops workers and makes further operations unavailable.

func (*Service) UsageStats added in v0.2.0

func (s *Service) UsageStats() UsageStats

UsageStats reports local collection state. Missing observations never prove that a key is unused; other replicas and dropped observations are not included.

type Snapshot

type Snapshot struct {
	Records    map[string]Record
	ReadAt     time.Time
	Generation uint64
}

type Source

type Source interface {
	Lookup(ctx context.Context, secretID string) (ReadResult, error)
	Snapshot(ctx context.Context) (Snapshot, error)
	Now() time.Time
	MaxStaleness() time.Duration
}

Source binds a cache to exactly the Service and Store it was constructed for.

type Store

type Store interface {
	CreateClient(ctx context.Context, client Client) error
	Client(ctx context.Context, id string) (Client, error)
	Clients(ctx context.Context, afterID string, limit int) ([]Client, error)
	SetClientScopes(ctx context.Context, id string, scopes []string) error
	SetClientBanned(ctx context.Context, id string, banned bool) error
	Issue(ctx context.Context, cred Credential, version SecretVersion) error
	Credential(ctx context.Context, id string) (Credential, error)
	Credentials(ctx context.Context, clientID, afterID string, limit int) ([]Credential, error)
	// Rotate locks client then credential; rejects banned/revoked state and a
	// mismatched generation. Only the preceding current secret's expiry is
	// shortened to min(original expiry, at+grace). Older deadlines never extend.
	Rotate(
		ctx context.Context, credID string, expectedGeneration int64, version SecretVersion, gracePeriod time.Duration, at time.Time,
	) (Credential, error)
	Revoke(ctx context.Context, id string, at time.Time) error
	RevokeAll(ctx context.Context, clientID string, at time.Time) error
	Lookup(ctx context.Context, secretID string) (Record, error)
	// Snapshot is all-or-nothing, keyed by secret version ID; expired or revoked
	// rows may be omitted. Partial query/scan/iteration results are errors.
	Snapshot(ctx context.Context) (map[string]Record, error)
}

Store implementations are concurrent-safe and enforce administrative invariants transactionally, not only in Service prechecks. Reads must use the authority's primary. Returned records belong to the caller, including slices and pointers.

type TimedStore

type TimedStore interface {
	IssueWithLifetime(
		ctx context.Context, cred Credential, version SecretVersion, ttl time.Duration,
	) (Credential, SecretVersion, error)
	RotateWithLifetime(
		ctx context.Context, credID string, expectedGeneration int64, version SecretVersion, ttl, grace time.Duration,
	) (Credential, SecretVersion, error)
}

TimedStore is an optional Store capability for assigning Issue and Rotate deadlines after authority locks are acquired. Returned times are the values actually committed. Stores without it retain the original Store contract.

type UsageConfig added in v0.2.0

type UsageConfig struct {
	FlushInterval time.Duration
	FlushTimeout  time.Duration
	CloseTimeout  time.Duration
	BatchSize     int
	MaxPending    int
}

type UsageObservation added in v0.2.0

type UsageObservation struct {
	ClientID     string
	CredentialID string
	VersionID    string
	At           time.Time
}

type UsageStats added in v0.2.0

type UsageStats struct {
	Started     bool      `json:"started"`
	Pending     int       `json:"pending"`
	Dropped     uint64    `json:"dropped"`
	LastFlushAt time.Time `json:"last_flush_at"`
}

type UsageStore added in v0.2.0

type UsageStore interface {
	RecordUsage(ctx context.Context, observations []UsageObservation) error
}

UsageStore merges times monotonically without authorization invalidation. Missing/pruned versions are ignored, never reinserted.

type Watcher

type Watcher interface {
	Watch(ctx context.Context, invalidate func()) error
}

Watcher is an optional best-effort invalidation source. Watch calls invalidate after initial subscription and every reconnection, then on committed changes. It reconnects until ctx is canceled; callbacks must be serialized.

type WatcherWithErrors

type WatcherWithErrors interface {
	Watcher
	WatchWithErrors(ctx context.Context, invalidate func(), report func(error)) error
}

WatcherWithErrors optionally reports recoverable listener failures. The callback must not block; Watch continues reconnecting until cancellation.

Directories

Path Synopsis
cache
arc
Package arc provides an optional bounded-staleness ARC adapter.
Package arc provides an optional bounded-staleness ARC adapter.
rcu
Package rcu provides a whole-snapshot bounded-staleness cache adapter.
Package rcu provides a whole-snapshot bounded-staleness cache adapter.
examples
access command
Command access demonstrates project-local M2M credential administration and a single protected HTTP endpoint.
Command access demonstrates project-local M2M credential administration and a single protected HTTP endpoint.
Package httpapi adapts API credential verification to net/http.
Package httpapi adapts API credential verification to net/http.
Package postgres provides the PostgreSQL authority for api credentials.
Package postgres provides the PostgreSQL authority for api credentials.

Jump to

Keyboard shortcuts

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