diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index 443effe08c..d88a8ed8a9 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -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 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c3639fb72e..122b70924a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -352,6 +352,7 @@ enable_update_checker = Enable Update Checker enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. env_config_keys = Environment Configuration env_config_keys_prompt = The following environment variables will also be applied to your configuration file: +config_write_file_prompt = These configuration options will be written into: [home] nav_menu = Navigation Menu diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index bb16858c81..1cea7d8c72 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -133,11 +133,6 @@ func DeleteBranch(ctx *context.APIContext) { branchName := ctx.PathParam("*") - if ctx.Repo.Repository.IsEmpty { - ctx.Error(http.StatusForbidden, "", "Git Repository is empty.") - return - } - // check whether branches of this repository has been synced totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{ RepoID: ctx.Repo.Repository.ID, diff --git a/routers/web/web.go b/routers/web/web.go index 607b644a42..e7f39d28bd 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -561,7 +561,7 @@ func registerRoutes(m *web.Router) { m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth) }, optSignInIgnoreCsrf, reqSignIn) - m.Methods("GET, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth) + m.Methods("GET, POST, OPTIONS", "/userinfo", optionsCorsHandler(), optSignInIgnoreCsrf, auth.InfoOAuth) m.Methods("POST, OPTIONS", "/access_token", optionsCorsHandler(), web.Bind(forms.AccessTokenForm{}), optSignInIgnoreCsrf, auth.AccessTokenOAuth) m.Methods("GET, OPTIONS", "/keys", optionsCorsHandler(), optSignInIgnoreCsrf, auth.OIDCKeys) m.Methods("POST, OPTIONS", "/introspect", optionsCorsHandler(), web.Bind(forms.IntrospectTokenForm{}), optSignInIgnoreCsrf, auth.IntrospectOAuth) diff --git a/services/actions/auth.go b/services/actions/auth.go index 8e934d89a8..1ef21f6e0e 100644 --- a/services/actions/auth.go +++ b/services/actions/auth.go @@ -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"]) } @@ -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") } diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index d0aec085b1..251ae5a244 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -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/oauth2_provider" ) @@ -54,6 +55,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. @@ -97,6 +110,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 diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go new file mode 100644 index 0000000000..75c231ff7a --- /dev/null +++ b/services/auth/oauth2_test.go @@ -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) + }) + } +} diff --git a/templates/install.tmpl b/templates/install.tmpl index 5055031a90..ea4023d409 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -338,7 +338,7 @@