From db8f606a5ced46a026222adffa6e7a2f97d66249 Mon Sep 17 00:00:00 2001 From: logikonline Date: Sat, 24 Jan 2026 14:57:37 -0500 Subject: [PATCH] feat(secrets): allow description-only updates and show global secrets Enables updating secret descriptions without changing the value by making the data field optional during updates. Displays global secrets as read-only in org/user/repo secret pages for visibility. Adds validation to require data only when creating new secrets. Updates locale strings for the new functionality. --- .notes/note-1769283562795-n834eh806.json | 5 +-- models/secret/secret.go | 22 ++++++++-- options/locale/locale_en-US.json | 4 ++ routers/web/shared/secrets/secrets.go | 11 +++++ services/forms/user_form.go | 5 ++- services/secrets/secrets.go | 12 +++++ templates/shared/secrets/add_list.tmpl | 56 +++++++++++++++++++++++- 7 files changed, 105 insertions(+), 10 deletions(-) diff --git a/.notes/note-1769283562795-n834eh806.json b/.notes/note-1769283562795-n834eh806.json index 7c2bb62ec4..bd233a39bf 100644 --- a/.notes/note-1769283562795-n834eh806.json +++ b/.notes/note-1769283562795-n834eh806.json @@ -1,8 +1,7 @@ { "id": "note-1769283562795-n834eh806", "title": "translate", - "content": " \"packages.visibility\": \"Visibility\",\n \"packages.settings.visibility.private.text\": \"This package is currently private. Make it public to allow anyone to access it.\",\n \"packages.settings.visibility.private.button\": \"Make Private\",\n \"packages.settings.visibility.private.bullet_title\": \"You are about to make this package private.\",\n \"packages.settings.visibility.private.bullet_one\": \"Only users with appropriate permissions will be able to access this package.\",\n \"packages.settings.visibility.private.success\": \"Package is now private.\",\n \"packages.settings.visibility.public.text\": \"This package is currently public. Make it private to restrict access.\",\n \"packages.settings.visibility.public.button\": \"Make Public\",\n \"packages.settings.visibility.public.bullet_title\": \"You are about to make this package public.\",\n \"packages.settings.visibility.public.bullet_one\": \"Anyone will be able to access and download this package.\",\n \"packages.settings.visibility.public.success\": \"Package is now public.\",\n \"packages.settings.visibility.error\": \"Failed to update package visibility.\",", + "content": " \u0022packages.visibility\u0022: \u0022Visibility\u0022,\n \u0022packages.settings.visibility.private.text\u0022: \u0022This package is currently private. Make it public to allow anyone to access it.\u0022,\n \u0022packages.settings.visibility.private.button\u0022: \u0022Make Private\u0022,\n \u0022packages.settings.visibility.private.bullet_title\u0022: \u0022You are about to make this package private.\u0022,\n \u0022packages.settings.visibility.private.bullet_one\u0022: \u0022Only users with appropriate permissions will be able to access this package.\u0022,\n \u0022packages.settings.visibility.private.success\u0022: \u0022Package is now private.\u0022,\n \u0022packages.settings.visibility.public.text\u0022: \u0022This package is currently public. Make it private to restrict access.\u0022,\n \u0022packages.settings.visibility.public.button\u0022: \u0022Make Public\u0022,\n \u0022packages.settings.visibility.public.bullet_title\u0022: \u0022You are about to make this package public.\u0022,\n \u0022packages.settings.visibility.public.bullet_one\u0022: \u0022Anyone will be able to access and download this package.\u0022,\n \u0022packages.settings.visibility.public.success\u0022: \u0022Package is now public.\u0022,\n \u0022packages.settings.visibility.error\u0022: \u0022Failed to update package visibility.\u0022,\n\n \u0022secrets.update.value_optional\u0022: \u0022Leave empty to keep the existing value and only update the description.\u0022,\n \u0022secrets.global_secrets\u0022: \u0022Global Secrets\u0022,\n \u0022secrets.global_secrets.description\u0022: \u0022These secrets are configured by system administrators and are available to all workflows. They cannot be modified here.\u0022,\n \u0022secrets.read_only\u0022: \u0022Read-only\u0022,", "createdAt": 1769283562793, - "updatedAt": 1769283566248, - "tags": [] + "updatedAt": 1769284645347 } \ No newline at end of file diff --git a/models/secret/secret.go b/models/secret/secret.go index 57c665a877..20235dc115 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -133,14 +133,28 @@ func (opts FindSecretsOptions) ToConds() builder.Cond { return cond } -// UpdateSecret changes org or user reop secret. +// UpdateSecret changes org or user repo secret. +// If data is empty, only the description is updated. func UpdateSecret(ctx context.Context, secretID int64, data, description string) error { + description = util.TruncateRunes(description, SecretDescriptionMaxLength) + + // If data is empty, only update description + if data == "" { + s := &Secret{ + Description: description, + } + affected, err := db.GetEngine(ctx).ID(secretID).Cols("description").Update(s) + if affected != 1 && err == nil { + return ErrSecretNotFound{} + } + return err + } + + // Update both data and description if len(data) > SecretDataMaxLength { return util.NewInvalidArgumentErrorf("data too long") } - description = util.TruncateRunes(description, SecretDescriptionMaxLength) - encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data) if err != nil { return err @@ -151,7 +165,7 @@ func UpdateSecret(ctx context.Context, secretID int64, data, description string) Description: description, } affected, err := db.GetEngine(ctx).ID(secretID).Cols("data", "description").Update(s) - if affected != 1 { + if affected != 1 && err == nil { return ErrSecretNotFound{} } return err diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 0dd6c57ec5..47c07f9ba7 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3729,6 +3729,7 @@ "secrets.creation.description": "Description", "secrets.creation.name_placeholder": "case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_", "secrets.creation.value_placeholder": "Input any content. Whitespace at the start and end will be omitted.", + "secrets.update.value_optional": "Leave empty to keep the existing value and only update the description.", "secrets.creation.description_placeholder": "Enter short description (optional).", "secrets.save_success": "The secret \"%s\" has been saved.", "secrets.save_failed": "Failed to save secret.", @@ -3739,6 +3740,9 @@ "secrets.deletion.success": "The secret has been removed.", "secrets.deletion.failed": "Failed to remove secret.", "secrets.management": "Secrets Management", + "secrets.global_secrets": "Global Secrets", + "secrets.global_secrets.description": "These secrets are configured by system administrators and are available to all workflows. They cannot be modified here.", + "secrets.read_only": "Read-only", "actions.actions": "Actions", "actions.unit.desc": "Manage actions", "actions.status.unknown": "Unknown", diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go index 1df9f32e46..dff24ef8f9 100644 --- a/routers/web/shared/secrets/secrets.go +++ b/routers/web/shared/secrets/secrets.go @@ -29,6 +29,17 @@ func SetSecretsContextWithGlobal(ctx *context.Context, ownerID, repoID int64, gl ctx.Data["Secrets"] = secrets ctx.Data["DataMaxLength"] = secret_model.SecretDataMaxLength ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength + + // For non-global contexts, also fetch global secrets to show as read-only + if !global { + globalSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{Global: true}) + if err != nil { + log.Error("FindGlobalSecrets failed: %v", err) + // Don't fail the request, just don't show global secrets + return + } + ctx.Data["GlobalSecrets"] = globalSecrets + } } func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) { diff --git a/services/forms/user_form.go b/services/forms/user_form.go index 252893fa51..fe16a8a2df 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -322,10 +322,11 @@ 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 +// AddSecretForm for adding or updating secrets +// Note: Data is optional when updating (to allow description-only updates) type AddSecretForm struct { Name string `binding:"Required;MaxSize(255)"` - Data string `binding:"Required;MaxSize(65535)"` + Data string `binding:"MaxSize(65535)"` Description string `binding:"MaxSize(65535)"` } diff --git a/services/secrets/secrets.go b/services/secrets/secrets.go index 0919b57c63..9ac8d265a2 100644 --- a/services/secrets/secrets.go +++ b/services/secrets/secrets.go @@ -5,9 +5,11 @@ package secrets import ( "context" + "fmt" "code.gitcaddy.com/server/v3/models/db" secret_model "code.gitcaddy.com/server/v3/models/secret" + "code.gitcaddy.com/server/v3/modules/util" ) func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data, description string) (*secret_model.Secret, bool, error) { @@ -25,6 +27,10 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data } if len(s) == 0 { + // Creating new secret - data is required + if data == "" { + return nil, false, fmt.Errorf("%w: secret value is required for new secrets", util.ErrInvalidArgument) + } s, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, name, data, description) if err != nil { return nil, false, err @@ -32,6 +38,7 @@ func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data return s, true, nil } + // Updating existing secret - data is optional (description-only update allowed) if err := secret_model.UpdateSecret(ctx, s[0].ID, data, description); err != nil { return nil, false, err } @@ -55,6 +62,10 @@ func CreateOrUpdateGlobalSecret(ctx context.Context, name, data, description str } if len(s) == 0 { + // Creating new secret - data is required + if data == "" { + return nil, false, fmt.Errorf("%w: secret value is required for new secrets", util.ErrInvalidArgument) + } // Insert with ownerID=0, repoID=0 for global secret s, err := secret_model.InsertEncryptedSecret(ctx, 0, 0, name, data, description) if err != nil { @@ -63,6 +74,7 @@ func CreateOrUpdateGlobalSecret(ctx context.Context, name, data, description str return s, true, nil } + // Updating existing secret - data is optional (description-only update allowed) if err := secret_model.UpdateSecret(ctx, s[0].ID, data, description); err != nil { return nil, false, err } diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl index 88f46c6854..f5ec927867 100644 --- a/templates/shared/secrets/add_list.tmpl +++ b/templates/shared/secrets/add_list.tmpl @@ -1,3 +1,38 @@ +{{if .GlobalSecrets}} +

+ {{ctx.Locale.Tr "secrets.global_secrets"}} + {{ctx.Locale.Tr "secrets.read_only"}} +

+
+

{{ctx.Locale.Tr "secrets.global_secrets.description"}}

+
+ {{range .GlobalSecrets}} +
+
+ {{svg "octicon-globe" 32}} +
+
+
+ {{.Name}} +
+
+ {{if .Description}}{{.Description}}{{else}}-{{end}} +
+
+ ****** +
+
+
+ + {{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}} + +
+
+ {{end}} +
+
+{{end}} +

{{ctx.Locale.Tr "secrets.management"}}
@@ -85,12 +120,13 @@
- +

{{ctx.Locale.Tr "secrets.update.value_optional"}}

@@ -106,3 +142,21 @@ {{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
+