Raw
  1package storage
  2
  3import (
  4	"context"
  5	"database/sql"
  6	"errors"
  7	"fmt"
  8)
  9
 10type rowScanner interface {
 11	Scan(dest ...any) error
 12}
 13
 14type execer interface {
 15	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
 16	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
 17}
 18
 19const userColumns = `id, username, display_name, password_hash, disabled, uid_number, created_at, last_login_at, credentials_epoch`
 20
 21func (s *Store) CreateUser(ctx context.Context, input CreateUserInput) (User, error) {
 22	const operation = "create user"
 23	if err := s.checkOpen(operation); err != nil {
 24		return User{}, err
 25	}
 26	user, err := s.prepareUser(operation, input)
 27	if err != nil {
 28		return User{}, err
 29	}
 30	tx, err := s.sql.BeginTx(ctx, nil)
 31	if err != nil {
 32		return User{}, classify("begin "+operation, err)
 33	}
 34	defer tx.Rollback()
 35	user, err = insertUser(ctx, tx, operation, user)
 36	if err != nil {
 37		return User{}, err
 38	}
 39	if err := tx.Commit(); err != nil {
 40		return User{}, classify("commit "+operation, err)
 41	}
 42	return user, nil
 43}
 44
 45func (s *Store) prepareUser(operation string, input CreateUserInput) (User, error) {
 46	username, err := normalizeUsername(input.Username)
 47	if err != nil {
 48		return User{}, invalid(operation, err)
 49	}
 50	if err := validateDisplayName(input.DisplayName); err != nil {
 51		return User{}, invalid(operation, err)
 52	}
 53	if len(input.PasswordHash) == 0 {
 54		return User{}, invalid(operation, fmt.Errorf("password hash is empty"))
 55	}
 56	id, err := s.newID()
 57	if err != nil {
 58		return User{}, fmt.Errorf("%s: %w", operation, err)
 59	}
 60	return User{
 61		ID:           id,
 62		Username:     username,
 63		DisplayName:  input.DisplayName,
 64		PasswordHash: append([]byte(nil), input.PasswordHash...),
 65		Disabled:     input.Disabled,
 66		CreatedAt:    canonicalTime(s.now()),
 67	}, nil
 68}
 69
 70func insertUser(ctx context.Context, tx *sql.Tx, operation string, user User) (User, error) {
 71	var uid int
 72	if err := tx.QueryRowContext(ctx, `SELECT next_uid_number FROM settings WHERE id = 1`).Scan(&uid); err != nil {
 73		return User{}, classify("read next uid", err)
 74	}
 75	if uid < firstUID || uid > lastUID {
 76		return User{}, fmt.Errorf("%s: %w", operation, ErrUIDExhausted)
 77	}
 78	if _, err := tx.ExecContext(ctx, `UPDATE settings SET next_uid_number = ? WHERE id = 1`, uid+1); err != nil {
 79		return User{}, classify("advance uid", err)
 80	}
 81	if _, err := tx.ExecContext(ctx, `
 82INSERT INTO users (
 83    id, username, display_name, password_hash,
 84    disabled, uid_number, created_at, last_login_at
 85) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)`,
 86		user.ID, user.Username, user.DisplayName,
 87		user.PasswordHash, boolToInt(user.Disabled), uid,
 88		timeToInt(user.CreatedAt)); err != nil {
 89		return User{}, classify(operation, err)
 90	}
 91	if err := clearBootstrap(ctx, tx, operation, user.CreatedAt); err != nil {
 92		return User{}, err
 93	}
 94	user.UIDNumber = uid
 95	return user, nil
 96}
 97
 98func (s *Store) UserByID(ctx context.Context, id string) (User, error) {
 99	return s.userBy(ctx, "get user by id", `SELECT `+userColumns+` FROM users WHERE id = ?`, id)
100}
101
102func (s *Store) UserByUsername(ctx context.Context, username string) (User, error) {
103	const operation = "get user by username"
104	normalized, err := normalizeUsername(username)
105	if err != nil {
106		return User{}, invalid(operation, err)
107	}
108	return s.userBy(ctx, operation, `SELECT `+userColumns+` FROM users WHERE username = ?`, normalized)
109}
110
111func (s *Store) userBy(ctx context.Context, operation, query string, arg any) (User, error) {
112	if err := s.checkOpen(operation); err != nil {
113		return User{}, err
114	}
115	user, err := scanUser(s.sql.QueryRowContext(ctx, query, arg))
116	if errors.Is(err, sql.ErrNoRows) {
117		return User{}, fmt.Errorf("%s: %w", operation, ErrNotFound)
118	}
119	if err != nil {
120		return User{}, classify(operation, err)
121	}
122	return user, nil
123}
124
125func (s *Store) ListUsers(ctx context.Context) ([]User, error) {
126	const operation = "list users"
127	if err := s.checkOpen(operation); err != nil {
128		return nil, err
129	}
130	rows, err := s.sql.QueryContext(ctx, `SELECT `+userColumns+` FROM users ORDER BY username, id`)
131	if err != nil {
132		return nil, classify(operation, err)
133	}
134	defer rows.Close()
135	var users []User
136	for rows.Next() {
137		user, err := scanUser(rows)
138		if err != nil {
139			return nil, classify(operation, err)
140		}
141		users = append(users, user)
142	}
143	if err := rows.Err(); err != nil {
144		return nil, classify(operation, err)
145	}
146	return users, nil
147}
148
149func (s *Store) UpdateUser(ctx context.Context, id string, input UpdateUserInput) (User, error) {
150	const operation = "update user"
151	if err := s.checkOpen(operation); err != nil {
152		return User{}, err
153	}
154	if err := validateDisplayName(input.DisplayName); err != nil {
155		return User{}, invalid(operation, err)
156	}
157	if len(input.PasswordHash) == 0 {
158		return User{}, invalid(operation, fmt.Errorf("password hash is empty"))
159	}
160	var lastLogin any
161	if input.LastLoginAt != nil {
162		lastLogin = timeToInt(*input.LastLoginAt)
163	}
164	result, err := s.sql.ExecContext(ctx, `
165UPDATE users
166SET display_name = ?, password_hash = ?,
167    disabled = ?, last_login_at = ?
168WHERE id = ?`, input.DisplayName,
169		append([]byte(nil), input.PasswordHash...), boolToInt(input.Disabled), lastLogin, id)
170	if err != nil {
171		return User{}, classify(operation, err)
172	}
173	if err := requireChanged(operation, result); err != nil {
174		return User{}, err
175	}
176	return s.UserByID(ctx, id)
177}
178
179func (s *Store) DeleteUser(ctx context.Context, id string) error {
180	return s.deleteByID(ctx, "delete user", "users", id)
181}
182
183func scanUser(row rowScanner) (User, error) {
184	var user User
185	var disabled int
186	var createdAt int64
187	var lastLogin sql.NullInt64
188	if err := row.Scan(&user.ID, &user.Username,
189		&user.DisplayName, &user.PasswordHash, &disabled, &user.UIDNumber,
190		&createdAt, &lastLogin, &user.CredentialsEpoch); err != nil {
191		return User{}, err
192	}
193	user.Disabled = disabled != 0
194	user.CreatedAt = intToTime(createdAt)
195	user.PasswordHash = append([]byte(nil), user.PasswordHash...)
196	if lastLogin.Valid {
197		value := intToTime(lastLogin.Int64)
198		user.LastLoginAt = &value
199	}
200	return user, nil
201}
202
203func (s *Store) CreateGroup(ctx context.Context, input CreateGroupInput) (Group, error) {
204	const operation = "create group"
205	if err := s.checkOpen(operation); err != nil {
206		return Group{}, err
207	}
208	group, err := s.prepareGroup(operation, input)
209	if err != nil {
210		return Group{}, err
211	}
212	tx, err := s.sql.BeginTx(ctx, nil)
213	if err != nil {
214		return Group{}, classify("begin "+operation, err)
215	}
216	defer tx.Rollback()
217	group, err = insertGroup(ctx, tx, operation, group)
218	if err != nil {
219		return Group{}, err
220	}
221	if err := tx.Commit(); err != nil {
222		return Group{}, classify("commit "+operation, err)
223	}
224	return group, nil
225}
226
227func (s *Store) prepareGroup(operation string, input CreateGroupInput) (Group, error) {
228	name, err := normalizeGroupName(input.Name)
229	if err != nil {
230		return Group{}, invalid(operation, err)
231	}
232	if err := validateGroupDescription(input.Description); err != nil {
233		return Group{}, invalid(operation, err)
234	}
235	id, err := s.newID()
236	if err != nil {
237		return Group{}, fmt.Errorf("%s: %w", operation, err)
238	}
239	return Group{ID: id, Name: name, Description: input.Description, CreatedAt: canonicalTime(s.now())}, nil
240}
241
242func insertGroup(ctx context.Context, tx *sql.Tx, operation string, group Group) (Group, error) {
243	var gid int64
244	if err := tx.QueryRowContext(ctx, `SELECT next_gid_number FROM settings WHERE id = 1`).Scan(&gid); err != nil {
245		return Group{}, classify("read next gid", err)
246	}
247	if gid < firstGID || gid > lastGID {
248		return Group{}, fmt.Errorf("%s: %w", operation, ErrGIDExhausted)
249	}
250	if _, err := tx.ExecContext(ctx, `UPDATE settings SET next_gid_number = ? WHERE id = 1`, gid+1); err != nil {
251		return Group{}, classify("advance gid", err)
252	}
253	if _, err := tx.ExecContext(ctx, `INSERT INTO groups (id, name, description, gid_number, created_at) VALUES (?, ?, ?, ?, ?)`, group.ID, group.Name, group.Description, gid, timeToInt(group.CreatedAt)); err != nil {
254		return Group{}, classify(operation, err)
255	}
256	group.GIDNumber = int(gid)
257	return group, nil
258}
259
260func (s *Store) GroupByID(ctx context.Context, id string) (Group, error) {
261	return s.groupBy(ctx, "get group by id", `SELECT id, name, description, gid_number, created_at FROM groups WHERE id = ?`, id)
262}
263
264func (s *Store) GroupByName(ctx context.Context, name string) (Group, error) {
265	const operation = "get group by name"
266	normalized, err := normalizeGroupName(name)
267	if err != nil {
268		return Group{}, invalid(operation, err)
269	}
270	return s.groupBy(ctx, operation, `SELECT id, name, description, gid_number, created_at FROM groups WHERE name = ?`, normalized)
271}
272
273func (s *Store) groupBy(ctx context.Context, operation, query string, arg any) (Group, error) {
274	if err := s.checkOpen(operation); err != nil {
275		return Group{}, err
276	}
277	group, err := scanGroup(s.sql.QueryRowContext(ctx, query, arg))
278	if errors.Is(err, sql.ErrNoRows) {
279		return Group{}, fmt.Errorf("%s: %w", operation, ErrNotFound)
280	}
281	if err != nil {
282		return Group{}, classify(operation, err)
283	}
284	return group, nil
285}
286
287func (s *Store) ListGroups(ctx context.Context) ([]Group, error) {
288	const operation = "list groups"
289	if err := s.checkOpen(operation); err != nil {
290		return nil, err
291	}
292	rows, err := s.sql.QueryContext(ctx, `SELECT id, name, description, gid_number, created_at FROM groups ORDER BY name, id`)
293	if err != nil {
294		return nil, classify(operation, err)
295	}
296	defer rows.Close()
297	var groups []Group
298	for rows.Next() {
299		group, err := scanGroup(rows)
300		if err != nil {
301			return nil, classify(operation, err)
302		}
303		groups = append(groups, group)
304	}
305	if err := rows.Err(); err != nil {
306		return nil, classify(operation, err)
307	}
308	return groups, nil
309}
310
311func (s *Store) UpdateGroupDescription(ctx context.Context, id, description string) (Group, error) {
312	const operation = "update group"
313	if err := s.checkOpen(operation); err != nil {
314		return Group{}, err
315	}
316	if err := validateGroupDescription(description); err != nil {
317		return Group{}, invalid(operation, err)
318	}
319	result, err := s.sql.ExecContext(ctx, `UPDATE groups SET description = ? WHERE id = ?`, description, id)
320	if err != nil {
321		return Group{}, classify(operation, err)
322	}
323	if err := requireChanged(operation, result); err != nil {
324		return Group{}, err
325	}
326	return s.GroupByID(ctx, id)
327}
328
329func (s *Store) DeleteGroup(ctx context.Context, id string) error {
330	return s.deleteByID(ctx, "delete group", "groups", id)
331}
332
333func scanGroup(row rowScanner) (Group, error) {
334	var group Group
335	var createdAt int64
336	if err := row.Scan(&group.ID, &group.Name, &group.Description, &group.GIDNumber, &createdAt); err != nil {
337		return Group{}, err
338	}
339	group.CreatedAt = intToTime(createdAt)
340	return group, nil
341}
342
343func (s *Store) deleteByID(ctx context.Context, operation, table, id string) error {
344	if err := s.checkOpen(operation); err != nil {
345		return err
346	}
347	query := `DELETE FROM ` + table + ` WHERE id = ?`
348	result, err := s.sql.ExecContext(ctx, query, id)
349	if err != nil {
350		return classify(operation, err)
351	}
352	return requireChanged(operation, result)
353}
354
355func requireChanged(operation string, result sql.Result) error {
356	count, err := result.RowsAffected()
357	if err != nil {
358		return classify(operation+" rows affected", err)
359	}
360	if count == 0 {
361		return fmt.Errorf("%s: %w", operation, ErrNotFound)
362	}
363	return nil
364}