internal/store/adminusers.go

v1.35.1
gitbay/internal/store/adminusers.go history · blame · raw

218 lines · 6622 bytes

  1package store
  2
  3import (
  4	"database/sql"
  5	"encoding/json"
  6	"errors"
  7	"fmt"
  8	"time"
  9)
 10
 11// AdminUser is one account as the instance admin sees it. LastSeen is the
 12// most recent authentication by any of the account's SSH keys or API
 13// tokens, "" when there has been none.
 14type AdminUser struct {
 15	Username  string
 16	IsAdmin   bool
 17	Pending   bool
 18	Disabled  bool
 19	CreatedAt string
 20	LastSeen  string
 21}
 22
 23const adminUserSelect = `SELECT u.username, u.is_admin, u.pending, u.disabled, u.created_at,
 24	COALESCE((SELECT MAX(t) FROM (
 25		SELECT last_used_at t FROM ssh_keys WHERE user_id = u.id
 26		UNION ALL SELECT last_used_at FROM api_tokens WHERE user_id = u.id)), '')
 27	FROM users u`
 28
 29func scanAdminUser(row interface{ Scan(...any) error }) (AdminUser, error) {
 30	var u AdminUser
 31	var admin, pending, disabled int
 32	err := row.Scan(&u.Username, &admin, &pending, &disabled, &u.CreatedAt, &u.LastSeen)
 33	u.IsAdmin = admin != 0
 34	u.Pending = pending != 0
 35	u.Disabled = disabled != 0
 36	return u, err
 37}
 38
 39// ListUsers returns accounts by username. state narrows the set: "" for
 40// every account, active (neither pending nor disabled), pending, disabled,
 41// or admin. after is the keyset cursor: usernames strictly greater than
 42// it, "" from the start. limit 0 means no cap.
 43func (s *Store) ListUsers(state string, limit int, after string) ([]AdminUser, error) {
 44	where := "WHERE u.username > ?"
 45	switch state {
 46	case "":
 47	case "active":
 48		where += " AND u.pending = 0 AND u.disabled = 0"
 49	case "pending":
 50		where += " AND u.pending = 1"
 51	case "disabled":
 52		where += " AND u.disabled = 1"
 53	case "admin":
 54		where += " AND u.is_admin = 1"
 55	default:
 56		return nil, fmt.Errorf("unknown state %q", state)
 57	}
 58	q := adminUserSelect + " " + where + " ORDER BY u.username"
 59	args := []any{after}
 60	if limit > 0 {
 61		q += " LIMIT ?"
 62		args = append(args, limit)
 63	}
 64	rows, err := s.DB.Query(q, args...)
 65	if err != nil {
 66		return nil, err
 67	}
 68	defer rows.Close()
 69	var out []AdminUser
 70	for rows.Next() {
 71		u, err := scanAdminUser(rows)
 72		if err != nil {
 73			return nil, err
 74		}
 75		out = append(out, u)
 76	}
 77	return out, rows.Err()
 78}
 79
 80// AdminUserByName is the ListUsers row for one account.
 81func (s *Store) AdminUserByName(name string) (AdminUser, error) {
 82	u, err := scanAdminUser(s.DB.QueryRow(adminUserSelect+" WHERE u.username = ?", name))
 83	if errors.Is(err, sql.ErrNoRows) {
 84		return u, ErrNotFound
 85	}
 86	return u, err
 87}
 88
 89// AdminMailAddresses returns where to reach the instance's admins: the
 90// verified primary address of every active admin who has activity mail
 91// on. An admin with no verified primary, or with mail off, is skipped
 92// rather than reported, the same rule ActivityMailAddress applies to
 93// anyone else (#234).
 94func (s *Store) AdminMailAddresses() ([]string, error) {
 95	rows, err := s.DB.Query(`SELECT e.address FROM users u
 96		JOIN emails e ON e.user_id = u.id AND e.is_primary = 1 AND e.verified_at IS NOT NULL
 97		WHERE u.is_admin = 1 AND u.pending = 0 AND u.disabled = 0 AND u.notify_mail != 0
 98		ORDER BY e.address`)
 99	if err != nil {
100		return nil, err
101	}
102	defer rows.Close()
103	var out []string
104	for rows.Next() {
105		var a string
106		if err := rows.Scan(&a); err != nil {
107			return nil, err
108		}
109		out = append(out, a)
110	}
111	return out, rows.Err()
112}
113
114// OwnedRepoCount counts repositories the user owns directly, not through
115// an org.
116func (s *Store) OwnedRepoCount(userID int64) (int64, error) {
117	var n int64
118	err := s.DB.QueryRow("SELECT COUNT(*) FROM repos WHERE owner_kind = 'user' AND owner_id = ?", userID).Scan(&n)
119	return n, err
120}
121
122// WebSessionCount counts the user's unexpired browser sessions.
123func (s *Store) WebSessionCount(userID int64) (int64, error) {
124	var n int64
125	err := s.DB.QueryRow("SELECT COUNT(*) FROM web_sessions WHERE user_id = ? AND expires_at > ?",
126		userID, fmtTime(time.Now())).Scan(&n)
127	return n, err
128}
129
130// ErrLastAdmin refuses the demotion that would leave the instance with no
131// admin at all.
132var ErrLastAdmin = errors.New("that is the only instance admin; promote someone else first")
133
134// SetUserAdmin grants or removes instance admin. Removing it from the last
135// admin is refused inside the same transaction that counts them.
136func (s *Store) SetUserAdmin(userID int64, admin bool) error {
137	tx, err := s.DB.Begin()
138	if err != nil {
139		return err
140	}
141	defer tx.Rollback()
142	if !admin {
143		var others int
144		if err := tx.QueryRow("SELECT COUNT(*) FROM users WHERE is_admin = 1 AND id != ?", userID).Scan(&others); err != nil {
145			return err
146		}
147		if others == 0 {
148			return ErrLastAdmin
149		}
150	}
151	res, err := tx.Exec("UPDATE users SET is_admin = ? WHERE id = ?", boolInt(admin), userID)
152	if err != nil {
153		return err
154	}
155	if n, _ := res.RowsAffected(); n == 0 {
156		return ErrNotFound
157	}
158	return tx.Commit()
159}
160
161// AdminRepo is one repository as the instance admin lists it. LastPush is
162// the newest push event, "" when nothing has been pushed.
163type AdminRepo struct {
164	Path       string // owner/name, the keyset cursor
165	OwnerName  string
166	Name       string
167	Visibility string
168	Archived   bool
169	CreatedAt  string
170	LastPush   string
171}
172
173// ListReposAdmin lists repositories across every owner, by path. owner and
174// visibility narrow the set when non-empty; after is the path keyset
175// cursor; limit 0 means no cap.
176func (s *Store) ListReposAdmin(owner, visibility string, limit int, after string) ([]AdminRepo, error) {
177	q := `SELECT COALESCE(u.username, o.name) || '/' || r.name, COALESCE(u.username, o.name), r.name,
178		r.visibility, r.settings_json, r.created_at,
179		COALESCE((SELECT MAX(created_at) FROM events WHERE repo_id = r.id AND kind = 'push'), '')
180		FROM repos r
181		LEFT JOIN users u ON r.owner_kind = 'user' AND u.id = r.owner_id
182		LEFT JOIN orgs o  ON r.owner_kind = 'org'  AND o.id = r.owner_id
183		WHERE COALESCE(u.username, o.name) || '/' || r.name > ?`
184	args := []any{after}
185	if owner != "" {
186		q += " AND COALESCE(u.username, o.name) = ?"
187		args = append(args, owner)
188	}
189	if visibility != "" {
190		q += " AND r.visibility = ?"
191		args = append(args, visibility)
192	}
193	q += " ORDER BY 1"
194	if limit > 0 {
195		q += " LIMIT ?"
196		args = append(args, limit)
197	}
198	rows, err := s.DB.Query(q, args...)
199	if err != nil {
200		return nil, err
201	}
202	defer rows.Close()
203	var out []AdminRepo
204	for rows.Next() {
205		var r AdminRepo
206		var settingsJSON string
207		if err := rows.Scan(&r.Path, &r.OwnerName, &r.Name, &r.Visibility, &settingsJSON, &r.CreatedAt, &r.LastPush); err != nil {
208			return nil, err
209		}
210		var st RepoSettings
211		if err := json.Unmarshal([]byte(settingsJSON), &st); err != nil {
212			return nil, fmt.Errorf("repo %s settings: %w", r.Path, err)
213		}
214		r.Archived = st.Archived
215		out = append(out, r)
216	}
217	return out, rows.Err()
218}