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>tags/v1.19.0-rc0
--- | |||||
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. |
NewMigration("Add package cleanup rule table", v1_19.CreatePackageCleanupRuleTable), | NewMigration("Add package cleanup rule table", v1_19.CreatePackageCleanupRuleTable), | ||||
// v235 -> v236 | // v235 -> v236 | ||||
NewMigration("Add index for access_token", v1_19.AddIndexForAccessToken), | NewMigration("Add index for access_token", v1_19.AddIndexForAccessToken), | ||||
// v236 -> v237 | |||||
NewMigration("Create secrets table", v1_19.CreateSecretsTable), | |||||
} | } | ||||
// GetCurrentDBVersion returns the current db version | // GetCurrentDBVersion returns the current db version |
// 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)) | |||||
} |
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
"code.gitea.io/gitea/models/perm" | "code.gitea.io/gitea/models/perm" | ||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
secret_model "code.gitea.io/gitea/models/secret" | |||||
"code.gitea.io/gitea/models/unit" | "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
"code.gitea.io/gitea/modules/log" | "code.gitea.io/gitea/modules/log" | ||||
&TeamUser{OrgID: org.ID}, | &TeamUser{OrgID: org.ID}, | ||||
&TeamUnit{OrgID: org.ID}, | &TeamUnit{OrgID: org.ID}, | ||||
&TeamInvite{OrgID: org.ID}, | &TeamInvite{OrgID: org.ID}, | ||||
&secret_model.Secret{OwnerID: org.ID}, | |||||
); err != nil { | ); err != nil { | ||||
return fmt.Errorf("DeleteBeans: %w", err) | return fmt.Errorf("DeleteBeans: %w", err) | ||||
} | } |
access_model "code.gitea.io/gitea/models/perm/access" | access_model "code.gitea.io/gitea/models/perm/access" | ||||
project_model "code.gitea.io/gitea/models/project" | project_model "code.gitea.io/gitea/models/project" | ||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
secret_model "code.gitea.io/gitea/models/secret" | |||||
system_model "code.gitea.io/gitea/models/system" | system_model "code.gitea.io/gitea/models/system" | ||||
"code.gitea.io/gitea/models/unit" | "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
&admin_model.Task{RepoID: repoID}, | &admin_model.Task{RepoID: repoID}, | ||||
&repo_model.Watch{RepoID: repoID}, | &repo_model.Watch{RepoID: repoID}, | ||||
&webhook.Webhook{RepoID: repoID}, | &webhook.Webhook{RepoID: repoID}, | ||||
&secret_model.Secret{RepoID: repoID}, | |||||
); err != nil { | ); err != nil { | ||||
return fmt.Errorf("deleteBeans: %w", err) | return fmt.Errorf("deleteBeans: %w", err) | ||||
} | } |
// 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) | |||||
} |
owner.settings.cleanuprules.remove.pattern = Remove versions matching | owner.settings.cleanuprules.remove.pattern = Remove versions matching | ||||
owner.settings.cleanuprules.success.update = Cleanup rule has been updated. | owner.settings.cleanuprules.success.update = Cleanup rule has been updated. | ||||
owner.settings.cleanuprules.success.delete = Cleanup rule has been deleted. | 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. |
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/models/db" | "code.gitea.io/gitea/models/db" | ||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
secret_model "code.gitea.io/gitea/models/secret" | |||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
"code.gitea.io/gitea/models/webhook" | "code.gitea.io/gitea/models/webhook" | ||||
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
tplSettingsHooks base.TplName = "org/settings/hooks" | tplSettingsHooks base.TplName = "org/settings/hooks" | ||||
// tplSettingsLabels template path for render labels settings | // tplSettingsLabels template path for render labels settings | ||||
tplSettingsLabels base.TplName = "org/settings/labels" | 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 | // Settings render the main settings page | ||||
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates | ctx.Data["LabelTemplates"] = repo_module.LabelTemplates | ||||
ctx.HTML(http.StatusOK, tplSettingsLabels) | 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", | |||||
}) | |||||
} |
"code.gitea.io/gitea/models/organization" | "code.gitea.io/gitea/models/organization" | ||||
"code.gitea.io/gitea/models/perm" | "code.gitea.io/gitea/models/perm" | ||||
repo_model "code.gitea.io/gitea/models/repo" | repo_model "code.gitea.io/gitea/models/repo" | ||||
secret_model "code.gitea.io/gitea/models/secret" | |||||
unit_model "code.gitea.io/gitea/models/unit" | unit_model "code.gitea.io/gitea/models/unit" | ||||
user_model "code.gitea.io/gitea/models/user" | user_model "code.gitea.io/gitea/models/user" | ||||
"code.gitea.io/gitea/modules/base" | "code.gitea.io/gitea/modules/base" | ||||
} | } | ||||
ctx.Data["Deploykeys"] = keys | 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) | 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 | // DeployKeysPost response for adding a deploy key of a repository | ||||
func DeployKeysPost(ctx *context.Context) { | func DeployKeysPost(ctx *context.Context) { | ||||
form := web.GetForm(ctx).(*forms.AddKeyForm) | form := web.GetForm(ctx).(*forms.AddKeyForm) | ||||
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") | ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") | ||||
ctx.Data["PageIsSettingsKeys"] = true | ctx.Data["PageIsSettingsKeys"] = true | ||||
ctx.Data["DisableSSH"] = setting.SSH.Disabled | ctx.Data["DisableSSH"] = setting.SSH.Disabled | ||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/keys") | 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 | // DeleteDeployKey response for deleting a deploy key | ||||
func DeleteDeployKey(ctx *context.Context) { | func DeleteDeployKey(ctx *context.Context) { | ||||
if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil { | if err := asymkey_service.DeleteDeployKey(ctx.Doer, ctx.FormInt64("id")); err != nil { |
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), org.InitializeLabels) | 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.Route("/delete", "GET,POST", org.SettingsDelete) | ||||
m.Group("/packages", func() { | m.Group("/packages", func() { | ||||
m.Combo("").Get(repo.DeployKeys). | m.Combo("").Get(repo.DeployKeys). | ||||
Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost) | Post(web.Bind(forms.AddKeyForm{}), repo.DeployKeysPost) | ||||
m.Post("/delete", repo.DeleteDeployKey) | 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() { | m.Group("/lfs", func() { |
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) | 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 | // NewAccessTokenForm form for creating access token | ||||
type NewAccessTokenForm struct { | type NewAccessTokenForm struct { | ||||
Name string `binding:"Required;MaxSize(255)"` | Name string `binding:"Required;MaxSize(255)"` |
<a class="{{if .PageIsOrgSettingsLabels}}active {{end}}item" href="{{.OrgLink}}/settings/labels"> | <a class="{{if .PageIsOrgSettingsLabels}}active {{end}}item" href="{{.OrgLink}}/settings/labels"> | ||||
{{.locale.Tr "repo.labels"}} | {{.locale.Tr "repo.labels"}} | ||||
</a> | </a> | ||||
<a class="{{if .PageIsOrgSettingsSecrets}}active {{end}}item" href="{{.OrgLink}}/settings/secrets"> | |||||
{{.locale.Tr "secrets.secrets"}} | |||||
</a> | |||||
{{if .EnableOAuth2}} | {{if .EnableOAuth2}} | ||||
<a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{.OrgLink}}/settings/applications"> | <a class="{{if .PageIsSettingsApplications}}active {{end}}item" href="{{.OrgLink}}/settings/applications"> | ||||
{{.locale.Tr "settings.applications"}} | {{.locale.Tr "settings.applications"}} |
{{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" .}} |
{{end}} | {{end}} | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<br/> | |||||
{{template "repo/settings/secrets" .}} | |||||
</div> | </div> | ||||
<div class="ui small basic delete modal"> | <div class="ui small basic delete modal"> |
{{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}} | {{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> | <li {{if .PageIsSettingsGitHooks}}class="current"{{end}}><a href="{{.RepoLink}}/settings/hooks/git">{{.locale.Tr "repo.settings.githooks"}}</a></li> | ||||
{{end}} | {{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> | </ul> | ||||
</div> | </div> | ||||
</div> | </div> |
</a> | </a> | ||||
{{end}} | {{end}} | ||||
<a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{.RepoLink}}/settings/keys"> | <a class="{{if .PageIsSettingsKeys}}active {{end}}item" href="{{.RepoLink}}/settings/keys"> | ||||
{{.locale.Tr "repo.settings.deploy_keys"}} | |||||
{{.locale.Tr "secrets.secrets"}} | |||||
</a> | </a> | ||||
{{if .LFSStartServer}} | {{if .LFSStartServer}} | ||||
<a class="{{if .PageIsSettingsLFS}}active {{end}}item" href="{{.RepoLink}}/settings/lfs"> | <a class="{{if .PageIsSettingsLFS}}active {{end}}item" href="{{.RepoLink}}/settings/lfs"> |
<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> |