rotate: safety-spine scaffold + mandatory backup gate (NO rotation)
Design-phase foundation for v2 rotation. Changes NO credential at any service. - internal/rotate: Rotator interface + registry + PlanAll (zero drivers registered, so every credential plans as "(none)") - internal/rotate.Snapshot: the MANDATORY backup gate — seals the gopass prefix into an authenticated bundle, re-opens it, and confirms the entry count matches before returning; any failure blocks rotation - internal/sink.CountBundleRecords: read-only bundle completeness check - cmd: `incredigo rotate` runs the backup gate + prints the plan; it is dry-run only and refuses --execute (design phase) - tests: backup gate happy path, no-overwrite, tamper detection; race-clean Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Package rotate is the credential-rotation scaffold.
|
||||
//
|
||||
// IMPORTANT — DESIGN PHASE: no Rotator that actually changes a credential at a
|
||||
// service is implemented. The interface, registry, planning, and the mandatory
|
||||
// backup gate (see backup.go and docs/ROTATION.md) exist so rotation can be built
|
||||
// safely later. Today there are zero registered drivers, so PlanAll reports
|
||||
// "(none)" for every credential and nothing can be rotated.
|
||||
package rotate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"incredigo/internal/discover"
|
||||
"incredigo/internal/vault"
|
||||
)
|
||||
|
||||
// Rotator changes one class of credential at its issuing authority.
|
||||
//
|
||||
// Contract (docs/ROTATION.md §5): Rotate mints and returns a NEW secret but MUST
|
||||
// NOT revoke the old one. RevokeOld runs only after the new value has been
|
||||
// verified against the service AND stored in gopass. A backup (see Snapshot) must
|
||||
// already exist before Rotate is ever called.
|
||||
type Rotator interface {
|
||||
Name() string
|
||||
// Detect reports whether this driver handles the given credential.
|
||||
Detect(c discover.Credential) bool
|
||||
// Rotate mints a new secret at the provider and returns a vault handle to it.
|
||||
// It must not revoke or invalidate the old credential.
|
||||
Rotate(ctx context.Context, c discover.Credential, v *vault.Vault) (*vault.Handle, error)
|
||||
// Verify proves the newly minted secret actually authenticates.
|
||||
Verify(ctx context.Context, newSecret *vault.Handle, v *vault.Vault) error
|
||||
// RevokeOld retires the previous credential — only after verify + store.
|
||||
RevokeOld(ctx context.Context, c discover.Credential) error
|
||||
}
|
||||
|
||||
var (
|
||||
regMu sync.Mutex
|
||||
registry []Rotator
|
||||
)
|
||||
|
||||
// Register adds a rotation driver. (No real driver registers yet.)
|
||||
func Register(r Rotator) {
|
||||
regMu.Lock()
|
||||
defer regMu.Unlock()
|
||||
registry = append(registry, r)
|
||||
}
|
||||
|
||||
// Drivers returns the registered rotators, sorted by name.
|
||||
func Drivers() []Rotator {
|
||||
regMu.Lock()
|
||||
defer regMu.Unlock()
|
||||
out := append([]Rotator(nil), registry...)
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
||||
return out
|
||||
}
|
||||
|
||||
// Plan is the non-destructive decision for one credential: which driver would
|
||||
// handle it, or none.
|
||||
type Plan struct {
|
||||
Credential discover.Credential
|
||||
Driver string // "" when no registered driver claims the credential
|
||||
}
|
||||
|
||||
// PlanAll classifies credentials against the registered drivers WITHOUT changing
|
||||
// anything. With no drivers registered (the current state) every Plan.Driver is "".
|
||||
func PlanAll(creds []discover.Credential) []Plan {
|
||||
drivers := Drivers()
|
||||
plans := make([]Plan, 0, len(creds))
|
||||
for _, c := range creds {
|
||||
p := Plan{Credential: c}
|
||||
for _, d := range drivers {
|
||||
if d.Detect(c) {
|
||||
p.Driver = d.Name()
|
||||
break
|
||||
}
|
||||
}
|
||||
plans = append(plans, p)
|
||||
}
|
||||
return plans
|
||||
}
|
||||
Reference in New Issue
Block a user