2
0

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.
This commit is contained in:
2026-01-24 14:57:37 -05:00
parent f514ec905f
commit db8f606a5c
7 changed files with 105 additions and 10 deletions

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,38 @@
{{if .GlobalSecrets}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "secrets.global_secrets"}}
<span class="ui basic label tw-ml-2">{{ctx.Locale.Tr "secrets.read_only"}}</span>
</h4>
<div class="ui attached segment">
<p class="tw-text-sm tw-text-gray-500 tw-mb-4">{{ctx.Locale.Tr "secrets.global_secrets.description"}}</p>
<div class="flex-list">
{{range .GlobalSecrets}}
<div class="flex-item tw-items-center">
<div class="flex-item-leading">
{{svg "octicon-globe" 32}}
</div>
<div class="flex-item-main">
<div class="flex-item-title">
{{.Name}}
</div>
<div class="flex-item-body">
{{if .Description}}{{.Description}}{{else}}-{{end}}
</div>
<div class="flex-item-body">
******
</div>
</div>
<div class="flex-item-trailing">
<span class="color-text-light-2">
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
</span>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "secrets.management"}}
<div class="ui right">
@@ -85,12 +120,13 @@
</div>
<div class="field">
<label for="secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea required
<textarea
id="secret-data"
name="data"
maxlength="{{.DataMaxLength}}"
placeholder="{{ctx.Locale.Tr "secrets.creation.value_placeholder"}}"
></textarea>
<p class="help tw-hidden" id="secret-data-optional-hint">{{ctx.Locale.Tr "secrets.update.value_optional"}}</p>
</div>
<div class="field">
<label for="secret-description">{{ctx.Locale.Tr "secrets.creation.description"}}</label>
@@ -106,3 +142,21 @@
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<script>
// Handle required attribute for secret data based on create vs edit mode
document.getElementById('add-secret-modal')?.addEventListener('shown.bs.modal', function() {
const nameInput = document.getElementById('secret-name');
const dataInput = document.getElementById('secret-data');
const optionalHint = document.getElementById('secret-data-optional-hint');
if (nameInput && dataInput && optionalHint) {
const isEditing = nameInput.readOnly;
if (isEditing) {
dataInput.removeAttribute('required');
optionalHint.classList.remove('tw-hidden');
} else {
dataInput.setAttribute('required', 'required');
optionalHint.classList.add('tw-hidden');
}
}
});
</script>