mirror of
https://gitea.com/gitea/tea.git
synced 2026-09-10 07:26:33 -04:00
fix(actions): validate secret name and value on create
`runSecretsCreate` previously accepted any string as a secret name and value, forwarding invalid input directly to the Gitea API. The API either rejected with a generic error or accepted inputs that don't match Gitea's documented constraints, producing confusing UX. Add `validateSecretName` enforcing the Gitea Actions naming rules: uppercase letters, numbers, and underscores only; cannot start with a number; cannot use the reserved `GITHUB_` / `GITEA_` prefixes. Add `validateSecretValue` enforcing a non-empty value with a 64KB upper bound (matches the Gitea API limit). Mirror the structure of the existing variable validation in `cmd/actions/variables/set.go` for consistency.
This commit is contained in:
parent
58931b5d17
commit
50bcc1a49d
|
|
@ -6,6 +6,8 @@ package secrets
|
||||||
import (
|
import (
|
||||||
stdctx "context"
|
stdctx "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
gitea "gitea.dev/sdk"
|
gitea "gitea.dev/sdk"
|
||||||
|
|
||||||
|
|
@ -50,6 +52,9 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
client := c.Login.Client()
|
client := c.Login.Client()
|
||||||
|
|
||||||
secretName := cmd.Args().First()
|
secretName := cmd.Args().First()
|
||||||
|
if err := validateSecretName(secretName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Read secret value using the utility
|
// Read secret value using the utility
|
||||||
secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
|
secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
|
||||||
|
|
@ -62,6 +67,10 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateSecretValue(secretValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
_, err = client.Actions.CreateRepoSecret(ctx, c.Owner, c.Repo, secretName, gitea.CreateOrUpdateSecretOption{
|
_, err = client.Actions.CreateRepoSecret(ctx, c.Owner, c.Repo, secretName, gitea.CreateOrUpdateSecretOption{
|
||||||
Data: secretValue,
|
Data: secretValue,
|
||||||
})
|
})
|
||||||
|
|
@ -72,3 +81,41 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
|
||||||
fmt.Printf("Secret '%s' created successfully\n", secretName)
|
fmt.Printf("Secret '%s' created successfully\n", secretName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateSecretName validates that a secret name follows the required format.
|
||||||
|
func validateSecretName(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("secret name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret names must be uppercase letters, numbers, and underscores only.
|
||||||
|
// Cannot start with a number.
|
||||||
|
validPattern := regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`)
|
||||||
|
if !validPattern.MatchString(name) {
|
||||||
|
return fmt.Errorf("secret name must contain only uppercase letters, numbers, and underscores, and cannot start with a number")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserved prefixes used by Gitea/GitHub internals.
|
||||||
|
reservedPrefixes := []string{"GITHUB_", "GITEA_"}
|
||||||
|
for _, prefix := range reservedPrefixes {
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
return fmt.Errorf("secret name cannot start with reserved prefix: %s", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateSecretValue validates that a secret value is acceptable.
|
||||||
|
func validateSecretValue(value string) error {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return fmt.Errorf("secret value cannot be empty or whitespace only")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 64KB upper limit per Gitea API.
|
||||||
|
if len(value) > 65536 {
|
||||||
|
return fmt.Errorf("secret value cannot exceed 64KB")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,133 @@
|
||||||
package secrets
|
package secrets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestValidateSecretName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid name",
|
||||||
|
input: "VALID_SECRET_NAME",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid name with numbers",
|
||||||
|
input: "SECRET_123",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid name starting with underscore",
|
||||||
|
input: "_SECRET",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - lowercase",
|
||||||
|
input: "invalid_secret",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - mixed case",
|
||||||
|
input: "Invalid_Secret",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - spaces",
|
||||||
|
input: "INVALID SECRET",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - special chars",
|
||||||
|
input: "INVALID-SECRET!",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - starts with number",
|
||||||
|
input: "1SECRET",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - empty",
|
||||||
|
input: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - reserved GITHUB_ prefix",
|
||||||
|
input: "GITHUB_TOKEN",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - reserved GITEA_ prefix",
|
||||||
|
input: "GITEA_SECRET",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateSecretName(tt.input)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateSecretName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSecretValue(t *testing.T) {
|
||||||
|
largeValue := strings.Repeat("a", 65537)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid value",
|
||||||
|
input: "secret_value",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid value with whitespace",
|
||||||
|
input: "secret value with spaces",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - empty",
|
||||||
|
input: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - whitespace only",
|
||||||
|
input: " \t\n ",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid - exceeds 64KB",
|
||||||
|
input: largeValue,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid - exactly 64KB",
|
||||||
|
input: strings.Repeat("a", 65536),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateSecretValue(tt.input)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("validateSecretValue(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetSecretSourceArgs(t *testing.T) {
|
func TestGetSecretSourceArgs(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue