Allow to set custom trusted clone plugins (#4352)

Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: Thomas Anderson <127358482+zc-devs@users.noreply.github.com>
Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
qwerty287 2024-11-26 15:27:05 +02:00 committed by GitHub
parent bf1750a291
commit 5bb7cef08b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 97 additions and 37 deletions

View File

@ -5070,8 +5070,11 @@ const docTemplate = `{
"name": { "name": {
"type": "string" "type": "string"
}, },
"netrc_only_trusted": { "netrc_trusted": {
"type": "boolean" "type": "array",
"items": {
"type": "string"
}
}, },
"org_id": { "org_id": {
"type": "integer" "type": "integer"
@ -5124,8 +5127,11 @@ const docTemplate = `{
"description": "TODO: deprecated in favor of RequireApproval =\u003e Remove in next major release", "description": "TODO: deprecated in favor of RequireApproval =\u003e Remove in next major release",
"type": "boolean" "type": "boolean"
}, },
"netrc_only_trusted": { "netrc_trusted": {
"type": "boolean" "type": "array",
"items": {
"type": "string"
}
}, },
"require_approval": { "require_approval": {
"type": "string" "type": "string"

View File

@ -39,16 +39,13 @@ Only server admins can set this option. If you are not a server admin this optio
::: :::
## Only inject Git credentials into trusted clone plugins ## Custom trusted clone plugins
The clone step may require Git credentials (e.g. for private repos) which are injected via `netrc`. The clone step may require Git credentials (e.g. for private repos) which are injected via `netrc`.
By default, they are only injected into trusted clone plugins listed in the env var `WOODPECKER_PLUGINS_TRUSTED_CLONE`. They are only injected into trusted plugins listed in the env var `WOODPECKER_PLUGINS_TRUSTED_CLONE` or in this repo setting.
If this option is disabled, the Git credentials are injected into every clone plugin, regardless of whether it is trusted or not.
:::note This allows you to use a trusted plugin for in the clone section or as a step to pull or push using your git credentials.
This option has no effect on steps other than the clone step.
:::
## Project visibility ## Project visibility

View File

@ -98,7 +98,6 @@ type Compiler struct {
defaultClonePlugin string defaultClonePlugin string
trustedClonePlugins []string trustedClonePlugins []string
securityTrustedPipeline bool securityTrustedPipeline bool
netrcOnlyTrusted bool
} }
// New creates a new Compiler with options. // New creates a new Compiler with options.
@ -196,7 +195,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
} }
// only inject netrc if it's a trusted repo or a trusted plugin // only inject netrc if it's a trusted repo or a trusted plugin
if !c.netrcOnlyTrusted || c.securityTrustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) { if c.securityTrustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv { for k, v := range c.cloneEnv {
step.Environment[k] = v step.Environment[k] = v
} }
@ -252,7 +251,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
return nil, err return nil, err
} }
// inject netrc if it's a trusted repo or a trusted clone-plugin // only inject netrc if it's a trusted repo or a trusted plugin
if c.securityTrustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) { if c.securityTrustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv { for k, v := range c.cloneEnv {
step.Environment[k] = v step.Environment[k] = v

View File

@ -176,13 +176,6 @@ func WithTrustedSecurity(trusted bool) Option {
} }
} }
// WithNetrcOnlyTrusted configures the compiler with the netrcOnlyTrusted repo option.
func WithNetrcOnlyTrusted(only bool) Option {
return func(compiler *Compiler) {
compiler.netrcOnlyTrusted = only
}
}
type ProxyOptions struct { type ProxyOptions struct {
NoProxy string NoProxy string
HTTPProxy string HTTPProxy string

View File

@ -94,7 +94,6 @@ func PostRepo(c *gin.Context) {
repo.RequireApproval = model.RequireApprovalForks repo.RequireApproval = model.RequireApprovalForks
repo.AllowPull = true repo.AllowPull = true
repo.AllowDeploy = false repo.AllowDeploy = false
repo.NetrcOnlyTrusted = true
repo.CancelPreviousPipelineEvents = server.Config.Pipeline.DefaultCancelPreviousPipelineEvents repo.CancelPreviousPipelineEvents = server.Config.Pipeline.DefaultCancelPreviousPipelineEvents
} }
repo.IsActive = true repo.IsActive = true
@ -275,8 +274,8 @@ func PatchRepo(c *gin.Context) {
if in.CancelPreviousPipelineEvents != nil { if in.CancelPreviousPipelineEvents != nil {
repo.CancelPreviousPipelineEvents = *in.CancelPreviousPipelineEvents repo.CancelPreviousPipelineEvents = *in.CancelPreviousPipelineEvents
} }
if in.NetrcOnlyTrusted != nil { if in.NetrcTrusted != nil {
repo.NetrcOnlyTrusted = *in.NetrcOnlyTrusted repo.NetrcTrustedPlugins = *in.NetrcTrusted
} }
if in.Visibility != nil { if in.Visibility != nil {
switch *in.Visibility { switch *in.Visibility {

View File

@ -71,7 +71,7 @@ type Repo struct {
Hash string `json:"-" xorm:"varchar(500) 'hash'"` Hash string `json:"-" xorm:"varchar(500) 'hash'"`
Perm *Perm `json:"-" xorm:"-"` Perm *Perm `json:"-" xorm:"-"`
CancelPreviousPipelineEvents []WebhookEvent `json:"cancel_previous_pipeline_events" xorm:"json 'cancel_previous_pipeline_events'"` CancelPreviousPipelineEvents []WebhookEvent `json:"cancel_previous_pipeline_events" xorm:"json 'cancel_previous_pipeline_events'"`
NetrcOnlyTrusted bool `json:"netrc_only_trusted" xorm:"NOT NULL DEFAULT true 'netrc_only_trusted'"` NetrcTrustedPlugins []string `json:"netrc_trusted" xorm:"json 'netrc_trusted'"`
} // @name Repo } // @name Repo
// TableName return database table name for xorm. // TableName return database table name for xorm.
@ -137,7 +137,7 @@ type RepoPatch struct {
AllowPull *bool `json:"allow_pr,omitempty"` AllowPull *bool `json:"allow_pr,omitempty"`
AllowDeploy *bool `json:"allow_deploy,omitempty"` AllowDeploy *bool `json:"allow_deploy,omitempty"`
CancelPreviousPipelineEvents *[]WebhookEvent `json:"cancel_previous_pipeline_events"` CancelPreviousPipelineEvents *[]WebhookEvent `json:"cancel_previous_pipeline_events"`
NetrcOnlyTrusted *bool `json:"netrc_only_trusted"` NetrcTrusted *[]string `json:"netrc_trusted"`
Trusted *TrustedConfigurationPatch `json:"trusted"` Trusted *TrustedConfigurationPatch `json:"trusted"`
} // @name RepoPatch } // @name RepoPatch

View File

@ -290,7 +290,7 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
b.Repo.IsSCMPrivate || server.Config.Pipeline.AuthenticatePublicRepos, b.Repo.IsSCMPrivate || server.Config.Pipeline.AuthenticatePublicRepos,
), ),
compiler.WithDefaultClonePlugin(server.Config.Pipeline.DefaultClonePlugin), compiler.WithDefaultClonePlugin(server.Config.Pipeline.DefaultClonePlugin),
compiler.WithTrustedClonePlugins(server.Config.Pipeline.TrustedClonePlugins), compiler.WithTrustedClonePlugins(append(b.Repo.NetrcTrustedPlugins, server.Config.Pipeline.TrustedClonePlugins...)),
compiler.WithRegistry(registries...), compiler.WithRegistry(registries...),
compiler.WithSecret(secrets...), compiler.WithSecret(secrets...),
compiler.WithPrefix( compiler.WithPrefix(
@ -304,7 +304,6 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
compiler.WithWorkspaceFromURL(compiler.DefaultWorkspaceBase, b.Repo.ForgeURL), compiler.WithWorkspaceFromURL(compiler.DefaultWorkspaceBase, b.Repo.ForgeURL),
compiler.WithMetadata(metadata), compiler.WithMetadata(metadata),
compiler.WithTrustedSecurity(b.Repo.Trusted.Security), compiler.WithTrustedSecurity(b.Repo.Trusted.Security),
compiler.WithNetrcOnlyTrusted(b.Repo.NetrcOnlyTrusted),
).Compile(parsed) ).Compile(parsed)
} }

View File

@ -0,0 +1,36 @@
// Copyright 2024 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
var removeRepoNetrcOnlyTrusted = xormigrate.Migration{
ID: "remove-repo-netrc-only-trusted",
MigrateSession: func(sess *xorm.Session) (err error) {
type repos struct {
NetrcOnlyTrusted string `xorm:"netrc_only_trusted"`
}
// ensure columns to drop exist
if err := sess.Sync(new(repos)); err != nil {
return err
}
return dropTableColumns(sess, "repos", "netrc_only_trusted")
},
}

View File

@ -48,6 +48,7 @@ var migrationTasks = []*xormigrate.Migration{
&splitTrusted, &splitTrusted,
&correctPotentialCorruptOrgsUsersRelation, &correctPotentialCorruptOrgsUsersRelation,
&gatedToRequireApproval, &gatedToRequireApproval,
&removeRepoNetrcOnlyTrusted,
} }
var allBeans = []any{ var allBeans = []any{

View File

@ -104,8 +104,8 @@
"desc": "Permit 'deployment' runs for successful pipelines. All users with with push permissions can trigger these, so use with caution." "desc": "Permit 'deployment' runs for successful pipelines. All users with with push permissions can trigger these, so use with caution."
}, },
"netrc_only_trusted": { "netrc_only_trusted": {
"netrc_only_trusted": "Only inject git credentials into trusted clone plugins", "netrc_only_trusted": "Custom trusted clone plugins",
"desc": "When enabled, git credentials are accessible only to trusted clone plugins specified in WOODPECKER_PLUGINS_TRUSTED_CLONE. Otherwise, custom clone plugins can use git credentials. This setting has no affect on non-clone steps." "desc": "Plugins listed here will get access to netrc credentials that can be used to clone repositories from the forge or push to it."
}, },
"trusted": { "trusted": {
"trusted": "Trusted", "trusted": "Trusted",

View File

@ -76,7 +76,7 @@ export interface Repo {
// Events that will cancel running pipelines before starting a new one // Events that will cancel running pipelines before starting a new one
cancel_previous_pipeline_events: string[]; cancel_previous_pipeline_events: string[];
netrc_only_trusted: boolean; netrc_trusted: string[];
} }
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
@ -104,7 +104,7 @@ export type RepoSettings = Pick<
| 'allow_pr' | 'allow_pr'
| 'allow_deploy' | 'allow_deploy'
| 'cancel_previous_pipeline_events' | 'cancel_previous_pipeline_events'
| 'netrc_only_trusted' | 'netrc_trusted'
>; >;
export interface RepoPermissions { export interface RepoPermissions {

View File

@ -15,11 +15,25 @@
:label="$t('repo.settings.general.allow_deploy.allow')" :label="$t('repo.settings.general.allow_deploy.allow')"
:description="$t('repo.settings.general.allow_deploy.desc')" :description="$t('repo.settings.general.allow_deploy.desc')"
/> />
<Checkbox </InputField>
v-model="repoSettings.netrc_only_trusted"
<InputField
v-slot="{ id }"
:label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')" :label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')"
:description="$t('repo.settings.general.netrc_only_trusted.desc')" docs-url="docs/usage/project-settings#custom-trusted-clone-plugins"
/> >
<span class="ml-1 mb-2 text-wp-text-alt-100">{{ $t('repo.settings.general.netrc_only_trusted.desc') }}</span>
<div class="flex flex-col gap-2">
<div v-for="image in repoSettings.netrc_trusted" :key="image" class="flex gap-2">
<TextField :id="id" :model-value="image" disabled />
<Button type="button" color="gray" start-icon="trash" @click="removeImage(image)" />
</div>
<div class="flex gap-2">
<TextField :id="id" v-model="newImage" @keydown.enter.prevent="addNewImage" />
<Button type="button" color="gray" start-icon="plus" @click="addNewImage" />
</div>
</div>
</InputField> </InputField>
<InputField <InputField
@ -178,7 +192,7 @@ function loadRepoSettings() {
allow_pr: repo.value.allow_pr, allow_pr: repo.value.allow_pr,
allow_deploy: repo.value.allow_deploy, allow_deploy: repo.value.allow_deploy,
cancel_previous_pipeline_events: repo.value.cancel_previous_pipeline_events || [], cancel_previous_pipeline_events: repo.value.cancel_previous_pipeline_events || [],
netrc_only_trusted: repo.value.netrc_only_trusted, netrc_trusted: repo.value.netrc_trusted || [],
}; };
} }
@ -236,4 +250,20 @@ const cancelPreviousPipelineEventsOptions: CheckboxOption[] = [
}, },
{ value: WebhookEvents.Deploy, text: i18n.t('repo.pipeline.event.deploy') }, { value: WebhookEvents.Deploy, text: i18n.t('repo.pipeline.event.deploy') },
]; ];
const newImage = ref('');
function addNewImage() {
if (!newImage.value) {
return;
}
repoSettings.value?.netrc_trusted.push(newImage.value);
newImage.value = '';
}
function removeImage(image: string) {
if (!repoSettings.value) {
throw new Error('Unexpected: repoSettings should be set');
}
repoSettings.value.netrc_trusted = repoSettings.value.netrc_trusted.filter((i) => i !== image);
}
</script> </script>