1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "maps"
7 "strings"
8 "time"
9)
10
11// RepoTokenPrefix starts the secret of a job repository token: the
12// "github.token" of a job, or the token of one action archive. Unlike the
13// stored tokens (TokenSecretPrefix) it is never created, listed or revoked by
14// a person: the CI engine issues it with a job and revokes it with the job.
15const RepoTokenPrefix = "ermitegh_"
16
17// IsRepoToken reports whether s has the shape of a job repository token.
18func IsRepoToken(s string) bool { return hasTokenShape(s, RepoTokenPrefix) }
19
20// repoTokenIDPrefix starts the TokenID of the principal of a job repository
21// token, so that it never collides with the ID of a stored token.
22const repoTokenIDPrefix = "job:"
23
24// IsRepoTokenID reports whether id is the TokenID of the principal of a job
25// repository token (see RepoTokenPrincipal).
26func IsRepoTokenID(id string) bool { return strings.HasPrefix(id, repoTokenIDPrefix) }
27
28// CIRepoToken is a job repository token: it reads one repository, until
29// ExpiresAt or the end of its job. The secret is never stored, only its hash.
30type CIRepoToken struct {
31 // ID identifies the token; it is not secret.
32 ID string
33 Hash TokenHash
34 // Repo is the one repository the token reads.
35 Repo RepoName
36 // RunID, RunAttempt and JobID name the job it belongs to.
37 RunID int64
38 RunAttempt int32
39 JobID string
40 // Archive is set for the token of an action archive, unset for the job's
41 // own token ("github.token"). An archive token downloads the commit Sha of
42 // Repo and nothing else.
43 Archive bool
44 // Sha is the commit an archive token is bound to; empty for the job's own.
45 Sha string
46 // OIDC is set for the ID token request token of the job
47 // (OIDCRequestTokenPrefix): it only asks the token endpoint for the job's
48 // ID tokens, and authenticates nothing else (RepoTokenPrincipal).
49 OIDC bool
50 // Permissions are the permissions of the job's own token (key to level, see
51 // TokenPermissions). Empty: the default, which reads the repository.
52 Permissions map[string]string
53 ExpiresAt time.Time
54 CreatedAt time.Time
55}
56
57// Expired reports whether the token is no longer valid at now.
58func (t CIRepoToken) Expired(now time.Time) bool { return !now.Before(t.ExpiresAt) }
59
60// RepoTokenGrace is how long a job repository token outlives the deadline of
61// its job at the most (a safety net: it is revoked when the job ends).
62const RepoTokenGrace = 5 * time.Minute
63
64// RepoTokenPrincipal returns the principal of the holder of a job repository
65// token.
66//
67// The job's own token is shaped like a stored token (scopes on one repository,
68// an expiry), so that every authorization rule of the forge applies to it
69// unchanged. Its scopes follow its permissions (RepoTokenScopes): by default it
70// reads that repository, even when private, and writes nothing; with
71// contents: write it also pushes to it. It has no branch prefixes: a run whose
72// actor is limited to branches is not trusted and holds no write permission.
73//
74// An archive token holds no scope at all: it is the grant to download one
75// commit of one repository as an archive, which only the archive endpoint
76// reads (Principal.ArchiveGrant). Git over HTTP, the repository endpoint and
77// the rest of the API see a caller with no rights, so a private repository
78// does not show itself to it.
79//
80// Both have a TokenID that starts with "job:" and matches no stored token.
81//
82// An ID token request token is nobody: it is anonymous everywhere but at the
83// token endpoint, which looks it up itself.
84func RepoTokenPrincipal(t CIRepoToken) Principal {
85 if t.OIDC {
86 return Anonymous()
87 }
88 if t.Archive {
89 return Principal{
90 kind: kindArchive, tokenID: repoTokenIDPrefix + t.ID, label: "ci archive", expiresAt: t.ExpiresAt,
91 archiveRepo: t.Repo, archiveSha: t.Sha,
92 }
93 }
94 p := TokenPrincipal(Token{
95 ID: repoTokenIDPrefix + t.ID, Label: "ci job", Scopes: RepoTokenScopes(t.Permissions),
96 Repos: []RepoName{t.Repo}, ExpiresAt: t.ExpiresAt, CreatedAt: t.CreatedAt,
97 })
98 p.jobPerms = maps.Clone(t.Permissions)
99 if len(p.jobPerms) == 0 {
100 p.jobPerms = DefaultTokenPermissions()
101 }
102 return p
103}
104
105// CIPusherKind tells who pushed the commit of a run.
106type CIPusherKind string
107
108// Pusher kinds. The zero value is a run stored before the pusher was recorded.
109const (
110 CIPusherNone CIPusherKind = ""
111 CIPusherRoot CIPusherKind = "root"
112 CIPusherOwner CIPusherKind = "owner"
113 CIPusherToken CIPusherKind = "token"
114)
115
116// CIPusher is who pushed the commit of a run, as recorded with the run. A
117// token is recorded by its ID only: what it may read is looked up when it is
118// needed, so a token revoked since no longer reads anything on behalf of the
119// run.
120type CIPusher struct {
121 Kind CIPusherKind
122 TokenID string
123}
124
125// PusherOf records the pusher p. Anonymous callers, which cannot push, and the
126// holders of job tokens, which cannot either, are recorded as nobody.
127func PusherOf(p Principal) CIPusher {
128 switch {
129 case p.IsOwner():
130 return CIPusher{Kind: CIPusherOwner}
131 case p.IsRoot():
132 return CIPusher{Kind: CIPusherRoot}
133 case p.TokenID() != "" && !IsRepoTokenID(p.TokenID()):
134 return CIPusher{Kind: CIPusherToken, TokenID: p.TokenID()}
135 }
136 return CIPusher{}
137}
138
139// CIActionSource says where an action was found.
140type CIActionSource int
141
142// Action sources, with the numbers of ActionSource in the protocol.
143const (
144 CIActionGitHub CIActionSource = 1
145 CIActionErmite CIActionSource = 2
146)
147
148// CIActionResolution is the resolution of an action reference for one run
149// attempt: every job of the attempt gets the same commit.
150type CIActionResolution struct {
151 RunID int64
152 RunAttempt int32
153 // Key is "owner/repo@ref", lower-cased.
154 Key string
155 NameWithOwner string
156 ResolvedSHA string
157 Source CIActionSource
158}