package storage import ( "context" "database/sql" "errors" "fmt" "time" ) const bootstrapHashLength = 32 func (s *Store) EnsureGroups(ctx context.Context, names []string, actor string) ([]Group, error) { const operation = "ensure groups" if err := s.checkOpen(operation); err != nil { return nil, err } tx, err := s.sql.BeginTx(ctx, nil) if err != nil { return nil, classify("begin "+operation, err) } defer tx.Rollback() var created []Group for _, name := range names { group, made, err := s.ensureGroup(ctx, tx, operation, name, actor) if err != nil { return nil, err } if made { created = append(created, group) } } if err := tx.Commit(); err != nil { return nil, classify("commit "+operation, err) } return created, nil } func (s *Store) ensureGroup(ctx context.Context, tx *sql.Tx, operation, name, actor string) (Group, bool, error) { group, err := s.prepareGroup(operation, CreateGroupInput{Name: name}) if err != nil { return Group{}, false, err } existing, err := scanGroup(tx.QueryRowContext(ctx, `SELECT id, name, description, gid_number, created_at FROM groups WHERE name = ?`, group.Name)) if err == nil { return existing, false, nil } if !errors.Is(err, sql.ErrNoRows) { return Group{}, false, classify(operation, err) } group, err = insertGroup(ctx, tx, operation, group) if err != nil { return Group{}, false, err } if _, err := appendAudit(ctx, tx, operation, AppendAuditInput{ Actor: actor, Action: "group.create", Subject: group.Name, Detail: fmt.Sprintf(`{"gid_number":%d}`, group.GIDNumber), }, group.CreatedAt); err != nil { return Group{}, false, err } return group, true, nil } func (s *Store) ReconcileBootstrap(ctx context.Context, tokenHash []byte, expiresAt time.Time) (bool, error) { const operation = "reconcile bootstrap" if err := s.checkOpen(operation); err != nil { return false, err } if len(tokenHash) != bootstrapHashLength { return false, invalid(operation, fmt.Errorf("bootstrap token hash is not %d bytes", bootstrapHashLength)) } now := canonicalTime(s.now()) expires := canonicalTime(expiresAt) if !expires.After(now) { return false, invalid(operation, fmt.Errorf("expiry is not after now")) } tx, err := s.sql.BeginTx(ctx, nil) if err != nil { return false, classify("begin "+operation, err) } defer tx.Rollback() users, err := countUsers(ctx, tx, operation) if err != nil { return false, err } issued := users == 0 if issued { 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 { return false, classify(operation, err) } if _, err := appendAudit(ctx, tx, operation, AppendAuditInput{ Actor: "system", Action: "bootstrap.issue", Subject: "bootstrap", Detail: fmt.Sprintf(`{"expires_at":%q}`, expires.Format(time.RFC3339)), }, now); err != nil { return false, err } } else if err := clearBootstrap(ctx, tx, operation, now); err != nil { return false, err } if err := tx.Commit(); err != nil { return false, classify("commit "+operation, err) } return issued, nil } func (s *Store) ClearBootstrapIfUsers(ctx context.Context) (bool, error) { const operation = "clear bootstrap" if err := s.checkOpen(operation); err != nil { return false, err } tx, err := s.sql.BeginTx(ctx, nil) if err != nil { return false, classify("begin "+operation, err) } defer tx.Rollback() users, err := countUsers(ctx, tx, operation) if err != nil { return false, err } if users == 0 { return false, nil } if err := clearBootstrap(ctx, tx, operation, canonicalTime(s.now())); err != nil { return false, err } if err := tx.Commit(); err != nil { return false, classify("commit "+operation, err) } return true, nil } func clearBootstrap(ctx context.Context, tx *sql.Tx, operation string, now time.Time) error { 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`) if err != nil { return classify(operation, err) } changed, err := result.RowsAffected() if err != nil { return classify(operation+" rows affected", err) } if changed == 0 { return nil } _, err = appendAudit(ctx, tx, operation, AppendAuditInput{Actor: "system", Action: "bootstrap.invalidate", Subject: "bootstrap"}, now) return err } func countUsers(ctx context.Context, q execer, operation string) (int, error) { var users int if err := q.QueryRowContext(ctx, `SELECT count(*) FROM users`).Scan(&users); err != nil { return 0, classify(operation+" count users", err) } return users, nil }