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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user