mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: allow renaming and replacing secrets (#11732)
So far, Forgejo's UI only allowed to create Forgejo Actions secrets. But renaming or replacing their value wasn't possible. With this change, users can do both. The existing secret value is never revealed for security reasons. Additionally, a confusing behaviour is removed. If a user created a new secret whose name matched an existing secret, the existing secret was silently updated. That does no longer happen. The new secret is rejected instead. Resolves https://codeberg.org/forgejo/forgejo/issues/5707. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Tests for JavaScript changes (can be removed for Go changes) - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. *The decision if the pull request will be shown in the release notes is up to the mergers / release team.* The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11732 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch> Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
This commit is contained in:
parent
ce1c0dc2cc
commit
bdbd0b5622
18 changed files with 776 additions and 251 deletions
23
models/secret/TestSecretGetSecretByID/secret.yml
Normal file
23
models/secret/TestSecretGetSecretByID/secret.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
- id: 637340
|
||||
name: TEST_SECRET
|
||||
owner_id: 3
|
||||
repo_id: 0
|
||||
# very secret
|
||||
data: 0x0e17d357077de0c1f72e9d87e1899c8026a207fcbbc0f1a2a79d0cb305da39fa3e9fe201b085e963fa1f9eeb59b4ff9ee6d748
|
||||
created_unix: 1773692671
|
||||
|
||||
- id: 637341
|
||||
name: ANOTHER_SECRET
|
||||
owner_id: 0
|
||||
repo_id: 62 # user2/test_workflows
|
||||
# also very secret
|
||||
data: 0xb697cd4a66b02bc36d0afddcb6f158d7602f6cf0b0d31a0c96e178469e8b66babccb30b95e01c84824add04c5bfe91b9b773a21a78088a3e
|
||||
created_unix: 1773692672
|
||||
|
||||
- id: 637342
|
||||
name: TEST_SECRET
|
||||
owner_id: 1
|
||||
repo_id: 0
|
||||
# super secret
|
||||
data: 0xd97d8cb662c07ca94953a388bc93209f713281a8b0d25499359cbd03a1fbe565a2a0ba183b8290fc110ee6b1c6437c569451e0c3
|
||||
created_unix: 1773692673
|
||||
|
|
@ -6,6 +6,7 @@ package secret
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
|
|
@ -17,6 +18,13 @@ import (
|
|||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var (
|
||||
namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
|
||||
forbiddenPrefixPattern = regexp.MustCompile("(?i)^FORGEJO_|GITEA_|GITHUB_")
|
||||
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name")
|
||||
)
|
||||
|
||||
// Secret represents a secret
|
||||
//
|
||||
// It can be:
|
||||
|
|
@ -63,6 +71,9 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
|
|||
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 err := ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret := &Secret{
|
||||
OwnerID: ownerID,
|
||||
|
|
@ -129,6 +140,46 @@ func (s *Secret) GetDecryptedData() (string, error) {
|
|||
return string(v), nil
|
||||
}
|
||||
|
||||
func GetSecretByID(ctx context.Context, ownerID, repoID, id int64) (*Secret, error) {
|
||||
query := db.GetEngine(ctx).Where("id=?", id)
|
||||
|
||||
if repoID > 0 {
|
||||
query = query.And(builder.Eq{"repo_id": repoID})
|
||||
} else if ownerID > 0 {
|
||||
query = query.And(builder.Eq{"owner_id": ownerID})
|
||||
} else {
|
||||
return nil, fmt.Errorf("ownerID and repoID cannot be simultaneously 0")
|
||||
}
|
||||
|
||||
var secret Secret
|
||||
has, err := query.Get(&secret)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("secret with ID %d: %w", id, util.ErrNotExist)
|
||||
}
|
||||
return &secret, nil
|
||||
}
|
||||
|
||||
func UpdateSecret(ctx context.Context, secret *Secret, columns ...string) error {
|
||||
e := db.GetEngine(ctx)
|
||||
|
||||
if err := ValidateName(secret.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
secret.Name = strings.ToUpper(secret.Name)
|
||||
|
||||
var err error
|
||||
if len(columns) == 0 {
|
||||
_, err = e.ID(secret.ID).AllCols().Update(secret)
|
||||
} else {
|
||||
_, err = e.ID(secret.ID).Cols(columns...).Update(secret)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func FetchActionSecrets(ctx context.Context, ownerID, repoID int64) (map[string]string, error) {
|
||||
secrets := map[string]string{}
|
||||
|
||||
|
|
@ -154,3 +205,10 @@ func FetchActionSecrets(ctx context.Context, ownerID, repoID int64) (map[string]
|
|||
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func ValidateName(name string) error {
|
||||
if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) {
|
||||
return ErrInvalidName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
package secret
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/models/unittest"
|
||||
|
|
@ -83,6 +84,11 @@ func TestInsertEncryptedSecret(t *testing.T) {
|
|||
})
|
||||
})
|
||||
|
||||
t.Run("Rejects invalid name", func(t *testing.T) {
|
||||
_, err := InsertEncryptedSecret(t.Context(), 2, 0, "invalid name", "some secret")
|
||||
require.ErrorContains(t, err, "invalid secret name")
|
||||
})
|
||||
|
||||
t.Run("FetchActionSecrets", func(t *testing.T) {
|
||||
secrets, err := FetchActionSecrets(t.Context(), 2, 1)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -123,3 +129,169 @@ func TestSecretGetDecryptedData(t *testing.T) {
|
|||
assert.ErrorContains(t, err, "unable to decrypt secret[id=495,name=\"A_SECRET\"]")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSecretGetSecretByID(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("models/secret/TestSecretGetSecretByID")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
id int64
|
||||
expectedName string
|
||||
expectedData string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "Organization secret",
|
||||
ownerID: 3,
|
||||
repoID: 0,
|
||||
id: 637340,
|
||||
expectedName: "TEST_SECRET",
|
||||
expectedData: "very secret",
|
||||
},
|
||||
{
|
||||
name: "Owner mismatch",
|
||||
ownerID: 4,
|
||||
repoID: 0,
|
||||
id: 637340,
|
||||
expectedError: "secret with ID 637340: resource does not exist",
|
||||
},
|
||||
{
|
||||
name: "Repository mismatch",
|
||||
ownerID: 0,
|
||||
repoID: 1,
|
||||
id: 637340,
|
||||
expectedError: "secret with ID 637340: resource does not exist",
|
||||
},
|
||||
{
|
||||
name: "Repository secret",
|
||||
ownerID: 0,
|
||||
repoID: 62,
|
||||
id: 637341,
|
||||
expectedName: "ANOTHER_SECRET",
|
||||
expectedData: "also very secret",
|
||||
},
|
||||
{
|
||||
name: "Unsupported instance secret",
|
||||
ownerID: 0,
|
||||
repoID: 0,
|
||||
id: 637341,
|
||||
expectedError: "ownerID and repoID cannot be simultaneously 0",
|
||||
},
|
||||
{
|
||||
name: "User secret",
|
||||
ownerID: 1,
|
||||
repoID: 0,
|
||||
id: 637342,
|
||||
expectedName: "TEST_SECRET",
|
||||
expectedData: "super secret",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
secret, err := GetSecretByID(t.Context(), testCase.ownerID, testCase.repoID, testCase.id)
|
||||
|
||||
if testCase.expectedError != "" {
|
||||
assert.ErrorContains(t, err, testCase.expectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.id, secret.ID)
|
||||
assert.Equal(t, testCase.ownerID, secret.OwnerID)
|
||||
assert.Equal(t, testCase.repoID, secret.RepoID)
|
||||
assert.Equal(t, testCase.expectedName, secret.Name)
|
||||
|
||||
data, err := secret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testCase.expectedData, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretUpdateSecret(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
secret, err := InsertEncryptedSecret(t.Context(), 2, 0, "a_secret", "very secret")
|
||||
require.NoError(t, err)
|
||||
|
||||
secret.Name = "new_name"
|
||||
secret.SetData("also very secret")
|
||||
|
||||
err = UpdateSecret(t.Context(), secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedSecret := unittest.AssertExistsAndLoadBean(t, &Secret{ID: secret.ID})
|
||||
decryptedData, err := updatedSecret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "NEW_NAME", updatedSecret.Name)
|
||||
assert.Equal(t, "also very secret", decryptedData)
|
||||
}
|
||||
|
||||
func TestSecretUpdateSecret_RejectsInvalidName(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
secret, err := InsertEncryptedSecret(t.Context(), 2, 0, "a_secret", "very secret")
|
||||
require.NoError(t, err)
|
||||
|
||||
secret.Name = "GITHUB_IS_REJECTED" // Because it starts with `GITHUB_`.
|
||||
secret.SetData("also very secret")
|
||||
|
||||
err = UpdateSecret(t.Context(), secret)
|
||||
require.ErrorContains(t, err, "invalid secret name")
|
||||
|
||||
updatedSecret := unittest.AssertExistsAndLoadBean(t, &Secret{ID: secret.ID})
|
||||
decryptedData, err := updatedSecret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "A_SECRET", updatedSecret.Name)
|
||||
assert.Equal(t, "very secret", decryptedData)
|
||||
}
|
||||
|
||||
func TestSecretValidateName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
valid bool
|
||||
}{
|
||||
{"FORGEJO_", false},
|
||||
{"FORGEJO_123", false},
|
||||
{"FORGEJO_ABC", false},
|
||||
{"GITEA_", false},
|
||||
{"GITEA_123", false},
|
||||
{"GITEA_ABC", false},
|
||||
{"GITHUB_", false},
|
||||
{"GITHUB_123", false},
|
||||
{"GITHUB_ABC", false},
|
||||
{"123_TEST", false},
|
||||
{"CI", true},
|
||||
{"_CI", true},
|
||||
{"CI_", true},
|
||||
{"CI123", true},
|
||||
{"CIABC", true},
|
||||
{"FORGEJO", true},
|
||||
{"FORGEJO123", true},
|
||||
{"FORGEJOABC", true},
|
||||
{"GITEA", true},
|
||||
{"GITEA123", true},
|
||||
{"GITEAABC", true},
|
||||
{"GITHUB", true},
|
||||
{"GITHUB123", true},
|
||||
{"GITHUBABC", true},
|
||||
{"_123_TEST", true},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
if tC.valid {
|
||||
assert.NoError(t, ValidateName(tC.name))
|
||||
assert.NoError(t, ValidateName(strings.ToLower(tC.name)))
|
||||
} else {
|
||||
require.ErrorIs(t, ValidateName(tC.name), ErrInvalidName)
|
||||
require.ErrorIs(t, ValidateName(strings.ToLower(tC.name)), ErrInvalidName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -534,6 +534,12 @@
|
|||
"actions.secrets.creation.value_description": "The value of a secret can be any text. Special characters are retained. CRLF (Windows-style line breaks) is automatically converted to LF. Encode the value with Base64 if linebreaks should be retained.",
|
||||
"actions.variables.mutation.name_description": "The name of a variable can only contain letters, numbers, and underscores. It cannot be named CI or start with FORGEJO_, GITEA_, GITHUB_, or a number. Forgejo will automatically convert it to uppercase.",
|
||||
"actions.variables.mutation.value_description": "A variable's value can be any text. Special characters are retained. CRLF (Windows-style line breaks) is automatically converted to LF. Encode the value with Base64 if linebreaks should be retained.",
|
||||
"actions.secrets.edit_button": "Edit secret \"%s\"",
|
||||
"actions.secrets.mutation.header": "Edit secret \"%s\"",
|
||||
"actions.secrets.mutation.success_message": "The secret \"%s\" has been updated.",
|
||||
"actions.secrets.mutation.failure_message": "The secret \"%s\" could not be updated.",
|
||||
"actions.secrets.mutation.name_description": "The name of a secret can only contain letters, numbers, and underscores. It cannot start with FORGEJO_, GITEA_, GITHUB_, or a number. Forgejo will automatically convert it to uppercase.",
|
||||
"actions.secrets.mutation.value_description": "The existing value will not be shown. Leave the field empty if you do not want to modify it. Special characters are retained. CRLF (Windows-style line breaks) is automatically converted to LF. Encode the value with Base64 if linebreaks should be retained.",
|
||||
"pulse.n_active_issues": {
|
||||
"one": "%s active issue",
|
||||
"other": "%s active issues"
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func Secrets(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, sCtx.SecretsTemplate)
|
||||
}
|
||||
|
||||
func SecretsPost(ctx *context.Context) {
|
||||
func SecretsCreatePost(ctx *context.Context) {
|
||||
sCtx, err := getSecretsCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getSecretsCtx", err)
|
||||
|
|
@ -104,7 +104,7 @@ func SecretsPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
shared.PerformSecretsPost(
|
||||
shared.CreateSecretPost(
|
||||
ctx,
|
||||
sCtx.OwnerID,
|
||||
sCtx.RepoID,
|
||||
|
|
@ -112,16 +112,32 @@ func SecretsPost(ctx *context.Context) {
|
|||
)
|
||||
}
|
||||
|
||||
func SecretsDelete(ctx *context.Context) {
|
||||
func SecretsEditPost(ctx *context.Context) {
|
||||
sCtx, err := getSecretsCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getSecretsCtx", err)
|
||||
return
|
||||
}
|
||||
shared.PerformSecretsDelete(
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
shared.EditSecretPost(ctx, sCtx.OwnerID, sCtx.RepoID, ctx.ParamsInt64(":secret_id"), sCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func SecretsDeletePost(ctx *context.Context) {
|
||||
sCtx, err := getSecretsCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getSecretsCtx", err)
|
||||
return
|
||||
}
|
||||
shared.DeleteSecretPost(
|
||||
ctx,
|
||||
sCtx.OwnerID,
|
||||
sCtx.RepoID,
|
||||
ctx.ParamsInt64(":secret_id"),
|
||||
sCtx.RedirectLink,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
package secrets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
secret_model "forgejo.org/models/secret"
|
||||
"forgejo.org/modules/log"
|
||||
|
|
@ -24,23 +26,50 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
|
|||
ctx.Data["Secrets"] = secrets
|
||||
}
|
||||
|
||||
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
form := web.GetForm(ctx).(*forms.AddSecretForm)
|
||||
func CreateSecretPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
form := web.GetForm(ctx).(*forms.CreateSecretForm)
|
||||
|
||||
s, _, err := secrets_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
|
||||
normalizedData := util.ReserveLineBreakForTextarea(form.Data)
|
||||
secret, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Name, normalizedData)
|
||||
if err != nil {
|
||||
log.Error("CreateOrUpdateSecret failed: %v", err)
|
||||
log.Error("InsertEncryptedSecret failed: %v", err)
|
||||
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
|
||||
ctx.Flash.Success(ctx.Tr("secrets.creation.success", secret.Name))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
id := ctx.FormInt64("id")
|
||||
func EditSecretPost(ctx *context.Context, ownerID, repoID, id int64, redirectURL string) {
|
||||
form := web.GetForm(ctx).(*forms.EditSecretForm)
|
||||
|
||||
secret, err := secret_model.GetSecretByID(ctx, ownerID, repoID, id)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.NotFound("GetSecretByID", err)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetSecretByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
secret.Name = form.Name
|
||||
if form.Data != "" {
|
||||
secret.SetData(util.ReserveLineBreakForTextarea(form.Data))
|
||||
}
|
||||
|
||||
err = secret_model.UpdateSecret(ctx, secret)
|
||||
if err != nil {
|
||||
log.Error("UpdateSecret failed: %v", err)
|
||||
ctx.JSONError(ctx.Tr("actions.secrets.mutation.failure_message", secret.Name))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("actions.secrets.mutation.success_message", secret.Name))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
func DeleteSecretPost(ctx *context.Context, ownerID, repoID, id int64, redirectURL string) {
|
||||
err := secrets_service.DeleteSecretByID(ctx, ownerID, repoID, id)
|
||||
if err != nil {
|
||||
log.Error("DeleteSecretByID(%d) failed: %v", id, err)
|
||||
|
|
|
|||
|
|
@ -454,8 +454,9 @@ func registerRoutes(m *web.Route) {
|
|||
addSettingsSecretsRoutes := func() {
|
||||
m.Group("/secrets", func() {
|
||||
m.Get("", repo_setting.Secrets)
|
||||
m.Post("", web.Bind(forms.AddSecretForm{}), repo_setting.SecretsPost)
|
||||
m.Post("/delete", repo_setting.SecretsDelete)
|
||||
m.Post("", web.Bind(forms.CreateSecretForm{}), repo_setting.SecretsCreatePost)
|
||||
m.Post("/{secret_id}/edit", web.Bind(forms.EditSecretForm{}), repo_setting.SecretsEditPost)
|
||||
m.Post("/{secret_id}/delete", repo_setting.SecretsDeletePost)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import (
|
|||
"strings"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/secret"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/util"
|
||||
secrets_service "forgejo.org/services/secrets"
|
||||
)
|
||||
|
||||
func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
|
||||
if err := secrets_service.ValidateName(name); err != nil {
|
||||
if err := secret.ValidateName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
|
|||
}
|
||||
|
||||
func UpdateVariable(ctx context.Context, variableID, ownerID, repoID int64, name, data string) (bool, error) {
|
||||
if err := secrets_service.ValidateName(name); err != nil {
|
||||
if err := secret.ValidateName(name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
|
|
|||
38
services/forms/secret.go
Normal file
38
services/forms/secret.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forms
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forgejo.org/modules/web/middleware"
|
||||
"forgejo.org/services/context"
|
||||
|
||||
"code.forgejo.org/go-chi/binding"
|
||||
)
|
||||
|
||||
// CreateSecretForm needs to be filled in by the user to create a new secret.
|
||||
type CreateSecretForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)"`
|
||||
Data string `binding:"Required;MaxSize(65535)"`
|
||||
}
|
||||
|
||||
// Validate validates the submitted CreateSecretForm.
|
||||
func (f *CreateSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// EditSecretForm needs to be filled in by the user to change an existing secret.
|
||||
type EditSecretForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)"`
|
||||
Data string `binding:"MaxSize(65535)"`
|
||||
}
|
||||
|
||||
// Validate validates the submitted EditSecretForm.
|
||||
func (f *EditSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
|
@ -346,18 +346,6 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er
|
|||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// AddSecretForm for adding secrets
|
||||
type AddSecretForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)"`
|
||||
Data string `binding:"Required;MaxSize(65535)"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
type EditVariableForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)"`
|
||||
Data string `binding:"Required;MaxSize(65535)"`
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*secret_model.Secret, bool, error) {
|
||||
if err := ValidateName(name); err != nil {
|
||||
if err := secret_model.ValidateName(name); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
|
||||
var (
|
||||
namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
|
||||
forbiddenPrefixPattern = regexp.MustCompile("(?i)^FORGEJO_|GITEA_|GITHUB_")
|
||||
|
||||
ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name")
|
||||
)
|
||||
|
||||
func ValidateName(name string) error {
|
||||
if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) {
|
||||
return ErrInvalidName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
valid bool
|
||||
}{
|
||||
{"FORGEJO_", false},
|
||||
{"FORGEJO_123", false},
|
||||
{"FORGEJO_ABC", false},
|
||||
{"GITEA_", false},
|
||||
{"GITEA_123", false},
|
||||
{"GITEA_ABC", false},
|
||||
{"GITHUB_", false},
|
||||
{"GITHUB_123", false},
|
||||
{"GITHUB_ABC", false},
|
||||
{"123_TEST", false},
|
||||
{"CI", true},
|
||||
{"_CI", true},
|
||||
{"CI_", true},
|
||||
{"CI123", true},
|
||||
{"CIABC", true},
|
||||
{"FORGEJO", true},
|
||||
{"FORGEJO123", true},
|
||||
{"FORGEJOABC", true},
|
||||
{"GITEA", true},
|
||||
{"GITEA123", true},
|
||||
{"GITEAABC", true},
|
||||
{"GITHUB", true},
|
||||
{"GITHUB123", true},
|
||||
{"GITHUBABC", true},
|
||||
{"_123_TEST", true},
|
||||
}
|
||||
for _, tC := range testCases {
|
||||
t.Run(tC.name, func(t *testing.T) {
|
||||
t.Helper()
|
||||
if tC.valid {
|
||||
assert.NoError(t, ValidateName(tC.name))
|
||||
assert.NoError(t, ValidateName(strings.ToLower(tC.name)))
|
||||
} else {
|
||||
require.ErrorIs(t, ValidateName(tC.name), ErrInvalidName)
|
||||
require.ErrorIs(t, ValidateName(strings.ToLower(tC.name)), ErrInvalidName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -30,8 +30,18 @@
|
|||
<span class="color-text-light-2">
|
||||
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
|
||||
</span>
|
||||
<button class="btn interact-bg tw-p-2 show-modal"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "actions.secrets.edit_button" .Name}}"
|
||||
data-modal="#edit-secret-modal"
|
||||
data-modal-form.action="{{$.Link}}/{{.ID}}/edit"
|
||||
data-modal-header="{{ctx.Locale.Tr "actions.secrets.mutation.header" .Name}}"
|
||||
data-modal-dialog-secret-name="{{.Name}}"
|
||||
data-modal-dialog-secret-data=""
|
||||
>
|
||||
{{svg "octicon-pencil"}}
|
||||
</button>
|
||||
<button class="ui btn interact-bg link-action tw-p-2"
|
||||
data-url="{{$.Link}}/delete?id={{.ID}}"
|
||||
data-url="{{$.Link}}/{{.ID}}/delete"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
|
||||
data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"
|
||||
>
|
||||
|
|
@ -57,7 +67,7 @@
|
|||
{{ctx.Locale.Tr "secrets.description"}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="secret-name">{{ctx.Locale.Tr "name"}}</label>
|
||||
<label class="required" for="secret-name">{{ctx.Locale.Tr "name"}}</label>
|
||||
<input autofocus required
|
||||
id="secret-name"
|
||||
name="name"
|
||||
|
|
@ -67,7 +77,7 @@
|
|||
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.creation.name_description"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="secret-data">{{ctx.Locale.Tr "value"}}</label>
|
||||
<label class="required" for="secret-data">{{ctx.Locale.Tr "value"}}</label>
|
||||
<textarea required
|
||||
id="secret-data"
|
||||
name="data"
|
||||
|
|
@ -78,3 +88,32 @@
|
|||
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
|
||||
</form>
|
||||
</div>
|
||||
{{/* Edit secret dialog */}}
|
||||
<div class="ui small modal" id="edit-secret-modal">
|
||||
<div class="header">
|
||||
<span id="actions-modal-header"></span>
|
||||
</div>
|
||||
<form class="ui form form-fetch-action" method="post">
|
||||
<fieldset class="content">
|
||||
<div class="field">
|
||||
{{ctx.Locale.Tr "secrets.description"}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="required" for="dialog-secret-name">{{ctx.Locale.Tr "name"}}</label>
|
||||
<input autofocus required
|
||||
id="dialog-secret-name"
|
||||
name="name"
|
||||
value="{{.name}}"
|
||||
pattern="^(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
|
||||
>
|
||||
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.name_description"}}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dialog-secret-data">{{ctx.Locale.Tr "value"}}</label>
|
||||
<textarea id="dialog-secret-data" name="data"></textarea>
|
||||
<p id="secret-data-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.value_description"}}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,156 +19,388 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsSecretsManageUserSecrets(t *testing.T) {
|
||||
func TestActionsSecretsCreateSecret(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
url := "/user/settings/actions/secrets"
|
||||
session := loginUser(t, user.Name)
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
|
||||
t.Run("Create secret", func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
sess := loginUser(t, user2.Name)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
}{
|
||||
{
|
||||
name: "User",
|
||||
url: "/user/settings/actions/secrets",
|
||||
ownerID: user2.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
{
|
||||
name: "Repository",
|
||||
url: "/" + repo1.FullName() + "/settings/actions/secrets",
|
||||
ownerID: 0,
|
||||
repoID: repo1.ID,
|
||||
},
|
||||
{
|
||||
name: "Organization",
|
||||
url: "/org/" + org3.Name + "/settings/actions/secrets",
|
||||
ownerID: org3.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "my_secret",
|
||||
"data": " \r\n\tSecrët dåtä\\ \r\n",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := session.GetCookie(app_context.CookieNameFlash)
|
||||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: user.ID, RepoID: 0, Name: "MY_SECRET"})
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "MY_SECRET"})
|
||||
assert.Equal(t, "MY_SECRET", secret.Name)
|
||||
|
||||
value, err := secret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
|
||||
})
|
||||
|
||||
t.Run("Remove secret", func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
"name": "TEST_SECRET",
|
||||
"data": "value",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := session.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: user.ID, RepoID: 0, Name: "TEST_SECRET"})
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("%s/delete?id=%d", url, secret.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie = session.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
|
||||
|
||||
unittest.AssertNotExistsBean(t, secret)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsSecretsManageRepositorySecrets(t *testing.T) {
|
||||
func TestActionsSecretsCreateSecretRejectsNameMatchingExistingSecret(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
|
||||
url := "/" + repo.FullName() + "/settings/actions/secrets"
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
sess := loginUser(t, user2.Name)
|
||||
|
||||
t.Run("Create secret", func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
}{
|
||||
{
|
||||
name: "User",
|
||||
url: "/user/settings/actions/secrets",
|
||||
ownerID: user2.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
{
|
||||
name: "Repository",
|
||||
url: "/" + repo1.FullName() + "/settings/actions/secrets",
|
||||
ownerID: 0,
|
||||
repoID: repo1.ID,
|
||||
},
|
||||
{
|
||||
name: "Organization",
|
||||
url: "/org/" + org3.Name + "/settings/actions/secrets",
|
||||
ownerID: org3.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "my_secret",
|
||||
"data": " \r\n\tSecrët dåtä\\ \r\n",
|
||||
"data": "original value",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := session.GetCookie(app_context.CookieNameFlash)
|
||||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: 0, RepoID: repo.ID, Name: "MY_SECRET"})
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "MY_SECRET"})
|
||||
assert.Equal(t, "MY_SECRET", secret.Name)
|
||||
|
||||
value, err := secret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
|
||||
assert.Equal(t, "original value", value)
|
||||
|
||||
// Try to create a new secret with the name but another value.
|
||||
req = NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "my_secret",
|
||||
"data": "changed value",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
t.Run("Remove secret", func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
"name": "TEST_SECRET",
|
||||
"data": "value",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := session.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: 0, RepoID: repo.ID, Name: "TEST_SECRET"})
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("%s/delete?id=%d", url, secret.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie = session.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
|
||||
|
||||
unittest.AssertNotExistsBean(t, secret)
|
||||
// Verify that the original secret has not been changed.
|
||||
secret = unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "MY_SECRET"})
|
||||
value, err = secret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "MY_SECRET", secret.Name)
|
||||
assert.Equal(t, "original value", value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsSecretsManageOrganizationSecrets(t *testing.T) {
|
||||
func TestActionsSecretsEditSecret(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
url := "/org/" + org.Name + "/settings/actions/secrets"
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
sess := loginUser(t, user2.Name)
|
||||
|
||||
t.Run("Create secret", func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
"name": "my_secret",
|
||||
"data": " \r\n\tSecrët dåtä\\ \r\n",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
}{
|
||||
{
|
||||
name: "User",
|
||||
url: "/user/settings/actions/secrets",
|
||||
ownerID: user2.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
{
|
||||
name: "Repository",
|
||||
url: "/" + repo1.FullName() + "/settings/actions/secrets",
|
||||
ownerID: 0,
|
||||
repoID: repo1.ID,
|
||||
},
|
||||
{
|
||||
name: "Organization",
|
||||
url: "/org/" + org3.Name + "/settings/actions/secrets",
|
||||
ownerID: org3.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
}
|
||||
|
||||
flashCookie := session.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: org.ID, RepoID: 0, Name: "MY_SECRET"})
|
||||
assert.Equal(t, "MY_SECRET", secret.Name)
|
||||
|
||||
value, err := secret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
|
||||
})
|
||||
|
||||
t.Run("Remove secret", func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", url, map[string]string{
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "TEST_SECRET",
|
||||
"data": "value",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := session.GetCookie(app_context.CookieNameFlash)
|
||||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: org.ID, RepoID: 0, Name: "TEST_SECRET"})
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("%s/delete?id=%d", url, secret.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", testCase.url, secret.ID), map[string]string{
|
||||
"name": secret.Name,
|
||||
"data": " \r\n\tSecrët dåtä\\ \r\n",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie = session.GetCookie(app_context.CookieNameFlash)
|
||||
flashCookie = sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
|
||||
|
||||
updatedSecret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{ID: secret.ID})
|
||||
decryptedValue, err := updatedSecret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, secret.Name, updatedSecret.Name)
|
||||
assert.Equal(t, " \n\tSecrët dåtä\\ \n", decryptedValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsSecretsEditSecretWithoutChangingItsValue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
|
||||
sess := loginUser(t, user2.Name)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
}{
|
||||
{
|
||||
name: "User",
|
||||
url: "/user/settings/actions/secrets",
|
||||
ownerID: user2.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
{
|
||||
name: "Repository",
|
||||
url: "/" + repo1.FullName() + "/settings/actions/secrets",
|
||||
ownerID: 0,
|
||||
repoID: repo1.ID,
|
||||
},
|
||||
{
|
||||
name: "Organization",
|
||||
url: "/org/" + org3.Name + "/settings/actions/secrets",
|
||||
ownerID: org3.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "TEST_SECRET",
|
||||
"data": " \r\n\tSecrët dåtä\\ \r\n",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", testCase.url, secret.ID), map[string]string{
|
||||
"name": "changed_secret",
|
||||
"data": "",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie = sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522CHANGED_SECRET%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
|
||||
|
||||
updatedSecret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{ID: secret.ID})
|
||||
decryptedValue, err := updatedSecret.GetDecryptedData()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "CHANGED_SECRET", updatedSecret.Name)
|
||||
assert.Equal(t, " \n\tSecrët dåtä\\ \n", decryptedValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsSecretsEditSecretRejectsInvalidName(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
|
||||
sess := loginUser(t, user2.Name)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
}{
|
||||
{
|
||||
name: "User",
|
||||
url: "/user/settings/actions/secrets",
|
||||
ownerID: user2.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
{
|
||||
name: "Repository",
|
||||
url: "/" + repo1.FullName() + "/settings/actions/secrets",
|
||||
ownerID: 0,
|
||||
repoID: repo1.ID,
|
||||
},
|
||||
{
|
||||
name: "Organization",
|
||||
url: "/org/" + org3.Name + "/settings/actions/secrets",
|
||||
ownerID: org3.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "TEST_SECRET",
|
||||
"data": "value",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", testCase.url, secret.ID), map[string]string{
|
||||
"name": "FORGEJO_IS_INVALID",
|
||||
"data": "",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsSecretsRemoveSecret(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
|
||||
sess := loginUser(t, user2.Name)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
ownerID int64
|
||||
repoID int64
|
||||
}{
|
||||
{
|
||||
name: "User",
|
||||
url: "/user/settings/actions/secrets",
|
||||
ownerID: user2.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
{
|
||||
name: "Repository",
|
||||
url: "/" + repo1.FullName() + "/settings/actions/secrets",
|
||||
ownerID: 0,
|
||||
repoID: repo1.ID,
|
||||
},
|
||||
{
|
||||
name: "Organization",
|
||||
url: "/org/" + org3.Name + "/settings/actions/secrets",
|
||||
ownerID: org3.ID,
|
||||
repoID: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
|
||||
"name": "TEST_SECRET",
|
||||
"data": "value",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
|
||||
|
||||
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", testCase.url, secret.ID))
|
||||
sess.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
flashCookie = sess.GetCookie(app_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
|
||||
|
||||
unittest.AssertNotExistsBean(t, secret)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
org_model "forgejo.org/models/organization"
|
||||
secret_model "forgejo.org/models/secret"
|
||||
"forgejo.org/models/unittest"
|
||||
|
|
@ -157,7 +158,8 @@ func TestAPIOrgSecrets(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Delete with forbidden names", func(t *testing.T) {
|
||||
secret, err := secret_model.InsertEncryptedSecret(t.Context(), org.ID, 0, "FORGEJO_FORBIDDEN", "illegal")
|
||||
secret := secret_model.Secret{OwnerID: org.ID, RepoID: 0, Name: "FORGEJO_FORBIDDEN"}
|
||||
err := db.Insert(t.Context(), secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets/%s", org.Name, secret.Name)
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ func TestAPIRepoSecrets(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Delete with forbidden names", func(t *testing.T) {
|
||||
secret, err := secret_model.InsertEncryptedSecret(t.Context(), 0, repo.ID, "FORGEJO_FORBIDDEN", "illegal")
|
||||
secret := secret_model.Secret{OwnerID: 0, RepoID: repo.ID, Name: "FORGEJO_FORBIDDEN"}
|
||||
err := db.Insert(t.Context(), secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/actions/secrets/%s", repo.FullName(), secret.Name)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
secret_model "forgejo.org/models/secret"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
|
@ -122,7 +123,8 @@ func TestAPIUserSecrets(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Delete with forbidden names", func(t *testing.T) {
|
||||
secret, err := secret_model.InsertEncryptedSecret(t.Context(), user.ID, 0, "FORGEJO_FORBIDDEN", "illegal")
|
||||
secret := secret_model.Secret{OwnerID: user.ID, RepoID: 0, Name: "FORGEJO_FORBIDDEN"}
|
||||
err := db.Insert(t.Context(), secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/user/actions/secrets/%s", secret.Name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue