mirror of https://github.com/go-gitea/gitea
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>pull/22187/head
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