diff --git a/docs/docs/30-administration/100-external-configuration-api.md b/docs/docs/30-administration/100-external-configuration-api.md index 8f703b271a..7970ff46e5 100644 --- a/docs/docs/30-administration/100-external-configuration-api.md +++ b/docs/docs/30-administration/100-external-configuration-api.md @@ -7,6 +7,10 @@ Every request sent by Woodpecker is signed using a [http-signature](https://data A simplistic example configuration service can be found here: [https://github.com/woodpecker-ci/example-config-service](https://github.com/woodpecker-ci/example-config-service) +:::warning +You need to trust the external config service as it is getting secret information about the repository and pipeline and has the ability to change pipeline configs that could run malicious tasks. +::: + ## Config ```shell diff --git a/server/api/pipeline.go b/server/api/pipeline.go index 68477b0730..5a64d2db1d 100644 --- a/server/api/pipeline.go +++ b/server/api/pipeline.go @@ -453,7 +453,12 @@ func PostPipeline(c *gin.Context) { } } - newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs) + netrc, err := server.Config.Services.Forge.Netrc(user, repo) + if err != nil { + handlePipelineErr(c, err) + } + + newpipeline, err := pipeline.Restart(c, _store, pl, user, repo, envs, netrc) if err != nil { handlePipelineErr(c, err) } else { diff --git a/server/forge/configFetcher.go b/server/forge/configFetcher.go index 042934e210..d4024d059c 100644 --- a/server/forge/configFetcher.go +++ b/server/forge/configFetcher.go @@ -74,10 +74,15 @@ func (cf *configFetcher) Fetch(ctx context.Context) (files []*types.FileMeta, er defer cancel() // ok here as we only try http fetching once, returning on fail and success log.Trace().Msgf("ConfigFetch[%s]: getting config from external http service", cf.repo.FullName) - newConfigs, useOld, err := cf.configExtension.FetchConfig(fetchCtx, cf.repo, cf.pipeline, files) + netrc, err := cf.forge.Netrc(cf.user, cf.repo) if err != nil { - log.Error().Msg("Got error " + err.Error()) - return nil, fmt.Errorf("On Fetching config via http : %w", err) + return nil, fmt.Errorf("could not get Netrc data from forge: %w", err) + } + + newConfigs, useOld, err := cf.configExtension.FetchConfig(fetchCtx, cf.repo, cf.pipeline, files, netrc) + if err != nil { + log.Error().Err(err).Msg("could not fetch config via http") + return nil, fmt.Errorf("could not fetch config via http: %w", err) } if !useOld { @@ -109,7 +114,7 @@ func (cf *configFetcher) fetch(c context.Context, timeout time.Duration, config return nil, fmt.Errorf("user defined config '%s' not found: %w", config, err) } - log.Trace().Msgf("ConfigFetch[%s]: user did not defined own config, following default procedure", cf.repo.FullName) + log.Trace().Msgf("ConfigFetch[%s]: user did not define own config, following default procedure", cf.repo.FullName) // for the order see shared/constants/constants.go fileMeta, err := cf.getFirstAvailableConfig(ctx, constant.DefaultConfigOrder[:], false) if err == nil { diff --git a/server/forge/configFetcher_test.go b/server/forge/configFetcher_test.go index d3f8818966..2a414be455 100644 --- a/server/forge/configFetcher_test.go +++ b/server/forge/configFetcher_test.go @@ -519,6 +519,8 @@ func TestFetchFromConfigService(t *testing.T) { f.On("File", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("File not found")) f.On("Dir", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, fmt.Errorf("Directory not found")) + f.On("Netrc", mock.Anything, mock.Anything).Return(&model.Netrc{Machine: "mock", Login: "mock", Password: "mock"}, nil) + configFetcher := forge.NewConfigFetcher( f, time.Second*3, diff --git a/server/pipeline/restart.go b/server/pipeline/restart.go index 88af3b377e..573503f346 100644 --- a/server/pipeline/restart.go +++ b/server/pipeline/restart.go @@ -30,7 +30,7 @@ import ( ) // Restart a pipeline by creating a new one out of the old and start it -func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string) (*model.Pipeline, error) { +func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipeline, user *model.User, repo *model.Repo, envs map[string]string, netrc *model.Netrc) (*model.Pipeline, error) { switch lastPipeline.Status { case model.StatusDeclined, model.StatusBlocked: @@ -58,7 +58,7 @@ func Restart(ctx context.Context, store store.Store, lastPipeline *model.Pipelin currentFileMeta[i] = &forge_types.FileMeta{Name: cfg.Name, Data: cfg.Data} } - newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(ctx, repo, lastPipeline, currentFileMeta) + newConfig, useOld, err := server.Config.Services.ConfigService.FetchConfig(ctx, repo, lastPipeline, currentFileMeta, netrc) if err != nil { return nil, &ErrBadRequest{ Msg: fmt.Sprintf("On fetching external pipeline config: %s", err), diff --git a/server/plugins/config/extension.go b/server/plugins/config/extension.go index 317d58fcb0..dd94dc36ef 100644 --- a/server/plugins/config/extension.go +++ b/server/plugins/config/extension.go @@ -23,5 +23,5 @@ import ( type Extension interface { IsConfigured() bool - FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta) (configData []*forge_types.FileMeta, useOld bool, err error) + FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta, netrc *model.Netrc) (configData []*forge_types.FileMeta, useOld bool, err error) } diff --git a/server/plugins/config/http.go b/server/plugins/config/http.go index c61b06e667..b58c245a61 100644 --- a/server/plugins/config/http.go +++ b/server/plugins/config/http.go @@ -39,6 +39,7 @@ type requestStructure struct { Repo *model.Repo `json:"repo"` Pipeline *model.Pipeline `json:"pipeline"` Configuration []*config `json:"configs"` + Netrc *model.Netrc `json:"netrc"` } type responseStructure struct { @@ -53,14 +54,20 @@ func (cp *http) IsConfigured() bool { return cp.endpoint != "" } -func (cp *http) FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta) (configData []*forge_types.FileMeta, useOld bool, err error) { +func (cp *http) FetchConfig(ctx context.Context, repo *model.Repo, pipeline *model.Pipeline, currentFileMeta []*forge_types.FileMeta, netrc *model.Netrc) (configData []*forge_types.FileMeta, useOld bool, err error) { currentConfigs := make([]*config, len(currentFileMeta)) for i, pipe := range currentFileMeta { currentConfigs[i] = &config{Name: pipe.Name, Data: string(pipe.Data)} } response := new(responseStructure) - body := requestStructure{Repo: repo, Pipeline: pipeline, Configuration: currentConfigs} + body := requestStructure{ + Repo: repo, + Pipeline: pipeline, + Configuration: currentConfigs, + Netrc: netrc, + } + status, err := utils.Send(ctx, "POST", cp.endpoint, cp.privateKey, body, response) if err != nil && status != 204 { return nil, false, fmt.Errorf("Failed to fetch config via http (%d) %w", status, err)