From 106b2ce4d2e31c906e406bfa451406f1299e204e Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Tue, 23 Aug 2022 17:09:19 +0200 Subject: [PATCH 01/10] feat(api,hooks): add v2 hook signature and github repository analysis --- cli/cdsctl/experimental_project_repository.go | 17 +++ engine/api/api_routes.go | 3 + .../repository/dao_vcs_project_repository.go | 42 +++--- engine/api/v2_project.go | 27 ++++ engine/api/v2_project_repository.go | 53 ++++++- engine/api/v2_repository_analyze.go | 16 ++- engine/hooks/entitieshook.go | 30 ++-- engine/hooks/hooks_handlers.go | 12 +- engine/hooks/hooks_router.go | 76 +++++++++- engine/sql/api/249_entitiesHooksSecret.sql | 5 + engine/vcs/gitea/client_commit.go | 31 ++-- engine/vcs/github/client_commit.go | 4 +- engine/vcs/github/client_file.go | 82 ++++++++++- engine/vcs/github/types.go | 24 ++++ sdk/cdsclient/client_hook.go | 7 + sdk/cdsclient/client_project_repository.go | 7 + sdk/cdsclient/interface.go | 2 + .../mock_cdsclient/interface_mock.go | 132 +++++++++++++----- sdk/gpg/gpg.go | 22 +++ sdk/hook.go | 13 ++ sdk/hooks.go | 21 ++- sdk/repositories_manager.go | 2 +- sdk/repository.go | 1 + tests/08_v2_analyze.yml | 6 +- 24 files changed, 537 insertions(+), 98 deletions(-) create mode 100644 engine/sql/api/249_entitiesHooksSecret.sql diff --git a/cli/cdsctl/experimental_project_repository.go b/cli/cdsctl/experimental_project_repository.go index bcf4c87a0e..7e66d3a242 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(projectRepositoryHookCreateCmd, projectRepositoryHookCreateFunc, 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 projectRepositoryHookCreateCmd = cli.Command{ + Name: "hook", + Short: "Display data to be able to create a hook", + Ctx: []cli.Arg{ + {Name: _ProjectKey}, + }, + Args: []cli.Arg{ + {Name: "vcs-name"}, + {Name: "repository-name"}, + }, +} + +func projectRepositoryHookCreateFunc(v cli.Values) (interface{}, error) { + return client.ProjectRepositoryHookAccessLink(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 aa63a4c281..dbaedc2e4c 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -433,12 +433,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/link", nil, r.GETv2(api.getRepositoryHookLinkHandler)) 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/repository/dao_vcs_project_repository.go b/engine/api/repository/dao_vcs_project_repository.go index d4f7ab739a..47459b4a2d 100644 --- a/engine/api/repository/dao_vcs_project_repository.go +++ b/engine/api/repository/dao_vcs_project_repository.go @@ -36,15 +36,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 +57,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 +97,15 @@ 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) { + 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 sdk.ProjectRepository{}, 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/v2_project.go b/engine/api/v2_project.go index 9ff3cd2923..39375e7b5e 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 need 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..545da9ed93 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,41 @@ func (api *API) getVCSProjectRepositoryAllHandler() ([]service.RbacChecker, serv return service.WriteJSON(w, repositories, http.StatusOK) } } + +func (api *API) getRepositoryHookLinkHandler() ([]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 + } + 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: repo.HookSignKey, + } + 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..2b2f8f4834 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -5,6 +5,7 @@ import ( "compress/gzip" "context" "fmt" + "io" "net/http" "net/url" @@ -27,6 +28,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" ) @@ -230,11 +232,17 @@ func (api *API) postRepositoryAnalysisHandler() ([]service.RbacChecker, service. 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 + if vcsCommit.Signature != "" { + keyId, err := gpg.GetKeyIdFromSignature(vcsCommit.Signature) + if err != nil { + repoAnalysis.Status = sdk.RepositoryAnalysisStatusError + repoAnalysis.Data.Error = fmt.Sprintf("unable to extract keyID from signature") + } else { + repoAnalysis.Data.SignKeyID = keyId + 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) diff --git a/engine/hooks/entitieshook.go b/engine/hooks/entitieshook.go index daedb0560a..d8b4b3cf23 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) } @@ -49,16 +49,30 @@ func (s *Service) doAnalyzeExecution(ctx context.Context, t *sdk.TaskExecution) 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_handlers.go b/engine/hooks/hooks_handlers.go index 6fb48aa147..d79f220345 100644 --- a/engine/hooks/hooks_handlers.go +++ b/engine/hooks/hooks_handlers.go @@ -126,10 +126,14 @@ func (s *Service) repositoryWebHookHandler() service.Handler { vars := mux.Vars(r) uuid := vars["uuid"] - defer r.Body.Close() - body, err := io.ReadAll(r.Body) - if err != nil { - return sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read body: %v", err) + body, ok := ctx.Value("body").([]byte) + if !ok { + defer r.Body.Close() + var err error + body, err = io.ReadAll(r.Body) + if err != nil { + return sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read body: %v", err) + } } hook := s.Dao.FindTask(ctx, uuid) diff --git a/engine/hooks/hooks_router.go b/engine/hooks/hooks_router.go index 4726e9b602..deba5c5b9d 100644 --- a/engine/hooks/hooks_router.go +++ b/engine/hooks/hooks_router.go @@ -2,10 +2,16 @@ package hooks import ( "context" + "crypto/hmac" "crypto/rsa" + "crypto/sha256" + "encoding/hex" + "io" "net/http" + "strings" "sync" + "github.com/gorilla/mux" "github.com/rockbears/log" "gopkg.in/spacemonkeygo/httpsig.v0" @@ -31,7 +37,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 +57,71 @@ 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") + } + + body, err := io.ReadAll(req.Body) + if err != nil { + return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to check signature") + } + + ctx = context.WithValue(ctx, "body", body) + 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") + } + + // 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") + } + + ctx = context.WithValue(ctx, "body", body) + return ctx, nil + } +} + type webhookHttpVerifier struct { sync.Mutex pubKey *rsa.PublicKey diff --git a/engine/sql/api/249_entitiesHooksSecret.sql b/engine/sql/api/249_entitiesHooksSecret.sql new file mode 100644 index 0000000000..00b383a0a0 --- /dev/null +++ b/engine/sql/api/249_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..af75aaeb08 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) ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { + path := fmt.Sprintf("/v2/project/%s/vcs/%s/repository/%s/hook/link", projectKey, url.PathEscape(vcsName), url.PathEscape(repoName)) + var hookData sdk.HookAccessData + _, err := c.GetJSON(ctx, path, &hookData) + return hookData, err +} diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go index 11e90664c0..00e988255b 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) + ProjectRepositoryHookAccessLink(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) @@ -318,6 +319,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 77a236c048..04fa35b43e 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) } +// ProjectRepositoryHookAccessLink mocks base method. +func (m *MockProjectClient) ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectRepositoryHookAccessLink", ctx, projectKey, vcsName, repoName) + ret0, _ := ret[0].(sdk.HookAccessData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProjectRepositoryHookAccessLink indicates an expected call of ProjectRepositoryHookAccessLink. +func (mr *MockProjectClientMockRecorder) ProjectRepositoryHookAccessLink(ctx, projectKey, vcsName, repoName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryHookAccessLink", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryHookAccessLink), ctx, projectKey, vcsName, repoName) +} + // ProjectRepositoryManagerDelete mocks base method. func (m *MockProjectClient) ProjectRepositoryManagerDelete(projectKey, repoManagerName string, force bool) error { m.ctrl.T.Helper() @@ -3973,6 +3988,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() @@ -6852,49 +6882,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. @@ -6911,6 +6941,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) } +// ProjectRepositoryHookAccessLink mocks base method. +func (m *MockInterface) ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ProjectRepositoryHookAccessLink", ctx, projectKey, vcsName, repoName) + ret0, _ := ret[0].(sdk.HookAccessData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProjectRepositoryHookAccessLink indicates an expected call of ProjectRepositoryHookAccessLink. +func (mr *MockInterfaceMockRecorder) ProjectRepositoryHookAccessLink(ctx, projectKey, vcsName, repoName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryHookAccessLink", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryHookAccessLink), ctx, projectKey, vcsName, repoName) +} + // ProjectRepositoryManagerDelete mocks base method. func (m *MockInterface) ProjectRepositoryManagerDelete(projectKey, repoManagerName string, force bool) error { m.ctrl.T.Helper() @@ -7437,6 +7482,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..7cbeec749d 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 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..f0337f6f3d 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 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 From 405e80cbc74ecd7f32aafcc6228f0ba4d4ede929 Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Wed, 24 Aug 2022 10:42:39 +0200 Subject: [PATCH 02/10] fix: keep synchronous part simple --- engine/api/v2_repository_analyze.go | 391 +++++++++++--------- engine/api/v2_repository_analyze_test.go | 437 +++++++++++++++++++++++ engine/hooks/entitieshook.go | 1 - sdk/hooks.go | 6 +- 4 files changed, 671 insertions(+), 164 deletions(-) diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go index 2b2f8f4834..50a5d856a1 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -5,7 +5,6 @@ import ( "compress/gzip" "context" "fmt" - "io" "net/http" "net/url" @@ -183,74 +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.Signature != "" { - keyId, err := gpg.GetKeyIdFromSignature(vcsCommit.Signature) - if err != nil { - repoAnalysis.Status = sdk.RepositoryAnalysisStatusError - repoAnalysis.Data.Error = fmt.Sprintf("unable to extract keyID from signature") - } else { - repoAnalysis.Data.SignKeyID = keyId - repoAnalysis.Data.CommitCheck = true - } - - } else { - 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 { @@ -258,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 { @@ -334,136 +265,261 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana 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 = telemetry.Span(ctx, "api.analyzeRepository.LoadVCSByID") + vcsProject, err := vcs.LoadVCSByID(ctx, api.mustDB(), analysis.ProjectKey, analysis.VCSProjectID) + if err != nil { + next() + if sdk.ErrorIs(err, sdk.ErrNotFound) { + return api.stopAnalysis(ctx, analysis, err) + } + return err + } + next() + + _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadRepositoryByID") + repoWithSecret, err := repository.LoadRepositoryByID(ctx, api.mustDB(), analysis.ProjectRepositoryID, gorpmapping.GetOptions.WithDecryption) + if err != nil { + next() + if sdk.ErrorIs(err, sdk.ErrNotFound) { + return api.stopAnalysis(ctx, analysis, err) + } + return err + } + next() + + switch vcsProject.Type { + case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud, sdk.VCSTypeGitlab, sdk.VCSTypeGerrit: + _, next = telemetry.Span(ctx, "api.analyzeRepository.analyzeCommitSignatureThroughOperation") + if err := api.analyzeCommitSignatureThroughOperation(ctx, analysis, *vcsProject, repoWithSecret); err != nil { next() return err } next() - - stopAnalyze := false - if ope.Status == sdk.OperationStatusDone && ope.Setup.Checkout.Result.CommitVerified { - analysis.Data.CommitCheck = true - analysis.Data.SignKeyID = ope.Setup.Checkout.Result.SignKeyID + case sdk.VCSTypeGitea, sdk.VCSTypeGithub: + _, next = telemetry.Span(ctx, "api.analyzeRepository.analyzeCommitSignatureThroughVcsAPI") + if err := api.analyzeCommitSignatureThroughVcsAPI(ctx, analysis, *vcsProject, repoWithSecret); err != nil { + next() + return 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 + next() + default: + return 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 sdk.WithStack(err) + } + defer tx.Rollback() //nolint + + if analysis.Status == sdk.RepositoryAnalysisStatusInProgress { + var cdsUser *sdk.AuthentifiedUser + _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadGPGKeyByKeyID") + gpgKey, err := user.LoadGPGKeyByKeyID(ctx, tx, analysis.Data.SignKeyID) + if err != nil { + if !sdk.ErrorIs(err, sdk.ErrNotFound) { + next() + return 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) } + next() - if ope.Status == sdk.OperationStatusError { - analysis.Data.Error = ope.Error.Message - analysis.Status = sdk.RepositoryAnalysisStatusError - stopAnalyze = true + if gpgKey != nil { + _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadByID") + cdsUser, err = user.LoadByID(ctx, tx, gpgKey.AuthentifiedUserID) + if err != nil { + if !sdk.ErrorIs(err, sdk.ErrNotFound) { + next() + return 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) + } + next() } - if stopAnalyze { - tx, err := api.mustDB().Begin() + if cdsUser != nil { + analysis.Data.CDSUserID = cdsUser.ID + analysis.Data.CDSUserName = cdsUser.Username + + // Check user right + _, next = telemetry.Span(ctx, "api.analyzeRepository.HasRoleOnProjectAndUserID") + b, err := rbac.HasRoleOnProjectAndUserID(ctx, tx, sdk.RoleManage, cdsUser.ID, analysis.ProjectKey) if err != nil { - return sdk.WrapError(err, "unable to start transaction") + next() + return err } - defer tx.Rollback() - if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { - return sdk.WrapError(err, "unable to failed analyze") + next() + 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 err + } + + switch vcsProject.Type { + case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud: + // get archive + _, next = telemetry.Span(ctx, "api.analyzeRepository.getCdsArchiveFileOnRepo") + err = api.getCdsArchiveFileOnRepo(ctx, client, repoWithSecret, analysis) + next() + case sdk.VCSTypeGitlab, sdk.VCSTypeGithub, sdk.VCSTypeGitea: + analysis.Data.Entities = make([]sdk.ProjectRepositoryDataEntity, 0) + _, next = telemetry.Span(ctx, "api.analyzeRepository.getCdsFilesOnVCSDirectory") + err = api.getCdsFilesOnVCSDirectory(ctx, client, analysis, repoWithSecret.Name, analysis.Commit, ".cds") + next() + case sdk.VCSTypeGerrit: + return sdk.WithStack(sdk.ErrNotImplemented) + } + if err != nil { + analysis.Status = sdk.RepositoryAnalysisStatusError + analysis.Data.Error = err.Error() + } 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 = telemetry.Span(ctx, "api.analyzeRepository.getCdsFilesOnVCSDirectory") + if err := repository.UpdateAnalysis(ctx, tx, analysis); 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 + 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 { 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 { + if analysis.Data.OperationUUID == "" { + proj, err := project.Load(ctx, api.mustDB(), analysis.ProjectKey) if err != nil { + if sdk.ErrorIs(err, sdk.ErrNotFound) { + return api.stopAnalysis(ctx, analysis, err) + } 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) - } + _, next := telemetry.Span(ctx, "api.analyzeCheckCommitThroughOperation.PostRepositoryOperation") + if err := operation.PostRepositoryOperation(ctx, tx, *proj, ope, nil); err != nil { + return err + } + next() + analysis.Data.OperationUUID = ope.UUID - // Update analyze - if err != nil { - analysis.Status = sdk.RepositoryAnalysisStatusError - analysis.Data.Error = err.Error() - } else { - analysis.Status = sdk.RepositoryAnalysisStatusSucceed - } + _, next = telemetry.Span(ctx, "api.analyzeCheckCommitThroughOperation.UpdateAnalysis") + if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { + return err + } + next() + if err := tx.Commit(); err != nil { + return sdk.WithStack(err) } } - - if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { + _, next := telemetry.Span(ctx, "api.analyzeRepository.Poll") + ope, err := operation.Poll(ctx, api.mustDB(), analysis.Data.OperationUUID) + if err != nil { + next() return err } - return sdk.WrapError(tx.Commit(), "unable to commit") + next() + + 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 { @@ -521,3 +577,20 @@ 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 { + 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 originalError +} 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/hooks/entitieshook.go b/engine/hooks/entitieshook.go index d8b4b3cf23..471ea8d710 100644 --- a/engine/hooks/entitieshook.go +++ b/engine/hooks/entitieshook.go @@ -45,7 +45,6 @@ func (s *Service) doAnalyzeExecution(ctx context.Context, t *sdk.TaskExecution) return err } t.EntitiesHook.AnalysisID = resp.AnalysisID - t.EntitiesHook.OperationID = resp.OperationID return nil } diff --git a/sdk/hooks.go b/sdk/hooks.go index 7cbeec749d..5ca4a3e3a7 100644 --- a/sdk/hooks.go +++ b/sdk/hooks.go @@ -140,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 @@ -168,6 +167,5 @@ type AnalysisRequest struct { } type AnalysisResponse struct { - AnalysisID string `json:"analysis_id"` - OperationID string `json:"operation_id"` + AnalysisID string `json:"analysis_id"` } From 09c3776fa5e7c8ab3567ab271d318b42e63324e6 Mon Sep 17 00:00:00 2001 From: Guiheux Steven Date: Thu, 25 Aug 2022 09:07:37 +0200 Subject: [PATCH 03/10] Update engine/api/v2_project.go Co-authored-by: Yvonnick Esnault --- engine/api/v2_project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/api/v2_project.go b/engine/api/v2_project.go index 39375e7b5e..e8fcd5fb01 100644 --- a/engine/api/v2_project.go +++ b/engine/api/v2_project.go @@ -32,7 +32,7 @@ func (api *API) getRepositoryHookHandler() ([]service.RbacChecker, service.Handl vars := mux.Vars(req) repoIdentifier, err := url.PathUnescape(vars["repositoryIdentifier"]) if !sdk.IsValidUUID(repoIdentifier) { - return sdk.NewErrorFrom(sdk.ErrWrongRequest, "this handler need the repository uuid") + 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 { From 6d2a9f6524a6ee601f140890e28357b9c1fa23b7 Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 11:34:36 +0200 Subject: [PATCH 04/10] fix: code review --- cli/cdsctl/experimental_project_repository.go | 12 ++-- engine/api/api_routes.go | 2 +- engine/api/operation/operation.go | 3 + engine/api/rbac/dao_rbac_project_key.go | 3 + .../repository/dao_vcs_project_repository.go | 9 ++- .../dao_vcs_project_repository_analyze.go | 3 + engine/api/user/dao_gpgkey.go | 3 + engine/api/v2_project_repository.go | 19 ++++++- engine/api/v2_repository_analyze.go | 57 +++++-------------- engine/api/vcs/dao_vcs_project.go | 3 + engine/hooks/hooks_handlers.go | 12 ++-- engine/hooks/hooks_router.go | 17 +++--- sdk/cdsclient/client_project_repository.go | 6 +- sdk/cdsclient/interface.go | 2 +- 14 files changed, 77 insertions(+), 74 deletions(-) diff --git a/cli/cdsctl/experimental_project_repository.go b/cli/cdsctl/experimental_project_repository.go index 7e66d3a242..6c4fcb3389 100644 --- a/cli/cdsctl/experimental_project_repository.go +++ b/cli/cdsctl/experimental_project_repository.go @@ -19,7 +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(projectRepositoryHookCreateCmd, projectRepositoryHookCreateFunc, nil, withAllCommandModifiers()...), + cli.NewGetCommand(projectRepositoryHookSecretRegenCmd, projectRepositoryHookSecretRegenFunc, nil, withAllCommandModifiers()...), }) } @@ -120,9 +120,9 @@ func projectRepositoryDeleteFunc(v cli.Values) error { return client.ProjectRepositoryDelete(context.Background(), v.GetString(_ProjectKey), v.GetString("vcs-name"), v.GetString("repository-name")) } -var projectRepositoryHookCreateCmd = cli.Command{ - Name: "hook", - Short: "Display data to be able to create a hook", +var projectRepositoryHookSecretRegenCmd = cli.Command{ + Name: "hook-regen", + Short: "Regenerate hook secret for webhook signature", Ctx: []cli.Arg{ {Name: _ProjectKey}, }, @@ -132,6 +132,6 @@ var projectRepositoryHookCreateCmd = cli.Command{ }, } -func projectRepositoryHookCreateFunc(v cli.Values) (interface{}, error) { - return client.ProjectRepositoryHookAccessLink(context.Background(), v.GetString(_ProjectKey), v.GetString("vcs-name"), v.GetString("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 dbaedc2e4c..2edbb9f9c0 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -441,7 +441,7 @@ func (api *API) InitRouter() { 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/link", nil, r.GETv2(api.getRepositoryHookLinkHandler)) + r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/hook/regen", nil, r.POSTv2(api.postRepositoryHookLinkHandler)) 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..cca7f2acf5 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" @@ -151,6 +152,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) { + _, 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..4b8ffb71aa 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) { + _, 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 47459b4a2d..bb1bfd128a 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" @@ -97,13 +98,15 @@ 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) { +func LoadRepositoryByID(ctx context.Context, db gorp.SqlExecutor, id string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { + _, 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 sdk.ProjectRepository{}, sdk.WrapError(err, "unable to get repository %s", id) + return nil, sdk.WrapError(err, "unable to get repository %s", id) } - return *repo, nil + return repo, nil } func LoadAllRepositories(ctx context.Context, db gorp.SqlExecutor) ([]sdk.ProjectRepository, error) { diff --git a/engine/api/repository/dao_vcs_project_repository_analyze.go b/engine/api/repository/dao_vcs_project_repository_analyze.go index ab9cbc3bb7..18bcdd7689 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) { + _, 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..c489c77cb2 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) { + _, next := telemetry.Span(ctx, "user.LoadGPGKeyByKeyID") + defer next() query := gorpmapping.NewQuery(` SELECT * FROM user_gpg_key diff --git a/engine/api/v2_project_repository.go b/engine/api/v2_project_repository.go index 545da9ed93..a0f69e6745 100644 --- a/engine/api/v2_project_repository.go +++ b/engine/api/v2_project_repository.go @@ -214,7 +214,7 @@ func (api *API) getVCSProjectRepositoryAllHandler() ([]service.RbacChecker, serv } } -func (api *API) getRepositoryHookLinkHandler() ([]service.RbacChecker, service.Handler) { +func (api *API) postRepositoryHookLinkHandler() ([]service.RbacChecker, service.Handler) { return service.RBAC(rbac.ProjectManage), func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) @@ -237,6 +237,23 @@ func (api *API) getRepositoryHookLinkHandler() ([]service.RbacChecker, service.H 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 diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go index 50a5d856a1..51081dd85f 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -233,75 +233,58 @@ 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") + 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) } - next() if analysis.Status != sdk.RepositoryAnalysisStatusInProgress { return nil } - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadVCSByID") vcsProject, err := vcs.LoadVCSByID(ctx, api.mustDB(), analysis.ProjectKey, analysis.VCSProjectID) if err != nil { - next() if sdk.ErrorIs(err, sdk.ErrNotFound) { return api.stopAnalysis(ctx, analysis, err) } return err } - next() - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadRepositoryByID") repoWithSecret, err := repository.LoadRepositoryByID(ctx, api.mustDB(), analysis.ProjectRepositoryID, gorpmapping.GetOptions.WithDecryption) if err != nil { - next() if sdk.ErrorIs(err, sdk.ErrNotFound) { return api.stopAnalysis(ctx, analysis, err) } return err } - next() switch vcsProject.Type { case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud, sdk.VCSTypeGitlab, sdk.VCSTypeGerrit: - _, next = telemetry.Span(ctx, "api.analyzeRepository.analyzeCommitSignatureThroughOperation") - if err := api.analyzeCommitSignatureThroughOperation(ctx, analysis, *vcsProject, repoWithSecret); err != nil { - next() + if err := api.analyzeCommitSignatureThroughOperation(ctx, analysis, *vcsProject, *repoWithSecret); err != nil { return err } - next() case sdk.VCSTypeGitea, sdk.VCSTypeGithub: - _, next = telemetry.Span(ctx, "api.analyzeRepository.analyzeCommitSignatureThroughVcsAPI") - if err := api.analyzeCommitSignatureThroughVcsAPI(ctx, analysis, *vcsProject, repoWithSecret); err != nil { - next() + if err := api.analyzeCommitSignatureThroughVcsAPI(ctx, analysis, *vcsProject, *repoWithSecret); err != nil { return err } - next() default: return sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to analyze vcs type: %s", vcsProject.Type) } @@ -317,30 +300,24 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana if analysis.Status == sdk.RepositoryAnalysisStatusInProgress { var cdsUser *sdk.AuthentifiedUser - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadGPGKeyByKeyID") gpgKey, err := user.LoadGPGKeyByKeyID(ctx, tx, analysis.Data.SignKeyID) if err != nil { if !sdk.ErrorIs(err, sdk.ErrNotFound) { - next() return sdk.WrapError(err, "unable to find gpg key: %s", analysis.Data.SignKeyID) } analysis.Status = sdk.RepositoryAnalysisStatusSkipped analysis.Data.Error = fmt.Sprintf("gpgkey %s not found", analysis.Data.SignKeyID) } - next() if gpgKey != nil { - _, next = telemetry.Span(ctx, "api.analyzeRepository.LoadByID") cdsUser, err = user.LoadByID(ctx, tx, gpgKey.AuthentifiedUserID) if err != nil { if !sdk.ErrorIs(err, sdk.ErrNotFound) { - next() return 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) } - next() } if cdsUser != nil { @@ -348,13 +325,10 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana analysis.Data.CDSUserName = cdsUser.Username // Check user right - _, next = telemetry.Span(ctx, "api.analyzeRepository.HasRoleOnProjectAndUserID") b, err := rbac.HasRoleOnProjectAndUserID(ctx, tx, sdk.RoleManage, cdsUser.ID, analysis.ProjectKey) if err != nil { - next() return err } - next() 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) @@ -369,14 +343,10 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana switch vcsProject.Type { case sdk.VCSTypeBitbucketServer, sdk.VCSTypeBitbucketCloud: // get archive - _, next = telemetry.Span(ctx, "api.analyzeRepository.getCdsArchiveFileOnRepo") - err = api.getCdsArchiveFileOnRepo(ctx, client, repoWithSecret, analysis) - next() + err = api.getCdsArchiveFileOnRepo(ctx, client, *repoWithSecret, analysis) case sdk.VCSTypeGitlab, sdk.VCSTypeGithub, sdk.VCSTypeGitea: analysis.Data.Entities = make([]sdk.ProjectRepositoryDataEntity, 0) - _, next = telemetry.Span(ctx, "api.analyzeRepository.getCdsFilesOnVCSDirectory") err = api.getCdsFilesOnVCSDirectory(ctx, client, analysis, repoWithSecret.Name, analysis.Commit, ".cds") - next() case sdk.VCSTypeGerrit: return sdk.WithStack(sdk.ErrNotImplemented) } @@ -390,16 +360,15 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana } } - _, next = telemetry.Span(ctx, "api.analyzeRepository.getCdsFilesOnVCSDirectory") if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { - next() 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 { + _, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughVcsAPI") + defer next() tx, err := api.mustDB().Begin() if err != nil { return sdk.WithStack(err) @@ -444,6 +413,8 @@ func (api *API) analyzeCommitSignatureThroughVcsAPI(ctx context.Context, analysi } func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, analysis *sdk.ProjectRepositoryAnalysis, vcsProject sdk.VCSProject, repoWithSecret sdk.ProjectRepository) error { + _, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughOperation") + defer next() if analysis.Data.OperationUUID == "" { proj, err := project.Load(ctx, api.mustDB(), analysis.ProjectKey) if err != nil { @@ -497,13 +468,10 @@ func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, anal return sdk.WithStack(err) } } - _, next := telemetry.Span(ctx, "api.analyzeRepository.Poll") ope, err := operation.Poll(ctx, api.mustDB(), analysis.Data.OperationUUID) if err != nil { - next() return err } - next() if ope.Status == sdk.OperationStatusDone && ope.Setup.Checkout.Result.CommitVerified { analysis.Data.CommitCheck = true @@ -523,6 +491,8 @@ func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, anal } func (api *API) getCdsFilesOnVCSDirectory(ctx context.Context, client sdk.VCSAuthorizedClientService, analysis *sdk.ProjectRepositoryAnalysis, repoName, commit, directory string) error { + _, 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) @@ -544,6 +514,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 { + _, 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 { @@ -579,6 +551,7 @@ func (api *API) getCdsArchiveFileOnRepo(ctx context.Context, client sdk.VCSAutho } 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) @@ -592,5 +565,5 @@ func (api *API) stopAnalysis(ctx context.Context, analysis *sdk.ProjectRepositor if err := tx.Commit(); err != nil { return err } - return originalError + return nil } diff --git a/engine/api/vcs/dao_vcs_project.go b/engine/api/vcs/dao_vcs_project.go index 0afebcc387..9c952794f4 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) { + _, 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/hooks_handlers.go b/engine/hooks/hooks_handlers.go index d79f220345..6fb48aa147 100644 --- a/engine/hooks/hooks_handlers.go +++ b/engine/hooks/hooks_handlers.go @@ -126,14 +126,10 @@ func (s *Service) repositoryWebHookHandler() service.Handler { vars := mux.Vars(r) uuid := vars["uuid"] - body, ok := ctx.Value("body").([]byte) - if !ok { - defer r.Body.Close() - var err error - body, err = io.ReadAll(r.Body) - if err != nil { - return sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read body: %v", err) - } + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + if err != nil { + return sdk.NewErrorFrom(sdk.ErrUnknownError, "unable to read body: %v", err) } hook := s.Dao.FindTask(ctx, uuid) diff --git a/engine/hooks/hooks_router.go b/engine/hooks/hooks_router.go index deba5c5b9d..82721f98d1 100644 --- a/engine/hooks/hooks_router.go +++ b/engine/hooks/hooks_router.go @@ -1,15 +1,18 @@ package hooks import ( + "bytes" "context" "crypto/hmac" "crypto/rsa" "crypto/sha256" "encoding/hex" "io" + "io/ioutil" "net/http" "strings" "sync" + "time" "github.com/gorilla/mux" "github.com/rockbears/log" @@ -76,13 +79,6 @@ func (s *Service) CheckHeaderToken(headerName string) service.Middleware { if tokenHeaderValue != hook.HookSignKey { return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "token mismatch") } - - body, err := io.ReadAll(req.Body) - if err != nil { - return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to check signature") - } - - ctx = context.WithValue(ctx, "body", body) return ctx, nil } } @@ -107,17 +103,20 @@ func (s *Service) CheckHmac256Signature(headerName string) service.Middleware { return ctx, sdk.NewErrorFrom(sdk.ErrUnauthorized, "unable to check signature") } + newRequestBody := ioutil.NopCloser(bytes.NewBuffer(body)) + req.Body = newRequestBody + + log.Warn(ctx, ">>>[%v]", time.Now()) // 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)) + log.Warn(ctx, ">>>[%v]", time.Now()) 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") } - - ctx = context.WithValue(ctx, "body", body) return ctx, nil } } diff --git a/sdk/cdsclient/client_project_repository.go b/sdk/cdsclient/client_project_repository.go index af75aaeb08..06134ed0dd 100644 --- a/sdk/cdsclient/client_project_repository.go +++ b/sdk/cdsclient/client_project_repository.go @@ -52,9 +52,9 @@ func (c *client) ProjectRepositoryAnalysisGet(ctx context.Context, projectKey st return analysis, err } -func (c *client) ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { - path := fmt.Sprintf("/v2/project/%s/vcs/%s/repository/%s/hook/link", projectKey, url.PathEscape(vcsName), url.PathEscape(repoName)) +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.GetJSON(ctx, path, &hookData) + _, err := c.PostJSON(ctx, path, nil, &hookData) return hookData, err } diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go index 00e988255b..8d25da8787 100644 --- a/sdk/cdsclient/interface.go +++ b/sdk/cdsclient/interface.go @@ -224,7 +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) - ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, 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) From f51671c471d6a8484c2558ab7f0ab80dcc299764 Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 11:35:11 +0200 Subject: [PATCH 05/10] fix: regen mock --- .../mock_cdsclient/interface_mock.go | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go index b3683d5dbc..d4ca5c618e 100644 --- a/sdk/cdsclient/mock_cdsclient/interface_mock.go +++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go @@ -2627,19 +2627,19 @@ func (mr *MockProjectClientMockRecorder) ProjectRepositoryDelete(ctx, projectKey return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryDelete", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryDelete), ctx, projectKey, vcsName, repositoryName) } -// ProjectRepositoryHookAccessLink mocks base method. -func (m *MockProjectClient) ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { +// 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, "ProjectRepositoryHookAccessLink", ctx, projectKey, vcsName, repoName) + ret := m.ctrl.Call(m, "ProjectRepositoryHookRegenSecret", ctx, projectKey, vcsName, repoName) ret0, _ := ret[0].(sdk.HookAccessData) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryHookAccessLink indicates an expected call of ProjectRepositoryHookAccessLink. -func (mr *MockProjectClientMockRecorder) ProjectRepositoryHookAccessLink(ctx, projectKey, vcsName, repoName interface{}) *gomock.Call { +// 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, "ProjectRepositoryHookAccessLink", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryHookAccessLink), ctx, projectKey, vcsName, repoName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryHookRegenSecret", reflect.TypeOf((*MockProjectClient)(nil).ProjectRepositoryHookRegenSecret), ctx, projectKey, vcsName, repoName) } // ProjectRepositoryManagerDelete mocks base method. @@ -6955,19 +6955,19 @@ func (mr *MockInterfaceMockRecorder) ProjectRepositoryDelete(ctx, projectKey, vc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryDelete", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryDelete), ctx, projectKey, vcsName, repositoryName) } -// ProjectRepositoryHookAccessLink mocks base method. -func (m *MockInterface) ProjectRepositoryHookAccessLink(ctx context.Context, projectKey, vcsName, repoName string) (sdk.HookAccessData, error) { +// 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, "ProjectRepositoryHookAccessLink", ctx, projectKey, vcsName, repoName) + ret := m.ctrl.Call(m, "ProjectRepositoryHookRegenSecret", ctx, projectKey, vcsName, repoName) ret0, _ := ret[0].(sdk.HookAccessData) ret1, _ := ret[1].(error) return ret0, ret1 } -// ProjectRepositoryHookAccessLink indicates an expected call of ProjectRepositoryHookAccessLink. -func (mr *MockInterfaceMockRecorder) ProjectRepositoryHookAccessLink(ctx, projectKey, vcsName, repoName interface{}) *gomock.Call { +// 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, "ProjectRepositoryHookAccessLink", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryHookAccessLink), ctx, projectKey, vcsName, repoName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProjectRepositoryHookRegenSecret", reflect.TypeOf((*MockInterface)(nil).ProjectRepositoryHookRegenSecret), ctx, projectKey, vcsName, repoName) } // ProjectRepositoryManagerDelete mocks base method. From aa6679f1155864ea0ba503cf7784fb66d46e14c7 Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 12:00:25 +0200 Subject: [PATCH 06/10] fix: regen hook secret --- engine/api/api_routes.go | 2 +- engine/api/v2_project_repository.go | 4 ++-- engine/hooks/hooks_router.go | 3 --- tests/08_v2_analyze.yml | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/engine/api/api_routes.go b/engine/api/api_routes.go index 9e47cfcb53..4afb643158 100644 --- a/engine/api/api_routes.go +++ b/engine/api/api_routes.go @@ -442,7 +442,7 @@ func (api *API) InitRouter() { 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.postRepositoryHookLinkHandler)) + 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/v2_project_repository.go b/engine/api/v2_project_repository.go index a0f69e6745..261b94b8d9 100644 --- a/engine/api/v2_project_repository.go +++ b/engine/api/v2_project_repository.go @@ -214,7 +214,7 @@ func (api *API) getVCSProjectRepositoryAllHandler() ([]service.RbacChecker, serv } } -func (api *API) postRepositoryHookLinkHandler() ([]service.RbacChecker, service.Handler) { +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) @@ -263,7 +263,7 @@ func (api *API) postRepositoryHookLinkHandler() ([]service.RbacChecker, service. } hook := sdk.HookAccessData{ URL: fmt.Sprintf("%s/v2/webhook/repository/%s/%s", srvs[0].HTTPURL, vcsProject.Type, repo.ID), - HookSignKey: repo.HookSignKey, + HookSignKey: newSecret, } return service.WriteJSON(w, hook, http.StatusOK) } diff --git a/engine/hooks/hooks_router.go b/engine/hooks/hooks_router.go index 82721f98d1..d9d6c6f349 100644 --- a/engine/hooks/hooks_router.go +++ b/engine/hooks/hooks_router.go @@ -12,7 +12,6 @@ import ( "net/http" "strings" "sync" - "time" "github.com/gorilla/mux" "github.com/rockbears/log" @@ -106,12 +105,10 @@ func (s *Service) CheckHmac256Signature(headerName string) service.Middleware { newRequestBody := ioutil.NopCloser(bytes.NewBuffer(body)) req.Body = newRequestBody - log.Warn(ctx, ">>>[%v]", time.Now()) // 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)) - log.Warn(ctx, ">>>[%v]", time.Now()) if strings.TrimPrefix(signHeaderValue, "sha256=") != sha { log.Error(ctx, "signature mismatch: got %s, compute %s", signHeaderValue, sha) diff --git a/tests/08_v2_analyze.yml b/tests/08_v2_analyze.yml index f0337f6f3d..5d8c3ba8cc 100644 --- a/tests/08_v2_analyze.yml +++ b/tests/08_v2_analyze.yml @@ -46,7 +46,7 @@ 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: {{.cdsctl}} -f {{.cdsctl.config}} experimental project repository hook ITCLIPRJVCS my_vcs_server {{.git.user}}/myrepo --format json + - 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 From 9b3001101358b050ef9bb133da2ecc80519bb59e Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 12:09:59 +0200 Subject: [PATCH 07/10] fix: remove retry --- engine/api/v2_repository_analyze.go | 34 ++++++++++------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go index 51081dd85f..c693fc2257 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -253,7 +253,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana return nil } if err != nil { - 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)) } if analysis.Status != sdk.RepositoryAnalysisStatusInProgress { @@ -262,31 +262,25 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana vcsProject, err := vcs.LoadVCSByID(ctx, api.mustDB(), analysis.ProjectKey, analysis.VCSProjectID) if err != nil { - if sdk.ErrorIs(err, sdk.ErrNotFound) { - return api.stopAnalysis(ctx, analysis, err) - } - return err + return api.stopAnalysis(ctx, analysis, err) } repoWithSecret, err := repository.LoadRepositoryByID(ctx, api.mustDB(), analysis.ProjectRepositoryID, gorpmapping.GetOptions.WithDecryption) if err != nil { - if sdk.ErrorIs(err, sdk.ErrNotFound) { - return api.stopAnalysis(ctx, analysis, err) - } - return err + 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 err + return api.stopAnalysis(ctx, analysis, err) } case sdk.VCSTypeGitea, sdk.VCSTypeGithub: if err := api.analyzeCommitSignatureThroughVcsAPI(ctx, analysis, *vcsProject, *repoWithSecret); err != nil { - return err + return api.stopAnalysis(ctx, analysis, err) } default: - return sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to analyze vcs type: %s", vcsProject.Type) + return api.stopAnalysis(ctx, analysis, sdk.NewErrorFrom(sdk.ErrInvalidData, "unable to analyze vcs type: %s", vcsProject.Type)) } // remove secret from repo @@ -294,7 +288,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana tx, err := api.mustDB().Begin() if err != nil { - return sdk.WithStack(err) + return api.stopAnalysis(ctx, analysis, sdk.WithStack(err)) } defer tx.Rollback() //nolint @@ -303,7 +297,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana gpgKey, err := user.LoadGPGKeyByKeyID(ctx, tx, analysis.Data.SignKeyID) if err != nil { if !sdk.ErrorIs(err, sdk.ErrNotFound) { - return sdk.WrapError(err, "unable to find gpg key: %s", analysis.Data.SignKeyID) + return api.stopAnalysis(ctx, analysis, sdk.WrapError(err, "unable to find gpg key: %s", analysis.Data.SignKeyID)) } analysis.Status = sdk.RepositoryAnalysisStatusSkipped analysis.Data.Error = fmt.Sprintf("gpgkey %s not found", analysis.Data.SignKeyID) @@ -313,7 +307,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana 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) + 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) @@ -327,7 +321,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana // Check user right b, err := rbac.HasRoleOnProjectAndUserID(ctx, tx, sdk.RoleManage, cdsUser.ID, analysis.ProjectKey) if err != nil { - return err + return api.stopAnalysis(ctx, analysis, err) } if !b { analysis.Status = sdk.RepositoryAnalysisStatusSkipped @@ -337,7 +331,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana if analysis.Status == sdk.RepositoryAnalysisStatusInProgress { client, err := repositoriesmanager.AuthorizedClient(ctx, tx, api.Cache, analysis.ProjectKey, vcsProject.Name) if err != nil { - return err + return api.stopAnalysis(ctx, analysis, err) } switch vcsProject.Type { @@ -351,8 +345,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana return sdk.WithStack(sdk.ErrNotImplemented) } if err != nil { - analysis.Status = sdk.RepositoryAnalysisStatusError - analysis.Data.Error = err.Error() + return api.stopAnalysis(ctx, analysis, err) } else { analysis.Status = sdk.RepositoryAnalysisStatusSucceed } @@ -418,9 +411,6 @@ func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, anal if analysis.Data.OperationUUID == "" { proj, err := project.Load(ctx, api.mustDB(), analysis.ProjectKey) if err != nil { - if sdk.ErrorIs(err, sdk.ErrNotFound) { - return api.stopAnalysis(ctx, analysis, err) - } return err } From d4f8aa62f70621652a3a02d7b2451a9eda978709 Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 13:45:50 +0200 Subject: [PATCH 08/10] fix: rename sql file --- .../{249_entitiesHooksSecret.sql => 250_entitiesHooksSecret.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename engine/sql/api/{249_entitiesHooksSecret.sql => 250_entitiesHooksSecret.sql} (100%) diff --git a/engine/sql/api/249_entitiesHooksSecret.sql b/engine/sql/api/250_entitiesHooksSecret.sql similarity index 100% rename from engine/sql/api/249_entitiesHooksSecret.sql rename to engine/sql/api/250_entitiesHooksSecret.sql From 974d46ac2f234f4823b1923c96b4926a9fee4a74 Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 16:00:05 +0200 Subject: [PATCH 09/10] fix(api): keep context when adding span --- engine/api/operation/operation.go | 4 +++- engine/api/rbac/dao_rbac_project_key.go | 2 +- engine/api/repository/dao_vcs_project_repository.go | 2 +- .../api/repository/dao_vcs_project_repository_analyze.go | 2 +- engine/api/user/dao_gpgkey.go | 2 +- engine/api/v2_repository_analyze.go | 8 ++------ 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/engine/api/operation/operation.go b/engine/api/operation/operation.go index cca7f2acf5..97547164a9 100644 --- a/engine/api/operation/operation.go +++ b/engine/api/operation/operation.go @@ -101,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") @@ -152,7 +154,7 @@ 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) { - _, next := telemetry.Span(ctx, "operation.Poll") + ctx, next := telemetry.Span(ctx, "operation.Poll") defer next() f := func() (*sdk.Operation, error) { ope, err := GetRepositoryOperation(ctx, db, operationUUID) diff --git a/engine/api/rbac/dao_rbac_project_key.go b/engine/api/rbac/dao_rbac_project_key.go index 4b8ffb71aa..e891d240c4 100644 --- a/engine/api/rbac/dao_rbac_project_key.go +++ b/engine/api/rbac/dao_rbac_project_key.go @@ -52,7 +52,7 @@ 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) { - _, next := telemetry.Span(ctx, "rbac.HasRoleOnProjectAndUserID") + ctx, next := telemetry.Span(ctx, "rbac.HasRoleOnProjectAndUserID") defer next() projectKeys, err := LoadProjectKeysByRoleAndUserID(ctx, db, role, userID) if err != nil { diff --git a/engine/api/repository/dao_vcs_project_repository.go b/engine/api/repository/dao_vcs_project_repository.go index bb1bfd128a..23366d65dd 100644 --- a/engine/api/repository/dao_vcs_project_repository.go +++ b/engine/api/repository/dao_vcs_project_repository.go @@ -99,7 +99,7 @@ func LoadAllRepositoriesByVCSProjectID(ctx context.Context, db gorp.SqlExecutor, } func LoadRepositoryByID(ctx context.Context, db gorp.SqlExecutor, id string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) { - _, next := telemetry.Span(ctx, "repository.LoadRepositoryByID") + 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...) diff --git a/engine/api/repository/dao_vcs_project_repository_analyze.go b/engine/api/repository/dao_vcs_project_repository_analyze.go index 18bcdd7689..5ce0ef3afa 100644 --- a/engine/api/repository/dao_vcs_project_repository_analyze.go +++ b/engine/api/repository/dao_vcs_project_repository_analyze.go @@ -112,7 +112,7 @@ func LoadRepositoryIDsAnalysisInProgress(ctx context.Context, db gorp.SqlExecuto } func LoadRepositoryAnalysisById(ctx context.Context, db gorp.SqlExecutor, projectRepoID, analysisID string) (*sdk.ProjectRepositoryAnalysis, error) { - _, next := telemetry.Span(ctx, "repository.LoadRepositoryAnalysisById") + 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 c489c77cb2..bf6a00caee 100644 --- a/engine/api/user/dao_gpgkey.go +++ b/engine/api/user/dao_gpgkey.go @@ -65,7 +65,7 @@ func LoadGPGKeysByUserID(ctx context.Context, db gorp.SqlExecutor, userID string } func LoadGPGKeyByKeyID(ctx context.Context, db gorp.SqlExecutor, keyID string) (*sdk.UserGPGKey, error) { - _, next := telemetry.Span(ctx, "user.LoadGPGKeyByKeyID") + ctx, next := telemetry.Span(ctx, "user.LoadGPGKeyByKeyID") defer next() query := gorpmapping.NewQuery(` SELECT * diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go index c693fc2257..1595621fc3 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -360,7 +360,7 @@ func (api *API) analyzeRepository(ctx context.Context, projectRepoID string, ana } func (api *API) analyzeCommitSignatureThroughVcsAPI(ctx context.Context, analysis *sdk.ProjectRepositoryAnalysis, vcsProject sdk.VCSProject, repoWithSecret sdk.ProjectRepository) error { - _, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughVcsAPI") + ctx, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughVcsAPI") defer next() tx, err := api.mustDB().Begin() if err != nil { @@ -406,7 +406,7 @@ func (api *API) analyzeCommitSignatureThroughVcsAPI(ctx context.Context, analysi } func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, analysis *sdk.ProjectRepositoryAnalysis, vcsProject sdk.VCSProject, repoWithSecret sdk.ProjectRepository) error { - _, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughOperation") + ctx, next := telemetry.Span(ctx, "api.analyzeCommitSignatureThroughOperation") defer next() if analysis.Data.OperationUUID == "" { proj, err := project.Load(ctx, api.mustDB(), analysis.ProjectKey) @@ -442,18 +442,14 @@ func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, anal return sdk.WithStack(err) } - _, next := telemetry.Span(ctx, "api.analyzeCheckCommitThroughOperation.PostRepositoryOperation") if err := operation.PostRepositoryOperation(ctx, tx, *proj, ope, nil); err != nil { return err } - next() analysis.Data.OperationUUID = ope.UUID - _, next = telemetry.Span(ctx, "api.analyzeCheckCommitThroughOperation.UpdateAnalysis") if err := repository.UpdateAnalysis(ctx, tx, analysis); err != nil { return err } - next() if err := tx.Commit(); err != nil { return sdk.WithStack(err) } From e26a71b35c605acdc21b64c030172c2de60923cd Mon Sep 17 00:00:00 2001 From: Steven Guiheux Date: Thu, 25 Aug 2022 16:01:51 +0200 Subject: [PATCH 10/10] fix(api): keep context when adding span --- engine/api/v2_repository_analyze.go | 6 +++--- engine/api/vcs/dao_vcs_project.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/engine/api/v2_repository_analyze.go b/engine/api/v2_repository_analyze.go index 1595621fc3..f873839033 100644 --- a/engine/api/v2_repository_analyze.go +++ b/engine/api/v2_repository_analyze.go @@ -232,7 +232,7 @@ 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) @@ -477,7 +477,7 @@ func (api *API) analyzeCommitSignatureThroughOperation(ctx context.Context, anal } func (api *API) getCdsFilesOnVCSDirectory(ctx context.Context, client sdk.VCSAuthorizedClientService, analysis *sdk.ProjectRepositoryAnalysis, repoName, commit, directory string) error { - _, next := telemetry.Span(ctx, "api.getCdsFilesOnVCSDirectory") + ctx, next := telemetry.Span(ctx, "api.getCdsFilesOnVCSDirectory") defer next() contents, err := client.ListContent(ctx, repoName, commit, directory) if err != nil { @@ -500,7 +500,7 @@ 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 { - _, next := telemetry.Span(ctx, "api.getCdsArchiveFileOnRepo") + 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) diff --git a/engine/api/vcs/dao_vcs_project.go b/engine/api/vcs/dao_vcs_project.go index 9c952794f4..09d6a86cbb 100644 --- a/engine/api/vcs/dao_vcs_project.go +++ b/engine/api/vcs/dao_vcs_project.go @@ -85,7 +85,7 @@ 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) { - _, next := telemetry.Span(ctx, "vcs.LoadVCSByID") + 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