package db import ( "context" "database/sql" "errors" "fmt" "time" "sourcecraft.dev/bigbes/sr-ht-dolt/core" ) // ACLEntry is one row of the access table, joined with the grantee's username // for display in the settings UI. type ACLEntry struct { ID int RepoID int UserID int Username string Mode core.AccessMode Created time.Time Updated time.Time } // EffectiveAccess returns the ACL grant a user holds on a repository, or nil if // the user has no access entry. This is exactly the aclMode input that // core.Allowed expects: it reflects only explicit ACL grants, never ownership or // visibility (those are the caller's to combine via core.Allowed). A nil result // with a nil error means "no grant", which is not an error condition. func (s *Store) EffectiveAccess(ctx context.Context, userID, repoID int) (*core.AccessMode, error) { const q = `SELECT mode FROM access WHERE user_id = $1 AND repo_id = $2` var mode string err := s.q.QueryRowContext(ctx, q, userID, repoID).Scan(&mode) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("effective access user=%d repo=%d: %w", userID, repoID, err) } m := core.AccessMode(mode) return &m, nil } // ListACL returns every access entry for a repository, ordered by username, with // the grantee's username resolved for display. func (s *Store) ListACL(ctx context.Context, repoID int) ([]*ACLEntry, error) { const q = ` SELECT a.id, a.repo_id, a.user_id, COALESCE(u.username, ''), a.mode, a.created, a.updated FROM access a JOIN "user" u ON u.id = a.user_id WHERE a.repo_id = $1 ORDER BY u.username ASC, a.id ASC` rows, err := s.q.QueryContext(ctx, q, repoID) if err != nil { return nil, fmt.Errorf("list acl repo=%d: %w", repoID, err) } defer rows.Close() var entries []*ACLEntry for rows.Next() { var ( e ACLEntry mode string ) if err := rows.Scan(&e.ID, &e.RepoID, &e.UserID, &e.Username, &mode, &e.Created, &e.Updated); err != nil { return nil, fmt.Errorf("scan acl: %w", err) } e.Mode = core.AccessMode(mode) entries = append(entries, &e) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate acl: %w", err) } return entries, nil } // UpsertACL grants (or updates) userID's access mode on repoID. If a grant // already exists it is updated in place (and updated is bumped); otherwise a new // row is inserted. func (s *Store) UpsertACL(ctx context.Context, repoID, userID int, mode core.AccessMode) error { now := time.Now().UTC() const q = ` INSERT INTO access (created, updated, repo_id, user_id, mode) VALUES ($1, $1, $2, $3, $4) ON CONFLICT ON CONSTRAINT uq_access_user_id_repo_id DO UPDATE SET mode = EXCLUDED.mode, updated = EXCLUDED.updated` _, err := s.q.ExecContext(ctx, q, now, repoID, userID, string(mode)) if err != nil { return fmt.Errorf("upsert acl repo=%d user=%d: %w", repoID, userID, err) } return nil } // DeleteACL revokes userID's access on repoID. Returns ErrNotFound if no grant // existed. func (s *Store) DeleteACL(ctx context.Context, repoID, userID int) error { res, err := s.q.ExecContext(ctx, `DELETE FROM access WHERE repo_id = $1 AND user_id = $2`, repoID, userID) if err != nil { return fmt.Errorf("delete acl repo=%d user=%d: %w", repoID, userID, err) } return requireOne(res, "delete acl") }