diff --git a/docs/docs/30-administration/100-external-configuration-api.md b/docs/docs/30-administration/100-external-configuration-api.md index 8f703b271..7970ff46e 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 68477b073..5a64d2db1 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 042934e21..d4024d059 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 d3f881896..2a414be45 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 88af3b377..573503f34 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 317d58fcb..dd94dc36e 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 c61b06e66..b58c245a6 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)