mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-30 21:26:17 +01:00
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>
This commit is contained in:
parent
40ba750c4b
commit
659055138b
17 changed files with 468 additions and 2 deletions
36
docs/content/doc/secrets/overview.en-us.md
Normal file
36
docs/content/doc/secrets/overview.en-us.md
Normal file
|
@ -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.
|
|
@ -442,6 +442,8 @@ var migrations = []Migration{
|
||||||
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
|
||||||
|
|
23
models/migrations/v1_19/v236.go
Normal file
23
models/migrations/v1_19/v236.go
Normal file
|
@ -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))
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"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"
|
||||||
|
@ -370,6 +371,7 @@ func DeleteOrganization(ctx context.Context, org *Organization) error {
|
||||||
&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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
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"
|
||||||
|
@ -150,6 +151,7 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
|
||||||
&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)
|
||||||
}
|
}
|
||||||
|
|
124
models/secret/secret.go
Normal file
124
models/secret/secret.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -3212,3 +3212,19 @@ owner.settings.cleanuprules.remove.days = Remove versions older than
|
||||||
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.
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"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"
|
||||||
|
@ -37,6 +38,8 @@ const (
|
||||||
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
|
||||||
|
@ -246,3 +249,51 @@ func Labels(ctx *context.Context) {
|
||||||
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"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"
|
||||||
|
@ -1113,12 +1114,37 @@ func DeployKeys(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
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
|
||||||
|
@ -1177,6 +1203,20 @@ func DeployKeysPost(ctx *context.Context) {
|
||||||
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 {
|
||||||
|
|
|
@ -774,6 +774,12 @@ func RegisterRoutes(m *web.Route) {
|
||||||
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() {
|
||||||
|
@ -912,6 +918,10 @@ func RegisterRoutes(m *web.Route) {
|
||||||
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() {
|
||||||
|
|
|
@ -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)
|
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)"`
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
<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"}}
|
||||||
|
|
83
templates/org/settings/secrets.tmpl
Normal file
83
templates/org/settings/secrets.tmpl
Normal file
|
@ -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" .}}
|
|
@ -75,6 +75,8 @@
|
||||||
{{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">
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
{{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>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</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">
|
||||||
|
|
60
templates/repo/settings/secrets.tmpl
Normal file
60
templates/repo/settings/secrets.tmpl
Normal file
|
@ -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>
|
Loading…
Reference in a new issue