diff --git a/cli/cdsctl/experimental_project_repository.go b/cli/cdsctl/experimental_project_repository.go index bcf4c87a0e..6c4fcb3389 100644 --- a/cli/cdsctl/experimental_project_repository.go +++ b/cli/cdsctl/experimental_project_repository.go @@ -19,6 +19,7 @@ func projectRepository() *cobra.Command { cli.NewListCommand(projectRepositoryListCmd, projectRepositoryListFunc, nil, withAllCommandModifiers()...), cli.NewDeleteCommand(projectRepositoryDeleteCmd, projectRepositoryDeleteFunc, nil, withAllCommandModifiers()...), cli.NewCommand(projectRepositoryAddCmd, projectRepositoryAddFunc, nil, withAllCommandModifiers()...), + cli.NewGetCommand(projectRepositoryHookSecretRegenCmd, projectRepositoryHookSecretRegenFunc, nil, withAllCommandModifiers()...), }) } @@ -118,3 +119,19 @@ var projectRepositoryDeleteCmd = cli.Command{ func projectRepositoryDeleteFunc(v cli.Values) error { return client.ProjectRepositoryDelete(context.Background(), v.GetString(_ProjectKey), v.GetString("vcs-name"), v.GetString("repository-name")) } + +var projectRepositoryHookSecretRegenCmd = cli.Command{ + Name: "hook-regen", + Short: "Regenerate hook secret for webhook signature", + Ctx: []cli.Arg{ + {Name: _ProjectKey}, + }, + Args: []cli.Arg{ + {Name: "vcs-name"}, + {Name: "repository-name"}, + }, +} + +func projectRepositoryHookSecretRegenFunc(v cli.Values) (interface{}, error) { + return client.ProjectRepositoryHookRegenSecret(context.Background(), v.GetString(_ProjectKey), v.GetString("vcs-name"), v.GetString("repository-name")) +} diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go index ed71bf0075..4afb643158 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -434,12 +434,15 @@ func (api *API) InitRouter() { r.Handle("/v2/repository/analyze", Scope(sdk.AuthConsumerScopeHooks), r.POSTv2(api.postRepositoryAnalysisHandler)) r.Handle("/v2/project/repositories", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getAllRepositoriesHandler)) + r.Handle("/v2/project/repositories/{repositoryIdentifier}/hook", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getRepositoryHookHandler)) + r.Handle("/v2/project/{projectKey}/vcs", nil, r.POSTv2(api.postVCSProjectHandler), r.GETv2(api.getVCSProjectAllHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}", nil, r.PUTv2(api.putVCSProjectHandler), r.DELETEv2(api.deleteVCSProjectHandler), r.GETv2(api.getVCSProjectHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository", nil, r.POSTv2(api.postProjectRepositoryHandler), r.GETv2(api.getVCSProjectRepositoryAllHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}", nil, r.DELETEv2(api.deleteProjectRepositoryHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/analysis", nil, r.GETv2(api.getProjectRepositoryAnalysesHandler)) r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/analysis/{analysisID}", nil, r.GETv2(api.getProjectRepositoryAnalysisHandler)) + r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/hook/regen", nil, r.POSTv2(api.postRepositoryHookRegenKeyHandler)) r.Handle("/v2/user/gpgkey/{gpgKeyID}", Scope(sdk.AuthConsumerScopeUser), r.GETv2(api.getUserGPGKeyHandler)) r.Handle("/v2/user/{user}/gpgkey", Scope(sdk.AuthConsumerScopeUser), r.GETv2(api.getUserGPGKeysHandler), r.POSTv2(api.postUserGPGGKeyHandler)) diff --git a/engine/api/operation/operation.go b/engine/api/operation/operation.go index 987ee869a5..97547164a9 100644 --- a/engine/api/operation/operation.go +++ b/engine/api/operation/operation.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/ovh/cds/sdk/telemetry" "net/http" "time" @@ -100,6 +101,8 @@ func PushOperationUpdate(ctx context.Context, db gorpmapper.SqlExecutorWithTx, s // PostRepositoryOperation creates a new repository operation func PostRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, prj sdk.Project, ope *sdk.Operation, multipartData *services.MultiPartData) error { + ctx, next := telemetry.Span(ctx, "operation.PostRepositoryOperation") + defer next() srvs, err := services.LoadAllByType(ctx, db, sdk.TypeRepositories) if err != nil { return sdk.WrapError(err, "Unable to found repositories service") @@ -151,6 +154,8 @@ func GetRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, uuid strin // Poll repository operation for given uuid. func Poll(ctx context.Context, db gorp.SqlExecutor, operationUUID string) (*sdk.Operation, error) { + ctx, next := telemetry.Span(ctx, "operation.Poll") + defer next() f := func() (*sdk.Operation, error) { ope, err := GetRepositoryOperation(ctx, db, operationUUID) if err != nil { diff --git a/engine/api/rbac/dao_rbac_project_key.go b/engine/api/rbac/dao_rbac_project_key.go index 4bd4397d7c..e891d240c4 100644 --- a/engine/api/rbac/dao_rbac_project_key.go +++ b/engine/api/rbac/dao_rbac_project_key.go @@ -2,6 +2,7 @@ package rbac import ( "context" + "github.com/ovh/cds/sdk/telemetry" "github.com/lib/pq" @@ -51,6 +52,8 @@ func loadRRBACProjectKeys(ctx context.Context, db gorp.SqlExecutor, rbacProjectI } func HasRoleOnProjectAndUserID(ctx context.Context, db gorp.SqlExecutor, role string, userID string, projectKey string) (bool, error) { + ctx, next := telemetry.Span(ctx, "rbac.HasRoleOnProjectAndUserID") + defer next() projectKeys, err := LoadProjectKeysByRoleAndUserID(ctx, db, role, userID) if err != nil { return false, err diff --git a/engine/api/repository/dao_vcs_project_repository.go b/engine/api/repository/dao_vcs_project_repository.go index d4f7ab739a..23366d65dd 100644 --- a/engine/api/repository/dao_vcs_project_repository.go +++ b/engine/api/repository/dao_vcs_project_repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "github.com/ovh/cds/sdk/telemetry" "time" "github.com/go-gorp/gorp" @@ -36,15 +37,14 @@ func Delete(db gorpmapper.SqlExecutorWithTx, vcsProjectID string, name string) e return sdk.WrapError(err, "cannot delete project_repository %s / %s", vcsProjectID, name) } -func LoadRepositoryByVCSAndID(ctx context.Context, db gorp.SqlExecutor, vcsProjectID, repoID string) (*sdk.ProjectRepository, error) { - query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.id = $2`).Args(vcsProjectID, repoID) +func getRepository(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { var res dbProjectRepository - found, err := gorpmapping.Get(ctx, db, query, &res) + found, err := gorpmapping.Get(ctx, db, query, &res, opts...) if err != nil { return nil, err } if !found { - return nil, sdk.NewErrorFrom(sdk.ErrNotFound, "repository: %s", repoID) + return nil, sdk.WithStack(sdk.ErrNotFound) } isValid, err := gorpmapping.CheckSignature(res, res.Signature) @@ -58,26 +58,22 @@ func LoadRepositoryByVCSAndID(ctx context.Context, db gorp.SqlExecutor, vcsProje return &res.ProjectRepository, nil } -func LoadRepositoryByName(ctx context.Context, db gorp.SqlExecutor, vcsProjectID string, repoName string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { - query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.name = $2`).Args(vcsProjectID, repoName) - var res dbProjectRepository - found, err := gorpmapping.Get(ctx, db, query, &res, opts...) +func LoadRepositoryByVCSAndID(ctx context.Context, db gorp.SqlExecutor, vcsProjectID, repoID string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { + query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.id = $2`).Args(vcsProjectID, repoID) + repo, err := getRepository(ctx, db, query, opts...) if err != nil { - return nil, err - } - if !found { - return nil, sdk.WrapError(sdk.ErrNotFound, "repository %s", repoName) + return nil, sdk.WrapError(err, "unable to get repository %s", repo.ID) } + return repo, nil +} - isValid, err := gorpmapping.CheckSignature(res, res.Signature) +func LoadRepositoryByName(ctx context.Context, db gorp.SqlExecutor, vcsProjectID string, repoName string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { + query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.name = $2`).Args(vcsProjectID, repoName) + repo, err := getRepository(ctx, db, query, opts...) if err != nil { - return nil, err - } - if !isValid { - log.Error(ctx, "project_repository %d / %s data corrupted", res.ID, res.Name) - return nil, sdk.WithStack(sdk.ErrNotFound) + return nil, sdk.WrapError(err, "unable to get repository %s/%s", vcsProjectID, repoName) } - return &res.ProjectRepository, nil + return repo, nil } func LoadAllRepositoriesByVCSProjectID(ctx context.Context, db gorp.SqlExecutor, vcsProjectID string) ([]sdk.ProjectRepository, error) { @@ -102,6 +98,17 @@ func LoadAllRepositoriesByVCSProjectID(ctx context.Context, db gorp.SqlExecutor, return repositories, nil } +func LoadRepositoryByID(ctx context.Context, db gorp.SqlExecutor, id string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { + ctx, next := telemetry.Span(ctx, "repository.LoadRepositoryByID") + defer next() + query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE id = $1`).Args(id) + repo, err := getRepository(ctx, db, query, opts...) + if err != nil { + return nil, sdk.WrapError(err, "unable to get repository %s", id) + } + return repo, nil +} + func LoadAllRepositories(ctx context.Context, db gorp.SqlExecutor) ([]sdk.ProjectRepository, error) { query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository`) var res []dbProjectRepository diff --git a/engine/api/repository/dao_vcs_project_repository_analyze.go b/engine/api/repository/dao_vcs_project_repository_analyze.go index ab9cbc3bb7..5ce0ef3afa 100644 --- a/engine/api/repository/dao_vcs_project_repository_analyze.go +++ b/engine/api/repository/dao_vcs_project_repository_analyze.go @@ -2,6 +2,7 @@ package repository import ( "context" + "github.com/ovh/cds/sdk/telemetry" "time" "github.com/go-gorp/gorp" @@ -111,6 +112,8 @@ func LoadRepositoryIDsAnalysisInProgress(ctx context.Context, db gorp.SqlExecuto } func LoadRepositoryAnalysisById(ctx context.Context, db gorp.SqlExecutor, projectRepoID, analysisID string) (*sdk.ProjectRepositoryAnalysis, error) { + ctx, next := telemetry.Span(ctx, "repository.LoadRepositoryAnalysisById") + defer next() query := gorpmapping.NewQuery("SELECT * FROM project_repository_analysis WHERE project_repository_id = $1 AND id = $2").Args(projectRepoID, analysisID) return getAnalysis(ctx, db, query) } diff --git a/engine/api/user/dao_gpgkey.go b/engine/api/user/dao_gpgkey.go index 0c016ec1fb..bf6a00caee 100644 --- a/engine/api/user/dao_gpgkey.go +++ b/engine/api/user/dao_gpgkey.go @@ -2,6 +2,7 @@ package user import ( "context" + "github.com/ovh/cds/sdk/telemetry" "time" "github.com/go-gorp/gorp" @@ -64,6 +65,8 @@ func LoadGPGKeysByUserID(ctx context.Context, db gorp.SqlExecutor, userID string } func LoadGPGKeyByKeyID(ctx context.Context, db gorp.SqlExecutor, keyID string) (*sdk.UserGPGKey, error) { + ctx, next := telemetry.Span(ctx, "user.LoadGPGKeyByKeyID") + defer next() query := gorpmapping.NewQuery(` SELECT * FROM user_gpg_key diff --git a/engine/api/v2_project.go b/engine/api/v2_project.go index 9ff3cd2923..e8fcd5fb01 100644 --- a/engine/api/v2_project.go +++ b/engine/api/v2_project.go @@ -3,10 +3,15 @@ package api import ( "context" "net/http" + "net/url" + "github.com/gorilla/mux" + + "github.com/ovh/cds/engine/api/database/gorpmapping" "github.com/ovh/cds/engine/api/rbac" "github.com/ovh/cds/engine/api/repository" "github.com/ovh/cds/engine/service" + "github.com/ovh/cds/sdk" ) // getAllRepositoriesHandler Get all repositories @@ -20,3 +25,25 @@ func (api *API) getAllRepositoriesHandler() ([]service.RbacChecker, service.Hand return service.WriteJSON(w, repos, http.StatusOK) } } + +func (api *API) getRepositoryHookHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.IsHookService), + func(ctx context.Context, w http.ResponseWriter, req *http.Request) error { + vars := mux.Vars(req) + repoIdentifier, err := url.PathUnescape(vars["repositoryIdentifier"]) + if !sdk.IsValidUUID(repoIdentifier) { + return sdk.NewErrorFrom(sdk.ErrWrongRequest, "this handler needs the repository uuid") + } + repo, err := repository.LoadRepositoryByID(ctx, api.mustDB(), repoIdentifier, gorpmapping.GetOptions.WithDecryption) + if err != nil { + return err + } + h := sdk.Hook{ + HookSignKey: repo.HookSignKey, + UUID: repo.ID, + HookType: sdk.RepositoryEntitiesHook, + Configuration: repo.HookConfiguration, + } + return service.WriteJSON(w, h, http.StatusOK) + } +} diff --git a/engine/api/v2_project_repository.go b/engine/api/v2_project_repository.go index f193bde5df..261b94b8d9 100644 --- a/engine/api/v2_project_repository.go +++ b/engine/api/v2_project_repository.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" "net/url" @@ -12,17 +13,18 @@ import ( "github.com/ovh/cds/engine/api/repositoriesmanager" "github.com/ovh/cds/engine/api/repository" "github.com/ovh/cds/engine/api/services" + "github.com/ovh/cds/engine/gorpmapper" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" ) -func (api *API) getRepositoryByIdentifier(ctx context.Context, vcsID string, repositoryIdentifier string) (*sdk.ProjectRepository, error) { +func (api *API) getRepositoryByIdentifier(ctx context.Context, vcsID string, repositoryIdentifier string, opts ...gorpmapper.GetOptionFunc) (*sdk.ProjectRepository, error) { var repo *sdk.ProjectRepository var err error if sdk.IsValidUUID(repositoryIdentifier) { - repo, err = repository.LoadRepositoryByVCSAndID(ctx, api.mustDB(), vcsID, repositoryIdentifier) + repo, err = repository.LoadRepositoryByVCSAndID(ctx, api.mustDB(), vcsID, repositoryIdentifier, opts...) } else { - repo, err = repository.LoadRepositoryByName(ctx, api.mustDB(), vcsID, repositoryIdentifier) + repo, err = repository.LoadRepositoryByName(ctx, api.mustDB(), vcsID, repositoryIdentifier, opts...) } if err != nil { return nil, err @@ -151,7 +153,11 @@ func (api *API) postProjectRepositoryHandler() ([]service.RbacChecker, service.H if len(srvs) < 1 { return sdk.NewErrorFrom(sdk.ErrNotFound, "unable to find hook uservice") } - repositoryHookRegister := sdk.NewEntitiesHook(repoDB.ID, pKey, vcsProject.Type, vcsProject.Name, repoDB.Name) + repositoryHookRegister, err := sdk.NewEntitiesHook(repoDB.ID, pKey, vcsProject.Type, vcsProject.Name, repoDB.Name) + if err != nil { + return err + } + _, code, errHooks := services.NewClient(tx, srvs).DoJSONRequest(ctx, http.MethodPost, "/v2/task", repositoryHookRegister, nil) if errHooks != nil || code >= 400 { return sdk.WrapError(errHooks, "unable to create hooks [HTTP: %d]", code) @@ -169,6 +175,7 @@ func (api *API) postProjectRepositoryHandler() ([]service.RbacChecker, service.H repoDB.CloneURL = vcsRepo.HTTPCloneURL } repoDB.Auth = repoBody.Auth + repoDB.HookSignKey = repositoryHookRegister.HookSignKey // Update repository with Hook configuration repoDB.HookConfiguration = repositoryHookRegister.Configuration @@ -206,3 +213,58 @@ func (api *API) getVCSProjectRepositoryAllHandler() ([]service.RbacChecker, serv return service.WriteJSON(w, repositories, http.StatusOK) } } + +func (api *API) postRepositoryHookRegenKeyHandler() ([]service.RbacChecker, service.Handler) { + return service.RBAC(rbac.ProjectManage), + func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + pKey := vars["projectKey"] + vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"]) + if err != nil { + return sdk.NewError(sdk.ErrWrongRequest, err) + } + repositoryIdentifier, err := url.PathUnescape(vars["repositoryIdentifier"]) + if err != nil { + return sdk.WithStack(err) + } + + vcsProject, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier) + if err != nil { + return err + } + + repo, err := api.getRepositoryByIdentifier(ctx, vcsProject.ID, repositoryIdentifier, gorpmapper.GetOptions.WithDecryption) + if err != nil { + return err + } + newSecret, err := sdk.GenerateHookSecret() + if err != nil { + return err + } + repo.HookSignKey = newSecret + + tx, err := api.mustDB().Begin() + if err != nil { + return sdk.WithStack(err) + } + if err := repository.Update(ctx, tx, repo); err != nil { + return err + } + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) + } + + srvs, err := services.LoadAllByType(ctx, api.mustDB(), sdk.TypeHooks) + if err != nil { + return err + } + if len(srvs) == 0 { + return sdk.NewErrorFrom(sdk.ErrNotFound, "no hook service found") + } + hook := sdk.HookAccessData{ + URL: fmt.Sprintf("%s/v2/webhook/repository/%s/%s", srvs[0].HTTPURL, vcsProject.Type, repo.ID), + HookSignKey: newSecret, + } + return service.WriteJSON(w, hook, http.StatusOK) + } +} diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go index 63d5af7db3..f873839033 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -27,6 +27,7 @@ import ( "github.com/ovh/cds/engine/cache" "github.com/ovh/cds/engine/service" "github.com/ovh/cds/sdk" + "github.com/ovh/cds/sdk/gpg" cdslog "github.com/ovh/cds/sdk/log" "github.com/ovh/cds/sdk/telemetry" ) @@ -181,68 +182,7 @@ func (api *API) postRepositoryAnalysisHandler() ([]service.RbacChecker, service. ProjectKey: proj.Key, Branch: analysis.Branch, Commit: analysis.Commit, - Data: sdk.ProjectRepositoryData{ - OperationUUID: "", - }, - } - - switch vcsProject.Type { - case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud, sdk.VCSTypeGitlab, sdk.VCSTypeGerrit: - ope := &sdk.Operation{ - VCSServer: vcsProject.Name, - RepoFullName: clearRepo.Name, - URL: clearRepo.CloneURL, - RepositoryStrategy: sdk.RepositoryStrategy{ - SSHKey: clearRepo.Auth.SSHKeyName, - User: clearRepo.Auth.Username, - Password: clearRepo.Auth.Token, - }, - Setup: sdk.OperationSetup{ - Checkout: sdk.OperationCheckout{ - Commit: analysis.Commit, - Branch: analysis.Branch, - CheckSignature: true, - }, - }, - } - - if clearRepo.Auth.SSHKeyName != "" { - ope.RepositoryStrategy.ConnectionType = "ssh" - } else { - ope.RepositoryStrategy.ConnectionType = "https" - } - - if err := operation.PostRepositoryOperation(ctx, tx, *proj, ope, nil); err != nil { - return err - } - repoAnalysis.Data.OperationUUID = ope.UUID - case sdk.VCSTypeGitea, sdk.VCSTypeGithub: - // Check commit signature - client, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, analysis.ProjectKey, vcsProject.Name) - if err != nil { - return err - } - vcsCommit, err := client.Commit(ctx, analysis.RepoName, analysis.Commit) - if err != nil { - return err - } - if vcsCommit.Hash == "" { - repoAnalysis.Status = sdk.RepositoryAnalysisStatusError - repoAnalysis.Data.Error = fmt.Sprintf("commit %s not found", analysis.Commit) - } else { - if vcsCommit.Verified && vcsCommit.KeySignID != "" { - repoAnalysis.Data.SignKeyID = vcsCommit.KeySignID - repoAnalysis.Data.CommitCheck = true - } else { - repoAnalysis.Data.SignKeyID = vcsCommit.KeySignID - repoAnalysis.Data.CommitCheck = false - repoAnalysis.Status = sdk.RepositoryAnalysisStatusSkipped - repoAnalysis.Data.Error = fmt.Sprintf("commit %s is not signed", vcsCommit.Hash) - } - } - - default: - return sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to analyze vcs type: %s", vcsProject.Type) + Data: sdk.ProjectRepositoryData{}, } if err := repository.InsertAnalysis(ctx, tx, &repoAnalysis); err != nil { @@ -250,8 +190,7 @@ func (api *API) postRepositoryAnalysisHandler() ([]service.RbacChecker, service. } response := sdk.AnalysisResponse{ - AnalysisID: repoAnalysis.ID, - OperationID: repoAnalysis.Data.OperationUUID, + AnalysisID: repoAnalysis.ID, } if err := tx.Commit(); err != nil { @@ -293,172 +232,253 @@ func (api *API) repositoryAnalysisPoller(ctx context.Context, tick time.Duration } func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, analysisID string) error { - _, next := telemetry.Span(ctx, "api.analyzeRepository.lock") + ctx, next := telemetry.Span(ctx, "api.analyzeRepository.lock") + defer next() + lockKey := cache.Key("api:analyzeRepository", analysisID) b, err := api.Cache.Lock(lockKey, 5*time.Minute, 0, 1) if err != nil { - next() return err } if !b { log.Debug(ctx, "api.analyzeRepository> analyze %s is locked in cache", analysisID) - next() return nil } - next() defer func() { _ = api.Cache.Unlock(lockKey) }() - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadRepositoryAnalysisById") analysis, err := repository.LoadRepositoryAnalysisById(ctx, api.mustDB(), projectRepoID, analysisID) if sdk.ErrorIs(err, sdk.ErrNotFound) { - next() return nil } if err != nil { - next() - return sdk.WrapError(err, "unable to load analyze %s", analysis.ID) + return api.stopAnalysis(ctx, analysis, sdk.WrapError(err, "unable to load analyze %s", analysis.ID)) } - next() if analysis.Status != sdk.RepositoryAnalysisStatusInProgress { return nil } - if analysis.Data.OperationUUID != "" { - _, next = telemetry.Span(ctx, "api.analyzeRepository.Poll") - ope, err := operation.Poll(ctx, api.mustDB(), analysis.Data.OperationUUID) - if err != nil { - next() - return err - } - next() + vcsProject, err := vcs.LoadVCSByID(ctx, api.mustDB(), analysis.ProjectKey, analysis.VCSProjectID) + if err != nil { + return api.stopAnalysis(ctx, analysis, err) + } - stopAnalyze := false - if ope.Status == sdk.OperationStatusDone && ope.Setup.Checkout.Result.CommitVerified { - analysis.Data.CommitCheck = true - analysis.Data.SignKeyID = ope.Setup.Checkout.Result.SignKeyID + repoWithSecret, err := repository.LoadRepositoryByID(ctx, api.mustDB(), analysis.ProjectRepositoryID, gorpmapping.GetOptions.WithDecryption) + if err != nil { + return api.stopAnalysis(ctx, analysis, err) + } + + switch vcsProject.Type { + case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud, sdk.VCSTypeGitlab, sdk.VCSTypeGerrit: + if err := api.analyzeCommitSignatureThroughOperation(ctx, analysis, *vcsProject, *repoWithSecret); err != nil { + return api.stopAnalysis(ctx, analysis, err) } - if ope.Status == sdk.OperationStatusDone && !ope.Setup.Checkout.Result.CommitVerified { - analysis.Data.CommitCheck = false - analysis.Data.SignKeyID = ope.Setup.Checkout.Result.SignKeyID - analysis.Data.Error = ope.Setup.Checkout.Result.Msg + case sdk.VCSTypeGitea, sdk.VCSTypeGithub: + if err := api.analyzeCommitSignatureThroughVcsAPI(ctx, analysis, *vcsProject, *repoWithSecret); err != nil { + return api.stopAnalysis(ctx, analysis, err) + } + default: + return api.stopAnalysis(ctx, analysis, sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to analyze vcs type: %s", vcsProject.Type)) + } + + // remove secret from repo + repoWithSecret.Auth = sdk.ProjectRepositoryAuth{} + + tx, err := api.mustDB().Begin() + if err != nil { + return api.stopAnalysis(ctx, analysis, sdk.WithStack(err)) + } + defer tx.Rollback() //nolint + + if analysis.Status == sdk.RepositoryAnalysisStatusInProgress { + var cdsUser *sdk.AuthentifiedUser + gpgKey, err := user.LoadGPGKeyByKeyID(ctx, tx, analysis.Data.SignKeyID) + if err != nil { + if !sdk.ErrorIs(err, sdk.ErrNotFound) { + return api.stopAnalysis(ctx, analysis, sdk.WrapError(err, "unable to find gpg key: %s", analysis.Data.SignKeyID)) + } analysis.Status = sdk.RepositoryAnalysisStatusSkipped - stopAnalyze = true + analysis.Data.Error = fmt.Sprintf("gpgkey %s not found", analysis.Data.SignKeyID) } - if ope.Status == sdk.OperationStatusError { - analysis.Data.Error = ope.Error.Message - analysis.Status = sdk.RepositoryAnalysisStatusError - stopAnalyze = true + if gpgKey != nil { + cdsUser, err = user.LoadByID(ctx, tx, gpgKey.AuthentifiedUserID) + if err != nil { + if !sdk.ErrorIs(err, sdk.ErrNotFound) { + return api.stopAnalysis(ctx, analysis, sdk.WrapError(err, "unable to find user %s", gpgKey.AuthentifiedUserID)) + } + analysis.Status = sdk.RepositoryAnalysisStatusError + analysis.Data.Error = fmt.Sprintf("user %s not found", gpgKey.AuthentifiedUserID) + } } - if stopAnalyze { - tx, err := api.mustDB().Begin() + if cdsUser != nil { + analysis.Data.CDSUserID = cdsUser.ID + analysis.Data.CDSUserName = cdsUser.Username + + // Check user right + b, err := rbac.HasRoleOnProjectAndUserID(ctx, tx, sdk.RoleManage, cdsUser.ID, analysis.ProjectKey) if err != nil { - return sdk.WrapError(err, "unable to start transaction") + return api.stopAnalysis(ctx, analysis, err) } - defer tx.Rollback() - if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { - return sdk.WrapError(err, "unable to failed analyze") + if !b { + analysis.Status = sdk.RepositoryAnalysisStatusSkipped + analysis.Data.Error = fmt.Sprintf("user %s doesn't have enough right on project %s", cdsUser.ID, analysis.ProjectKey) + } + + if analysis.Status == sdk.RepositoryAnalysisStatusInProgress { + client, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, analysis.ProjectKey, vcsProject.Name) + if err != nil { + return api.stopAnalysis(ctx, analysis, err) + } + + switch vcsProject.Type { + case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud: + // get archive + err = api.getCdsArchiveFileOnRepo(ctx, client, *repoWithSecret, analysis) + case sdk.VCSTypeGitlab, sdk.VCSTypeGithub, sdk.VCSTypeGitea: + analysis.Data.Entities = make([]sdk.ProjectRepositoryDataEntity, 0) + err = api.getCdsFilesOnVCSDirectory(ctx, client, analysis, repoWithSecret.Name, analysis.Commit, ".cds") + case sdk.VCSTypeGerrit: + return sdk.WithStack(sdk.ErrNotImplemented) + } + if err != nil { + return api.stopAnalysis(ctx, analysis, err) + } else { + analysis.Status = sdk.RepositoryAnalysisStatusSucceed + } } - return sdk.WithStack(tx.Commit()) } } - // Retrieve cds files - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadRepositoryByVCSAndID") - repo, err := repository.LoadRepositoryByVCSAndID(ctx, api.mustDB(), analysis.VCSProjectID, analysis.ProjectRepositoryID) - if err != nil { - next() - return err - } - next() - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadVCSByID") - vcsProject, err := vcs.LoadVCSByID(ctx, api.mustDB(), analysis.ProjectKey, analysis.VCSProjectID, gorpmapping.GetOptions.WithDecryption) - if err != nil { - next() - return err + if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { + return sdk.WrapError(err, "unable to failed analyze") } - next() + return sdk.WithStack(tx.Commit()) +} +func (api *API) analyzeCommitSignatureThroughVcsAPI(ctx context.Context, analysis *sdk.ProjectRepositoryAnalysis, vcsProject sdk.VCSProject, repoWithSecret sdk.ProjectRepository) error { + ctx, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughVcsAPI") + defer next() tx, err := api.mustDB().Begin() if err != nil { - return sdk.WrapError(err, "unable to start transaction") + return sdk.WithStack(err) } - defer tx.Rollback() // nolint - // Search User by gpgkey - var cdsUser *sdk.AuthentifiedUser - gpgKey, err := user.LoadGPGKeyByKeyID(ctx, tx, analysis.Data.SignKeyID) + // Check commit signature + client, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, analysis.ProjectKey, vcsProject.Name) if err != nil { - if !sdk.ErrorIs(err, sdk.ErrNotFound) { - return sdk.WrapError(err, "unable to find gpg key: %s", analysis.Data.SignKeyID) - } - analysis.Status = sdk.RepositoryAnalysisStatusError - analysis.Data.Error = fmt.Sprintf("gpgkey %s not found", analysis.Data.SignKeyID) + _ = tx.Rollback() // nolint + return err + } + vcsCommit, err := client.Commit(ctx, repoWithSecret.Name, analysis.Commit) + if err != nil { + _ = tx.Rollback() // nolint + return err + } + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) } - if gpgKey != nil { - cdsUser, err = user.LoadByID(ctx, tx, gpgKey.AuthentifiedUserID) - if err != nil { - if !sdk.ErrorIs(err, sdk.ErrNotFound) { - return sdk.WrapError(err, "unable to find user %s", gpgKey.AuthentifiedUserID) + if vcsCommit.Hash == "" { + analysis.Status = sdk.RepositoryAnalysisStatusError + analysis.Data.Error = fmt.Sprintf("commit %s not found", analysis.Commit) + } else { + if vcsCommit.Signature != "" { + keyId, err := gpg.GetKeyIdFromSignature(vcsCommit.Signature) + if err != nil { + log.ErrorWithStackTrace(ctx, err) + analysis.Status = sdk.RepositoryAnalysisStatusError + analysis.Data.Error = fmt.Sprintf("unable to extract keyID from signature: %v", err) + } else { + analysis.Data.SignKeyID = keyId + analysis.Data.CommitCheck = true } - analysis.Status = sdk.RepositoryAnalysisStatusError - analysis.Data.Error = fmt.Sprintf("user %s not found", gpgKey.AuthentifiedUserID) + } else { + analysis.Data.CommitCheck = false + analysis.Status = sdk.RepositoryAnalysisStatusSkipped + analysis.Data.Error = fmt.Sprintf("commit %s is not signed", vcsCommit.Hash) } } + return nil +} - if cdsUser != nil { - analysis.Data.CDSUserID = cdsUser.ID - analysis.Data.CDSUserName = cdsUser.Username - - // Check user right - b, err := rbac.HasRoleOnProjectAndUserID(ctx, tx, sdk.RoleManage, cdsUser.ID, analysis.ProjectKey) +func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, analysis *sdk.ProjectRepositoryAnalysis, vcsProject sdk.VCSProject, repoWithSecret sdk.ProjectRepository) error { + ctx, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughOperation") + defer next() + if analysis.Data.OperationUUID == "" { + proj, err := project.Load(ctx, api.mustDB(), analysis.ProjectKey) if err != nil { return err } - if !b { - analysis.Status = sdk.RepositoryAnalysisStatusSkipped - analysis.Data.Error = fmt.Sprintf("user %s doesn't have enough right on project %s", cdsUser.ID, analysis.ProjectKey) + + ope := &sdk.Operation{ + VCSServer: vcsProject.Name, + RepoFullName: repoWithSecret.Name, + URL: repoWithSecret.CloneURL, + RepositoryStrategy: sdk.RepositoryStrategy{ + SSHKey: repoWithSecret.Auth.SSHKeyName, + User: repoWithSecret.Auth.Username, + Password: repoWithSecret.Auth.Token, + }, + Setup: sdk.OperationSetup{ + Checkout: sdk.OperationCheckout{ + Commit: analysis.Commit, + Branch: analysis.Branch, + CheckSignature: true, + }, + }, + } + if repoWithSecret.Auth.SSHKeyName != "" { + ope.RepositoryStrategy.ConnectionType = "ssh" + } else { + ope.RepositoryStrategy.ConnectionType = "https" } - if analysis.Status != sdk.RepositoryAnalysisStatusSkipped { - client, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, analysis.ProjectKey, vcsProject.Name) - if err != nil { - return err - } + tx, err := api.mustDB().Begin() + if err != nil { + return sdk.WithStack(err) + } - switch vcsProject.Type { - case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud: - // get archive - err = api.getCdsArchiveFileOnRepo(ctx, client, *repo, analysis) - case sdk.VCSTypeGitlab, sdk.VCSTypeGithub, sdk.VCSTypeGitea: - analysis.Data.Entities = make([]sdk.ProjectRepositoryDataEntity, 0) - err = api.getCdsFilesOnVCSDirectory(ctx, client, analysis, repo.Name, analysis.Commit, ".cds") - case sdk.VCSTypeGerrit: - return sdk.WithStack(sdk.ErrNotImplemented) - } + if err := operation.PostRepositoryOperation(ctx, tx, *proj, ope, nil); err != nil { + return err + } + analysis.Data.OperationUUID = ope.UUID - // Update analyze - if err != nil { - analysis.Status = sdk.RepositoryAnalysisStatusError - analysis.Data.Error = err.Error() - } else { - analysis.Status = sdk.RepositoryAnalysisStatusSucceed - } + if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { + return err + } + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) } } - - if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { + ope, err := operation.Poll(ctx, api.mustDB(), analysis.Data.OperationUUID) + if err != nil { return err } - return sdk.WrapError(tx.Commit(), "unable to commit") + + if ope.Status == sdk.OperationStatusDone && ope.Setup.Checkout.Result.CommitVerified { + analysis.Data.CommitCheck = true + analysis.Data.SignKeyID = ope.Setup.Checkout.Result.SignKeyID + } + if ope.Status == sdk.OperationStatusDone && !ope.Setup.Checkout.Result.CommitVerified { + analysis.Data.CommitCheck = false + analysis.Data.SignKeyID = ope.Setup.Checkout.Result.SignKeyID + analysis.Data.Error = ope.Setup.Checkout.Result.Msg + analysis.Status = sdk.RepositoryAnalysisStatusSkipped + } + if ope.Status == sdk.OperationStatusError { + analysis.Data.Error = ope.Error.Message + analysis.Status = sdk.RepositoryAnalysisStatusError + } + return nil } func (api *API) getCdsFilesOnVCSDirectory(ctx context.Context, client sdk.VCSAuthorizedClientService, analysis *sdk.ProjectRepositoryAnalysis, repoName, commit, directory string) error { + ctx, next := telemetry.Span(ctx, "api.getCdsFilesOnVCSDirectory") + defer next() contents, err := client.ListContent(ctx, repoName, commit, directory) if err != nil { return sdk.WrapError(err, "unable to list content on commit [%s] in directory %s: %v", commit, directory, err) @@ -480,6 +500,8 @@ func (api *API) getCdsFilesOnVCSDirectory(ctx context.Context, client sdk.VCSAut } func (api *API) getCdsArchiveFileOnRepo(ctx context.Context, client sdk.VCSAuthorizedClientService, repo sdk.ProjectRepository, analysis *sdk.ProjectRepositoryAnalysis) error { + ctx, next := telemetry.Span(ctx, "api.getCdsArchiveFileOnRepo") + defer next() analysis.Data.Entities = make([]sdk.ProjectRepositoryDataEntity, 0) reader, _, err := client.GetArchive(ctx, repo.Name, ".cds", "tar.gz", analysis.Commit) if err != nil { @@ -513,3 +535,21 @@ func (api *API) getCdsArchiveFileOnRepo(ctx context.Context, client sdk.VCSAutho } return nil } + +func (api *API) stopAnalysis(ctx context.Context, analysis *sdk.ProjectRepositoryAnalysis, originalError error) error { + log.ErrorWithStackTrace(ctx, originalError) + tx, err := api.mustDB().Begin() + if err != nil { + return sdk.WithStack(err) + } + defer tx.Rollback() // nolint + analysis.Status = sdk.RepositoryAnalysisStatusError + analysis.Data.Error = fmt.Sprintf("%v", originalError) + if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { + return err + } + if err := tx.Commit(); err != nil { + return err + } + return nil +} diff --git a/engine/api/v2_repository_analyze_test.go b/engine/api/v2_repository_analyze_test.go index 1fb47746fa..cbf1d2d7eb 100644 --- a/engine/api/v2_repository_analyze_test.go +++ b/engine/api/v2_repository_analyze_test.go @@ -2,11 +2,17 @@ package api import ( "context" + "github.com/go-gorp/gorp" + "github.com/golang/mock/gomock" "github.com/ovh/cds/engine/api/repository" + "github.com/ovh/cds/engine/api/services" + "github.com/ovh/cds/engine/api/services/mock_services" "github.com/ovh/cds/engine/api/test/assets" + "github.com/ovh/cds/engine/api/user" "github.com/ovh/cds/engine/api/vcs" "github.com/ovh/cds/sdk" "github.com/stretchr/testify/require" + "net/http" "testing" "time" ) @@ -58,3 +64,434 @@ func TestCleanAnalysis(t *testing.T) { require.NoError(t, err) require.Len(t, analyses, 50) } + +func TestAnalyzeBitbucketServerWithoutHash(t *testing.T) { + api, db, _ := newTestAPI(t) + ctx := context.TODO() + + // Create project + key1 := sdk.RandomString(10) + proj1 := assets.InsertTestProject(t, db, api.Cache, key1, key1) + + // Create VCS + vcsProject := assets.InsertTestVCSProject(t, db, proj1.ID, "vcs-server", "github") + + repo := sdk.ProjectRepository{ + Name: "myrepo", + Auth: sdk.ProjectRepositoryAuth{ + Username: "myuser", + Token: "mytoken", + }, + Created: time.Now(), + VCSProjectID: vcsProject.ID, + CreatedBy: "me", + } + require.NoError(t, repository.Insert(context.TODO(), db, &repo)) + + analysis := sdk.ProjectRepositoryAnalysis{ + ID: "", + Status: sdk.RepositoryAnalysisStatusInProgress, + Commit: "abcdef", + ProjectKey: proj1.Key, + ProjectRepositoryID: repo.ID, + Created: time.Now(), + LastModified: time.Now(), + Branch: "master", + VCSProjectID: vcsProject.ID, + } + require.NoError(t, repository.InsertAnalysis(ctx, db, &analysis)) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/vcs-server/repos/myrepo/commits/abcdef", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + commit := &sdk.VCSCommit{ + Signature: "fakesign", + Verified: true, + } + *(out.(*sdk.VCSCommit)) = *commit + return nil, 200, nil + }, + ).MaxTimes(1) + + require.NoError(t, api.analyzeRepository(ctx, repo.ID, analysis.ID)) + + analysisUpdated, err := repository.LoadRepositoryAnalysisById(ctx, db, repo.ID, analysis.ID) + require.NoError(t, err) + require.Equal(t, "commit abcdef not found", analysisUpdated.Data.Error) + require.Equal(t, sdk.RepositoryAnalysisStatusError, analysisUpdated.Status) +} + +func TestAnalyzeBitbucketServerWrongSignature(t *testing.T) { + api, db, _ := newTestAPI(t) + ctx := context.TODO() + + // Create project + key1 := sdk.RandomString(10) + proj1 := assets.InsertTestProject(t, db, api.Cache, key1, key1) + + // Create VCS + vcsProject := assets.InsertTestVCSProject(t, db, proj1.ID, "vcs-server", "github") + + repo := sdk.ProjectRepository{ + Name: "myrepo", + Auth: sdk.ProjectRepositoryAuth{ + Username: "myuser", + Token: "mytoken", + }, + Created: time.Now(), + VCSProjectID: vcsProject.ID, + CreatedBy: "me", + } + require.NoError(t, repository.Insert(context.TODO(), db, &repo)) + + analysis := sdk.ProjectRepositoryAnalysis{ + ID: "", + Status: sdk.RepositoryAnalysisStatusInProgress, + Commit: "abcdef", + ProjectKey: proj1.Key, + ProjectRepositoryID: repo.ID, + Created: time.Now(), + LastModified: time.Now(), + Branch: "master", + VCSProjectID: vcsProject.ID, + } + require.NoError(t, repository.InsertAnalysis(ctx, db, &analysis)) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/vcs-server/repos/myrepo/commits/abcdef", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + commit := &sdk.VCSCommit{ + Signature: "fakesign", + Verified: true, + Hash: "abcdef", + } + *(out.(*sdk.VCSCommit)) = *commit + return nil, 200, nil + }, + ).MaxTimes(1) + + require.NoError(t, api.analyzeRepository(ctx, repo.ID, analysis.ID)) + + analysisUpdated, err := repository.LoadRepositoryAnalysisById(ctx, db, repo.ID, analysis.ID) + require.NoError(t, err) + require.Equal(t, sdk.RepositoryAnalysisStatusError, analysisUpdated.Status) + require.Contains(t, analysisUpdated.Data.Error, "unable to extract keyID from signature") +} + +func TestAnalyzeBitbucketServerGPGKeyNotFound(t *testing.T) { + api, db, _ := newTestAPI(t) + ctx := context.TODO() + + // Create project + key1 := sdk.RandomString(10) + proj1 := assets.InsertTestProject(t, db, api.Cache, key1, key1) + + userKey, err := user.LoadGPGKeyByKeyID(ctx, db, "F344BDDCE15F17D7") + if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { + require.NoError(t, err) + } + if userKey != nil { + require.NoError(t, user.DeleteGPGKey(db, *userKey)) + } + + // Create VCS + vcsProject := assets.InsertTestVCSProject(t, db, proj1.ID, "vcs-server", "github") + + repo := sdk.ProjectRepository{ + Name: "myrepo", + Auth: sdk.ProjectRepositoryAuth{ + Username: "myuser", + Token: "mytoken", + }, + Created: time.Now(), + VCSProjectID: vcsProject.ID, + CreatedBy: "me", + } + require.NoError(t, repository.Insert(context.TODO(), db, &repo)) + + analysis := sdk.ProjectRepositoryAnalysis{ + ID: "", + Status: sdk.RepositoryAnalysisStatusInProgress, + Commit: "abcdef", + ProjectKey: proj1.Key, + ProjectRepositoryID: repo.ID, + Created: time.Now(), + LastModified: time.Now(), + Branch: "master", + VCSProjectID: vcsProject.ID, + } + require.NoError(t, repository.InsertAnalysis(ctx, db, &analysis)) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/vcs-server/repos/myrepo/commits/abcdef", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + commit := &sdk.VCSCommit{ + Signature: "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEfYJxMHx+E0DPuqaA80S93OFfF9cFAmME7aIACgkQ80S93OFf\nF9eFWBAAq5hOcZIx/A+8J6/NwRtXMs5OW+TJxzJb5siXdRC8Mjrm+fqwpTPPHqtB\nbb7iuiRnmY/HqCegULiw4qVxDyA3sswyDHPLcyUcfG4drJGylPW9ZYg3YeRslX2B\niQykYZyd4h3R/euYAuBKA9vMGoWnaU/Vh22A11Po1pXpPq623FTkiFOSAZrD8Hql\nEvmlhw26qHSPlhsdSKsR+/FPvpLUXlNUiYB5oq7W9qy0yOOafgwZ9r3vvxshzvkt\nvW5zG+R05thQ8icCyrWfEfIWp+TTtQX3asOopnQG9dFs2LRODLXXaHTRVRB/MWPa\nNVvUD/dIzBVyNimpik+2Uqq5jWNiXavQmqoxyL9n4A372AIH7Hu78NnfmAz7VnYo\nyVHRNBryiCcYNj5g0x/WnGsDuhQr7170ODw7QfEYJdCPxGgYuhdYovHdjcMcgWpF\ncWEtayj8bhuLTjjxEsqXTv+psxwB55N5OUvyXmNAaFLhJSEI+l1VHW14L3gZFdPT\n+VgPQtT9a1+GEjPqLvZ6wLVTcSI9uogK6NHowmyM261FtFQqLVdkOdUU8RCR8qLC\nekZWQaJutqicIZTolAQyBPBw8aQz0i+uBUgdWkoiHf/zEEudu0b06IpDq2oYFFVH\nVmCuZ3/AcXrW6T3XXcE5pu+Rvsi57O7iR8i7TIP0CaDTr2FfQWc=\n=/H7t\n-----END PGP SIGNATURE-----", + Verified: true, + Hash: "abcdef", + } + *(out.(*sdk.VCSCommit)) = *commit + return nil, 200, nil + }, + ).MaxTimes(1) + + require.NoError(t, api.analyzeRepository(ctx, repo.ID, analysis.ID)) + + analysisUpdated, err := repository.LoadRepositoryAnalysisById(ctx, db, repo.ID, analysis.ID) + require.NoError(t, err) + require.Equal(t, sdk.RepositoryAnalysisStatusSkipped, analysisUpdated.Status) + require.Equal(t, "gpgkey F344BDDCE15F17D7 not found", analysisUpdated.Data.Error) +} + +func TestAnalyzeBitbucketServerUserNotEnoughPerm(t *testing.T) { + api, db, _ := newTestAPI(t) + ctx := context.TODO() + + // Create project + key1 := sdk.RandomString(10) + proj1 := assets.InsertTestProject(t, db, api.Cache, key1, key1) + + uk, err := user.LoadGPGKeyByKeyID(ctx, db, "F344BDDCE15F17D7") + if err != nil && !sdk.ErrorIs(err, sdk.ErrNotFound) { + require.NoError(t, err) + } + if uk != nil { + require.NoError(t, user.DeleteGPGKey(db, *uk)) + } + + u, _ := assets.InsertLambdaUser(t, db) + userKey := &sdk.UserGPGKey{ + KeyID: "F344BDDCE15F17D7", + PublicKey: `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFXv+IMBEADYp5xTZ0YKvUgXvvE0SSeXg+bo8mPTTq5clIYWfdmfVjS6NL8T +IYhnjj5MXXIoGs/Lyx+B0VUC9Jo5ObSVCViJRXGVwfHpMIW2+n4i251pGO4bUPPw +o7SpEbvEc1tqE4P3OU26BZhZoIv3AaslMXi+v2eZjJe5Qr4BSc6FLOo5pdAm9HAZ +7vkj7M/WKbbpoXKpfZF+DLmJsrWU/2/TVD2ZdLANAwiXSVLmLeJr0z/zVX+9o6b9 +Rz7HV3euPDCWb/t2fEI4yT8+e92QlxCtVcMpG7ZpxftQbl4z0U8kHASr38UqjTL5 +VtCHKUFD5KyrxHUxFEUingI+M8NstzObho65oK2yxzcoufHTQBo2sfL4xWqPmFj8 +hZeNSz3P6XPLQ+wdIganRGweEv+LSpbSMXIaWpiE2GjwFVRRTaffCgWvth1JRBti +deJI5rxe7UztytDTg8Ekt5MAqTBIoxqZ24zOdbxEef4EpEiYnaa5GXMg8EHH1bJr +aIc2nuY7Zfoz7uvqS8F5ohh69q/LbSv+gxw7aU36oogd13+8/MYPE29vfb+tIIwz +xen0PUcPkt83EQ0RdTbG7AnrvNMXDINp+ZGz3Oks3OXehezX/syPAe7BunPU/Zfy +wK/GDhpjsS9R+y/ZWDXX/LyQfHiHw5nIoX0m6I43BdshrQH5fyrTvJA02wARAQAB +tCxTdGV2ZW4gR3VpaGV1eCA8c3RldmVuLmd1aWhldXhAY29ycC5vdmguY29tPokC +OAQTAQIAIgUCVe/4gwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ80S9 +3OFfF9dDYw//VuE85jnUS6bFwdvkFtdbXPZxOsFDMX9tiCjYDdXfT+98AoGgZboC +Ya/E8T5NhFjG8yGC8WOsiZZhQ/DyFr7TT+CwLvZ2JmLarEKHpL//YNr5ACp7Q8lo +7PSAACEJx2J3s2qpEbpMrvXVOJkAbwiFUnSz8R14RMJZLCmgbA5CDKpYqCSM/1B1 +ED/WY8phhV6GknsqvG/cQiyQNQBg8PEdsyiNn79QWRGD8q5ZvWsxAuMMY7j/WSLy +VHZJ9wR9lBM9Lf3NJ+vDoVq56WaAH30vuVJ2LzGwHOULDKSFkQZ1JPodsu+7tDAZ +QDENAMaD1940GzmBANH/FOHD5T2VrOYMtPHMcyXJRSUOgw3MtvSuKJJliLMO0DNa +EZG14nCcdDP7xoS9da2JddMxDmqhzuCpsPk0IVH+JSjrAKOJ7r5YE3/vWcI2dQaU +nOYBhqST73RN2g6wF5xLt9Oi1DXYFBfdhz+oXJ1ck34MB3oPx5yzlY9Rp7N5F9a+ +gDiuE1Y1iqRX0uuoDq8b2EsZrQ4dSvpjZwWYRsDghjSATjiAcrhC70NjpG22Avwt +0x3SPG+HQYgzYs9idQMI6lpKqoFU9QUHMsWQKuBFE0ZXJs9Q9d+zjjUCebFZ7LjN +twZyhn8QXg5FUhLygfF6Pq8jnYMXMzAbKXm3NEC8X1/VGaZjB1Lszcq5Ag0EVe/4 +gwEQAMGVA4T9qs/a8zy10Tc8nSGAMdNzI26D0fhH2rRtjeNJs5BqGNMPu2Eg5DKR +7rStsw58fDvdKeB116ZPXq4Hoe66H+Pw83QIwDQk/vN965fPwqz9BIgDE/xTx09w +wVLvfKAHIFQF7znqqUYrES2gYpvirVD7knGKjVMMkB4Hil7TMcya6MTD2a9L32be +nMfZ5sA4311TJPS+kIEeEuG+SU2w3i6YRho+atUvsxkMNzmx92ow6JDznX8Kpbr/ +PVExZObUW0+379yMKlgaZLhrgqbcwm+IOCgsM5XSs/zGb2AFACADnOdqOYToRtIt +bdvH2Y/2fq3t3upuzbpM3fiUu0Vs2rVRe5w4luHt6ZpKdZo43blEL9MN/ZbQVYE0 +N/5/9SAizfyyOGmrNvB4EwPLpyImBre9MRcZJRvg22tFxcbnM2+SJGwfmD0FnPGe +gIRihPgsQxrx6BOCB1JzCUCOUqZ12gy2ul2RuopGEEX8YKLWNryNN8v0ooS+PU8D +Ii2biB9O9UYecXPVhxVP64gl48lN8psIFL+YSJ+svAErsQYGASApRF240Nor98+L +zgHm1+60JNU1i5gYQV6RzDMUML43XYWxsVqA21mTZZSJFwC/TcmLDl9yGyIOTNG4 +kFPT/c1xibi5MGBQE8gIxdwEwfrj9iqohMt8afJfIMhcfwdzABEBAAGJAh8EGAEC +AAkFAlXv+IMCGwwACgkQ80S93OFfF9ceWxAAprlvofJ8qkREkhNznF9YacuDru8n +8BfWINLHKMI8zmOaijcdZVjC/+5FxC7rIx/Bc+vJCmMTTAkud0RfF4zDBPAqEv0q +I+4lR/ATThkRmX3XJSBDeI62MJTOPHqZ13mPnof5fAdy9HFclc1vwMoBjOofJpq4 +DiQqchzR8eg0YXFDfaKptDrjvBGeffb14RjI7MeNwp5YIrEc4zZfQGZ3p3Q8oH84 +vMbWjiWp/OZH+ZBVixLWQVMrTu1jSE7Hj7FgbBJzaXGoH/NyYqTTWany06Mpltu7 ++71v/gJGgav+VxGcPoEzI83SCKdWdlLdtK5HjzpmqMixX1NaO5gfQblatmi7qLIT +f42j7Ul9tumMOLPtKQmiuloMJHO7mUmqOZDxmbrNmb47rAmIU3KRx5oNID9rLhxe +4tuAIsY8Lu2mU+PR5XQlgjG1J0aCunxUOZ4HhLUqJ6U+QWLUpRAq74zjPGocIv1e +GAH2qkfaNTarBQKytsA7k6vnzHmY7KYup3c9qQjMC8XzjuKBF5oJXl3yBU2VCPaw +qVWF89Lpz5nHVxmY2ejU/DvV7zUUAiqlVyzFmiOed5O66jVtPG4YM5x2EMwNvejk +e9rMe4DS8qoQg4er1Z3WNcb4JOAc33HDOol1LFOH1buNN5V+KrkUo0fPWMf4nQ97 +GDFkaTe3nUJdYV4= +=SNcy +-----END PGP PUBLIC KEY BLOCK-----`, + AuthentifiedUserID: u.ID, + } + require.NoError(t, user.InsertGPGKey(ctx, db, userKey)) + + // Create VCS + vcsProject := assets.InsertTestVCSProject(t, db, proj1.ID, "vcs-server", "github") + + repo := sdk.ProjectRepository{ + Name: "myrepo", + Auth: sdk.ProjectRepositoryAuth{ + Username: "myuser", + Token: "mytoken", + }, + Created: time.Now(), + VCSProjectID: vcsProject.ID, + CreatedBy: "me", + } + require.NoError(t, repository.Insert(context.TODO(), db, &repo)) + + analysis := sdk.ProjectRepositoryAnalysis{ + ID: "", + Status: sdk.RepositoryAnalysisStatusInProgress, + Commit: "abcdef", + ProjectKey: proj1.Key, + ProjectRepositoryID: repo.ID, + Created: time.Now(), + LastModified: time.Now(), + Branch: "master", + VCSProjectID: vcsProject.ID, + } + require.NoError(t, repository.InsertAnalysis(ctx, db, &analysis)) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/vcs-server/repos/myrepo/commits/abcdef", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + commit := &sdk.VCSCommit{ + Signature: "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEfYJxMHx+E0DPuqaA80S93OFfF9cFAmME7aIACgkQ80S93OFf\nF9eFWBAAq5hOcZIx/A+8J6/NwRtXMs5OW+TJxzJb5siXdRC8Mjrm+fqwpTPPHqtB\nbb7iuiRnmY/HqCegULiw4qVxDyA3sswyDHPLcyUcfG4drJGylPW9ZYg3YeRslX2B\niQykYZyd4h3R/euYAuBKA9vMGoWnaU/Vh22A11Po1pXpPq623FTkiFOSAZrD8Hql\nEvmlhw26qHSPlhsdSKsR+/FPvpLUXlNUiYB5oq7W9qy0yOOafgwZ9r3vvxshzvkt\nvW5zG+R05thQ8icCyrWfEfIWp+TTtQX3asOopnQG9dFs2LRODLXXaHTRVRB/MWPa\nNVvUD/dIzBVyNimpik+2Uqq5jWNiXavQmqoxyL9n4A372AIH7Hu78NnfmAz7VnYo\nyVHRNBryiCcYNj5g0x/WnGsDuhQr7170ODw7QfEYJdCPxGgYuhdYovHdjcMcgWpF\ncWEtayj8bhuLTjjxEsqXTv+psxwB55N5OUvyXmNAaFLhJSEI+l1VHW14L3gZFdPT\n+VgPQtT9a1+GEjPqLvZ6wLVTcSI9uogK6NHowmyM261FtFQqLVdkOdUU8RCR8qLC\nekZWQaJutqicIZTolAQyBPBw8aQz0i+uBUgdWkoiHf/zEEudu0b06IpDq2oYFFVH\nVmCuZ3/AcXrW6T3XXcE5pu+Rvsi57O7iR8i7TIP0CaDTr2FfQWc=\n=/H7t\n-----END PGP SIGNATURE-----", + Verified: true, + Hash: "abcdef", + } + *(out.(*sdk.VCSCommit)) = *commit + return nil, 200, nil + }, + ).MaxTimes(1) + + require.NoError(t, api.analyzeRepository(ctx, repo.ID, analysis.ID)) + + analysisUpdated, err := repository.LoadRepositoryAnalysisById(ctx, db, repo.ID, analysis.ID) + require.NoError(t, err) + require.Equal(t, sdk.RepositoryAnalysisStatusSkipped, analysisUpdated.Status) + require.Contains(t, analysisUpdated.Data.Error, "doesn't have enough right on project") +} + +func TestAnalyzeBitbucketServerCommitNotSigned(t *testing.T) { + api, db, _ := newTestAPI(t) + ctx := context.TODO() + + // Create project + key1 := sdk.RandomString(10) + proj1 := assets.InsertTestProject(t, db, api.Cache, key1, key1) + + // Create VCS + vcsProject := assets.InsertTestVCSProject(t, db, proj1.ID, "vcs-server", "github") + + repo := sdk.ProjectRepository{ + Name: "myrepo", + Auth: sdk.ProjectRepositoryAuth{ + Username: "myuser", + Token: "mytoken", + }, + Created: time.Now(), + VCSProjectID: vcsProject.ID, + CreatedBy: "me", + } + require.NoError(t, repository.Insert(context.TODO(), db, &repo)) + + analysis := sdk.ProjectRepositoryAnalysis{ + ID: "", + Status: sdk.RepositoryAnalysisStatusInProgress, + Commit: "abcdef", + ProjectKey: proj1.Key, + ProjectRepositoryID: repo.ID, + Created: time.Now(), + LastModified: time.Now(), + Branch: "master", + VCSProjectID: vcsProject.ID, + } + require.NoError(t, repository.InsertAnalysis(ctx, db, &analysis)) + + // Mock VCS + s, _ := assets.InsertService(t, db, t.Name()+"_VCS", sdk.TypeVCS) + // Setup a mock for all services called by the API + ctrl := gomock.NewController(t) + defer ctrl.Finish() + servicesClients := mock_services.NewMockClient(ctrl) + services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client { + return servicesClients + } + defer func() { + _ = services.Delete(db, s) + services.NewClient = services.NewDefaultClient + }() + + servicesClients.EXPECT(). + DoJSONRequest(gomock.Any(), "GET", "/vcs/vcs-server/repos/myrepo/commits/abcdef", gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn( + func(ctx context.Context, method, path string, in interface{}, out interface{}, _ interface{}) (http.Header, int, error) { + commit := &sdk.VCSCommit{ + Hash: "abcdef", + } + *(out.(*sdk.VCSCommit)) = *commit + return nil, 200, nil + }, + ).MaxTimes(1) + + require.NoError(t, api.analyzeRepository(ctx, repo.ID, analysis.ID)) + + analysisUpdated, err := repository.LoadRepositoryAnalysisById(ctx, db, repo.ID, analysis.ID) + require.NoError(t, err) + require.Equal(t, sdk.RepositoryAnalysisStatusSkipped, analysisUpdated.Status) + require.Equal(t, "commit abcdef is not signed", analysisUpdated.Data.Error) +} diff --git a/engine/api/vcs/dao_vcs_project.go b/engine/api/vcs/dao_vcs_project.go index 0afebcc387..09d6a86cbb 100644 --- a/engine/api/vcs/dao_vcs_project.go +++ b/engine/api/vcs/dao_vcs_project.go @@ -2,6 +2,7 @@ package vcs import ( "context" + "github.com/ovh/cds/sdk/telemetry" "time" "github.com/go-gorp/gorp" @@ -84,6 +85,8 @@ func LoadVCSByProject(ctx context.Context, db gorp.SqlExecutor, projectKey strin } func LoadVCSByID(ctx context.Context, db gorp.SqlExecutor, projectKey string, vcsID string, opts ...gorpmapping.GetOptionFunc) (*sdk.VCSProject, error) { + ctx, next := telemetry.Span(ctx, "vcs.LoadVCSByID") + defer next() query := gorpmapping.NewQuery(`SELECT vcs_project.* FROM vcs_project JOIN project ON project.id = vcs_project.project_id WHERE project.projectkey = $1 AND vcs_project.id = $2`).Args(projectKey, vcsID) var res dbVCSProject found, err := gorpmapping.Get(ctx, db, query, &res, opts...) diff --git a/engine/hooks/entitieshook.go b/engine/hooks/entitieshook.go index daedb0560a..471ea8d710 100644 --- a/engine/hooks/entitieshook.go +++ b/engine/hooks/entitieshook.go @@ -12,17 +12,17 @@ func (s *Service) doAnalyzeExecution(ctx context.Context, t *sdk.TaskExecution) var err error switch t.Configuration[sdk.HookConfigVCSType].Value { case sdk.VCSTypeGithub: - return sdk.WithStack(sdk.ErrNotImplemented) - case sdk.VCSTypeGerrit: - return sdk.WithStack(sdk.ErrNotImplemented) + branch, commit, err = s.extractAnalyzeDataFromGithubRequest(t.EntitiesHook.RequestBody) case sdk.VCSTypeGitlab: - return sdk.WithStack(sdk.ErrNotImplemented) - case sdk.VCSTypeBitbucketCloud: - return sdk.WithStack(sdk.ErrNotImplemented) + branch, commit, err = s.extractAnalyzeDataFromGitlabRequest(t.EntitiesHook.RequestBody) case sdk.VCSTypeGitea: branch, commit, err = s.extractAnalyzeDataFromGiteaRequest(t.EntitiesHook.RequestBody) case sdk.VCSTypeBitbucketServer: branch, commit, err = s.extractAnalyzeDataFromBitbucketRequest(t.EntitiesHook.RequestBody) + case sdk.VCSTypeGerrit: + return sdk.WithStack(sdk.ErrNotImplemented) + case sdk.VCSTypeBitbucketCloud: + return sdk.WithStack(sdk.ErrNotImplemented) default: return sdk.NewErrorFrom(sdk.ErrInvalidData, "unknown vcs of type: %s", t.Configuration[sdk.HookConfigVCSType].Value) } @@ -45,20 +45,33 @@ func (s *Service) doAnalyzeExecution(ctx context.Context, t *sdk.TaskExecution) return err } t.EntitiesHook.AnalysisID = resp.AnalysisID - t.EntitiesHook.OperationID = resp.OperationID return nil } +func (s *Service) extractAnalyzeDataFromGitlabRequest(body []byte) (string, string, error) { + var request GitlabEvent + if err := sdk.JSONUnmarshal(body, &request); err != nil { + return "", "", sdk.WrapError(err, "unable ro read gitlab request: %s", string(body)) + } + return request.Ref, request.After, nil +} + +func (s *Service) extractAnalyzeDataFromGithubRequest(body []byte) (string, string, error) { + var request GithubWebHookEvent + if err := sdk.JSONUnmarshal(body, &request); err != nil { + return "", "", sdk.WrapError(err, "unable ro read github request: %s", string(body)) + } + return request.Ref, request.After, nil +} + func (s *Service) extractAnalyzeDataFromBitbucketRequest(body []byte) (string, string, error) { var request sdk.BitbucketServerWebhookEvent if err := sdk.JSONUnmarshal(body, &request); err != nil { return "", "", sdk.WrapError(err, "unable ro read bitbucket request: %s", string(body)) } - if len(request.Changes) == 0 { return "", "", sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to know branch and commit: %s", string(body)) } - return request.Changes[0].RefID, request.Changes[0].ToHash, nil } diff --git a/engine/hooks/hooks_router.go b/engine/hooks/hooks_router.go index 4726e9b602..d9d6c6f349 100644 --- a/engine/hooks/hooks_router.go +++ b/engine/hooks/hooks_router.go @@ -1,11 +1,19 @@ package hooks import ( + "bytes" "context" + "crypto/hmac" "crypto/rsa" + "crypto/sha256" + "encoding/hex" + "io" + "io/ioutil" "net/http" + "strings" "sync" + "github.com/gorilla/mux" "github.com/rockbears/log" "gopkg.in/spacemonkeygo/httpsig.v0" @@ -31,7 +39,10 @@ func (s *Service) initRouter(ctx context.Context) { r.Handle("/mon/metrics/all", nil, r.GET(service.GetMetricsHandler, service.OverrideAuth(service.NoAuthMiddleware))) r.Handle("/v2/webhook/repository", nil, r.POST(s.repositoryHooksHandler, service.OverrideAuth(CheckWebhookRequestSignatureMiddleware(s.WebHooksParsedPublicKey)))) - r.Handle("/v2/webhook/repository/gitea/{uuid}", nil, r.POST(s.repositoryWebHookHandler, service.OverrideAuth(service.NoAuthMiddleware))) + r.Handle("/v2/webhook/repository/gitea/{uuid}", nil, r.POST(s.repositoryWebHookHandler, service.OverrideAuth(s.CheckHmac256Signature("X-Hub-Signature-256")))) + r.Handle("/v2/webhook/repository/github/{uuid}", nil, r.POST(s.repositoryWebHookHandler, service.OverrideAuth(s.CheckHmac256Signature("X-Hub-Signature-256")))) + r.Handle("/v2/webhook/repository/bitbucketserver/{uuid}", nil, r.POST(s.repositoryWebHookHandler, service.OverrideAuth(s.CheckHmac256Signature("X-Hub-Signature")))) + r.Handle("/v2/webhook/repository/gitlab/{uuid}", nil, r.POST(s.repositoryWebHookHandler, service.OverrideAuth(s.CheckHeaderToken("X-Gitlab-Token")))) r.Handle("/v2/task", nil, r.POST(s.registerHookHandler)) r.Handle("/webhook/{uuid}", nil, r.POST(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware)), r.GET(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware)), r.DELETE(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware)), r.PUT(s.webhookHandler, service.OverrideAuth(service.NoAuthMiddleware))) @@ -48,6 +59,65 @@ func (s *Service) initRouter(ctx context.Context) { r.Handle("/task/{uuid}/execution/{timestamp}/stop", nil, r.POST(s.postStopTaskExecutionHandler)) } +func (s *Service) CheckHeaderToken(headerName string) service.Middleware { + return func(ctx context.Context, w http.ResponseWriter, req *http.Request, rc *service.HandlerConfig) (context.Context, error) { + tokenHeaderValue := req.Header.Get(headerName) + if tokenHeaderValue == "" { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to check token") + } + vars := mux.Vars(req) + uuid := vars["uuid"] + + hook, err := s.Client.RepositoryHook(ctx, uuid) + if err != nil { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to retrieve sign key") + } + + defer req.Body.Close() + + if tokenHeaderValue != hook.HookSignKey { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "token mismatch") + } + return ctx, nil + } +} + +func (s *Service) CheckHmac256Signature(headerName string) service.Middleware { + return func(ctx context.Context, w http.ResponseWriter, req *http.Request, rc *service.HandlerConfig) (context.Context, error) { + signHeaderValue := req.Header.Get(headerName) + if signHeaderValue == "" { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to check signature") + } + vars := mux.Vars(req) + uuid := vars["uuid"] + + hook, err := s.Client.RepositoryHook(ctx, uuid) + if err != nil { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to retrieve sign key") + } + + defer req.Body.Close() + body, err := io.ReadAll(req.Body) + if err != nil { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to check signature") + } + + newRequestBody := ioutil.NopCloser(bytes.NewBuffer(body)) + req.Body = newRequestBody + + // Create a new HMAC by defining the hash type and the key (as byte array) + h := hmac.New(sha256.New, []byte(hook.HookSignKey)) + h.Write(body) + sha := hex.EncodeToString(h.Sum(nil)) + + if strings.TrimPrefix(signHeaderValue, "sha256=") != sha { + log.Error(ctx, "signature mismatch: got %s, compute %s", signHeaderValue, sha) + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "wrong signature") + } + return ctx, nil + } +} + type webhookHttpVerifier struct { sync.Mutex pubKey *rsa.PublicKey diff --git a/engine/sql/api/250_entitiesHooksSecret.sql b/engine/sql/api/250_entitiesHooksSecret.sql new file mode 100644 index 0000000000..00b383a0a0 --- /dev/null +++ b/engine/sql/api/250_entitiesHooksSecret.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE project_repository ADD COLUMN hook_sign_key BYTEA; + +-- +migrate Down +ALTER TABLE project_repository DROP COLUMN hook_sign_key; diff --git a/engine/vcs/gitea/client_commit.go b/engine/vcs/gitea/client_commit.go index 4ffb207242..56cfd6cde5 100644 --- a/engine/vcs/gitea/client_commit.go +++ b/engine/vcs/gitea/client_commit.go @@ -2,9 +2,9 @@ package gitea import ( "context" - "strings" - + gg "code.gitea.io/sdk/gitea" + "github.com/ovh/cds/sdk" ) @@ -21,8 +21,8 @@ func (g *giteaClient) Commit(_ context.Context, repo, hash string) (sdk.VCSCommi if err != nil { return sdk.VCSCommit{}, err } - if giteaCommit.RepoCommit == nil || giteaCommit.RepoCommit.Author == nil { - return sdk.VCSCommit{}, sdk.NewErrorFrom(sdk.ErrNotFound, "commit data not found") + if giteaCommit.RepoCommit == nil { + return sdk.VCSCommit{}, sdk.NewErrorFrom(sdk.ErrNotFound, "commit %s data not found", hash) } return g.toVCSCommit(giteaCommit), nil } @@ -32,25 +32,34 @@ func (g *giteaClient) CommitsBetweenRefs(ctx context.Context, repo, base, head s } func (g *giteaClient) toVCSCommit(commit *gg.Commit) sdk.VCSCommit { + vcsCommit := sdk.VCSCommit{ - KeySignID: "", + Signature: "", Verified: false, Message: commit.RepoCommit.Message, Hash: commit.SHA, URL: commit.URL, - Author: sdk.VCSAuthor{ + Timestamp: commit.Created.Unix() * 1000, + } + if commit.Committer != nil { + vcsCommit.Author = sdk.VCSAuthor{ Name: commit.Committer.UserName, Avatar: commit.Committer.AvatarURL, Email: commit.Committer.Email, DisplayName: commit.Committer.UserName, - }, - Timestamp: commit.Created.Unix() * 1000, + } + } else { + if commit.RepoCommit.Committer != nil { + vcsCommit.Author = sdk.VCSAuthor{ + Name: commit.RepoCommit.Committer.Name, + Email: commit.RepoCommit.Committer.Email, + DisplayName: commit.RepoCommit.Committer.Name, + } + } } if commit.RepoCommit.Verification != nil && commit.RepoCommit.Verification.Signature != "" { - reasonSplitted := strings.Split(commit.RepoCommit.Verification.Reason, " ") - vcsCommit.KeySignID = reasonSplitted[len(reasonSplitted)-1] + vcsCommit.Signature = commit.RepoCommit.Verification.Signature vcsCommit.Verified = commit.RepoCommit.Verification.Verified - } return vcsCommit } diff --git a/engine/vcs/github/client_commit.go b/engine/vcs/github/client_commit.go index 32c17a79cd..7ff28b85ff 100644 --- a/engine/vcs/github/client_commit.go +++ b/engine/vcs/github/client_commit.go @@ -267,7 +267,9 @@ func (g *githubClient) Commit(ctx context.Context, repo, hash string) (sdk.VCSCo Name: c.Author.Login, Avatar: c.Author.AvatarURL, }, - URL: c.HTMLURL, + URL: c.HTMLURL, + Verified: c.Commit.Verification.Verified, + Signature: c.Commit.Verification.Signature, } return commit, nil diff --git a/engine/vcs/github/client_file.go b/engine/vcs/github/client_file.go index 799db13c05..f61943f45d 100644 --- a/engine/vcs/github/client_file.go +++ b/engine/vcs/github/client_file.go @@ -2,20 +2,96 @@ package github import ( "context" + "fmt" "io" "net/http" + "github.com/rockbears/log" + + "github.com/ovh/cds/engine/cache" "github.com/ovh/cds/sdk" ) -func (g *githubClient) ListContent(_ context.Context, repo string, commit, dir string) ([]sdk.VCSContent, error) { - return nil, sdk.WithStack(sdk.ErrNotImplemented) +func (g *githubClient) ListContent(ctx context.Context, repo string, commit, dir string) ([]sdk.VCSContent, error) { + url := fmt.Sprintf("/repos/%s/contents/%s?ref=%s", repo, dir, commit) + status, body, _, err := g.get(ctx, url) + if err != nil { + log.Warn(ctx, "githubClient.ListContent> Error %s", err) + return nil, err + } + if status >= 400 { + return nil, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body)) + } + var c []Content + + //Github may return 304 status because we are using conditional request with ETag based headers + if status == http.StatusNotModified { + //If repo isn't updated, lets get them from cache + k := cache.Key("vcs", "github", "content", sdk.Hash512(g.OAuthToken+g.username), url) + if _, err := g.Cache.Get(k, &c); err != nil { + log.Error(ctx, "cannot get from cache %s: %v", k, err) + } + } else { + if err := sdk.JSONUnmarshal(body, &c); err != nil { + log.Warn(ctx, "githubClient.ListContent> Unable to parse github content: %s", err) + return nil, err + } + //Put the body on cache for one hour and one minute + k := cache.Key("vcs", "github", "content", sdk.Hash512(g.OAuthToken+g.username), url) + if err := g.Cache.SetWithTTL(k, c, 61*60); err != nil { + log.Error(ctx, "cannot SetWithTTL: %s: %v", k, err) + } + } + contents := make([]sdk.VCSContent, 0, len(c)) + for _, co := range c { + contents = append(contents, g.ToVCSContent(co)) + } + return contents, nil + } func (g *githubClient) GetContent(ctx context.Context, repo string, commit, filePath string) (sdk.VCSContent, error) { - return sdk.VCSContent{}, sdk.WithStack(sdk.ErrNotImplemented) + url := fmt.Sprintf("/repos/%s/contents/%s?ref=%s", repo, filePath, commit) + status, body, _, err := g.get(ctx, url) + if err != nil { + log.Warn(ctx, "githubClient.ListContent> Error %s", err) + return sdk.VCSContent{}, err + } + if status >= 400 { + return sdk.VCSContent{}, sdk.NewError(sdk.ErrRepoNotFound, errorAPI(body)) + } + var c Content + + //Github may return 304 status because we are using conditional request with ETag based headers + if status == http.StatusNotModified { + //If repo isn't updated, lets get them from cache + k := cache.Key("vcs", "github", "content", sdk.Hash512(g.OAuthToken+g.username), url) + if _, err := g.Cache.Get(k, &c); err != nil { + log.Error(ctx, "cannot get from cache %s: %v", k, err) + } + } else { + if err := sdk.JSONUnmarshal(body, &c); err != nil { + log.Warn(ctx, "githubClient.ListContent> Unable to parse github content: %s", err) + return sdk.VCSContent{}, err + } + //Put the body on cache for one hour and one minute + k := cache.Key("vcs", "github", "content", sdk.Hash512(g.OAuthToken+g.username), url) + if err := g.Cache.SetWithTTL(k, c, 61*60); err != nil { + log.Error(ctx, "cannot SetWithTTL: %s: %v", k, err) + } + } + return g.ToVCSContent(c), nil } func (g *githubClient) GetArchive(ctx context.Context, repo, dir, format, commit string) (io.Reader, http.Header, error) { return nil, nil, sdk.WithStack(sdk.ErrNotImplemented) } + +func (g *githubClient) ToVCSContent(c Content) sdk.VCSContent { + return sdk.VCSContent{ + Content: c.Content, + Name: c.Name, + IsFile: c.Type == "file", + IsDirectory: c.Type == "dir", + } +} diff --git a/engine/vcs/github/types.go b/engine/vcs/github/types.go index 229b4d15b9..11c2a2bbea 100644 --- a/engine/vcs/github/types.go +++ b/engine/vcs/github/types.go @@ -263,6 +263,12 @@ type Commit struct { } `json:"tree"` URL string `json:"url"` CommentCount int `json:"comment_count"` + Verification struct { + Verified bool `json:"verified"` + Reason string `json:"reason"` + Signature string `json:"signature"` + Payload string `json:"payload"` + } `json:"verification"` } `json:"commit"` URL string `json:"url"` HTMLURL string `json:"html_url"` @@ -704,3 +710,21 @@ type UserPermissionResponse struct { Permission string `json:"permission"` User GithubOwner `json:"user"` } + +type Content struct { + Name string `json:"name"` + Path string `json:"path"` + Sha string `json:"sha"` + Size int `json:"size"` + Url string `json:"url"` + HtmlUrl string `json:"html_url"` + GitUrl string `json:"git_url"` + DownloadUrl string `json:"download_url"` + Type string `json:"type"` + Content string `json:"content"` + Links struct { + Self string `json:"self"` + Git string `json:"git"` + Html string `json:"html"` + } `json:"_links"` +} diff --git a/sdk/cdsclient/client_hook.go b/sdk/cdsclient/client_hook.go index 7c092f1eed..6087031c31 100644 --- a/sdk/cdsclient/client_hook.go +++ b/sdk/cdsclient/client_hook.go @@ -33,3 +33,10 @@ func (c *client) RepositoriesListAll(ctx context.Context) ([]sdk.ProjectReposito _, err := c.GetJSON(ctx, url, &repos) return repos, err } + +func (c *client) RepositoryHook(ctx context.Context, uuid string) (sdk.Hook, error) { + url := fmt.Sprintf("/v2/project/repositories/%s/hook", uuid) + var h sdk.Hook + _, err := c.GetJSON(ctx, url, &h) + return h, err +} diff --git a/sdk/cdsclient/client_project_repository.go b/sdk/cdsclient/client_project_repository.go index f6a4d4e73e..06134ed0dd 100644 --- a/sdk/cdsclient/client_project_repository.go +++ b/sdk/cdsclient/client_project_repository.go @@ -51,3 +51,10 @@ func (c *client) ProjectRepositoryAnalysisGet(ctx context.Context, projectKey st _, err := c.GetJSON(ctx, path, &analysis) return analysis, err } + +func (c *client) ProjectRepositoryHookRegenSecret(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { + path := fmt.Sprintf("/v2/project/%s/vcs/%s/repository/%s/hook/regen", projectKey, url.PathEscape(vcsName), url.PathEscape(repoName)) + var hookData sdk.HookAccessData + _, err := c.PostJSON(ctx, path, nil, &hookData) + return hookData, err +} diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go index 3cf1619ea5..7e239c8f88 100644 --- a/sdk/cdsclient/interface.go +++ b/sdk/cdsclient/interface.go @@ -224,6 +224,7 @@ type ProjectClient interface { ProjectVCSDelete(ctx context.Context, projectKey string, vcsName string) error ProjectVCSRepositoryAdd(ctx context.Context, projectKey string, vcsName string, repo sdk.ProjectRepository) error ProjectVCSRepositoryList(ctx context.Context, projectKey string, vcsName string) ([]sdk.ProjectRepository, error) + ProjectRepositoryHookRegenSecret(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) ProjectRepositoryDelete(ctx context.Context, projectKey string, vcsName string, repositoryName string) error ProjectRepositoryAnalysis(ctx context.Context, analysis sdk.AnalysisRequest) (sdk.AnalysisResponse, error) ProjectRepositoryAnalysisList(ctx context.Context, projectKey string, vcsIdentifier string, repositoryIdentifier string) ([]sdk.ProjectRepositoryAnalysis, error) @@ -319,6 +320,7 @@ type HookClient interface { VCSConfiguration() (map[string]sdk.VCSConfiguration, error) VCSGerritConfiguration() (map[string]sdk.VCSGerritConfiguration, error) RepositoriesListAll(ctx context.Context) ([]sdk.ProjectRepository, error) + RepositoryHook(ctx context.Context, uuid string) (sdk.Hook, error) } // ServiceClient exposes functions used for services diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go index e870593658..d4ca5c618e 100644 --- a/sdk/cdsclient/mock_cdsclient/interface_mock.go +++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go @@ -2568,49 +2568,49 @@ func (mr *MockProjectClientMockRecorder) ProjectList(withApplications, withWorkf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectList", reflect.TypeOf((*MockProjectClient)(nil).ProjectList), varargs...) } -// ProjectRepositoryAnalyze mocks base method. -func (m *MockProjectClient) ProjectRepositoryAnalysis(ctx context.Context, analyze sdk.AnalysisRequest) (sdk.AnalysisResponse, error) { +// ProjectRepositoryAnalysis mocks base method. +func (m *MockProjectClient) ProjectRepositoryAnalysis(ctx context.Context, analysis sdk.AnalysisRequest) (sdk.AnalysisResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProjectRepositoryAnalysis", ctx, analyze) + ret := m.ctrl.Call(m, "ProjectRepositoryAnalysis", ctx, analysis) ret0, _ := ret[0].(sdk.AnalysisResponse) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryAnalyze indicates an expected call of ProjectRepositoryAnalyze. -func (mr *MockProjectClientMockRecorder) ProjectRepositoryAnalyze(ctx, analyze interface{}) *gomock.Call { +// ProjectRepositoryAnalysis indicates an expected call of ProjectRepositoryAnalysis. +func (mr *MockProjectClientMockRecorder) ProjectRepositoryAnalysis(ctx, analysis interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysis", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryAnalysis), ctx, analyze) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysis", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryAnalysis), ctx, analysis) } -// ProjectRepositoryAnalyzeGet mocks base method. -func (m *MockProjectClient) ProjectRepositoryAnalysisGet(ctx context.Context, projectKey, vcsName, repositoryName, analyzeID string) (sdk.ProjectRepositoryAnalysis, error) { +// ProjectRepositoryAnalysisGet mocks base method. +func (m *MockProjectClient) ProjectRepositoryAnalysisGet(ctx context.Context, projectKey, vcsIdentifier, repositoryIdentifier, analysisID string) (sdk.ProjectRepositoryAnalysis, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisGet", ctx, projectKey, vcsName, repositoryName, analyzeID) + ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisGet", ctx, projectKey, vcsIdentifier, repositoryIdentifier, analysisID) ret0, _ := ret[0].(sdk.ProjectRepositoryAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryAnalyzeGet indicates an expected call of ProjectRepositoryAnalyzeGet. -func (mr *MockProjectClientMockRecorder) ProjectRepositoryAnalyzeGet(ctx, projectKey, vcsName, repositoryName, analyzeID interface{}) *gomock.Call { +// ProjectRepositoryAnalysisGet indicates an expected call of ProjectRepositoryAnalysisGet. +func (mr *MockProjectClientMockRecorder) ProjectRepositoryAnalysisGet(ctx, projectKey, vcsIdentifier, repositoryIdentifier, analysisID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisGet", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryAnalysisGet), ctx, projectKey, vcsName, repositoryName, analyzeID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisGet", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryAnalysisGet), ctx, projectKey, vcsIdentifier, repositoryIdentifier, analysisID) } -// ProjectRepositoryAnalyzeList mocks base method. -func (m *MockProjectClient) ProjectRepositoryAnalysisList(ctx context.Context, projectKey, vcsName, repositoryName string) ([]sdk.ProjectRepositoryAnalysis, error) { +// ProjectRepositoryAnalysisList mocks base method. +func (m *MockProjectClient) ProjectRepositoryAnalysisList(ctx context.Context, projectKey, vcsIdentifier, repositoryIdentifier string) ([]sdk.ProjectRepositoryAnalysis, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisList", ctx, projectKey, vcsName, repositoryName) + ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisList", ctx, projectKey, vcsIdentifier, repositoryIdentifier) ret0, _ := ret[0].([]sdk.ProjectRepositoryAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryAnalyzeList indicates an expected call of ProjectRepositoryAnalyzeList. -func (mr *MockProjectClientMockRecorder) ProjectRepositoryAnalyzeList(ctx, projectKey, vcsName, repositoryName interface{}) *gomock.Call { +// ProjectRepositoryAnalysisList indicates an expected call of ProjectRepositoryAnalysisList. +func (mr *MockProjectClientMockRecorder) ProjectRepositoryAnalysisList(ctx, projectKey, vcsIdentifier, repositoryIdentifier interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisList", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryAnalysisList), ctx, projectKey, vcsName, repositoryName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisList", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryAnalysisList), ctx, projectKey, vcsIdentifier, repositoryIdentifier) } // ProjectRepositoryDelete mocks base method. @@ -2627,6 +2627,21 @@ func (mr *MockProjectClientMockRecorder) ProjectRepositoryDelete(ctx, projectKey return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryDelete", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryDelete), ctx, projectKey, vcsName, repositoryName) } +// ProjectRepositoryHookRegenSecret mocks base method. +func (m *MockProjectClient) ProjectRepositoryHookRegenSecret(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectRepositoryHookRegenSecret", ctx, projectKey, vcsName, repoName) + ret0, _ := ret[0].(sdk.HookAccessData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProjectRepositoryHookRegenSecret indicates an expected call of ProjectRepositoryHookRegenSecret. +func (mr *MockProjectClientMockRecorder) ProjectRepositoryHookRegenSecret(ctx, projectKey, vcsName, repoName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryHookRegenSecret", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryHookRegenSecret), ctx, projectKey, vcsName, repoName) +} + // ProjectRepositoryManagerDelete mocks base method. func (m *MockProjectClient) ProjectRepositoryManagerDelete(projectKey, repoManagerName string, force bool) error { m.ctrl.T.Helper() @@ -3987,6 +4002,21 @@ func (mr *MockHookClientMockRecorder) RepositoriesListAll(ctx interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoriesListAll", reflect.TypeOf((*MockHookClient)(nil).RepositoriesListAll), ctx) } +// RepositoryHook mocks base method. +func (m *MockHookClient) RepositoryHook(ctx context.Context, uuid string) (sdk.Hook, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RepositoryHook", ctx, uuid) + ret0, _ := ret[0].(sdk.Hook) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RepositoryHook indicates an expected call of RepositoryHook. +func (mr *MockHookClientMockRecorder) RepositoryHook(ctx, uuid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoryHook", reflect.TypeOf((*MockHookClient)(nil).RepositoryHook), ctx, uuid) +} + // VCSConfiguration mocks base method. func (m *MockHookClient) VCSConfiguration() (map[string]sdk.VCSConfiguration, error) { m.ctrl.T.Helper() @@ -6866,49 +6896,49 @@ func (mr *MockInterfaceMockRecorder) ProjectList(withApplications, withWorkflow return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectList", reflect.TypeOf((*MockInterface)(nil).ProjectList), varargs...) } -// ProjectRepositoryAnalyze mocks base method. -func (m *MockInterface) ProjectRepositoryAnalysis(ctx context.Context, analyze sdk.AnalysisRequest) (sdk.AnalysisResponse, error) { +// ProjectRepositoryAnalysis mocks base method. +func (m *MockInterface) ProjectRepositoryAnalysis(ctx context.Context, analysis sdk.AnalysisRequest) (sdk.AnalysisResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProjectRepositoryAnalysis", ctx, analyze) + ret := m.ctrl.Call(m, "ProjectRepositoryAnalysis", ctx, analysis) ret0, _ := ret[0].(sdk.AnalysisResponse) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryAnalyze indicates an expected call of ProjectRepositoryAnalyze. -func (mr *MockInterfaceMockRecorder) ProjectRepositoryAnalyze(ctx, analyze interface{}) *gomock.Call { +// ProjectRepositoryAnalysis indicates an expected call of ProjectRepositoryAnalysis. +func (mr *MockInterfaceMockRecorder) ProjectRepositoryAnalysis(ctx, analysis interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysis", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryAnalysis), ctx, analyze) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysis", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryAnalysis), ctx, analysis) } -// ProjectRepositoryAnalyzeGet mocks base method. -func (m *MockInterface) ProjectRepositoryAnalysisGet(ctx context.Context, projectKey, vcsName, repositoryName, analyzeID string) (sdk.ProjectRepositoryAnalysis, error) { +// ProjectRepositoryAnalysisGet mocks base method. +func (m *MockInterface) ProjectRepositoryAnalysisGet(ctx context.Context, projectKey, vcsIdentifier, repositoryIdentifier, analysisID string) (sdk.ProjectRepositoryAnalysis, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisGet", ctx, projectKey, vcsName, repositoryName, analyzeID) + ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisGet", ctx, projectKey, vcsIdentifier, repositoryIdentifier, analysisID) ret0, _ := ret[0].(sdk.ProjectRepositoryAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryAnalyzeGet indicates an expected call of ProjectRepositoryAnalyzeGet. -func (mr *MockInterfaceMockRecorder) ProjectRepositoryAnalyzeGet(ctx, projectKey, vcsName, repositoryName, analyzeID interface{}) *gomock.Call { +// ProjectRepositoryAnalysisGet indicates an expected call of ProjectRepositoryAnalysisGet. +func (mr *MockInterfaceMockRecorder) ProjectRepositoryAnalysisGet(ctx, projectKey, vcsIdentifier, repositoryIdentifier, analysisID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisGet", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryAnalysisGet), ctx, projectKey, vcsName, repositoryName, analyzeID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisGet", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryAnalysisGet), ctx, projectKey, vcsIdentifier, repositoryIdentifier, analysisID) } -// ProjectRepositoryAnalyzeList mocks base method. -func (m *MockInterface) ProjectRepositoryAnalysisList(ctx context.Context, projectKey, vcsName, repositoryName string) ([]sdk.ProjectRepositoryAnalysis, error) { +// ProjectRepositoryAnalysisList mocks base method. +func (m *MockInterface) ProjectRepositoryAnalysisList(ctx context.Context, projectKey, vcsIdentifier, repositoryIdentifier string) ([]sdk.ProjectRepositoryAnalysis, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisList", ctx, projectKey, vcsName, repositoryName) + ret := m.ctrl.Call(m, "ProjectRepositoryAnalysisList", ctx, projectKey, vcsIdentifier, repositoryIdentifier) ret0, _ := ret[0].([]sdk.ProjectRepositoryAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryAnalyzeList indicates an expected call of ProjectRepositoryAnalyzeList. -func (mr *MockInterfaceMockRecorder) ProjectRepositoryAnalyzeList(ctx, projectKey, vcsName, repositoryName interface{}) *gomock.Call { +// ProjectRepositoryAnalysisList indicates an expected call of ProjectRepositoryAnalysisList. +func (mr *MockInterfaceMockRecorder) ProjectRepositoryAnalysisList(ctx, projectKey, vcsIdentifier, repositoryIdentifier interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisList", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryAnalysisList), ctx, projectKey, vcsName, repositoryName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryAnalysisList", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryAnalysisList), ctx, projectKey, vcsIdentifier, repositoryIdentifier) } // ProjectRepositoryDelete mocks base method. @@ -6925,6 +6955,21 @@ func (mr *MockInterfaceMockRecorder) ProjectRepositoryDelete(ctx, projectKey, vc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryDelete", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryDelete), ctx, projectKey, vcsName, repositoryName) } +// ProjectRepositoryHookRegenSecret mocks base method. +func (m *MockInterface) ProjectRepositoryHookRegenSecret(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectRepositoryHookRegenSecret", ctx, projectKey, vcsName, repoName) + ret0, _ := ret[0].(sdk.HookAccessData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProjectRepositoryHookRegenSecret indicates an expected call of ProjectRepositoryHookRegenSecret. +func (mr *MockInterfaceMockRecorder) ProjectRepositoryHookRegenSecret(ctx, projectKey, vcsName, repoName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryHookRegenSecret", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryHookRegenSecret), ctx, projectKey, vcsName, repoName) +} + // ProjectRepositoryManagerDelete mocks base method. func (m *MockInterface) ProjectRepositoryManagerDelete(projectKey, repoManagerName string, force bool) error { m.ctrl.T.Helper() @@ -7465,6 +7510,21 @@ func (mr *MockInterfaceMockRecorder) RepositoriesListAll(ctx interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoriesListAll", reflect.TypeOf((*MockInterface)(nil).RepositoriesListAll), ctx) } +// RepositoryHook mocks base method. +func (m *MockInterface) RepositoryHook(ctx context.Context, uuid string) (sdk.Hook, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RepositoryHook", ctx, uuid) + ret0, _ := ret[0].(sdk.Hook) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RepositoryHook indicates an expected call of RepositoryHook. +func (mr *MockInterfaceMockRecorder) RepositoryHook(ctx, uuid interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoryHook", reflect.TypeOf((*MockInterface)(nil).RepositoryHook), ctx, uuid) +} + // Request mocks base method. func (m *MockInterface) Request(ctx context.Context, method, path string, body io.Reader, mods ...cdsclient.RequestModifier) ([]byte, http.Header, int, error) { m.ctrl.T.Helper() diff --git a/sdk/gpg/gpg.go b/sdk/gpg/gpg.go index 73e61313e1..77c89e9c5f 100644 --- a/sdk/gpg/gpg.go +++ b/sdk/gpg/gpg.go @@ -309,3 +309,25 @@ func (k PublicKey) GetKey() interface{} { } return fmt.Errorf("unsupported key type/format: %T", pk) } + +func GetKeyIdFromSignature(signature string) (string, error) { + block, err := armor.Decode(bytes.NewReader([]byte(signature))) + if err != nil { + return "", errors.Wrapf(err, "unable to decode signature %s", signature) + } + if block == nil { + return "", errors.Wrapf(err, "unable to decode signature, block nil: %s", signature) + } + + reader := packet.NewReader(block.Body) + pkt, err := reader.Next() + if err != nil { + return "", errors.Wrap(err, "unable to read block body") + } + + key, ok := pkt.(*packet.Signature) + if !ok || key == nil { + return "", errors.Wrap(err, "unable to cast packet") + } + return fmt.Sprintf("%X", *key.IssuerKeyId), nil +} diff --git a/sdk/hook.go b/sdk/hook.go index 9fe831dbc5..9a739c05a6 100644 --- a/sdk/hook.go +++ b/sdk/hook.go @@ -1,5 +1,10 @@ package sdk +import ( + "crypto/rand" + "encoding/base64" +) + // These are constants about hooks const ( WebHookModelName = "WebHook" @@ -286,3 +291,11 @@ func GetDefaultHookModel(modelName string) WorkflowHookModel { return WebHookModel } + +func GenerateHookSecret() (string, error) { + b := make([]byte, 128) + if _, err := rand.Read(b); err != nil { + return "", WrapError(err, "unable to generate hook secret") + } + return base64.URLEncoding.EncodeToString(b), nil +} diff --git a/sdk/hooks.go b/sdk/hooks.go index 8c3b95ec0f..5ca4a3e3a7 100644 --- a/sdk/hooks.go +++ b/sdk/hooks.go @@ -14,10 +14,16 @@ const ( SignHeaderVCSType = "X-Cds-Hooks-Vcs-Type" ) +type HookAccessData struct { + URL string `json:"url" cli:"url"` + HookSignKey string `json:"hook_sign_key" cli:"hook_sign_key"` +} + type Hook struct { - UUID string - HookType string - Configuration HookConfiguration + UUID string `json:"uuid"` + HookType string `json:"hook_type"` + Configuration HookConfiguration `json:"configuration"` + HookSignKey string `json:"hook_sign_key,omitempty"` } type HookConfiguration map[string]WorkflowNodeHookConfigValue @@ -37,7 +43,11 @@ func (hc *HookConfiguration) Scan(src interface{}) error { return WrapError(JSONUnmarshal(source, hc), "cannot unmarshal HookConfiguration") } -func NewEntitiesHook(uuid, projectKey, vcsType, vcsName, repoName string) Hook { +func NewEntitiesHook(uuid, projectKey, vcsType, vcsName, repoName string) (Hook, error) { + hookSignKey, err := GenerateHookSecret() + if err != nil { + return Hook{}, err + } return Hook{ UUID: uuid, HookType: RepositoryEntitiesHook, @@ -63,7 +73,8 @@ func NewEntitiesHook(uuid, projectKey, vcsType, vcsName, repoName string) Hook { Configurable: false, }, }, - } + HookSignKey: hookSignKey, + }, nil } // HookConfigValue represents the value of a node hook config @@ -129,8 +140,7 @@ type EntitiesHookExecution struct { RequestMethod string `json:"request_method"` // Execution result - AnalysisID string `json:"analysis_id"` - OperationID string `json:"operation_id"` + AnalysisID string `json:"analysis_id"` } // KafkaTaskExecution contains specific data for a kafka hook @@ -157,6 +167,5 @@ type AnalysisRequest struct { } type AnalysisResponse struct { - AnalysisID string `json:"analysis_id"` - OperationID string `json:"operation_id"` + AnalysisID string `json:"analysis_id"` } diff --git a/sdk/repositories_manager.go b/sdk/repositories_manager.go index 36de858a8d..29758b37c5 100644 --- a/sdk/repositories_manager.go +++ b/sdk/repositories_manager.go @@ -58,7 +58,7 @@ type VCSCommit struct { Message string `json:"message"` URL string `json:"url"` Verified bool `json:"verified"` - KeySignID string `json:"keySignID"` + Signature string `json:"signature"` } //VCSRemote represents remotes known by the repositories manager diff --git a/sdk/repository.go b/sdk/repository.go index ae6e4cc8e3..b343c74894 100644 --- a/sdk/repository.go +++ b/sdk/repository.go @@ -21,6 +21,7 @@ type ProjectRepository struct { CreatedBy string `json:"created_by" db:"created_by"` VCSProjectID string `json:"-" db:"vcs_project_id"` HookConfiguration HookConfiguration `json:"hook_configuration" db:"hook_configuration"` + HookSignKey string `json:"hook_sign_key" db:"hook_sign_key" gorpmapping:"encrypted,ID,VCSProjectID"` CloneURL string `json:"clone_url" db:"clone_url"` Auth ProjectRepositoryAuth `json:"auth" db:"auth" gorpmapping:"encrypted,ID,VCSProjectID"` } diff --git a/tests/08_v2_analyze.yml b/tests/08_v2_analyze.yml index 7d2a8d2f01..5d8c3ba8cc 100644 --- a/tests/08_v2_analyze.yml +++ b/tests/08_v2_analyze.yml @@ -46,7 +46,11 @@ testcases: - script: mkdir -p /tmp/myrepo/.cds/models && echo 'new file' > /tmp/myrepo/.cds/models/my-worker-model.yml - script: cd /tmp/myrepo && git config user.email "{{.git.user}}@gitea.eu" && git config user.name "{{.git.user}}" - script: cd /tmp/myrepo && git add /tmp/myrepo/.cds/models/my-worker-model.yml && git commit . --gpg-sign=2B74B3591CEFB2F534265465E027B500E97E52E7 -m "add file and sign" && git push - - script: 'curl -H "Content-Type: application/json" -u "{{.git.user}}:{{.git.password}}" -X POST {{.git.host}}/api/v1/repos/{{.git.user}}/myrepo/hooks -d "{\"active\":true, \"type\":\"gitea\", \"config\": {\"content_type\": \"json\", \"url\": \"{{.gitea.hook.url}}/v2/webhook/repository/gitea/{{.repositoryProject.repoID}}\"}}"' + - script: {{.cdsctl}} -f {{.cdsctl.config}} experimental project repository hook-regen ITCLIPRJVCS my_vcs_server {{.git.user}}/myrepo --format json + vars: + hookSecret: + from: result.systemoutjson.hook_sign_key + - script: 'curl -H "Content-Type: application/json" -u "{{.git.user}}:{{.git.password}}" -X POST {{.git.host}}/api/v1/repos/{{.git.user}}/myrepo/hooks -d "{\"active\":true, \"type\":\"gitea\", \"config\": {\"secret\": \"{{.hookSecret}}\", \"content_type\": \"json\", \"url\": \"{{.gitea.hook.url}}/v2/webhook/repository/gitea/{{.repositoryProject.repoID}}\"}}"' vars: hookID: from: result.systemoutjson.id