Skip to content

Commit 31e8ad3

Browse files
committed
feat: Add basic permissions support for actions
This depends on a few changes in act and act_runner: https://gitea.com/gitea/act_runner/pulls/272 https://gitea.com/gitea/act/pulls/73
1 parent 3a16680 commit 31e8ad3

File tree

8 files changed

+269
-10
lines changed

8 files changed

+269
-10
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
303303

304304
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
305305

306-
replace github.com/nektos/act => gitea.com/gitea/act v0.243.4
306+
replace github.com/nektos/act => gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6
307307

308308
exclude github.com/gofrs/uuid v3.2.0+incompatible
309309

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtf
5151
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
5252
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
5353
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
54+
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
55+
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
5456
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
5557
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
5658
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
57-
gitea.com/gitea/act v0.243.4 h1:MuBHBLCJfpa6mzwwvs4xqQynrSP2RRzpHpWfTV16PmI=
58-
gitea.com/gitea/act v0.243.4/go.mod h1:mabw6AZAiDgxGlK83orWLrNERSPvgBJzEUS3S7u2bHI=
5959
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo=
6060
gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
6161
gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
@@ -69,6 +69,8 @@ gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfr
6969
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
7070
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
7171
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
72+
gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6 h1:ANNwt5ZqFG7FhDjdwCfsfoi7zlEV7uAfbrYTV5R8CNg=
73+
gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6/go.mod h1:tfannUyz3cgmq1P1o69KW1AMB1aSlNOMzlswHkRjzcQ=
7274
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
7375
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
7476
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=

