People → Sign-in shows single sign-on, Active Directory and SCIM, and the directory's last sync. E sets up Entra ID from the tenant and client IDs; D sets up Active Directory, tried from the control plane first: each server bound, people who may sign in, members of each mapped group. The secrets go into the store as store:auth/..., sealed by the master key, and the CA into the settings, so no control plane needs a file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/secrets"
|
|
)
|
|
|
|
// ClusterSecret returns the named secret, creating a random one the first time
|
|
// it is asked for.
|
|
//
|
|
// Insert-if-absent and then read, so two control planes asking at once end up
|
|
// with the same value: the loser's insert changes nothing and it reads the
|
|
// winner's.
|
|
func (s *DB) ClusterSecret(ctx context.Context, name string) (string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
fresh := hex.EncodeToString(b)
|
|
stored := fresh
|
|
if s.sealer != nil {
|
|
var err error
|
|
if stored, err = s.sealer.Seal(ctx, []byte(fresh)); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if _, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO cluster_secrets (name, value, created_at) VALUES (?, ?, ?)
|
|
ON CONFLICT (name) DO NOTHING`,
|
|
name, stored, time.Now().UnixMilli()); err != nil {
|
|
return "", err
|
|
}
|
|
var v string
|
|
if err := s.db.QueryRowContext(ctx,
|
|
`SELECT value FROM cluster_secrets WHERE name = ?`, name).Scan(&v); err != nil {
|
|
return "", err
|
|
}
|
|
return s.openSecret(ctx, name, v)
|
|
}
|
|
|
|
// SetClusterSecret keeps value under name, sealed when a master key is set,
|
|
// in place of what was there: a secret an operator gives, such as the
|
|
// password the directory is read with, the same for every control plane.
|
|
func (s *DB) SetClusterSecret(ctx context.Context, name, value string) error {
|
|
stored := value
|
|
if s.sealer != nil {
|
|
var err error
|
|
if stored, err = s.sealer.Seal(ctx, []byte(value)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO cluster_secrets (name, value, created_at) VALUES (?, ?, ?)
|
|
ON CONFLICT (name) DO UPDATE SET value = excluded.value, created_at = excluded.created_at`,
|
|
name, stored, time.Now().UnixMilli())
|
|
return err
|
|
}
|
|
|
|
// StoredSecret is the named secret, ErrNotFound when none was kept; unlike
|
|
// ClusterSecret it never makes one.
|
|
func (s *DB) StoredSecret(ctx context.Context, name string) (string, error) {
|
|
var v string
|
|
err := s.db.QueryRowContext(ctx, `SELECT value FROM cluster_secrets WHERE name = ?`, name).Scan(&v)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return "", ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return s.openSecret(ctx, name, v)
|
|
}
|
|
|
|
// openSecret is a kept value in the clear.
|
|
func (s *DB) openSecret(ctx context.Context, name, v string) (string, error) {
|
|
if secrets.IsSealed(v) {
|
|
if s.sealer == nil {
|
|
return "", fmt.Errorf("the store's secret %q is sealed, and secrets.master_key is not set to open it", name)
|
|
}
|
|
plain, err := s.sealer.Open(ctx, v)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the store's secret %q: %w", name, err)
|
|
}
|
|
return string(plain), nil
|
|
}
|
|
// Kept plain from before a master key: sealed now, the value unchanged.
|
|
if s.sealer != nil {
|
|
if sealed, err := s.sealer.Seal(ctx, []byte(v)); err == nil {
|
|
_, _ = s.db.ExecContext(ctx, `UPDATE cluster_secrets SET value = ? WHERE name = ? AND value = ?`, sealed, name, v)
|
|
}
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// SetSealer seals the store's own secrets with s from now on; values kept
|
|
// plain are sealed as they are next read.
|
|
func (s *DB) SetSealer(x secrets.Sealer) { s.sealer = x }
|