Skip to content

Commit aa49e48

Browse files
committed
Add instance-level secrets
1 parent be2a6b4 commit aa49e48

File tree

19 files changed

+813
-85
lines changed

19 files changed

+813
-85
lines changed

models/secret/secret.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ import (
2525
// It can be:
2626
// 1. org/user level secret, OwnerID is org/user ID and RepoID is 0
2727
// 2. repo level secret, OwnerID is 0 and RepoID is repo ID
28+
// 3. global level secret, OwnerID is 0 and RepoID is 0
2829
//
2930
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
3031
// or it will be complicated to find secrets belonging to a specific owner.
3132
// For example, conditions like `OwnerID = 1` will also return secret {OwnerID: 1, RepoID: 1},
3233
// but it's a repo level secret, not an org/user level secret.
3334
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level secrets.
34-
//
35-
// Please note that it's not acceptable to have both OwnerID and RepoID to zero, global secrets are not supported.
36-
// It's for security reasons, admin may be not aware of that the secrets could be stolen by any user when setting them as global.
3735
type Secret struct {
3836
ID int64
3937
OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"`
@@ -69,9 +67,6 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
6967
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
7068
ownerID = 0
7169
}
72-
if ownerID == 0 && repoID == 0 {
73-
return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
74-
}
7570

7671
if len(data) > SecretDataMaxLength {
7772
return nil, util.NewInvalidArgumentErrorf("data too long")
@@ -108,14 +103,8 @@ type FindSecretsOptions struct {
108103

109104
func (opts FindSecretsOptions) ToConds() builder.Cond {
110105
cond := builder.NewCond()
111-
106+
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
112107
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
113-
if opts.RepoID != 0 { // if RepoID is set
114-
// ignore OwnerID and treat it as 0
115-
cond = cond.And(builder.Eq{"owner_id": 0})
116-
} else {
117-
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
118-
}
119108

120109
if opts.SecretID != 0 {
121110
cond = cond.And(builder.Eq{"id": opts.SecretID})

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3777,6 +3777,7 @@ deletion.description = Removing a secret is permanent and cannot be undone. Cont
37773777
deletion.success = The secret has been removed.
37783778
deletion.failed = Failed to remove secret.
37793779
management = Secrets Management
3780+
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.
37803781

37813782
[actions]
37823783
actions = Actions

routers/api/actions/runner/utils.go

Lines changed: 251 additions & 0 deletions
Large diffs are not rendered by default.

routers/api/v1/admin/action.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,15 @@
44
package admin
55

66
import (
7+
"errors"
8+
"net/http"
9+
10+
api "code.gitea.io/gitea/modules/structs"
11+
"code.gitea.io/gitea/modules/util"
12+
"code.gitea.io/gitea/modules/web"
713
"code.gitea.io/gitea/routers/api/v1/shared"
814
"code.gitea.io/gitea/services/context"
15+
secret_service "code.gitea.io/gitea/services/secrets"
916
)
1017

1118
// ListWorkflowJobs Lists all jobs
@@ -91,3 +98,91 @@ func ListWorkflowRuns(ctx *context.APIContext) {
9198

9299
shared.ListRuns(ctx, 0, 0)
93100
}
101+
102+
// CreateOrUpdateSecret create or update one secret in instance scope
103+
func CreateOrUpdateSecret(ctx *context.APIContext) {
104+
// swagger:operation PUT /admin/actions/secrets/{secretname} admin updateAdminSecret
105+
// ---
106+
// summary: Create or Update a secret value in instance scope
107+
// consumes:
108+
// - application/json
109+
// produces:
110+
// - application/json
111+
// parameters:
112+
// - name: secretname
113+
// in: path
114+
// description: name of the secret
115+
// type: string
116+
// required: true
117+
// - name: body
118+
// in: body
119+
// schema:
120+
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
121+
// responses:
122+
// "201":
123+
// description: secret created
124+
// "204":
125+
// description: secret updated
126+
// "400":
127+
// "$ref": "#/responses/error"
128+
// "404":
129+
// "$ref": "#/responses/notFound"
130+
131+
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
132+
133+
_, created, err := secret_service.CreateOrUpdateSecret(ctx, 0, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
134+
if err != nil {
135+
if errors.Is(err, util.ErrInvalidArgument) {
136+
ctx.APIError(http.StatusBadRequest, err)
137+
} else if errors.Is(err, util.ErrNotExist) {
138+
ctx.APIError(http.StatusNotFound, err)
139+
} else {
140+
ctx.APIError(http.StatusInternalServerError, err)
141+
}
142+
return
143+
}
144+
145+
if created {
146+
ctx.Status(http.StatusCreated)
147+
} else {
148+
ctx.Status(http.StatusNoContent)
149+
}
150+
}
151+
152+
// DeleteSecret delete one secret in instance scope
153+
func DeleteSecret(ctx *context.APIContext) {
154+
// swagger:operation DELETE /admin/actions/secrets/{secretname} admin deleteAdminSecret
155+
// ---
156+
// summary: Delete a secret in instance scope
157+
// consumes:
158+
// - application/json
159+
// produces:
160+
// - application/json
161+
// parameters:
162+
// - name: secretname
163+
// in: path
164+
// description: name of the secret
165+
// type: string
166+
// required: true
167+
// responses:
168+
// "204":
169+
// description: secret deleted
170+
// "400":
171+
// "$ref": "#/responses/error"
172+
// "404":
173+
// "$ref": "#/responses/notFound"
174+
175+
err := secret_service.DeleteSecretByName(ctx, 0, 0, ctx.PathParam("secretname"))
176+
if err != nil {
177+
if errors.Is(err, util.ErrInvalidArgument) {
178+
ctx.APIError(http.StatusBadRequest, err)
179+
} else if errors.Is(err, util.ErrNotExist) {
180+
ctx.APIError(http.StatusNotFound, err)
181+
} else {
182+
ctx.APIError(http.StatusInternalServerError, err)
183+
}
184+
return
185+
}
186+
187+
ctx.Status(http.StatusNoContent)
188+
}

routers/api/v1/api.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,6 @@ func Routes() *web.Router {
10551055
Post(bind(api.CreateEmailOption{}), user.AddEmail).
10561056
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
10571057

1058-
// manage user-level actions features
10591058
m.Group("/actions", func() {
10601059
m.Group("/secrets", func() {
10611060
m.Combo("/{secretname}").
@@ -1710,6 +1709,11 @@ func Routes() *web.Router {
17101709
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly())
17111710

17121711
m.Group("/admin", func() {
1712+
m.Group("/actions/secrets", func() {
1713+
m.Combo("/{secretname}").
1714+
Put(bind(api.CreateOrUpdateSecretOption{}), admin.CreateOrUpdateSecret).
1715+
Delete(admin.DeleteSecret)
1716+
})
17131717
m.Group("/cron", func() {
17141718
m.Get("", admin.ListCronTasks)
17151719
m.Post("/{task}", admin.PostCronTask)

routers/api/v1/org/secrets.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package org
5+
6+
import (
7+
"errors"
8+
"net/http"
9+
10+
"code.gitea.io/gitea/models/db"
11+
secret_model "code.gitea.io/gitea/models/secret"
12+
api "code.gitea.io/gitea/modules/structs"
13+
"code.gitea.io/gitea/modules/util"
14+
"code.gitea.io/gitea/modules/web"
15+
"code.gitea.io/gitea/routers/api/v1/utils"
16+
"code.gitea.io/gitea/services/context"
17+
secret_service "code.gitea.io/gitea/services/secrets"
18+
)
19+
20+
// ListActionsSecrets list an organization's actions secrets
21+
func ListActionsSecrets(ctx *context.APIContext) {
22+
// swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets
23+
// ---
24+
// summary: List an organization's actions secrets
25+
// produces:
26+
// - application/json
27+
// parameters:
28+
// - name: org
29+
// in: path
30+
// description: name of the organization
31+
// type: string
32+
// required: true
33+
// - name: page
34+
// in: query
35+
// description: page number of results to return (1-based)
36+
// type: integer
37+
// - name: limit
38+
// in: query
39+
// description: page size of results
40+
// type: integer
41+
// responses:
42+
// "200":
43+
// "$ref": "#/responses/SecretList"
44+
// "404":
45+
// "$ref": "#/responses/notFound"
46+
47+
opts := &secret_model.FindSecretsOptions{
48+
OwnerID: ctx.Org.Organization.ID,
49+
ListOptions: utils.GetListOptions(ctx),
50+
}
51+
52+
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
53+
if err != nil {
54+
ctx.APIErrorInternal(err)
55+
return
56+
}
57+
58+
apiSecrets := make([]*api.Secret, len(secrets))
59+
for k, v := range secrets {
60+
apiSecrets[k] = &api.Secret{
61+
Name: v.Name,
62+
Created: v.CreatedUnix.AsTime(),
63+
}
64+
}
65+
66+
ctx.SetTotalCountHeader(count)
67+
ctx.JSON(http.StatusOK, apiSecrets)
68+
}
69+
70+
// CreateOrUpdateSecret create or update one secret in an organization
71+
func CreateOrUpdateSecret(ctx *context.APIContext) {
72+
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
73+
// ---
74+
// summary: Create or Update a secret value in an organization
75+
// consumes:
76+
// - application/json
77+
// produces:
78+
// - application/json
79+
// parameters:
80+
// - name: org
81+
// in: path
82+
// description: name of organization
83+
// type: string
84+
// required: true
85+
// - name: secretname
86+
// in: path
87+
// description: name of the secret
88+
// type: string
89+
// required: true
90+
// - name: body
91+
// in: body
92+
// schema:
93+
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
94+
// responses:
95+
// "201":
96+
// description: secret created
97+
// "204":
98+
// description: secret updated
99+
// "400":
100+
// "$ref": "#/responses/error"
101+
// "404":
102+
// "$ref": "#/responses/notFound"
103+
104+
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
105+
106+
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"), opt.Data, opt.Description)
107+
if err != nil {
108+
if errors.Is(err, util.ErrInvalidArgument) {
109+
ctx.APIError(http.StatusBadRequest, err)
110+
} else if errors.Is(err, util.ErrNotExist) {
111+
ctx.APIError(http.StatusNotFound, err)
112+
} else {
113+
ctx.APIError(http.StatusInternalServerError, err)
114+
}
115+
return
116+
}
117+
118+
if created {
119+
ctx.Status(http.StatusCreated)
120+
} else {
121+
ctx.Status(http.StatusNoContent)
122+
}
123+
}
124+
125+
// DeleteSecret delete one secret in an organization
126+
func DeleteSecret(ctx *context.APIContext) {
127+
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
128+
// ---
129+
// summary: Delete a secret in an organization
130+
// consumes:
131+
// - application/json
132+
// produces:
133+
// - application/json
134+
// parameters:
135+
// - name: org
136+
// in: path
137+
// description: name of organization
138+
// type: string
139+
// required: true
140+
// - name: secretname
141+
// in: path
142+
// description: name of the secret
143+
// type: string
144+
// required: true
145+
// responses:
146+
// "204":
147+
// description: secret deleted
148+
// "400":
149+
// "$ref": "#/responses/error"
150+
// "404":
151+
// "$ref": "#/responses/notFound"
152+
153+
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.PathParam("secretname"))
154+
if err != nil {
155+
if errors.Is(err, util.ErrInvalidArgument) {
156+
ctx.APIError(http.StatusBadRequest, err)
157+
} else if errors.Is(err, util.ErrNotExist) {
158+
ctx.APIError(http.StatusNotFound, err)
159+
} else {
160+
ctx.APIError(http.StatusInternalServerError, err)
161+
}
162+
return
163+
}
164+
165+
ctx.Status(http.StatusNoContent)
166+
}

routers/api/v1/repo/action.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import (
3636
"github.com/nektos/act/pkg/model"
3737
)
3838

39-
// ListActionsSecrets list an repo's actions secrets
39+
// ListActionsSecrets list a repo's actions secrets
4040
func (Action) ListActionsSecrets(ctx *context.APIContext) {
4141
// swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets
4242
// ---
@@ -94,7 +94,7 @@ func (Action) ListActionsSecrets(ctx *context.APIContext) {
9494
ctx.JSON(http.StatusOK, apiSecrets)
9595
}
9696

97-
// create or update one secret of the repository
97+
// CreateOrUpdateSecret create or update one secret in a repository
9898
func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
9999
// swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
100100
// ---
@@ -125,9 +125,9 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
125125
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
126126
// responses:
127127
// "201":
128-
// description: response when creating a secret
128+
// description: secret created
129129
// "204":
130-
// description: response when updating a secret
130+
// description: secret updated
131131
// "400":
132132
// "$ref": "#/responses/error"
133133
// "404":
@@ -156,7 +156,7 @@ func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
156156
}
157157
}
158158

159-
// DeleteSecret delete one secret of the repository
159+
// DeleteSecret delete one secret in a repository
160160
func (Action) DeleteSecret(ctx *context.APIContext) {
161161
// swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret
162162
// ---
@@ -183,7 +183,7 @@ func (Action) DeleteSecret(ctx *context.APIContext) {
183183
// required: true
184184
// responses:
185185
// "204":
186-
// description: delete one secret of the repository
186+
// description: secret deleted
187187
// "400":
188188
// "$ref": "#/responses/error"
189189
// "404":

0 commit comments

Comments
 (0)