Split repo trusted setting (#4025)

This commit is contained in:
qwerty287 2024-11-01 22:37:31 +02:00 committed by GitHub
parent 383bfbb6de
commit 29474fc7d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 373 additions and 193 deletions

View file

@ -207,7 +207,11 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
// lint the yaml file
err = linter.New(
linter.WithTrusted(true),
linter.WithTrusted(linter.TrustedConfiguration{
Security: c.Bool("repo-trusted-security"),
Network: c.Bool("repo-trusted-network"),
Volumes: c.Bool("repo-trusted-volumes"),
}),
linter.PrivilegedPlugins(privilegedPlugins),
linter.WithTrustedClonePlugins(constant.TrustedClonePlugins),
).Lint([]*linter.WorkflowConfig{{

View file

@ -185,9 +185,19 @@ var flags = []cli.Flag{
Usage: "Set the metadata environment variable \"CI_REPO_PRIVATE\".",
},
&cli.BoolFlag{
Sources: cli.EnvVars("CI_REPO_TRUSTED"),
Name: "repo-trusted",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED\".",
Sources: cli.EnvVars("CI_REPO_TRUSTED_NETWORK"),
Name: "repo-trusted-network",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED_NETWORK\".",
},
&cli.BoolFlag{
Sources: cli.EnvVars("CI_REPO_TRUSTED_VOLUMES"),
Name: "repo-trusted-volumes",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED_VOLUMES\".",
},
&cli.BoolFlag{
Sources: cli.EnvVars("CI_REPO_TRUSTED_SECURITY"),
Name: "repo-trusted-security",
Usage: "Set the metadata environment variable \"CI_REPO_TRUSTED_SECURITY\".",
},
&cli.IntFlag{
Sources: cli.EnvVars("CI_PIPELINE_NUMBER"),

View file

@ -83,7 +83,9 @@ func metadataFromContext(_ context.Context, c *cli.Command, axis matrix.Axis, w
metadataFileAndOverrideOrDefault(c, "repo-clone-url", func(s string) { m.Repo.CloneURL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-clone-ssh-url", func(s string) { m.Repo.CloneSSHURL = s }, c.String)
metadataFileAndOverrideOrDefault(c, "repo-private", func(b bool) { m.Repo.Private = b }, c.Bool)
metadataFileAndOverrideOrDefault(c, "repo-trusted", func(b bool) { m.Repo.Trusted = b }, c.Bool)
metadataFileAndOverrideOrDefault(c, "repo-trusted-network", func(b bool) { m.Repo.Trusted.Network = b }, c.Bool)
metadataFileAndOverrideOrDefault(c, "repo-trusted-security", func(b bool) { m.Repo.Trusted.Security = b }, c.Bool)
metadataFileAndOverrideOrDefault(c, "repo-trusted-volumes", func(b bool) { m.Repo.Trusted.Volumes = b }, c.Bool)
// Current Pipeline
metadataFileAndOverrideOrDefault(c, "pipeline-number", func(i int64) { m.Curr.Number = i }, c.Int)

View file

@ -110,7 +110,11 @@ func lintFile(_ context.Context, c *cli.Command, file string) error {
// TODO: lint multiple files at once to allow checks for sth like "depends_on" to work
err = linter.New(
linter.WithTrusted(true),
linter.WithTrusted(linter.TrustedConfiguration{
Network: true,
Volumes: true,
Security: true,
}),
linter.PrivilegedPlugins(c.StringSlice("plugins-privileged")),
linter.WithTrustedClonePlugins(c.StringSlice("plugins-trusted-clone")),
).Lint([]*linter.WorkflowConfig{config})

View file

@ -5099,7 +5099,7 @@ const docTemplate = `{
"type": "integer"
},
"trusted": {
"type": "boolean"
"$ref": "#/definitions/model.TrustedConfiguration"
},
"visibility": {
"$ref": "#/definitions/RepoVisibility"
@ -5134,7 +5134,7 @@ const docTemplate = `{
"type": "integer"
},
"trusted": {
"type": "boolean"
"$ref": "#/definitions/model.TrustedConfigurationPatch"
},
"visibility": {
"type": "string"
@ -5555,7 +5555,7 @@ const docTemplate = `{
"type": "string"
},
"trusted": {
"type": "boolean"
"$ref": "#/definitions/metadata.TrustedConfiguration"
}
}
},
@ -5590,6 +5590,20 @@ const docTemplate = `{
}
}
},
"metadata.TrustedConfiguration": {
"type": "object",
"properties": {
"network": {
"type": "boolean"
},
"security": {
"type": "boolean"
},
"volumes": {
"type": "boolean"
}
}
},
"metadata.Workflow": {
"type": "object",
"properties": {
@ -5628,6 +5642,34 @@ const docTemplate = `{
"ForgeTypeAddon"
]
},
"model.TrustedConfiguration": {
"type": "object",
"properties": {
"network": {
"type": "boolean"
},
"security": {
"type": "boolean"
},
"volumes": {
"type": "boolean"
}
}
},
"model.TrustedConfigurationPatch": {
"type": "object",
"properties": {
"network": {
"type": "boolean"
},
"security": {
"type": "boolean"
},
"volumes": {
"type": "boolean"
}
}
},
"model.Workflow": {
"type": "object",
"properties": {

View file

@ -62,7 +62,9 @@ This is the reference list of all environment variables available to your pipeli
| `CI_REPO_CLONE_SSH_URL` | repository SSH clone URL | `git@git.example.com:john-doe/my-repo.git` |
| `CI_REPO_DEFAULT_BRANCH` | repository default branch | `main` |
| `CI_REPO_PRIVATE` | repository is private | `true` |
| `CI_REPO_TRUSTED` | repository is trusted | `false` |
| `CI_REPO_TRUSTED_NETWORK` | repository has trusted network access | `false` |
| `CI_REPO_TRUSTED_VOLUMES` | repository has trusted volumes access | `false` |
| `CI_REPO_TRUSTED_SECURITY` | repository has trusted security access | `false` |
| | **Current Commit** | |
| `CI_COMMIT_SHA` | commit SHA | `eba09b46064473a1d345da7abf28b477468e8dbd` |
| `CI_COMMIT_REF` | commit ref | `refs/heads/main` |

View file

@ -32,6 +32,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Removed `WOODPECKER_WEBHOOK_HOST` in favor of `WOODPECKER_EXPERT_WEBHOOK_HOST`
- Migrated to rfc9421 for webhook signatures
- Renamed `start_time`, `end_time`, `created_at`, `started_at`, `finished_at` and `reviewed_at` JSON fields to `started`, `finished`, `created`, `started`, `finished`, `reviewed`
- JSON field `trusted` on repo model was changed from boolean to object
- Update all webhooks by pressing the "Repair all" button in the admin settings as the webhook token claims have changed
- Crons now use standard Linux syntax without seconds
- Replaced `configs` object by `netrc` in external configuration APIs

View file

@ -166,6 +166,9 @@ CI_REPO_PRIVATE=false
CI_REPO_REMOTE_ID=4
CI_REPO_SCM=git
CI_REPO_TRUSTED=false
CI_REPO_TRUSTED_NETWORK=false
CI_REPO_TRUSTED_VOLUMES=false
CI_REPO_TRUSTED_SECURITY=false
CI_REPO_URL=http://1.2.3.4:3000/test/woodpecker-test
CI_STEP_NAME=
CI_STEP_NUMBER=0

View file

@ -51,18 +51,22 @@ func (m *Metadata) Environ() map[string]string {
prevSourceBranch, prevTargetBranch := getSourceTargetBranches(m.Prev.Commit.Refspec)
params := map[string]string{
"CI": m.Sys.Name,
"CI_REPO": path.Join(m.Repo.Owner, m.Repo.Name),
"CI_REPO_NAME": m.Repo.Name,
"CI_REPO_OWNER": m.Repo.Owner,
"CI_REPO_REMOTE_ID": m.Repo.RemoteID,
"CI_REPO_SCM": m.Repo.SCM,
"CI_REPO_URL": m.Repo.ForgeURL,
"CI_REPO_CLONE_URL": m.Repo.CloneURL,
"CI_REPO_CLONE_SSH_URL": m.Repo.CloneSSHURL,
"CI_REPO_DEFAULT_BRANCH": m.Repo.Branch,
"CI_REPO_PRIVATE": strconv.FormatBool(m.Repo.Private),
"CI_REPO_TRUSTED": strconv.FormatBool(m.Repo.Trusted),
"CI": m.Sys.Name,
"CI_REPO": path.Join(m.Repo.Owner, m.Repo.Name),
"CI_REPO_NAME": m.Repo.Name,
"CI_REPO_OWNER": m.Repo.Owner,
"CI_REPO_REMOTE_ID": m.Repo.RemoteID,
"CI_REPO_SCM": m.Repo.SCM,
"CI_REPO_URL": m.Repo.ForgeURL,
"CI_REPO_CLONE_URL": m.Repo.CloneURL,
"CI_REPO_CLONE_SSH_URL": m.Repo.CloneSSHURL,
"CI_REPO_DEFAULT_BRANCH": m.Repo.Branch,
"CI_REPO_PRIVATE": strconv.FormatBool(m.Repo.Private),
"CI_REPO_TRUSTED_NETWORK": strconv.FormatBool(m.Repo.Trusted.Network),
"CI_REPO_TRUSTED_VOLUMES": strconv.FormatBool(m.Repo.Trusted.Volumes),
"CI_REPO_TRUSTED_SECURITY": strconv.FormatBool(m.Repo.Trusted.Security),
// Deprecated remove in 4.x
"CI_REPO_TRUSTED": strconv.FormatBool(m.Repo.Trusted.Security && m.Repo.Trusted.Network && m.Repo.Trusted.Volumes),
"CI_COMMIT_SHA": m.Curr.Commit.Sha,
"CI_COMMIT_REF": m.Curr.Commit.Ref,

View file

@ -29,17 +29,17 @@ type (
// Repo defines runtime metadata for a repository.
Repo struct {
ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
RemoteID string `json:"remote_id,omitempty"`
ForgeURL string `json:"forge_url,omitempty"`
SCM string `json:"scm,omitempty"`
CloneURL string `json:"clone_url,omitempty"`
CloneSSHURL string `json:"clone_url_ssh,omitempty"`
Private bool `json:"private,omitempty"`
Branch string `json:"default_branch,omitempty"`
Trusted bool `json:"trusted,omitempty"`
ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Owner string `json:"owner,omitempty"`
RemoteID string `json:"remote_id,omitempty"`
ForgeURL string `json:"forge_url,omitempty"`
SCM string `json:"scm,omitempty"`
CloneURL string `json:"clone_url,omitempty"`
CloneSSHURL string `json:"clone_url_ssh,omitempty"`
Private bool `json:"private,omitempty"`
Branch string `json:"default_branch,omitempty"`
Trusted TrustedConfiguration `json:"trusted,omitempty"`
}
// Pipeline defines runtime metadata for a pipeline.
@ -113,4 +113,10 @@ type (
// URL returns the root url of a configured forge
URL() string
}
TrustedConfiguration struct {
Network bool `json:"network,omitempty"`
Volumes bool `json:"volumes,omitempty"`
Security bool `json:"security,omitempty"`
}
)

View file

@ -83,22 +83,22 @@ func (s *Secret) Match(event string) bool {
// Compiler compiles the yaml.
type Compiler struct {
local bool
escalated []string
prefix string
volumes []string
networks []string
env map[string]string
cloneEnv map[string]string
workspaceBase string
workspacePath string
metadata metadata.Metadata
registries []Registry
secrets map[string]Secret
defaultClonePlugin string
trustedClonePlugins []string
trustedPipeline bool
netrcOnlyTrusted bool
local bool
escalated []string
prefix string
volumes []string
networks []string
env map[string]string
cloneEnv map[string]string
workspaceBase string
workspacePath string
metadata metadata.Metadata
registries []Registry
secrets map[string]Secret
defaultClonePlugin string
trustedClonePlugins []string
securityTrustedPipeline bool
netrcOnlyTrusted bool
}
// New creates a new Compiler with options.
@ -196,7 +196,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
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
if !c.netrcOnlyTrusted || c.securityTrustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv {
step.Environment[k] = v
}
@ -253,7 +253,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
}
// inject netrc if it's a trusted repo or a trusted clone-plugin
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
if c.securityTrustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
for k, v := range c.cloneEnv {
step.Environment[k] = v
}

View file

@ -169,10 +169,10 @@ func WithTrustedClonePlugins(images []string) Option {
}
}
// WithTrusted configures the compiler with the trusted repo option.
func WithTrusted(trusted bool) Option {
// WithTrustedSecurity configures the compiler with the trusted repo option.
func WithTrustedSecurity(trusted bool) Option {
return func(compiler *Compiler) {
compiler.trustedPipeline = trusted
compiler.securityTrustedPipeline = trusted
}
}

View file

@ -30,11 +30,17 @@ import (
// A Linter lints a pipeline configuration.
type Linter struct {
trusted bool
trusted TrustedConfiguration
privilegedPlugins *[]string
trustedClonePlugins *[]string
}
type TrustedConfiguration struct {
Network bool
Volumes bool
Security bool
}
// New creates a new Linter with options.
func New(opts ...Option) *Linter {
linter := new(Linter)
@ -143,10 +149,8 @@ func (l *Linter) lintContainers(config *WorkflowConfig, area string) error {
if err := l.lintImage(config, container, area); err != nil {
linterErr = multierr.Append(linterErr, err)
}
if !l.trusted {
if err := l.lintTrusted(config, container, area); err != nil {
linterErr = multierr.Append(linterErr, err)
}
if err := l.lintTrusted(config, container, area); err != nil {
linterErr = multierr.Append(linterErr, err)
}
if err := l.lintSettings(config, container, area); err != nil {
linterErr = multierr.Append(linterErr, err)
@ -204,29 +208,35 @@ func (l *Linter) lintSettings(config *WorkflowConfig, c *types.Container, field
func (l *Linter) lintTrusted(config *WorkflowConfig, c *types.Container, area string) error {
yamlPath := fmt.Sprintf("%s.%s", area, c.Name)
errors := []string{}
if c.Privileged {
errors = append(errors, "Insufficient privileges to use privileged mode")
if !l.trusted.Security {
if c.Privileged {
errors = append(errors, "Insufficient privileges to use privileged mode")
}
}
if len(c.DNS) != 0 {
errors = append(errors, "Insufficient privileges to use custom dns")
if !l.trusted.Network {
if len(c.DNS) != 0 {
errors = append(errors, "Insufficient privileges to use custom dns")
}
if len(c.DNSSearch) != 0 {
errors = append(errors, "Insufficient privileges to use dns_search")
}
if len(c.ExtraHosts) != 0 {
errors = append(errors, "Insufficient privileges to use extra_hosts")
}
if len(c.NetworkMode) != 0 {
errors = append(errors, "Insufficient privileges to use network_mode")
}
}
if len(c.DNSSearch) != 0 {
errors = append(errors, "Insufficient privileges to use dns_search")
}
if len(c.Devices) != 0 {
errors = append(errors, "Insufficient privileges to use devices")
}
if len(c.ExtraHosts) != 0 {
errors = append(errors, "Insufficient privileges to use extra_hosts")
}
if len(c.NetworkMode) != 0 {
errors = append(errors, "Insufficient privileges to use network_mode")
}
if len(c.Volumes.Volumes) != 0 {
errors = append(errors, "Insufficient privileges to use volumes")
}
if len(c.Tmpfs) != 0 {
errors = append(errors, "Insufficient privileges to use tmpfs")
if !l.trusted.Volumes {
if len(c.Devices) != 0 {
errors = append(errors, "Insufficient privileges to use devices")
}
if len(c.Volumes.Volumes) != 0 {
errors = append(errors, "Insufficient privileges to use volumes")
}
if len(c.Tmpfs) != 0 {
errors = append(errors, "Insufficient privileges to use tmpfs")
}
}
if len(errors) > 0 {

View file

@ -94,7 +94,11 @@ steps:
conf, err := yaml.ParseString(testd.Data)
assert.NoError(t, err)
assert.NoError(t, linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{{
assert.NoError(t, linter.New(linter.WithTrusted(linter.TrustedConfiguration{
Network: true,
Volumes: true,
Security: true,
})).Lint([]*linter.WorkflowConfig{{
File: testd.Title,
RawConfig: testd.Data,
Workflow: conf,

View file

@ -18,7 +18,7 @@ package linter
type Option func(*Linter)
// WithTrusted adds the trusted option to the linter.
func WithTrusted(trusted bool) Option {
func WithTrusted(trusted TrustedConfiguration) Option {
return func(linter *Linter) {
linter.trusted = trusted
}

View file

@ -224,10 +224,23 @@ func PatchRepo(c *gin.Context) {
c.String(http.StatusForbidden, fmt.Sprintf("Timeout is not allowed to be higher than max timeout (%d min)", server.Config.Pipeline.MaxTimeout))
return
}
if in.IsTrusted != nil && *in.IsTrusted != repo.IsTrusted && !user.Admin {
log.Trace().Msgf("user '%s' wants to make repo trusted without being an instance admin", user.Login)
c.String(http.StatusForbidden, "Insufficient privileges")
return
if in.Trusted != nil {
if (*in.Trusted.Network != repo.Trusted.Network || *in.Trusted.Volumes != repo.Trusted.Volumes || *in.Trusted.Security != repo.Trusted.Security) && !user.Admin {
log.Trace().Msgf("user '%s' wants to change trusted without being an instance admin", user.Login)
c.String(http.StatusForbidden, "Insufficient privileges")
return
}
if in.Trusted.Network != nil {
repo.Trusted.Network = *in.Trusted.Network
}
if in.Trusted.Security != nil {
repo.Trusted.Security = *in.Trusted.Security
}
if in.Trusted.Volumes != nil {
repo.Trusted.Volumes = *in.Trusted.Volumes
}
}
if in.AllowPull != nil {
@ -239,9 +252,6 @@ func PatchRepo(c *gin.Context) {
if in.IsGated != nil {
repo.IsGated = *in.IsGated
}
if in.IsTrusted != nil {
repo.IsTrusted = *in.IsTrusted
}
if in.Timeout != nil {
repo.Timeout = *in.Timeout
}

View file

@ -26,31 +26,31 @@ type Repo struct {
UserID int64 `json:"-" xorm:"INDEX 'user_id'"`
ForgeID int64 `json:"forge_id,omitempty" xorm:"forge_id"`
// ForgeRemoteID is the unique identifier for the repository on the forge.
ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id"`
OrgID int64 `json:"org_id" xorm:"INDEX 'org_id'"`
Owner string `json:"owner" xorm:"UNIQUE(name) 'owner'"`
Name string `json:"name" xorm:"UNIQUE(name) 'name'"`
FullName string `json:"full_name" xorm:"UNIQUE 'full_name'"`
Avatar string `json:"avatar_url,omitempty" xorm:"varchar(500) 'avatar'"`
ForgeURL string `json:"forge_url,omitempty" xorm:"varchar(1000) 'forge_url'"`
Clone string `json:"clone_url,omitempty" xorm:"varchar(1000) 'clone'"`
CloneSSH string `json:"clone_url_ssh" xorm:"varchar(1000) 'clone_ssh'"`
Branch string `json:"default_branch,omitempty" xorm:"varchar(500) 'branch'"`
SCMKind SCMKind `json:"scm,omitempty" xorm:"varchar(50) 'scm'"`
PREnabled bool `json:"pr_enabled" xorm:"DEFAULT TRUE 'pr_enabled'"`
Timeout int64 `json:"timeout,omitempty" xorm:"timeout"`
Visibility RepoVisibility `json:"visibility" xorm:"varchar(10) 'visibility'"`
IsSCMPrivate bool `json:"private" xorm:"private"`
IsTrusted bool `json:"trusted" xorm:"trusted"`
IsGated bool `json:"gated" xorm:"gated"`
IsActive bool `json:"active" xorm:"active"`
AllowPull bool `json:"allow_pr" xorm:"allow_pr"`
AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"`
Config string `json:"config_file" xorm:"varchar(500) 'config_path'"`
Hash string `json:"-" xorm:"varchar(500) 'hash'"`
Perm *Perm `json:"-" xorm:"-"`
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'"`
ForgeRemoteID ForgeRemoteID `json:"forge_remote_id" xorm:"forge_remote_id"`
OrgID int64 `json:"org_id" xorm:"INDEX 'org_id'"`
Owner string `json:"owner" xorm:"UNIQUE(name) 'owner'"`
Name string `json:"name" xorm:"UNIQUE(name) 'name'"`
FullName string `json:"full_name" xorm:"UNIQUE 'full_name'"`
Avatar string `json:"avatar_url,omitempty" xorm:"varchar(500) 'avatar'"`
ForgeURL string `json:"forge_url,omitempty" xorm:"varchar(1000) 'forge_url'"`
Clone string `json:"clone_url,omitempty" xorm:"varchar(1000) 'clone'"`
CloneSSH string `json:"clone_url_ssh" xorm:"varchar(1000) 'clone_ssh'"`
Branch string `json:"default_branch,omitempty" xorm:"varchar(500) 'branch'"`
SCMKind SCMKind `json:"scm,omitempty" xorm:"varchar(50) 'scm'"`
PREnabled bool `json:"pr_enabled" xorm:"DEFAULT TRUE 'pr_enabled'"`
Timeout int64 `json:"timeout,omitempty" xorm:"timeout"`
Visibility RepoVisibility `json:"visibility" xorm:"varchar(10) 'visibility'"`
IsSCMPrivate bool `json:"private" xorm:"private"`
Trusted TrustedConfiguration `json:"trusted" xorm:"json 'trusted'"`
IsGated bool `json:"gated" xorm:"gated"`
IsActive bool `json:"active" xorm:"active"`
AllowPull bool `json:"allow_pr" xorm:"allow_pr"`
AllowDeploy bool `json:"allow_deploy" xorm:"allow_deploy"`
Config string `json:"config_file" xorm:"varchar(500) 'config_path'"`
Hash string `json:"-" xorm:"varchar(500) 'hash'"`
Perm *Perm `json:"-" xorm:"-"`
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'"`
} // @name Repo
// TableName return database table name for xorm.
@ -108,15 +108,15 @@ func (r *Repo) Update(from *Repo) {
// RepoPatch represents a repository patch object.
type RepoPatch struct {
Config *string `json:"config_file,omitempty"`
IsTrusted *bool `json:"trusted,omitempty"`
IsGated *bool `json:"gated,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
Visibility *string `json:"visibility,omitempty"`
AllowPull *bool `json:"allow_pr,omitempty"`
AllowDeploy *bool `json:"allow_deploy,omitempty"`
CancelPreviousPipelineEvents *[]WebhookEvent `json:"cancel_previous_pipeline_events"`
NetrcOnlyTrusted *bool `json:"netrc_only_trusted"`
Config *string `json:"config_file,omitempty"`
IsGated *bool `json:"gated,omitempty"`
Timeout *int64 `json:"timeout,omitempty"`
Visibility *string `json:"visibility,omitempty"`
AllowPull *bool `json:"allow_pr,omitempty"`
AllowDeploy *bool `json:"allow_deploy,omitempty"`
CancelPreviousPipelineEvents *[]WebhookEvent `json:"cancel_previous_pipeline_events"`
NetrcOnlyTrusted *bool `json:"netrc_only_trusted"`
Trusted *TrustedConfigurationPatch `json:"trusted"`
} // @name RepoPatch
type ForgeRemoteID string
@ -124,3 +124,15 @@ type ForgeRemoteID string
func (r ForgeRemoteID) IsValid() bool {
return r != "" && r != "0"
}
type TrustedConfiguration struct {
Network bool `json:"network"`
Volumes bool `json:"volumes"`
Security bool `json:"security"`
}
type TrustedConfigurationPatch struct {
Network *bool `json:"network"`
Volumes *bool `json:"volumes"`
Security *bool `json:"security"`
}

View file

@ -53,7 +53,11 @@ func MetadataFromStruct(forge metadata.ServerForge, repo *model.Repo, pipeline,
CloneSSHURL: repo.CloneSSH,
Private: repo.IsSCMPrivate,
Branch: repo.Branch,
Trusted: repo.IsTrusted,
Trusted: metadata.TrustedConfiguration{
Network: repo.Trusted.Network,
Volumes: repo.Trusted.Volumes,
Security: repo.Trusted.Security,
},
}
if idx := strings.LastIndex(repo.FullName, "/"); idx != -1 {

View file

@ -53,7 +53,8 @@ func TestMetadataFromStruct(t *testing.T) {
"CI_PREV_COMMIT_MESSAGE": "", "CI_PREV_COMMIT_REF": "", "CI_PREV_COMMIT_REFSPEC": "", "CI_PREV_COMMIT_SHA": "", "CI_PREV_COMMIT_URL": "", "CI_PREV_PIPELINE_CREATED": "0",
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_DEPLOY_TASK": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_NUMBER": "0", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_STARTED": "0", "CI_PREV_PIPELINE_STATUS": "", "CI_PREV_PIPELINE_URL": "/repos/0/pipeline/0", "CI_PREV_PIPELINE_FORGE_URL": "", "CI_REPO": "", "CI_REPO_CLONE_URL": "", "CI_REPO_CLONE_SSH_URL": "", "CI_REPO_DEFAULT_BRANCH": "", "CI_REPO_REMOTE_ID": "",
"CI_REPO_NAME": "", "CI_REPO_OWNER": "", "CI_REPO_PRIVATE": "false", "CI_REPO_SCM": "", "CI_REPO_TRUSTED": "false", "CI_REPO_URL": "",
"CI_REPO_NAME": "", "CI_REPO_OWNER": "", "CI_REPO_PRIVATE": "false", "CI_REPO_SCM": "", "CI_REPO_TRUSTED": "false", "CI_REPO_TRUSTED_NETWORK": "false",
"CI_REPO_TRUSTED_VOLUMES": "false", "CI_REPO_TRUSTED_SECURITY": "false", "CI_REPO_URL": "",
"CI_STEP_NAME": "", "CI_STEP_NUMBER": "0", "CI_STEP_STARTED": "", "CI_STEP_URL": "/repos/0/pipeline/0", "CI_SYSTEM_HOST": "", "CI_SYSTEM_NAME": "woodpecker",
"CI_SYSTEM_PLATFORM": "", "CI_SYSTEM_URL": "", "CI_SYSTEM_VERSION": "", "CI_WORKFLOW_NAME": "", "CI_WORKFLOW_NUMBER": "0",
},
@ -89,7 +90,9 @@ func TestMetadataFromStruct(t *testing.T) {
"CI_PREV_PIPELINE_DEPLOY_TARGET": "", "CI_PREV_PIPELINE_DEPLOY_TASK": "", "CI_PREV_PIPELINE_EVENT": "", "CI_PREV_PIPELINE_FINISHED": "0", "CI_PREV_PIPELINE_NUMBER": "2", "CI_PREV_PIPELINE_PARENT": "0",
"CI_PREV_PIPELINE_STARTED": "0", "CI_PREV_PIPELINE_STATUS": "", "CI_PREV_PIPELINE_URL": "https://example.com/repos/0/pipeline/2", "CI_PREV_PIPELINE_FORGE_URL": "", "CI_REPO": "testUser/testRepo", "CI_REPO_CLONE_URL": "https://gitea.com/testUser/testRepo.git", "CI_REPO_CLONE_SSH_URL": "git@gitea.com:testUser/testRepo.git",
"CI_REPO_DEFAULT_BRANCH": "main", "CI_REPO_NAME": "testRepo", "CI_REPO_OWNER": "testUser", "CI_REPO_PRIVATE": "true", "CI_REPO_REMOTE_ID": "",
"CI_REPO_SCM": "git", "CI_REPO_TRUSTED": "false", "CI_REPO_URL": "https://gitea.com/testUser/testRepo",
"CI_REPO_SCM": "git", "CI_REPO_TRUSTED": "false", "CI_REPO_TRUSTED_NETWORK": "false",
"CI_REPO_TRUSTED_VOLUMES": "false", "CI_REPO_TRUSTED_SECURITY": "false",
"CI_REPO_URL": "https://gitea.com/testUser/testRepo",
"CI_STEP_NAME": "", "CI_STEP_NUMBER": "0", "CI_STEP_STARTED": "", "CI_STEP_URL": "https://example.com/repos/0/pipeline/3", "CI_SYSTEM_HOST": "example.com",
"CI_SYSTEM_NAME": "woodpecker", "CI_SYSTEM_PLATFORM": "", "CI_SYSTEM_URL": "https://example.com", "CI_SYSTEM_VERSION": "", "CI_WORKFLOW_NAME": "hello", "CI_WORKFLOW_NUMBER": "0",
},

View file

@ -141,7 +141,11 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A
// lint pipeline
errorsAndWarnings = multierr.Append(errorsAndWarnings, linter.New(
linter.WithTrusted(b.Repo.IsTrusted),
linter.WithTrusted(linter.TrustedConfiguration{
Network: b.Repo.Trusted.Network,
Volumes: b.Repo.Trusted.Volumes,
Security: b.Repo.Trusted.Security,
}),
linter.PrivilegedPlugins(server.Config.Pipeline.PrivilegedPlugins),
linter.WithTrustedClonePlugins(server.Config.Pipeline.TrustedClonePlugins),
).Lint([]*linter.WorkflowConfig{{
@ -295,7 +299,7 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
compiler.WithProxy(b.ProxyOpts),
compiler.WithWorkspaceFromURL(compiler.DefaultWorkspaceBase, b.Repo.ForgeURL),
compiler.WithMetadata(metadata),
compiler.WithTrusted(b.Repo.IsTrusted),
compiler.WithTrustedSecurity(b.Repo.Trusted.Security),
compiler.WithNetrcOnlyTrusted(b.Repo.NetrcOnlyTrusted),
).Compile(parsed)
}

View file

@ -0,0 +1,73 @@
// 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 (
"fmt"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
"go.woodpecker-ci.org/woodpecker/v2/server/model"
)
type repoV035 struct {
ID int64 `xorm:"pk autoincr 'id'"`
IsTrusted bool `xorm:"'trusted'"`
Trusted model.TrustedConfiguration `xorm:"json 'trusted_conf'"`
}
func (repoV035) TableName() string {
return "repos"
}
var splitTrusted = xormigrate.Migration{
ID: "split-trusted",
MigrateSession: func(sess *xorm.Session) error {
if err := sess.Sync(new(repoV035)); err != nil {
return fmt.Errorf("sync new models failed: %w", err)
}
if _, err := sess.Where("trusted = ?", false).Cols("trusted_conf").Update(&repoV035{
Trusted: model.TrustedConfiguration{
Network: false,
Security: false,
Volumes: false,
},
}); err != nil {
return err
}
if _, err := sess.Where("trusted = ?", true).Cols("trusted_conf").Update(&repoV035{
Trusted: model.TrustedConfiguration{
Network: true,
Security: true,
Volumes: true,
},
}); err != nil {
return err
}
if err := dropTableColumns(sess, "repos", "trusted"); err != nil {
return err
}
if err := sess.Commit(); err != nil {
return err
}
return renameColumn(sess, "repos", "trusted_conf", "trusted")
},
}

View file

@ -76,66 +76,14 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
}
}
// Here we need to get the columns from the original table
sql := fmt.Sprintf("SELECT sql FROM sqlite_master WHERE tbl_name='%s' and type='table'", tableName)
res, err := sess.Query(sql)
if err != nil {
return err
}
tableSQL := normalizeSQLiteTableSchema(string(res[0]["sql"]))
// Separate out the column definitions
sqlIndex := strings.Index(tableSQL, "(")
if sqlIndex < 0 {
return fmt.Errorf("could not separate column definitions")
}
tableSQL = tableSQL[sqlIndex:]
// Remove the required columnNames
tableSQL = removeColumnFromSQLITETableSchema(tableSQL, columnNames...)
// Ensure the query is ended properly
tableSQL = strings.TrimSpace(tableSQL)
if tableSQL[len(tableSQL)-1] != ')' {
if tableSQL[len(tableSQL)-1] == ',' {
tableSQL = tableSQL[:len(tableSQL)-1]
// Now drop the columns
for _, columnName := range columnNames {
_, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN `%s`;", tableName, columnName))
if err != nil {
return fmt.Errorf("table `%s`, drop column %v: %w", tableName, columnName, err)
}
tableSQL += ")"
}
// Find all the columns in the table
var columns []string
for _, rawColumn := range strings.Split(strings.ReplaceAll(tableSQL[1:len(tableSQL)-1], ", ", ",\n"), "\n") {
if strings.ContainsAny(rawColumn, "()") {
continue
}
rawColumn = strings.TrimSpace(rawColumn)
columns = append(columns,
strings.ReplaceAll(rawColumn[0:strings.Index(rawColumn, " ")], "`", ""),
)
}
tableSQL = fmt.Sprintf("CREATE TABLE `new_%s_new` ", tableName) + tableSQL
if _, err := sess.Exec(tableSQL); err != nil {
return err
}
// Now restore the data
columnsSeparated := strings.Join(columns, ",")
insertSQL := fmt.Sprintf("INSERT INTO `new_%s_new` (%s) SELECT %s FROM %s", tableName, columnsSeparated, columnsSeparated, tableName)
if _, err := sess.Exec(insertSQL); err != nil {
return err
}
// Now drop the old table
if _, err := sess.Exec(fmt.Sprintf("DROP TABLE `%s`", tableName)); err != nil {
return err
}
// Rename the table
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `new_%s_new` RENAME TO `%s`", tableName, tableName)); err != nil {
return err
}
case schemas.POSTGRES:
cols := ""
for _, col := range columnNames {
@ -145,7 +93,7 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
cols += "DROP COLUMN `" + col + "` CASCADE"
}
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err)
return fmt.Errorf("table `%s`, drop columns %v: %w", tableName, columnNames, err)
}
case schemas.MYSQL:
// Drop indexes on columns first
@ -173,7 +121,7 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
cols += "DROP COLUMN `" + col + "`"
}
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err)
return fmt.Errorf("table `%s`, drop columns %v: %w", tableName, columnNames, err)
}
default:
return fmt.Errorf("dialect '%s' not supported", dialect)

View file

@ -45,6 +45,7 @@ var migrationTasks = []*xormigrate.Migration{
&removeOldMigrationsOfV1,
&addOrgAgents,
&addCustomLabelsToAgent,
&splitTrusted,
}
var allBeans = []any{

View file

@ -98,7 +98,18 @@
},
"trusted": {
"trusted": "Trusted",
"desc": "Underlying pipeline containers get access to escalated capabilities (like mounting volumes)."
"network": {
"network": "Network",
"desc": "Underlying pipeline containers get access to network privileges like changing DNS."
},
"volumes": {
"volumes": "Volumes",
"desc": "Underlying pipeline containers get access to volume privileges."
},
"security": {
"security": "Security",
"desc": "Underlying pipeline containers get access to security privileges."
}
},
"visibility": {
"visibility": "Project visibility",

View file

@ -45,11 +45,27 @@
:label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')"
:description="$t('repo.settings.general.netrc_only_trusted.desc')"
/>
</InputField>
<InputField
v-if="user?.admin"
docs-url="docs/usage/project-settings#project-settings-1"
:label="$t('repo.settings.general.trusted.trusted')"
>
<Checkbox
v-if="user?.admin"
v-model="repoSettings.trusted"
:label="$t('repo.settings.general.trusted.trusted')"
:description="$t('repo.settings.general.trusted.desc')"
v-model="repoSettings.trusted.network"
:label="$t('repo.settings.general.trusted.network.network')"
:description="$t('repo.settings.general.trusted.network.desc')"
/>
<Checkbox
v-model="repoSettings.trusted.volumes"
:label="$t('repo.settings.general.trusted.volumes.volumes')"
:description="$t('repo.settings.general.trusted.volumes.desc')"
/>
<Checkbox
v-model="repoSettings.trusted.security"
:label="$t('repo.settings.general.trusted.security.security')"
:description="$t('repo.settings.general.trusted.security.desc')"
/>
</InputField>

View file

@ -50,7 +50,7 @@ export interface Repo {
// Whether the repository has trusted access for pipelines.
// If the repository is trusted then the host network can be used and
// volumes can be created.
trusted: boolean;
trusted: RepoTrusted;
// x-dart-type: Duration
// The amount of time in minutes before the pipeline is killed.
@ -102,3 +102,9 @@ export interface RepoPermissions {
admin: boolean;
synced: number;
}
export interface RepoTrusted {
network: boolean;
volumes: boolean;
security: boolean;
}