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