Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api,hooks): add v2 hook signature and github repository analysis #6255

Merged
merged 12 commits into from
Aug 26, 2022
17 changes: 17 additions & 0 deletions cli/cdsctl/experimental_project_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func projectRepository() *cobra.Command {
cli.NewListCommand(projectRepositoryListCmd, projectRepositoryListFunc, nil, withAllCommandModifiers()...),
cli.NewDeleteCommand(projectRepositoryDeleteCmd, projectRepositoryDeleteFunc, nil, withAllCommandModifiers()...),
cli.NewCommand(projectRepositoryAddCmd, projectRepositoryAddFunc, nil, withAllCommandModifiers()...),
cli.NewGetCommand(projectRepositoryHookSecretRegenCmd, projectRepositoryHookSecretRegenFunc, nil, withAllCommandModifiers()...),
})
}

Expand Down Expand Up @@ -118,3 +119,19 @@ var projectRepositoryDeleteCmd = cli.Command{
func projectRepositoryDeleteFunc(v cli.Values) error {
return client.ProjectRepositoryDelete(context.Background(), v.GetString(_ProjectKey), v.GetString("vcs-name"), v.GetString("repository-name"))
}

var projectRepositoryHookSecretRegenCmd = cli.Command{
Name: "hook-regen",
Short: "Regenerate hook secret for webhook signature",
Ctx: []cli.Arg{
{Name: _ProjectKey},
},
Args: []cli.Arg{
{Name: "vcs-name"},
{Name: "repository-name"},
},
}

func projectRepositoryHookSecretRegenFunc(v cli.Values) (interface{}, error) {
return client.ProjectRepositoryHookRegenSecret(context.Background(), v.GetString(_ProjectKey), v.GetString("vcs-name"), v.GetString("repository-name"))
}
3 changes: 3 additions & 0 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,15 @@ func (api *API) InitRouter() {

r.Handle("/v2/repository/analyze", Scope(sdk.AuthConsumerScopeHooks), r.POSTv2(api.postRepositoryAnalysisHandler))
r.Handle("/v2/project/repositories", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getAllRepositoriesHandler))
r.Handle("/v2/project/repositories/{repositoryIdentifier}/hook", Scope(sdk.AuthConsumerScopeHooks), r.GETv2(api.getRepositoryHookHandler))

r.Handle("/v2/project/{projectKey}/vcs", nil, r.POSTv2(api.postVCSProjectHandler), r.GETv2(api.getVCSProjectAllHandler))
r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}", nil, r.PUTv2(api.putVCSProjectHandler), r.DELETEv2(api.deleteVCSProjectHandler), r.GETv2(api.getVCSProjectHandler))
r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository", nil, r.POSTv2(api.postProjectRepositoryHandler), r.GETv2(api.getVCSProjectRepositoryAllHandler))
r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}", nil, r.DELETEv2(api.deleteProjectRepositoryHandler))
r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/analysis", nil, r.GETv2(api.getProjectRepositoryAnalysesHandler))
r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/analysis/{analysisID}", nil, r.GETv2(api.getProjectRepositoryAnalysisHandler))
r.Handle("/v2/project/{projectKey}/vcs/{vcsIdentifier}/repository/{repositoryIdentifier}/hook/regen", nil, r.POSTv2(api.postRepositoryHookRegenKeyHandler))

r.Handle("/v2/user/gpgkey/{gpgKeyID}", Scope(sdk.AuthConsumerScopeUser), r.GETv2(api.getUserGPGKeyHandler))
r.Handle("/v2/user/{user}/gpgkey", Scope(sdk.AuthConsumerScopeUser), r.GETv2(api.getUserGPGKeysHandler), r.POSTv2(api.postUserGPGGKeyHandler))
Expand Down
5 changes: 5 additions & 0 deletions engine/api/operation/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"github.com/ovh/cds/sdk/telemetry"
"net/http"
"time"

