package inventory import ( "context" "github.com/novox/mesh-control/internal/secrets" ) // Where sealed secrets live. // // The table holds nothing usable — see the migration and internal/secrets for why that is the // design rather than an inconvenience. // Secret is one provision's credential, sealed to each end. type Secret struct { Name string Consumer string Provider string ForConsumer string ForProvider string ConsumerKey string ProviderKey string } // SecretFor is the credential for one provision between two nodes, making one the first time. // // **Made once and kept**, rather than regenerated whenever it is asked for. A secret that changed // on every declaration would restart both ends on every push and would mean the password a // provider was told to create never matches the one a consumer was given — which is a mesh that // reports success and cannot connect. // // **Remade when either end's sealing key changes.** A node that rejoined generated a new key and // can no longer open what was sealed to the old one, so keeping the blob would deliver something // unreadable for ever. The new secret reaches both ends in the same push, which is the only // moment they can be changed together. func (i *Inventory) SecretFor(ctx context.Context, name, consumer, provider string) (Secret, error) { consumerKey, err := i.SealingKeyOf(ctx, consumer) if err != nil { return Secret{}, err } providerKey, err := i.SealingKeyOf(ctx, provider) if err != nil { return Secret{}, err } consumerNode, err := i.NodeByName(ctx, consumer) if err != nil { return Secret{}, err } providerNode, err := i.NodeByName(ctx, provider) if err != nil { return Secret{}, err } var held Secret err = i.store.Pool().QueryRow(ctx, `select for_consumer, for_provider, consumer_key, provider_key from secret where name = $1 and consumer = $2 and provider = $3`, name, consumerNode.ID, providerNode.ID). Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey) if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey { held.Name, held.Consumer, held.Provider = name, consumer, provider return held, nil } made, err := secrets.Make(consumerKey, providerKey) if err != nil { return Secret{}, err } _, err = i.store.Pool().Exec(ctx, `insert into secret (name, consumer, provider, for_consumer, for_provider, consumer_key, provider_key) values ($1, $2, $3, $4, $5, $6, $7) on conflict (name, consumer, provider) do update set for_consumer = excluded.for_consumer, for_provider = excluded.for_provider, consumer_key = excluded.consumer_key, provider_key = excluded.provider_key, created_at = now()`, name, consumerNode.ID, providerNode.ID, made.ForConsumer, made.ForProvider, made.ConsumerKey, made.ProviderKey) if err != nil { return Secret{}, err } return Secret{Name: name, Consumer: consumer, Provider: provider, ForConsumer: made.ForConsumer, ForProvider: made.ForProvider, ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey}, nil } // RotateSecret discards what was there, so the next declaration carries a new one. // // Only a delete. Nothing reads the old value first, because nothing can — and making the // replacement here rather than on the next read would be a second path to the same act, which is // how two ends come to hold different passwords. // // The new secret then reaches both ends on the same push, together, which is what makes rotation // a single event rather than a fanout with a window where half the mesh holds a dead credential. func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, provider string) error { consumerNode, err := i.NodeByName(ctx, consumer) if err != nil { return err } providerNode, err := i.NodeByName(ctx, provider) if err != nil { return err } _, err = i.store.Pool().Exec(ctx, `delete from secret where name = $1 and consumer = $2 and provider = $3`, name, consumerNode.ID, providerNode.ID) return err } // SecretsFrom is every credential a provider node was issued, so it can be told what to create. func (i *Inventory) SecretsFrom(ctx context.Context, provider string) ([]Secret, error) { providerNode, err := i.NodeByName(ctx, provider) if err != nil { return nil, err } rows, err := i.store.Pool().Query(ctx, `select s.name, c.name, s.for_provider from secret s join node c on c.id = s.consumer where s.provider = $1 order by s.name, c.name`, providerNode.ID) if err != nil { return nil, err } defer rows.Close() var out []Secret for rows.Next() { s := Secret{Provider: provider} if err := rows.Scan(&s.Name, &s.Consumer, &s.ForProvider); err != nil { return nil, err } out = append(out, s) } return out, rows.Err() }