auth

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package auth authenticates the callers of the Reporting API. It mints and hashes the two kinds of bearer token, resolves a presented token into a principal, and hands that principal to the handlers through the request context.

Three middlewares guard the three classes of route: Ingest for the ingest endpoints, Query for the read endpoints, and Internal for /internal/*. All three answer a missing, malformed, unknown, or revoked credential with a 401 problem document, and a lookup that fails for any other reason with a 500: a database outage must not read as a bad credential.

httpapi picks one of them per route, after chi has matched the route, so the matched pattern decides which guard a request meets and which role it needs. A route the pattern table does not name is refused there rather than served, which keeps a new operation from going online with its credential unchecked.

The Authenticator interface is where an OIDC provider slots in later. Today the only implementation is the static lookup against the api_tokens table.

The middlewares take an accessor for the request-scoped logger instead of importing httpapi, because httpapi is what mounts them.

The normative specification is roadmap/01-phase-1-core-platform-openstack.md, WP 1.4.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GenerateAPIToken

func GenerateAPIToken() (string, error)

GenerateAPIToken returns a new query token: tly_a_ followed by 32 random bytes in lower-case hex. The caller shows the token to the operator once and stores nothing but HashToken's digest.

func GenerateIngestToken

func GenerateIngestToken() (string, error)

GenerateIngestToken returns a new ingest token: tly_i_ followed by 32 random bytes in lower-case hex. The caller shows the token to the operator once and stores nothing but HashToken's digest.

func HashToken

func HashToken(token string) string

HashToken returns the sha256 digest of the whole token, prefix included, in lower-case hex. This is what the token_hash columns hold: the database never stores a usable token, so a dump of it hands out no access.

func Ingest

func Ingest(q *sqlcgen.Queries, mode Mode, logger func(context.Context) *slog.Logger) func(http.Handler) http.Handler

Ingest builds the middleware guarding the ingest endpoints. It resolves the bearer token to an ingest credential and puts that credential in the request context, where IngestFromContext reads it.

In ModeDisabled it checks nothing, so a handler finds no credential in the context.

The logger accessor reports lookup failures against the request-scoped logger; a nil accessor falls back to slog.Default.

func Internal

func Internal(token string, mode Mode) func(http.Handler) http.Handler

Internal builds the middleware guarding /internal/*, the routes the other Tally components call. They share one secret rather than holding a row in api_tokens, so the comparison happens here and in constant time.

In ModeDisabled it serves every request.

func Query

func Query(a Authenticator, required Role, mode Mode, logger func(context.Context) *slog.Logger) func(http.Handler) http.Handler

Query builds the middleware guarding a class of read endpoints. It authenticates the bearer token, refuses a principal whose role does not cover required with a 403, and otherwise puts the principal in the request context, where QueryFromContext reads it.

In ModeDisabled it serves every request as an admin, so a development setup reaches the same handlers a token would.

The logger accessor reports lookup failures against the request-scoped logger; a nil accessor falls back to slog.Default.

Types

type Authenticator

type Authenticator interface {
	Authenticate(ctx context.Context, token string) (QueryPrincipal, error)
}

Authenticator turns the bearer token of a query request into the principal it stands for. It returns an error matching pgx.ErrNoRows when the token is unknown or revoked, and any other error when the lookup itself failed.

It is the seam for accepting credentials the api_tokens table knows nothing about, such as the OIDC JWTs behind TALLY_REPORTING_OIDC_JWKS_URL.

type IngestPrincipal

type IngestPrincipal struct {
	// CredentialID is the ingest_credentials row the token resolved to.
	CredentialID uuid.UUID
	// Platform is the platform the credential reports for, openstack for
	// example.
	Platform string
	// Cloud is the cloud the credential reports for.
	Cloud string
}

IngestPrincipal is the credential an accepted ingest request presented. The platform and the cloud are the ones the credential was issued for, so a handler takes them from here rather than from the request body: a credential can only report for its own cloud.

func IngestFromContext

func IngestFromContext(ctx context.Context) (IngestPrincipal, bool)

IngestFromContext returns the ingest credential the request authenticated with. The second result is false outside an ingest route and on requests served while authentication is disabled.

type Mode

type Mode string

Mode says whether the middlewares check anything. It mirrors the values of TALLY_REPORTING_AUTH_MODE, which the server maps onto these constants.

const (
	// ModeEnforced is the production mode: every request needs a credential.
	ModeEnforced Mode = "enforced"
	// ModeDisabled serves every request without checking a credential. It
	// exists for development and tests.
	ModeDisabled Mode = "disabled"
)

type ProjectRef

type ProjectRef struct {
	// Cloud is the cloud the project lives in.
	Cloud string
	// ExternalID is the project id that cloud uses.
	ExternalID string
}

ProjectRef identifies a project the way the event and resource tables do, which is not by its registry id.

type QueryPrincipal

type QueryPrincipal struct {
	// TokenID is the api_tokens row the token resolved to.
	TokenID uuid.UUID
	// Role is what the token may do.
	Role Role
	// ProjectIDs are the registry project ids a project-role token is limited
	// to. It is empty for the other roles, which see every project.
	ProjectIDs []uuid.UUID
}

QueryPrincipal is the API token an accepted query request presented.

func QueryFromContext

func QueryFromContext(ctx context.Context) (QueryPrincipal, bool)

QueryFromContext returns the API token the request authenticated with. The second result is false outside a query route.

type Role

type Role string

Role is what an API token may do. The roles form a chain: admin contains read_all, and read_all contains project.

const (
	// RoleAdmin may use every endpoint, including the administrative ones.
	RoleAdmin Role = "admin"
	// RoleReadAll may read the data of every project.
	RoleReadAll Role = "read_all"
	// RoleProject may read the data of the projects its token names.
	RoleProject Role = "project"
)

func (Role) Allows

func (r Role) Allows(required Role) bool

Allows reports whether a caller holding r may enter a route that requires the given role.

type Scope

type Scope struct {
	// Unfiltered says the principal reads every project. Refs is empty then.
	Unfiltered bool
	// Refs are the projects a filtered principal reads, empty when it reaches
	// none. A caller filters on these whenever Unfiltered is false, so a token
	// whose projects are gone reads nothing rather than everything.
	Refs []ProjectRef
}

Scope is what a principal may read. The two cases it separates look alike in a bare slice of refs and mean the opposite: no filter at all, and a filter nothing passes.

func ResolveProjectScope

func ResolveProjectScope(ctx context.Context, q *sqlcgen.Queries, p QueryPrincipal) (Scope, error)

ResolveProjectScope reports what a query principal may read. An admin or a read_all token is unfiltered. A project token resolves to the (cloud, external id) pairs the query filters run on, which is how the events and resources tables name a project.

A project token stays filtered even when it resolves to no pair: registry ids that name no project drop out, which narrows the scope rather than widening it, so a project removed from the registry stops being visible instead of turning its token into an unfiltered one.

The roles are matched against an allow-list, the way roleRank ranks them: a role this build does not know is an error rather than an unfiltered scope, so a token row written by a newer build reads nothing rather than everything.

type StaticTokenAuthenticator

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

StaticTokenAuthenticator resolves the tokens issued by the admin CLI, which live in the api_tokens table.

func NewStaticTokenAuthenticator

func NewStaticTokenAuthenticator(q *sqlcgen.Queries) *StaticTokenAuthenticator

NewStaticTokenAuthenticator returns an Authenticator reading from q.

func (*StaticTokenAuthenticator) Authenticate

func (a *StaticTokenAuthenticator) Authenticate(ctx context.Context, token string) (QueryPrincipal, error)

Authenticate looks the token's hash up in api_tokens. The query skips revoked rows, so a revoked token is reported the same way an unknown one is: as pgx.ErrNoRows, which the returned error wraps.

Jump to

Keyboard shortcuts

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