Documentation
¶
Overview ¶
Package connection is a principal's identity at an outside service: Alice's GitHub account, a pony's own machine account, an app installation acting for a team. The chassis holds the credential and renews it; a stack names the connection and never sees a token (docs/todo-connections.md in the service overlay).
Two declarations take part, both small YAML files a stack deploys, parsed at apply and activate like SANDBOXES/ and CAPS/:
OPS/<stack>/PROVIDERS/<name>.yaml how authentication at the service
works, where each credential may go,
and which stacks may use it (one
active stack per name in a tenant)
OPS/<stack>/CONNECTIONS/<provider>.yaml how THIS stack uses an identity:
the origins it calls, the header the
token goes on
A connection's identity is (tenant, principal, provider, label), never a stack: moving a provider's file to another stack changes no row.
This file is the leaf layer: the reserved-path vocabulary, with no dependency on stores, so the CLI, the admin producer and the control-event applier can import it cheaply.
Index ¶
- Constants
- Variables
- func ConsumerName(p string) string
- func ConsumerPath(provider string) string
- func IsConsumerPath(p string) bool
- func IsProviderPath(p string) bool
- func Loopback(host string) bool
- func ProviderName(p string) string
- func ProviderPath(name string) string
- func SameIssuer(declared, got string) bool
- func SecretName(ref string) (string, bool)
- func ValidName(s string) bool
- type Account
- type App
- type Config
- type Consumer
- type Decls
- type Destinations
- type Error
- type FinishInput
- type FinishResult
- type Flow
- type Margin
- type Material
- type NewRow
- type Origin
- type Provider
- type Refresh
- type ReleaseInput
- type Row
- type Secrets
- type Service
- func (s *Service) Finish(ctx context.Context, in FinishInput) (FinishResult, *Error)
- func (s *Service) HTTPClient(u *url.URL) *http.Client
- func (s *Service) List(ctx context.Context, tenantID, principal string) ([]Row, error)
- func (s *Service) ListProvider(ctx context.Context, tenantID, provider string) ([]Row, error)
- func (s *Service) ProviderDecl(ctx context.Context, tenant, name string) (string, *Provider, error)
- func (s *Service) Release(ctx context.Context, in ReleaseInput) (string, Row, *Error)
- func (s *Service) ReleaseCheck(ctx context.Context, in ReleaseInput) *Error
- func (s *Service) Revoke(ctx context.Context, tenant, tenantID, principal, provider string) (Row, bool, *Error)
- func (s *Service) SetClock(now func() time.Time)
- func (s *Service) Start(ctx context.Context, in StartInput) (StartResult, *Error)
- func (s *Service) SweepFlows(ctx context.Context) (int64, error)
- func (s *Service) Use(ctx context.Context, in UseInput) (UseResult, *Error)
- type Signer
- type StartInput
- type StartResult
- type Store
- func (s *Store) Claim(ctx context.Context, id string, generation int64, claimID string, ...) (bool, error)
- func (s *Store) ConsumeFlow(ctx context.Context, nonceHash string) (Flow, error)
- func (s *Store) Delete(ctx context.Context, tenantID, principalID, provider, label string) (Row, bool, error)
- func (s *Store) DeletePrincipal(ctx context.Context, tenantID, principalID string) (int64, error)
- func (s *Store) Get(ctx context.Context, tenantID, principalID, provider, label string) (Row, error)
- func (s *Store) ListByPrincipal(ctx context.Context, tenantID, principalID string) ([]Row, error)
- func (s *Store) ListByProvider(ctx context.Context, tenantID, provider string) ([]Row, error)
- func (s *Store) Open(ctx context.Context, r Row) (Material, error)
- func (s *Store) Put(ctx context.Context, in NewRow, m Material) (Row, error)
- func (s *Store) PutFlow(ctx context.Context, f Flow) error
- func (s *Store) Release(ctx context.Context, r Row, claimID string) error
- func (s *Store) ReleaseStale(ctx context.Context, r Row) (bool, error)
- func (s *Store) Reload(ctx context.Context, id string) (Row, error)
- func (s *Store) Renew(ctx context.Context, r Row, claimID string, m Material, ...) (Row, error)
- func (s *Store) SetClock(now func() time.Time)
- func (s *Store) SetStatus(ctx context.Context, r Row, claimID, status string) error
- func (s *Store) SweepFlows(ctx context.Context) (int64, error)
- type Use
- type UseInput
- type UseResult
Constants ¶
const ( // KindOAuth2 is the authorization-code flow: the person signs in, the // chassis holds and renews the tokens. KindOAuth2 = "oauth2" // KindGitHubApp is OAuth 2 plus the app's own key, which mints // installation tokens (a later slice). Its user flow is oauth2's. KindGitHubApp = "github_app" )
Provider kinds: how authentication at the service works.
const ( // CredentialOAuth2 is a user access token (and its refresh token). CredentialOAuth2 = "oauth2" // CredentialInstallation is a GitHub App installation token, minted // from the app's key (a later slice). CredentialInstallation = "github_app_installation" )
Credential kinds: which credential a consumer uses, and what a row holds.
const ( ClientAuthBody = "body" ClientAuthBasic = "basic" )
How the client authenticates at the token destination.
const ( // RotationSingleUse: a refresh token works once; the answer carries the // next one. Re-sending a used one is refused, and may end the family. RotationSingleUse = "single_use" // RotationReusable: the refresh token stays valid until revoked. RotationReusable = "reusable" )
Refresh-token rotation, the provider's own behaviour, declared rather than assumed.
const ( // OnUncertainReconnect: the old refresh token is never re-sent; the // current access token serves until it expires, then the owner // reconnects. OnUncertainReconnect = "reconnect" // OnUncertainRetry: one more try, under a new claim. OnUncertainRetry = "retry" )
What follows a renewal whose outcome is unknown (a timeout after the request was sent, a claim its holder never completed).
const ( MaxTitle = 64 MaxScopes = 32 MaxOrigins = 16 MaxConsumers = 64 )
Ceilings on one declaration.
const ( DefaultMarginFraction = 0.2 DefaultMarginFloorS = 60 DefaultMarginCeilingS = 900 )
Margin defaults: renewal starts when a fifth of the lifetime is left, never under a minute before expiry, never more than 15 minutes before — and never in a token's first half-life (Provider.Margin).
const ( ProviderDir = "PROVIDERS" ConsumerDir = "CONNECTIONS" DeclExt = ".yaml" )
Reserved top-level directories and the declaration extension.
const ( CodeInvalidArg = "invalid_arg" CodeNotDeclared = "not_declared" CodeUnsupported = "unsupported" CodeNoClient = "no_client" // the client id or secret is not set CodeStore = "store" CodeBadState = "bad_state" CodeExpired = "expired" CodeGone = "gone" // the flow was used or has expired CodeMismatch = "mismatch" CodeRefused = "refused" // the provider refused the code or the sign-in CodeMissing = "missing" CodeReconnect = "reconnect" CodeBusy = "busy" CodeDestination = "destination" CodeDenied = "denied" )
Error codes.
const ( StatusActive = "active" StatusUncertain = "uncertain" // a renewal's outcome is unknown StatusReconnect = "reconnect" // the provider refused the refresh token )
Row statuses.
const CookieName = "txco_connect"
CookieName is the per-flow browser cookie `start` sets and `finish` reads. Host-only (no Domain), so a callback on another host sees none.
const ( // FlowTTL is how long a sign-in may take. FlowTTL = 10 * time.Minute )
const KeyLabel = "txco/connection/v1"
KeyLabel is the derivation label of the signing key (secrets.DeriveKey). Part of the wire contract: changing it invalidates every live flow.
const RequestTimeout = 10 * time.Second
RequestTimeout bounds one credentialed request. Shorter than the renewal claim's lease (claimLease), so a claimant that is still waiting on the provider has not lost its claim.
const SandboxConsumerPrefix = "sandbox:"
SandboxConsumerPrefix marks a consumer entry that is a sandbox of the stack, not a capability: `loop: [sandbox:github]` lets the loop's SANDBOXES/github.yaml hand the token to the work that opens it.
Variables ¶
var ( // ErrBadState: malformed, forged, or signed under another key. ErrBadState = errors.New("connection: bad state") // ErrStateExpired: a genuine state past its expiry. ErrStateExpired = errors.New("connection: state expired") )
var ( // ErrNotFound: the principal has no such connection. ErrNotFound = errors.New("connection: not found") // ErrFlowGone: no such flow, or it was consumed or has expired. ErrFlowGone = errors.New("connection: no such sign-in, or it was already used") // ErrFenced: the row moved on (another node renewed it, or the claim // was not this writer's). ErrFenced = errors.New("connection: write fenced") )
var ErrNotDeclared = errors.New("connection: not declared")
ErrNotDeclared: no active stack of the tenant declares the provider, or the stack has no consumer declaration for it.
Functions ¶
func ConsumerName ¶
ConsumerName returns the provider a consumer declaration names: "CONNECTIONS/github.yaml" → "github"; "" when it is not a declaration.
func ConsumerPath ¶
ConsumerPath is the stack_files path of a stack's consumer declaration for a provider.
func IsConsumerPath ¶
IsConsumerPath reports whether a stack_files path lives under CONNECTIONS/.
func IsProviderPath ¶
IsProviderPath reports whether a stack_files path lives under PROVIDERS/.
func Loopback ¶
Loopback reports whether host is the local machine by name or literal: the one place an http:// origin, and a dial the private guard would refuse, is allowed on a development node.
func ProviderName ¶
ProviderName returns the provider a declaration path belongs to: "PROVIDERS/github.yaml" → "github"; "" when the path is not a declaration (nested, wrong extension, bad name) — the shape validateStackFilePath enforces at upload.
func ProviderPath ¶
ProviderPath is the stack_files path of a provider's declaration.
func SameIssuer ¶
SameIssuer reports whether a callback's `iss` names the provider's issuer: the same URL, as normalizeIssuer sees both.
func SecretName ¶
SecretName returns the tenant secret a `secret:NAME` reference names, or ("", false) for a literal.
Types ¶
type Account ¶
type Account struct {
URL string `yaml:"url"`
Subject string `yaml:"subject"`
Name string `yaml:"name"`
}
Account says how to learn who signed in: a GET at URL (an api origin) with the new token; Subject is the field holding the provider's stable id, Name the field holding the display name.
type Config ¶
type Config struct {
// Node is the node's egress guard; Open reports that its policy is
// open (development), which allows loopback origins.
Node egress.Guard
Open bool
// SlugToID resolves a tenant slug (what a request pins) to the tenant
// id the rows key on.
SlugToID func(ctx context.Context, slug string) (string, error)
UserAgent string
Logger *zap.Logger
}
Config is what New needs beside the store and the seams.
type Consumer ¶
type Consumer struct {
Provider string `yaml:"provider"`
Kind string `yaml:"kind"`
Origins []string `yaml:"origins"`
Use Use `yaml:"use"`
// contains filtered or unexported fields
}
Consumer is one CONNECTIONS/<provider>.yaml: how the declaring stack uses an identity at that provider.
func ParseConsumer ¶
ParseConsumer strictly decodes and validates a consumer declaration. The file's own name (the provider it is for) is checked by the caller against Consumer.Provider.
func (*Consumer) OriginAllowed ¶
OriginAllowed reports whether the consumer declares o.
func (*Consumer) OriginList ¶
OriginList is the consumer's origins, parsed, in declaration order.
type Decls ¶
type Decls interface {
// Provider resolves (tenant slug, name) → the declaring stack and its
// active declaration, or ErrNotDeclared.
Provider(ctx context.Context, tenant, name string) (string, *Provider, error)
// Consumer resolves (tenant slug, stack, provider) → the stack's active
// consumer declaration, or ErrNotDeclared.
Consumer(ctx context.Context, tenant, stack, provider string) (*Consumer, error)
}
Decls finds the declarations: a provider by name across the tenant's active stacks, and a stack's consumer declaration for a provider.
type Destinations ¶
type Destinations struct {
// Authorization is where the browser goes to sign in. No credential is
// sent there.
Authorization string `yaml:"authorization"`
// Token is where a code or refresh token is exchanged: the client
// credentials go here and nowhere else.
Token string `yaml:"token"`
// Revocation is "none", or an RFC 7009 endpoint the token being revoked
// is posted to.
Revocation string `yaml:"revocation"`
// API is every origin an access token may be sent to; a consumer
// narrows it to the origins it calls.
API []string `yaml:"api"`
}
Destinations is where each credential may go. Each kind of credential reaches one kind of destination and no other (Service.send).
type Error ¶
Error is what a caller shapes for a rule: a code (without the `txco_connection_` prefix the op adds) and a message that never holds material.
type FinishInput ¶
type FinishInput struct {
Tenant, TenantID string
Stack string
Host, Path string // the request's
Code, State string
Iss string
ProviderError string // the provider's `error` query parameter, if any
Cookie string // the flow cookie's value
}
FinishInput is read from the callback request the managing stack is serving — never from a WITH clause.
type FinishResult ¶
FinishResult is the stored connection's metadata and the cookie line that clears the flow cookie.
type Flow ¶
type Flow struct {
NonceHash string
TenantID string
PrincipalID string
Provider string
Label string
Kind string
Stack string
RedirectURI string
BrowserHash string
Initiator string
ExpiresAt time.Time
ConsumedAt *time.Time
}
Flow mirrors a connection_flows row.
type Margin ¶
type Margin struct {
Fraction float64 `yaml:"fraction"`
FloorS int `yaml:"floor_s"`
CeilingS int `yaml:"ceiling_s"`
}
Margin is when renewal starts: a fraction of the lifetime, clamped.
type Material ¶
type Material struct {
Access string `json:"a,omitempty"`
Refresh string `json:"r,omitempty"`
// TokenType is the provider's word for the access token ("bearer");
// kept for the record, the consumer's format decides the header.
TokenType string `json:"t,omitempty"`
}
Material is what a row holds, sealed: the tokens. It never leaves this package except as a header value on a request (Service.Use) or a form field at the token destination (renew).
type NewRow ¶
type NewRow struct {
TenantID, PrincipalID, Provider, Label, Kind string
Subject, Account string
Scopes []string
ExpiresAt, RefreshExpiresAt *time.Time
LifetimeS int64
CreatedBy string
}
NewRow is what a finished sign-in stores.
type Origin ¶
type Origin struct {
Scheme string
Host string // lower case, IDNA ASCII
Port string // always set: the scheme's default when the URL had none
}
An Origin is where a credential may be sent: scheme, host and port, exactly — never a host name alone, so a port the declaration did not name is refused. `https://` only, except `http://` to a loopback host on a development node.
func OriginOf ¶
OriginOf is the origin a request URL reaches, normalized the same way, so a declaration and a request compare exactly.
func ParseOrigin ¶
ParseOrigin reads "https://api.github.com" or "https://host:8443". A path other than "/" (or none), a query, a fragment or user info is refused: an origin names a place, not a resource.
type Provider ¶
type Provider struct {
Title string `yaml:"title"`
Kind string `yaml:"kind"`
Issuer string `yaml:"issuer"`
Destinations Destinations `yaml:"destinations"`
ClientID string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
ClientAuth string `yaml:"client_auth"`
PKCE bool `yaml:"pkce"`
Scopes []string `yaml:"scopes"`
Account *Account `yaml:"account"`
Refresh Refresh `yaml:"refresh"`
App *App `yaml:"app"`
Consumers map[string][]string `yaml:"consumers"`
// contains filtered or unexported fields
}
Provider is one PROVIDERS/<name>.yaml: how authentication at a service works, where each credential may go, and which stacks may use its connections for which capabilities. Parsed strictly: an unknown key is a deploy error.
func ParseProvider ¶
ParseProvider strictly decodes and validates a provider declaration.
func (*Provider) Allows ¶
Allows reports whether stack may use this provider's connections for the capability (or, as "sandbox:<name>", for that sandbox). No wildcards: the stack and the capability are both named.
func (*Provider) ConsumerStacks ¶
ConsumerStacks is the sorted list of stacks the provider names.
type Refresh ¶
type Refresh struct {
Rotation string `yaml:"rotation"`
OnUncertain string `yaml:"on_uncertain"`
Margin Margin `yaml:"margin"`
}
Refresh is the provider's renewal behaviour.
type ReleaseInput ¶
type ReleaseInput struct {
Tenant string // slug
Stack string // the stack whose SANDBOXES/ declares the sandbox
Provider string
Principal string
Sandbox string
}
ReleaseInput is one sandbox entry that names this provider (`connection:<provider>`): whose connection, by which stack's sandbox. Every field is the run grant's own, never the asking program's.
type Row ¶
type Row struct {
ID string
TenantID string
PrincipalID string
Provider string
Label string
Kind string
Subject string
Account string
Status string
Scopes []string
Generation int64
ExpiresAt *time.Time
LifetimeS int64
RefreshExpiresAt *time.Time
ClaimID string
ClaimUntil *time.Time
CreatedBy string
CreatedAt time.Time
UpdatedAt time.Time
// contains filtered or unexported fields
}
Row mirrors a connections row. The material is not in it: Open returns it, to the one caller that sends it.
type Secrets ¶
type Secrets interface {
Materialize(ctx context.Context, tenant, stack, name string) ([]byte, error)
}
Secrets materializes a tenant secret a declaration references, in the declaring stack's scope (the tenant secret store's own fallback rule).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service — see the package comment.
func New ¶
New builds the service. signer is keyed from the master key under KeyLabel (secrets.DeriveKey).
func (*Service) Finish ¶
func (s *Service) Finish(ctx context.Context, in FinishInput) (FinishResult, *Error)
Finish consumes the flow, exchanges the code and stores the tokens.
func (*Service) HTTPClient ¶
HTTPClient is the client a request that carries a connection's token is made with: guarded, and following no redirect.
func (*Service) ListProvider ¶
ListProvider is every connection at a provider, metadata only.
func (*Service) ProviderDecl ¶
ProviderDecl resolves a provider's declaration: the declaring stack and the declaration, or ErrNotDeclared. For the ops that must know who may list a provider's connections.
func (*Service) Release ¶
Release hands over the principal's current token at the provider for a sandbox, renewing it when due: the bare token, for the work that opened the sandbox to hold. The provider must name the stack's sandbox as a consumer (`<stack>: [sandbox:<name>]`). There is no destination check: the token is meant to leave for the machine the work runs on.
func (*Service) ReleaseCheck ¶
func (s *Service) ReleaseCheck(ctx context.Context, in ReleaseInput) *Error
ReleaseCheck answers whether the principal holds a connection this stack's sandbox may release, without renewing or reading its material: what the grant gateway shows the tenant's rules before it decides. A nil error: release would find it.
func (*Service) Revoke ¶
func (s *Service) Revoke(ctx context.Context, tenant, tenantID, principal, provider string) (Row, bool, *Error)
Revoke removes a connection and, when the provider has a revocation destination, tells it. It returns false when there was none.
func (*Service) Start ¶
func (s *Service) Start(ctx context.Context, in StartInput) (StartResult, *Error)
Start records a flow and builds the provider's sign-in URL.
func (*Service) SweepFlows ¶
SweepFlows is for a periodic sweeper: flows past expiry are deleted.
func (*Service) Use ¶
Use decides whether this request may carry the principal's token at this provider, renews the token when due, and returns the header. Four facts must agree (docs/todo-connections.md §4): the pinned principal and capability, the executing stack as a consumer the provider names, and the destination in both declarations.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer signs states and derives verifiers under one key.
type StartInput ¶
type StartInput struct {
Tenant, TenantID string
Stack string // the managing stack, as dispatched (a slot reduces to its base)
Principal string
Provider string
Kind string // "" is oauth2
Label string // "" in this slice
RedirectURI string
Host string // the request's host: where the cookie goes
Initiator string // who the stack says is connecting, for the record
}
StartInput is what the managing stack asks for.
type StartResult ¶
type StartResult struct {
URL string
ExpiresAt time.Time
Cookie string // a Set-Cookie line
Provider *Provider
}
StartResult is the sign-in URL and the cookie the response must set.
type Store ¶
type Store struct {
DB *sql.DB
Dialect registry.Dialect
MK secrets.MasterKeyProvider
// contains filtered or unexported fields
}
Store reads and writes the two connection tables in the auth database (db/schema/*/auth/0009_connections.sql): `connections`, one row per identity, holding only the current material; and `connection_flows`, a sign-in in progress, consumed once.
Not the tenant secret store: a secret is the operator's, versioned forever and fed to every node; a connection is a principal's, renewed every few hours on whichever node uses it, in the shared auth database.
func NewStore ¶
NewStore builds a Store over an open, migrated auth DB. A nil dialect is SQLite. mk seals and opens material; nil means every open fails.
func (*Store) Claim ¶
func (s *Store) Claim(ctx context.Context, id string, generation int64, claimID string, until time.Time, status string) (bool, error)
Claim takes the row's renewal for claimID until `until`. It succeeds for one caller: the row at this generation, in the status the caller saw (active; or uncertain, when the provider's rule allows a retry), with no claim at all. An expired claim does not count as none — nobody takes over a renewal whose outcome is unknown (ReleaseStale is what clears one, as uncertain).
func (*Store) ConsumeFlow ¶
ConsumeFlow marks the flow used and returns it. Exactly one caller gets it: a replayed state, a racing second callback, or an expired flow is ErrFlowGone. The code is exchanged only after this.
func (*Store) Delete ¶
func (s *Store) Delete(ctx context.Context, tenantID, principalID, provider, label string) (Row, bool, error)
Delete removes a connection. It returns the row it removed, for the revocation at the provider, and false when there was none.
func (*Store) DeletePrincipal ¶
DeletePrincipal removes every connection of a principal (the principal is gone).
func (*Store) Get ¶
func (s *Store) Get(ctx context.Context, tenantID, principalID, provider, label string) (Row, error)
Get is one connection by its identity.
func (*Store) ListByPrincipal ¶
ListByPrincipal is every connection of a principal, by provider.
func (*Store) ListByProvider ¶
ListByProvider is every connection at a provider, by principal.
func (*Store) Open ¶
Open decrypts a row's material. The caller zeroes nothing: the strings are short-lived values on one request.
func (*Store) Put ¶
Put stores a connection: a new row, or the identity's existing row re-sealed at the next generation with status active and no claim. A reconnect replaces whatever was there.
func (*Store) Release ¶
Release clears a claim whose request was never sent: nothing changed, the next use may claim again.
func (*Store) ReleaseStale ¶
ReleaseStale clears a claim past its lease and marks the row uncertain: the claimant may have died after sending the refresh token. It fences on the stale claim's id, so a claimant that finishes late is fenced too.
func (*Store) Reload ¶
Reload re-reads a row by id (after a claim, to see what the claimant wrote). ErrNotFound when it was deleted meanwhile.
func (*Store) Renew ¶
func (s *Store) Renew(ctx context.Context, r Row, claimID string, m Material, expiresAt, refreshExpiresAt *time.Time, lifetimeS int64) (Row, error)
Renew writes a renewal's result, fenced by the claim and the generation: a late writer whose claim was cleared, or whose generation moved on, is ErrFenced and changes nothing.