Skip to content

Commit a33e74d

Browse files
authored
Clarify Actions resources ownership (#31724)
Fix #31707. Also related to #31715. Some Actions resources could has different types of ownership. It could be: - global: all repos and orgs/users can use it. - org/user level: only the org/user can use it. - repo level: only the repo can use it. There are two ways to distinguish org/user level from repo level: 1. `{owner_id: 1, repo_id: 2}` for repo level, and `{owner_id: 1, repo_id: 0}` for org level. 2. `{owner_id: 0, repo_id: 2}` for repo level, and `{owner_id: 1, repo_id: 0}` for org level. The first way seems more reasonable, but it may not be true. The point is that although a resource, like a runner, belongs to a repo (it can be used by the repo), the runner doesn't belong to the repo's org (other repos in the same org cannot use the runner). So, the second method makes more sense. And the first way is not user-friendly to query, we must set the repo id to zero to avoid wrong results. So, #31715 should be right. And the most simple way to fix #31707 is just: ```diff - shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) + shared.GetRegistrationToken(ctx, 0, ctx.Repo.Repository.ID) ``` However, it is quite intuitive to set both owner id and repo id since the repo belongs to the owner. So I prefer to be compatible with it. If we get both owner id and repo id not zero when creating or finding, it's very clear that the caller want one with repo level, but set owner id accidentally. So it's OK to accept it but fix the owner id to zero.
1 parent 9e31b22 commit a33e74d

File tree

5 files changed

+102
-37
lines changed

5 files changed

+102
-37
lines changed

models/actions/runner.go

+20-5
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,25 @@ import (
2323
)
2424

2525
// ActionRunner represents runner machines
26+
//
27+
// It can be:
28+
// 1. global runner, OwnerID is 0 and RepoID is 0
29+
// 2. org/user level runner, OwnerID is org/user ID and RepoID is 0
30+
// 3. repo level runner, OwnerID is 0 and RepoID is repo ID
31+
//
32+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
33+
// or it will be complicated to find runners belonging to a specific owner.
34+
// For example, conditions like `OwnerID = 1` will also return runner {OwnerID: 1, RepoID: 1},
35+
// but it's a repo level runner, not an org/user level runner.
36+
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level runners.
2637
type ActionRunner struct {
2738
ID int64
2839
UUID string `xorm:"CHAR(36) UNIQUE"`
2940
Name string `xorm:"VARCHAR(255)"`
3041
Version string `xorm:"VARCHAR(64)"`
31-
OwnerID int64 `xorm:"index"` // org level runner, 0 means system
42+
OwnerID int64 `xorm:"index"`
3243
Owner *user_model.User `xorm:"-"`
33-
RepoID int64 `xorm:"index"` // repo level runner, if OwnerID also is zero, then it's a global
44+
RepoID int64 `xorm:"index"`
3445
Repo *repo_model.Repository `xorm:"-"`
3546
Description string `xorm:"TEXT"`
3647
Base int // 0 native 1 docker 2 virtual machine
@@ -157,7 +168,7 @@ func init() {
157168
type FindRunnerOptions struct {
158169
db.ListOptions
159170
RepoID int64
160-
OwnerID int64
171+
OwnerID int64 // it will be ignored if RepoID is set
161172
Sort string
162173
Filter string
163174
IsOnline optional.Option[bool]
@@ -174,8 +185,7 @@ func (opts FindRunnerOptions) ToConds() builder.Cond {
174185
c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
175186
}
176187
cond = cond.And(c)
177-
}
178-
if opts.OwnerID > 0 {
188+
} else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set
179189
c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID})
180190
if opts.WithAvailable {
181191
c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0})
@@ -263,6 +273,11 @@ func DeleteRunner(ctx context.Context, id int64) error {
263273

264274
// CreateRunner creates new runner.
265275
func CreateRunner(ctx context.Context, t *ActionRunner) error {
276+
if t.OwnerID != 0 && t.RepoID != 0 {
277+
// It's trying to create a runner that belongs to a repository, but OwnerID has been set accidentally.
278+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
279+
t.OwnerID = 0
280+
}
266281
return db.Insert(ctx, t)
267282
}
268283

models/actions/runner_token.go

+26-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,23 @@ import (
1515
)
1616

1717
// ActionRunnerToken represents runner tokens
18+
//
19+
// It can be:
20+
// 1. global token, OwnerID is 0 and RepoID is 0
21+
// 2. org/user level token, OwnerID is org/user ID and RepoID is 0
22+
// 3. repo level token, OwnerID is 0 and RepoID is repo ID
23+
//
24+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
25+
// or it will be complicated to find tokens belonging to a specific owner.
26+
// For example, conditions like `OwnerID = 1` will also return token {OwnerID: 1, RepoID: 1},
27+
// but it's a repo level token, not an org/user level token.
28+
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level tokens.
1829
type ActionRunnerToken struct {
1930
ID int64
2031
Token string `xorm:"UNIQUE"`
21-
OwnerID int64 `xorm:"index"` // org level runner, 0 means system
32+
OwnerID int64 `xorm:"index"`
2233
Owner *user_model.User `xorm:"-"`
23-
RepoID int64 `xorm:"index"` // repo level runner, if orgid also is zero, then it's a global
34+
RepoID int64 `xorm:"index"`
2435
Repo *repo_model.Repository `xorm:"-"`
2536
IsActive bool // true means it can be used
2637

@@ -58,7 +69,14 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string
5869
}
5970

6071
// NewRunnerToken creates a new active runner token and invalidate all old tokens
72+
// ownerID will be ignored and treated as 0 if repoID is non-zero.
6173
func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
74+
if ownerID != 0 && repoID != 0 {
75+
// It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally.
76+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
77+
ownerID = 0
78+
}
79+
6280
token, err := util.CryptoRandomString(40)
6381
if err != nil {
6482
return nil, err
@@ -84,6 +102,12 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo
84102

85103
// GetLatestRunnerToken returns the latest runner token
86104
func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) {
105+
if ownerID != 0 && repoID != 0 {
106+
// It's trying to get a runner token that belongs to a repository, but OwnerID has been set accidentally.
107+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
108+
ownerID = 0
109+
}
110+
87111
var runnerToken ActionRunnerToken
88112
has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=?", ownerID, repoID).
89113
OrderBy("id DESC").Get(&runnerToken)

models/actions/variable.go

+24-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package actions
55

66
import (
77
"context"
8-
"errors"
98
"strings"
109

1110
"code.gitea.io/gitea/models/db"
@@ -15,6 +14,18 @@ import (
1514
"xorm.io/builder"
1615
)
1716

17+
// ActionVariable represents a variable that can be used in actions
18+
//
19+
// It can be:
20+
// 1. global variable, OwnerID is 0 and RepoID is 0
21+
// 2. org/user level variable, OwnerID is org/user ID and RepoID is 0
22+
// 3. repo level variable, OwnerID is 0 and RepoID is repo ID
23+
//
24+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
25+
// or it will be complicated to find variables belonging to a specific owner.
26+
// For example, conditions like `OwnerID = 1` will also return variable {OwnerID: 1, RepoID: 1},
27+
// but it's a repo level variable, not an org/user level variable.
28+
// To avoid this, make it clear with {OwnerID: 0, RepoID: 1} for repo level variables.
1829
type ActionVariable struct {
1930
ID int64 `xorm:"pk autoincr"`
2031
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
@@ -29,39 +40,40 @@ func init() {
2940
db.RegisterModel(new(ActionVariable))
3041
}
3142

32-
func (v *ActionVariable) Validate() error {
33-
if v.OwnerID != 0 && v.RepoID != 0 {
34-
return errors.New("a variable should not be bound to an owner and a repository at the same time")
43+
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
44+
if ownerID != 0 && repoID != 0 {
45+
// It's trying to create a variable that belongs to a repository, but OwnerID has been set accidentally.
46+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
47+
ownerID = 0
3548
}
36-
return nil
37-
}
3849

39-
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
4050
variable := &ActionVariable{
4151
OwnerID: ownerID,
4252
RepoID: repoID,
4353
Name: strings.ToUpper(name),
4454
Data: data,
4555
}
46-
if err := variable.Validate(); err != nil {
47-
return variable, err
48-
}
4956
return variable, db.Insert(ctx, variable)
5057
}
5158

5259
type FindVariablesOpts struct {
5360
db.ListOptions
54-
OwnerID int64
5561
RepoID int64
62+
OwnerID int64 // it will be ignored if RepoID is set
5663
Name string
5764
}
5865

5966
func (opts FindVariablesOpts) ToConds() builder.Cond {
6067
cond := builder.NewCond()
6168
// Since we now support instance-level variables,
6269
// there is no need to check for null values for `owner_id` and `repo_id`
63-
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
6470
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
71+
if opts.RepoID != 0 { // if RepoID is set
72+
// ignore OwnerID and treat it as 0
73+
cond = cond.And(builder.Eq{"owner_id": 0})
74+
} else {
75+
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
76+
}
6577

6678
if opts.Name != "" {
6779
cond = cond.And(builder.Eq{"name": strings.ToUpper(opts.Name)})

models/secret/secret.go

+30-16
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package secret
55

66
import (
77
"context"
8-
"errors"
98
"fmt"
109
"strings"
1110

@@ -22,6 +21,19 @@ import (
2221
)
2322

2423
// Secret represents a secret
24+
//
25+
// It can be:
26+
// 1. org/user level secret, OwnerID is org/user ID and RepoID is 0
27+
// 2. repo level secret, OwnerID is 0 and RepoID is repo ID
28+
//
29+
// Please note that it's not acceptable to have both OwnerID and RepoID to be non-zero,
30+
// or it will be complicated to find secrets belonging to a specific owner.
31+
// For example, conditions like `OwnerID = 1` will also return secret {OwnerID: 1, RepoID: 1},
32+
// but it's a repo level secret, not an org/user level secret.
33+
// 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.
2537
type Secret struct {
2638
ID int64
2739
OwnerID int64 `xorm:"INDEX UNIQUE(owner_repo_name) NOT NULL"`
@@ -46,6 +58,15 @@ func (err ErrSecretNotFound) Unwrap() error {
4658

4759
// InsertEncryptedSecret Creates, encrypts, and validates a new secret with yet unencrypted data and insert into database
4860
func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*Secret, error) {
61+
if ownerID != 0 && repoID != 0 {
62+
// It's trying to create a secret that belongs to a repository, but OwnerID has been set accidentally.
63+
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
64+
ownerID = 0
65+
}
66+
if ownerID == 0 && repoID == 0 {
67+
return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
68+
}
69+
4970
encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
5071
if err != nil {
5172
return nil, err
@@ -56,39 +77,32 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
5677
Name: strings.ToUpper(name),
5778
Data: encrypted,
5879
}
59-
if err := secret.Validate(); err != nil {
60-
return secret, err
61-
}
6280
return secret, db.Insert(ctx, secret)
6381
}
6482

6583
func init() {
6684
db.RegisterModel(new(Secret))
6785
}
6886

69-
func (s *Secret) Validate() error {
70-
if s.OwnerID == 0 && s.RepoID == 0 {
71-
return errors.New("the secret is not bound to any scope")
72-
}
73-
return nil
74-
}
75-
7687
type FindSecretsOptions struct {
7788
db.ListOptions
78-
OwnerID int64
7989
RepoID int64
90+
OwnerID int64 // it will be ignored if RepoID is set
8091
SecretID int64
8192
Name string
8293
}
8394

8495
func (opts FindSecretsOptions) ToConds() builder.Cond {
8596
cond := builder.NewCond()
86-
if opts.OwnerID > 0 {
97+
98+
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
99+
if opts.RepoID != 0 { // if RepoID is set
100+
// ignore OwnerID and treat it as 0
101+
cond = cond.And(builder.Eq{"owner_id": 0})
102+
} else {
87103
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
88104
}
89-
if opts.RepoID > 0 {
90-
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
91-
}
105+
92106
if opts.SecretID != 0 {
93107
cond = cond.And(builder.Eq{"id": opts.SecretID})
94108
}

tests/integration/api_user_variables_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestAPIUserVariables(t *testing.T) {
1919
session := loginUser(t, "user1")
2020
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
2121

22-
t.Run("CreateRepoVariable", func(t *testing.T) {
22+
t.Run("CreateUserVariable", func(t *testing.T) {
2323
cases := []struct {
2424
Name string
2525
ExpectedStatus int
@@ -70,7 +70,7 @@ func TestAPIUserVariables(t *testing.T) {
7070
}
7171
})
7272

73-
t.Run("UpdateRepoVariable", func(t *testing.T) {
73+
t.Run("UpdateUserVariable", func(t *testing.T) {
7474
variableName := "test_update_var"
7575
url := fmt.Sprintf("/api/v1/user/actions/variables/%s", variableName)
7676
req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{

0 commit comments

Comments
 (0)