package inventory import ( "context" "fmt" "time" ) // An ask for a module's own secret at a desk (novox/hq ADR 0277, migration 0091): every one is recorded // before the prompt opens, and the bounds are read from the record. A verb may ask the operator to type a // secret; it may not keep a prompt in front of them. **One open ask per secret, and few per hour.** // The bounds of asking for one secret. const ( // SecretAskOpenFor is how long an ask that has not ended counts as open: longer than any prompt waits, // so a process that died with its prompt does not hold the secret for ever. SecretAskOpenFor = 2 * time.Minute // SecretAsksPerHour is how many asks for one secret an hour takes. SecretAsksPerHour = 3 ) // OpenSecretAsk records that an ask for a module's own secret on a machine opens at a desk, or refuses it in // words when one is still open for that secret or the hour's asks are spent. It answers the record's id, which // EndSecretAsk closes. func (i *Inventory) OpenSecretAsk(ctx context.Context, node, module, name, askedBy, desk string) (int64, error) { record, err := i.NodeByName(ctx, node) if err != nil { return 0, err } tx, err := i.store.Pool().Begin(ctx) if err != nil { return 0, err } defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }() // Serialised per secret, so two asks at once do not both pass the count. if _, err := tx.Exec(ctx, `select pg_advisory_xact_lock(hashtext($1))`, node+"/"+module+"/"+name); err != nil { return 0, err } var open int var openBy string if err := tx.QueryRow(ctx, `select count(*), coalesce(min(asked_by), '') from secret_ask where node = $1 and module = $2 and name = $3 and ended_at is null and opened_at > now() - $4::interval`, record.ID, module, name, SecretAskOpenFor.String()).Scan(&open, &openBy); err != nil { return 0, err } if open > 0 { return 0, fmt.Errorf("an ask for %s of %s on %s is still open (asked by %s): one prompt at a time for a secret, "+ "and this one ends within %s", name, module, node, openBy, SecretAskOpenFor) } var lastHour int if err := tx.QueryRow(ctx, `select count(*) from secret_ask where node = $1 and module = $2 and name = $3 and opened_at > now() - interval '1 hour'`, record.ID, module, name).Scan(&lastHour); err != nil { return 0, err } if lastHour >= SecretAsksPerHour { return 0, fmt.Errorf("%s of %s on %s was asked for %d times in the last hour, and an hour takes %d asks for one "+ "secret: the operator is not kept at a prompt", name, module, node, lastHour, SecretAsksPerHour) } var id int64 if err := tx.QueryRow(ctx, `insert into secret_ask (node, module, name, asked_by, desk) values ($1, $2, $3, $4, $5) returning id`, record.ID, module, name, askedBy, desk).Scan(&id); err != nil { return 0, err } return id, tx.Commit(ctx) } // EndSecretAsk closes an ask with how it ended: given, dismissed, timed-out, empty, refused or failed. func (i *Inventory) EndSecretAsk(ctx context.Context, id int64, outcome string) error { _, err := i.store.Pool().Exec(ctx, `update secret_ask set ended_at = now(), outcome = $2 where id = $1 and ended_at is null`, id, outcome) return err } // SecretAsk is one recorded ask for a module's own secret. type SecretAsk struct { ID int64 Node string Module string Name string AskedBy string Desk string OpenedAt time.Time EndedAt *time.Time Outcome string } // SecretAsks is every ask for secrets since a moment, newest first. func (i *Inventory) SecretAsks(ctx context.Context, since time.Time) ([]SecretAsk, error) { rows, err := i.store.Pool().Query(ctx, `select a.id, n.name, a.module, a.name, a.asked_by, a.desk, a.opened_at, a.ended_at, coalesce(a.outcome, '') from secret_ask a join node n on n.id = a.node where a.opened_at >= $1 order by a.opened_at desc`, since) if err != nil { return nil, err } defer rows.Close() var out []SecretAsk for rows.Next() { var a SecretAsk if err := rows.Scan(&a.ID, &a.Node, &a.Module, &a.Name, &a.AskedBy, &a.Desk, &a.OpenedAt, &a.EndedAt, &a.Outcome); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() }