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")
}