diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index aa2fcee765507..046c8a7351ba8 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -2836,6 +2836,8 @@ LEVEL = Info ;ABANDONED_JOB_TIMEOUT = 24h ;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow ;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip] +;; Enable/Disable global secrets +;GLOBAL_SECRETS_ENABLED = false ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/models/secret/secret.go b/models/secret/secret.go index 10a0287dfd961..0ec2ba43bee4f 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -25,15 +25,13 @@ import ( // It can be: // 1. org/user level secret, OwnerID is org/user ID and RepoID is 0 // 2. repo level secret, OwnerID is 0 and RepoID is repo ID +// 3. global level secret, OwnerID is 0 and RepoID is 0 // // Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero, // or it will be complicated to find secrets belonging to a specific owner. // For example, conditions like `OwnerID = 1` will also return secret {OwnerID: 1, RepoID: 1}, // but it's a repo level secret, not an org/user level secret. // To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level secrets. -// -// Please note that it's not acceptable to have both OwnerID and RepoID to zero, global secrets are not supported. -// It's for security reasons, admin may be not aware of that the secrets could be stolen by any user when setting them as global. type Secret struct { ID int64 OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"` @@ -69,9 +67,6 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat // Remove OwnerID to avoid confusion; it's not worth returning an error here. ownerID = 0 } - if ownerID == 0 && repoID == 0 { - return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument) - } if len(data) > SecretDataMaxLength { return nil, util.NewInvalidArgumentErrorf("data too long") @@ -108,14 +103,8 @@ type FindSecretsOptions struct { func (opts FindSecretsOptions) ToConds() builder.Cond { cond := builder.NewCond() - + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - if opts.RepoID != 0 { // if RepoID is set - // ignore OwnerID and treat it as 0 - cond = cond.And(builder.Eq{"owner_id": 0}) - } else { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) - } if opts.SecretID != 0 { cond = cond.And(builder.Eq{"id": opts.SecretID}) @@ -164,6 +153,11 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[ return secrets, nil } + globalSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: 0, RepoID: 0}) + if err != nil { + log.Error("find global secrets: %v", err) + return nil, err + } ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) if err != nil { log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) @@ -175,7 +169,8 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[ return nil, err } - for _, secret := range append(ownerSecrets, repoSecrets...) { + // Level precedence: Repo > Org / User > Global + for _, secret := range append(globalSecrets, append(ownerSecrets, repoSecrets...)...) { v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data) if err != nil { log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) diff --git a/modules/setting/actions.go b/modules/setting/actions.go index 8bace1f75044d..75dbb16e605f6 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -25,10 +25,12 @@ var ( EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"` AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` SkipWorkflowStrings []string `ìni:"SKIP_WORKFLOW_STRINGS"` + GlobalSecretsEnabled bool `ìni:"GLOBAL_SECRETS_ENABLED"` }{ - Enabled: true, - DefaultActionsURL: defaultActionsURLGitHub, - SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, + Enabled: true, + DefaultActionsURL: defaultActionsURLGitHub, + SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, + GlobalSecretsEnabled: false, } ) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 53fbf0af767ad..a36058a46f943 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3777,6 +3777,7 @@ deletion.description = Removing a secret is permanent and cannot be undone. Cont deletion.success = The secret has been removed. deletion.failed = Failed to remove secret. management = Secrets Management +instance_desc = Although secrets will be masked if users try to print them in Actions workflows, this is not absolutely secure. Users can still obtain the contents of secrets by writing malicious workflows, so please ensure that global secrets are not used by people you do not trust. Otherwise, please use organization/user-level or repository-level secrets to limit their scope of use. Alternatively, if it's acceptable to expose their contents, please use global variables. [actions] actions = Actions diff --git a/routers/api/v1/admin/action.go b/routers/api/v1/admin/action.go index 2fbb8e1a95548..f99bea072b631 100644 --- a/routers/api/v1/admin/action.go +++ b/routers/api/v1/admin/action.go @@ -4,8 +4,15 @@ package admin import ( + "errors" + "net/http" + + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/services/context" + secret_service "code.gitea.io/gitea/services/secrets" ) // ListWorkflowJobs Lists all jobs @@ -91,3 +98,91 @@ func ListWorkflowRuns(ctx *context.APIContext) { shared.ListRuns(ctx, 0, 0) } + +// CreateOrUpdateSecret create or update one secret in instance scope +func CreateOrUpdateSecret(ctx *context.APIContext) { + // swagger:operation PUT /admin/actions/secrets/{secretname} admin updateAdminSecret + // --- + // summary: Create or Update a secret value in instance scope + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateOrUpdateSecretOption" + // responses: + // "201": + // description: secret created + // "204": + // description: secret updated + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) + + _, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusBadRequest, err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIError(http.StatusInternalServerError, err) + } + return + } + + if created { + ctx.Status(http.StatusCreated) + } else { + ctx.Status(http.StatusNoContent) + } +} + +// DeleteSecret delete one secret in instance scope +func DeleteSecret(ctx *context.APIContext) { + // swagger:operation DELETE /admin/actions/secrets/{secretname} admin deleteAdminSecret + // --- + // summary: Delete a secret in instance scope + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // responses: + // "204": + // description: secret deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + err := secret_service.DeleteSecretByName(ctx, 0, 0, ctx.PathParam("secretname")) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusBadRequest, err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIError(http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f412e8a06caca..b68b045a7f6f3 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1055,7 +1055,6 @@ func Routes() *web.Router { Post(bind(api.CreateEmailOption{}), user.AddEmail). Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail) - // manage user-level actions features m.Group("/actions", func() { m.Group("/secrets", func() { m.Combo("/{secretname}"). @@ -1710,6 +1709,11 @@ func Routes() *web.Router { }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly()) m.Group("/admin", func() { + m.Group("/actions/secrets", func() { + m.Combo("/{secretname}"). + Put(bind(api.CreateOrUpdateSecretOption{}), admin.CreateOrUpdateSecret). + Delete(admin.DeleteSecret) + }) m.Group("/cron", func() { m.Get("", admin.ListCronTasks) m.Post("/{task}", admin.PostCronTask) diff --git a/routers/api/v1/org/secrets.go b/routers/api/v1/org/secrets.go new file mode 100644 index 0000000000000..fd7294b0c550c --- /dev/null +++ b/routers/api/v1/org/secrets.go @@ -0,0 +1,166 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package org + +import ( + "errors" + "net/http" + + "code.gitea.io/gitea/models/db" + secret_model "code.gitea.io/gitea/models/secret" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + secret_service "code.gitea.io/gitea/services/secrets" +) + +// ListActionsSecrets list an organization's actions secrets +func ListActionsSecrets(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets + // --- + // summary: List an organization's actions secrets + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/SecretList" + // "404": + // "$ref": "#/responses/notFound" + + opts := &secret_model.FindSecretsOptions{ + OwnerID: ctx.Org.Organization.ID, + ListOptions: utils.GetListOptions(ctx), + } + + secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + apiSecrets := make([]*api.Secret, len(secrets)) + for k, v := range secrets { + apiSecrets[k] = &api.Secret{ + Name: v.Name, + Created: v.CreatedUnix.AsTime(), + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiSecrets) +} + +// CreateOrUpdateSecret create or update one secret in an organization +func CreateOrUpdateSecret(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret + // --- + // summary: Create or Update a secret value in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateOrUpdateSecretOption" + // responses: + // "201": + // description: secret created + // "204": + // description: secret updated + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption) + + _, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusBadRequest, err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIError(http.StatusInternalServerError, err) + } + return + } + + if created { + ctx.Status(http.StatusCreated) + } else { + ctx.Status(http.StatusNoContent) + } +} + +// DeleteSecret delete one secret in an organization +func DeleteSecret(ctx *context.APIContext) { + // swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret + // --- + // summary: Delete a secret in an organization + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of organization + // type: string + // required: true + // - name: secretname + // in: path + // description: name of the secret + // type: string + // required: true + // responses: + // "204": + // description: secret deleted + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname")) + if err != nil { + if errors.Is(err, util.ErrInvalidArgument) { + ctx.APIError(http.StatusBadRequest, err) + } else if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + } else { + ctx.APIError(http.StatusInternalServerError, err) + } + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 99eef2f53b7df..59dced8a33806 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -36,7 +36,7 @@ import ( "github.com/nektos/act/pkg/model" ) -// ListActionsSecrets list an repo's actions secrets +// ListActionsSecrets list a repo's actions secrets func (Action) ListActionsSecrets(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets // --- @@ -94,7 +94,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiSecrets) } -// create or update one secret of the repository +// CreateOrUpdateSecret create or update one secret in a repository func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { // swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret // --- @@ -125,9 +125,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { // "$ref": "#/definitions/CreateOrUpdateSecretOption" // responses: // "201": - // description: response when creating a secret + // description: secret created // "204": - // description: response when updating a secret + // description: secret updated // "400": // "$ref": "#/responses/error" // "404": @@ -156,7 +156,7 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) { } } -// DeleteSecret delete one secret of the repository +// DeleteSecret delete one secret in a repository func (Action) DeleteSecret(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret // --- @@ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) { // required: true // responses: // "204": - // description: delete one secret of the repository + // description: secret deleted // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index e934d02aa78ee..481a9a3633163 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -19,7 +19,7 @@ import ( secret_service "code.gitea.io/gitea/services/secrets" ) -// create or update one secret of the user scope +// CreateOrUpdateSecret create or update one secret in a user scope func CreateOrUpdateSecret(ctx *context.APIContext) { // swagger:operation PUT /user/actions/secrets/{secretname} user updateUserSecret // --- @@ -40,9 +40,9 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { // "$ref": "#/definitions/CreateOrUpdateSecretOption" // responses: // "201": - // description: response when creating a secret + // description: secret created // "204": - // description: response when updating a secret + // description: secret updated // "400": // "$ref": "#/responses/error" // "404": @@ -69,7 +69,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) { } } -// DeleteSecret delete one secret of the user scope +// DeleteSecret delete one secret in a user scope func DeleteSecret(ctx *context.APIContext) { // swagger:operation DELETE /user/actions/secrets/{secretname} user deleteUserSecret // --- @@ -86,7 +86,7 @@ func DeleteSecret(ctx *context.APIContext) { // required: true // responses: // "204": - // description: delete one secret of the user + // description: secret deleted // "400": // "$ref": "#/responses/error" // "404": diff --git a/routers/web/repo/setting/secrets.go b/routers/web/repo/setting/secrets.go index c6e2d18249c2d..cee0b0c55835a 100644 --- a/routers/web/repo/setting/secrets.go +++ b/routers/web/repo/setting/secrets.go @@ -17,9 +17,10 @@ import ( const ( // TODO: Separate secrets from runners when layout is ready - tplRepoSecrets templates.TplName = "repo/settings/actions" - tplOrgSecrets templates.TplName = "org/settings/actions" - tplUserSecrets templates.TplName = "user/settings/actions" + tplRepoSecrets templates.TplName = "repo/settings/actions" + tplOrgSecrets templates.TplName = "org/settings/actions" + tplUserSecrets templates.TplName = "user/settings/actions" + tplAdminSecrets templates.TplName = "admin/actions" ) type secretsCtx struct { @@ -28,6 +29,7 @@ type secretsCtx struct { IsRepo bool IsOrg bool IsUser bool + IsGlobal bool SecretsTemplate templates.TplName RedirectLink string } @@ -67,6 +69,16 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) { }, nil } + if ctx.Data["PageIsAdmin"] == true { + return &secretsCtx{ + OwnerID: 0, + RepoID: 0, + IsGlobal: true, + SecretsTemplate: tplAdminSecrets, + RedirectLink: setting.AppSubURL + "/admin/actions/secrets", + }, nil + } + return nil, errors.New("unable to set Secrets context") } diff --git a/routers/web/web.go b/routers/web/web.go index 09be0c39045e0..1016449f3f84a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -817,6 +817,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/actions", func() { m.Get("", admin.RedirectToDefaultSetting) addSettingsRunnersRoutes() + addSettingsSecretsRoutes() addSettingsVariablesRoutes() }) }, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled)) diff --git a/templates/admin/actions.tmpl b/templates/admin/actions.tmpl index 597863d73b15e..e71330c67415d 100644 --- a/templates/admin/actions.tmpl +++ b/templates/admin/actions.tmpl @@ -3,6 +3,9 @@ {{if eq .PageType "runners"}} {{template "shared/actions/runner_list" .}} {{end}} + {{if eq .PageType "secrets"}} + {{template "shared/secrets/add_list" (dict "ctxData" . "desc" "secrets.instance_desc")}} + {{end}} {{if eq .PageType "variables"}} {{template "shared/variables/variable_list" .}} {{end}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 72584ec799cc3..4b119a020c9b3 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -69,12 +69,15 @@ {{end}} {{end}} {{if .EnableActions}} -
+
{{ctx.Locale.Tr "actions.actions"}}
- {{if .Secrets}}
+ {{if .desc}} +
+ {{ctx.Locale.Tr .desc}} +
+ {{end}} {{range .Secrets}} -
-
- {{svg "octicon-key" 32}} -
-
-
- {{.Name}} +
+
+ {{svg "octicon-key" 32}}
-
- {{if .Description}}{{.Description}}{{else}}-{{end}} +
+
+ {{.Name}} +
+
+ {{if .Description}}{{.Description}}{{else}}-{{end}} +
+
+ ****** +
-
- ****** +
+ + {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} + + +
-
- - {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} - - - + {{else}} +
+ {{ctx.Locale.Tr "secrets.none"}}
-
{{end}}
- {{else}} - {{ctx.Locale.Tr "secrets.none"}} - {{end}}
{{/* Add secret dialog */}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 35c743dcd4a24..d093f85f90341 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -287,6 +287,84 @@ } } }, + "/admin/actions/secrets/{secretname}": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Create or Update a secret value in instance scope", + "operationId": "updateAdminSecret", + "parameters": [ + { + "type": "string", + "description": "name of the secret", + "name": "secretname", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateOrUpdateSecretOption" + } + } + ], + "responses": { + "201": { + "description": "secret created" + }, + "204": { + "description": "secret updated" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete a secret in instance scope", + "operationId": "deleteAdminSecret", + "parameters": [ + { + "type": "string", + "description": "name of the secret", + "name": "secretname", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "secret deleted" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/admin/cron": { "get": { "produces": [ @@ -2270,10 +2348,10 @@ ], "responses": { "201": { - "description": "response when creating a secret" + "description": "secret created" }, "204": { - "description": "response when updating a secret" + "description": "secret updated" }, "400": { "$ref": "#/responses/error" @@ -2313,7 +2391,7 @@ ], "responses": { "204": { - "description": "delete one secret of the organization" + "description": "secret deleted" }, "400": { "$ref": "#/responses/error" @@ -5475,10 +5553,10 @@ ], "responses": { "201": { - "description": "response when creating a secret" + "description": "secret created" }, "204": { - "description": "response when updating a secret" + "description": "secret updated" }, "400": { "$ref": "#/responses/error" @@ -5525,7 +5603,7 @@ ], "responses": { "204": { - "description": "delete one secret of the repository" + "description": "secret deleted" }, "400": { "$ref": "#/responses/error" @@ -18429,10 +18507,10 @@ ], "responses": { "201": { - "description": "response when creating a secret" + "description": "secret created" }, "204": { - "description": "response when updating a secret" + "description": "secret updated" }, "400": { "$ref": "#/responses/error" @@ -18465,7 +18543,7 @@ ], "responses": { "204": { - "description": "delete one secret of the user" + "description": "secret deleted" }, "400": { "$ref": "#/responses/error" diff --git a/templates/user/settings/actions.tmpl b/templates/user/settings/actions.tmpl index abc544338355b..37f211eeff092 100644 --- a/templates/user/settings/actions.tmpl +++ b/templates/user/settings/actions.tmpl @@ -1,7 +1,7 @@ {{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings actions")}}
{{if eq .PageType "secrets"}} - {{template "shared/secrets/add_list" .}} + {{template "shared/secrets/add_list" (dict "ctxData" .)}} {{else if eq .PageType "runners"}} {{template "shared/actions/runner_list" .}} {{else if eq .PageType "variables"}} diff --git a/tests/integration/api_admin_secrets_test.go b/tests/integration/api_admin_secrets_test.go new file mode 100644 index 0000000000000..136ae65001107 --- /dev/null +++ b/tests/integration/api_admin_secrets_test.go @@ -0,0 +1,97 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" +) + +func TestAPIAdminSecrets(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin) + + t.Run("Create", func(t *testing.T) { + cases := []struct { + Name string + ExpectedStatus int + }{ + { + Name: "", + ExpectedStatus: http.StatusNotFound, + }, + { + Name: "-", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "_", + ExpectedStatus: http.StatusCreated, + }, + { + Name: "secret", + ExpectedStatus: http.StatusCreated, + }, + { + Name: "2secret", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "GITEA_secret", + ExpectedStatus: http.StatusBadRequest, + }, + { + Name: "GITHUB_secret", + ExpectedStatus: http.StatusBadRequest, + }, + } + + for _, c := range cases { + req := NewRequestWithJSON(t, "PUT", "/api/v1/admin/actions/secrets/"+c.Name, api.CreateOrUpdateSecretOption{ + Data: "data", + }).AddTokenAuth(token) + MakeRequest(t, req, c.ExpectedStatus) + } + }) + + t.Run("Update", func(t *testing.T) { + name := "update_secret" + url := "/api/v1/admin/actions/secrets/" + name + + req := NewRequestWithJSON(t, "PUT", url, api.CreateOrUpdateSecretOption{ + Data: "initial", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequestWithJSON(t, "PUT", url, api.CreateOrUpdateSecretOption{ + Data: "changed", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + }) + + t.Run("Delete", func(t *testing.T) { + name := "delete_secret" + url := "/api/v1/admin/actions/secrets/" + name + + req := NewRequestWithJSON(t, "PUT", url, api.CreateOrUpdateSecretOption{ + Data: "initial", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", url).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNotFound) + + req = NewRequest(t, "DELETE", "/api/v1/admin/actions/secrets/000").AddTokenAuth(token) + MakeRequest(t, req, http.StatusBadRequest) + }) +}