Skip to content
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

Add instance-level secrets #27725

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions models/secret/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ func init() {
}

func (s *Secret) Validate() error {
if s.OwnerID == 0 && s.RepoID == 0 {
return errors.New("the secret is not bound to any scope")
if s.OwnerID != 0 && s.RepoID != 0 {
return errors.New("a secret should not be bound to an owner and a repository at the same time")
}
return nil
}
Expand All @@ -80,12 +80,8 @@ type FindSecretsOptions struct {

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})
}
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can add some comments on these fields of FindSecretsOptions

cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
if opts.SecretID != 0 {
cond = cond.And(builder.Eq{"id": opts.SecretID})
}
Expand Down
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3495,6 +3495,7 @@ deletion.description = Removing a secret is permanent and cannot be undone. Cont
deletion.success = The secret has been removed.
deletion.failed = Failed to remove secret.
management = Secrets Management
instance_desc = Although secrets will be masked if users try to print them in Actions workflows, this is not absolutely secure. Users can still obtain the contents of secrets by writing malicious workflows, so please ensure that global secrets are not used by people you do not trust. Otherwise, please use organization/user-level or repository-level secrets to limit their scope of use. Alternatively, if it's acceptable to expose their contents, please use global variables.

[actions]
actions = Actions
Expand Down
12 changes: 9 additions & 3 deletions routers/api/actions/runner/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,24 @@ func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[s
return secrets
}

ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
globalSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: 0, RepoID: 0})
if err != nil {
log.Error("find global secrets: %v", err)
// go on
}
ownerSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID, RepoID: 0})
if err != nil {
log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
// go on
}
repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID})
repoSecrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: 0, RepoID: task.Job.Run.RepoID})
if err != nil {
log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
// go on
}

for _, secret := range append(ownerSecrets, repoSecrets...) {
// Level precedence: Repo > Org / User > Global
for _, secret := range append(globalSecrets, append(ownerSecrets, repoSecrets...)...) {
if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil {
log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
// go on
Expand Down
103 changes: 103 additions & 0 deletions routers/api/v1/admin/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package admin

import (
"errors"
"net/http"

"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
secret_service "code.gitea.io/gitea/services/secrets"
)

// CreateOrUpdateSecret create or update one secret in instance scope
func CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /admin/actions/secrets/{secretname} admin updateAdminSecret
// ---
// summary: Create or Update a secret value in instance scope
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: secret created
// "204":
// description: secret updated
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"

opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)

_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, 0, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
}
return
}

if created {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}

// DeleteSecret delete one secret in instance scope
func DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /admin/actions/secrets/{secretname} admin deleteAdminSecret
// ---
// summary: Delete a secret in instance scope
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// responses:
// "204":
// description: secret deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"

err := secret_service.DeleteSecretByName(ctx, 0, 0, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
}
return
}

ctx.Status(http.StatusNoContent)
}
6 changes: 5 additions & 1 deletion routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,6 @@ func Routes() *web.Route {
Post(bind(api.CreateEmailOption{}), user.AddEmail).
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)

// manage user-level actions features
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Combo("/{secretname}").
Expand Down Expand Up @@ -1499,6 +1498,11 @@ func Routes() *web.Route {
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership())

m.Group("/admin", func() {
m.Group("/actions/secrets", func() {
m.Combo("/{secretname}").
Put(bind(api.CreateOrUpdateSecretOption{}), admin.CreateOrUpdateSecret).
Delete(admin.DeleteSecret)
})
m.Group("/cron", func() {
m.Get("", admin.ListCronTasks)
m.Post("/{task}", admin.PostCronTask)
Expand Down
10 changes: 5 additions & 5 deletions routers/api/v1/org/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func ListActionsSecrets(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiSecrets)
}

// create or update one secret of the organization
// CreateOrUpdateSecret create or update one secret in an organization
func CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
// ---
Expand All @@ -93,9 +93,9 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// description: secret created
// "204":
// description: response when updating a secret
// description: secret updated
// "400":
// "$ref": "#/responses/error"
// "404":
Expand All @@ -122,7 +122,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
}
}

// DeleteSecret delete one secret of the organization
// DeleteSecret delete one secret in an organization
func DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
// ---
Expand All @@ -144,7 +144,7 @@ func DeleteSecret(ctx *context.APIContext) {
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// description: secret deleted
// "400":
// "$ref": "#/responses/error"
// "404":
Expand Down
16 changes: 7 additions & 9 deletions routers/api/v1/repo/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
secret_service "code.gitea.io/gitea/services/secrets"
)

// create or update one secret of the repository
// CreateOrUpdateSecret create or update one secret in a repository
func CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
// ---
Expand Down Expand Up @@ -45,20 +45,19 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// description: secret created
// "204":
// description: response when updating a secret
// description: secret updated
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"

owner := ctx.Repo.Owner
repo := ctx.Repo.Repository

opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)

