package core
// Allowed decides whether caller may perform op on repo, given the caller's
// resolved per-repo ACL grant (aclMode, nil if the caller has no ACL entry).
// It implements the dolt.sr.ht access matrix exactly:
//
// Caller \ Visibility | PUBLIC | UNLISTED | PRIVATE
// --------------------|---------------|---------------|----------------------
// anon / any user | browse, clone | browse, clone | —
// ACL RO | browse, clone | browse, clone | browse, clone
// ACL RW | + push | + push | browse, clone, push
// owner | all | all | all
//
// Rules:
// - The owner may do anything (subject to the suspension rule below).
// - An ACL RO grant permits browse and clone on any visibility, including
// PRIVATE.
// - An ACL RW grant additionally permits push (but never admin).
// - With no ACL and non-owner: PUBLIC and UNLISTED permit browse and clone
// for everyone (including anonymous); PRIVATE permits nothing.
// - A suspended caller may read (browse, clone) but never push or admin,
// regardless of ownership or ACL.
// - Any operation outside the four known Ops is denied.
//
// Access control fails closed: a nil repo denies everything.
func Allowed(caller *Caller, repo *Repo, aclMode *AccessMode, op Op) bool {
switch op {
case OpBrowse, OpCloneRead, OpPush, OpAdmin:
// known op
default:
return false
}
if repo == nil {
return false
}
suspended := caller != nil && caller.Suspended
isWrite := op == OpPush || op == OpAdmin
if suspended && isWrite {
return false
}
// Owner may do anything (write already gated by the suspension check).
if caller != nil && caller.UserID == repo.OwnerID {
return true
}
// Explicit ACL grants (only meaningful for authenticated callers).
if caller != nil && aclMode != nil {
switch *aclMode {
case AccessRO:
return op == OpBrowse || op == OpCloneRead
case AccessRW:
// RO reads plus push; never admin.
return op == OpBrowse || op == OpCloneRead || op == OpPush
}
}
// No ownership, no ACL: fall back to visibility. Only reads are ever
// granted this way.
switch repo.Visibility {
case VisibilityPublic, VisibilityUnlisted:
return op == OpBrowse || op == OpCloneRead
default: // VisibilityPrivate and anything unrecognized
return false
}
}
// NotFoundForPrivate reports whether a denied request for repo should be
// surfaced as "not found" (404) rather than "forbidden" (403), so that the
// existence of PRIVATE repositories is not leaked. Consult it only after
// Allowed has already returned false for the request.
//
// A PRIVATE repo that the caller cannot even browse is reported as not found;
// PUBLIC/UNLISTED repos, and PRIVATE repos the caller may browse, reveal their
// existence normally (a plain 403). A nil repo is always "not found".
func NotFoundForPrivate(caller *Caller, repo *Repo, aclMode *AccessMode) bool {
if repo == nil {
return true
}
if repo.Visibility != VisibilityPrivate {
return false
}
return !Allowed(caller, repo, aclMode, OpBrowse)
}