v0.13.0 dc42452

ci_permissions.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5// The permissions of a job repository token (CI-3, D3) are the keys and levels
6// of the "permissions:" of a GitHub workflow. They are computed once, when the
7// job is planned, and stored as a map from key to level; a key that is absent
8// from the map is "none". What they allow on ermite is in RepoTokenScopes.
9
10// Permission levels, as a workflow spells them.
11const (
12	PermissionNone  = "none"
13	PermissionRead  = "read"
14	PermissionWrite = "write"
15)
16
17// Shorthands of a "permissions:" that is a string.
18const (
19	PermissionsReadAll  = "read-all"
20	PermissionsWriteAll = "write-all"
21)
22
23// Permission keys ermite acts on.
24const (
25	PermContents     = "contents"
26	PermPullRequests = "pull-requests"
27	// PermMetadata is always read: a workflow cannot set it.
28	PermMetadata = "metadata"
29)
30
31// permissionKey describes a key of "permissions:" (workflow-v1.0.json,
32// permissions-mapping).
33type permissionKey struct {
34	name string
35	// levels tell what the key accepts besides none: "read", "write" or both.
36	canRead, canWrite bool
37	// writeAll is the level "write-all" grants.
38	writeAll string
39}
40
41// permissionKeys are the keys a workflow can set. "write-all" keeps models and
42// vulnerability-alerts at read, as GitHub does; "read-all" grants read to the
43// keys that have one.
44var permissionKeys = []permissionKey{
45	{"actions", true, true, PermissionWrite},
46	{"artifact-metadata", true, true, PermissionWrite},
47	{"attestations", true, true, PermissionWrite},
48	{"checks", true, true, PermissionWrite},
49	{"code-quality", true, true, PermissionWrite},
50	{PermContents, true, true, PermissionWrite},
51	{"copilot-requests", false, true, PermissionWrite},
52	{"deployments", true, true, PermissionWrite},
53	{"discussions", true, true, PermissionWrite},
54	{"drives", true, true, PermissionWrite},
55	{"id-token", false, true, PermissionWrite},
56	{"issues", true, true, PermissionWrite},
57	{"models", true, false, PermissionRead},
58	{"packages", true, true, PermissionWrite},
59	{"pages", true, true, PermissionWrite},
60	{PermPullRequests, true, true, PermissionWrite},
61	{"repository-projects", true, true, PermissionWrite},
62	{"security-events", true, true, PermissionWrite},
63	{"statuses", true, true, PermissionWrite},
64	{"vulnerability-alerts", true, false, PermissionRead},
65}
66
67func findPermissionKey(name string) (permissionKey, bool) {
68	for _, k := range permissionKeys {
69		if k.name == name {
70			return k, true
71		}
72	}
73	return permissionKey{}, false
74}
75
76// PermissionSpec is a "permissions:" as written in a workflow or a job: a
77// shorthand (read-all, write-all) or a level per key. A nil *PermissionSpec is
78// no "permissions:" at all; an empty spec is "permissions: {}".
79type PermissionSpec struct {
80	All    string
81	Levels map[string]string
82}
83
84// DefaultTokenPermissions are the permissions of a token whose workflow says
85// nothing: GitHub's restricted default (ermite offers no permissive setting).
86func DefaultTokenPermissions() map[string]string {
87	return map[string]string{PermContents: PermissionRead, PermMetadata: PermissionRead, "packages": PermissionRead}
88}
89
90// ResolvePermissions returns the permissions a job asks for: the job's
91// "permissions:" replaces the workflow's as a whole (GitHub does not merge
92// them), and metadata is always read. It returns nil when neither says anything:
93// the job then gets the default (TokenPermissions). A key or level ermite does
94// not know is none. The trust rule of the run is not applied here: it is applied
95// when the token is issued (TokenPermissions), so that a re-run of the same job
96// by another caller lowers the token without planning again.
97func ResolvePermissions(workflow, job *PermissionSpec) map[string]string {
98	spec := job
99	if spec == nil {
100		spec = workflow
101	}
102	if spec == nil {
103		return nil
104	}
105	out := map[string]string{PermMetadata: PermissionRead}
106	switch spec.All {
107	case PermissionsReadAll:
108		for _, k := range permissionKeys {
109			if k.canRead {
110				out[k.name] = PermissionRead
111			}
112		}
113	case PermissionsWriteAll:
114		for _, k := range permissionKeys {
115			out[k.name] = k.writeAll
116		}
117	default:
118		for name, level := range spec.Levels {
119			k, ok := findPermissionKey(name)
120			switch {
121			case !ok, level == PermissionNone:
122			case level == PermissionRead && k.canRead, level == PermissionWrite && k.canWrite:
123				out[name] = level
124			}
125		}
126	}
127	return out
128}
129
130// TokenPermissions returns the permissions a job token is issued with, given
131// those stored with the job: the default when none are stored (a job planned
132// before they existed), and, for a run that is not trusted (D1), every write
133// lowered to read (to none for a key that has no read). It copies its input.
134func TokenPermissions(stored map[string]string, trusted bool) map[string]string {
135	if len(stored) == 0 {
136		stored = DefaultTokenPermissions()
137	}
138	out := make(map[string]string, len(stored)+1)
139	for name, level := range stored {
140		if level == PermissionWrite && !trusted {
141			if k, ok := findPermissionKey(name); ok && !k.canRead {
142				continue
143			}
144			level = PermissionRead
145		}
146		if level == PermissionRead || level == PermissionWrite {
147			out[name] = level
148		}
149	}
150	out[PermMetadata] = PermissionRead
151	return out
152}
153
154// RepoTokenScopes maps the permissions of a job token to the scopes of the
155// principal on the run's repository: contents read is repo:read, contents write
156// adds repo:write, pull-requests write is mr:write. Never repo:admin, mr:merge,
157// secrets, tokens or CI scopes, whatever the permissions say. Permissions
158// without an effect on ermite grant nothing.
159func RepoTokenScopes(perms map[string]string) []Scope {
160	if len(perms) == 0 {
161		perms = DefaultTokenPermissions()
162	}
163	var scopes []Scope
164	switch perms[PermContents] {
165	case PermissionWrite:
166		scopes = append(scopes, ScopeRepoRead, ScopeRepoWrite)
167	case PermissionRead:
168		scopes = append(scopes, ScopeRepoRead)
169	}
170	if perms[PermPullRequests] == PermissionWrite {
171		scopes = append(scopes, ScopeMRWrite)
172	}
173	return scopes
174}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 43

↑↓ moveenter openesc close