1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6 "fmt"
7
8 "bnema.dev/ermite/internal/domain"
9)
10
11// canRead is the access rule for reading a repository, the single place that
12// decides it: root reads everything, anyone reads a public repository, and a
13// stored token reads a private one only with repo:read on it (the scope, and
14// the repository within its list). Callers answer domain.ErrNotFound when it
15// is false, so a private repository never shows itself.
16func canRead(p domain.Principal, repo domain.Repo) bool {
17 return canReadAs(p, repo.Name, repo.Visibility)
18}
19
20// canReadAs is canRead for a repository known by its name and visibility only,
21// such as one that no longer exists.
22func canReadAs(p domain.Principal, name domain.RepoName, vis domain.Visibility) bool {
23 if _, _, archive := p.ArchiveGrant(); archive {
24 // The holder of an archive token downloads one commit through the
25 // archive endpoint, which checks its grant; everywhere else it reads
26 // nothing, not even a public repository (domain.RepoTokenPrincipal).
27 return false
28 }
29 if vis == domain.VisibilityPublic {
30 return true
31 }
32 return p.Allows(domain.ScopeRepoRead, name)
33}
34
35// adminAccess is the access rule for changing or deleting a repository:
36// repo:admin on it, and no branch prefixes. A token limited to some branches
37// may not delete the repository (that deletes every branch, those outside its
38// prefixes included), nor change its default branch, visibility, description,
39// merge modes or failing-checks rule: these decide what happens to branches the
40// token may not write. A caller that may not administer it learns nothing it
41// could not already read: not found when it cannot see the repository,
42// unauthenticated when it is anonymous, forbidden otherwise.
43func adminAccess(p domain.Principal, repo domain.Repo) error {
44 switch {
45 case canAdmin(p, repo.Name) && p.IsBranchLimited():
46 return fmt.Errorf("%w: a token limited to branch prefixes may not administer a repository", domain.ErrForbidden)
47 case canAdmin(p, repo.Name):
48 return nil
49 case !canRead(p, repo):
50 return domain.ErrNotFound
51 case p.IsAnonymous():
52 return domain.ErrUnauthenticated
53 }
54 return domain.ErrForbidden
55}
56
57// canWrite is the access rule for pushing to a repository: repo:write on it.
58func canWrite(p domain.Principal, name domain.RepoName) bool {
59 return p.Allows(domain.ScopeRepoWrite, name)
60}
61
62// canCreateRepo is the access rule for creating a repository, by the API or on
63// first push: repo:write on the name, and no branch prefixes. A token limited
64// to some branches may push to a repository that exists; it never makes one.
65func canCreateRepo(p domain.Principal, name domain.RepoName) bool {
66 return canWrite(p, name) && !p.IsBranchLimited() && !p.ViaJobToken()
67}
68
69// canManageTokens is the access rule for creating, listing and revoking
70// stored tokens: token:admin, which root holds.
71func canManageTokens(p domain.Principal) bool {
72 return p.HasScope(domain.ScopeTokenAdmin)
73}
74
75// canAdmin is the access rule for deleting a repository and changing its
76// visibility, description or default branch: repo:admin on it.
77func canAdmin(p domain.Principal, name domain.RepoName) bool {
78 return p.Allows(domain.ScopeRepoAdmin, name)
79}
80
81// scopeAccess is the access rule for an action that needs a scope on a
82// repository the caller can already read, such as mr:write or mr:merge:
83// unauthenticated for an anonymous caller, forbidden for one without the scope
84// on this repository.
85func scopeAccess(p domain.Principal, repo domain.Repo, scope domain.Scope) error {
86 switch {
87 case p.Allows(scope, repo.Name):
88 return nil
89 case p.IsAnonymous():
90 return domain.ErrUnauthenticated
91 }
92 return fmt.Errorf("%w: this needs the %s scope on %s", domain.ErrForbidden, scope, repo.Name)
93}
94
95// authorOf names who acts, for the author of a merge request or a comment: the
96// label of the token, "root" for the root token, "owner" for the owner signed
97// in on the web and for any other caller without a label.
98func authorOf(p domain.Principal) string {
99 switch {
100 case p.IsOwner():
101 return "owner"
102 case p.IsRoot():
103 return "root"
104 case p.TokenLabel() != "":
105 return p.TokenLabel()
106 }
107 return "owner"
108}