models/actions/permissions.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package actions
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
10+
"gopkg.in/yaml.v3"
11+
)
12+
13+
type Permission int
14+
15+
const (
16+
PermissionUnspecified Permission = iota
17+
PermissionNone
18+
PermissionRead
19+
PermissionWrite
20+
)
21+
22+
// Per https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions
23+
type Permissions struct {
24+
Actions Permission `yaml:"actions"`
25+
Checks Permission `yaml:"checks"`
26+
Contents Permission `yaml:"contents"`
27+
Deployments Permission `yaml:"deployments"`
28+
IDToken Permission `yaml:"id-token"`
29+
Issues Permission `yaml:"issues"`
30+
Discussions Permission `yaml:"discussions"`
31+
Packages Permission `yaml:"packages"`
32+
Pages Permission `yaml:"pages"`
33+
PullRequests Permission `yaml:"pull-requests"`
34+
RepositoryProjects Permission `yaml:"repository-projects"`
35+
SecurityEvents Permission `yaml:"security-events"`
36+
Statuses Permission `yaml:"statuses"`
37+
}
38+
39+
// WorkflowPermissions parses a workflow and returns
40+
// a Permissions struct representing the permissions set
41+
// at the workflow (i.e. file) level
42+
func WorkflowPermissions(contents []byte) (Permissions, error) {
43+
p := struct {
44+
Permissions Permissions `yaml:"permissions"`
45+
}{}
46+
err := yaml.Unmarshal(contents, &p)
47+
return p.Permissions, err
48+
}
49+
50+
// Given the contents of a workflow, JobPermissions
51+
// returns a Permissions object representing the permissions
52+
// of THE FIRST job in the file.
53+
func JobPermissions(contents []byte) (Permissions, error) {
54+
p := struct {
55+
Jobs []struct {
56+
Permissions Permissions `yaml:"permissions"`
57+
} `yaml:"jobs"`
58+
}{}
59+
err := yaml.Unmarshal(contents, &p)
60+
if len(p.Jobs) > 0 {
61+
return p.Jobs[0].Permissions, err
62+
}
63+
return Permissions{}, errors.New("no jobs detected in workflow")
64+
}
65+
66+
func (p *Permission) UnmarshalYAML(unmarshal func(interface{}) error) error {
67+
var data string
68+
if err := unmarshal(&data); err != nil {
69+
return err
70+
}
71+
72+
switch data {
73+
case "none":
74+
*p = PermissionNone
75+
case "read":
76+
*p = PermissionRead
77+
case "write":
78+
*p = PermissionWrite
79+
default:
80+
return fmt.Errorf("invalid permission: %s", data)
81+
}
82+
83+
return nil
84+
}
85+
86+
// DefaultAccessPermissive is the default "permissive" set granted to actions on repositories
87+
// per https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
88+
// That page also lists a "metadata" permission that I can't find mentioned anywhere else.
89+
// However, it seems to always have "read" permission, so it doesn't really matter.
90+
// Interestingly, it doesn't list "Discussions", so we assume "write" for permissive and "none" for restricted.
91+
var DefaultAccessPermissive = Permissions{
92+
Actions: PermissionWrite,
93+
Checks: PermissionWrite,
94+
Contents: PermissionWrite,
95+
Deployments: PermissionWrite,
96+
IDToken: PermissionNone,
97+
Issues: PermissionWrite,
98+
Discussions: PermissionWrite,
99+
Packages: PermissionWrite,
100+
Pages: PermissionWrite,
101+
PullRequests: PermissionWrite,
102+
RepositoryProjects: PermissionWrite,
103+
SecurityEvents: PermissionWrite,
104+
Statuses: PermissionWrite,
105+
}
106+
107+
// DefaultAccessRestricted is the default "restrictive" set granted. See docs for
108+
// DefaultAccessPermissive above.
109+
//
110+
// This is not currently used, since Gitea does not have a permissive/restricted setting.
111+
var DefaultAccessRestricted = Permissions{
112+
Actions: PermissionNone,
113+
Checks: PermissionNone,
114+
Contents: PermissionWrite,
115+
Deployments: PermissionNone,
116+
IDToken: PermissionNone,
117+
Issues: PermissionNone,
118+
Discussions: PermissionNone,
119+
Packages: PermissionRead,
120+
Pages: PermissionNone,
121+
PullRequests: PermissionNone,
122+
RepositoryProjects: PermissionNone,
123+
SecurityEvents: PermissionNone,
124+
Statuses: PermissionNone,
125+
}
126+
127+
var ReadAllPermissions = Permissions{
128+
Actions: PermissionRead,
129+
Checks: PermissionRead,
130+
Contents: PermissionRead,
131+
Deployments: PermissionRead,
132+
IDToken: PermissionRead,
133+
Issues: PermissionRead,
134+
Discussions: PermissionRead,
135+
Packages: PermissionRead,
136+
Pages: PermissionRead,
137+
PullRequests: PermissionRead,
138+
RepositoryProjects: PermissionRead,
139+
SecurityEvents: PermissionRead,
140+
Statuses: PermissionRead,
141+
}
142+
143+
var WriteAllPermissions = Permissions{
144+
Actions: PermissionWrite,
145+
Checks: PermissionWrite,
146+
Contents: PermissionWrite,
147+
Deployments: PermissionWrite,
148+
IDToken: PermissionWrite,
149+
Issues: PermissionWrite,
150+
Discussions: PermissionWrite,
151+
Packages: PermissionWrite,
152+
Pages: PermissionWrite,
153+
PullRequests: PermissionWrite,
154+
RepositoryProjects: PermissionWrite,
155+
SecurityEvents: PermissionWrite,
156+
Statuses: PermissionWrite,
157+
}
158+
159+
// FromYAML takes a yaml.Node representing a permissions
160+
// definition and parses it into a Permissions struct
161+
func (p *Permissions) FromYAML(rawPermissions *yaml.Node) error {
162+
switch rawPermissions.Kind {
163+
case yaml.ScalarNode:
164+
var val string
165+
err := rawPermissions.Decode(&val)
166+
if err != nil {
167+
return err
168+
}
169+
if val == "read-all" {
170+
*p = ReadAllPermissions
171+
}
172+
if val == "write-all" {
173+
*p = WriteAllPermissions
174+
}
175+
return fmt.Errorf("unexpected `permissions` value: %v", rawPermissions)
176+
case yaml.MappingNode:
177+
var perms Permissions
178+
err := rawPermissions.Decode(&perms)
179+
if err != nil {
180+
return err
181+
}
182+
return nil
183+
case 0:
184+
*p = Permissions{}
185+
return nil
186+
default:
187+
return fmt.Errorf("invalid permissions value: %v", rawPermissions)
188+
}
189+
}
190+
191+
func merge[T comparable](a, b T) T {
192+
var zero T
193+
if a == zero {
194+
return b
195+
}
196+
return a
197+
}
198+
199+
// Merge merges two Permission values
200+
//
201+
// Already set values take precedence over `other`.
202+
// I.e. you want to call jobLevel.Permissions.Merge(topLevel.Permissions)
203+
func (p *Permissions) Merge(other Permissions) {
204+
p.Actions = merge(p.Actions, other.Actions)
205+
p.Checks = merge(p.Checks, other.Checks)
206+
p.Contents = merge(p.Contents, other.Contents)
207+
p.Deployments = merge(p.Deployments, other.Deployments)
208+
p.IDToken = merge(p.IDToken, other.IDToken)
209+
p.Issues = merge(p.Issues, other.Issues)
210+
p.Discussions = merge(p.Discussions, other.Discussions)
211+
p.Packages = merge(p.Packages, other.Packages)
212+
p.Pages = merge(p.Pages, other.Pages)
213+
p.PullRequests = merge(p.PullRequests, other.PullRequests)
214+
p.RepositoryProjects = merge(p.RepositoryProjects, other.RepositoryProjects)
215+
p.SecurityEvents = merge(p.SecurityEvents, other.SecurityEvents)
216+
p.Statuses = merge(p.Statuses, other.Statuses)
217+
}

