targets

package
v0.1.0 Latest Latest
Warning

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

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

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

View Source
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

func (g Grant) Validate() error

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

func (m *MemoryStore) CreateTarget(_ context.Context, t Target) (Target, error)

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

func (m *MemoryStore) GetTarget(_ context.Context, id string) (Target, error)

GetTarget returns a target by id (including WrappedCreds for worker use).

func (*MemoryStore) ListGrants

func (m *MemoryStore) ListGrants(_ context.Context, targetID string) ([]Grant, error)

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

func (m *MemoryStore) MayUse(_ context.Context, userID, spaceID, targetID string) (bool, error)

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

func (m *MemoryStore) ReplaceGrants(_ context.Context, targetID string, grants []Grant) error

ReplaceGrants sets a target's whole audience in one write.

func (*MemoryStore) UpdateTarget

func (m *MemoryStore) UpdateTarget(_ context.Context, t Target) (Target, error)

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

type PlainCreds struct {
	AccessKeyID     string
	SecretAccessKey string
}

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".

func (PlainCreds) Empty

func (c PlainCreds) Empty() bool

Empty reports whether the pair is unset.

type PublicView

type PublicView struct {
	ID   string
	Name string
}

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.

const (
	// RoleBackup writes snapshots and reads them back. Backup and restore runs
	// use it.
	RoleBackup Role = "backup"
	// RoleMaintenance expires snapshots and reclaims their storage. Only prune
	// runs use it.
	RoleMaintenance Role = "maintenance"
)

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

func (s *StateStore) CreateTarget(ctx context.Context, t Target) (Target, error)

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

func (s *StateStore) GetTarget(ctx context.Context, id string) (Target, error)

GetTarget returns a target by id (including WrappedCreds for worker use).

func (*StateStore) ListGrants

func (s *StateStore) ListGrants(ctx context.Context, targetID string) ([]Grant, error)

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

func (s *StateStore) MayUse(ctx context.Context, userID, spaceID, targetID string) (bool, error)

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

func (s *StateStore) ReplaceGrants(ctx context.Context, targetID string, grants []Grant) error

ReplaceGrants sets a target's whole audience in a single append.

func (*StateStore) UpdateTarget

func (s *StateStore) UpdateTarget(ctx context.Context, t Target) (Target, error)

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

Jump to

Keyboard shortcuts

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