Raw
  1package storage
  2
  3import (
  4	"context"
  5	"database/sql"
  6	"errors"
  7	"fmt"
  8	"time"
  9)
 10
 11const bootstrapHashLength = 32
 12
 13func (s *Store) EnsureGroups(ctx context.Context, names []string, actor string) ([]Group, error) {
 14	const operation = "ensure groups"
 15	if err := s.checkOpen(operation); err != nil {
 16		return nil, err
 17	}
 18	tx, err := s.sql.BeginTx(ctx, nil)
 19	if err != nil {
 20		return nil, classify("begin "+operation, err)
 21	}
 22	defer tx.Rollback()
 23	var created []Group
 24	for _, name := range names {
 25		group, made, err := s.ensureGroup(ctx, tx, operation, name, actor)
 26		if err != nil {
 27			return nil, err
 28		}
 29		if made {
 30			created = append(created, group)
 31		}
 32	}
 33	if err := tx.Commit(); err != nil {
 34		return nil, classify("commit "+operation, err)
 35	}
 36	return created, nil
 37}
 38
 39func (s *Store) ensureGroup(ctx context.Context, tx *sql.Tx, operation, name, actor string) (Group, bool, error) {
 40	group, err := s.prepareGroup(operation, CreateGroupInput{Name: name})
 41	if err != nil {
 42		return Group{}, false, err
 43	}
 44	existing, err := scanGroup(tx.QueryRowContext(ctx, `SELECT id, name, description, gid_number, created_at FROM groups WHERE name = ?`, group.Name))
 45	if err == nil {
 46		return existing, false, nil
 47	}
 48	if !errors.Is(err, sql.ErrNoRows) {
 49		return Group{}, false, classify(operation, err)
 50	}
 51	group, err = insertGroup(ctx, tx, operation, group)
 52	if err != nil {
 53		return Group{}, false, err
 54	}
 55	if _, err := appendAudit(ctx, tx, operation, AppendAuditInput{
 56		Actor:   actor,
 57		Action:  "group.create",
 58		Subject: group.Name,
 59		Detail:  fmt.Sprintf(`{"gid_number":%d}`, group.GIDNumber),
 60	}, group.CreatedAt); err != nil {
 61		return Group{}, false, err
 62	}
 63	return group, true, nil
 64}
 65
 66func (s *Store) ReconcileBootstrap(ctx context.Context, tokenHash []byte, expiresAt time.Time) (bool, error) {
 67	const operation = "reconcile bootstrap"
 68	if err := s.checkOpen(operation); err != nil {
 69		return false, err
 70	}
 71	if len(tokenHash) != bootstrapHashLength {
 72		return false, invalid(operation, fmt.Errorf("bootstrap token hash is not %d bytes", bootstrapHashLength))
 73	}
 74	now := canonicalTime(s.now())
 75	expires := canonicalTime(expiresAt)
 76	if !expires.After(now) {
 77		return false, invalid(operation, fmt.Errorf("expiry is not after now"))
 78	}
 79	tx, err := s.sql.BeginTx(ctx, nil)
 80	if err != nil {
 81		return false, classify("begin "+operation, err)
 82	}
 83	defer tx.Rollback()
 84	users, err := countUsers(ctx, tx, operation)
 85	if err != nil {
 86		return false, err
 87	}
 88	issued := users == 0
 89	if issued {
 90		if _, err := tx.ExecContext(ctx, `UPDATE settings SET bootstrap_token_hash = ?, bootstrap_expires_at = ? WHERE id = 1`, append([]byte(nil), tokenHash...), timeToInt(expires)); err != nil {
 91			return false, classify(operation, err)
 92		}
 93		if _, err := appendAudit(ctx, tx, operation, AppendAuditInput{
 94			Actor:   "system",
 95			Action:  "bootstrap.issue",
 96			Subject: "bootstrap",
 97			Detail:  fmt.Sprintf(`{"expires_at":%q}`, expires.Format(time.RFC3339)),
 98		}, now); err != nil {
 99			return false, err
100		}
101	} else if err := clearBootstrap(ctx, tx, operation, now); err != nil {
102		return false, err
103	}
104	if err := tx.Commit(); err != nil {
105		return false, classify("commit "+operation, err)
106	}
107	return issued, nil
108}
109
110func (s *Store) ClearBootstrapIfUsers(ctx context.Context) (bool, error) {
111	const operation = "clear bootstrap"
112	if err := s.checkOpen(operation); err != nil {
113		return false, err
114	}
115	tx, err := s.sql.BeginTx(ctx, nil)
116	if err != nil {
117		return false, classify("begin "+operation, err)
118	}
119	defer tx.Rollback()
120	users, err := countUsers(ctx, tx, operation)
121	if err != nil {
122		return false, err
123	}
124	if users == 0 {
125		return false, nil
126	}
127	if err := clearBootstrap(ctx, tx, operation, canonicalTime(s.now())); err != nil {
128		return false, err
129	}
130	if err := tx.Commit(); err != nil {
131		return false, classify("commit "+operation, err)
132	}
133	return true, nil
134}
135
136func clearBootstrap(ctx context.Context, tx *sql.Tx, operation string, now time.Time) error {
137	result, err := tx.ExecContext(ctx, `UPDATE settings SET bootstrap_token_hash = NULL, bootstrap_expires_at = NULL WHERE id = 1 AND bootstrap_token_hash IS NOT NULL`)
138	if err != nil {
139		return classify(operation, err)
140	}
141	changed, err := result.RowsAffected()
142	if err != nil {
143		return classify(operation+" rows affected", err)
144	}
145	if changed == 0 {
146		return nil
147	}
148	_, err = appendAudit(ctx, tx, operation, AppendAuditInput{Actor: "system", Action: "bootstrap.invalidate", Subject: "bootstrap"}, now)
149	return err
150}
151
152func countUsers(ctx context.Context, q execer, operation string) (int, error) {
153	var users int
154	if err := q.QueryRowContext(ctx, `SELECT count(*) FROM users`).Scan(&users); err != nil {
155		return 0, classify(operation+" count users", err)
156	}
157	return users, nil
158}