summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Song <i@wolfogre.com>2022-12-20 17:07:13 +0800
committerGitHub <noreply@github.com>2022-12-20 17:07:13 +0800
commit659055138b6d32492b20c9f4d1d5a3cdaa47188d (patch)
treee2e7741be2b7b349e04f6901bff92b75b9b7c9ac
parent40ba750c4bf1f3f5f8dff5af57b2db4b600f237f (diff)
downloadgitea-659055138b6d32492b20c9f4d1d5a3cdaa47188d.tar.gz
gitea-659055138b6d32492b20c9f4d1d5a3cdaa47188d.zip
Secrets storage with SecretKey encrypted (#22142)
Fork of #14483, but [gave up MasterKey](https://github.com/go-gitea/gitea/pull/14483#issuecomment-1350728557), and fixed some problems. Close #12065. Needed by #13539. Featrues: - Secrets for repo and org, not user yet. - Use SecretKey to encrypte/encrypt secrets. - Trim spaces of secret value. - Add a new locale ini block, to make it easy to support secrets for user. Snapshots: Repo level secrets: ![image](https://user-images.githubusercontent.com/9418365/207823319-b8a4903f-38ca-4af7-9d05-336a5af906f3.png) Rrg level secrets ![image](https://user-images.githubusercontent.com/9418365/207823371-8bd02e93-1928-40d1-8c76-f48b255ace36.png) Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: KN4CK3R <admin@oldschoolhack.me>
-rw-r--r--docs/content/doc/secrets/overview.en-us.md36
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v1_19/v236.go23
-rw-r--r--models/organization/org.go2
-rw-r--r--models/repo.go2
-rw-r--r--models/secret/secret.go124
-rw-r--r--options/locale/locale_en-US.ini16
-rw-r--r--routers/web/org/setting.go51
-rw-r--r--routers/web/repo/setting.go40
-rw-r--r--routers/web/web.go10
-rw-r--r--services/forms/user_form.go12
-rw-r--r--templates/org/settings/navbar.tmpl3
-rw-r--r--templates/org/settings/secrets.tmpl83
-rw-r--r--templates/repo/settings/deploy_keys.tmpl2
-rw-r--r--templates/repo/settings/nav.tmpl2
-rw-r--r--templates/repo/settings/navbar.tmpl2
-rw-r--r--templates/repo/settings/secrets.tmpl60
17 files changed, 468 insertions, 2 deletions
diff --git a/docs/content/doc/secrets/overview.en-us.md b/docs/content/doc/secrets/overview.en-us.md
new file mode 100644
index 0000000000..1a88d6cfbc
--- /dev/null
+++ b/docs/content/doc/secrets/overview.en-us.md
@@ -0,0 +1,36 @@
+---
+date: "2022-12-19T21:26:00+08:00"
+title: "Encrypted secrets"
+slug: "secrets/overview"
+draft: false
+toc: false
+menu:
+ sidebar:
+ parent: "secrets"
+ name: "Overview"
+ weight: 1
+ identifier: "overview"
+---
+
+# Encrypted secrets
+
+Encrypted secrets allow you to store sensitive information in your organization or repository.
+Secrets are available on Gitea 1.19+.
+
+# Naming your secrets
+
+The following rules apply to secret names:
+
+Secret names can only contain alphanumeric characters (`[a-z]`, `[A-Z]`, `[0-9]`) or underscores (`_`). Spaces are not allowed.
+
+Secret names must not start with the `GITHUB_` and `GITEA_` prefix.
+
+Secret names must not start with a number.
+
+Secret names are not case-sensitive.
+
+Secret names must be unique at the level they are created at.
+
+For example, a secret created at the repository level must have a unique name in that repository, and a secret created at the organization level must have a unique name at that level.
+
+If a secret with the same name exists at multiple levels, the secret at the lowest level takes precedence. For example, if an organization-level secret has the same name as a repository-level secret, then the repository-level secret takes precedence.
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index e718355f83..591bfa3e86 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -442,6 +442,8 @@ var migrations = []Migration{
NewMigration("Add package cleanup rule table", v1_19.CreatePackageCleanupRuleTable),
// v235 -> v236
NewMigration("Add index for access_token", v1_19.AddIndexForAccessToken),
+ // v236 -> v237
+ NewMigration("Create secrets table", v1_19.CreateSecretsTable),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_19/v236.go b/models/migrations/v1_19/v236.go
new file mode 100644
index 0000000000..f172a85b1f
--- /dev/null
+++ b/models/migrations/v1_19/v236.go
@@ -0,0 +1,23 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_19 //nolint
+
+import (
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/xorm"
+)
+
+func CreateSecretsTable(x *xorm.Engine) error {
+ type Secret struct {
+ ID int64
+ OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"`
+ RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL DEFAULT 0"`
+ Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
+ Data string `xorm:"LONGTEXT"`
+ CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
+ }
+
+ return x.Sync(new(Secret))
+}
diff --git a/models/organization/org.go b/models/organization/org.go
index b3d77b4ec6..9d9e9cda46 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
+ secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@@ -370,6 +371,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
&TeamUser{OrgID: org.ID},
&TeamUnit{OrgID: org.ID},
&TeamInvite{OrgID: org.ID},
+ &secret_model.Secret{OwnerID: org.ID},
); err != nil {
return fmt.Errorf("DeleteBeans: %w", err)
}
diff --git a/models/repo.go b/models/repo.go
index 9af600c9ba..e95887077c 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -21,6 +21,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
repo_model "code.gitea.io/gitea/models/repo"
+ secret_model "code.gitea.io/gitea/models/secret"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -150,6 +151,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
&admin_model.Task{RepoID: repoID},
&repo_model.Watch{RepoID: repoID},
&webhook.Webhook{RepoID: repoID},
+ &secret_model.Secret{RepoID: repoID},
); err != nil {
return fmt.Errorf("deleteBeans: %w", err)
}
diff --git a/models/secret/secret.go b/models/secret/secret.go
new file mode 100644
index 0000000000..f970d5319e
--- /dev/null
+++ b/models/secret/secret.go
@@ -0,0 +1,124 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package secret
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ secret_module "code.gitea.io/gitea/modules/secret"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
+)
+
+type ErrSecretInvalidValue struct {
+ Name *string
+ Data *string
+}
+
+func (err ErrSecretInvalidValue) Error() string {
+ if err.Name != nil {
+ return fmt.Sprintf("secret name %q is invalid", *err.Name)
+ }
+ if err.Data != nil {
+ return fmt.Sprintf("secret data %q is invalid", *err.Data)
+ }
+ return util.ErrInvalidArgument.Error()
+}
+
+func (err ErrSecretInvalidValue) Unwrap() error {
+ return util.ErrInvalidArgument
+}
+
+// Secret represents a secret
+type Secret struct {
+ ID int64
+ OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"`
+ RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL DEFAULT 0"`
+ Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
+ Data string `xorm:"LONGTEXT"` // encrypted data
+ CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
+}
+
+// newSecret Creates a new already encrypted secret
+func newSecret(ownerID, repoID int64, name, data string) *Secret {
+ return &Secret{
+ OwnerID: ownerID,
+ RepoID: repoID,
+ Name: strings.ToUpper(name),
+ Data: data,
+ }
+}
+
+// InsertEncryptedSecret Creates, encrypts, and validates a new secret with yet unencrypted data and insert into database
+func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*Secret, error) {
+ encrypted, err := secret_module.EncryptSecret(setting.SecretKey, strings.TrimSpace(data))
+ if err != nil {
+ return nil, err
+ }
+ secret := newSecret(ownerID, repoID, name, encrypted)
+ if err := secret.Validate(); err != nil {
+ return secret, err
+ }
+ return secret, db.Insert(ctx, secret)
+}
+
+func init() {
+ db.RegisterModel(new(Secret))
+}
+
+var (
+ secretNameReg = regexp.MustCompile("^[A-Z_][A-Z0-9_]*$")
+ forbiddenSecretPrefixReg = regexp.MustCompile("^GIT(EA|HUB)_")
+)
+
+// Validate validates the required fields and formats.
+func (s *Secret) Validate() error {
+ switch {
+ case len(s.Name) == 0 || len(s.Name) > 50:
+ return ErrSecretInvalidValue{Name: &s.Name}
+ case len(s.Data) == 0:
+ return ErrSecretInvalidValue{Data: &s.Data}
+ case !secretNameReg.MatchString(s.Name) ||
+ forbiddenSecretPrefixReg.MatchString(s.Name):
+ return ErrSecretInvalidValue{Name: &s.Name}
+ default:
+ return nil
+ }
+}
+
+type FindSecretsOptions struct {
+ db.ListOptions
+ OwnerID int64
+ RepoID int64
+}
+
+func (opts *FindSecretsOptions) toConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.OwnerID > 0 {
+ cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
+ }
+ if opts.RepoID > 0 {
+ cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
+ }
+
+ return cond
+}
+
+func FindSecrets(ctx context.Context, opts FindSecretsOptions) ([]*Secret, error) {
+ var secrets []*Secret
+ sess := db.GetEngine(ctx)
+ if opts.PageSize != 0 {
+ sess = db.SetSessionPagination(sess, &opts.ListOptions)
+ }
+ return secrets, sess.
+ Where(opts.toConds()).
+ Find(&secrets)
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 70f982b8dc..dd31562c3a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3212,3 +3212,19 @@ owner.settings.cleanuprules.remove.days = Remove versions older than
owner.settings.cleanuprules.remove.pattern = Remove versions matching
owner.settings.cleanuprules.success.update = Cleanup rule has been updated.
owner.settings.cleanuprules.success.delete = Cleanup rule has been deleted.
+
+[secrets]
+secrets = Secrets
+description = Secrets will be passed to certain actions and cannot be read otherwise.
+none = There are no secrets yet.
+value = Value
+name = Name
+creation = Add Secret
+creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_
+creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted.
+creation.success = The secret '%s' has been added.
+creation.failed = Failed to add secret.
+deletion = Remove secret
+deletion.description = Removing a secret will revoke its access to repositories. Continue?
+deletion.success = The secret has been removed.
+deletion.failed = Failed to remove secret.
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 899e554ba0..e625962f75 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
+ secret_model "code.gitea.io/gitea/models/secret"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/base"
@@ -37,6 +38,8 @@ const (
tplSettingsHooks base.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
tplSettingsLabels base.TplName = "org/settings/labels"
+ // tplSettingsSecrets template path for render secrets settings
+ tplSettingsSecrets base.TplName = "org/settings/secrets"
)
// Settings render the main settings page
@@ -246,3 +249,51 @@ func Labels(ctx *context.Context) {
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
+
+// Secrets render organization secrets page
+func Secrets(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("repo.secrets")
+ ctx.Data["PageIsOrgSettings"] = true
+ ctx.Data["PageIsOrgSettingsSecrets"] = true
+
+ secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: ctx.Org.Organization.ID})
+ if err != nil {
+ ctx.ServerError("FindSecrets", err)
+ return
+ }
+ ctx.Data["Secrets"] = secrets
+
+ ctx.HTML(http.StatusOK, tplSettingsSecrets)
+}
+
+// SecretsPost add secrets
+func SecretsPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.AddSecretForm)
+
+ _, err := secret_model.InsertEncryptedSecret(ctx, ctx.Org.Organization.ID, 0, form.Title, form.Content)
+ if err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
+ log.Error("validate secret: %v", err)
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets")
+ return
+ }
+
+ log.Trace("Org %d: secret added", ctx.Org.Organization.ID)
+ ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title))
+ ctx.Redirect(ctx.Org.OrgLink + "/settings/secrets")
+}
+
+// SecretsDelete delete secrets
+func SecretsDelete(ctx *context.Context) {
+ id := ctx.FormInt64("id")
+ if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
+ log.Error("delete secret %d: %v", id, err)
+ } else {
+ ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Org.OrgLink + "/settings/secrets",
+ })
+}
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index f35adcaa10..913ed6c7cb 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -19,6 +19,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
+ secret_model "code.gitea.io/gitea/models/secret"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@@ -1113,12 +1114,37 @@ func DeployKeys(ctx *context.Context) {
}
ctx.Data["Deploykeys"] = keys
+ secrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: ctx.Repo.Repository.ID})
+ if err != nil {
+ ctx.ServerError("FindSecrets", err)
+ return
+ }
+ ctx.Data["Secrets"] = secrets
+
ctx.HTML(http.StatusOK, tplDeployKeys)
}
+// SecretsPost response for creating a new secret
+func SecretsPost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.AddSecretForm)
+
+ _, err := secret_model.InsertEncryptedSecret(ctx, 0, ctx.Repo.Repository.ID, form.Title, form.Content)
+ if err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.creation.failed"))
+ log.Error("validate secret: %v", err)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+ return
+ }
+
+ log.Trace("Secret added: %d", ctx.Repo.Repository.ID)
+ ctx.Flash.Success(ctx.Tr("secrets.creation.success", form.Title))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
+}
+
// DeployKeysPost response for adding a deploy key of a repository
func DeployKeysPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AddKeyForm)
+
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
ctx.Data["PageIsSettingsKeys"] = true
ctx.Data["DisableSSH"] = setting.SSH.Disabled
@@ -1177,6 +1203,20 @@ func DeployKeysPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys")
}
+func DeleteSecret(ctx *context.Context) {
+ id := ctx.FormInt64("id")
+ if _, err := db.DeleteByBean(ctx, &secret_model.Secret{ID: id}); err != nil {
+ ctx.Flash.Error(ctx.Tr("secrets.deletion.failed"))
+ log.Error("delete secret %d: %v", id, err)
+ } else {
+ ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "redirect": ctx.Repo.RepoLink + "/settings/keys",
+ })
+}
+
// DeleteDeployKey response for deleting a deploy key
func DeleteDeployKey(ctx *context.Context) {
if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil {
diff --git a/routers/web/web.go b/routers/web/web.go
index f9d97758a1..20d067a163 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -774,6 +774,12 @@ func RegisterRoutes(m *web.Route) {
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), org.InitializeLabels)
})
+ m.Group("/secrets", func() {
+ m.Get("", org.Secrets)
+ m.Post("", web.Bind(forms.AddSecretForm{}), org.SecretsPost)
+ m.Post("/delete", org.SecretsDelete)
+ })
+
m.Route("/delete", "GET,POST", org.SettingsDelete)
m.Group("/packages", func() {
@@ -912,6 +918,10 @@ func RegisterRoutes(m *web.Route) {
m.Combo("").Get(repo.DeployKeys).
Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost)
m.Post("/delete", repo.DeleteDeployKey)
+ m.Group("/secrets", func() {
+ m.Post("", web.Bind(forms.AddSecretForm{}), repo.SecretsPost)
+ m.Post("/delete", repo.DeleteSecret)
+ })
})
m.Group("/lfs", func() {
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index cd2c45261b..bbea58310a 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -363,6 +363,18 @@ 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 {
+ Title string `binding:"Required;MaxSize(50)"`
+ Content string `binding:"Required"`
+}
+
+// Validate validates the fields
+func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
+ ctx := context.GetContext(req)
+ return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
+}
+
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
Name string `binding:"Required;MaxSize(255)"`
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index 9ff30ae4ff..765bb6aaae 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -12,6 +12,9 @@
<a class="{{if .PageIsOrgSettingsLabels}}active {{end}}item" href="{{.OrgLink}}/settings/labels">
{{.locale.Tr "repo.labels"}}
</a>
+ <a class="{{if .PageIsOrgSettingsSecrets}}active {{end}}item" href="{{.OrgLink}}/settings/secrets">
+ {{.locale.Tr "secrets.secrets"}}
+ </a>
{{if .EnableOAuth2}}
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{.OrgLink}}/settings/applications">
{{.locale.Tr "settings.applications"}}
diff --git a/templates/org/settings/secrets.tmpl b/templates/org/settings/secrets.tmpl
new file mode 100644
index 0000000000..dd2a437b75
--- /dev/null
+++ b/templates/org/settings/secrets.tmpl
@@ -0,0 +1,83 @@
+{{template "base/head" .}}
+<div class="page-content organization settings webhooks">
+ {{template "org/header" .}}
+ <div class="ui container">
+ <div class="ui grid">
+ {{template "org/settings/navbar" .}}
+ <div class="ui twelve wide column content">
+ {{template "base/alert" .}}
+ <h4 class="ui top attached header">
+ {{.locale.Tr "secrets.secrets"}}
+ <div class="ui right">
+ <div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "secrets.creation"}}</div>
+ </div>
+ </h4>
+ <div class="ui attached segment">
+ <div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel">
+ <form class="ui form" action="{{.Link}}" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="field">
+ {{.locale.Tr "secrets.description"}}
+ </div>
+ <div class="field{{if .Err_Title}} error{{end}}">
+ <label for="secret-title">{{.locale.Tr "secrets.name"}}</label>
+ <input id="secret-title" name="title" value="{{.title}}" autofocus required pattern="^[a-zA-Z_][a-zA-Z0-9_]*$" placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}">
+ </div>
+ <div class="field{{if .Err_Content}} error{{end}}">
+ <label for="secret-content">{{.locale.Tr "secrets.value"}}</label>
+ <textarea id="secret-content" name="content" required placeholder="{{.locale.Tr "secrets.creation.value_placeholder"}}">{{.content}}</textarea>
+ </div>
+ <button class="ui green button">
+ {{.locale.Tr "secrets.creation"}}
+ </button>
+ <button class="ui hide-panel button" data-panel="#add-secret-panel">
+ {{.locale.Tr "cancel"}}
+ </button>
+ </form>
+ </div>
+ {{if .Secrets}}
+ <div class="ui key list">
+ {{range .Secrets}}
+ <div class="item">
+ <div class="right floated content">
+ <button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
+ {{$.locale.Tr "settings.delete_key"}}
+ </button>
+ </div>
+ <div class="left floated content">
+ <i>{{svg "octicon-key" 32}}</i>
+ </div>
+ <div class="content">
+ <strong>{{.Name}}</strong>
+ <div class="print meta">******</div>
+ <div class="activity meta">
+ <i>
+ {{$.locale.Tr "settings.add_on"}}
+ <span>{{.CreatedUnix.FormatShort}}</span>
+ </i>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ </div>
+ {{else}}
+ {{.locale.Tr "secrets.none"}}
+ {{end}}
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<div class="ui small basic delete modal">
+ <div class="ui header">
+ {{svg "octicon-trash" 16 "mr-2"}}
+ {{.locale.Tr "secrets.deletion"}}
+ </div>
+ <div class="content">
+ <p>{{.locale.Tr "secrets.deletion.description"}}</p>
+ </div>
+ {{template "base/delete_modal_actions" .}}
+</div>
+
+{{template "base/footer" .}}
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index 44c916eefb..31d1c1f7ab 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -75,6 +75,8 @@
{{end}}
</div>
</div>
+ <br/>
+ {{template "repo/settings/secrets" .}}
</div>
<div class="ui small basic delete modal">
diff --git a/templates/repo/settings/nav.tmpl b/templates/repo/settings/nav.tmpl
index 6239b04ed4..3c00c5e188 100644
--- a/templates/repo/settings/nav.tmpl
+++ b/templates/repo/settings/nav.tmpl
@@ -12,7 +12,7 @@
{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}}
<li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.locale.Tr "repo.settings.githooks"}}</a></li>
{{end}}
- <li {{if .PageIsSettingsKeys}}class="current"{{end}}><a href="{{.RepoLink}}/settings/keys">{{.locale.Tr "repo.settings.deploy_keys"}}</a></li>
+ <li {{if .PageIsSettingsKeys}}class="current"{{end}}><a href="{{.RepoLink}}/settings/keys">{{.locale.Tr "secrets.secrets"}}</a></li>
</ul>
</div>
</div>
diff --git a/templates/repo/settings/navbar.tmpl b/templates/repo/settings/navbar.tmpl
index e2b741b8d0..236a82f348 100644
--- a/templates/repo/settings/navbar.tmpl
+++ b/templates/repo/settings/navbar.tmpl
@@ -25,7 +25,7 @@
</a>
{{end}}
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{.RepoLink}}/settings/keys">
- {{.locale.Tr "repo.settings.deploy_keys"}}
+ {{.locale.Tr "secrets.secrets"}}
</a>
{{if .LFSStartServer}}
<a class="{{if .PageIsSettingsLFS}}active {{end}}item" href="{{.RepoLink}}/settings/lfs">
diff --git a/templates/repo/settings/secrets.tmpl b/templates/repo/settings/secrets.tmpl
new file mode 100644
index 0000000000..6fb97beb4a
--- /dev/null
+++ b/templates/repo/settings/secrets.tmpl
@@ -0,0 +1,60 @@
+<div class="ui container">
+ <h4 class="ui top attached header">
+ {{.locale.Tr "secrets.secrets"}}
+ <div class="ui right">
+ <div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "secrets.creation"}}</div>
+ </div>
+ </h4>
+ <div class="ui attached segment">
+ <div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel">
+ <form class="ui form" action="{{.Link}}/secrets" method="post">
+ {{.CsrfTokenHtml}}
+ <div class="field">
+ {{.locale.Tr "secrets.description"}}
+ </div>
+ <div class="field{{if .Err_Title}} error{{end}}">
+ <label for="secret-title">{{.locale.Tr "secrets.name"}}</label>
+ <input id="secret-title" name="title" value="{{.title}}" autofocus required pattern="^[a-zA-Z_][a-zA-Z0-9_]*$" placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}">
+ </div>
+ <div class="field{{if .Err_Content}} error{{end}}">
+ <label for="secret-content">{{.locale.Tr "secrets.value"}}</label>
+ <textarea id="secret-content" name="content" required placeholder="{{.locale.Tr "secrets.creation.value_placeholder"}}">{{.content}}</textarea>
+ </div>
+ <button class="ui green button">
+ {{.locale.Tr "secrets.creation"}}
+ </button>
+ <button class="ui hide-panel button" data-panel="#add-secret-panel">
+ {{.locale.Tr "cancel"}}
+ </button>
+ </form>
+ </div>
+ {{if .Secrets}}
+ <div class="ui key list">
+ {{range .Secrets}}
+ <div class="item">
+ <div class="right floated content">
+ <button class="ui red tiny button delete-button" data-url="{{$.Link}}/secrets/delete" data-id="{{.ID}}">
+ {{$.locale.Tr "settings.delete_key"}}
+ </button>
+ </div>
+ <div class="left floated content">
+ <i>{{svg "octicon-key" 32}}</i>
+ </div>
+ <div class="content">
+ <strong>{{.Name}}</strong>
+ <div class="print meta">******</div>
+ <div class="activity meta">
+ <i>
+ {{$.locale.Tr "settings.add_on"}}
+ <span>{{.CreatedUnix.FormatShort}}</span>
+ </i>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ </div>
+ {{else}}
+ {{.locale.Tr "secrets.none"}}
+ {{end}}
+ </div>
+</div>