_, created, err := secret_service.CreateOrUpdateSecret(ctx, owner.ID, repo.ID, ctx.Params("secretname"), opt.Data)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, repo.ID, ctx.Params("secretname"), opt.Data)
lunny marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
Expand All @@ -77,7 +76,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
}
}

// DeleteSecret delete one secret of the repository
// DeleteSecret delete one secret in a repository
func DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret
// ---
Expand All @@ -104,16 +103,15 @@ func DeleteSecret(ctx *context.APIContext) {
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// description: secret deleted
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"

owner := ctx.Repo.Owner
repo := ctx.Repo.Repository

err := secret_service.DeleteSecretByName(ctx, owner.ID, repo.ID, ctx.Params("secretname"))
err := secret_service.DeleteSecretByName(ctx, 0, repo.ID, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
Expand Down
10 changes: 5 additions & 5 deletions routers/api/v1/user/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
secret_service "code.gitea.io/gitea/services/secrets"
)

// create or update one secret of the user scope
// CreateOrUpdateSecret create or update one secret in a user scope
func CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /user/actions/secrets/{secretname} user updateUserSecret
// ---
Expand All @@ -35,9 +35,9 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// description: secret created
// "204":
// description: response when updating a secret
// description: secret updated
// "400":
// "$ref": "#/responses/error"
// "404":
Expand All @@ -64,7 +64,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
}
}

// DeleteSecret delete one secret of the user scope
// DeleteSecret delete one secret in a user scope
func DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /user/actions/secrets/{secretname} user deleteUserSecret
// ---
Expand All @@ -81,7 +81,7 @@ func DeleteSecret(ctx *context.APIContext) {
// required: true
// responses:
// "204":
// description: delete one secret of the user
// description: secret deleted
// "400":
// "$ref": "#/responses/error"
// "404":
Expand Down
19 changes: 16 additions & 3 deletions routers/web/repo/setting/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (

const (
// TODO: Separate secrets from runners when layout is ready
tplRepoSecrets base.TplName = "repo/settings/actions"
tplOrgSecrets base.TplName = "org/settings/actions"
tplUserSecrets base.TplName = "user/settings/actions"
tplRepoSecrets base.TplName = "repo/settings/actions"
tplOrgSecrets base.TplName = "org/settings/actions"
tplUserSecrets base.TplName = "user/settings/actions"
tplAdminSecrets base.TplName = "admin/actions"
)

type secretsCtx struct {
Expand All @@ -27,10 +28,12 @@ type secretsCtx struct {
IsRepo bool
IsOrg bool
IsUser bool
IsGlobal bool
SecretsTemplate base.TplName
RedirectLink string
}

//nolint:dupl
func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &secretsCtx{
Expand Down Expand Up @@ -67,6 +70,16 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
}, nil
}

if ctx.Data["PageIsAdmin"] == true {
return &secretsCtx{
OwnerID: 0,
RepoID: 0,
IsGlobal: true,
SecretsTemplate: tplAdminSecrets,
RedirectLink: setting.AppSubURL + "/admin/actions/secrets",
}, nil
}

return nil, errors.New("unable to set Secrets context")
}

Expand Down
1 change: 1 addition & 0 deletions routers/web/repo/setting/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type variablesCtx struct {
RedirectLink string
}

//nolint:dupl
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
if ctx.Data["PageIsRepoSettings"] == true {
return &variablesCtx{
Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ func registerRoutes(m *web.Route) {
m.Group("/actions", func() {
m.Get("", admin.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
lunny marked this conversation as resolved.
Show resolved Hide resolved
addSettingsVariablesRoutes()
})
}, adminReq, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled))
Expand Down
3 changes: 3 additions & 0 deletions templates/admin/actions.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
{{if eq .PageType "runners"}}
{{template "shared/actions/runner_list" .}}
{{end}}
{{if eq .PageType "secrets"}}
{{template "shared/secrets/add_list" (dict "ctxData" . "desc" "secrets.instance_desc")}}
{{end}}
{{if eq .PageType "variables"}}
{{template "shared/variables/variable_list" .}}
{{end}}
Expand Down
Loading
Loading