mirror of https://github.com/go-gitea/gitea.git
Backport #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 go-gitea/gitea/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.
This commit is contained in:
parent
81ec66c257
commit
0b5da27570
|
@ -1,3 +1,22 @@
|
||||||
|
-
|
||||||
|
id: 46
|
||||||
|
attempt: 3
|
||||||
|
runner_id: 1
|
||||||
|
status: 3 # 3 is the status code for "cancelled"
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4260c64a69a2cc1508825121b7b8394e48e00b1bf8718b2aaaaa
|
||||||
|
token_salt: eeeeeeee
|
||||||
|
token_last_eight: eeeeeeee
|
||||||
|
log_filename: artifact-test2/2f/47.log
|
||||||
|
log_in_storage: 1
|
||||||
|
log_length: 707
|
||||||
|
log_size: 90179
|
||||||
|
log_expired: 0
|
||||||
-
|
-
|
||||||
id: 47
|
id: 47
|
||||||
job_id: 192
|
job_id: 192
|
||||||
|
|
|
@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||||
return 0, fmt.Errorf("split token failed")
|
return 0, fmt.Errorf("split token failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := jwt.ParseWithClaims(parts[1], &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
return TokenToTaskID(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenToTaskID returns the TaskID associated with the provided JWT token
|
||||||
|
func TokenToTaskID(token string) (int64, error) {
|
||||||
|
parsedToken, err := jwt.ParseWithClaims(token, &actionsClaims{}, func(t *jwt.Token) (any, error) {
|
||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||||
}
|
}
|
||||||
|
@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, ok := token.Claims.(*actionsClaims)
|
c, ok := parsedToken.Claims.(*actionsClaims)
|
||||||
if !token.Valid || !ok {
|
if !parsedToken.Valid || !ok {
|
||||||
return 0, fmt.Errorf("invalid token claim")
|
return 0, fmt.Errorf("invalid token claim")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
"code.gitea.io/gitea/services/actions"
|
||||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,6 +53,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
|
||||||
return grant.UserID
|
return grant.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckTaskIsRunning verifies that the TaskID corresponds to a running task
|
||||||
|
func CheckTaskIsRunning(ctx context.Context, taskID int64) bool {
|
||||||
|
// Verify the task exists
|
||||||
|
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that it's running
|
||||||
|
return task.Status == actions_model.StatusRunning
|
||||||
|
}
|
||||||
|
|
||||||
// OAuth2 implements the Auth interface and authenticates requests
|
// OAuth2 implements the Auth interface and authenticates requests
|
||||||
// (API requests only) by looking for an OAuth token in query parameters or the
|
// (API requests only) by looking for an OAuth token in query parameters or the
|
||||||
// "Authorization" header.
|
// "Authorization" header.
|
||||||
|
@ -95,6 +108,16 @@ func parseToken(req *http.Request) (string, bool) {
|
||||||
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
|
||||||
// Let's see if token is valid.
|
// Let's see if token is valid.
|
||||||
if strings.Contains(tokenSHA, ".") {
|
if strings.Contains(tokenSHA, ".") {
|
||||||
|
// First attempt to decode an actions JWT, returning the actions user
|
||||||
|
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
|
||||||
|
if CheckTaskIsRunning(ctx, taskID) {
|
||||||
|
store.GetData()["IsActionsToken"] = true
|
||||||
|
store.GetData()["ActionsTaskID"] = taskID
|
||||||
|
return user_model.ActionsUserID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, check if this is an OAuth access token
|
||||||
uid := CheckOAuthAccessToken(ctx, tokenSHA)
|
uid := CheckOAuthAccessToken(ctx, tokenSHA)
|
||||||
if uid != 0 {
|
if uid != 0 {
|
||||||
store.GetData()["IsApiToken"] = true
|
store.GetData()["IsApiToken"] = true
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
"code.gitea.io/gitea/services/actions"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserIDFromToken(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
t.Run("Actions JWT", func(t *testing.T) {
|
||||||
|
const RunningTaskID = 47
|
||||||
|
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
ds := make(middleware.ContextData)
|
||||||
|
|
||||||
|
o := OAuth2{}
|
||||||
|
uid := o.userIDFromToken(context.Background(), token, ds)
|
||||||
|
assert.Equal(t, int64(user_model.ActionsUserID), uid)
|
||||||
|
assert.Equal(t, ds["IsActionsToken"], true)
|
||||||
|
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckTaskIsRunning(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
TaskID int64
|
||||||
|
Expected bool
|
||||||
|
}{
|
||||||
|
"Running": {TaskID: 47, Expected: true},
|
||||||
|
"Missing": {TaskID: 1, Expected: false},
|
||||||
|
"Cancelled": {TaskID: 46, Expected: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range cases {
|
||||||
|
c := cases[name]
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
actual := CheckTaskIsRunning(context.Background(), c.TaskID)
|
||||||
|
assert.Equal(t, c.Expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue