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>release
parent
40ba750c4b
commit
659055138b
@ -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.
|
@ -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))
|
||||
}
|
@ -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)
|
||||
}
|
@ -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" .}}
|
@ -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 New Issue