coverage~bigbes/sr-ht-dolt3523280cdb/access.go

Coverage
82.4% 28/34 statements
Δ
+0.0
Blob
d4fda31
1 package db
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "time"
9
10 "sourcecraft.dev/bigbes/sr-ht-dolt/core"
11 )
12
13 // ACLEntry is one row of the access table, joined with the grantee's username
14 // for display in the settings UI.
15 type ACLEntry struct {
16 ID int
17 RepoID int
18 UserID int
19 Username string
20 Mode core.AccessMode
21 Created time.Time
22 Updated time.Time
23 }
24
25 // EffectiveAccess returns the ACL grant a user holds on a repository, or nil if
26 // the user has no access entry. This is exactly the aclMode input that
27 // core.Allowed expects: it reflects only explicit ACL grants, never ownership or
28 // visibility (those are the caller's to combine via core.Allowed). A nil result
29 // with a nil error means "no grant", which is not an error condition.
30 4 func (s *Store) EffectiveAccess(ctx context.Context, userID, repoID int) (*core.AccessMode, error) {
31 4 const q = `SELECT mode FROM access WHERE user_id = $1 AND repo_id = $2`
32 4 var mode string
33 4 err := s.q.QueryRowContext(ctx, q, userID, repoID).Scan(&mode)
34 4 if errors.Is(err, sql.ErrNoRows) {
35 2 return nil, nil
36 2 }
37 2 if err != nil {
38 0 return nil, fmt.Errorf("effective access user=%d repo=%d: %w", userID, repoID, err)
39 0 }
40 2 m := core.AccessMode(mode)
41 2 return &m, nil
42 }
43
44 // ListACL returns every access entry for a repository, ordered by username, with
45 // the grantee's username resolved for display.
46 1 func (s *Store) ListACL(ctx context.Context, repoID int) ([]*ACLEntry, error) {
47 1 const q = `
48 1 SELECT a.id, a.repo_id, a.user_id, COALESCE(u.username, ''), a.mode, a.created, a.updated
49 1 FROM access a
50 1 JOIN "user" u ON u.id = a.user_id
51 1 WHERE a.repo_id = $1
52 1 ORDER BY u.username ASC, a.id ASC`
53 1 rows, err := s.q.QueryContext(ctx, q, repoID)
54 1 if err != nil {
55 0 return nil, fmt.Errorf("list acl repo=%d: %w", repoID, err)
56 0 }
57 1 defer rows.Close()
58 1 var entries []*ACLEntry
59 1 for rows.Next() {
60 1 var (
61 1 e ACLEntry
62 1 mode string
63 1 )
64 1 if err := rows.Scan(&e.ID, &e.RepoID, &e.UserID, &e.Username,
65 1 &mode, &e.Created, &e.Updated); err != nil {
66 0 return nil, fmt.Errorf("scan acl: %w", err)
67 0 }
68 1 e.Mode = core.AccessMode(mode)
69 1 entries = append(entries, &e)
70 }
71 1 if err := rows.Err(); err != nil {
72 0 return nil, fmt.Errorf("iterate acl: %w", err)
73 0 }
74 1 return entries, nil
75 }
76
77 // UpsertACL grants (or updates) userID's access mode on repoID. If a grant
78 // already exists it is updated in place (and updated is bumped); otherwise a new
79 // row is inserted.
80 7 func (s *Store) UpsertACL(ctx context.Context, repoID, userID int, mode core.AccessMode) error {
81 7 now := time.Now().UTC()
82 7 const q = `
83 7 INSERT INTO access (created, updated, repo_id, user_id, mode)
84 7 VALUES ($1, $1, $2, $3, $4)
85 7 ON CONFLICT ON CONSTRAINT uq_access_user_id_repo_id
86 7 DO UPDATE SET mode = EXCLUDED.mode, updated = EXCLUDED.updated`
87 7 _, err := s.q.ExecContext(ctx, q, now, repoID, userID, string(mode))
88 7 if err != nil {
89 0 return fmt.Errorf("upsert acl repo=%d user=%d: %w", repoID, userID, err)
90 0 }
91 7 return nil
92 }
93
94 // DeleteACL revokes userID's access on repoID. Returns ErrNotFound if no grant
95 // existed.
96 2 func (s *Store) DeleteACL(ctx context.Context, repoID, userID int) error {
97 2 res, err := s.q.ExecContext(ctx,
98 2 `DELETE FROM access WHERE repo_id = $1 AND user_id = $2`, repoID, userID)
99 2 if err != nil {
100 0 return fmt.Errorf("delete acl repo=%d user=%d: %w", repoID, userID, err)
101 0 }
102 2 return requireOne(res, "delete acl")
103 }