gitmote / internal / meta / acls.go · history · branches
2856 bytes · raw
package meta
import (
"context"
"errors"
)
// Perm is a repository access level, ordered read < write < admin. It matches
// the CHECK constraint on acls.perm.
type Perm string
const (
PermRead Perm = "read"
PermWrite Perm = "write"
PermAdmin Perm = "admin"
)
// SetACL grants (or updates) user's permission on repo. It is an upsert: a
// second call for the same (repo, user) replaces the level.
func (m *Metadata) SetACL(ctx context.Context, repoID, userID int64, perm Perm) error {
_, err := m.db.ExecContext(ctx,
`INSERT INTO acls (repo_id, user_id, perm) VALUES (?, ?, ?)
ON CONFLICT(repo_id, user_id) DO UPDATE SET perm = excluded.perm`,
repoID, userID, string(perm))
return err
}
// ACL is a grant on a repository, joined to the grantee's handle for display.
type ACL struct {
UserID int64
Handle string
Perm Perm
}
// ListACLs returns all grants on repo, ordered by handle, for the management UI.
func (m *Metadata) ListACLs(ctx context.Context, repoID int64) ([]ACL, error) {
rows, err := m.db.QueryContext(ctx,
`SELECT a.user_id, u.handle, a.perm
FROM acls a JOIN users u ON u.id = a.user_id
WHERE a.repo_id = ? ORDER BY u.handle`, repoID)
if err != nil {
return nil, err
}
defer rows.Close()
var acls []ACL
for rows.Next() {
var (
a ACL
perm string
)
if err := rows.Scan(&a.UserID, &a.Handle, &perm); err != nil {
return nil, err
}
a.Perm = Perm(perm)
acls = append(acls, a)
}
return acls, rows.Err()
}
// DeleteACL revokes user's grant on repo. It is idempotent — revoking an absent
// grant is not an error.
func (m *Metadata) DeleteACL(ctx context.Context, repoID, userID int64) error {
_, err := m.db.ExecContext(ctx,
`DELETE FROM acls WHERE repo_id = ? AND user_id = ?`, repoID, userID)
return err
}
// CanRead reports whether a viewer may read repo: a public repo is readable by
// anyone (userID 0 = anonymous); a private repo needs any ACL grant (read,
// write, or admin). It is the visibility-aware repo-read shared by the git-read
// and browse paths (docs/architecture/urls.md, auth.md).
func (m *Metadata) CanRead(ctx context.Context, repo *Repo, userID int64) (bool, error) {
if repo.Public() {
return true, nil
}
if userID == 0 {
return false, nil
}
_, err := m.GetACL(ctx, repo.ID, userID)
if errors.Is(err, ErrNotFound) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// GetACL returns user's permission on repo, or ErrNotFound when none is set
// (no access).
func (m *Metadata) GetACL(ctx context.Context, repoID, userID int64) (Perm, error) {
var perm string
err := m.db.QueryRowContext(ctx,
`SELECT perm FROM acls WHERE repo_id = ? AND user_id = ?`, repoID, userID).
Scan(&perm)
if isNoRows(err) {
return "", ErrNotFound
}
if err != nil {
return "", err
}
return Perm(perm), nil
}