Documentation
¶
Overview ¶
In-memory Store + Authorizer for the target/grant model. The service runs the state-backed store (state.go); this stays as the test double and the behaviour reference the contract tests hold both implementations to.
It upholds the least-disclosure rule: VisibleTargets returns only PublicView projections ({id,name}); credentials never leave the store (decisions.md #12/#14). Grant evaluation is entirely server-side — the caller passes their OpenCloud user id and the spaces they belong to (derived from CS3 membership), never a client-supplied target id.
Package targets is the in-app, admin-managed backup-target store and its access model (decisions.md #12–#15).
An OpenCloud admin creates S3 targets and grants each to all users or specific users/spaces. End users see and use only the targets granted to them.
Hard rules from decisions.md #14 and AGENTS.md — enforced by this package's shape:
- Target S3 credentials are key-class. They are stored only as TW-wrapped ciphertext (WrappedCreds), never persisted or returned in plaintext.
- Credentials are write-only across the API: no read path returns them. Accordingly, the read type (Target) carries no plaintext credentials at all; only the sealed blob is stored, and it is opened solely in worker memory at run time via CredSealer.
- Never log credentials or the TW key.
This file defines value types and boundary interfaces only. Storage (state.go), at-rest crypto (credsealer.go, reusing the key-envelope primitive), optional first-start seeding (bootstrap.go) and worker use all exist. The admin HTTP surface for creating targets and granting them does not — it lands with the UI in Phase 8, and until then the only writer is the seeder.
Index ¶
- Constants
- func Bootstrap(ctx context.Context, store Store, sealer CredSealer, cfg BootstrapConfig) (bool, error)
- type Authorizer
- type BootstrapConfig
- type BootstrapEnv
- type CredSealer
- type CredentialSet
- type Grant
- type GrantScope
- type MemoryStore
- func (m *MemoryStore) CreateTarget(_ context.Context, t Target) (Target, error)
- func (m *MemoryStore) DeleteGrant(_ context.Context, g Grant) error
- func (m *MemoryStore) DeleteTarget(_ context.Context, id string) error
- func (m *MemoryStore) GetTarget(_ context.Context, id string) (Target, error)
- func (m *MemoryStore) ListGrants(_ context.Context, targetID string) ([]Grant, error)
- func (m *MemoryStore) ListTargets(_ context.Context) ([]Target, error)
- func (m *MemoryStore) MayUse(_ context.Context, userID, spaceID, targetID string) (bool, error)
- func (m *MemoryStore) PutGrant(_ context.Context, g Grant) error
- func (m *MemoryStore) ReplaceGrants(_ context.Context, targetID string, grants []Grant) error
- func (m *MemoryStore) UpdateTarget(_ context.Context, t Target) (Target, error)
- func (m *MemoryStore) VisibleTargets(_ context.Context, userID string, spaceIDs []string) ([]PublicView, error)
- type NotFoundError
- type PlainCreds
- type PublicView
- type Role
- type StateStore
- func (s *StateStore) CreateTarget(ctx context.Context, t Target) (Target, error)
- func (s *StateStore) DeleteGrant(ctx context.Context, g Grant) error
- func (s *StateStore) DeleteTarget(ctx context.Context, id string) error
- func (s *StateStore) GetTarget(ctx context.Context, id string) (Target, error)
- func (s *StateStore) ListGrants(ctx context.Context, targetID string) ([]Grant, error)
- func (s *StateStore) ListTargets(ctx context.Context) ([]Target, error)
- func (s *StateStore) MayUse(ctx context.Context, userID, spaceID, targetID string) (bool, error)
- func (s *StateStore) PutGrant(ctx context.Context, g Grant) error
- func (s *StateStore) ReplaceGrants(ctx context.Context, targetID string, grants []Grant) error
- func (s *StateStore) UpdateTarget(ctx context.Context, t Target) (Target, error)
- func (s *StateStore) VisibleTargets(ctx context.Context, userID string, spaceIDs []string) ([]PublicView, error)
- type Store
- type Target
- type UnreadableTargetsError
Constants ¶
const DefaultBootstrapID = "default"
DefaultBootstrapID is used when BootstrapConfig.ID is empty.
Variables ¶
This section is empty.
Functions ¶
func Bootstrap ¶
func Bootstrap(ctx context.Context, store Store, sealer CredSealer, cfg BootstrapConfig) (bool, error)
Bootstrap seeds a single default target and grants it to all users, but only when seeding is enabled and the store holds no targets yet. It reports whether a target was created.
Types ¶
type Authorizer ¶
type Authorizer interface {
// VisibleTargets returns the targets granted to the user, given the spaces
// they are a member of. Returns least-disclosure projections.
VisibleTargets(ctx context.Context, userID string, spaceIDs []string) ([]PublicView, error)
// MayUse reports whether the user may use target targetID for space spaceID.
MayUse(ctx context.Context, userID, spaceID, targetID string) (bool, error)
}
Authorizer answers what a given user may see and use. All grant checks are server-side; client-supplied target IDs are never trusted (decisions.md #12, phase-2 authorization rule).
type BootstrapConfig ¶
type BootstrapConfig struct {
// Enable gates the whole operation; false is the safe default.
Enable bool
// ID is the stable identifier of the seeded target.
ID string
Name string
Endpoint string
Region string
Bucket string
Prefix string
UsePathStyle bool
DisableTLS bool
// Creds are the S3 credentials a backup run uses. They are sealed
// immediately and never retained in this struct beyond the call.
Creds PlainCreds
// MaintenanceCreds are the credentials a prune run uses. Optional: left
// empty, both roles use Creds, which is the single-key deployment.
MaintenanceCreds PlainCreds
}
BootstrapConfig describes the optional default target. Credentials come from a Secret, the rest from non-secret configuration.
func (BootstrapConfig) Validate ¶
func (c BootstrapConfig) Validate() error
Validate reports what an enabled seeding is missing. It says nothing about the credentials beyond whether they are there.
type BootstrapEnv ¶
type BootstrapEnv struct {
Enable bool `env:"BOOTSTRAP_ENABLE" doc:"Seed the target below on start, if no target exists yet. Needs TW_KEY."`
TargetID string `env:"BOOTSTRAP_TARGET_ID" doc:"Id of the seeded target. Unset, a fixed default id."`
TargetName string `env:"BOOTSTRAP_TARGET_NAME" doc:"Display name of the seeded target."`
Endpoint string `env:"BOOTSTRAP_S3_ENDPOINT" doc:"S3 endpoint, host:port or URL."`
Region string `env:"BOOTSTRAP_S3_REGION" doc:"S3 region."`
Bucket string `env:"BOOTSTRAP_S3_BUCKET" doc:"S3 bucket."`
Prefix string `env:"BOOTSTRAP_S3_PREFIX" doc:"Prefix inside the bucket."`
UsePathStyle bool `env:"BOOTSTRAP_S3_USE_PATH_STYLE" doc:"Path-style addressing (Garage and most self-hosted S3 need it)."`
DisableTLS bool `env:"BOOTSTRAP_S3_DISABLE_TLS" doc:"Plain HTTP to the endpoint."`
AccessKeyID config.Secret `env:"BOOTSTRAP_S3_ACCESS_KEY_ID" doc:"Access key id backups write with."`
SecretAccessKey config.Secret `env:"BOOTSTRAP_S3_SECRET_ACCESS_KEY" doc:"Secret for that access key id."`
MaintenanceAccessKeyID config.Secret `` /* 134-byte string literal not displayed */
MaintenanceSecretAccessKey config.Secret `env:"BOOTSTRAP_S3_MAINTENANCE_SECRET_ACCESS_KEY" doc:"Secret for the maintenance access key id."`
}
BootstrapEnv is the optional default target, as the environment declares it (internal/config). It is seeded on first start, when no target exists yet, and never overrides what an admin configured.
func (BootstrapEnv) Config ¶
func (e BootstrapEnv) Config() BootstrapConfig
Config is the seeding configuration from e. The credentials must come from a Secret (decisions.md #14) and are handed straight to the sealer.
func (*BootstrapEnv) Validate ¶
func (e *BootstrapEnv) Validate() error
Validate refuses an enabled seeding that could not seed anything, at startup rather than after the service has claimed its instance record.
type CredSealer ¶
type CredSealer interface {
// Seal wraps a target's credentials for storage.
Seal(c CredentialSet) (wrapped []byte, version int, err error)
// Open unwraps stored credentials for immediate, in-memory use.
Open(wrapped []byte) (CredentialSet, error)
}
CredSealer seals and opens S3 credentials using the cluster/KMS Target-Wrap (TW) key (decisions.md #14). Seal happens on admin write; Open happens only in worker memory at run time. The implementation reuses the maintained AEAD-envelope primitive from pkg/keys — no hand-rolled crypto.
func NewCredSealer ¶
func NewCredSealer(twKey []byte) (CredSealer, error)
NewCredSealer builds a CredSealer from the 32-byte cluster/KMS Target Wrap key. The key is copied; the caller may zeroize its own buffer.
type CredentialSet ¶
type CredentialSet struct {
Backup PlainCreds
Maintenance PlainCreds
}
CredentialSet is a target's credentials, one pair per Role.
Maintenance is optional: a target configured with a single credential leaves it empty and both roles resolve to Backup. That is the shape every target written before roles existed has, and it stays valid — a deployment with one key is not misconfigured, it is just not separated.
What separating them does and does not buy ¶
On a backend that can express "may write, may not delete" the separation is a bound: a leaked backup credential cannot destroy history. Garage — the target this project ships against — cannot express it. Its grants are read, write and owner; write includes DeleteObject, owner covers administering the bucket and grants no object access at all, and a key without read cannot open an encrypted repository. Both roles therefore end up holding read+write, which is the same capability. TestGarageGrantMatrix pins this rather than asserting it from documentation.
So on Garage the separation is organisational: two keys that can be rotated and revoked independently, and an audit trail that distinguishes the actor that wrote a backup from the actor that deleted one. It is not a blast-radius bound, and the operations runbook says so in those words.
func (CredentialSet) For ¶
func (c CredentialSet) For(role Role) PlainCreds
For returns the credentials a run in the given role must use. An unconfigured maintenance pair falls back to the backup pair, which is the single-credential deployment.
func (CredentialSet) Separated ¶
func (c CredentialSet) Separated() bool
Separated reports whether the roles actually resolve to different keys. Used for diagnostics; it never decides anything.
type Grant ¶
type Grant struct {
TargetID string `json:"target_id"`
Scope GrantScope `json:"scope"`
// UserID is set when Scope == ScopeUser: an OpenCloud user id.
UserID string `json:"user_id,omitempty"`
// SpaceID is set when Scope == ScopeSpace.
SpaceID string `json:"space_id,omitempty"`
}
Grant binds a target to an audience. Exactly one of the scope-specific fields is set, per Scope.
func (Grant) Validate ¶
Validate reports whether a grant is well formed: a scope that was actually chosen, carrying exactly the field that scope defines.
A grant is an authorization decision, so a malformed one is refused rather than interpreted. In particular the zero Scope is not "all users": it is a caller that never said who this target is for.
type GrantScope ¶
type GrantScope int
GrantScope enumerates how a target is granted.
const ( // ScopeUnknown is the zero value and must not be persisted. ScopeUnknown GrantScope = iota // ScopeAllUsers grants a target to every authenticated user. ScopeAllUsers // ScopeUser grants a target to a specific user, by OpenCloud user id (the // graph `/me` id, which CS3 grants use; not the OIDC `sub`). ScopeUser // ScopeSpace grants a target to members of a specific space. ScopeSpace )
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is a concurrency-safe in-memory Store and Authorizer.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore constructs an empty MemoryStore.
func (*MemoryStore) CreateTarget ¶
CreateTarget persists a target (WrappedCreds already sealed by the caller).
func (*MemoryStore) DeleteGrant ¶
func (m *MemoryStore) DeleteGrant(_ context.Context, g Grant) error
DeleteGrant removes a grant.
func (*MemoryStore) DeleteTarget ¶
func (m *MemoryStore) DeleteTarget(_ context.Context, id string) error
DeleteTarget removes a target and its grants.
func (*MemoryStore) GetTarget ¶
GetTarget returns a target by id (including WrappedCreds for worker use).
func (*MemoryStore) ListGrants ¶
ListGrants returns the grants for a target.
func (*MemoryStore) ListTargets ¶
func (m *MemoryStore) ListTargets(_ context.Context) ([]Target, error)
ListTargets returns all targets (admin view).
func (*MemoryStore) MayUse ¶
MayUse reports whether the user may use targetID for spaceID. All checks are server-side; a client-supplied targetID that is not granted returns false.
func (*MemoryStore) PutGrant ¶
func (m *MemoryStore) PutGrant(_ context.Context, g Grant) error
PutGrant adds or replaces a grant.
func (*MemoryStore) ReplaceGrants ¶
ReplaceGrants sets a target's whole audience in one write.
func (*MemoryStore) UpdateTarget ¶
UpdateTarget updates metadata; nil WrappedCreds preserves stored credentials.
func (*MemoryStore) VisibleTargets ¶
func (m *MemoryStore) VisibleTargets(_ context.Context, userID string, spaceIDs []string) ([]PublicView, error)
VisibleTargets returns least-disclosure projections of the targets granted to the user, given the spaces they are a member of (server-side check).
type NotFoundError ¶
type NotFoundError struct{ ID string }
NotFoundError is returned when a target does not exist.
func (NotFoundError) Error ¶
func (e NotFoundError) Error() string
type PlainCreds ¶
PlainCreds are S3 credentials in plaintext. They exist only transiently: inbound on an admin create/update (write-only), or in worker memory after CredSealer.Open. Never persisted, never logged.
func (PlainCreds) Complete ¶
func (c PlainCreds) Complete() bool
Complete reports whether both halves of the pair are present. A half-filled pair is a configuration mistake, never a way of saying "not configured".
type PublicView ¶
PublicView is the least-disclosure projection returned to end users (decisions.md #12, phase-2 `GET /targets`): only what the UI needs to pick a target. No endpoint, bucket, or credentials.
type Role ¶
type Role string
Role names what a run intends to do to a target, so the two can be given different credentials (decisions.md #9, Tier 2).
The split is real in this service: a backup run never holds the maintenance credential and a maintenance run never holds the backup one. Whether it is also a *bound* depends entirely on the storage backend, and on the one this project ships against it is not — see the CredentialSet doc comment.
type StateStore ¶
type StateStore struct {
// contains filtered or unexported fields
}
StateStore is a Store and Authorizer backed by durable state.
func NewStateStore ¶
func NewStateStore(st state.Store) *StateStore
NewStateStore returns a Store persisting to st.
func (*StateStore) CreateTarget ¶
CreateTarget persists a target (WrappedCreds already sealed by the caller).
func (*StateStore) DeleteGrant ¶
func (s *StateStore) DeleteGrant(ctx context.Context, g Grant) error
DeleteGrant removes a grant.
func (*StateStore) DeleteTarget ¶
func (s *StateStore) DeleteTarget(ctx context.Context, id string) error
DeleteTarget removes a target and its grants.
func (*StateStore) GetTarget ¶
GetTarget returns a target by id (including WrappedCreds for worker use).
func (*StateStore) ListGrants ¶
ListGrants returns the grants for a target.
func (*StateStore) ListTargets ¶
func (s *StateStore) ListTargets(ctx context.Context) ([]Target, error)
ListTargets returns all targets (admin view).
A target record that cannot be decoded fails the listing with UnreadableTargetsError rather than dropping out of it. Every caller of the full list acts on what it does not see: TW rotation would leave that target sealed under the retiring key, and bootstrap would take a store whose one record is corrupt for an empty one (review-2026-10.md F2).
func (*StateStore) MayUse ¶
MayUse reports whether the user may use targetID for spaceID. All checks are server-side; a client-supplied targetID that is not granted returns false.
func (*StateStore) PutGrant ¶
func (s *StateStore) PutGrant(ctx context.Context, g Grant) error
PutGrant adds or replaces a grant.
func (*StateStore) ReplaceGrants ¶
ReplaceGrants sets a target's whole audience in a single append.
func (*StateStore) UpdateTarget ¶
UpdateTarget updates metadata; nil WrappedCreds preserves stored credentials, which is what makes "edit a target without re-entering its secret" possible while credentials stay write-only.
func (*StateStore) VisibleTargets ¶
func (s *StateStore) VisibleTargets(ctx context.Context, userID string, spaceIDs []string) ([]PublicView, error)
VisibleTargets returns least-disclosure projections of the targets granted to the user, given the spaces they are a member of (server-side check).
Unlike ListTargets it skips a target record that cannot be decoded: a member cannot use or repair it either way, and one corrupt record must not take the target list away from every member of every Space.
type Store ¶
type Store interface {
// CreateTarget persists a target (with its already-sealed WrappedCreds).
CreateTarget(ctx context.Context, t Target) (Target, error)
// UpdateTarget updates target metadata. If WrappedCreds is nil the stored
// credentials are left unchanged (supports "edit without re-entering creds",
// phase-8).
UpdateTarget(ctx context.Context, t Target) (Target, error)
// DeleteTarget removes a target and its grants.
DeleteTarget(ctx context.Context, id string) error
// GetTarget returns a target by ID (including WrappedCreds for worker use).
GetTarget(ctx context.Context, id string) (Target, error)
// ListTargets returns all targets (admin view).
ListTargets(ctx context.Context) ([]Target, error)
// PutGrant adds or replaces a grant.
PutGrant(ctx context.Context, g Grant) error
// DeleteGrant removes a grant.
DeleteGrant(ctx context.Context, g Grant) error
// ListGrants returns the grants for a target.
ListGrants(ctx context.Context, targetID string) ([]Grant, error)
// ReplaceGrants sets a target's whole audience in one write. The admin API
// edits grants as a set, and the durable store keeps them as one document
// per version, so applying such an edit as a sequence of PutGrant and
// DeleteGrant calls would leave a crash able to stop halfway through — with
// a grant list nobody chose. Passing an empty list revokes everything,
// which is a decision an admin can legitimately make and is therefore not
// the same as deleting the target.
ReplaceGrants(ctx context.Context, targetID string, grants []Grant) error
}
Store persists targets and grants. It stores credentials only as the sealed blob on Target.WrappedCreds and never exposes plaintext.
type Target ¶
type Target struct {
ID string `json:"id"`
Name string `json:"name"`
Endpoint string `json:"endpoint"`
Region string `json:"region,omitempty"`
Bucket string `json:"bucket"`
Prefix string `json:"prefix,omitempty"`
UsePathStyle bool `json:"use_path_style,omitempty"`
DisableTLS bool `json:"disable_tls,omitempty"`
// WrappedCreds is the TW-wrapped S3 credential blob (never plaintext, never
// logged, never returned by any read API). Versioned like the key envelope.
// It holds the target's whole CredentialSet — every Role — as one sealed
// record, so a rotation re-seals one blob rather than keeping several in
// step.
WrappedCreds []byte `json:"wrapped_creds,omitempty"`
CreatedAt time.Time `json:"created_at,omitzero"`
UpdatedAt time.Time `json:"updated_at,omitzero"`
// Version is the WrappedCreds envelope version (compatibility promise).
Version int `json:"version,omitempty"`
// MaintenanceConfigured records whether WrappedCreds holds a separate
// maintenance pair. It is metadata, not key material: a yes/no written at
// seal time from what the writer supplied.
//
// It exists because the admin UI must be able to show whether a target is
// credential-separated, and the only other way to answer that is to open
// the sealed blob — which the admin path may not do (decisions.md #14).
// Records written before this field read as false, which is the right
// answer for the single-credential deployment they describe.
MaintenanceConfigured bool `json:"maintenance_configured,omitempty"`
}
Target is an admin-managed S3 destination ("Buddy-S3"). It deliberately holds no plaintext credentials: the sealed credential blob lives in the store and is opened only at run time (decisions.md #14).
func (Target) Public ¶
func (t Target) Public() PublicView
Public returns the end-user-safe projection of a Target.
type UnreadableTargetsError ¶
type UnreadableTargetsError struct{ Keys []string }
UnreadableTargetsError is returned when stored target records cannot be decoded. It names the state keys, which are identifiers, never content.
func (UnreadableTargetsError) Error ¶
func (e UnreadableTargetsError) Error() string