diff --git a/cmd/server/openapi/docs.go b/cmd/server/openapi/docs.go index 074fafbab..9c0092a23 100644 --- a/cmd/server/openapi/docs.go +++ b/cmd/server/openapi/docs.go @@ -5054,6 +5054,12 @@ const docTemplate = `{ "allow_pr": { "type": "boolean" }, + "approval_allowed_users": { + "type": "array", + "items": { + "type": "string" + } + }, "avatar_url": { "type": "string" }, @@ -5135,6 +5141,12 @@ const docTemplate = `{ "allow_pr": { "type": "boolean" }, + "approval_allowed_users": { + "type": "array", + "items": { + "type": "string" + } + }, "cancel_previous_pipeline_events": { "type": "array", "items": { diff --git a/server/api/repo.go b/server/api/repo.go index c9127c219..e9e49648a 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -259,6 +259,9 @@ func PatchRepo(c *gin.Context) { return } } + if in.ApprovalAllowedUsers != nil { + repo.ApprovalAllowedUsers = *in.ApprovalAllowedUsers + } if in.Timeout != nil { repo.Timeout = *in.Timeout } diff --git a/server/model/repo.go b/server/model/repo.go index e61646216..ce944afa1 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -63,6 +63,7 @@ type Repo struct { IsSCMPrivate bool `json:"private" xorm:"private"` Trusted TrustedConfiguration `json:"trusted" xorm:"json 'trusted'"` RequireApproval ApprovalMode `json:"require_approval" xorm:"varchar(50) require_approval"` + ApprovalAllowedUsers []string `json:"approval_allowed_users" xorm:"json approval_allowed_users"` IsActive bool `json:"active" xorm:"active"` AllowPull bool `json:"allow_pr" xorm:"allow_pr"` AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"` @@ -129,6 +130,7 @@ func (r *Repo) Update(from *Repo) { type RepoPatch struct { Config *string `json:"config_file,omitempty"` RequireApproval *string `json:"require_approval,omitempty"` + ApprovalAllowedUsers *[]string `json:"approval_allowed_users,omitempty"` Timeout *int64 `json:"timeout,omitempty"` Visibility *string `json:"visibility,omitempty"` AllowPull *bool `json:"allow_pr,omitempty"` diff --git a/server/pipeline/gated.go b/server/pipeline/gated.go index 4a4f3dc36..8804b8c2b 100644 --- a/server/pipeline/gated.go +++ b/server/pipeline/gated.go @@ -14,7 +14,11 @@ package pipeline -import "go.woodpecker-ci.org/woodpecker/v3/server/model" +import ( + "slices" + + "go.woodpecker-ci.org/woodpecker/v3/server/model" +) func setApprovalState(repo *model.Repo, pipeline *model.Pipeline) { if !needsApproval(repo, pipeline) { @@ -31,6 +35,12 @@ func needsApproval(repo *model.Repo, pipeline *model.Pipeline) bool { return false } + // skip if user is allowed + // It's enough to check the username as the repo matches the forge of the pipeline already (no username clashes from different forges possible) + if slices.Contains(repo.ApprovalAllowedUsers, pipeline.Author) { + return false + } + switch repo.RequireApproval { // repository allows all events without approval case model.RequireApprovalNone: diff --git a/server/pipeline/gated_test.go b/server/pipeline/gated_test.go index 76004ff3d..c429c9a03 100644 --- a/server/pipeline/gated_test.go +++ b/server/pipeline/gated_test.go @@ -69,6 +69,18 @@ func TestSetGatedState(t *testing.T) { }, expectBlocked: true, }, + { + name: "require approval for everything with allowed user", + repo: &model.Repo{ + RequireApproval: model.RequireApprovalAllEvents, + ApprovalAllowedUsers: []string{"user"}, + }, + pipeline: &model.Pipeline{ + Event: model.EventPush, + Author: "user", + }, + expectBlocked: false, + }, } for _, tc := range testCases { diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index d97f82b2c..a11287cad 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -509,7 +509,11 @@ "none_desc": "Every event triggers pipelines, including pull requests. This setting can be dangerous and is only recommended for private instances.", "forks": "Pull request from forked repository", "pull_requests": "All pull requests", - "all_events": "All events from forge" + "all_events": "All events from forge", + "allowed_users": { + "allowed_users": "Allowed users", + "desc": "Pipelines created by the listed users never require approval." + } }, "all_repositories": "All repositories", "no_search_results": "No results found" diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index 18698aaae..c2cb8d61b 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -73,6 +73,8 @@ export interface Repo { require_approval: RepoRequireApproval; + approval_allowed_users: string[]; + // Events that will cancel running pipelines before starting a new one cancel_previous_pipeline_events: string[]; @@ -101,6 +103,7 @@ export type RepoSettings = Pick< | 'visibility' | 'trusted' | 'require_approval' + | 'approval_allowed_users' | 'allow_pr' | 'allow_deploy' | 'cancel_previous_pipeline_events' diff --git a/web/src/views/repo/settings/General.vue b/web/src/views/repo/settings/General.vue index b6edd8815..b42a8f040 100644 --- a/web/src/views/repo/settings/General.vue +++ b/web/src/views/repo/settings/General.vue @@ -88,6 +88,27 @@ + + + + + @@ -191,6 +212,7 @@ function loadRepoSettings() { visibility: repo.value.visibility, require_approval: repo.value.require_approval, trusted: repo.value.trusted, + approval_allowed_users: repo.value.approval_allowed_users || [], allow_pr: repo.value.allow_pr, allow_deploy: repo.value.allow_deploy, cancel_previous_pipeline_events: repo.value.cancel_previous_pipeline_events || [], @@ -268,4 +290,20 @@ function removeImage(image: string) { repoSettings.value.netrc_trusted = repoSettings.value.netrc_trusted.filter((i) => i !== image); } + +const newUser = ref(''); +function addNewUser() { + if (!newUser.value) { + return; + } + repoSettings.value?.approval_allowed_users.push(newUser.value); + newUser.value = ''; +} +function removeUser(user: string) { + if (!repoSettings.value) { + throw new Error('Unexpected: repoSettings should be set'); + } + + repoSettings.value.approval_allowed_users = repoSettings.value.approval_allowed_users.filter((i) => i !== user); +}