Expand Down Expand Up @@ -100,6 +101,8 @@ func PushOperationUpdate(ctx context.Context, db gorpmapper.SqlExecutorWithTx, s

// PostRepositoryOperation creates a new repository operation
func PostRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, prj sdk.Project, ope *sdk.Operation, multipartData *services.MultiPartData) error {
ctx, next := telemetry.Span(ctx, "operation.PostRepositoryOperation")
defer next()
srvs, err := services.LoadAllByType(ctx, db, sdk.TypeRepositories)
if err != nil {
return sdk.WrapError(err, "Unable to found repositories service")
Expand Down Expand Up @@ -151,6 +154,8 @@ func GetRepositoryOperation(ctx context.Context, db gorp.SqlExecutor, uuid strin

// Poll repository operation for given uuid.
func Poll(ctx context.Context, db gorp.SqlExecutor, operationUUID string) (*sdk.Operation, error) {
ctx, next := telemetry.Span(ctx, "operation.Poll")
defer next()
f := func() (*sdk.Operation, error) {
ope, err := GetRepositoryOperation(ctx, db, operationUUID)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions engine/api/rbac/dao_rbac_project_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rbac

import (
"context"
"github.com/ovh/cds/sdk/telemetry"

"github.com/lib/pq"

Expand Down Expand Up @@ -51,6 +52,8 @@ func loadRRBACProjectKeys(ctx context.Context, db gorp.SqlExecutor, rbacProjectI
}

func HasRoleOnProjectAndUserID(ctx context.Context, db gorp.SqlExecutor, role string, userID string, projectKey string) (bool, error) {
ctx, next := telemetry.Span(ctx, "rbac.HasRoleOnProjectAndUserID")
defer next()
projectKeys, err := LoadProjectKeysByRoleAndUserID(ctx, db, role, userID)
if err != nil {
return false, err
Expand Down
45 changes: 26 additions & 19 deletions engine/api/repository/dao_vcs_project_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repository

import (
"context"
"github.com/ovh/cds/sdk/telemetry"
"time"

"github.com/go-gorp/gorp"
Expand Down Expand Up @@ -36,15 +37,14 @@ func Delete(db gorpmapper.SqlExecutorWithTx, vcsProjectID string, name string) e
return sdk.WrapError(err, "cannot delete project_repository %s / %s", vcsProjectID, name)
}

func LoadRepositoryByVCSAndID(ctx context.Context, db gorp.SqlExecutor, vcsProjectID, repoID string) (*sdk.ProjectRepository, error) {
query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.id = $2`).Args(vcsProjectID, repoID)
func getRepository(ctx context.Context, db gorp.SqlExecutor, query gorpmapping.Query, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) {
var res dbProjectRepository
found, err := gorpmapping.Get(ctx, db, query, &res)
found, err := gorpmapping.Get(ctx, db, query, &res, opts...)
if err != nil {
return nil, err
}
if !found {
return nil, sdk.NewErrorFrom(sdk.ErrNotFound, "repository: %s", repoID)
return nil, sdk.WithStack(sdk.ErrNotFound)
}

isValid, err := gorpmapping.CheckSignature(res, res.Signature)
Expand All @@ -58,26 +58,22 @@ func LoadRepositoryByVCSAndID(ctx context.Context, db gorp.SqlExecutor, vcsProje
return &res.ProjectRepository, nil
}

func LoadRepositoryByName(ctx context.Context, db gorp.SqlExecutor, vcsProjectID string, repoName string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) {
query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.name = $2`).Args(vcsProjectID, repoName)
var res dbProjectRepository
found, err := gorpmapping.Get(ctx, db, query, &res, opts...)
func LoadRepositoryByVCSAndID(ctx context.Context, db gorp.SqlExecutor, vcsProjectID, repoID string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) {
query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.id = $2`).Args(vcsProjectID, repoID)
repo, err := getRepository(ctx, db, query, opts...)
if err != nil {
return nil, err
}
if !found {
return nil, sdk.WrapError(sdk.ErrNotFound, "repository %s", repoName)
return nil, sdk.WrapError(err, "unable to get repository %s", repo.ID)
}
return repo, nil
}

isValid, err := gorpmapping.CheckSignature(res, res.Signature)
func LoadRepositoryByName(ctx context.Context, db gorp.SqlExecutor, vcsProjectID string, repoName string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) {
query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE project_repository.vcs_project_id = $1 AND project_repository.name = $2`).Args(vcsProjectID, repoName)
repo, err := getRepository(ctx, db, query, opts...)
if err != nil {
return nil, err
}
if !isValid {
log.Error(ctx, "project_repository %d / %s data corrupted", res.ID, res.Name)
return nil, sdk.WithStack(sdk.ErrNotFound)
return nil, sdk.WrapError(err, "unable to get repository %s/%s", vcsProjectID, repoName)
}
return &res.ProjectRepository, nil
return repo, nil
}

func LoadAllRepositoriesByVCSProjectID(ctx context.Context, db gorp.SqlExecutor, vcsProjectID string) ([]sdk.ProjectRepository, error) {
Expand All @@ -102,6 +98,17 @@ func LoadAllRepositoriesByVCSProjectID(ctx context.Context, db gorp.SqlExecutor,
return repositories, nil
}

func LoadRepositoryByID(ctx context.Context, db gorp.SqlExecutor, id string, opts ...gorpmapping.GetOptionFunc) (*sdk.ProjectRepository, error) {
ctx, next := telemetry.Span(ctx, "repository.LoadRepositoryByID")
defer next()
query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository WHERE id = $1`).Args(id)
repo, err := getRepository(ctx, db, query, opts...)
if err != nil {
return nil, sdk.WrapError(err, "unable to get repository %s", id)
}
return repo, nil
}

func LoadAllRepositories(ctx context.Context, db gorp.SqlExecutor) ([]sdk.ProjectRepository, error) {
query := gorpmapping.NewQuery(`SELECT project_repository.* FROM project_repository`)
var res []dbProjectRepository
Expand Down
3 changes: 3 additions & 0 deletions engine/api/repository/dao_vcs_project_repository_analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repository

import (
"context"
"github.com/ovh/cds/sdk/telemetry"
"time"

"github.com/go-gorp/gorp"
Expand Down Expand Up @@ -111,6 +112,8 @@ func LoadRepositoryIDsAnalysisInProgress(ctx context.Context, db gorp.SqlExecuto
}

func LoadRepositoryAnalysisById(ctx context.Context, db gorp.SqlExecutor, projectRepoID, analysisID string) (*sdk.ProjectRepositoryAnalysis, error) {
ctx, next := telemetry.Span(ctx, "repository.LoadRepositoryAnalysisById")
defer next()
query := gorpmapping.NewQuery("SELECT * FROM project_repository_analysis WHERE project_repository_id = $1 AND id = $2").Args(projectRepoID, analysisID)
return getAnalysis(ctx, db, query)
}
3 changes: 3 additions & 0 deletions engine/api/user/dao_gpgkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package user

import (
"context"
"github.com/ovh/cds/sdk/telemetry"
"time"

"github.com/go-gorp/gorp"
Expand Down Expand Up @@ -64,6 +65,8 @@ func LoadGPGKeysByUserID(ctx context.Context, db gorp.SqlExecutor, userID string
}

func LoadGPGKeyByKeyID(ctx context.Context, db gorp.SqlExecutor, keyID string) (*sdk.UserGPGKey, error) {
ctx, next := telemetry.Span(ctx, "user.LoadGPGKeyByKeyID")
defer next()
query := gorpmapping.NewQuery(`
SELECT *
FROM user_gpg_key
Expand Down
27 changes: 27 additions & 0 deletions engine/api/v2_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,3 +25,25 @@ func (api *API) getAllRepositoriesHandler() ([]service.RbacChecker, service.Hand
return service.WriteJSON(w, repos, http.StatusOK)
}
}

func (api *API) getRepositoryHookHandler() ([]service.RbacChecker, service.Handler) {
return service.RBAC(rbac.IsHookService),
func(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
vars := mux.Vars(req)
repoIdentifier, err := url.PathUnescape(vars["repositoryIdentifier"])
if !sdk.IsValidUUID(repoIdentifier) {
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "this handler needs the repository uuid")
}
repo, err := repository.LoadRepositoryByID(ctx, api.mustDB(), repoIdentifier, gorpmapping.GetOptions.WithDecryption)
if err != nil {
return err
}
h := sdk.Hook{
HookSignKey: repo.HookSignKey,
UUID: repo.ID,
HookType: sdk.RepositoryEntitiesHook,
Configuration: repo.HookConfiguration,
}
return service.WriteJSON(w, h, http.StatusOK)
}
}
70 changes: 66 additions & 4 deletions engine/api/v2_project_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"fmt"
"net/http"
"net/url"

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -206,3 +213,58 @@ func (api *API) getVCSProjectRepositoryAllHandler() ([]service.RbacChecker, serv
return service.WriteJSON(w, repositories, http.StatusOK)
}
}

func (api *API) postRepositoryHookRegenKeyHandler() ([]service.RbacChecker, service.Handler) {
return service.RBAC(rbac.ProjectManage),
func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
pKey := vars["projectKey"]
vcsIdentifier, err := url.PathUnescape(vars["vcsIdentifier"])
if err != nil {
return sdk.NewError(sdk.ErrWrongRequest, err)
}
repositoryIdentifier, err := url.PathUnescape(vars["repositoryIdentifier"])
if err != nil {
return sdk.WithStack(err)
}

vcsProject, err := api.getVCSByIdentifier(ctx, pKey, vcsIdentifier)
if err != nil {
return err
}

repo, err := api.getRepositoryByIdentifier(ctx, vcsProject.ID, repositoryIdentifier, gorpmapper.GetOptions.WithDecryption)
if err != nil {
return err
}
newSecret, err := sdk.GenerateHookSecret()
if err != nil {
return err
}
repo.HookSignKey = newSecret

tx, err := api.mustDB().Begin()
if err != nil {
return sdk.WithStack(err)
}
if err := repository.Update(ctx, tx, repo); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return sdk.WithStack(err)
}

srvs, err := services.LoadAllByType(ctx, api.mustDB(), sdk.TypeHooks)
if err != nil {
return err
}
if len(srvs) == 0 {
return sdk.NewErrorFrom(sdk.ErrNotFound, "no hook service found")
}
hook := sdk.HookAccessData{
URL: fmt.Sprintf("%s/v2/webhook/repository/%s/%s", srvs[0].HTTPURL, vcsProject.Type, repo.ID),
HookSignKey: newSecret,
}
return service.WriteJSON(w, hook, http.StatusOK)
}
}
Loading