mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00: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:  Rrg level secrets  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…
	
	Add table
		Add a link
		
	
		Reference in a new issue