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}