| 1 |
|
package core |
| 2 |
|
|
| 3 |
|
// Allowed decides whether caller may perform op on repo, given the caller's |
| 4 |
|
// resolved per-repo ACL grant (aclMode, nil if the caller has no ACL entry). |
| 5 |
|
// It implements the dolt.sr.ht access matrix exactly: |
| 6 |
|
// |
| 7 |
|
// Caller \ Visibility | PUBLIC | UNLISTED | PRIVATE |
| 8 |
|
// --------------------|---------------|---------------|---------------------- |
| 9 |
|
// anon / any user | browse, clone | browse, clone | — |
| 10 |
|
// ACL RO | browse, clone | browse, clone | browse, clone |
| 11 |
|
// ACL RW | + push | + push | browse, clone, push |
| 12 |
|
// owner | all | all | all |
| 13 |
|
// |
| 14 |
|
// Rules: |
| 15 |
|
// - The owner may do anything (subject to the suspension rule below). |
| 16 |
|
// - An ACL RO grant permits browse and clone on any visibility, including |
| 17 |
|
// PRIVATE. |
| 18 |
|
// - An ACL RW grant additionally permits push (but never admin). |
| 19 |
|
// - With no ACL and non-owner: PUBLIC and UNLISTED permit browse and clone |
| 20 |
|
// for everyone (including anonymous); PRIVATE permits nothing. |
| 21 |
|
// - A suspended caller may read (browse, clone) but never push or admin, |
| 22 |
|
// regardless of ownership or ACL. |
| 23 |
|
// - Any operation outside the four known Ops is denied. |
| 24 |
|
// |
| 25 |
|
// Access control fails closed: a nil repo denies everything. |
| 26 |
89 |
func Allowed(caller *Caller, repo *Repo, aclMode *AccessMode, op Op) bool { |
| 27 |
89 |
switch op { |
| 28 |
|
case OpBrowse, OpCloneRead, OpPush, OpAdmin: |
| 29 |
|
// known op |
| 30 |
1 |
default: |
| 31 |
1 |
return false |
| 32 |
|
} |
| 33 |
88 |
if repo == nil { |
| 34 |
4 |
return false |
| 35 |
4 |
} |
| 36 |
|
|
| 37 |
84 |
suspended := caller != nil && caller.Suspended |
| 38 |
84 |
isWrite := op == OpPush || op == OpAdmin |
| 39 |
84 |
if suspended && isWrite { |
| 40 |
10 |
return false |
| 41 |
10 |
} |
| 42 |
|
|
| 43 |
|
// Owner may do anything (write already gated by the suspension check). |
| 44 |
74 |
if caller != nil && caller.UserID == repo.OwnerID { |
| 45 |
17 |
return true |
| 46 |
17 |
} |
| 47 |
|
|
| 48 |
|
// Explicit ACL grants (only meaningful for authenticated callers). |
| 49 |
57 |
if caller != nil && aclMode != nil { |
| 50 |
31 |
switch *aclMode { |
| 51 |
15 |
case AccessRO: |
| 52 |
15 |
return op == OpBrowse || op == OpCloneRead |
| 53 |
16 |
case AccessRW: |
| 54 |
16 |
// RO reads plus push; never admin. |
| 55 |
16 |
return op == OpBrowse || op == OpCloneRead || op == OpPush |
| 56 |
|
} |
| 57 |
|
} |
| 58 |
|
|
| 59 |
|
// No ownership, no ACL: fall back to visibility. Only reads are ever |
| 60 |
|
// granted this way. |
| 61 |
26 |
switch repo.Visibility { |
| 62 |
16 |
case VisibilityPublic, VisibilityUnlisted: |
| 63 |
16 |
return op == OpBrowse || op == OpCloneRead |
| 64 |
10 |
default: // VisibilityPrivate and anything unrecognized |
| 65 |
10 |
return false |
| 66 |
|
} |
| 67 |
|
} |
| 68 |
|
|
| 69 |
|
// NotFoundForPrivate reports whether a denied request for repo should be |
| 70 |
|
// surfaced as "not found" (404) rather than "forbidden" (403), so that the |
| 71 |
|
// existence of PRIVATE repositories is not leaked. Consult it only after |
| 72 |
|
// Allowed has already returned false for the request. |
| 73 |
|
// |
| 74 |
|
// A PRIVATE repo that the caller cannot even browse is reported as not found; |
| 75 |
|
// PUBLIC/UNLISTED repos, and PRIVATE repos the caller may browse, reveal their |
| 76 |
|
// existence normally (a plain 403). A nil repo is always "not found". |
| 77 |
7 |
func NotFoundForPrivate(caller *Caller, repo *Repo, aclMode *AccessMode) bool { |
| 78 |
7 |
if repo == nil { |
| 79 |
1 |
return true |
| 80 |
1 |
} |
| 81 |
6 |
if repo.Visibility != VisibilityPrivate { |
| 82 |
2 |
return false |
| 83 |
2 |
} |
| 84 |
4 |
return !Allowed(caller, repo, aclMode, OpBrowse) |
| 85 |
|
} |