From 29474fc7d91a6fac12e2cb811d0ffb800b976afe Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:37:31 +0200 Subject: [PATCH] Split repo trusted setting (#4025) --- cli/exec/exec.go | 6 +- cli/exec/flags.go | 16 +++- cli/exec/metadata.go | 4 +- cli/lint/lint.go | 6 +- cmd/server/docs/docs.go | 48 ++++++++++- docs/docs/20-usage/50-environment.md | 4 +- docs/docs/91-migrations.md | 1 + .../metadata/drone_compatibility_test.go | 3 + pipeline/frontend/metadata/environment.go | 28 ++++--- pipeline/frontend/metadata/types.go | 28 ++++--- pipeline/frontend/yaml/compiler/compiler.go | 36 ++++----- pipeline/frontend/yaml/compiler/option.go | 6 +- pipeline/frontend/yaml/linter/linter.go | 62 ++++++++------ pipeline/frontend/yaml/linter/linter_test.go | 6 +- pipeline/frontend/yaml/linter/option.go | 2 +- server/api/repo.go | 24 ++++-- server/model/repo.go | 80 +++++++++++-------- server/pipeline/stepbuilder/metadata.go | 6 +- server/pipeline/stepbuilder/metadata_test.go | 7 +- server/pipeline/stepbuilder/stepBuilder.go | 8 +- .../datastore/migration/017_split_trusted.go | 73 +++++++++++++++++ server/store/datastore/migration/common.go | 66 ++------------- server/store/datastore/migration/migration.go | 1 + web/src/assets/locales/en.json | 13 ++- .../components/repo/settings/GeneralTab.vue | 24 +++++- web/src/lib/api/types/repo.ts | 8 +- 26 files changed, 373 insertions(+), 193 deletions(-) create mode 100644 server/store/datastore/migration/017_split_trusted.go diff --git a/cli/exec/exec.go b/cli/exec/exec.go index afededad9..15ac5e4e8 100644 --- a/cli/exec/exec.go +++ b/cli/exec/exec.go @@ -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{{ diff --git a/cli/exec/flags.go b/cli/exec/flags.go index 3044628a7..d50376bdb 100644 --- a/cli/exec/flags.go +++ b/cli/exec/flags.go @@ -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"), diff --git a/cli/exec/metadata.go b/cli/exec/metadata.go index c0bc47c93..799e843a4 100644 --- a/cli/exec/metadata.go +++ b/cli/exec/metadata.go @@ -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) diff --git a/cli/lint/lint.go b/cli/lint/lint.go index 577c54c2b..56b17bae9 100644 --- a/cli/lint/lint.go +++ b/cli/lint/lint.go @@ -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}) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 8e00e7afc..4f13dacbe 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -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": { diff --git a/docs/docs/20-usage/50-environment.md b/docs/docs/20-usage/50-environment.md index 0aa0a988d..9f6bd75e7 100644 --- a/docs/docs/20-usage/50-environment.md +++ b/docs/docs/20-usage/50-environment.md @@ -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` | diff --git a/docs/docs/91-migrations.md b/docs/docs/91-migrations.md index 90d31cfd4..f1d9f4b6e 100644 --- a/docs/docs/91-migrations.md +++ b/docs/docs/91-migrations.md @@ -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 diff --git a/pipeline/frontend/metadata/drone_compatibility_test.go b/pipeline/frontend/metadata/drone_compatibility_test.go index e8bb86ab4..f38bdc5e2 100644 --- a/pipeline/frontend/metadata/drone_compatibility_test.go +++ b/pipeline/frontend/metadata/drone_compatibility_test.go @@ -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 diff --git a/pipeline/frontend/metadata/environment.go b/pipeline/frontend/metadata/environment.go index 1a018867f..25ac55d31 100644 --- a/pipeline/frontend/metadata/environment.go +++ b/pipeline/frontend/metadata/environment.go @@ -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, diff --git a/pipeline/frontend/metadata/types.go b/pipeline/frontend/metadata/types.go index 9d3529fd9..5fa56e85e 100644 --- a/pipeline/frontend/metadata/types.go +++ b/pipeline/frontend/metadata/types.go @@ -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"` + } ) diff --git a/pipeline/frontend/yaml/compiler/compiler.go b/pipeline/frontend/yaml/compiler/compiler.go index 68eb1495e..be2b59cbc 100644 --- a/pipeline/frontend/yaml/compiler/compiler.go +++ b/pipeline/frontend/yaml/compiler/compiler.go @@ -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 } diff --git a/pipeline/frontend/yaml/compiler/option.go b/pipeline/frontend/yaml/compiler/option.go index 96c18f8c2..3787bbd82 100644 --- a/pipeline/frontend/yaml/compiler/option.go +++ b/pipeline/frontend/yaml/compiler/option.go @@ -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 } } diff --git a/pipeline/frontend/yaml/linter/linter.go b/pipeline/frontend/yaml/linter/linter.go index a0eb71dea..71c4efe6f 100644 --- a/pipeline/frontend/yaml/linter/linter.go +++ b/pipeline/frontend/yaml/linter/linter.go @@ -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 { diff --git a/pipeline/frontend/yaml/linter/linter_test.go b/pipeline/frontend/yaml/linter/linter_test.go index 588888c79..943cd5593 100644 --- a/pipeline/frontend/yaml/linter/linter_test.go +++ b/pipeline/frontend/yaml/linter/linter_test.go @@ -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, diff --git a/pipeline/frontend/yaml/linter/option.go b/pipeline/frontend/yaml/linter/option.go index ca9a3afd9..5c297152a 100644 --- a/pipeline/frontend/yaml/linter/option.go +++ b/pipeline/frontend/yaml/linter/option.go @@ -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 } diff --git a/server/api/repo.go b/server/api/repo.go index b96eeaae6..da2dbbcd4 100644 --- a/server/api/repo.go +++ b/server/api/repo.go @@ -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 } diff --git a/server/model/repo.go b/server/model/repo.go index 36080f93e..3c18e17e0 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -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"` +} diff --git a/server/pipeline/stepbuilder/metadata.go b/server/pipeline/stepbuilder/metadata.go index 24ba90e96..52d79b4bb 100644 --- a/server/pipeline/stepbuilder/metadata.go +++ b/server/pipeline/stepbuilder/metadata.go @@ -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 { diff --git a/server/pipeline/stepbuilder/metadata_test.go b/server/pipeline/stepbuilder/metadata_test.go index 53cf388e6..1111a5603 100644 --- a/server/pipeline/stepbuilder/metadata_test.go +++ b/server/pipeline/stepbuilder/metadata_test.go @@ -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", }, diff --git a/server/pipeline/stepbuilder/stepBuilder.go b/server/pipeline/stepbuilder/stepBuilder.go index 8cf437606..8adc1d71b 100644 --- a/server/pipeline/stepbuilder/stepBuilder.go +++ b/server/pipeline/stepbuilder/stepBuilder.go @@ -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) } diff --git a/server/store/datastore/migration/017_split_trusted.go b/server/store/datastore/migration/017_split_trusted.go new file mode 100644 index 000000000..ef5e53b37 --- /dev/null +++ b/server/store/datastore/migration/017_split_trusted.go @@ -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") + }, +} diff --git a/server/store/datastore/migration/common.go b/server/store/datastore/migration/common.go index 0ba2ff0d5..5fd10c534 100644 --- a/server/store/datastore/migration/common.go +++ b/server/store/datastore/migration/common.go @@ -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) diff --git a/server/store/datastore/migration/migration.go b/server/store/datastore/migration/migration.go index 7ba1f435c..d12e21628 100644 --- a/server/store/datastore/migration/migration.go +++ b/server/store/datastore/migration/migration.go @@ -45,6 +45,7 @@ var migrationTasks = []*xormigrate.Migration{ &removeOldMigrationsOfV1, &addOrgAgents, &addCustomLabelsToAgent, + &splitTrusted, } var allBeans = []any{ diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 3a1ec3313..194c6afcd 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -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", diff --git a/web/src/components/repo/settings/GeneralTab.vue b/web/src/components/repo/settings/GeneralTab.vue index 83f484f3f..5c3460eaa 100644 --- a/web/src/components/repo/settings/GeneralTab.vue +++ b/web/src/components/repo/settings/GeneralTab.vue @@ -45,11 +45,27 @@ :label="$t('repo.settings.general.netrc_only_trusted.netrc_only_trusted')" :description="$t('repo.settings.general.netrc_only_trusted.desc')" /> + + + + + diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index a6b73576f..965d91d22 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -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; +}