-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Add Gitea secrets storage and management #14483
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b24ada
47c472a
64a91b4
9f2d204
368c97e
d39f7c4
9465c13
eb2b139
f4df973
abde871
b1e8915
a077ee2
5b1044a
834c7c1
8e1291d
1788de5
0a3be05
a0ebf78
319da15
b08114b
073656c
f89bd80
850d936
41310a7
e54785a
ccb57c8
e30f532
29e4f6b
b01b2a9
a1aee64
4c8f590
bc999bd
41e9be0
2c7ae0c
dd84d07
f5effc1
c08fc15
44ca6bf
6fcb7bf
b79b156
acc0c12
c754525
eb5bcec
3183368
e86e30f
e6cee41
641d37a
7c82f7a
f9d58d4
aa10928
f738069
5103f1d
a23241f
9f8fdaa
b32bb7a
23dd7a7
a8c192d
f1ef5ae
4a2676e
5aa55fe
d1a729b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package auth | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
"code.gitea.io/gitea/modules/util" | ||
) | ||
|
||
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 | ||
} | ||
|
||
var nameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9-_.]*$") | ||
|
||
// Secret represents a secret | ||
type Secret struct { | ||
ID int64 | ||
OwnerID int64 `xorm:"UNIQUE(owner_repo_name) NOTNULL"` | ||
RepoID int64 `xorm:"UNIQUE(owner_repo_name) NOTNULL"` | ||
Comment on lines
+39
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should not remove the indexes. They can still be valuable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solved in the new PR. |
||
Name string `xorm:"UNIQUE(owner_repo_name) NOTNULL"` | ||
Data string `xorm:"LONGTEXT"` // encrypted data, or plaintext data if there's no master key | ||
CreatedUnix timeutil.TimeStamp `xorm:"created NOTNULL"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also store There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so, we can only add or remove a secret, so there's no "updated time". |
||
} | ||
|
||
func init() { | ||
db.RegisterModel(new(Secret)) | ||
} | ||
|
||
// Validate validates the required fields and formats. | ||
func (s *Secret) Validate() error { | ||
switch { | ||
case len(s.Name) == 0: | ||
return ErrSecretInvalidValue{Name: &s.Name} | ||
case len(s.Data) == 0: | ||
return ErrSecretInvalidValue{Data: &s.Data} | ||
case nameRE.MatchString(s.Name): | ||
return ErrSecretInvalidValue{Name: &s.Name} | ||
default: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and given my suggestion above, the case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? If it's a repo secret, the OwnerID(aka UserID) should be 0. So the secret has nothing to do with the repo's owner, and works well with repo transferring. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, since we don't need to change our assumptions anymore, that's not needed. |
||
return nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should also be updated when you update the data model as requested above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solved. |
||
ID int64 | ||
OwnerID int64 `xorm:"UNIQUE(owner_repo_name) NOTNULL"` | ||
RepoID int64 `xorm:"UNIQUE(owner_repo_name) NOTNULL"` | ||
Name string `xorm:"UNIQUE(owner_repo_name) NOTNULL"` | ||
Data string `xorm:"LONGTEXT"` | ||
CreatedUnix timeutil.TimeStamp `xorm:"created NOTNULL"` | ||
} | ||
|
||
return x.Sync(new(Secret)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
package setting | ||
|
||
import ( | ||
"crypto/sha1" | ||
"encoding/base64" | ||
"fmt" | ||
"math" | ||
|
@@ -27,6 +28,7 @@ import ( | |
"code.gitea.io/gitea/modules/user" | ||
"code.gitea.io/gitea/modules/util" | ||
|
||
"golang.org/x/crypto/pbkdf2" | ||
gossh "golang.org/x/crypto/ssh" | ||
ini "gopkg.in/ini.v1" | ||
) | ||
|
@@ -214,6 +216,8 @@ var ( | |
HMACKey string `ini:"HMAC_KEY"` | ||
Allways bool | ||
}{} | ||
MasterKeyProvider string | ||
MasterKey []byte | ||
Comment on lines
+219
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those two config options are currently missing from |
||
|
||
// UI settings | ||
UI = struct { | ||
|
@@ -973,6 +977,19 @@ func loadFromConf(allowEmpty bool, extraConfig string) { | |
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) | ||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) | ||
|
||
// Master key provider configuration | ||
MasterKeyProvider = sec.Key("MASTER_KEY_PROVIDER").MustString("plain") | ||
switch MasterKeyProvider { | ||
case "plain": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why plain? |
||
tempSalt := []byte{'g', 'i', 't', 'e', 'a'} | ||
MasterKey = []byte(sec.Key("MASTER_KEY").MustString(SecretKey)) | ||
MasterKey = pbkdf2.Key(MasterKey, tempSalt, 4096, 32, sha1.New) | ||
case "none": | ||
default: | ||
log.Fatal("invalid master key provider type: %v", MasterKeyProvider) | ||
return | ||
} | ||
|
||
InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN") | ||
if InstallLock && InternalToken == "" { | ||
// if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,7 @@ import ( | |
"code.gitea.io/gitea/modules/timeutil" | ||
"code.gitea.io/gitea/modules/util" | ||
"code.gitea.io/gitea/services/gitdiff" | ||
secret_service "code.gitea.io/gitea/services/secrets" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Admit it: You wanted to name it that way :) |
||
|
||
"github.com/editorconfig/editorconfig-core-go/v2" | ||
) | ||
|
@@ -459,6 +460,13 @@ func NewFuncMap() []template.FuncMap { | |
return items | ||
}, | ||
"HasPrefix": strings.HasPrefix, | ||
"Shadow": func(s string) string { | ||
return "******" | ||
}, | ||
"DecryptSecret": func(s string) string { | ||
v, _ := secret_service.DecryptString(s) | ||
return v | ||
}, | ||
"CompareLink": func(baseRepo, repo *repo_model.Repository, branchName string) string { | ||
var curBranch string | ||
if repo.ID != baseRepo.ID { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -185,6 +185,12 @@ app_url_helper = Base address for HTTP(S) clone URLs and email notifications. | |||||
log_root_path = Log Path | ||||||
log_root_path_helper = Log files will be written to this directory. | ||||||
|
||||||
security_title = Security Settings | ||||||
master_key_provider = Master Key Provider | ||||||
master_key_provider_none = None | ||||||
master_key_provider_plain = Plain | ||||||
master_key_provider_helper = Master Key Provider to use to store secret key that will be used for other secret encryption. Use "None" to not encrypt secrets. Use "Plain" to store automatically generated secret in configuration file. | ||||||
|
||||||
optional_title = Optional Settings | ||||||
email_title = Email Settings | ||||||
smtp_addr = SMTP Host | ||||||
|
@@ -243,6 +249,7 @@ no_reply_address = Hidden Email Domain | |||||
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'. | ||||||
password_algorithm = Password Hash Algorithm | ||||||
password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. `argon2` whilst having good characteristics uses a lot of memory and may be inappropriate for small systems. | ||||||
master_key_failed = Failed to generate master key: %v | ||||||
enable_update_checker = Enable Update Checker | ||||||
enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. | ||||||
|
||||||
|
@@ -466,6 +473,7 @@ max_size_error = ` must contain at most %s characters.` | |||||
email_error = ` is not a valid email address.` | ||||||
url_error = `'%s' is not a valid URL.` | ||||||
include_error = ` must contain substring '%s'.` | ||||||
in_error = ` can only contain specific values: %s.` | ||||||
glob_pattern_error = ` glob pattern is invalid: %s.` | ||||||
regex_pattern_error = ` regex pattern is invalid: %s.` | ||||||
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` | ||||||
|
@@ -2059,6 +2067,18 @@ settings.deploy_key_desc = Deploy keys have read-only pull access to the reposit | |||||
settings.is_writable = Enable Write Access | ||||||
settings.is_writable_info = Allow this deploy key to <strong>push</strong> to the repository. | ||||||
settings.no_deploy_keys = There are no deploy keys yet. | ||||||
settings.secrets = Secrets | ||||||
settings.add_secret = Add Secret | ||||||
settings.add_secret_success = The secret '%s' has been added. | ||||||
settings.secret_value_content_placeholder = Input any content | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't that placeholder redundant? Speaking of which: Do we strip surrounding whitespace already? I hope so as that is in general much more user-friendly… There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming my suggestion above is implemented:
Suggested change
Then suddenly this placeholder isn't redundant anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solved in the new PR. |
||||||
settings.secret_desc = Secrets will be passed to certain actions and cannot be read otherwise. | ||||||
settings.secret_content = Value | ||||||
settings.secret_name = Name | ||||||
settings.no_secret = There are no secrets yet. | ||||||
settings.secret_deletion = Remove secret | ||||||
settings.secret_deletion_desc = Removing a secret will revoke its access to this repository. Continue? | ||||||
settings.secret_deletion_success = The secret has been removed. | ||||||
settings.secret_deletion_failed = Failed to remove secret. | ||||||
settings.title = Title | ||||||
settings.deploy_key_content = Content | ||||||
settings.key_been_used = A deploy key with identical content is already in use. | ||||||
|
@@ -2377,6 +2397,7 @@ settings.update_setting_success = Organization settings have been updated. | |||||
settings.change_orgname_prompt = Note: changing the organization name also changes the organization's URL. | ||||||
settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. | ||||||
settings.update_avatar_success = The organization's avatar has been updated. | ||||||
settings.secrets = Secrets | ||||||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
settings.delete = Delete Organization | ||||||
settings.delete_account = Delete This Organization | ||||||
settings.delete_prompt = The organization will be permanently removed. This <strong>CANNOT</strong> be undone! | ||||||
|
Uh oh!
There was an error while loading. Please reload this page.