package console import ( "context" "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "io" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" ) // Request-local per-call effect adapters for the enabled create path. Each // backend or store call is its own durable effect under the request's // registration; the effect's permit is what lets the call through the // boundary handles beneath. Named private fields, every method explicit. type claimEffectContextKey struct{} type claimStoreEffects struct { Runner hostops.EffectRunner OperationID string } func claimEffectID() (string, error) { var b [16]byte if _, err := rand.Read(b[:]); err != nil { return "", err } return hex.EncodeToString(b[:]), nil } func (e claimStoreEffects) run(ctx context.Context, resource, action string, inputs any, call func(context.Context) (string, error)) (string, error) { id, err := claimEffectID() if err != nil { return "", err } body, err := json.Marshal(inputs) if err != nil { return "", err } return e.Runner.Run(ctx, hostops.Effect{ID: id, Resource: resource, Action: action, Inputs: map[string]string{"request": string(body)}}, call) } type claimEffectProvisioner struct { base objstore.Provisioner effects claimStoreEffects } var _ objstore.Provisioner = claimEffectProvisioner{} func (p claimEffectProvisioner) mutation(ctx context.Context, resource, action string, inputs any, call func(context.Context) error) error { _, err := p.effects.run(ctx, resource, action, inputs, func(ctx context.Context) (string, error) { if err := call(ctx); err != nil { return "", err } return "joined", nil }) return err } func (p claimEffectProvisioner) EnsureBucket(ctx context.Context, name string) (string, error) { return p.effects.run(ctx, "bucket:"+name, "ensure-bucket", name, func(ctx context.Context) (string, error) { return p.base.EnsureBucket(ctx, name) }) } func (p claimEffectProvisioner) EnsureUser(ctx context.Context, name, path, arn string, deny *objstore.DenyTag) (string, error) { return p.effects.run(ctx, "iam-user:"+name, "ensure-user", []any{name, path, arn, deny}, func(ctx context.Context) (string, error) { return p.base.EnsureUser(ctx, name, path, arn, deny) }) } func (p claimEffectProvisioner) MintKey(ctx context.Context, user string) (string, string, error) { var id, secret string _, err := p.effects.run(ctx, "iam-user:"+user, "mint-key", user, func(ctx context.Context) (string, error) { var callErr error id, secret, callErr = p.base.MintKey(ctx, user) return id, callErr }) // Only the key ID is journaled; the secret travels through the sealed // config walk, never through admission. if err != nil { return "", "", err } return id, secret, nil } func (p claimEffectProvisioner) DeleteKey(ctx context.Context, user, id string) error { return p.mutation(ctx, "iam-user:"+user, "delete-key", []string{user, id}, func(ctx context.Context) error { return p.base.DeleteKey(ctx, user, id) }) } func (p claimEffectProvisioner) ReapKeys(ctx context.Context, user string, keep []string) error { return p.mutation(ctx, "iam-user:"+user, "reap-keys", []any{user, keep}, func(ctx context.Context) error { return p.base.ReapKeys(ctx, user, keep) }) } func (p claimEffectProvisioner) EmptyBucket(ctx context.Context, name string) error { return p.mutation(ctx, "bucket:"+name, "empty-bucket", name, func(ctx context.Context) error { return p.base.EmptyBucket(ctx, name) }) } func (p claimEffectProvisioner) DeleteBucket(ctx context.Context, name string) error { return p.mutation(ctx, "bucket:"+name, "delete-bucket", name, func(ctx context.Context) error { return p.base.DeleteBucket(ctx, name) }) } func (p claimEffectProvisioner) DeleteUser(ctx context.Context, name string) error { return p.mutation(ctx, "iam-user:"+name, "delete-user", name, func(ctx context.Context) error { return p.base.DeleteUser(ctx, name) }) } // claimEffectStore wraps the data boundary for one request: every Put is an // effect. Delete is refused (the walk never needs a bare delete). type claimEffectStore struct { data release.Store effects claimStoreEffects } var ( _ release.Store = claimEffectStore{} _ release.ConditionalDeleter = claimEffectStore{} _ release.Opener = claimEffectStore{} ) func (s claimEffectStore) Get(ctx context.Context, key string) ([]byte, string, error) { return s.data.Get(ctx, key) } func (s claimEffectStore) List(ctx context.Context, prefix string) ([]string, error) { return s.data.List(ctx, prefix) } func (s claimEffectStore) ListDirs(ctx context.Context, prefix string) ([]string, error) { return s.data.ListDirs(ctx, prefix) } func (s claimEffectStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) { return release.OpenObject(ctx, s.data, key) } func (s claimEffectStore) Put(ctx context.Context, key string, body []byte, etag string) (string, error) { // Hash and coordinates only: config payloads can carry sealed values. sum := sha256.Sum256(body) return s.effects.run(ctx, "control-object:"+key, "put", map[string]string{"key": key, "expected_etag": etag, "sha256": hex.EncodeToString(sum[:])}, func(ctx context.Context) (string, error) { return s.data.Put(ctx, key, body, etag) }) } func (s claimEffectStore) Delete(context.Context, string) error { return hostops.ErrContract } func (s claimEffectStore) DeleteIfMatch(ctx context.Context, key, etag string) error { _, err := s.effects.run(ctx, "control-object:"+key, "delete", map[string]string{"key": key, "expected_etag": etag}, func(ctx context.Context) (string, error) { if err := release.DeleteIfMatch(ctx, s.data, key, etag); err != nil { return "", err } return "joined", nil }) return err } func claimStoreAdapters(ctx context.Context, store release.Store, p objstore.Provisioner) (release.Store, objstore.Provisioner) { e, ok := ctx.Value(claimEffectContextKey{}).(claimStoreEffects) if !ok { return store, p } var prov objstore.Provisioner if p != nil { prov = claimEffectProvisioner{base: p, effects: e} } return claimEffectStore{data: store, effects: e}, prov }