1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
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 vis == domain.VisibilityPublic {
24 return true
25 }
26 return p.Allows(domain.ScopeRepoRead, name)
27}
28
29// adminAccess is the access rule for changing or deleting a repository. A
30// caller that may not administer it learns nothing it could not already read:
31// not found when it cannot see the repository, unauthenticated when it is
32// anonymous, forbidden otherwise.
33func adminAccess(p domain.Principal, repo domain.Repo) error {
34 switch {
35 case canAdmin(p, repo.Name):
36 return nil
37 case !canRead(p, repo):
38 return domain.ErrNotFound
39 case p.IsAnonymous():
40 return domain.ErrUnauthenticated
41 }
42 return domain.ErrForbidden
43}
44
45// canWrite is the access rule for pushing to a repository, or creating it on
46// first push: repo:write on it.
47func canWrite(p domain.Principal, name domain.RepoName) bool {
48 return p.Allows(domain.ScopeRepoWrite, name)
49}
50
51// canManageTokens is the access rule for creating, listing and revoking
52// stored tokens: token:admin, which root holds.
53func canManageTokens(p domain.Principal) bool {
54 return p.HasScope(domain.ScopeTokenAdmin)
55}
56
57// canAdmin is the access rule for deleting a repository and changing its
58// visibility, description or default branch: repo:admin on it.
59func canAdmin(p domain.Principal, name domain.RepoName) bool {
60 return p.Allows(domain.ScopeRepoAdmin, name)
61}
62
63// scopeAccess is the access rule for an action that needs a scope on a
64// repository the caller can already read, such as mr:write or mr:merge:
65// unauthenticated for an anonymous caller, forbidden for one without the scope
66// on this repository.
67func scopeAccess(p domain.Principal, repo domain.Repo, scope domain.Scope) error {
68 switch {
69 case p.Allows(scope, repo.Name):
70 return nil
71 case p.IsAnonymous():
72 return domain.ErrUnauthenticated
73 }
74 return fmt.Errorf("%w: this needs the %s scope on %s", domain.ErrForbidden, scope, repo.Name)
75}
76
77// authorOf names who acts, for the author of a merge request or a comment: the
78// label of the token, "root" for the root token, "owner" for the owner signed
79// in on the web and for any other caller without a label.
80func authorOf(p domain.Principal) string {
81 switch {
82 case p.IsOwner():
83 return "owner"
84 case p.IsRoot():
85 return "root"
86 case p.TokenLabel() != "":
87 return p.TokenLabel()
88 }
89 return "owner"
90}