diff --git a/cla-backend-go/auth/authorizer.go b/cla-backend-go/auth/authorizer.go index 49569d2b3..76532c094 100644 --- a/cla-backend-go/auth/authorizer.go +++ b/cla-backend-go/auth/authorizer.go @@ -125,6 +125,7 @@ func (a Authorizer) SecurityAuth(token string, scopes []string) (*user.CLAUser, } return nil, err } + //log.WithFields(f).Debugf("user loaded : %+v with scopes : %+v", lfuser, scopes) for _, scope := range scopes { switch Scope(scope) { @@ -151,5 +152,6 @@ func (a Authorizer) SecurityAuth(token string, scopes []string) (*user.CLAUser, } } + //log.WithFields(f).Debugf("returning user from auth : %+v", lfuser) return &lfuser, nil } diff --git a/cla-backend-go/cmd/dynamo_events_lambda/main.go b/cla-backend-go/cmd/dynamo_events_lambda/main.go index 8dbf6d764..72edafe5f 100644 --- a/cla-backend-go/cmd/dynamo_events_lambda/main.go +++ b/cla-backend-go/cmd/dynamo_events_lambda/main.go @@ -8,6 +8,8 @@ import ( "encoding/json" "os" + "github.com/communitybridge/easycla/cla-backend-go/gitlab" + "github.com/communitybridge/easycla/cla-backend-go/github_organizations" "github.com/communitybridge/easycla/cla-backend-go/utils" @@ -91,6 +93,8 @@ func init() { token.Init(configFile.Auth0Platform.ClientID, configFile.Auth0Platform.ClientSecret, configFile.Auth0Platform.URL, configFile.Auth0Platform.Audience) github.Init(configFile.GitHub.AppID, configFile.GitHub.AppPrivateKey, configFile.GitHub.AccessToken) + // initialize gitlab + gitlab.Init(configFile.Gitlab.AppID, configFile.Gitlab.AppPrivateKey) user_service.InitClient(configFile.APIGatewayURL, configFile.AcsAPIKey) project_service.InitClient(configFile.APIGatewayURL) diff --git a/cla-backend-go/cmd/gitlab/api/main.go b/cla-backend-go/cmd/gitlab/api/main.go new file mode 100644 index 000000000..dec0f60aa --- /dev/null +++ b/cla-backend-go/cmd/gitlab/api/main.go @@ -0,0 +1,117 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "flag" + "fmt" + "os" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/xanzy/go-gitlab" +) + +const ( + ProjectsURL = "https://gitlab.com/api/v4/projects" +) + +var state = flag.String("state", "failed", "the state of the MR to set") + +func main() { + flag.Parse() + + access_token := os.Getenv("GITLAB_ACCESS_TOKEN") + if access_token == "" { + log.Fatal("GITLAB_ACCESS_TOKEN is required") + } + + log.Infof("The gitlab access token is : %s", access_token) + + gitlabClient, err := gitlab.NewOAuthClient(access_token) + if err != nil { + log.Fatalf("creating client failed : %v", err) + } + + user, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + log.Fatalf("fetching current user failed : %v", err) + } + + log.Infof("fetched current user : %s", user.Name) + + projects, _, err := gitlabClient.Projects.ListUserProjects(user.ID, &gitlab.ListProjectsOptions{}) + if err != nil { + log.Fatalf("listing projects failed : %v", err) + } + log.Printf("we fetched : %d projects for the account", len(projects)) + for _, p := range projects { + log.Println("Name : ", p.Name) + log.Println("ID: ", p.ID) + } + + projectID := 28118160 + commitSha := "f7036ab67a4e464e83e16af0b02d447c53fffa74" + + statuses, _, err := gitlabClient.Commits.GetCommitStatuses(projectID, commitSha, + &gitlab.GetCommitStatusesOptions{}) + if err != nil { + log.Fatalf("fetching commit statuses failed : %v", err) + } + + if len(statuses) == 0 { + log.Infof("no statuses found for commit sha") + setState := gitlab.Failed + if *state != string(gitlab.Failed) { + setState = gitlab.Success + } + + _, _, err = gitlabClient.Commits.SetCommitStatus(projectID, commitSha, &gitlab.SetCommitStatusOptions{ + State: setState, + Name: gitlab.String("easyCLA Bot"), + Description: gitlab.String(getDescription(setState)), + TargetURL: gitlab.String(getTargetURL("deniskurov@gmail.com")), + }) + if err != nil { + log.Fatalf("setting commit status for the sha failed : %v", err) + } + + statuses, _, err = gitlabClient.Commits.GetCommitStatuses(projectID, commitSha, + &gitlab.GetCommitStatusesOptions{}) + if err != nil { + log.Fatalf("fetching commit statuses failed : %v", err) + } + + } + + for _, status := range statuses { + log.Println("Status : ", status.Status) + if status.Status != *state { + log.Infof("setting state of commit sha to %s", *state) + _, _, err = gitlabClient.Commits.SetCommitStatus(projectID, commitSha, &gitlab.SetCommitStatusOptions{ + State: gitlab.BuildStateValue(*state), + Name: gitlab.String("easyCLA Bot"), + Description: gitlab.String(getDescription(gitlab.BuildStateValue(*state))), + TargetURL: gitlab.String(getTargetURL("deniskurov@gmail.com")), + }) + if err != nil { + log.Fatalf("setting commit status for the sha failed : %v", err) + } + } + log.Println("Status Name : ", status.Name) + log.Println("Status Description : ", status.Description) + log.Println("Status Author : ", status.Author.Name) + log.Println("Status Author Email : ", status.Author.Email) + } +} + +func getDescription(status gitlab.BuildStateValue) string { + if status == gitlab.Failed { + return "User hasn't signed CLA" + } + return "User signed CLA" +} + +func getTargetURL(email string) string { + return fmt.Sprintf("http://localhost:8080/gitlab/sign/%s", email) +} diff --git a/cla-backend-go/cmd/gitlab/auth/main.go b/cla-backend-go/cmd/gitlab/auth/main.go new file mode 100644 index 000000000..798a07ef6 --- /dev/null +++ b/cla-backend-go/cmd/gitlab/auth/main.go @@ -0,0 +1,312 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "strconv" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/gin-gonic/gin" + "github.com/go-resty/resty/v2" + "github.com/xanzy/go-gitlab" +) + +const ( + REDIRECT_URI = "http://localhost:8080/gitlab/oauth/callback" + APPLICATION_ID = "18718b478096e6a257eda51414d0d446ad28866c15187aa765f602fe906d0b17" + APPLICATION_SECRET = "8dd14ace0eb0e4674b849b6fed4ce51bbcc456fc62d9149aff15353c1dda6327" +) + +const ( + hookURL = "https://4c1ba3f4f3c1.ngrok.io/gitlab/events" +) + +type OauthSuccessResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + CreatedAt int `json:"created_at"` +} + +var passingUsers = map[string]bool{ + "deniskurov@gmail.com": true, +} + +func main() { + r := gin.Default() + r.GET("/gitlab/sign", func(c *gin.Context) { + email := c.Query("email") + if email == "" { + c.JSON(400, gin.H{ + "message": "email is required parameter", + }) + return + } + + projectID := c.Query("project_id") + if projectID == "" { + c.JSON(400, gin.H{ + "message": "projectID is required parameter", + }) + return + } + + lastCommitSha := c.Query("sha") + if lastCommitSha == "" { + c.JSON(400, gin.H{ + "message": "sha is required parameter", + }) + return + } + + projectIDInt, err := strconv.Atoi(projectID) + if err != nil { + log.Error("project id conversion failed ", err) + c.JSON(400, gin.H{ + "message": "project id conversion", + }) + return + } + + if err := setCommitStatus(projectIDInt, lastCommitSha, email, string(gitlab.Success)); err != nil { + log.Error("setting commit status failed", err) + c.JSON(500, gin.H{ + "message": "setting commit status failed", + }) + return + } + + log.Infof("email to sign is : %s", email) + log.Infof("project id : %s, sha : %s", projectID, lastCommitSha) + + c.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("user : %s, signed for project : %s", email, projectID), + }) + + }) + + r.POST("/gitlab/events", func(c *gin.Context) { + jsonData, err := ioutil.ReadAll(c.Request.Body) + event, err := gitlab.ParseWebhook(gitlab.EventTypeMergeRequest, jsonData) + if err != nil { + log.Error("parsing json body failed", err) + c.JSON(400, gin.H{ + "message": "code is required parameter", + }) + return + } + + mergeEvent, ok := event.(*gitlab.MergeEvent) + if !ok { + c.JSON(400, gin.H{ + "message": "type cast failed", + }) + return + } + + if mergeEvent.ObjectAttributes.State != "opened" { + c.JSON(200, gin.H{ + "message": "only interested in opened events", + }) + return + } + + projectName := mergeEvent.Project.Name + projectID := mergeEvent.Project.ID + + mergeID := mergeEvent.ObjectAttributes.IID + lastCommitSha := mergeEvent.ObjectAttributes.LastCommit.ID + lastCommitMessage := mergeEvent.ObjectAttributes.LastCommit.Message + + authorName := mergeEvent.ObjectAttributes.LastCommit.Author.Name + authorEmail := mergeEvent.ObjectAttributes.LastCommit.Author.Email + + log.Printf("Received MR (%d) for Project %s:%d", mergeID, projectName, projectID) + log.Printf("last commit : %s : %s", lastCommitSha, lastCommitMessage) + log.Printf("author name : %s, author email : %s", authorName, authorEmail) + + if err := setCommitStatus(projectID, lastCommitSha, authorEmail, ""); err != nil { + log.Error("setting commit status failed", err) + c.JSON(500, gin.H{ + "message": "setting commit status failed", + }) + return + } + + //empJSON, err := json.MarshalIndent(mergeEvent, "", " ") + //if err != nil { + // log.Fatalf(err.Error()) + //} + //fmt.Printf("MarshalIndent funnction output %s\n", string(empJSON)) + c.JSON(http.StatusOK, gin.H{}) + + }) + r.GET("/gitlab/oauth/callback", func(c *gin.Context) { + code := c.Query("code") + if code == "" { + c.JSON(400, gin.H{ + "message": "code is required parameter", + }) + return + } + + state := c.Query("state") + if state == "" { + c.JSON(400, gin.H{ + "message": "state is required parameter", + }) + return + } + log.Printf("received code : %s, STATE: %s", code, state) + + client := resty.New() + params := map[string]string{ + "client_id": APPLICATION_ID, + "client_secret": APPLICATION_SECRET, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": REDIRECT_URI, + } + + resp, err := client.R(). + SetQueryParams(params). + SetResult(&OauthSuccessResponse{}). + Post("https://gitlab.com/oauth/token") + + if err != nil { + c.JSON(500, gin.H{ + "message": fmt.Sprintf("getting the token failed : %v", err), + }) + return + } + + result := resp.Result().(*OauthSuccessResponse) + accessToken := result.AccessToken + + err = registerWebHooksForUserProjects(accessToken) + if err != nil { + log.Error("register webhook ", err) + } + + respData := gin.H{ + "message": "OK", + "data": result, + } + + if err != nil { + respData["error"] = err + } + + c.JSON(200, respData) + }) + r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") +} + +func registerWebHooksForUserProjects(accessToken string) error { + gitlabClient, err := gitlab.NewOAuthClient(accessToken) + if err != nil { + return fmt.Errorf("creating client failed : %v", err) + } + + user, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + return fmt.Errorf("fetching current user failed : %v", err) + } + + log.Infof("fetched current user : %s", user.Name) + + projects, _, err := gitlabClient.Projects.ListUserProjects(user.ID, &gitlab.ListProjectsOptions{}) + if err != nil { + return fmt.Errorf("listing projects failed : %v", err) + } + + log.Printf("we fetched : %d projects for the account", len(projects)) + + for _, p := range projects { + log.Println("**********************") + log.Println("Name : ", p.Name) + log.Println("ID: ", p.ID) + log.Infof("adding webhook to the project : %s (%d)", p.Name, p.ID) + if err := addCLAHookToProject(gitlabClient, p.ID); err != nil { + return fmt.Errorf("adding hook to the project : %s (%d) failed : %v", p.Name, p.ID, err) + } + } + + return nil +} + +func addCLAHookToProject(gitlabClient *gitlab.Client, projectID int) error { + _, _, err := gitlabClient.Projects.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ + URL: gitlab.String(hookURL), + MergeRequestsEvents: gitlab.Bool(true), + EnableSSLVerification: gitlab.Bool(false), + }) + return err +} + +func setCommitStatus(projectID interface{}, commitSha string, userEmail string, forceState string) error { + accessToken := os.Getenv("GITLAB_ACCESS_TOKEN") + if accessToken == "" { + return fmt.Errorf("GITLAB_ACCESS_TOKEN is required") + } + + gitlabClient, err := gitlab.NewOAuthClient(accessToken) + if err != nil { + return fmt.Errorf("creating client failed : %v", err) + } + + setState := gitlab.Failed + + if forceState == "" { + if passingUsers[userEmail] { + setState = gitlab.Success + } + } else { + setState = gitlab.BuildStateValue(forceState) + } + + options := &gitlab.SetCommitStatusOptions{ + State: setState, + Name: gitlab.String("easyCLA Bot"), + Description: gitlab.String(getDescription(setState)), + } + + if setState == gitlab.Failed { + options.TargetURL = gitlab.String(getTargetURL(projectID, commitSha, userEmail)) + } + + _, _, err = gitlabClient.Commits.SetCommitStatus(projectID, commitSha, options) + if err != nil { + return fmt.Errorf("setting commit status for the sha failed : %v", err) + } + + return nil +} + +func getDescription(status gitlab.BuildStateValue) string { + if status == gitlab.Failed { + return "User hasn't signed CLA" + } + return "User signed CLA" +} + +func getTargetURL(projectID interface{}, lastCommitSha, email string) string { + base := "http://localhost:8080/gitlab/sign" + + projectIDInt := projectID.(int) + projectIDStr := strconv.Itoa(projectIDInt) + + params := url.Values{} + params.Add("project_id", projectIDStr) + params.Add("sha", lastCommitSha) + params.Add("email", email) + + return base + "?" + params.Encode() +} diff --git a/cla-backend-go/cmd/gitlab/webhook/main.go b/cla-backend-go/cmd/gitlab/webhook/main.go new file mode 100644 index 000000000..b7a187b98 --- /dev/null +++ b/cla-backend-go/cmd/gitlab/webhook/main.go @@ -0,0 +1,88 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package main + +import ( + "os" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/xanzy/go-gitlab" +) + +const ( + hookURL = "https://7e182f2774e2.ngrok.io/gitlab/events" +) + +func main() { + log.Println("register webhook") + access_token := os.Getenv("GITLAB_ACCESS_TOKEN") + if access_token == "" { + log.Fatal("GITLAB_ACCESS_TOKEN is required") + } + + log.Infof("The gitlab access token is : %s", access_token) + + gitlabClient, err := gitlab.NewOAuthClient(access_token) + if err != nil { + log.Fatalf("creating client failed : %v", err) + } + + user, _, err := gitlabClient.Users.CurrentUser() + if err != nil { + log.Fatalf("fetching current user failed : %v", err) + } + + log.Infof("fetched current user : %s", user.Name) + + projects, _, err := gitlabClient.Projects.ListUserProjects(user.ID, &gitlab.ListProjectsOptions{}) + if err != nil { + log.Fatalf("listing projects failed : %v", err) + } + log.Printf("we fetched : %d projects for the account", len(projects)) + for _, p := range projects { + log.Println("**********************") + log.Println("Name : ", p.Name) + log.Println("ID: ", p.ID) + hooks, _, err := gitlabClient.Projects.ListProjectHooks(p.ID, &gitlab.ListProjectHooksOptions{}) + + if err != nil { + log.Fatalf("fetching hooks for project : %s, failed : %v", p.Name, err) + } + + var claHookFound bool + for _, hook := range hooks { + log.Println("**********************") + log.Infof("hook ID : %d", hook.ID) + log.Infof("URL : %s", hook.URL) + log.Infof("Merge Request Events Enabled : %v", hook.MergeRequestsEvents) + log.Infof("Enable SSL Verification : %v", hook.EnableSSLVerification) + + if hookURL == hook.URL { + claHookFound = true + break + } + } + + if claHookFound { + log.Infof("CLA Hook was found nothing to do") + continue + } + + log.Infof("adding webhook to the project : %s (%d)", p.Name, p.ID) + if err := addCLAHookToProject(gitlabClient, p.ID); err != nil { + log.Fatalf("adding hook to the project : %s (%d) failed : %v", p.Name, p.ID, err) + } + + } + +} + +func addCLAHookToProject(gitlabClient *gitlab.Client, projectID int) error { + _, _, err := gitlabClient.Projects.AddProjectHook(projectID, &gitlab.AddProjectHookOptions{ + URL: gitlab.String(hookURL), + MergeRequestsEvents: gitlab.Bool(true), + EnableSSLVerification: gitlab.Bool(false), + }) + return err +} diff --git a/cla-backend-go/cmd/server.go b/cla-backend-go/cmd/server.go index 4cf1af8be..148d0d181 100644 --- a/cla-backend-go/cmd/server.go +++ b/cla-backend-go/cmd/server.go @@ -14,6 +14,10 @@ import ( "strconv" "strings" + "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" + + "github.com/communitybridge/easycla/cla-backend-go/gitlab" + "github.com/communitybridge/easycla/cla-backend-go/emails" "github.com/communitybridge/easycla/cla-backend-go/v2/dynamo_events" @@ -226,7 +230,10 @@ func server(localMode bool) http.Handler { if err != nil { logrus.Panic(err) } + // initialize github github.Init(configFile.GitHub.AppID, configFile.GitHub.AppPrivateKey, configFile.GitHub.AccessToken) + // initialize gitlab + gitlab.Init(configFile.Gitlab.AppID, configFile.Gitlab.AppPrivateKey) // Our backend repository handlers userRepo := user.NewDynamoRepository(awsSession, stage) @@ -241,6 +248,7 @@ func server(localMode bool) http.Handler { v1CLAGroupRepo := project.NewRepository(awsSession, stage, repositoriesRepo, gerritRepo, v1ProjectClaGroupRepo) metricsRepo := metrics.NewRepository(awsSession, stage, configFile.APIGatewayURL, v1ProjectClaGroupRepo) githubOrganizationsRepo := github_organizations.NewRepository(awsSession, stage) + gitlabOrganizationRepo := gitlab_organizations.NewRepository(awsSession, stage) claManagerReqRepo := cla_manager.NewRepository(awsSession, stage) // Our service layer handlers @@ -291,6 +299,7 @@ func server(localMode bool) http.Handler { authorizer := auth.NewAuthorizer(authValidator, userRepo) v2MetricsService := metrics.NewService(metricsRepo, v1ProjectClaGroupRepo) githubOrganizationsService := github_organizations.NewService(githubOrganizationsRepo, repositoriesRepo, v1ProjectClaGroupRepo) + gitlabOrganizationsService := gitlab_organizations.NewService(gitlabOrganizationRepo, v1ProjectClaGroupRepo) v2GithubOrganizationsService := v2GithubOrganizations.NewService(githubOrganizationsRepo, repositoriesRepo, v1ProjectClaGroupRepo, githubOrganizationsService) autoEnableService := dynamo_events.NewAutoEnableService(v1RepositoriesService, repositoriesRepo, githubOrganizationsRepo, v1ProjectClaGroupRepo, v1ProjectService) v2GithubActivityService := v2GithubActivity.NewService(repositoriesRepo, githubOrganizationsRepo, eventsService, autoEnableService, emailService) @@ -330,6 +339,7 @@ func server(localMode bool) http.Handler { v2Metrics.Configure(v2API, v2MetricsService, v1CompanyRepo) github_organizations.Configure(api, githubOrganizationsService, eventsService) v2GithubOrganizations.Configure(v2API, v2GithubOrganizationsService, eventsService) + gitlab_organizations.Configure(v2API, gitlabOrganizationsService, eventsService) repositories.Configure(api, v1RepositoriesService, eventsService) v2Repositories.Configure(v2API, v2RepositoriesService, eventsService) gerrits.Configure(api, gerritService, v1ProjectService, eventsService) diff --git a/cla-backend-go/config/config.go b/cla-backend-go/config/config.go index 2d3629674..07c6d58dd 100644 --- a/cla-backend-go/config/config.go +++ b/cla-backend-go/config/config.go @@ -51,6 +51,9 @@ type Config struct { // GitHub Application GitHub GitHub `json:"github"` + // Gitlab Application + Gitlab Gitlab `json:"gitlab"` + // Dynamo Session Store SessionStoreTableName string `json:"sessionStoreTableName"` @@ -134,6 +137,15 @@ type GitHub struct { TestRepositoryID string `json:"test_repository_id"` } +// Gitlab model +type Gitlab struct { + ClientSecret string `json:"clientSecret"` + AppID string `json:"app_id"` + AppPrivateKey string `json:"app_private_key"` + RedirectURI string `json:"redirect_uri"` + WebHookURI string `json:"web_hook_uri"` +} + // MetricsReport keeps the config needed to send the metrics data report type MetricsReport struct { AwsSQSRegion string `json:"aws_sqs_region"` diff --git a/cla-backend-go/config/ssm.go b/cla-backend-go/config/ssm.go index 06d83a970..4d98e3f1e 100644 --- a/cla-backend-go/config/ssm.go +++ b/cla-backend-go/config/ssm.go @@ -70,6 +70,11 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint fmt.Sprintf("cla-gh-test-organization-installation-id-%s", stage), fmt.Sprintf("cla-gh-test-repository-%s", stage), fmt.Sprintf("cla-gh-test-repository-id-%s", stage), + fmt.Sprintf("cla-gitlab-oauth-secret-go-backend-%s", stage), + fmt.Sprintf("cla-gitlab-app-id-%s", stage), + fmt.Sprintf("cla-gitlab-app-private-key-%s", stage), + fmt.Sprintf("cla-gitlab-app-redirect-uri-%s", stage), + fmt.Sprintf("cla-gitlab-app-web-hook-uri-%s", stage), fmt.Sprintf("cla-corporate-base-%s", stage), fmt.Sprintf("cla-corporate-v1-base-%s", stage), fmt.Sprintf("cla-corporate-v2-base-%s", stage), @@ -150,6 +155,18 @@ func loadSSMConfig(awsSession *session.Session, stage string) Config { //nolint case fmt.Sprintf("cla-gh-test-repository-id-%s", stage): config.GitHub.TestRepositoryID = resp.value + // gitlab ssm + case fmt.Sprintf("cla-gitlab-oauth-secret-go-backend-%s", stage): + config.Gitlab.ClientSecret = resp.value + case fmt.Sprintf("cla-gitlab-app-id-%s", stage): + config.Gitlab.AppID = resp.value + case fmt.Sprintf("cla-gitlab-app-private-key-%s", stage): + config.Gitlab.AppPrivateKey = resp.value + case fmt.Sprintf("cla-gitlab-app-redirect-uri-%s", stage): + config.Gitlab.RedirectURI = resp.value + case fmt.Sprintf("cla-gitlab-app-web-hook-uri-%s", stage): + config.Gitlab.WebHookURI = resp.value + case fmt.Sprintf("cla-corporate-base-%s", stage): config.CorporateConsoleURL = resp.value case fmt.Sprintf("cla-corporate-v1-base-%s", stage): diff --git a/cla-backend-go/events/event_data.go b/cla-backend-go/events/event_data.go index 7a2a7247c..47954707b 100644 --- a/cla-backend-go/events/event_data.go +++ b/cla-backend-go/events/event_data.go @@ -191,6 +191,26 @@ type GitHubOrganizationUpdatedEventData struct { BranchProtectionEnabled bool } +// GitlabOrganizationAddedEventData data model +type GitlabOrganizationAddedEventData struct { + GitlabOrganizationName string + AutoEnabled bool + AutoEnabledClaGroupID string + BranchProtectionEnabled bool +} + +// GitlabOrganizationDeletedEventData data model +type GitlabOrganizationDeletedEventData struct { + GitlabOrganizationName string +} + +// GitlabOrganizationUpdatedEventData data model +type GitlabOrganizationUpdatedEventData struct { + GitlabOrganizationName string + AutoEnabled bool + AutoEnabledClaGroupID string +} + // CCLAApprovalListRequestCreatedEventData data model type CCLAApprovalListRequestCreatedEventData struct { RequestID string @@ -652,6 +672,44 @@ func (ed *GitHubOrganizationUpdatedEventData) GetEventDetailsString(args *LogEve return data, true } +// GetEventDetailsString returns the details string for this event +func (ed *GitlabOrganizationAddedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("Gitlab Organization: %s was added with auto-enabled: %t, with branch protection enabled: %t", + ed.GitlabOrganizationName, ed.AutoEnabled, ed.BranchProtectionEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *GitlabOrganizationDeletedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("GitHub Organization: %s was deleted ", ed.GitlabOrganizationName) + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventDetailsString returns the details string for this event +func (ed *GitlabOrganizationUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("GitHub Organization:%s was updated with auto-enabled: %t", + ed.GitlabOrganizationName, ed.AutoEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + // GetEventDetailsString returns the details string for this event func (ed *CCLAApprovalListRequestApprovedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("User: %s approved a CCLA Approval Request for Project: %s and Company: %s with Request ID: %s.", @@ -1558,6 +1616,62 @@ func (ed *GitHubOrganizationUpdatedEventData) GetEventSummaryString(args *LogEve return data, true } +// GetEventSummaryString returns the summary string for this event +func (ed *GitlabOrganizationAddedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The Gitlab organization %s was added with auto-enabled set to %t with branch protection enabled set to %t", + ed.GitlabOrganizationName, ed.AutoEnabled, ed.BranchProtectionEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = data + fmt.Sprintf(" with auto-enabled-cla-group set to %s", ed.AutoEnabledClaGroupID) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for the CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for the project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GitlabOrganizationDeletedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("The Gitlab organization %s was deleted", ed.GitlabOrganizationName) + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + +// GetEventSummaryString returns the summary string for this event +func (ed *GitlabOrganizationUpdatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { + data := fmt.Sprintf("Gitlab Organization: %s was updated with auto-enabled: %t", + ed.GitlabOrganizationName, ed.AutoEnabled) + if ed.AutoEnabledClaGroupID != "" { + data = data + fmt.Sprintf(" with auto-enabled-cla-group: %s", ed.AutoEnabledClaGroupID) + } + if args.CLAGroupName != "" { + data = data + fmt.Sprintf(" for CLA Group %s", args.CLAGroupName) + } + if args.ProjectName != "" { + data = data + fmt.Sprintf(" for project %s", args.ProjectName) + } + if args.UserName != "" { + data = data + fmt.Sprintf(" by the user %s", args.UserName) + } + data = data + "." + return data, true +} + // GetEventSummaryString returns the summary string for this event func (ed *CCLAApprovalListRequestApprovedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) { data := fmt.Sprintf("The user %s approved a CCLA approval request", args.UserName) diff --git a/cla-backend-go/events/event_types.go b/cla-backend-go/events/event_types.go index 4f36d5459..85cffeea7 100644 --- a/cla-backend-go/events/event_types.go +++ b/cla-backend-go/events/event_types.go @@ -49,6 +49,10 @@ const ( GitHubOrganizationDeleted = "github_organization.deleted" GitHubOrganizationUpdated = "github_organization.updated" + GitlabOrganizationAdded = "gitlab_organization.added" + GitlabOrganizationDeleted = "gitlab_organization.deleted" + GitlabOrganizationUpdated = "gitlab_organization.updated" + CompanyACLUserAdded = "company_acl.user_added" CompanyACLRequestAdded = "company_acl.request_added" CompanyACLRequestApproved = "company_acl.request_approved" diff --git a/cla-backend-go/gitlab/auth.go b/cla-backend-go/gitlab/auth.go new file mode 100644 index 000000000..d9da3a218 --- /dev/null +++ b/cla-backend-go/gitlab/auth.go @@ -0,0 +1,33 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "github.com/communitybridge/easycla/cla-backend-go/config" + "github.com/go-resty/resty/v2" +) + +// FetchOauthCredentials is responsible for fetching the credentials from gitlab for alredy started Oauth process (access_token, refresh_token) +func FetchOauthCredentials(code string) (*OauthSuccessResponse, error) { + client := resty.New() + params := map[string]string{ + "client_id": config.GetConfig().Gitlab.AppID, + "client_secret": config.GetConfig().Gitlab.ClientSecret, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": config.GetConfig().Gitlab.RedirectURI, + //"redirect_uri": "http://localhost:8080/v4/gitlab/oauth/callback", + } + + resp, err := client.R(). + SetQueryParams(params). + SetResult(&OauthSuccessResponse{}). + Post("https://gitlab.com/oauth/token") + + if err != nil { + return nil, err + } + + return resp.Result().(*OauthSuccessResponse), nil +} diff --git a/cla-backend-go/gitlab/client.go b/cla-backend-go/gitlab/client.go new file mode 100644 index 000000000..d04757245 --- /dev/null +++ b/cla-backend-go/gitlab/client.go @@ -0,0 +1,141 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + + "github.com/xanzy/go-gitlab" +) + +// OauthSuccessResponse is success response from Gitlab +type OauthSuccessResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + CreatedAt int `json:"created_at"` +} + +// NewGitlabOauthClient creates a new gitlab client from the given oauth info, authInfo is encrypted +func NewGitlabOauthClient(authInfo string) (*gitlab.Client, error) { + oauthResp, err := DecryptAuthInfo(authInfo) + if err != nil { + return nil, err + } + + log.Infof("creating oauth client with access token : %s", oauthResp.AccessToken) + return gitlab.NewOAuthClient(oauthResp.AccessToken) +} + +// EncryptAuthInfo encrypts the oauth response into a string +func EncryptAuthInfo(oauthResp *OauthSuccessResponse) (string, error) { + key := getGitlabAppPrivateKey() + keyDecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", fmt.Errorf("decode key : %v", err) + } + + b, err := json.Marshal(oauthResp) + if err != nil { + return "", fmt.Errorf("oauth resp json marshall : %v", err) + } + authInfo := string(b) + //log.Infof("auth info before encrypting : %s", authInfo) + + encrypted, err := encrypt(keyDecoded, []byte(authInfo)) + if err != nil { + return "", fmt.Errorf("encrypt failed : %v", err) + } + + return hex.EncodeToString(encrypted), nil +} + +// DecryptAuthInfo decrytps the authinfo into OauthSuccessResponse data structure +func DecryptAuthInfo(authInfoEncoded string) (*OauthSuccessResponse, error) { + ciphertext, err := hex.DecodeString(authInfoEncoded) + if err != nil { + return nil, fmt.Errorf("decode auth info %s : %v", authInfoEncoded, err) + } + + //log.Infof("auth info decoded : %s", ciphertext) + + key := getGitlabAppPrivateKey() + keyDecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, fmt.Errorf("decode key : %v", err) + } + + //log.Debugf("before decrypt : keyDecoded : %s, cipherText : %s", keyDecoded, ciphertext) + decrypted, err := decrypt(keyDecoded, ciphertext) + if err != nil { + return nil, fmt.Errorf("decrypt failed : %v", err) + } + //log.Debugf("after decrypt : keyDecoded : %s, decrypted : %s", keyDecoded, decrypted) + + var oauthResp OauthSuccessResponse + if err := json.Unmarshal(decrypted, &oauthResp); err != nil { + return nil, fmt.Errorf("unmarshall auth info : %v", err) + } + + return &oauthResp, nil +} + +func encrypt(key, message []byte) ([]byte, error) { + // Initialize block cipher + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // Create the byte slice that will hold encrypted message + cipherText := make([]byte, aes.BlockSize+len(message)) + + // Generate the Initialization Vector (IV) nonce + // which is stored at the beginning of the byte slice + // The IV is the same length as the AES blocksize + iv := cipherText[:aes.BlockSize] + _, err = io.ReadFull(rand.Reader, iv) + if err != nil { + return nil, err + } + + // Choose the block cipher mode of operation + // Using the cipher feedback (CFB) mode here. + // CBCEncrypter also available. + cfb := cipher.NewCFBEncrypter(block, iv) + // Generate the encrypted message and store it + // in the remaining bytes after the IV nonce + cfb.XORKeyStream(cipherText[aes.BlockSize:], message) + + return cipherText, nil +} + +// AES decryption +func decrypt(key, cipherText []byte) ([]byte, error) { + // Initialize block cipher + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // Separate the IV nonce from the encrypted message bytes + iv := cipherText[:aes.BlockSize] + cipherText = cipherText[aes.BlockSize:] + + // Decrypt the message using the CFB block mode + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(cipherText, cipherText) + + return cipherText, nil +} diff --git a/cla-backend-go/gitlab/client_test.go b/cla-backend-go/gitlab/client_test.go new file mode 100644 index 000000000..46c994c2c --- /dev/null +++ b/cla-backend-go/gitlab/client_test.go @@ -0,0 +1,57 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +var key = "0WqnDWHnZKo2cmQ8m93EtY9ZBpfzQW4UnnEuRmgtJKM=" +var oauthRespStr = `{"access_token":"a30671b8749ba5d48925712344377f11a5aba43ec630f099e464b9843796e6a6","token_type":"Bearer","expires_in":0,"refresh_token":"0838a31d0d796973eacefdf513523e6e47aa06fac9d26622964da1e473509458","created_at":1626435922}` + +func TestNewGitlabOauthClient(t *testing.T) { + Init("124453345", key) + t.Cleanup(func() { + gitlabAppPrivateKey = "" + }) + + t.Logf("app private key is : %s", getGitlabAppPrivateKey()) + + var oauthResp OauthSuccessResponse + err := json.Unmarshal([]byte(oauthRespStr), &oauthResp) + assert.NoError(t, err) + + encrypted, err := EncryptAuthInfo(&oauthResp) + assert.NoError(t, err) + + client, err := NewGitlabOauthClient(encrypted) + assert.NoError(t, err) + assert.NotNil(t, client) +} + +func TestEncryptDecryptAuthInfo(t *testing.T) { + Init("124453345", key) + t.Cleanup(func() { + gitlabAppPrivateKey = "" + }) + + t.Logf("app private key is : %s", getGitlabAppPrivateKey()) + + var oauthResp OauthSuccessResponse + err := json.Unmarshal([]byte(oauthRespStr), &oauthResp) + assert.NoError(t, err) + t.Logf("unmarshall ok : %+v", oauthResp) + + encrypted, err := EncryptAuthInfo(&oauthResp) + assert.NoError(t, err) + t.Logf("encrypted auth info : %s", encrypted) + + oauthRespDecrypted, err := DecryptAuthInfo(encrypted) + assert.NoError(t, err) + + assert.Equal(t, &oauthResp, oauthRespDecrypted) +} diff --git a/cla-backend-go/gitlab/init.go b/cla-backend-go/gitlab/init.go new file mode 100644 index 000000000..b390e6124 --- /dev/null +++ b/cla-backend-go/gitlab/init.go @@ -0,0 +1,15 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +var gitlabAppPrivateKey string + +// Init initializes the required gitlab variables +func Init(glAppID string, glAppPrivateKey string) { + gitlabAppPrivateKey = glAppPrivateKey +} + +func getGitlabAppPrivateKey() string { + return gitlabAppPrivateKey +} diff --git a/cla-backend-go/gitlab/organization.go b/cla-backend-go/gitlab/organization.go new file mode 100644 index 000000000..5144bd4ad --- /dev/null +++ b/cla-backend-go/gitlab/organization.go @@ -0,0 +1,26 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab + +import ( + "fmt" + + "github.com/xanzy/go-gitlab" +) + +// GetGroupByName gets a gitlab Group by the given name +func GetGroupByName(client *gitlab.Client, name string) (*gitlab.Group, error) { + groups, _, err := client.Groups.ListGroups(&gitlab.ListGroupsOptions{}) + if err != nil { + return nil, fmt.Errorf("fetching groups failed : %v", err) + } + + for _, group := range groups { + if group.Name == name { + return group, nil + } + } + + return nil, nil +} diff --git a/cla-backend-go/go.mod b/cla-backend-go/go.mod index 106261197..6e9d455f7 100644 --- a/cla-backend-go/go.mod +++ b/cla-backend-go/go.mod @@ -20,6 +20,7 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/fnproject/fdk-go v0.0.2 github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/gin-gonic/gin v1.7.2 github.com/go-openapi/errors v0.19.6 github.com/go-openapi/loads v0.19.5 github.com/go-openapi/runtime v0.19.19 @@ -27,10 +28,11 @@ require ( github.com/go-openapi/strfmt v0.19.5 github.com/go-openapi/swag v0.19.9 github.com/go-openapi/validate v0.19.10 + github.com/go-playground/validator/v10 v10.7.0 // indirect github.com/go-resty/resty/v2 v2.3.0 github.com/gofrs/uuid v4.0.0+incompatible github.com/golang/mock v1.4.4 - github.com/golang/protobuf v1.4.3 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-github/v33 v33.0.0 github.com/google/uuid v1.1.4 github.com/gorilla/sessions v1.2.1 // indirect @@ -38,9 +40,12 @@ require ( github.com/jessevdk/go-flags v1.4.0 github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a github.com/jmoiron/sqlx v1.2.0 + github.com/json-iterator/go v1.1.11 // indirect github.com/juju/mempool v0.0.0-20160205104927-24974d6c264f // indirect github.com/juju/zip v0.0.0-20160205105221-f6b1e93fa2e2 github.com/kr/pretty v0.2.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect github.com/mitchellh/mapstructure v1.3.2 github.com/mozillazg/request v0.8.0 // indirect github.com/pdfcpu/pdfcpu v0.3.5-0.20200802160406-be1e0eb55afc @@ -57,13 +62,19 @@ require ( github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.6.1 github.com/tencentyun/scf-go-lib v0.0.0-20200116145541-9a6ea1bf75b8 + github.com/ugorji/go v1.2.6 // indirect github.com/verdverm/frisby v0.0.0-20170604211311-b16556248a9a + github.com/xanzy/go-gitlab v0.50.1 go.uber.org/ratelimit v0.1.0 - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + golang.org/x/text v0.3.6 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e google.golang.org/appengine v1.6.6 // indirect - google.golang.org/protobuf v1.24.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect gopkg.in/ini.v1 v1.57.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/cla-backend-go/go.sum b/cla-backend-go/go.sum index 7e1875a77..e5bde5d75 100644 --- a/cla-backend-go/go.sum +++ b/cla-backend-go/go.sum @@ -80,8 +80,6 @@ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I= github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug= -github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= @@ -113,10 +111,6 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fnproject/fdk-go v0.0.2 h1:nebofQYAY8SbcjqmoaBo6KLNTwUrJq6lGdi7RCbq/EA= @@ -126,10 +120,12 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= -github.com/gin-gonic/gin v0.0.0-20180126034611-783c7ee9c14e h1:5CNDPg63TSvbNi3viqFnIywPu2TqLMlWAPuFuuTrFe4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v0.0.0-20180126034611-783c7ee9c14e/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= +github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -206,6 +202,15 @@ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2K github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNPlDK+Yk= github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.7.0 h1:gLi5ajTBBheLNt0ctewgq7eolXoDALQd5/y90Hh9ZgM= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= @@ -267,14 +272,15 @@ github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -283,14 +289,17 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github/v29 v29.0.2 h1:opYN6Wc7DOz7Ku3Oh4l7prmkOMwEcQxpFtxdU8N8Pts= github.com/google/go-github/v29 v29.0.2/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= github.com/google/go-github/v33 v33.0.0 h1:qAf9yP0qc54ufQxzwv+u9H0tiVOnPJxo0lI/JXqw3ZM= github.com/google/go-github/v33 v33.0.0/go.mod h1:GMdDnVZY/2TsWgp/lkYnpSAh6TrzhANBBwm6k6TTEXg= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -334,12 +343,16 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= +github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= @@ -390,8 +403,10 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v0.0.0-20180128142709-bca911dae073/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -434,6 +449,9 @@ github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8 github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0= github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= @@ -452,8 +470,10 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -475,8 +495,10 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= @@ -520,9 +542,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -604,8 +625,13 @@ github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v0.0.0-20180129160544-d2b24cf3d3b4 h1:euf5tLM++W5h5uyfs6NSMoCGGhw+hRXMLE/DU6hireM= github.com/ugorji/go v0.0.0-20180129160544-d2b24cf3d3b4/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/negroni v0.0.0-20180130044549-22c5532ea862 h1:eg5xqGZGatsyRpVnFJkdeUWSFk46lDgkXLvOryv5ySg= @@ -616,6 +642,8 @@ github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8W github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/verdverm/frisby v0.0.0-20170604211311-b16556248a9a h1:Mt+KWT4h97wIDQahX1eD3OLkmc/fGbLy7EndiE85kMQ= github.com/verdverm/frisby v0.0.0-20170604211311-b16556248a9a/go.mod h1:Z+jvFzFlZ6eHAKMfi8PZZphUtg4S0gc2EZYOL9UnWgA= +github.com/xanzy/go-gitlab v0.50.1 h1:eH1G0/ZV1j81rhGrtbcePjbM5Ern7mPA4Xjt+yE+2PQ= +github.com/xanzy/go-gitlab v0.50.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo= @@ -654,8 +682,9 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -703,9 +732,12 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= @@ -717,8 +749,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -741,17 +774,25 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -768,7 +809,6 @@ golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -793,6 +833,7 @@ google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -806,25 +847,21 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -845,8 +882,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= @@ -854,7 +892,6 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= diff --git a/cla-backend-go/serverless.yml b/cla-backend-go/serverless.yml index a92ccab80..60cf10ac0 100644 --- a/cla-backend-go/serverless.yml +++ b/cla-backend-go/serverless.yml @@ -134,6 +134,7 @@ provider: - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-users" - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-metrics" - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gitlab-orgs" - Effect: Allow Action: - dynamodb:Query @@ -193,6 +194,9 @@ provider: - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-cla-manager-requests/index/cla-manager-requests-project-index" - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups/index/cla-group-id-index" - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-projects-cla-groups/index/foundation-sfid-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gitlab-orgs/index/gitlab-org-sfid-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gitlab-orgs/index/gitlab-organization-name-lower-search-index" + - "arn:aws:dynamodb:${self:custom.dynamodb.region}:#{AWS::AccountId}:table/cla-${opt:stage}-gitlab-orgs/index/gitlab-project-sfid-organization-name-index" environment: STAGE: ${self:provider.stage} diff --git a/cla-backend-go/swagger/cla.v2.yaml b/cla-backend-go/swagger/cla.v2.yaml index 06a4ac8b5..27131ee98 100644 --- a/cla-backend-go/swagger/cla.v2.yaml +++ b/cla-backend-go/swagger/cla.v2.yaml @@ -1604,6 +1604,81 @@ paths: tags: - github-repositories + /project/{projectSFID}/gitlab/organizations: + post: + summary: Add new Gitlab Organization in the project + description: Endpoint to create a new Gitlab Organization in EasyCLA + operationId: addProjectGitlabOrganization + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + required: true + - in: body + name: body + schema: + $ref: '#/definitions/create-gitlab-organization' + required: true + responses: + '200': + description: 'Success' + headers: + x-request-id: + type: string + description: The unique request ID value - assigned/set by the API Gateway based on the session + schema: + $ref: '#/definitions/gitlab-organization' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '409': + $ref: '#/responses/conflict' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-organizations + get: + summary: Get the Gitlab organizations of the project + description: Endpoint to return the list of Gitlab organization for the project + operationId: getProjectGitlabOrganizations + parameters: + - $ref: "#/parameters/x-request-id" + - $ref: "#/parameters/x-acl" + - $ref: "#/parameters/x-username" + - $ref: "#/parameters/x-email" + - name: projectSFID + in: path + type: string + required: true + responses: + '200': + description: 'Success' + headers: + x-request-id: + type: string + description: The unique request ID value - assigned/set by the API Gateway based on the session + schema: + $ref: '#/definitions/project-gitlab-organizations' + '400': + $ref: '#/responses/invalid-request' + '401': + $ref: '#/responses/unauthorized' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-organizations + /cla-group/{claGroupID}/icla/signatures: get: summary: List individual signatures for CLA Group @@ -3665,6 +3740,43 @@ paths: tags: - github-activity + /gitlab/oauth/callback: + get: + summary: The endpoint is called after user authorizes EasyCLA bot + description: The endpoint is responsible for storing the access token for the user and registering the webhooks is autoenable is on + security: [] + operationId: gitlabOauthCallback + parameters: + - name: code + description: oauth code used to fetch the access token + in: query + type: string + required: true + - name: state + description: state is used to find the gitlab organization + in: query + type: string + required: true + responses: + '200': + description: 'Success' + headers: + x-request-id: + type: string + description: The unique request ID value - assigned/set by the API Gateway based on the session + schema: + $ref: '#/definitions/success-response' + '400': + $ref: '#/responses/invalid-request' + '403': + $ref: '#/responses/forbidden' + '404': + $ref: '#/responses/not-found' + '500': + $ref: '#/responses/internal-server-error' + tags: + - gitlab-activity + responses: unauthorized: description: Unauthorized @@ -3825,7 +3937,7 @@ parameters: in: query type: string required: false - enum: [ccla,ecla,cla] + enum: [ ccla,ecla,cla ] claType: name: claType description: > @@ -3836,7 +3948,7 @@ parameters: in: query type: string required: false - enum: [ccla,ecla,icla] + enum: [ ccla,ecla,icla ] templateCLAType: name: claType in: query @@ -4119,6 +4231,12 @@ definitions: update-github-organization: $ref: './common/update-github-organization.yaml' + gitlab-organization: + $ref: './common/gitlab-organization.yaml' + + create-gitlab-organization: + $ref: './common/create-github-organization.yaml' + user: $ref: './common/user.yaml' @@ -5139,6 +5257,9 @@ definitions: items: type: string + gitlab-organizations: + $ref: './common/gitlab-organizations.yaml' + project-github-organizations: type: object properties: @@ -5190,6 +5311,56 @@ definitions: items: $ref: '#/definitions/project-github-repository' + project-gitlab-organizations: + type: object + properties: + list: + type: array + items: + $ref: '#/definitions/project-gitlab-organization' + + project-gitlab-organization: + type: object + properties: + auto_enabled: + type: boolean + description: Flag to indicate if auto-enabled flag should be enabled. Organizations with auto-enable turned on will automatically include any new repositories to the EasyCLA configuration. + x-omitempty: false + autoEnableCLAGroupID: + type: string + description: The CLA Group ID which is attached to the auto-enabled flag + autoEnabledCLAGroupName: + type: string + description: The CLA Group name which is attached to the auto-enabled flag + branchProtectionEnabled: + type: boolean + description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. + x-omitempty: false + installationURL: + type: string + x-nullable: true + format: uri + gitlab_organization_name: + type: string + description: The Gitlab Organization name + example: "kubernetes" + # Pattern aligns with UI and other platform services including Org Service + # \w Any word character (alphanumeric & underscore), dashes, periods + pattern: '^([\w\-\.]+){2,255}$' + minLength: 2 + maxLength: 255 + connection_status: + type: string + enum: + - connected + - partial_connection + - connection_failure + - no_connection + repositories: + type: array + items: + $ref: '#/definitions/project-github-repository' + project-github-repository: type: object properties: diff --git a/cla-backend-go/swagger/common/create-github-organization.yaml b/cla-backend-go/swagger/common/create-github-organization.yaml index 56e32e3ab..1432b16ad 100644 --- a/cla-backend-go/swagger/common/create-github-organization.yaml +++ b/cla-backend-go/swagger/common/create-github-organization.yaml @@ -7,7 +7,7 @@ required: properties: organizationName: type: string - description: The GitHub Organization name + description: The Organization name example: "kubernetes" # Pattern aligns with UI and other platform services including Org Service # \w Any word character (alphanumeric & underscore), dashes, periods @@ -23,5 +23,5 @@ properties: description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. branchProtectionEnabled: type: boolean - description: Flag to indicate if this GitHub Organization is configured to automatically setup branch protection on CLA enabled repositories. + description: Flag to indicate if this Organization is configured to automatically setup branch protection on CLA enabled repositories. default: false diff --git a/cla-backend-go/swagger/common/gitlab-organization.yaml b/cla-backend-go/swagger/common/gitlab-organization.yaml new file mode 100644 index 000000000..e344805bf --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-organization.yaml @@ -0,0 +1,82 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + organizationID: + type: string + description: internal id of the gitlab organization + dateCreated: + type: string + example: "2020-02-06T09:31:49.245630+0000" + minLength: 18 + maxLength: 64 + dateModified: + type: string + example: "2020-02-06T09:31:49.245646+0000" + minLength: 18 + maxLength: 64 + organizationName: + type: string + example: "communitybridge" + organizationSfid: + type: string + example: "a0941000002wBz4AAA" + version: + type: string + example: "v1" + projectSFID: + type: string + example: "a0941000002wBz4AAA" + enabled: + type: boolean + description: Flag that indicates whether this Gitlab Organization is active + x-omitempty: false + connected: + type: boolean + description: Flag that indicates whether this Gitlab Organization is authorized with Gitlab, if false it might mean that Gitlab Oauth process is not compeleted yet or the token was revoked and user needs to go through the auth process again + x-omitempty: false + autoEnabled: + type: boolean + description: Flag to indicate if this Gitlab Organization is configured to allow new repositories to be auto-enabled/auto-enrolled in EasyCLA. + x-omitempty: false + autoEnabledClaGroupID: + type: string + description: Specifies which Cla group ID to be used when autoEnabled flag in enabled for the Github Organization. If autoEnabled is on this field needs to be set as well. + gitlabInfo: + type: object + properties: + error: + type: string + example: "unable to get gitlab info of communitybridge" + details: + type: object + properties: + id: + type: integer + x-nullable: true + example: 1476068 + bio: + type: string + x-nullable: true + htmlUrl: + type: string + x-nullable: true + example: "https://github.com/communitybridge" + format: uri + installationURL: + type: string + x-nullable: true + description: "if the Gitlab Organization is not connected yet can use this url to go through the process of authorizing the easyCLA bot" + format: uri + + repositories: + type: object + properties: + error: + type: string + example: "unable to get repositories for installation id : 6854001" + list: + type: array + items: + $ref: '#/definitions/github-repository-info' diff --git a/cla-backend-go/swagger/common/gitlab-organizations.yaml b/cla-backend-go/swagger/common/gitlab-organizations.yaml new file mode 100644 index 000000000..42a292a78 --- /dev/null +++ b/cla-backend-go/swagger/common/gitlab-organizations.yaml @@ -0,0 +1,9 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +properties: + list: + type: array + items: + $ref: '#/definitions/gitlab-organization' diff --git a/cla-backend-go/utils/const.go b/cla-backend-go/utils/const.go new file mode 100644 index 000000000..bc69f3844 --- /dev/null +++ b/cla-backend-go/utils/const.go @@ -0,0 +1,15 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package utils + +const ( + // Connected status + Connected = "connected" + // PartialConnection status + PartialConnection = "partial_connection" + // ConnectionFailure status + ConnectionFailure = "connection_failure" + // NoConnection status + NoConnection = "no_connection" +) diff --git a/cla-backend-go/utils/utils_user_auth_standalone.go b/cla-backend-go/utils/utils_user_auth_standalone.go index 8654d9d7e..85a61676f 100644 --- a/cla-backend-go/utils/utils_user_auth_standalone.go +++ b/cla-backend-go/utils/utils_user_auth_standalone.go @@ -87,6 +87,8 @@ func IsUserAuthorizedForProjectTree(ctx context.Context, user *auth.User, projec "adminScopeAllowed": adminScopeAllowed, } + log.WithFields(f).Debugf("checking user auth for project tree") + // If we are running locally and want to disable permission checks if skipPermissionChecks() { log.WithFields(f).Debug("skipping permissions check") diff --git a/cla-backend-go/v2/github_organizations/service.go b/cla-backend-go/v2/github_organizations/service.go index 3655538ba..e4ccf784b 100644 --- a/cla-backend-go/v2/github_organizations/service.go +++ b/cla-backend-go/v2/github_organizations/service.go @@ -62,17 +62,6 @@ func NewService(repo v1GithubOrg.RepositoryInterface, ghRepository v1Repositorie } } -const ( - // Connected status - Connected = "connected" - // PartialConnection status - PartialConnection = "partial_connection" - // ConnectionFailure status - ConnectionFailure = "connection_failure" - // NoConnection status - NoConnection = "no_connection" -) - func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) (*models.ProjectGithubOrganizations, error) { f := logrus.Fields{ "functionName": "v2.github_organizations.service.GetGitHubOrganizations", @@ -166,12 +155,12 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) orgmap[org.OrganizationName] = rorg out.List = append(out.List, rorg) if org.OrganizationInstallationID == 0 { - rorg.ConnectionStatus = NoConnection + rorg.ConnectionStatus = utils.NoConnection } else { if org.Repositories.Error != "" { - rorg.ConnectionStatus = ConnectionFailure + rorg.ConnectionStatus = utils.ConnectionFailure } else { - rorg.ConnectionStatus = Connected + rorg.ConnectionStatus = utils.Connected } } } @@ -208,7 +197,7 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) log.WithFields(f).WithError(err).Warn("repository github id is not integer") } rorg.Repositories = append(rorg.Repositories, &models.ProjectGithubRepository{ - ConnectionStatus: Connected, + ConnectionStatus: utils.Connected, Enabled: repo.Enabled, RepositoryID: repo.RepositoryID, RepositoryName: repo.RepositoryName, @@ -223,7 +212,7 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) delete(connectedRepo, key) } else { rorg.Repositories = append(rorg.Repositories, &models.ProjectGithubRepository{ - ConnectionStatus: ConnectionFailure, + ConnectionStatus: utils.ConnectionFailure, Enabled: repo.Enabled, RepositoryID: repo.RepositoryID, RepositoryName: repo.RepositoryName, @@ -231,8 +220,8 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) ProjectID: repo.ProjectSFID, ParentProjectID: repo.RepositorySfdcID, }) - if rorg.ConnectionStatus == Connected { - rorg.ConnectionStatus = PartialConnection + if rorg.ConnectionStatus == utils.Connected { + rorg.ConnectionStatus = utils.PartialConnection } } } @@ -244,7 +233,7 @@ func (s service) GetGithubOrganizations(ctx context.Context, projectSFID string) continue } rorg.Repositories = append(rorg.Repositories, &models.ProjectGithubRepository{ - ConnectionStatus: Connected, + ConnectionStatus: utils.Connected, Enabled: false, RepositoryID: "", RepositoryName: notEnabledRepo.repoInfo.RepositoryName, diff --git a/cla-backend-go/v2/gitlab_organizations/handlers.go b/cla-backend-go/v2/gitlab_organizations/handlers.go new file mode 100644 index 000000000..ef77190ba --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/handlers.go @@ -0,0 +1,206 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +import ( + "context" + "fmt" + "strings" + + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/gitlab_activity" + "github.com/communitybridge/easycla/cla-backend-go/gitlab" + "github.com/gofrs/uuid" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/sirupsen/logrus" + + "github.com/LF-Engineering/lfx-kit/auth" + "github.com/communitybridge/easycla/cla-backend-go/events" + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations" + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/gitlab_organizations" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/go-openapi/runtime/middleware" +) + +// Configure setups handlers on api with service +func Configure(api *operations.EasyclaAPI, service Service, eventService events.Service) { + + api.GitlabOrganizationsGetProjectGitlabOrganizationsHandler = gitlab_organizations.GetProjectGitlabOrganizationsHandlerFunc( + func(params gitlab_organizations.GetProjectGitlabOrganizationsParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint + + f := logrus.Fields{ + "functionName": "gitlab_organizations.handlers.GitlabOrganizationsGetProjectGitlabOrganizationsHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Get Project GitHub Organizations with Project scope of %s", + authUser.UserName, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewGetProjectGitlabOrganizationsForbidden().WithPayload( + utils.ErrorResponseForbidden(reqID, msg)) + } + + result, err := service.GetGitlabOrganizations(ctx, params.ProjectSFID) + if err != nil { + if strings.ContainsAny(err.Error(), "getProjectNotFound") { + msg := fmt.Sprintf("Gitlab organization with project SFID not found: %s", params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewGetProjectGitlabOrganizationsNotFound().WithPayload( + utils.ErrorResponseNotFound(reqID, msg)) + } + + msg := fmt.Sprintf("failed to locate Gitlab organization by project SFID: %s, error: %+v", params.ProjectSFID, err) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewGetProjectGitlabOrganizationsBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + return gitlab_organizations.NewGetProjectGitlabOrganizationsOK().WithPayload(result) + }) + + api.GitlabOrganizationsAddProjectGitlabOrganizationHandler = gitlab_organizations.AddProjectGitlabOrganizationHandlerFunc( + func(params gitlab_organizations.AddProjectGitlabOrganizationParams, authUser *auth.User) middleware.Responder { + reqID := utils.GetRequestID(params.XREQUESTID) + utils.SetAuthUserProperties(authUser, params.XUSERNAME, params.XEMAIL) + ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqID) // nolint + + f := logrus.Fields{ + "functionName": "Gitlab_organization.handlers.GitlabOrganizationsAddProjectGitlabOrganizationHandler", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "authUser": authUser.UserName, + "authEmail": authUser.Email, + "projectSFID": params.ProjectSFID, + } + + if !utils.IsUserAuthorizedForProjectTree(ctx, authUser, params.ProjectSFID, utils.ALLOW_ADMIN_SCOPE) { + msg := fmt.Sprintf("user %s does not have access to Add Project Gitlab Organizations with Project scope of %s", + authUser.UserName, params.ProjectSFID) + log.WithFields(f).Debug(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationForbidden().WithPayload( + utils.ErrorResponseForbidden(reqID, msg)) + } + + // Quick check of the parameters + if params.Body == nil || params.Body.OrganizationName == nil { + msg := fmt.Sprintf("missing organization name in body: %+v", params.Body) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + f["organizationName"] = utils.StringValue(params.Body.OrganizationName) + + if params.Body.AutoEnabled == nil { + msg := fmt.Sprintf("missing autoEnabled name in body: %+v", params.Body) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + f["autoEnabled"] = utils.BoolValue(params.Body.AutoEnabled) + f["autoEnabledClaGroupID"] = params.Body.AutoEnabledClaGroupID + + if !utils.ValidateAutoEnabledClaGroupID(params.Body.AutoEnabled, params.Body.AutoEnabledClaGroupID) { + msg := "AutoEnabledClaGroupID can't be empty when AutoEnabled" + err := fmt.Errorf(msg) + log.WithFields(f).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + result, err := service.AddGitlabOrganization(ctx, params.ProjectSFID, params.Body) + if err != nil { + msg := fmt.Sprintf("unable to add github organization, error: %+v", err) + log.WithFields(f).WithError(err).Warn(msg) + return gitlab_organizations.NewAddProjectGitlabOrganizationBadRequest().WithPayload( + utils.ErrorResponseBadRequestWithError(reqID, msg, err)) + } + + // Log the event + eventService.LogEventWithContext(ctx, &events.LogEventArgs{ + LfUsername: authUser.UserName, + EventType: events.GitlabOrganizationAdded, + ProjectSFID: params.ProjectSFID, + EventData: &events.GitlabOrganizationAddedEventData{ + GitlabOrganizationName: *params.Body.OrganizationName, + }, + }) + + return gitlab_organizations.NewAddProjectGitlabOrganizationOK().WithPayload(result) + }) + + api.GitlabActivityGitlabOauthCallbackHandler = gitlab_activity.GitlabOauthCallbackHandlerFunc(func(params gitlab_activity.GitlabOauthCallbackParams) middleware.Responder { + f := logrus.Fields{ + "functionName": "gitlab_organization.handlers.GitlabActivityGitlabOauthCallbackHandler", + "code": params.Code, + "state": params.State, + } + + requestID, _ := uuid.NewV4() + reqID := requestID.String() + if params.Code == "" { + msg := "missing code parameter" + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabOauthCallbackBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + if params.State == "" { + msg := "missing state parameter" + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabOauthCallbackBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + codeParts := strings.Split(params.State, ":") + if len(codeParts) != 2 { + msg := fmt.Sprintf("invalid state variable passed : %s", params.State) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabOauthCallbackBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + gitlabOrganizationID := codeParts[0] + stateVar := codeParts[1] + + ctx := context.Background() + _, err := service.GetGitlabOrganizationByState(ctx, gitlabOrganizationID, stateVar) + if err != nil { + msg := fmt.Sprintf("fetching gitlab model failed : %s : %v", gitlabOrganizationID, err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabOauthCallbackBadRequest().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + // now fetch the oauth credentials and store to db + oauthResp, err := gitlab.FetchOauthCredentials(params.Code) + if err != nil { + msg := fmt.Sprintf("fetching gitlab credentials failed : %s : %v", gitlabOrganizationID, err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabOauthCallbackInternalServerError().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + log.Infof("oauth resp is like : %+v", oauthResp) + + err = service.UpdateGitlabOrganizationAuth(ctx, gitlabOrganizationID, oauthResp) + if err != nil { + msg := fmt.Sprintf("updating gitlab credentials failed : %s : %v", gitlabOrganizationID, err) + log.WithFields(f).Errorf(msg) + return gitlab_activity.NewGitlabOauthCallbackInternalServerError().WithPayload( + utils.ErrorResponseBadRequest(reqID, msg)) + } + + return gitlab_activity.NewGitlabOauthCallbackOK().WithPayload(&models.SuccessResponse{ + Code: "200", + Message: "oauth credentials stored successfully", + XRequestID: reqID, + }) + }) +} diff --git a/cla-backend-go/v2/gitlab_organizations/models.go b/cla-backend-go/v2/gitlab_organizations/models.go new file mode 100644 index 000000000..2094a002b --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/models.go @@ -0,0 +1,50 @@ +package gitlab_organizations + +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +import ( + models2 "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" +) + +// GitlabOrganization is data model for gitlab organizations +type GitlabOrganization struct { + OrganizationID string `json:"organization_id"` + DateCreated string `json:"date_created,omitempty"` + DateModified string `json:"date_modified,omitempty"` + OrganizationName string `json:"organization_name,omitempty"` + OrganizationNameLower string `json:"organization_name_lower,omitempty"` + OrganizationSFID string `json:"organization_sfid,omitempty"` + ProjectSFID string `json:"project_sfid"` + Enabled bool `json:"enabled"` + AutoEnabled bool `json:"auto_enabled"` + BranchProtectionEnabled bool `json:"branch_protection_enabled"` + AutoEnabledClaGroupID string `json:"auto_enabled_cla_group_id,omitempty"` + AuthInfo string `json:"auth_info"` + AuthState string `json:"auth_state"` + Version string `json:"version,omitempty"` +} + +// ToModel converts to models.GitlabOrganization +func ToModel(in *GitlabOrganization) *models2.GitlabOrganization { + return &models2.GitlabOrganization{ + OrganizationID: in.OrganizationID, + DateCreated: in.DateCreated, + DateModified: in.DateModified, + OrganizationName: in.OrganizationName, + OrganizationSfid: in.OrganizationSFID, + Version: in.Version, + Enabled: in.Enabled, + AutoEnabled: in.AutoEnabled, + AutoEnabledClaGroupID: in.AutoEnabledClaGroupID, + ProjectSFID: in.ProjectSFID, + } +} + +func toModels(input []*GitlabOrganization) []*models2.GitlabOrganization { + out := make([]*models2.GitlabOrganization, 0) + for _, in := range input { + out = append(out, ToModel(in)) + } + return out +} diff --git a/cla-backend-go/v2/gitlab_organizations/repository.go b/cla-backend-go/v2/gitlab_organizations/repository.go new file mode 100644 index 000000000..667726689 --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/repository.go @@ -0,0 +1,352 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +import ( + "context" + "errors" + "fmt" + "github.com/gofrs/uuid" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" + models2 "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" +) + +// indexes +const ( + GitlabOrgSFIDIndex = "gitlab-org-sfid-index" + GitlabOrgLowerNameIndex = "gitlab-organization-name-lower-search-index" + GitlabProjectSFIDOrganizationNameIndex = "gitlab-project-sfid-organization-name-index" +) + +// RepositoryInterface is interface for gitlab org data model +type RepositoryInterface interface { + AddGitlabOrganization(ctx context.Context, parentProjectSFID string, projectSFID string, input *models2.CreateGitlabOrganization) (*models2.GitlabOrganization, error) + GetGitlabOrganizations(ctx context.Context, projectSFID string) (*models2.GitlabOrganizations, error) + GetGitlabOrganization(ctx context.Context, gitlabOrganizationID string) (*GitlabOrganization, error) + UpdateGitlabOrganizationAuth(ctx context.Context, gitlabOrganizationID, authInfo string) error +} + +// Repository object/struct +type Repository struct { + stage string + dynamoDBClient *dynamodb.DynamoDB + gitlabOrgTableName string +} + +// NewRepository creates a new instance of the gitlabOrganizations repository +func NewRepository(awsSession *session.Session, stage string) RepositoryInterface { + return Repository{ + stage: stage, + dynamoDBClient: dynamodb.New(awsSession), + gitlabOrgTableName: fmt.Sprintf("cla-%s-gitlab-orgs", stage), + } +} + +func (repo Repository) AddGitlabOrganization(ctx context.Context, parentProjectSFID string, projectSFID string, input *models2.CreateGitlabOrganization) (*models2.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.AddGitlabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "parentProjectSFID": parentProjectSFID, + "projectSFID": projectSFID, + "organizationName": utils.StringValue(input.OrganizationName), + "autoEnabled": utils.BoolValue(input.AutoEnabled), + "branchProtectionEnabled": utils.BoolValue(input.BranchProtectionEnabled), + } + + // First, let's check to see if we have an existing gitlab organization with the same name + existingRecord, getErr := repo.GetGitlabOrganizationByName(ctx, utils.StringValue(input.OrganizationName)) + if getErr != nil { + log.WithFields(f).WithError(getErr).Debug("unable to locate existing github organization by name") + } + + if existingRecord != nil && len(existingRecord.List) > 0 { + log.WithFields(f).Debugf("Existing github organization exists in our database, count: %d", len(existingRecord.List)) + if len(existingRecord.List) > 1 { + log.WithFields(f).Warning("more than one github organization with the same name in the database") + } + if parentProjectSFID == existingRecord.List[0].OrganizationSfid { + log.WithFields(f).Debug("Existing github organization with same parent SFID - should be able to update it") + } else { + log.WithFields(f).Debug("Existing github organization with different parent SFID - won't be able to update it - will return conflict") + } + return nil, fmt.Errorf("record already exists") + } + + // No existing records - create one + _, currentTime := utils.CurrentTime() + organizationID, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warnf("Unable to generate a UUID for gitlab org, error: %v", err) + return nil, err + } + + authStateNonce, err := uuid.NewV4() + if err != nil { + log.WithFields(f).WithError(err).Warnf("Unable to generate a auth nonce UUID for gitlab org, error: %v", err) + return nil, err + } + + enabled := false + gitlabOrg := &GitlabOrganization{ + OrganizationID: organizationID.String(), + DateCreated: currentTime, + DateModified: currentTime, + OrganizationName: *input.OrganizationName, + OrganizationNameLower: strings.ToLower(*input.OrganizationName), + OrganizationSFID: parentProjectSFID, + ProjectSFID: projectSFID, + Enabled: aws.BoolValue(&enabled), + AutoEnabled: aws.BoolValue(input.AutoEnabled), + AutoEnabledClaGroupID: input.AutoEnabledClaGroupID, + BranchProtectionEnabled: aws.BoolValue(input.BranchProtectionEnabled), + AuthState: authStateNonce.String(), + Version: "v1", + } + + log.WithFields(f).Debug("Encoding github organization record for adding to the database...") + av, err := dynamodbattribute.MarshalMap(gitlabOrg) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to marshall request for query") + return nil, err + } + + log.WithFields(f).Debug("Adding gitlab organization record to the database...") + _, err = repo.dynamoDBClient.PutItem(&dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(repo.gitlabOrgTableName), + ConditionExpression: aws.String("attribute_not_exists(organization_name)"), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + switch aerr.Code() { + case dynamodb.ErrCodeConditionalCheckFailedException: + log.WithFields(f).WithError(err).Warn("gitlab organization already exists") + return nil, fmt.Errorf("gitlab organization already exists") + } + } + log.WithFields(f).WithError(err).Warn("cannot put gitlab organization in dynamodb") + return nil, err + } + + return ToModel(gitlabOrg), nil +} + +// GetGitlabOrganizations get github organizations based on the project SFID +func (repo Repository) GetGitlabOrganizations(ctx context.Context, projectSFID string) (*models2.GitlabOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.repository.GetGitHubOrganizations", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + } + + condition := expression.Key("organization_sfid").Equal(expression.Value(projectSFID)) + builder := expression.NewBuilder().WithKeyCondition(condition) + + //filter := expression.Name("enabled").Equal(expression.Value(true)) + //builder = builder.WithFilter(filter) + + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + log.WithFields(f).Warnf("problem building query expression, error: %+v", err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(GitlabOrgSFIDIndex), + } + + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).Warnf("error retrieving github_organizations using project_sfid = %s. error = %s", projectSFID, err.Error()) + return nil, err + } + + if len(results.Items) == 0 { + log.WithFields(f).Debug("no results from query") + return &models2.GitlabOrganizations{ + List: []*models2.GitlabOrganization{}, + }, nil + } + + var resultOutput []*GitlabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + return nil, err + } + + log.WithFields(f).Debug("building response model...") + gitlabOrgList := buildGitlabOrganizationListModels(ctx, resultOutput) + return &models2.GitlabOrganizations{List: gitlabOrgList}, nil +} + +// GetGitlabOrganizationByName get github organization by name +func (repo Repository) GetGitlabOrganizationByName(ctx context.Context, githubOrganizationName string) (*models2.GitlabOrganizations, error) { + f := logrus.Fields{ + "functionName": "v1.github_organizations.repository.GetGitHubOrganizationByName", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "githubOrganizationName": githubOrganizationName, + } + + condition := expression.Key("organization_name_lower").Equal(expression.Value(strings.ToLower(githubOrganizationName))) + builder := expression.NewBuilder().WithKeyCondition(condition) + // Use the nice builder to create the expression + expr, err := builder.Build() + if err != nil { + return nil, err + } + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + FilterExpression: expr.Filter(), + TableName: aws.String(repo.gitlabOrgTableName), + IndexName: aws.String(GitlabOrgLowerNameIndex), + } + + log.WithFields(f).Debugf("querying for github organization by name using organization_name_lower=%s...", strings.ToLower(githubOrganizationName)) + results, err := repo.dynamoDBClient.Query(queryInput) + if err != nil { + log.WithFields(f).WithError(err).Warnf("error retrieving github_organizations using githubOrganizationName = %s", githubOrganizationName) + return nil, err + } + if len(results.Items) == 0 { + log.WithFields(f).Debug("Unable to find github organization by name - no results") + return &models2.GitlabOrganizations{ + List: []*models2.GitlabOrganization{}, + }, nil + } + var resultOutput []*GitlabOrganization + err = dynamodbattribute.UnmarshalListOfMaps(results.Items, &resultOutput) + if err != nil { + log.WithFields(f).Warnf("problem decoding database results, error: %+v", err) + return nil, err + } + + ghOrgList := buildGitlabOrganizationListModels(ctx, resultOutput) + return &models2.GitlabOrganizations{List: ghOrgList}, nil +} + +// GetGitlabOrganization by organization name +func (repo Repository) GetGitlabOrganization(ctx context.Context, gitlabOrganizationID string) (*GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "gitlab_organizations.repository.GetGitlabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabOrganizationID": gitlabOrganizationID, + } + + log.WithFields(f).Debug("Querying for github organization by name...") + result, err := repo.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "organization_id": { + S: aws.String(gitlabOrganizationID), + }, + }, + TableName: aws.String(repo.gitlabOrgTableName), + }) + if err != nil { + return nil, err + } + if len(result.Item) == 0 { + log.WithFields(f).Debug("Unable to find github organization by name - no results") + return nil, nil + } + + var org GitlabOrganization + err = dynamodbattribute.UnmarshalMap(result.Item, &org) + if err != nil { + log.WithFields(f).Warnf("error unmarshalling organization table data, error: %v", err) + return nil, err + } + return &org, nil +} + +// UpdateGitlabOrganizationAuth updates the specified Gitlab organization oauth info +func (repo Repository) UpdateGitlabOrganizationAuth(ctx context.Context, gitlabOrganizationID, authInfo string) error { + f := logrus.Fields{ + "functionName": "gitlab_organizations.repository.UpdateGitlabOrganizationAuth", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabOrganizationID": gitlabOrganizationID, + "tableName": repo.gitlabOrgTableName, + } + + _, currentTime := utils.CurrentTime() + gitlabOrg, lookupErr := repo.GetGitlabOrganization(ctx, gitlabOrganizationID) + if lookupErr != nil { + log.WithFields(f).Warnf("error looking up Gitlab organization by id, error: %+v", lookupErr) + return lookupErr + } + if gitlabOrg == nil { + lookupErr := errors.New("unable to lookup Gitlab organization by id") + log.WithFields(f).Warnf("error looking up Gitlab organization, error: %+v", lookupErr) + return lookupErr + } + + expressionAttributeNames := map[string]*string{ + "#A": aws.String("auth_info"), + "#M": aws.String("date_modified"), + } + expressionAttributeValues := map[string]*dynamodb.AttributeValue{ + ":a": { + S: aws.String(authInfo), + }, + ":m": { + S: aws.String(currentTime), + }, + } + updateExpression := "SET #A = :a, #M = :m" + + input := &dynamodb.UpdateItemInput{ + Key: map[string]*dynamodb.AttributeValue{ + "organization_id": { + S: aws.String(gitlabOrg.OrganizationID), + }, + }, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + UpdateExpression: &updateExpression, + TableName: aws.String(repo.gitlabOrgTableName), + } + + log.WithFields(f).Debug("updating gitlab organization record...") + _, updateErr := repo.dynamoDBClient.UpdateItem(input) + if updateErr != nil { + log.WithFields(f).Warnf("unable to update Gitlab organization record, error: %+v", updateErr) + return updateErr + } + + return nil +} + +func buildGitlabOrganizationListModels(ctx context.Context, gitlabOrganizations []*GitlabOrganization) []*models2.GitlabOrganization { + f := logrus.Fields{ + "functionName": "buildGitlabOrganizationListModels", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + } + + log.WithFields(f).Debugf("fetching gitlab info for the list") + // Convert the database model to a response model + return toModels(gitlabOrganizations) + + // TODO: Fetch the gitlab information +} diff --git a/cla-backend-go/v2/gitlab_organizations/service.go b/cla-backend-go/v2/gitlab_organizations/service.go new file mode 100644 index 000000000..7ef3cf11a --- /dev/null +++ b/cla-backend-go/v2/gitlab_organizations/service.go @@ -0,0 +1,266 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package gitlab_organizations + +import ( + "context" + "fmt" + "github.com/communitybridge/easycla/cla-backend-go/config" + "github.com/go-openapi/strfmt" + "net/url" + "sort" + "strings" + + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + "github.com/communitybridge/easycla/cla-backend-go/gen/v2/models" + "github.com/communitybridge/easycla/cla-backend-go/gitlab" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" + "github.com/communitybridge/easycla/cla-backend-go/utils" + v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + "github.com/sirupsen/logrus" +) + +// Service contains functions of GitlabOrganizations service +type Service interface { + GetGitlabOrganizations(ctx context.Context, projectSFID string) (*models.ProjectGitlabOrganizations, error) + AddGitlabOrganization(ctx context.Context, projectSFID string, input *models.CreateGitlabOrganization) (*models.GitlabOrganization, error) + GetGitlabOrganization(ctx context.Context, gitlabOrganizationID string) (*models.GitlabOrganization, error) + GetGitlabOrganizationByState(ctx context.Context, gitlabOrganizationID, authState string) (*models.GitlabOrganization, error) + UpdateGitlabOrganizationAuth(ctx context.Context, gitlabOrganizationID string, oauthResp *gitlab.OauthSuccessResponse) error +} + +type service struct { + repo RepositoryInterface + projectsCLAGroupService projects_cla_groups.Repository +} + +// NewService creates a new githubOrganizations service +func NewService(repo RepositoryInterface, projectsCLAGroupService projects_cla_groups.Repository) Service { + return service{ + repo: repo, + projectsCLAGroupService: projectsCLAGroupService, + } +} + +func (s service) GetGitlabOrganization(ctx context.Context, gitlabOrganizationID string) (*models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitlabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabOrganizationID": gitlabOrganizationID, + } + + log.WithFields(f).Debugf("fetching gitlab organization for gitlab org id : %s", gitlabOrganizationID) + dbModel, err := s.repo.GetGitlabOrganization(ctx, gitlabOrganizationID) + if err != nil { + return nil, err + } + + return ToModel(dbModel), nil +} + +func (s service) UpdateGitlabOrganizationAuth(ctx context.Context, gitlabOrganizationID string, oauthResp *gitlab.OauthSuccessResponse) error { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.UpdateGitlabOrganizationAuth", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabOrganizationID": gitlabOrganizationID, + } + + log.WithFields(f).Debugf("updating gitlab org auth") + authInfoEncrypted, err := gitlab.EncryptAuthInfo(oauthResp) + if err != nil { + return fmt.Errorf("encrypt failed : %v", err) + } + + return s.repo.UpdateGitlabOrganizationAuth(ctx, gitlabOrganizationID, authInfoEncrypted) + +} + +func (s service) GetGitlabOrganizationByState(ctx context.Context, gitlabOrganizationID, authState string) (*models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitlabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "gitlabOrganizationID": gitlabOrganizationID, + "authState": authState, + } + + log.WithFields(f).Debugf("fetching gitlab organization for gitlab org id : %s", gitlabOrganizationID) + dbModel, err := s.repo.GetGitlabOrganization(ctx, gitlabOrganizationID) + if err != nil { + return nil, err + } + + if dbModel.AuthState != authState { + return nil, fmt.Errorf("auth state doesn't match") + } + + return ToModel(dbModel), nil +} + +func (s service) AddGitlabOrganization(ctx context.Context, projectSFID string, input *models.CreateGitlabOrganization) (*models.GitlabOrganization, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.AddGitlabOrganization", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + "autoEnabled": utils.BoolValue(input.AutoEnabled), + "branchProtectionEnabled": utils.BoolValue(input.BranchProtectionEnabled), + "organizationName": utils.StringValue(input.OrganizationName), + } + + log.WithFields(f).Debug("looking up project in project service...") + psc := v2ProjectService.GetClient() + project, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading project details from the project service") + return nil, err + } + + var parentProjectSFID string + if utils.StringValue(project.Parent) == "" || (project.Foundation != nil && + (project.Foundation.Name == utils.TheLinuxFoundation || project.Foundation.Name == utils.LFProjectsLLC)) { + parentProjectSFID = projectSFID + } else { + parentProjectSFID = utils.StringValue(project.Parent) + } + f["parentProjectSFID"] = parentProjectSFID + log.WithFields(f).Debug("located parentProjectID...") + + log.WithFields(f).Debug("adding github organization...") + resp, err := s.repo.AddGitlabOrganization(ctx, parentProjectSFID, projectSFID, input) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem adding github organization for project") + return nil, err + } + + return resp, nil +} + +func (s service) GetGitlabOrganizations(ctx context.Context, projectSFID string) (*models.ProjectGitlabOrganizations, error) { + f := logrus.Fields{ + "functionName": "v2.gitlab_organizations.service.GetGitlabOrganizations", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "projectSFID": projectSFID, + } + + // Load the GitHub Organization and Repository details - result will be missing CLA Group info and ProjectSFID details + log.WithFields(f).Debugf("loading Gitlab organizations for projectSFID: %s", projectSFID) + orgs, err := s.repo.GetGitlabOrganizations(ctx, projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading gitlab organizations from the project service") + return nil, err + } + + psc := v2ProjectService.GetClient() + log.WithFields(f).Debug("loading project details from the project service...") + projectServiceRecord, err := psc.GetProject(projectSFID) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem loading project details from the project service") + return nil, err + } + + var parentProjectSFID string + if utils.IsProjectHasRootParent(projectServiceRecord) { + parentProjectSFID = projectSFID + } else { + parentProjectSFID = utils.StringValue(projectServiceRecord.Parent) + } + f["parentProjectSFID"] = parentProjectSFID + log.WithFields(f).Debug("located parentProjectID...") + + // Our response model + out := &models.ProjectGitlabOrganizations{ + List: make([]*models.ProjectGitlabOrganization, 0), + } + + // Next, we need to load a bunch of additional data for the response including the github status (if it's still connected/live, not renamed/moved), the CLA Group details, etc. + + // A temp data model for holding the intermediate results + type gitlabRepoInfo struct { + orgName string + repoInfo *v1Models.GithubRepositoryInfo + } + + orgmap := make(map[string]*models.ProjectGitlabOrganization) + for _, org := range orgs.List { + autoEnabledCLAGroupName := "" + if org.AutoEnabledClaGroupID != "" { + log.WithFields(f).Debugf("Loading CLA Group by ID: %s to obtain the name for GitHub auth enabled CLA Group response", org.AutoEnabledClaGroupID) + claGroupMode, claGroupLookupErr := s.projectsCLAGroupService.GetCLAGroup(ctx, org.AutoEnabledClaGroupID) + if claGroupLookupErr != nil { + log.WithFields(f).WithError(claGroupLookupErr).Warnf("Unable to lookup CLA Group by ID: %s", org.AutoEnabledClaGroupID) + } + if claGroupMode != nil { + autoEnabledCLAGroupName = claGroupMode.ProjectName + } + } + + orgDetailed, err := s.repo.GetGitlabOrganization(ctx, org.OrganizationID) + if err != nil { + log.WithFields(f).Errorf("fetching gitlab org failed : %s : %v", org.OrganizationID, err) + continue + } + + installationURL := buildInstallationURL(org.OrganizationID, orgDetailed.AuthState) + rorg := &models.ProjectGitlabOrganization{ + AutoEnabled: org.AutoEnabled, + AutoEnableCLAGroupID: org.AutoEnabledClaGroupID, + AutoEnabledCLAGroupName: autoEnabledCLAGroupName, + GitlabOrganizationName: org.OrganizationName, + Repositories: make([]*models.ProjectGithubRepository, 0), + InstallationURL: installationURL, + } + + if orgDetailed.AuthInfo == "" { + rorg.ConnectionStatus = utils.NoConnection + } else { + glClient, err := gitlab.NewGitlabOauthClient(orgDetailed.AuthInfo) + if err != nil { + log.WithFields(f).Errorf("initializing gitlab client for gitlab org : %s failed : %v", org.OrganizationID, err) + rorg.ConnectionStatus = utils.ConnectionFailure + } else { + user, _, err := glClient.Users.CurrentUser() + if err != nil { + log.WithFields(f).Errorf("using gitlab client for gitlab org : %s failed : %v", org.OrganizationID, err) + rorg.ConnectionStatus = utils.ConnectionFailure + } else { + log.WithFields(f).Debugf("connected to user : %s for gitlab org : %s", user.Name, org.OrganizationID) + rorg.ConnectionStatus = utils.Connected + } + } + } + + orgmap[org.OrganizationName] = rorg + out.List = append(out.List, rorg) + } + + // Sort everything nicely + sort.Slice(out.List, func(i, j int) bool { + return strings.ToLower(out.List[i].GitlabOrganizationName) < strings.ToLower(out.List[j].GitlabOrganizationName) + }) + for _, orgList := range out.List { + sort.Slice(orgList.Repositories, func(i, j int) bool { + return strings.ToLower(orgList.Repositories[i].RepositoryName) < strings.ToLower(orgList.Repositories[j].RepositoryName) + }) + } + + return out, nil +} + +func buildInstallationURL(gitlabOrgID string, authStateNonce string) *strfmt.URI { + base := "https://gitlab.com/oauth/authorize" + c := config.GetConfig() + state := fmt.Sprintf("%s:%s", gitlabOrgID, authStateNonce) + + params := url.Values{} + params.Add("client_id", c.Gitlab.AppID) + params.Add("redirect_uri", c.Gitlab.RedirectURI) + //params.Add("redirect_uri", "http://localhost:8080/v4/gitlab/oauth/callback") + params.Add("response_type", "code") + params.Add("state", state) + params.Add("scope", "read_user read_api read_repository write_repository api") + + installationURL := strfmt.URI(base + "?" + params.Encode()) + return &installationURL + +}