From b62c8e7765a371600a300f62da96483a1ae0c731 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Thu, 24 Aug 2023 10:07:00 +0800 Subject: [PATCH] feat(API): update and delete secret for managing organization secrets (#26660) - Add `UpdateSecret` function to modify org or user repo secret - Add `DeleteSecret` function to delete secret from an organization - Add `UpdateSecretOption` struct for updating secret options - Add `UpdateOrgSecret` function to update a secret in an organization - Add `DeleteOrgSecret` function to delete a secret in an organization GitHub API 1. Update Org Secret: https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28#create-or-update-an-organization-secret 2. Delete Org Secret: https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28#delete-an-organization-secret --------- Signed-off-by: Bo-Yi Wu --- models/secret/secret.go | 67 ++++++++++++++++++++ modules/structs/secret.go | 9 +++ routers/api/v1/api.go | 3 + routers/api/v1/org/action.go | 91 +++++++++++++++++++++++++++ routers/api/v1/swagger/options.go | 3 + templates/swagger/v1_json.tmpl | 100 +++++++++++++++++++++++++++++- 6 files changed, 272 insertions(+), 1 deletion(-) diff --git a/models/secret/secret.go b/models/secret/secret.go index c9c95e82d30..410cb3770eb 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -6,12 +6,14 @@ package secret import ( "context" "errors" + "fmt" "strings" "code.gitea.io/gitea/models/db" secret_module "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) @@ -26,6 +28,25 @@ type Secret struct { CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` } +// ErrSecretNotFound represents a "secret not found" error. +type ErrSecretNotFound struct { + Name string +} + +// IsErrSecretNotFound checks if an error is a ErrSecretNotFound. +func IsErrSecretNotFound(err error) bool { + _, ok := err.(ErrSecretNotFound) + return ok +} + +func (err ErrSecretNotFound) Error() string { + return fmt.Sprintf("secret was not found [name: %s]", err.Name) +} + +func (err ErrSecretNotFound) Unwrap() error { + return util.ErrNotExist +} + // newSecret Creates a new already encrypted secret func newSecret(ownerID, repoID int64, name, data string) *Secret { return &Secret{ @@ -93,3 +114,49 @@ func FindSecrets(ctx context.Context, opts FindSecretsOptions) ([]*Secret, error func CountSecrets(ctx context.Context, opts *FindSecretsOptions) (int64, error) { return db.GetEngine(ctx).Where(opts.toConds()).Count(new(Secret)) } + +// UpdateSecret changes org or user reop secret. +func UpdateSecret(ctx context.Context, orgID, repoID int64, name, data string) error { + sc := new(Secret) + name = strings.ToUpper(name) + has, err := db.GetEngine(ctx). + Where("owner_id=?", orgID). + And("repo_id=?", repoID). + And("name=?", name). + Get(sc) + if err != nil { + return err + } else if !has { + return ErrSecretNotFound{Name: name} + } + + encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data) + if err != nil { + return err + } + + sc.Data = encrypted + _, err = db.GetEngine(ctx).ID(sc.ID).Cols("data").Update(sc) + return err +} + +// DeleteSecret deletes secret from an organization. +func DeleteSecret(ctx context.Context, orgID, repoID int64, name string) error { + sc := new(Secret) + has, err := db.GetEngine(ctx). + Where("owner_id=?", orgID). + And("repo_id=?", repoID). + And("name=?", strings.ToUpper(name)). + Get(sc) + if err != nil { + return err + } else if !has { + return ErrSecretNotFound{Name: name} + } + + if _, err := db.GetEngine(ctx).ID(sc.ID).Delete(new(Secret)); err != nil { + return fmt.Errorf("Delete: %w", err) + } + + return nil +} diff --git a/modules/structs/secret.go b/modules/structs/secret.go index c707eb2278b..52221b51f0a 100644 --- a/modules/structs/secret.go +++ b/modules/structs/secret.go @@ -25,3 +25,12 @@ type CreateSecretOption struct { // Data of the secret to create Data string `json:"data" binding:"Required"` } + +// UpdateSecretOption options when updating secret +// swagger:model +type UpdateSecretOption struct { + // Data of the secret to update + // + // required: true + Data string `json:"data" binding:"Required"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9613bd610dc..2d644507d5f 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1301,6 +1301,9 @@ func Routes() *web.Route { m.Group("/actions/secrets", func() { m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets) m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateSecretOption{}), org.CreateOrgSecret) + m.Combo("/{secretname}"). + Put(reqToken(), reqOrgOwnership(), bind(api.UpdateSecretOption{}), org.UpdateOrgSecret). + Delete(reqToken(), reqOrgOwnership(), org.DeleteOrgSecret) }) m.Group("/public_members", func() { m.Get("", org.ListPublicMembers) diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 7659191946f..9697a11363d 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -103,6 +103,10 @@ func CreateOrgSecret(ctx *context.APIContext) { // "403": // "$ref": "#/responses/forbidden" opt := web.GetForm(ctx).(*api.CreateSecretOption) + if err := actions.NameRegexMatch(opt.Name); err != nil { + ctx.Error(http.StatusBadRequest, "CreateOrgSecret", err) + return + } s, err := secret_model.InsertEncryptedSecret( ctx, ctx.Org.Organization.ID, 0, opt.Name, actions.ReserveLineBreakForTextarea(opt.Data), ) @@ -113,3 +117,90 @@ func CreateOrgSecret(ctx *context.APIContext) { ctx.JSON(http.StatusCreated, convert.ToSecret(s)) } + +// UpdateOrgSecret update one secret of the organization +func UpdateOrgSecret(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret + // --- + // summary: 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/UpdateSecretOption" + // responses: + // "204": + // description: update one secret of the organization + // "403": + // "$ref": "#/responses/forbidden" + secretName := ctx.Params(":secretname") + opt := web.GetForm(ctx).(*api.UpdateSecretOption) + err := secret_model.UpdateSecret( + ctx, ctx.Org.Organization.ID, 0, secretName, opt.Data, + ) + if secret_model.IsErrSecretNotFound(err) { + ctx.NotFound(err) + return + } + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateSecret", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// DeleteOrgSecret delete one secret of the organization +func DeleteOrgSecret(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: delete one secret of the organization + // "403": + // "$ref": "#/responses/forbidden" + secretName := ctx.Params(":secretname") + err := secret_model.DeleteSecret( + ctx, ctx.Org.Organization.ID, 0, secretName, + ) + if secret_model.IsErrSecretNotFound(err) { + ctx.NotFound(err) + return + } + if err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteSecret", err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 8e7e6ec3dfb..e41ee667760 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -190,4 +190,7 @@ type swaggerParameterBodies struct { // in:body CreateSecretOption api.CreateSecretOption + + // in:body + UpdateSecretOption api.UpdateSecretOption } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 5e75f6f8b46..aff4490899e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1631,6 +1631,89 @@ } } }, + "/orgs/{org}/actions/secrets/{secretname}": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Update a secret value in an organization", + "operationId": "updateOrgSecret", + "parameters": [ + { + "type": "string", + "description": "name of organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the secret", + "name": "secretname", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateSecretOption" + } + } + ], + "responses": { + "204": { + "description": "update one secret of the organization" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Delete a secret in an organization", + "operationId": "deleteOrgSecret", + "parameters": [ + { + "type": "string", + "description": "name of organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the secret", + "name": "secretname", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "delete one secret of the organization" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, "/orgs/{org}/activities/feeds": { "get": { "produces": [ @@ -21891,6 +21974,21 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateSecretOption": { + "description": "UpdateSecretOption options when updating secret", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "description": "Data of the secret to update", + "type": "string", + "x-go-name": "Data" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateUserAvatarOption": { "description": "UpdateUserAvatarUserOption options when updating the user avatar", "type": "object", @@ -23207,7 +23305,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateSecretOption" + "$ref": "#/definitions/UpdateSecretOption" } }, "redirect": {