Documentation
¶
Overview ¶
Package connectionforge manages a tenant's connections to external services: the provider, the connection, its encrypted credential, who may use it and what depends on it.
It owns the credential lifecycle, not the provider API clients. A caller asks the broker for an authenticated net/http.Client and never handles a raw secret. Storage, encryption, authorization, audit and egress policy are ports that the host application supplies.
The design is in docs/specs/initiatives/INIT-CONNECTIONFORGE-001.
Index ¶
- Constants
- Variables
- func CredentialAAD(owner OwnerID, connectionID string, version int) []byte
- func Permanent(err error) bool
- type Action
- type Auditor
- type Authorizer
- type Binding
- type BindingStore
- type Clock
- type Connection
- type ConnectionStore
- type Consumer
- type Credential
- type CredentialStore
- type Cryptographer
- type Event
- type EventType
- type Filter
- type Grant
- type GrantStore
- type Health
- type Lease
- type Leaser
- type Outcome
- type OwnerID
- type Principal
- type PrincipalKind
- type Secret
- func (s Secret) Clear()
- func (Secret) Format(f fmt.State, _ rune)
- func (Secret) GoString() string
- func (s Secret) IsZero() bool
- func (Secret) LogValue() slog.Value
- func (Secret) MarshalJSON() ([]byte, error)
- func (Secret) MarshalText() ([]byte, error)
- func (s Secret) Reveal() []byte
- func (Secret) String() string
- type SecretSource
- type State
- type Store
- type SystemClock
- type Use
Constants ¶
const CredentialPurpose = "connectionforge.credential"
CredentialPurpose is the purpose under which credentials are sealed.
Variables ¶
var ( // ErrNotFound is returned when a record does not exist for the owner. A // record that belongs to another owner is also not found. ErrNotFound = errors.New("connectionforge: not found") // ErrExists is returned when creating a record that already exists. ErrExists = errors.New("connectionforge: already exists") // ErrConflict is returned when a compare-and-swap finds a different version // than expected: another writer got there first. ErrConflict = errors.New("connectionforge: version conflict") // ErrInvalidTransition is returned for a lifecycle change the transition // table does not allow. ErrInvalidTransition = errors.New("connectionforge: invalid state transition") // ErrLeaseHeld is returned when a lease is held by someone else. ErrLeaseHeld = errors.New("connectionforge: lease held") )
Store errors.
var ( // ErrDenied is returned when the host's authorizer refuses an action. ErrDenied = errors.New("connectionforge: denied") // ErrNotUsable is returned when a connection's state does not allow use. ErrNotUsable = errors.New("connectionforge: connection not usable") // decrypted. The connection is not used: there is no fallback. ErrCredentialUnavailable = errors.New("connectionforge: credential unavailable") )
Use errors.
var ( // ErrInvalidCredential means the provider rejected the credential: it is // wrong, expired or revoked. It is a permanent failure. ErrInvalidCredential = errors.New("connectionforge: invalid credential") // ErrInsufficientScope means the credential is valid but lacks a // permission the connection needs. It is a permanent failure. ErrInsufficientScope = errors.New("connectionforge: insufficient scope") // ErrRateLimited means the provider is limiting requests. It is transient. ErrRateLimited = errors.New("connectionforge: rate limited") // ErrTransient means a network failure or provider error that may succeed // on retry. ErrTransient = errors.New("connectionforge: transient failure") )
Provider errors, as classified by a provider adapter. An adapter wraps one of these; it never puts a request or response body in an error.
Functions ¶
func CredentialAAD ¶
CredentialAAD returns the additional data a credential is sealed with. It binds the ciphertext to its owner, connection and version, so it cannot be moved to another record.
Types ¶
type Authorizer ¶
type Authorizer interface {
Allowed(ctx context.Context, p Principal, a Action, c Connection) error
}
Authorizer decides whether a principal may act on a connection. A refusal is an error wrapping ErrDenied.
type Binding ¶
type Binding struct {
Owner OwnerID `json:"owner"`
ConnectionID string `json:"connectionId"`
Consumer Consumer `json:"consumer"`
Purpose string `json:"purpose"`
CreatedAt time.Time `json:"createdAt"`
}
Binding is dependency: a consumer that uses a connection for a purpose.
type BindingStore ¶
type BindingStore interface {
// PutBinding records that a consumer uses a connection for a purpose. It
// is idempotent.
PutBinding(ctx context.Context, b Binding) error
// RemoveBinding removes a binding. Removing none is not an error.
RemoveBinding(ctx context.Context, owner OwnerID, connectionID string, c Consumer, purpose string) error
// ListBindings returns a connection's bindings.
ListBindings(ctx context.Context, owner OwnerID, connectionID string) ([]Binding, error)
}
BindingStore holds bindings.
type Connection ¶
type Connection struct {
ID string `json:"id"`
Owner OwnerID `json:"owner"`
Provider string `json:"provider"`
AuthMethod string `json:"authMethod"`
DisplayName string `json:"displayName"`
ExternalAccountID string `json:"externalAccountId,omitempty"`
ExternalLabel string `json:"externalLabel,omitempty"`
Scopes []string `json:"scopes,omitempty"`
State State `json:"state"`
Health Health `json:"health"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedBy Principal `json:"createdBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Revision increases on every update and drives compare-and-swap.
Revision uint64 `json:"revision"`
}
Connection is an owner's relationship with one account at a provider. It holds no secret.
type ConnectionStore ¶
type ConnectionStore interface {
// CreateConnection stores a new connection with revision 1. It returns
// [ErrExists] if the ID is taken.
CreateConnection(ctx context.Context, c Connection) (Connection, error)
// GetConnection returns one connection.
GetConnection(ctx context.Context, owner OwnerID, id string) (Connection, error)
// ListConnections returns the owner's connections that match f, ordered
// by ID.
ListConnections(ctx context.Context, owner OwnerID, f Filter) ([]Connection, error)
// UpdateConnection replaces a connection if its stored revision equals
// c.Revision, and returns it with the next revision. Otherwise it returns
// [ErrConflict]. A state change the transition table forbids returns
// [ErrInvalidTransition].
UpdateConnection(ctx context.Context, c Connection) (Connection, error)
// DeleteConnection removes a connection with its credentials, grants and
// bindings.
DeleteConnection(ctx context.Context, owner OwnerID, id string) error
}
ConnectionStore holds connections. Every method is scoped by owner: a record of another owner is ErrNotFound. Returned records are copies.
type Consumer ¶
Consumer is something that depends on a connection: an application, a workflow or an agent.
type Credential ¶
type Credential struct {
Owner OwnerID `json:"owner"`
ConnectionID string `json:"connectionId"`
// Version starts at 1 and increases by one with each replacement or
// refresh.
Version int `json:"version"`
// Kind names the plaintext's structure, which its provider defines, for
// example "api_token" or "oauth_tokens".
Kind string `json:"kind"`
// Ciphertext is the sealed material, from the [Cryptographer].
Ciphertext []byte `json:"-"`
// ExpiresAt is when the access token expires, if known.
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
Credential is the encrypted authorization material for a connection, one version of it. Plaintext never appears here.
type CredentialStore ¶
type CredentialStore interface {
// SwapCredential installs next as the active version if the active
// version is expectActive (0 for none) and next.Version is
// expectActive+1. Otherwise it returns [ErrConflict]. The previous
// version is kept until destroyed.
SwapCredential(ctx context.Context, expectActive int, next Credential) error
// ActiveCredential returns the active version.
ActiveCredential(ctx context.Context, owner OwnerID, connectionID string) (Credential, error)
// DestroyCredentials removes every version for a connection.
DestroyCredentials(ctx context.Context, owner OwnerID, connectionID string) error
}
CredentialStore holds sealed credential versions.
type Cryptographer ¶
type Cryptographer interface {
Seal(ctx context.Context, purpose string, plaintext, aad []byte) ([]byte, error)
Open(ctx context.Context, purpose string, ciphertext, aad []byte) ([]byte, error)
}
Cryptographer seals and opens credential material. A key-management library implements it; the host supplies the adapter. Open with a different purpose or aad must fail.
type Event ¶
type Event struct {
Time time.Time `json:"time"`
Type EventType `json:"type"`
Owner OwnerID `json:"owner"`
Actor Principal `json:"actor"`
OnBehalfOf *Principal `json:"onBehalfOf,omitempty"`
ConnectionID string `json:"connectionId,omitempty"`
Provider string `json:"provider,omitempty"`
Success bool `json:"success"`
Reason string `json:"reason,omitempty"`
Details map[string]string `json:"details,omitempty"`
}
Event is an audit record.
type EventType ¶
type EventType string
EventType names what happened.
const ( EventConnectionCreated EventType = "connection.created" EventConnectionValidated EventType = "connection.validated" EventConnectionStateChanged EventType = "connection.state_changed" EventConnectionUpdated EventType = "connection.updated" EventConnectionDisconnected EventType = "connection.disconnected" EventConnectionDeleted EventType = "connection.deleted" EventCredentialReplaced EventType = "credential.replaced" //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret EventCredentialRefreshed EventType = "credential.refreshed" //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret EventCredentialRevoked EventType = "credential.revoked" //nolint:gosec // G101: Enum/constant identifier matches the credential-name heuristic, but the value is a public tag, not a secret EventGrantChanged EventType = "grant.changed" EventAccessDenied EventType = "access.denied" )
Event types.
type Filter ¶
Filter narrows a connection listing. Zero fields match everything.
func (Filter) Match ¶
func (f Filter) Match(c Connection) bool
Match reports whether c passes the filter.
type Grant ¶
type Grant struct {
Owner OwnerID `json:"owner"`
ConnectionID string `json:"connectionId"`
Principal Principal `json:"principal"`
Actions []Action `json:"actions"`
}
Grant is authority: what a principal may do with a connection.
type GrantStore ¶
type GrantStore interface {
// PutGrant creates or replaces the grant for a principal on a connection.
PutGrant(ctx context.Context, g Grant) error
// RemoveGrant removes a principal's grant. Removing none is not an error.
RemoveGrant(ctx context.Context, owner OwnerID, connectionID string, p Principal) error
// ListGrants returns a connection's grants.
ListGrants(ctx context.Context, owner OwnerID, connectionID string) ([]Grant, error)
}
GrantStore holds grants.
type Health ¶
type Health struct {
LastValidatedAt time.Time `json:"lastValidatedAt,omitzero"`
LastValidation Outcome `json:"lastValidation,omitempty"`
LastError string `json:"lastError,omitempty"`
ConsecutiveFails int `json:"consecutiveFails,omitempty"`
}
Health is what was last observed about a connection, kept apart from its lifecycle State.
type Lease ¶
type Lease interface {
// Release gives the lease up before it expires.
Release(ctx context.Context) error
}
Lease is a held lease.
type Leaser ¶
type Leaser interface {
// Acquire takes the lease named key for ttl, or returns [ErrLeaseHeld].
Acquire(ctx context.Context, key string, ttl time.Duration) (Lease, error)
}
Leaser grants short exclusive leases, used to make refresh single-flight across processes.
type OwnerID ¶
type OwnerID string
OwnerID is the tenant scope that owns a record. It is opaque to the library; a host typically uses an organization ID. Every store method is scoped by it.
type Principal ¶
type Principal struct {
Kind PrincipalKind `json:"kind"`
ID string `json:"id"`
}
Principal is who or what performs an action, as the host identifies it.
type PrincipalKind ¶
type PrincipalKind string
PrincipalKind distinguishes who or what acts.
const ( PrincipalUser PrincipalKind = "user" PrincipalService PrincipalKind = "service" PrincipalAgent PrincipalKind = "agent" )
Principal kinds.
type Secret ¶
type Secret struct {
// contains filtered or unexported fields
}
Secret holds credential material. It redacts itself wherever a value is printed, logged or encoded, so a secret placed in a struct, an error or a log record does not leak. Only Secret.Reveal returns the value, and it is called only by the broker and provider adapters.
func (Secret) Clear ¶
func (s Secret) Clear()
Clear overwrites the secret's value in place, on a best-effort basis.
func (Secret) Format ¶
Format prints a fixed placeholder for every verb. Format has no way to report a failed write to fmt, which records write errors itself, so the result of Write is deliberately not used.
func (Secret) MarshalJSON ¶
MarshalJSON encodes a fixed placeholder.
func (Secret) MarshalText ¶
MarshalText encodes a fixed placeholder.
type SecretSource ¶
SecretSource reads secrets that belong to the deployment rather than to an owner, such as an OAuth client secret or a GitHub App private key. A secret manager implements it; the host supplies the adapter.
type State ¶
type State string
State is a connection's lifecycle state: what was decided. What was last observed is Health; a transient failure changes health, never state.
const ( // StatePending is a connection created but not yet authorized or validated. StatePending State = "pending" // StateConnected is a connection whose credential the provider accepted at // the last check. StateConnected State = "connected" // StateNeedsAttention is a connection with a permanent failure: revoked // upstream, expired, or a scope lost. It is not used until reconnected. StateNeedsAttention State = "needs_attention" // StateDisconnected is a connection a person disconnected. Its credentials // are destroyed. StateDisconnected State = "disconnected" // StateRevoked is a connection whose credential was revoked at the // provider. It is terminal. StateRevoked State = "revoked" )
Lifecycle states.
func (State) CanTransition ¶
CanTransition reports whether s may move to next. Staying in the same state is always allowed.
func (State) CheckTransition ¶
CheckTransition returns an error wrapping ErrInvalidTransition if s may not move to next.
type Store ¶
type Store interface {
ConnectionStore
CredentialStore
GrantStore
BindingStore
}
Store is every store the library needs. A host may implement it with one type, as the packages memstore and sqlstore do.
type Use ¶
type Use struct {
Consumer Consumer `json:"consumer"`
// Purpose is the feature using the connection, for example "code-health".
Purpose string `json:"purpose"`
// OnBehalfOf is the principal an agent or service acts for, if any.
OnBehalfOf *Principal `json:"onBehalfOf,omitempty"`
}
Use describes why a connection is being used, for authorization and for use counters.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package broker is the only path from a stored credential to an authenticated request.
|
Package broker is the only path from a stored credential to an authenticated request. |
|
Package cryptotest is the conformance suite for a [connectionforge.Cryptographer], and an in-memory cryptographer for tests.
|
Package cryptotest is the conformance suite for a [connectionforge.Cryptographer], and an in-memory cryptographer for tests. |
|
Package egress restricts where a provider request may go: only to the hosts a provider's manifest names, over HTTPS, without following a redirect to another host, and never to a private, loopback or link-local address, which is checked after DNS resolution so a rebinding name cannot reach one.
|
Package egress restricts where a provider request may go: only to the hosts a provider's manifest names, over HTTPS, without following a redirect to another host, and never to a private, loopback or link-local address, which is checked after DNS resolution so a rebinding name cannot reach one. |
|
internal
|
|
|
fakeapi
Package fakeapi is a token-authenticated HTTPS API for tests: a stand-in for a provider such as a CDN or analytics service.
|
Package fakeapi is a token-authenticated HTTPS API for tests: a stand-in for a provider such as a CDN or analytics service. |
|
Package provider defines what a provider adapter implements: a declarative Manifest, validation with account discovery, and authentication of outgoing requests.
|
Package provider defines what a provider adapter implements: a declarative Manifest, validation with account discovery, and authentication of outgoing requests. |
|
apikey
Package apikey is a provider for services that issue a static API key or token, driven by configuration: where the token goes in a request, which endpoint verifies it, and where the account is in the response.
|
Package apikey is a provider for services that issue a static API key or token, driven by configuration: where the token goes in a request, which endpoint verifies it, and where the account is in the response. |
|
Package service holds connectionforge's operations: create, get, list, rename, test and disconnect.
|
Package service holds connectionforge's operations: create, get, list, rename, test and disconnect. |
|
Package sqlstore is connectionforge's [connectionforge.Store] and [connectionforge.Leaser] on SQLite or PostgreSQL, through Ent.
|
Package sqlstore is connectionforge's [connectionforge.Store] and [connectionforge.Leaser] on SQLite or PostgreSQL, through Ent. |
|
ent/schema
Package schema is the Ent schema of connectionforge's SQL store.
|
Package schema is the Ent schema of connectionforge's SQL store. |
|
store
|
|
|
memstore
Package memstore is an in-memory [connectionforge.Store] and [connectionforge.Leaser].
|
Package memstore is an in-memory [connectionforge.Store] and [connectionforge.Leaser]. |
|
storetest
Package storetest is the conformance suite every [connectionforge.Store] and [connectionforge.Leaser] must pass.
|
Package storetest is the conformance suite every [connectionforge.Store] and [connectionforge.Leaser] must pass. |