v0.4.2 257b319

idtoken_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"strings"
7	"testing"
8)
9
10const (
11	idTokenTestURL   = "http://ermite.example:8088/-/oidc/token?api-version=2.0"
12	idTokenTestToken = "ermiteoidc_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
13)
14
15func withIDToken(c *Config) {
16	c.IDTokenURL, c.IDTokenRequestToken = idTokenTestURL, idTokenTestToken
17}
18
19const idTokenEcho = `echo "oidc url=[${ACTIONS_ID_TOKEN_REQUEST_URL-unset}] token=[${ACTIONS_ID_TOKEN_REQUEST_TOKEN-unset}]"`
20
21const idTokenAction = "name: e\ndescription: d\nruns:\n  using: node20\n  main: main.sh\n"
22
23var idTokenWant = "oidc url=[" + idTokenTestURL + "] token=[***]"
24
25const idTokenUnset = "oidc url=[unset] token=[unset]"
26
27// ScriptHandler.cs:315-319: a run step gets ACTIONS_ID_TOKEN_REQUEST_URL and
28// ACTIONS_ID_TOKEN_REQUEST_TOKEN in its process, the token masked. The env of
29// the step (its header, the env context) does not show them.
30func TestRunStepGetsIDTokenEnv(t *testing.T) {
31	rec, res := actionRun(t, files{},
32		"      - name: r\n        env:\n          FOO: bar\n        run: |\n          "+idTokenEcho+"\n          echo \"ctx=[${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }}]\"\n", withIDToken)
33	if res.Conclusion != ConclusionSuccess {
34		t.Fatalf("%+v", res)
35	}
36	got := logOf(rec.step(t, "r"))
37	if !strings.Contains(got, idTokenWant) || !strings.Contains(got, "ctx=[]") {
38		t.Errorf("want %q and an empty env context in\n%s", idTokenWant, got)
39	}
40	if strings.Contains(got, idTokenTestToken) || strings.Contains(got, "  ACTIONS_ID_TOKEN_REQUEST_URL:") {
41		t.Errorf("the token or the variables are in the log:\n%s", got)
42	}
43}
44
45// NodeScriptActionHandler.cs:66-70: a node action gets them too, without a
46// cache service, and they are not in the env block of its header.
47func TestNodeActionGetsIDTokenEnv(t *testing.T) {
48	rec, res := actionRun(t, files{".github/actions/e/action.yml": idTokenAction, ".github/actions/e/main.sh": idTokenEcho + "\n"},
49		"      - uses: ./.github/actions/e\n", withIDToken)
50	if res.Conclusion != ConclusionSuccess {
51		t.Fatalf("%+v", res)
52	}
53	got := logOf(rec.step(t, "Run ./.github/actions/e"))
54	if !strings.Contains(got, idTokenWant) {
55		t.Errorf("want %q in\n%s", idTokenWant, got)
56	}
57	if strings.Contains(got, idTokenTestToken) || strings.Contains(got, "  ACTIONS_ID_TOKEN_REQUEST_URL:") {
58		t.Errorf("the token or the variables are in the log:\n%s", got)
59	}
60}
61
62// A composite action sets nothing itself; its steps get the variables from
63// their own handlers, as on GitHub.
64func TestCompositeStepsGetIDTokenEnv(t *testing.T) {
65	composite := "name: c\ndescription: d\nruns:\n  using: composite\n  steps:\n    - shell: bash\n      run: '" + idTokenEcho + "'\n"
66	rec, res := actionRun(t, files{".github/actions/c/action.yml": composite}, "      - uses: ./.github/actions/c\n", withIDToken)
67	if res.Conclusion != ConclusionSuccess {
68		t.Fatalf("%+v", res)
69	}
70	if got := logOf(rec.step(t, "Run ./.github/actions/c")); !strings.Contains(got, idTokenWant) || strings.Contains(got, idTokenTestToken) {
71		t.Errorf("want %q in\n%s", idTokenWant, got)
72	}
73}
74
75// ContainerActionHandler.cs:232-236: a docker action gets them by name on the
76// "docker run" command line, never by value.
77func TestDockerActionGetsIDTokenEnv(t *testing.T) {
78	d, lf := fakeDocker(t, 0)
79	rec, res := actionRun(t, files{}, "      - uses: docker://alpine:3.20\n", func(c *Config) {
80		c.Actions.DockerPath = d
81		withIDToken(c)
82	})
83	if res.Conclusion != ConclusionSuccess {
84		t.Fatalf("%+v %v", res, rec.names())
85	}
86	cs := calls(t, lf)
87	if len(cs) != 2 {
88		t.Fatalf("calls %v", cs)
89	}
90	args := "\n" + strings.Join(cs[1], "\n") + "\n"
91	for _, n := range []string{"ACTIONS_ID_TOKEN_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"} {
92		if !strings.Contains(args, "\n-e\n"+n+"\n") {
93			t.Errorf("docker run lacks -e %s:\n%s", n, args)
94		}
95	}
96	if strings.Contains(args, idTokenTestToken) {
97		t.Errorf("the token value is on the command line:\n%s", args)
98	}
99}
100
101// A run step in the job container gets them through "docker exec -e NAME".
102func TestRunStepInContainerGetsIDTokenEnv(t *testing.T) {
103	f := newFakeCtr(t)
104	rec, res := ctrRun(t, f, "    container: ubuntu:24.04\n", "      - name: s\n        run: echo hi\n", withIDToken)
105	if res.Conclusion != ConclusionSuccess {
106		t.Fatalf("%+v %v", res, rec.names())
107	}
108	ex := f.execCalls(t)
109	if len(ex) != 1 {
110		t.Fatalf("exec calls %v", ex)
111	}
112	c := "\n" + strings.Join(ex[0], "\n") + "\n"
113	for _, n := range []string{"ACTIONS_ID_TOKEN_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"} {
114		if !strings.Contains(c, "\n-e\n"+n+"\n") {
115			t.Errorf("exec lacks -e %s:\n%s", n, c)
116		}
117	}
118	if strings.Contains(c, idTokenTestToken) {
119		t.Errorf("the token value is on the command line:\n%s", c)
120	}
121}
122
123// Nothing is set unless the job has both the URL and the token.
124func TestIDTokenEnvNeedsBoth(t *testing.T) {
125	for name, tweak := range map[string]func(*Config){
126		"none":       nil,
127		"url only":   func(c *Config) { c.IDTokenURL = idTokenTestURL },
128		"token only": func(c *Config) { c.IDTokenRequestToken = idTokenTestToken },
129	} {
130		t.Run(name, func(t *testing.T) {
131			rec, res := actionRun(t, files{".github/actions/e/action.yml": idTokenAction, ".github/actions/e/main.sh": idTokenEcho + "\n"},
132				"      - name: r\n        run: '"+idTokenEcho+"'\n      - uses: ./.github/actions/e\n", tweak)
133			if res.Conclusion != ConclusionSuccess {
134				t.Fatalf("%+v", res)
135			}
136			for _, s := range []string{"r", "Run ./.github/actions/e"} {
137				if got := logOf(rec.step(t, s)); !strings.Contains(got, idTokenUnset) {
138					t.Errorf("%s: want %q in\n%s", s, idTokenUnset, got)
139				}
140			}
141		})
142	}
143}
144
145// The token is in the masker before the first step: a step that prints it,
146// even by another name, shows ***.
147func TestIDTokenIsMasked(t *testing.T) {
148	rec, res := actionRun(t, files{}, "      - name: r\n        run: echo \"leak "+idTokenTestToken+"\"\n", withIDToken)
149	if res.Conclusion != ConclusionSuccess {
150		t.Fatalf("%+v", res)
151	}
152	for _, s := range rec.steps {
153		if got := logOf(s); strings.Contains(got, idTokenTestToken) {
154			t.Errorf("the token is in the log:\n%s", got)
155		}
156	}
157	if got := logOf(rec.step(t, "r")); !strings.Contains(got, "leak ***") {
158		t.Errorf("want the token masked in\n%s", got)
159	}
160}

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 4

Tags 11

↑↓ moveenter openesc close