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 <appleboy.tw@gmail.com>
This commit is contained in:
Bo-Yi Wu 2023-08-24 10:07:00 +08:00 committed by GitHub
parent 7e30986667
commit b62c8e7765
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 272 additions and 1 deletions

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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)

View File

@ -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)
}

View File

@ -190,4 +190,7 @@ type swaggerParameterBodies struct {
// in:body
CreateSecretOption api.CreateSecretOption
// in:body
UpdateSecretOption api.UpdateSecretOption
}

View File

@ -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": {