1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "slices"
7 "strings"
8 "time"
9)
10
11// principalKind tells who a caller is.
12type principalKind uint8
13
14// Principal kinds. The zero value is anonymous, the least privileged.
15const (
16 kindAnonymous principalKind = iota
17 kindRoot
18 kindToken
19 kindOwner
20)
21
22// Principal is the authenticated identity of a caller: nobody, the root
23// token, the owner signed in on the web, or a stored token with its scopes
24// and repositories.
25type Principal struct {
26 kind principalKind
27 tokenID string
28 scopes []Scope
29 repos []RepoName
30 // branchPrefixes limits the branches a token may write; empty = all.
31 branchPrefixes []string
32 expiresAt time.Time
33 label string
34}
35
36// Anonymous returns the principal of a caller without credentials.
37func Anonymous() Principal { return Principal{kind: kindAnonymous} }
38
39// Root returns the principal of the admin token holder.
40func Root() Principal { return Principal{kind: kindRoot} }
41
42// Owner returns the principal of the owner's web session (ADR 0018): the
43// rights of root, labelled "owner" in logs and messages. Only the web
44// middleware creates it, for a valid session cookie; no secret maps to it.
45func Owner() Principal { return Principal{kind: kindOwner} }
46
47// TokenPrincipal returns the principal of the holder of a stored token.
48func TokenPrincipal(t Token) Principal {
49 return Principal{
50 kind: kindToken,
51 tokenID: t.ID,
52 scopes: slices.Clone(t.Scopes),
53 repos: slices.Clone(t.Repos),
54 branchPrefixes: slices.Clone(t.BranchPrefixes),
55 expiresAt: t.ExpiresAt,
56 label: t.Label,
57 }
58}
59
60// Label names the kind of principal for logs and messages: "anonymous",
61// "root", "owner" or "token".
62func (p Principal) Label() string {
63 switch p.kind {
64 case kindRoot:
65 return "root"
66 case kindOwner:
67 return "owner"
68 case kindToken:
69 return "token"
70 }
71 return "anonymous"
72}
73
74// IsRoot reports whether p may do everything: the root token, or the owner
75// signed in on the web.
76func (p Principal) IsRoot() bool { return p.kind == kindRoot || p.kind == kindOwner }
77
78// HasOwnerRights reports whether p is the owner: root, the owner's web
79// session, or a stored token that holds every scope and is limited neither to
80// a list of repositories nor to branch prefixes. It decides who may manage browser sessions.
81func (p Principal) HasOwnerRights() bool {
82 if p.IsRoot() {
83 return true
84 }
85 if p.kind != kindToken || len(p.repos) > 0 || p.IsBranchLimited() {
86 return false
87 }
88 for _, s := range AllScopes() {
89 if !slices.Contains(p.scopes, s) {
90 return false
91 }
92 }
93 return true
94}
95
96// IsOwner reports whether p is the owner signed in on the web.
97func (p Principal) IsOwner() bool { return p.kind == kindOwner }
98
99// IsAnonymous reports whether p has no credentials.
100func (p Principal) IsAnonymous() bool { return p.kind == kindAnonymous }
101
102// TokenID returns the ID of the stored token, or "" for other principals.
103func (p Principal) TokenID() string { return p.tokenID }
104
105// Scopes returns the scopes of a stored token; nil for other principals.
106func (p Principal) Scopes() []Scope { return slices.Clone(p.scopes) }
107
108// Repos returns the repository list of a stored token; empty means every
109// repository.
110func (p Principal) Repos() []RepoName { return slices.Clone(p.repos) }
111
112// BranchPrefixes returns the branch prefixes of a stored token; empty means
113// every branch.
114func (p Principal) BranchPrefixes() []string { return slices.Clone(p.branchPrefixes) }
115
116// IsBranchLimited reports whether p is limited to branch prefixes. A limited
117// principal has no owner rights, creates no repository and administers none.
118func (p Principal) IsBranchLimited() bool { return len(p.branchPrefixes) > 0 }
119
120// CoversBranchPrefix reports whether s lies under one of the branch prefixes of
121// p: s starts with one of them. An empty prefix, which ParseBranchPrefix never
122// lets in, covers nothing instead of everything, so that a corrupt record
123// limits a token and never widens it. It is false without prefixes: callers
124// test IsBranchLimited first.
125func (p Principal) CoversBranchPrefix(s string) bool {
126 for _, prefix := range p.branchPrefixes {
127 if prefix != "" && strings.HasPrefix(s, prefix) {
128 return true
129 }
130 }
131 return false
132}
133
134// AllowsRef reports whether p may write the ref (create, update or delete
135// it). Without branch prefixes every ref is allowed. With them, only a branch
136// "refs/heads/<name>" under one of the prefixes is: tags, every other ref and
137// a branch outside the prefixes are not.
138//
139// It judges the name of the ref only, and rests on two assumptions about the
140// repositories ermite serves. First, no symbolic ref lives under refs/heads
141// (HEAD is the only symref): ermite never creates one and receive-pack cannot,
142// so a name under a prefix never points at a branch outside it. Second, ref
143// names are stored case-sensitively (Linux file systems): "refs/heads/Agent/x"
144// is not "refs/heads/agent/x". On a case-insensitive file system the two could
145// name the same ref and the prefixes would not hold.
146func (p Principal) AllowsRef(ref string) bool {
147 if !p.IsBranchLimited() {
148 return true
149 }
150 name, ok := strings.CutPrefix(ref, "refs/heads/")
151 if !ok {
152 return false
153 }
154 if _, err := ParseBranchName(name); err != nil {
155 return false
156 }
157 return p.CoversBranchPrefix(name)
158}
159
160// ExpiresAt returns the expiry of a stored token; zero means none.
161func (p Principal) ExpiresAt() time.Time { return p.expiresAt }
162
163// HasScope reports whether p holds the scope, for some repository. Root and
164// the owner hold every scope; anonymous none.
165func (p Principal) HasScope(s Scope) bool {
166 switch p.kind {
167 case kindRoot, kindOwner:
168 return true
169 case kindToken:
170 return slices.Contains(p.scopes, s)
171 }
172 return false
173}
174
175// Allows reports whether p holds the scope on the repository: the scope, and
176// the repository within the token's list when it has one.
177func (p Principal) Allows(s Scope, repo RepoName) bool {
178 if !p.HasScope(s) {
179 return false
180 }
181 if p.kind == kindToken && len(p.repos) > 0 {
182 return slices.Contains(p.repos, repo)
183 }
184 return true
185}
186
187// TokenLabel returns the label of the stored token, or "" for other
188// principals.
189func (p Principal) TokenLabel() string { return p.label }