Add allow list for approvals (#4768)

Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
Co-authored-by: Robert Kaussow <xoxys@rknet.org>
Co-authored-by: Robert Kaussow <mail@thegeeklab.de>
This commit is contained in:
qwerty287 2025-01-30 01:21:33 +02:00 committed by GitHub
parent ee0e5fb47f
commit 8e99551d18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 86 additions and 2 deletions

View file

@ -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": {

View file

@ -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
}

View file

@ -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"`

View file

@ -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:

View file

@ -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 {

View file

@ -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"

View file

@ -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'

View file

@ -88,6 +88,27 @@
</template>
</InputField>
<InputField
v-if="repoSettings.require_approval !== RepoRequireApproval.None"
:label="$t('require_approval.allowed_users.allowed_users')"
>
<template #default="{ id }">
<div class="flex flex-col gap-2">
<div v-for="allowedUser in repoSettings.approval_allowed_users" :key="allowedUser" class="flex gap-2">
<TextField :id="id" :model-value="allowedUser" disabled />
<Button type="button" color="gray" start-icon="trash" @click="removeUser(allowedUser)" />
</div>
<div class="flex gap-2">
<TextField :id="id" v-model="newUser" @keydown.enter.prevent="addNewUser" />
<Button type="button" color="gray" start-icon="plus" @click="addNewUser" />
</div>
</div>
</template>
<template #description>
{{ $t('require_approval.allowed_users.desc') }}
</template>
</InputField>
<InputField docs-url="docs/usage/project-settings#project-visibility" :label="$t('repo.visibility.visibility')">
<RadioField v-model="repoSettings.visibility" :options="projectVisibilityOptions" />
</InputField>
@ -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);
}
</script>