models/actions/run.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type ActionRun struct {
4545
EventPayload string `xorm:"LONGTEXT"`
4646
TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow
4747
Status Status `xorm:"index"`
48+
Permissions Permissions `xorm:"-"`
4849
Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed
4950
Started timeutil.TimeStamp
5051
Stopped timeutil.TimeStamp
@@ -280,7 +281,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
280281
hasWaiting = true
281282
}
282283
job.Name, _ = util.SplitStringAtByteN(job.Name, 255)
283-
runJobs = append(runJobs, &ActionRunJob{
284+
runJob := &ActionRunJob{
284285
RunID: run.ID,
285286
RepoID: run.RepoID,
286287
OwnerID: run.OwnerID,
@@ -292,7 +293,18 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
292293
Needs: needs,
293294
RunsOn: job.RunsOn(),
294295
Status: status,
295-
})
296+
}
297+
298+
// Parse the job's permissions
299+
if err := job.RawPermissions.Decode(&runJob.Permissions); err != nil {
300+
return err
301+
}
302+
303+
// Merge the job's permissions with the workflow permissions.
304+
// Job permissions take precedence.
305+
runJob.Permissions.Merge(run.Permissions)
306+
307+
runJobs = append(runJobs, runJob)
296308
}
297309
if err := db.Insert(ctx, runJobs); err != nil {
298310
return err

models/actions/run_job.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ type ActionRunJob struct {
2828
Name string `xorm:"VARCHAR(255)"`
2929
Attempt int64
3030
WorkflowPayload []byte
31-
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
32-
Needs []string `xorm:"JSON TEXT"`
33-
RunsOn []string `xorm:"JSON TEXT"`
34-
TaskID int64 // the latest task of the job
35-
Status Status `xorm:"index"`
31+
JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id
32+
Needs []string `xorm:"JSON TEXT"`
33+
RunsOn []string `xorm:"JSON TEXT"`
34+
Permissions Permissions `xorm:"JSON TEXT"`
35+
TaskID int64 // the latest task of the job
36+
Status Status `xorm:"index"`
3637
Started timeutil.TimeStamp
3738
Stopped timeutil.TimeStamp
3839
Created timeutil.TimeStamp `xorm:"created"`

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,8 @@ var migrations = []Migration{
532532
NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable),
533533
// v275 -> v276
534534
NewMigration("Add ScheduleID for ActionRun", v1_21.AddScheduleIDForActionRun),
535+
// v276 -> v277
536+
NewMigration("Add Permissions to Actions Task", v1_21.AddPermissions),
535537
}
536538

537539
// GetCurrentDBVersion returns the current db version

models/migrations/v1_21/v276.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_21 //nolint
5+
6+
import (
7+
actions_model "code.gitea.io/gitea/models/actions"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func AddPermissions(x *xorm.Engine) error {
13+
type ActionRunJob struct {
14+
Permissions actions_model.Permissions `xorm:"JSON TEXT"`
15+
}
16+
17+
return x.Sync(new(ActionRunJob))
18+
}

services/actions/notifier_helper.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,13 @@ func handleWorkflows(
268268
}
269269
}
270270

271+
wp, err := actions_model.WorkflowPermissions(dwf.Content)
272+
if err != nil {
273+
log.Error("WorkflowPermissions: %v", err)
274+
continue
275+
}
276+
run.Permissions = wp
277+
271278
if err := actions_model.InsertRun(ctx, run, jobs); err != nil {
272279
log.Error("InsertRun: %v", err)
273280
continue

0 commit comments

Comments
 (0)