internal/store/users.go

605 lines · 18648 bytes

  1package store
  2
  3import (
  4	"database/sql"
  5	"errors"
  6	"fmt"
  7	"strings"
  8	"time"
  9)
 10
 11type User struct {
 12	ID       int64
 13	Username string
 14	IsAdmin  bool
 15	Pending  bool // self-registered, email not yet verified
 16	Disabled bool // administratively suspended
 17	// SignedInAt is when the browser session this user came from was
 18	// created by a login. Set by WebSessionUser only; zero elsewhere.
 19	SignedInAt time.Time
 20}
 21
 22type SSHKey struct {
 23	ID          int64
 24	UserID      int64
 25	Fingerprint string
 26	Algo        string
 27	Blob        []byte
 28	Scope       string
 29	Label       string // "" when the key was added with no name
 30	CreatedAt   string
 31	LastUsedAt  string     // "" when the key has never authenticated
 32	CreatedBy   string     // name of the API token that added the key; "" for none. ListSSHKeys only.
 33	ExpiresAt   *time.Time // nil when the key never expires
 34}
 35
 36// Expired reports whether the key has lapsed at now.
 37func (k SSHKey) Expired(now time.Time) bool {
 38	return k.ExpiresAt != nil && !k.ExpiresAt.After(now)
 39}
 40
 41// ErrDuplicateKey carries the exact user-facing message from the spec. It
 42// deliberately does not name the owning account (enumeration oracle).
 43var ErrDuplicateKey = errors.New("that key is already registered to another account; remove it there first or use a different key")
 44
 45var ErrNotFound = errors.New("not found")
 46
 47func (s *Store) CreateUser(username string, isAdmin bool) (int64, error) {
 48	if taken, err := ownerNameTaken(s.DB, username); err != nil {
 49		return 0, err
 50	} else if taken {
 51		return 0, fmt.Errorf("username %q is taken", username)
 52	}
 53	res, err := s.DB.Exec("INSERT INTO users (username, is_admin) VALUES (?, ?)", username, boolInt(isAdmin))
 54	if err != nil {
 55		if isUniqueErr(err) {
 56			return 0, fmt.Errorf("username %q is taken", username)
 57		}
 58		return 0, err
 59	}
 60	return res.LastInsertId()
 61}
 62
 63// DeleteUser removes an account whose removal orphans nothing: no owned
 64// repositories, no authored issues, MRs, comments, or reviews, and not the
 65// only admin of an org. Everything else (keys, emails, sessions, tokens,
 66// pins, memberships, activity) cascades. Blockers come back as an error
 67// naming what stands in the way, so the operator can transfer, delete, or
 68// disable instead.
 69func (s *Store) DeleteUser(id int64) error {
 70	var blockers []string
 71	var checkErr error
 72	count := func(q string, what string) {
 73		var n int
 74		if err := s.DB.QueryRow(q, id).Scan(&n); err != nil {
 75			if checkErr == nil {
 76				checkErr = fmt.Errorf("checking %s: %w", what, err)
 77			}
 78			return
 79		}
 80		if n > 0 {
 81			blockers = append(blockers, fmt.Sprintf("%d %s", n, what))
 82		}
 83	}
 84	count("SELECT COUNT(*) FROM repos WHERE owner_kind = 'user' AND owner_id = ?", "owned repositories")
 85	count("SELECT COUNT(*) FROM issues WHERE author_id = ?", "authored issues")
 86	count("SELECT COUNT(*) FROM merge_requests WHERE author_id = ?", "authored merge requests")
 87	count("SELECT COUNT(*) FROM issue_comments WHERE author_id = ?", "issue comments")
 88	count("SELECT COUNT(*) FROM mr_comments WHERE author_id = ?", "MR comments")
 89	count("SELECT COUNT(*) FROM mr_diff_comments WHERE author_id = ?", "diff comments")
 90	count("SELECT COUNT(*) FROM mr_reviews WHERE reviewer_id = ?", "reviews")
 91	count(`SELECT COUNT(*) FROM org_members m WHERE m.user_id = ? AND m.role = 'admin'
 92		AND NOT EXISTS (SELECT 1 FROM org_members o
 93			WHERE o.org_id = m.org_id AND o.role = 'admin' AND o.user_id != m.user_id)`,
 94		"organizations with no other admin")
 95	if checkErr != nil {
 96		return checkErr
 97	}
 98	if len(blockers) > 0 {
 99		return fmt.Errorf("account still anchors: %s — transfer or delete those first, or disable the account instead",
100			strings.Join(blockers, ", "))
101	}
102	res, err := s.DB.Exec("DELETE FROM users WHERE id = ?", id)
103	if err != nil {
104		return err
105	}
106	if n, _ := res.RowsAffected(); n == 0 {
107		return ErrNotFound
108	}
109	s.announce(Revoked{UserID: id})
110	return nil
111}
112
113// OwnerExists reports whether a user or org owns the name — the ACME host
114// policy check for pages subdomains.
115func (s *Store) OwnerExists(name string) bool {
116	var n int
117	s.DB.QueryRow(`SELECT (SELECT COUNT(*) FROM users WHERE username = ?1)
118		+ (SELECT COUNT(*) FROM orgs WHERE name = ?1)`, name).Scan(&n)
119	return n > 0
120}
121
122func (s *Store) UserByUsername(name string) (User, error) {
123	var u User
124	var admin, pending, disabled int
125	err := s.DB.QueryRow("SELECT id, username, is_admin, pending, disabled FROM users WHERE username = ?", name).
126		Scan(&u.ID, &u.Username, &admin, &pending, &disabled)
127	if errors.Is(err, sql.ErrNoRows) {
128		return u, ErrNotFound
129	}
130	u.IsAdmin = admin != 0
131	u.Pending = pending != 0
132	u.Disabled = disabled != 0
133	return u, err
134}
135
136// UserEmailAddresses returns every address on the account, verified or not.
137func (s *Store) UserEmailAddresses(userID int64) ([]string, error) {
138	rows, err := s.DB.Query("SELECT address FROM emails WHERE user_id = ? ORDER BY is_primary DESC, address", userID)
139	if err != nil {
140		return nil, err
141	}
142	defer rows.Close()
143	var out []string
144	for rows.Next() {
145		var a string
146		if err := rows.Scan(&a); err != nil {
147			return nil, err
148		}
149		out = append(out, a)
150	}
151	return out, rows.Err()
152}
153
154// Email is one address on an account, with the state the signature rules
155// and notification routing depend on.
156type Email struct {
157	Address    string
158	Verified   bool
159	VerifiedBy string // smtp | admin, empty when unverified
160	Primary    bool
161}
162
163// ListEmails returns every address on the account with its state.
164func (s *Store) ListEmails(userID int64) ([]Email, error) {
165	rows, err := s.DB.Query(`SELECT address, verified_at IS NOT NULL,
166		COALESCE(verified_by, ''), is_primary
167		FROM emails WHERE user_id = ? ORDER BY is_primary DESC, address`, userID)
168	if err != nil {
169		return nil, err
170	}
171	defer rows.Close()
172	var out []Email
173	for rows.Next() {
174		var e Email
175		if err := rows.Scan(&e.Address, &e.Verified, &e.VerifiedBy, &e.Primary); err != nil {
176			return nil, err
177		}
178		out = append(out, e)
179	}
180	return out, rows.Err()
181}
182
183// SetUserDisabled suspends or restores an account. Disabling drops every
184// credential that would grant a session on its own — web sessions, API
185// tokens, unclaimed login links — and leaves the SSH keys registered but
186// refused at every entry point until re-enabled; connections they opened
187// are closed.
188func (s *Store) SetUserDisabled(userID int64, disabled bool) error {
189	v := 0
190	if disabled {
191		v = 1
192	}
193	res, err := s.DB.Exec("UPDATE users SET disabled = ? WHERE id = ?", v, userID)
194	if err != nil {
195		return err
196	}
197	if n, _ := res.RowsAffected(); n == 0 {
198		return ErrNotFound
199	}
200	if disabled {
201		// A pending login link is a session in waiting, so it goes with
202		// the sessions and API tokens. Re-enabling means minting again.
203		for _, table := range []string{"web_sessions", "api_tokens", "login_tokens"} {
204			if _, err = s.DB.Exec("DELETE FROM "+table+" WHERE user_id = ?", userID); err != nil {
205				return err
206			}
207		}
208		s.announce(Revoked{UserID: userID})
209	}
210	return err
211}
212
213// MailEnabled reports whether activity notifications reach the account
214// by mail as well as the inbox.
215func (s *Store) MailEnabled(userID int64) (bool, error) {
216	var on int
217	err := s.DB.QueryRow("SELECT notify_mail FROM users WHERE id = ?", userID).Scan(&on)
218	if errors.Is(err, sql.ErrNoRows) {
219		return false, ErrNotFound
220	}
221	return on != 0, err
222}
223
224func (s *Store) SetMailEnabled(userID int64, on bool) error {
225	v := 0
226	if on {
227		v = 1
228	}
229	_, err := s.DB.Exec("UPDATE users SET notify_mail = ? WHERE id = ?", v, userID)
230	return err
231}
232
233// WatchEnabled reports whether the account hears about every issue and
234// merge request on the repositories it can write to, without a
235// repo_watchers row on each (#194).
236func (s *Store) WatchEnabled(userID int64) (bool, error) {
237	var on int
238	err := s.DB.QueryRow("SELECT notify_watch FROM users WHERE id = ?", userID).Scan(&on)
239	if errors.Is(err, sql.ErrNoRows) {
240		return false, ErrNotFound
241	}
242	return on != 0, err
243}
244
245func (s *Store) SetWatchEnabled(userID int64, on bool) error {
246	v := 0
247	if on {
248		v = 1
249	}
250	_, err := s.DB.Exec("UPDATE users SET notify_watch = ? WHERE id = ?", v, userID)
251	return err
252}
253
254// Theme is the web colour scheme the account chose: system, light or
255// dark (#232).
256func (s *Store) Theme(userID int64) (string, error) {
257	var theme string
258	err := s.DB.QueryRow("SELECT theme FROM users WHERE id = ?", userID).Scan(&theme)
259	if errors.Is(err, sql.ErrNoRows) {
260		return "", ErrNotFound
261	}
262	return theme, err
263}
264
265func (s *Store) SetTheme(userID int64, theme string) error {
266	_, err := s.DB.Exec("UPDATE users SET theme = ? WHERE id = ?", theme, userID)
267	return err
268}
269
270func (s *Store) UserByID(id int64) (User, error) {
271	var u User
272	var admin, pending, disabled int
273	err := s.DB.QueryRow("SELECT id, username, is_admin, pending, disabled FROM users WHERE id = ?", id).
274		Scan(&u.ID, &u.Username, &admin, &pending, &disabled)
275	if errors.Is(err, sql.ErrNoRows) {
276		return u, ErrNotFound
277	}
278	u.IsAdmin = admin != 0
279	u.Pending = pending != 0
280	u.Disabled = disabled != 0
281	return u, err
282}
283
284// KeyOrigin is how a key came to be.
285type KeyOrigin struct {
286	CreatedByToken int64      // the API token that added it; 0 for none
287	ExpiresAt      *time.Time // when it stops authenticating; nil for never
288}
289
290// AddSSHKey registers a key and bumps the key epoch in one transaction.
291func (s *Store) AddSSHKey(userID int64, fingerprint, algo string, blob []byte, scope, label string) error {
292	return s.AddSSHKeyFrom(userID, fingerprint, algo, blob, scope, label, KeyOrigin{})
293}
294
295// AddSSHKeyFrom is AddSSHKey recording where the key came from.
296func (s *Store) AddSSHKeyFrom(userID int64, fingerprint, algo string, blob []byte, scope, label string, o KeyOrigin) error {
297	tx, err := s.DB.Begin()
298	if err != nil {
299		return err
300	}
301	defer tx.Rollback()
302	var exp any
303	if o.ExpiresAt != nil {
304		exp = fmtTime(*o.ExpiresAt)
305	}
306	if _, err := tx.Exec(
307		"INSERT INTO ssh_keys (user_id, fingerprint, algo, blob, scope, label, created_by_token, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
308		userID, fingerprint, algo, blob, scope, label, nullID(o.CreatedByToken), exp); err != nil {
309		if isUniqueErr(err) {
310			return ErrDuplicateKey
311		}
312		return err
313	}
314	if err := bumpKeyEpoch(tx); err != nil {
315		return err
316	}
317	return tx.Commit()
318}
319
320// RemoveSSHKey removes a key owned by userID, bumps the key epoch, and
321// announces the revocation.
322func (s *Store) RemoveSSHKey(userID int64, fingerprint string) error {
323	tx, err := s.DB.Begin()
324	if err != nil {
325		return err
326	}
327	defer tx.Rollback()
328	var id int64
329	err = tx.QueryRow("DELETE FROM ssh_keys WHERE user_id = ? AND fingerprint = ? RETURNING id", userID, fingerprint).Scan(&id)
330	if errors.Is(err, sql.ErrNoRows) {
331		return ErrNotFound
332	}
333	if err != nil {
334		return err
335	}
336	if err := bumpKeyEpoch(tx); err != nil {
337		return err
338	}
339	if err := tx.Commit(); err != nil {
340		return err
341	}
342	s.announce(Revoked{KeyIDs: []int64{id}})
343	return nil
344}
345
346// SetSSHKeyLabel renames a key owned by userID. Labels do not touch the
347// key epoch: nothing about authentication changes.
348func (s *Store) SetSSHKeyLabel(userID int64, fingerprint, label string) error {
349	res, err := s.DB.Exec("UPDATE ssh_keys SET label = ? WHERE user_id = ? AND fingerprint = ?", label, userID, fingerprint)
350	if err != nil {
351		return err
352	}
353	if n, _ := res.RowsAffected(); n == 0 {
354		return ErrNotFound
355	}
356	return nil
357}
358
359func (s *Store) SSHKeyByFingerprint(fingerprint string) (SSHKey, error) {
360	var k SSHKey
361	var exp sql.NullString
362	err := s.DB.QueryRow(
363		"SELECT id, user_id, fingerprint, algo, blob, scope, label, expires_at FROM ssh_keys WHERE fingerprint = ?",
364		fingerprint).Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope, &k.Label, &exp)
365	if errors.Is(err, sql.ErrNoRows) {
366		return k, ErrNotFound
367	}
368	k.ExpiresAt = parseTime(exp)
369	return k, err
370}
371
372func (s *Store) ListSSHKeys(userID int64) ([]SSHKey, error) {
373	rows, err := s.DB.Query(
374		`SELECT k.id, k.user_id, k.fingerprint, k.algo, k.blob, k.scope, k.label, k.created_at,
375		        COALESCE(k.last_used_at, ''), COALESCE(t.name, ''), k.expires_at
376		 FROM ssh_keys k LEFT JOIN api_tokens t ON t.id = k.created_by_token
377		 WHERE k.user_id = ? ORDER BY k.id`,
378		userID)
379	if err != nil {
380		return nil, err
381	}
382	defer rows.Close()
383	var keys []SSHKey
384	for rows.Next() {
385		var k SSHKey
386		var exp sql.NullString
387		if err := rows.Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope, &k.Label, &k.CreatedAt, &k.LastUsedAt, &k.CreatedBy, &exp); err != nil {
388			return nil, err
389		}
390		k.ExpiresAt = parseTime(exp)
391		keys = append(keys, k)
392	}
393	return keys, rows.Err()
394}
395
396// TouchSSHKey records key use; best-effort, callers ignore the error.
397func (s *Store) TouchSSHKey(id int64) error {
398	_, err := s.DB.Exec(
399		"UPDATE ssh_keys SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?", id)
400	return err
401}
402
403// AddEmail adds an address; verifiedBy is "" (unverified), "smtp", or "admin".
404// Adding an already-verified address bumps the key epoch: it is a trust input
405// for signature states.
406func (s *Store) AddEmail(userID int64, address, verifiedBy string, primary bool) error {
407	tx, err := s.DB.Begin()
408	if err != nil {
409		return err
410	}
411	defer tx.Rollback()
412	var vAt, vBy any
413	if verifiedBy != "" {
414		vAt = "now"
415		vBy = verifiedBy
416	}
417	_, err = tx.Exec(
418		`INSERT INTO emails (user_id, address, verified_at, verified_by, is_primary)
419		 VALUES (?, ?, CASE WHEN ? IS NULL THEN NULL ELSE strftime('%Y-%m-%dT%H:%M:%fZ','now') END, ?, ?)`,
420		userID, address, vAt, vBy, boolInt(primary))
421	if isUniqueErr(err) {
422		return fmt.Errorf("address %q is already in use", address)
423	}
424	if err != nil {
425		return err
426	}
427	if verifiedBy != "" {
428		if err := bumpKeyEpoch(tx); err != nil {
429			return err
430		}
431	}
432	return tx.Commit()
433}
434
435var (
436	ErrPrimaryEmail      = errors.New("that is the primary address; make another address primary first")
437	ErrLastVerifiedEmail = errors.New("that is the only verified address on the account; verify another first")
438	ErrUnverifiedEmail   = errors.New("that address is not verified")
439)
440
441// RemoveEmail drops an address from the account, and any verification
442// code pending for it. The primary and the last verified address stay:
443// activation, login links and commit identity all resolve through
444// verified addresses. Removing a verified address bumps the key epoch,
445// since the signature cache keys on verified addresses too.
446func (s *Store) RemoveEmail(userID int64, address string) error {
447	tx, err := s.DB.Begin()
448	if err != nil {
449		return err
450	}
451	defer tx.Rollback()
452	var primary, verified bool
453	err = tx.QueryRow("SELECT is_primary, verified_at IS NOT NULL FROM emails WHERE user_id = ? AND address = ?",
454		userID, address).Scan(&primary, &verified)
455	if errors.Is(err, sql.ErrNoRows) {
456		return ErrNotFound
457	}
458	if err != nil {
459		return err
460	}
461	if primary {
462		return ErrPrimaryEmail
463	}
464	if verified {
465		var others int
466		if err := tx.QueryRow("SELECT count(*) FROM emails WHERE user_id = ? AND verified_at IS NOT NULL AND address != ?",
467			userID, address).Scan(&others); err != nil {
468			return err
469		}
470		if others == 0 {
471			return ErrLastVerifiedEmail
472		}
473	}
474	if _, err := tx.Exec("DELETE FROM email_tokens WHERE user_id = ? AND address = ?", userID, address); err != nil {
475		return err
476	}
477	if _, err := tx.Exec("DELETE FROM emails WHERE user_id = ? AND address = ?", userID, address); err != nil {
478		return err
479	}
480	if verified {
481		if err := bumpKeyEpoch(tx); err != nil {
482			return err
483		}
484	}
485	return tx.Commit()
486}
487
488// SetPrimaryEmail makes a verified address the account's primary. The
489// verified set is unchanged, so the key epoch is not.
490func (s *Store) SetPrimaryEmail(userID int64, address string) error {
491	tx, err := s.DB.Begin()
492	if err != nil {
493		return err
494	}
495	defer tx.Rollback()
496	var verified bool
497	err = tx.QueryRow("SELECT verified_at IS NOT NULL FROM emails WHERE user_id = ? AND address = ?",
498		userID, address).Scan(&verified)
499	if errors.Is(err, sql.ErrNoRows) {
500		return ErrNotFound
501	}
502	if err != nil {
503		return err
504	}
505	if !verified {
506		return ErrUnverifiedEmail
507	}
508	if _, err := tx.Exec("UPDATE emails SET is_primary = 0 WHERE user_id = ?", userID); err != nil {
509		return err
510	}
511	if _, err := tx.Exec("UPDATE emails SET is_primary = 1 WHERE user_id = ? AND address = ?", userID, address); err != nil {
512		return err
513	}
514	return tx.Commit()
515}
516
517func (s *Store) KeyEpoch() (int64, error) {
518	var v int64
519	err := s.DB.QueryRow("SELECT value FROM settings WHERE key = 'key_epoch'").Scan(&v)
520	return v, err
521}
522
523type execer interface {
524	Exec(query string, args ...any) (sql.Result, error)
525}
526
527func bumpKeyEpoch(tx execer) error {
528	_, err := tx.Exec("UPDATE settings SET value = value + 1 WHERE key = 'key_epoch'")
529	return err
530}
531
532func boolInt(b bool) int {
533	if b {
534		return 1
535	}
536	return 0
537}
538
539func isUniqueErr(err error) bool {
540	return err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed")
541}
542
543func (s *Store) SSHKeyByID(id int64) (SSHKey, error) {
544	var k SSHKey
545	var exp sql.NullString
546	err := s.DB.QueryRow(
547		"SELECT id, user_id, fingerprint, algo, blob, scope, label, expires_at FROM ssh_keys WHERE id = ?",
548		id).Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope, &k.Label, &exp)
549	if errors.Is(err, sql.ErrNoRows) {
550		return k, ErrNotFound
551	}
552	k.ExpiresAt = parseTime(exp)
553	return k, err
554}
555
556// ListDeployKeys returns the deploy keys bound to a repository.
557func (s *Store) ListDeployKeys(repoID int64) ([]SSHKey, error) {
558	rows, err := s.DB.Query(
559		`SELECT id, user_id, fingerprint, algo, blob, scope, label, COALESCE(last_used_at, ''), expires_at
560		 FROM ssh_keys WHERE scope LIKE 'deploy:' || ? || ':%' ORDER BY id`,
561		repoID)
562	if err != nil {
563		return nil, err
564	}
565	defer rows.Close()
566	var keys []SSHKey
567	for rows.Next() {
568		var k SSHKey
569		var exp sql.NullString
570		if err := rows.Scan(&k.ID, &k.UserID, &k.Fingerprint, &k.Algo, &k.Blob, &k.Scope, &k.Label, &k.LastUsedAt, &exp); err != nil {
571			return nil, err
572		}
573		k.ExpiresAt = parseTime(exp)
574		keys = append(keys, k)
575	}
576	return keys, rows.Err()
577}
578
579// RemoveDeployKey removes a deploy key from a repository by fingerprint;
580// any repo admin may remove it regardless of who added it.
581func (s *Store) RemoveDeployKey(repoID int64, fingerprint string) error {
582	tx, err := s.DB.Begin()
583	if err != nil {
584		return err
585	}
586	defer tx.Rollback()
587	var id int64
588	err = tx.QueryRow(
589		"DELETE FROM ssh_keys WHERE fingerprint = ? AND scope LIKE 'deploy:' || ? || ':%' RETURNING id",
590		fingerprint, repoID).Scan(&id)
591	if errors.Is(err, sql.ErrNoRows) {
592		return ErrNotFound
593	}
594	if err != nil {
595		return err
596	}
597	if err := bumpKeyEpoch(tx); err != nil {
598		return err
599	}
600	if err := tx.Commit(); err != nil {
601		return err
602	}
603	s.announce(Revoked{KeyIDs: []int64{id}})
604	return nil
605}