Skip to content

Commit 407b6e6

Browse files
authored
allow the actions user to login via the jwt token (#32527)
We have some actions that leverage the Gitea API that began receiving 401 errors, with a message that the user was not found. These actions use the `ACTIONS_RUNTIME_TOKEN` env var in the actions job to authenticate with the Gitea API. The format of this env var in actions jobs changed with /pull/28885 to be a JWT (with a corresponding update to `act_runner`) Since it was a JWT, the OAuth parsing logic attempted to parse it as an OAuth token, and would return user not found, instead of falling back to look up the running task and assigning it to the actions user. Make ACTIONS_RUNTIME_TOKEN in action runners could be used, attempting to parse Oauth JWTs. The code to parse potential old `ACTION_RUNTIME_TOKEN` was kept in case someone is running an older version of act_runner that doesn't support the Actions JWT.
1 parent 56bff7a commit 407b6e6

File tree

4 files changed

+105
-3
lines changed

4 files changed

+105
-3
lines changed

models/fixtures/action_task.yml

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
-
2+
id: 46
3+
attempt: 3
4+
runner_id: 1
5+
status: 3 # 3 is the status code for "cancelled"
6+
started: 1683636528
7+
stopped: 1683636626
8+
repo_id: 4
9+
owner_id: 1
10+
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
11+
is_fork_pull_request: 0
12+
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa
13+
token_salt: eeeeeeee
14+
token_last_eight: eeeeeeee
15+
log_filename: artifact-test2/2f/47.log
16+
log_in_storage: 1
17+
log_length: 707
18+
log_size: 90179
19+
log_expired: 0
120
-
221
id: 47
322
job_id: 192

services/actions/auth.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
8383
return 0, fmt.Errorf("split token failed")
8484
}
8585

86-
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
86+
return TokenToTaskID(parts[1])
87+
}
88+
89+
// TokenToTaskID returns the TaskID associated with the provided JWT token
90+
func TokenToTaskID(token string) (int64, error) {
91+
parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) {
8792
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
8893
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
8994
}
@@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
9398
return 0, err
9499
}
95100

96-
c, ok := token.Claims.(*actionsClaims)
97-
if !token.Valid || !ok {
101+
c, ok := parsedToken.Claims.(*actionsClaims)
102+
if !parsedToken.Valid || !ok {
98103
return 0, fmt.Errorf("invalid token claim")
99104
}
100105

services/auth/oauth2.go

+23
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"code.gitea.io/gitea/modules/setting"
1818
"code.gitea.io/gitea/modules/timeutil"
1919
"code.gitea.io/gitea/modules/web/middleware"
20+
"code.gitea.io/gitea/services/actions"
2021
"code.gitea.io/gitea/services/oauth2_provider"
2122
)
2223

@@ -54,6 +55,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
5455
return grant.UserID
5556
}
5657

58+
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
59+
func CheckTaskIsRunning(ctx context.Context, taskID int64) bool {
60+
// Verify the task exists
61+
task, err := actions_model.GetTaskByID(ctx, taskID)
62+
if err != nil {
63+
return false
64+
}
65+
66+
// Verify that it's running
67+
return task.Status == actions_model.StatusRunning
68+
}
69+
5770
// OAuth2 implements the Auth interface and authenticates requests
5871
// (API requests only) by looking for an OAuth token in query parameters or the
5972
// "Authorization" header.
@@ -97,6 +110,16 @@ func parseToken(req *http.Request) (string, bool) {
97110
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
98111
// Let's see if token is valid.
99112
if strings.Contains(tokenSHA, ".") {
113+
// First attempt to decode an actions JWT, returning the actions user
114+
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
115+
if CheckTaskIsRunning(ctx, taskID) {
116+
store.GetData()["IsActionsToken"] = true
117+
store.GetData()["ActionsTaskID"] = taskID
118+
return user_model.ActionsUserID
119+
}
120+
}
121+
122+
// Otherwise, check if this is an OAuth access token
100123
uid := CheckOAuthAccessToken(ctx, tokenSHA)
101124
if uid != 0 {
102125
store.GetData()["IsApiToken"] = true

services/auth/oauth2_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package auth
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"code.gitea.io/gitea/models/unittest"
11+
user_model "code.gitea.io/gitea/models/user"
12+
"code.gitea.io/gitea/modules/web/middleware"
13+
"code.gitea.io/gitea/services/actions"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestUserIDFromToken(t *testing.T) {
19+
assert.NoError(t, unittest.PrepareTestDatabase())
20+
21+
t.Run("Actions JWT", func(t *testing.T) {
22+
const RunningTaskID = 47
23+
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
24+
assert.NoError(t, err)
25+
26+
ds := make(middleware.ContextData)
27+
28+
o := OAuth2{}
29+
uid := o.userIDFromToken(context.Background(), token, ds)
30+
assert.Equal(t, int64(user_model.ActionsUserID), uid)
31+
assert.Equal(t, ds["IsActionsToken"], true)
32+
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
33+
})
34+
}
35+
36+
func TestCheckTaskIsRunning(t *testing.T) {
37+
assert.NoError(t, unittest.PrepareTestDatabase())
38+
39+
cases := map[string]struct {
40+
TaskID int64
41+
Expected bool
42+
}{
43+
"Running": {TaskID: 47, Expected: true},
44+
"Missing": {TaskID: 1, Expected: false},
45+
"Cancelled": {TaskID: 46, Expected: false},
46+
}
47+
48+
for name := range cases {
49+
c := cases[name]
50+
t.Run(name, func(t *testing.T) {
51+
actual := CheckTaskIsRunning(context.Background(), c.TaskID)
52+
assert.Equal(t, c.Expected, actual)
53+
})
54+
}
55+
}

0 commit comments

Comments
 (0)