Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow the actions user to login via the jwt token (#32527) #32580

Merged
merged 2 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions models/fixtures/action_task.yml
Original file line number Diff line number Diff line change
@@ -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
job_id: 192
Expand Down
11 changes: 8 additions & 3 deletions services/actions/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
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 {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
Expand All @@ -93,8 +98,8 @@ func ParseAuthorizationToken(req *http.Request) (int64, error) {
return 0, err
}

c, ok := token.Claims.(*actionsClaims)
if !token.Valid || !ok {
c, ok := parsedToken.Claims.(*actionsClaims)
if !parsedToken.Valid || !ok {
return 0, fmt.Errorf("invalid token claim")
}

Expand Down
23 changes: 23 additions & 0 deletions services/auth/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth/source/oauth2"
)

Expand Down Expand Up @@ -52,6 +53,18 @@ func CheckOAuthAccessToken(ctx context.Context, accessToken string) int64 {
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
// (API requests only) by looking for an OAuth token in query parameters or the
// "Authorization" header.
Expand Down Expand Up @@ -95,6 +108,16 @@ func parseToken(req *http.Request) (string, bool) {
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
// Let's see if token is valid.
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)
if uid != 0 {
store.GetData()["IsApiToken"] = true
Expand Down
55 changes: 55 additions & 0 deletions services/auth/oauth2_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}