From b91057b172dea07a9db1bf96a32d2ab25a0e030d Mon Sep 17 00:00:00 2001
From: Bo-Yi Wu <appleboy.tw@gmail.com>
Date: Wed, 30 Aug 2023 04:54:49 +0800
Subject: [PATCH] feat(API): add route and implementation for creating/updating
 repository secret (#26766)

spec:
https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28#create-or-update-a-repository-secret

- Add a new route for creating or updating a secret value in a
repository
- Create a new file `routers/api/v1/repo/action.go` with the
implementation of the `CreateOrUpdateSecret` function
- Update the Swagger documentation for the `updateRepoSecret` operation
in the `v1_json.tmpl` template file

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
---
 models/secret/secret.go        | 28 +++++++++++++
 routers/api/v1/api.go          |  6 ++-
 routers/api/v1/org/action.go   | 23 ++++-------
 routers/api/v1/repo/action.go  | 75 ++++++++++++++++++++++++++++++++++
 templates/swagger/v1_json.tmpl | 59 ++++++++++++++++++++++++++
 5 files changed, 174 insertions(+), 17 deletions(-)
 create mode 100644 routers/api/v1/repo/action.go

diff --git a/models/secret/secret.go b/models/secret/secret.go
index 410cb3770e..1cb816e9db 100644
--- a/models/secret/secret.go
+++ b/models/secret/secret.go
@@ -160,3 +160,31 @@ func DeleteSecret(ctx context.Context, orgID, repoID int64, name string) error {
 
 	return nil
 }
+
+// CreateOrUpdateSecret creates or updates a secret and returns true if it was created
+func CreateOrUpdateSecret(ctx context.Context, orgID, repoID int64, name, data string) (bool, 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 false, err
+	}
+
+	if !has {
+		_, err = InsertEncryptedSecret(ctx, orgID, repoID, name, data)
+		if err != nil {
+			return false, err
+		}
+		return true, nil
+	}
+
+	if err := UpdateSecret(ctx, orgID, repoID, name, data); err != nil {
+		return false, err
+	}
+
+	return false, nil
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 6424931a47..32e5a10bbe 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -933,6 +933,10 @@ func Routes() *web.Route {
 					m.Post("/accept", repo.AcceptTransfer)
 					m.Post("/reject", repo.RejectTransfer)
 				}, reqToken())
+				m.Group("/actions/secrets", func() {
+					m.Combo("/{secretname}").
+						Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret)
+				})
 				m.Group("/hooks/git", func() {
 					m.Combo("").Get(repo.ListGitHooks)
 					m.Group("/{id}", func() {
@@ -1301,7 +1305,7 @@ func Routes() *web.Route {
 			m.Group("/actions/secrets", func() {
 				m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets)
 				m.Combo("/{secretname}").
-					Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateOrgSecret).
+					Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret).
 					Delete(reqToken(), reqOrgOwnership(), org.DeleteOrgSecret)
 			})
 			m.Group("/public_members", func() {
diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go
index ee18cca26d..0bf741e825 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -74,7 +74,7 @@ func listActionsSecrets(ctx *context.APIContext) {
 }
 
 // create or update one secret of the organization
-func CreateOrUpdateOrgSecret(ctx *context.APIContext) {
+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
@@ -108,26 +108,17 @@ func CreateOrUpdateOrgSecret(ctx *context.APIContext) {
 	//     "$ref": "#/responses/forbidden"
 	secretName := ctx.Params(":secretname")
 	if err := actions.NameRegexMatch(secretName); err != nil {
-		ctx.Error(http.StatusBadRequest, "CreateOrUpdateOrgSecret", err)
+		ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
 		return
 	}
 	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
-	err := secret_model.UpdateSecret(
-		ctx, ctx.Org.Organization.ID, 0, secretName, opt.Data,
-	)
-	if secret_model.IsErrSecretNotFound(err) {
-		_, err := secret_model.InsertEncryptedSecret(
-			ctx, ctx.Org.Organization.ID, 0, secretName, actions.ReserveLineBreakForTextarea(opt.Data),
-		)
-		if err != nil {
-			ctx.Error(http.StatusInternalServerError, "InsertEncryptedSecret", err)
-			return
-		}
-		ctx.Status(http.StatusCreated)
+	isCreated, err := secret_model.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, secretName, opt.Data)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
 		return
 	}
-	if err != nil {
-		ctx.Error(http.StatusInternalServerError, "UpdateSecret", err)
+	if isCreated {
+		ctx.Status(http.StatusCreated)
 		return
 	}
 
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
new file mode 100644
index 0000000000..015c731a75
--- /dev/null
+++ b/routers/api/v1/repo/action.go
@@ -0,0 +1,75 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+	"net/http"
+
+	secret_model "code.gitea.io/gitea/models/secret"
+	"code.gitea.io/gitea/modules/context"
+	api "code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/web/shared/actions"
+)
+
+// create or update one secret of the repository
+func CreateOrUpdateSecret(ctx *context.APIContext) {
+	// swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
+	// ---
+	// summary: Create or Update a secret value in a repository
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// parameters:
+	// - name: owner
+	//   in: path
+	//   description: owner of the repository
+	//   type: string
+	//   required: true
+	// - name: repo
+	//   in: path
+	//   description: name of the repository
+	//   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: response when creating a secret
+	//   "204":
+	//     description: response when updating a secret
+	//   "400":
+	//     "$ref": "#/responses/error"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
+
+	owner := ctx.Repo.Owner
+	repo := ctx.Repo.Repository
+
+	secretName := ctx.Params(":secretname")
+	if err := actions.NameRegexMatch(secretName); err != nil {
+		ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
+		return
+	}
+	opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
+	isCreated, err := secret_model.CreateOrUpdateSecret(ctx, owner.ID, repo.ID, secretName, opt.Data)
+	if err != nil {
+		ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
+		return
+	}
+	if isCreated {
+		ctx.Status(http.StatusCreated)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d37f4463f5..78491de2e1 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -3230,6 +3230,65 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/actions/secrets/{secretname}": {
+      "put": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Create or Update a secret value in a repository",
+        "operationId": "updateRepoSecret",
+        "parameters": [
+          {
+            "type": "string",
+            "description": "owner of the repository",
+            "name": "owner",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the repository",
+            "name": "repo",
+            "in": "path",
+            "required": true
+          },
+          {
+            "type": "string",
+            "description": "name of the secret",
+            "name": "secretname",
+            "in": "path",
+            "required": true
+          },
+          {
+            "name": "body",
+            "in": "body",
+            "schema": {
+              "$ref": "#/definitions/CreateOrUpdateSecretOption"
+            }
+          }
+        ],
+        "responses": {
+          "201": {
+            "description": "response when creating a secret"
+          },
+          "204": {
+            "description": "response when updating a secret"
+          },
+          "400": {
+            "$ref": "#/responses/error"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/activities/feeds": {
       "get": {
         "produces": [