From 03a695db7d0ae1c38e8add40bb251adb329ff8ad Mon Sep 17 00:00:00 2001 From: Harold Wanyama Date: Tue, 17 Oct 2023 10:33:51 +0300 Subject: [PATCH] - Added initiate sign request (void and create envelope) Signed-off-by: Harold Wanyama --- cla-backend-go/github/github_repository.go | 35 ++ cla-backend-go/project/common/helpers.go | 27 +- cla-backend-go/project/service/service.go | 8 +- cla-backend-go/serverless.yml | 1 + cla-backend-go/signatures/converters.go | 1 + cla-backend-go/signatures/dbmodels.go | 1 + cla-backend-go/swagger/cla.v1.yaml | 3 + cla-backend-go/swagger/cla.v2.yaml | 3 + .../swagger/common/cla-group-document.yaml | 4 + .../swagger/common/document-tab.yaml | 41 ++ cla-backend-go/swagger/common/signature.yaml | 3 + cla-backend-go/v2/sign/docusign.go | 68 +++ cla-backend-go/v2/sign/handlers.go | 17 +- cla-backend-go/v2/sign/models.go | 34 ++ cla-backend-go/v2/sign/service.go | 388 ++++++++++++++++-- 15 files changed, 591 insertions(+), 43 deletions(-) create mode 100644 cla-backend-go/swagger/common/document-tab.yaml diff --git a/cla-backend-go/github/github_repository.go b/cla-backend-go/github/github_repository.go index b88f12f96..651798a6e 100644 --- a/cla-backend-go/github/github_repository.go +++ b/cla-backend-go/github/github_repository.go @@ -643,3 +643,38 @@ func CreateStatus(ctx context.Context, client *github.Client, owner, repo, sha s return c, resp, nil } + +func GetReturnURL(ctx context.Context, installationID, repositoryID int64, pullRequestID int) (string, error) { + f := logrus.Fields{ + "functionName": "github.github_repository.GetReturnURL", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "installationID": installationID, + "repositoryID": repositoryID, + "pullRequestID": pullRequestID, + } + + client, err := NewGithubAppClient(installationID) + + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to create Github client") + return "", err + } + + log.WithFields(f).Debugf("getting github repository by id: %d", repositoryID) + repo, _, err := client.Repositories.GetByID(ctx, repositoryID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get repository by ID: %d", repositoryID) + return "", err + } + + log.WithFields(f).Debugf("getting pull request by id: %d", pullRequestID) + pullRequest, _, err := client.PullRequests.Get(ctx, *repo.Owner.Login, *repo.Name, pullRequestID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get pull request by ID: %d", pullRequestID) + return "", err + } + + log.WithFields(f).Debugf("returning pull request html url: %s", *pullRequest.HTMLURL) + + return *pullRequest.HTMLURL, nil +} diff --git a/cla-backend-go/project/common/helpers.go b/cla-backend-go/project/common/helpers.go index 4a6e8a5a5..111b393c8 100644 --- a/cla-backend-go/project/common/helpers.go +++ b/cla-backend-go/project/common/helpers.go @@ -102,13 +102,13 @@ func GetCurrentDocument(ctx context.Context, docs []models.ClaGroupDocument) (mo continue } - // No previous, use the first... - if currentDoc == (models.ClaGroupDocument{}) { - currentDoc = doc - currentDocVersion = version - currentDocDateTime = dateTime - continue - } + // // No previous, use the first... + // if currentDoc == (models.ClaGroupDocument{}) { + // currentDoc = doc + // currentDocVersion = version + // currentDocDateTime = dateTime + // continue + // } // Newer version... if version > currentDocVersion { @@ -127,3 +127,16 @@ func GetCurrentDocument(ctx context.Context, docs []models.ClaGroupDocument) (mo return currentDoc, nil } + +func AreClaGroupDocumentsEqual(doc1, doc2 models.ClaGroupDocument) bool { + return doc1.DocumentName == doc2.DocumentName && + doc1.DocumentAuthorName == doc2.DocumentAuthorName && + doc1.DocumentContentType == doc2.DocumentContentType && + doc1.DocumentFileID == doc2.DocumentFileID && + doc1.DocumentLegalEntityName == doc2.DocumentLegalEntityName && + doc1.DocumentPreamble == doc2.DocumentPreamble && + doc1.DocumentS3URL == doc2.DocumentS3URL && + doc1.DocumentMajorVersion == doc2.DocumentMajorVersion && + doc1.DocumentMinorVersion == doc2.DocumentMinorVersion && + doc1.DocumentCreationDate == doc2.DocumentCreationDate +} diff --git a/cla-backend-go/project/service/service.go b/cla-backend-go/project/service/service.go index df4fc8c07..f6a303e35 100644 --- a/cla-backend-go/project/service/service.go +++ b/cla-backend-go/project/service/service.go @@ -212,8 +212,8 @@ func (s ProjectService) GetCLAGroupCurrentICLATemplateURLByID(ctx context.Contex } } - if currentDoc == (models.ClaGroupDocument{}) { - log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group") + if common.AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group - document is empty") return "", &utils.CLAGroupICLANotConfigured{ CLAGroupID: claGroupID, CLAGroupName: claGroupModel.ProjectName, @@ -288,8 +288,8 @@ func (s ProjectService) GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Contex } } - if currentDoc == (models.ClaGroupDocument{}) { - log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group") + if common.AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { + log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group - document is empty") return "", &utils.CLAGroupCCLANotConfigured{ CLAGroupID: claGroupID, CLAGroupName: claGroupModel.ProjectName, diff --git a/cla-backend-go/serverless.yml b/cla-backend-go/serverless.yml index 764d8daf5..f06aaf8f6 100644 --- a/cla-backend-go/serverless.yml +++ b/cla-backend-go/serverless.yml @@ -239,6 +239,7 @@ provider: DOCUSIGN_INTEGRATOR_KEY: ${file(./env.json):docusign-integrator-key, ssm:/cla-docusign-integrator-key-${opt:stage}} DOCUSIGN_AUTH_SERVER: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-auth-server-${opt:stage}} DOCUSIGN_USER_ID: ${file(./env.json):docusign-auth-server, ssm:/cla-docusign-user-id-${opt:stage}} + DOCUSIGN_ACCOUNT_ID: ${file(./env.json):docusign-account-id, ssm:/cla-docusign-account-id-${opt:stage}} CLA_API_BASE: ${file(./env.json):cla-api-base, ssm:/cla-api-base-${opt:stage}} CLA_CONTRIBUTOR_BASE: ${file(./env.json):cla-contributor-base, ssm:/cla-contributor-base-${opt:stage}} CLA_CONTRIBUTOR_V2_BASE: ${file(./env.json):cla-contributor-v2-base, ssm:/cla-contributor-v2-base-${opt:stage}} diff --git a/cla-backend-go/signatures/converters.go b/cla-backend-go/signatures/converters.go index f350687e5..ef1b10b5c 100644 --- a/cla-backend-go/signatures/converters.go +++ b/cla-backend-go/signatures/converters.go @@ -102,6 +102,7 @@ func (repo repository) buildProjectSignatureModels(ctx context.Context, results SignatureCallbackURL: dbSignature.SignatureCallbackURL, SignatureReturnURL: dbSignature.SignatureReturnURL, SignatureReturnURLType: dbSignature.SignatureReturnURLType, + SignatureEnvelopeID: dbSignature.SignatureEnvelopeID, } sigs = append(sigs, sig) diff --git a/cla-backend-go/signatures/dbmodels.go b/cla-backend-go/signatures/dbmodels.go index a36157998..a5e46be5a 100644 --- a/cla-backend-go/signatures/dbmodels.go +++ b/cla-backend-go/signatures/dbmodels.go @@ -22,6 +22,7 @@ type ItemSignature struct { SignatureProjectID string `json:"signature_project_id"` SignatureReferenceType string `json:"signature_reference_type"` SignatureType string `json:"signature_type"` + SignatureEnvelopeID string `json:"signature_envelope_id"` SignatureUserCompanyID string `json:"signature_user_ccla_company_id"` EmailApprovalList []string `json:"email_whitelist"` EmailDomainApprovalList []string `json:"domain_whitelist"` diff --git a/cla-backend-go/swagger/cla.v1.yaml b/cla-backend-go/swagger/cla.v1.yaml index beaa11886..93579b54b 100644 --- a/cla-backend-go/swagger/cla.v1.yaml +++ b/cla-backend-go/swagger/cla.v1.yaml @@ -2854,6 +2854,9 @@ definitions: cla-group-document: $ref: './common/cla-group-document.yaml' + + document-tab: + $ref: './common/document-tab.yaml' create-cla-group-template: $ref: './common/create-cla-group-template.yaml' diff --git a/cla-backend-go/swagger/cla.v2.yaml b/cla-backend-go/swagger/cla.v2.yaml index bbc271aed..9832825d7 100644 --- a/cla-backend-go/swagger/cla.v2.yaml +++ b/cla-backend-go/swagger/cla.v2.yaml @@ -5680,6 +5680,9 @@ definitions: cla-group-summary: $ref: './common/cla-group-summary.yaml' + + document-tab: + $ref: './common/document-tab.yaml' cla-group-project: type: object diff --git a/cla-backend-go/swagger/common/cla-group-document.yaml b/cla-backend-go/swagger/common/cla-group-document.yaml index 8f6d9e8bb..f9d7f5bdb 100644 --- a/cla-backend-go/swagger/common/cla-group-document.yaml +++ b/cla-backend-go/swagger/common/cla-group-document.yaml @@ -46,3 +46,7 @@ properties: description: the document creation date example: '2019-08-01T06:55:09Z' type: string + documentTabs: + type: array + items: + $ref: '#/definitions/document-tab' diff --git a/cla-backend-go/swagger/common/document-tab.yaml b/cla-backend-go/swagger/common/document-tab.yaml new file mode 100644 index 000000000..7ceabc47d --- /dev/null +++ b/cla-backend-go/swagger/common/document-tab.yaml @@ -0,0 +1,41 @@ +# Copyright The Linux Foundation and each contributor to CommunityBridge. +# SPDX-License-Identifier: MIT + +type: object +x-nullable: false +title: Docusign Document Tab +description: Docusign Document Tab +properties: + document_tab_type: + type: string + document_tab_id: + type: string + document_tab_name: + type: string + document_tab_page: + type: integer + document_tab_position_x: + type: integer + document_tab_position_y: + type: integer + document_tab_width: + type: integer + document_tab_height: + type: integer + document_tab_is_locked: + type: boolean + document_tab_is_required: + type: boolean + document_tab_anchor_string: + type: string + document_tab_anchor_ignore_if_not_present: + type: boolean + document_tab_anchor_x_offset: + type: integer + document_tab_anchor_y_offset: + type: integer + + + + + diff --git a/cla-backend-go/swagger/common/signature.yaml b/cla-backend-go/swagger/common/signature.yaml index b3fc5eeaf..795eeb1ef 100644 --- a/cla-backend-go/swagger/common/signature.yaml +++ b/cla-backend-go/swagger/common/signature.yaml @@ -134,6 +134,9 @@ properties: signatureReturnURLType: type: string description: the signature return URL type + signatureEnvelopeId: + type: string + description: the signature envelope ID emailApprovalList: type: array description: a list of zero or more email addresses in the approval list diff --git a/cla-backend-go/v2/sign/docusign.go b/cla-backend-go/v2/sign/docusign.go index 9d4f2c5c2..06cd83ad7 100644 --- a/cla-backend-go/v2/sign/docusign.go +++ b/cla-backend-go/v2/sign/docusign.go @@ -89,3 +89,71 @@ func (s *service) getAccessToken(ctx context.Context) (string, error) { return tokenResponse.AccessToken, nil } + +// Void envelope +func (s *service) VoidEnvelope(ctx context.Context, envelopeID, message string) error { + f := logrus.Fields{ + "functionName": "v2.VoidEnvelope", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "envelopeID": envelopeID, + "message": message, + } + + accessToken, err := s.getAccessToken(ctx) + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem getting the access token") + return err + } + + voidRequest := struct { + VoidReason string `json:"voidReason"` + }{ + VoidReason: message, + } + + voidRequestJSON, err := json.Marshal(voidRequest) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("problem marshalling the void request") + return err + } + + url := fmt.Sprintf("https://%s/restapi/v2.1/accounts/%s/envelopes/%s/void", utils.GetProperty("DOCUSIGN_ROOT_URL"), utils.GetProperty("DOCUSIGN_ACCOUNT_ID"), envelopeID) + + req, err := http.NewRequest("PUT", url, strings.NewReader(string(voidRequestJSON))) + + if err != nil { + return err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Add("Content-Type", "application/json") + + // Make the request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + return err + } + + defer func() { + if err = resp.Body.Close(); err != nil { + log.WithFields(f).WithError(err).Warnf("problem closing the response body") + } + }() + + _, err = io.ReadAll(resp.Body) + + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return errors.New("problem making the HTTP request") + } + + return nil + +} diff --git a/cla-backend-go/v2/sign/handlers.go b/cla-backend-go/v2/sign/handlers.go index 3a8647b64..5ee209d0c 100644 --- a/cla-backend-go/v2/sign/handlers.go +++ b/cla-backend-go/v2/sign/handlers.go @@ -112,8 +112,8 @@ func Configure(api *operations.EasyclaAPI, service Service) { if strings.ToLower(params.Input.ReturnURLType) == "github" || strings.ToLower(params.Input.ReturnURLType) == "gitlab" { if strings.ToLower(params.Input.ReturnURLType) == "github" { log.WithFields(f).Debug("fetching github emails") - emails, err := fetchGithubEmails(session, clientID) - if err != nil { + emails, fetchErr := fetchGithubEmails(session, clientID) + if fetchErr != nil { return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, err)) } @@ -124,7 +124,9 @@ func Configure(api *operations.EasyclaAPI, service Service) { } for _, email := range emails { if email["verified"].(bool) && email["primary"].(bool) { - preferredEmail = email["email"].(string) + if emailVal, ok := email["email"].(string); ok { + preferredEmail = emailVal + } break } } @@ -180,7 +182,14 @@ func getRequestSession(req *http.Request) map[string]interface{} { func fetchGithubEmails(session map[string]interface{}, clientID string) ([]map[string]interface{}, error) { var emails []map[string]interface{} - token := session["github_oauth2_token"].(string) + var token string + + if tokenVal, ok := session["token"].(string); ok { + token = tokenVal + } else { + return emails, nil + } + if token == "" { return emails, nil } diff --git a/cla-backend-go/v2/sign/models.go b/cla-backend-go/v2/sign/models.go index a7fc65635..69fb3040f 100644 --- a/cla-backend-go/v2/sign/models.go +++ b/cla-backend-go/v2/sign/models.go @@ -129,6 +129,17 @@ type DocuSignRecipient struct { Tabs DocuSignTab `json:"tabs"` // The tabs associated with the recipient. The tabs property enables you to programmatically position tabs on the document. For example, you can specify that the SIGN_HERE tab is placed at a given (x,y) location on the document. You can also specify the font, font color, font size, and other properties of the text in the tab. You can also specify the location and size of the tab. For example, you can specify that the tab is 50 pixels wide and 20 pixels high. You can also specify the page number on which the tab is located and whether the tab is located in a document, a template, or an inline template. For more information about tabs, see the Tabs section of the REST API documentation. } +// TextOptionalTab + +type TextOptionalTab struct { + Name string `json:"name"` + Value string `json:"value"` + Height int `json:"height"` + Width int `json:"width"` + Locked bool `json:"locked"` + Required bool `json:"required"` +} + // DocuSignTab is the data model for a tab from DocuSign type DocuSignTab struct { ApproveTabs []DocuSignTabDetails `json:"approveTabs,omitempty"` @@ -170,6 +181,8 @@ type DocuSignTab struct { TitleTabs []DocuSignTabDetails `json:"titleTabs,omitempty"` ViewTabs []DocuSignTabDetails `json:"viewTabs,omitempty"` ZipTabs []DocuSignTabDetails `json:"zipTabs,omitempty"` + TextOptionalTabs []DocuSignTabDetails `json:"textOptionalTabs,omitempty"` + SignHereOptionalTabs []DocuSignTabDetails `json:"signHereOptionalTabs,omitempty"` } // DocuSignTabDetails is the data model for a tab from DocuSign @@ -204,6 +217,7 @@ type DocuSignTabDetails struct { YPosition string `json:"yPosition,omitempty"` // x position ValidationType string `json:"validationType,omitempty"` // validation type, "string", "number", "date", "zipcode", "currency" Value string `json:"value,omitempty"` + CustomTabId string `json:"customTabId,omitempty"` } // DocuSignTemplateRole is the request body for a template role from DocuSign @@ -290,3 +304,23 @@ type IndividualMembershipDocuSignDBSummaryModel struct { Memo sql.NullString `db:"memo"` //DocuSignEnvelopeSignedDate time.Time `json:"docusign_envelope_signed_date"` } + + +package main + +import ( + "fmt" + "strings" +) + +type ClaSignatoryEmailParams struct { + ClaGroupName string + SignatoryName string + ClaManagerName string + ClaManagerEmail string + CompanyName string + ProjectVersion string + ProjectNames []string +} + + diff --git a/cla-backend-go/v2/sign/service.go b/cla-backend-go/v2/sign/service.go index c1046cb1c..566d0a051 100644 --- a/cla-backend-go/v2/sign/service.go +++ b/cla-backend-go/v2/sign/service.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" + "github.com/communitybridge/easycla/cla-backend-go/github" "github.com/communitybridge/easycla/cla-backend-go/github_organizations" "github.com/communitybridge/easycla/cla-backend-go/project/common" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" @@ -63,6 +64,8 @@ type ProjectRepo interface { // Service interface defines the sign service methods type Service interface { + VoidEnvelope(ctx context.Context, envelopeID, message string) error + RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) RequestIndividualSignature(ctx context.Context, input *models.IndividualSignatureInput, preferredEmail string) (*models.IndividualSignatureOutput, error) RequestIndividualSignatureGerrit(ctx context.Context, input *models.IndividualSignatureInput) (*models.IndividualSignatureOutput, error) @@ -389,14 +392,14 @@ func (s *service) RequestIndividualSignature(ctx context.Context, input *models. return nil, err } - if &latestDocument == nil { + if common.AreClaGroupDocumentsEqual(latestDocument, v1Models.ClaGroupDocument{}) { log.WithFields(f).WithError(err).Warnf("unable to lookup latest individual document for project: %s", *input.ProjectID) return nil, errors.New("unable to lookup latest individual document for project") } // creating individual default values log.WithFields(f).Debugf("creating individual default values...") - defaultValues, err := s.createDefaultIndiviualValues(user, preferredEmail) + defaultValues, err := s.createDefaultIndividualValues(user, preferredEmail) if err != nil { log.WithFields(f).WithError(err).Warnf("unable to create default values for user: %s", *input.UserID) @@ -439,14 +442,14 @@ func (s *service) RequestIndividualSignature(ctx context.Context, input *models. // Regenerate and set the signing URL - This will update the signature record log.WithFields(f).Debugf("regenerating signing URL for user: %s", *input.UserID) - err := s.populateSignURL(latestSignature, callBackURL, defaultValues, preferredEmail) + err = s.populateSignURL(ctx, latestSignature, callBackURL, "", "", false, "", "", defaultValues, preferredEmail) if err != nil { log.WithFields(f).WithError(err).Warnf("unable to populate sign url for user: %s", *input.UserID) return nil, err } return &models.IndividualSignatureOutput{ - SignURL: latestSignature.SignatureReferenceID, + SignURL: latestSignature.SignatureSignURL, SignatureID: latestSignature.SignatureID, UserID: latestSignature.SignatureReferenceID, ProjectID: *input.ProjectID, @@ -456,13 +459,22 @@ func (s *service) RequestIndividualSignature(ctx context.Context, input *models. // 5. Get signature return URL log.WithFields(f).Debugf("getting signature return url...") + var returnURL string if input.ReturnURL.String() == "" { - return &models.IndividualSignatureOutput{ - ProjectID: *input.ProjectID, - SignURL: "", - SignatureID: "", - UserID: *input.UserID, - }, errors.New("signature return url is empty") + log.WithFields(f).Warnf("signature return url is empty") + returnURL, err = getActiveSignatureReturnURL(activeSignatureMetadata) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature return url for user: %s", *input.UserID) + return nil, err + } + if returnURL == "" { + log.WithFields(f).Warnf("signature return url is empty") + return &models.IndividualSignatureOutput{ + UserID: *input.UserID, + ProjectID: *input.ProjectID, + }, nil + } + } // 6. Get latest document @@ -474,7 +486,7 @@ func (s *service) RequestIndividualSignature(ctx context.Context, input *models. } // 7. if the CCLA/ICLA template is missing we wont have a document and return an error - if &document == nil { + if common.AreClaGroupDocumentsEqual(document, v1Models.ClaGroupDocument{}) { log.WithFields(f).WithError(err).Warnf("unable to get latest document for project: %s", *input.ProjectID) return nil, errors.New("unable to get latest document for project") } @@ -507,7 +519,7 @@ func (s *service) RequestIndividualSignature(ctx context.Context, input *models. // 10. Populate sign url log.WithFields(f).Debugf("populating sign url...") - err = s.populateSignURL(signatureModel, callBackURL, defaultValues, preferredEmail) + err = s.populateSignURL(ctx, signatureModel, callBackURL, "", "", false, "", "", defaultValues, preferredEmail) if err != nil { log.WithFields(f).WithError(err).Warnf("unable to populate sign url for user: %s", *input.UserID) return nil, err @@ -547,18 +559,6 @@ func (s *service) getIndividualSignatureCallbackURLGitlab(ctx context.Context, u githubRepositoryID := metadata["repository_id"].(string) mergeRequestID := metadata["pull_request_id"].(string) - // # Get organization id - // organization_id = get_organization_id_from_gitlab_repository(gitlab_repository_id) - - // if organization_id is None: - // cla.log.error('Could not find GitLab organization ID that is configured for this repository ID: %s', - // gitlab_repository_id) - // return None - - // return os.path.join(API_BASE_URL, 'v2/signed/gitlab/individual', str(user_id), str(organization_id), - // str(metadata['repository_id']), - // str(metadata['merge_request_id'])) - gitlabOrg, err := s.gitlabOrgService.GetGitLabOrganization(ctx, githubRepositoryID) if err != nil { log.WithFields(f).WithError(err).Warnf("unable to get organization ID for repository ID: %s", githubRepositoryID) @@ -582,7 +582,7 @@ func (s *service) getIndividualSignatureCallbackURL(ctx context.Context, userID log.WithFields(f).Debugf("generating signature callback url...") var err error - var installationId int64 = 0 // default to 0 + var installationId int64 if metadata == nil { metadata, err = s.storeRepository.GetActiveSignatureMetaData(ctx, userID) @@ -626,16 +626,334 @@ func (s *service) getIndividualSignatureCallbackURL(ctx context.Context, userID } -func (s *service) populateSignURL(latestSignature *v1Models.Signature, callbackURL string, defaultValues map[string]interface{}, preferredEmail string) error { +func (s *service) populateSignURL(ctx context.Context, + latestSignature *v1Models.Signature, callbackURL string, + authorityOrSignatoryName, authorityOrSignatoryEmail string, + sendAsEmail bool, + claManagerName, claManagerEmail string, + defaultValues map[string]interface{}, preferredEmail string) error { + f := logrus.Fields{ "functionName": "sign.populateSignURL", } - log.WithFields(f).Debugf("populating sign url...") + signatureReferenceType := latestSignature.SignatureReferenceType + + log.WithFields(f).Debugf("signatureReferenceType: %s", signatureReferenceType) + log.WithFields(f).Debugf("processing signing request...") + + userSignatureName := "Unknown" + userSignatureEmail := "Unknown" + var document v1Models.ClaGroupDocument + var project *v1Models.ClaGroup + var companyModel *v1Models.Company + var err error + var signer DocuSignRecipient + var emailBody string + var emailSubject string + + if signatureReferenceType == utils.SignatureReferenceTypeCompany { + companyModel, err = s.companyRepo.GetCompany(ctx, latestSignature.SignatureReferenceID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup company by ID: %s", latestSignature.SignatureReferenceID) + return err + } + if companyModel == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup company by ID: %s", latestSignature.SignatureReferenceID) + return errors.New("No CLA manager lookup error") + } + userSignatureName = claManagerName + userSignatureEmail = claManagerEmail + } else if signatureReferenceType == utils.SignatureReferenceTypeUser { + if !sendAsEmail { + userModel, userErr := s.userService.GetUser(latestSignature.SignatureReferenceID) + if userErr != nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", latestSignature.SignatureReferenceID) + return userErr + } + log.WithFields(f).Debugf("loaded user : %+v", userModel) + + if userModel == nil { + log.WithFields(f).WithError(userErr).Warnf("unable to lookup user by ID: %s", latestSignature.SignatureReferenceID) + msg := fmt.Sprintf("No user lookup error for user ID: %s", latestSignature.SignatureReferenceID) + return errors.New(msg) + } + + if userModel.Username != "" { + userSignatureName = userModel.Username + } + if getUserEmail(userModel, preferredEmail) != "" { + userSignatureEmail = getUserEmail(userModel, preferredEmail) + } + } + } else { + log.WithFields(f).Warnf("unknown signature reference type: %s", signatureReferenceType) + return errors.New("unknown signature reference type") + } + + // Get the document template to sign + log.WithFields(f).Debugf("getting document template to sign...") + project, err = s.projectRepo.GetCLAGroupByID(ctx, latestSignature.ProjectID, DontLoadRepoDetails) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project by ID: %s", latestSignature.ProjectID) + return err + } + + if project == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project by ID: %s", latestSignature.ProjectID) + return errors.New("No project lookup error") + } + + if signatureReferenceType == utils.SignatureReferenceTypeCompany { + log.WithFields(f).Debugf("loading project corporate document...") + document, err = common.GetCurrentDocument(ctx, project.ProjectCorporateDocuments) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project corporate document for project: %s", latestSignature.ProjectID) + return err + } + } else { + log.WithFields(f).Debugf("loading project individual document...") + document, err = common.GetCurrentDocument(ctx, project.ProjectIndividualDocuments) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup project individual document for project: %s", latestSignature.ProjectID) + return err + } + } + + // Void the existing envelope to prevent multiple envelopes pending for a signer + envelopeID := latestSignature.SignatureEnvelopeID + if envelopeID != "" { + message := fmt.Sprintf("You are getting this message because your DocuSign Session for project %s expired. A new session will be in place for your signing process.", project.ProjectName) + log.WithFields(f).Debug(message) + err = s.VoidEnvelope(ctx, envelopeID, message) + if err != nil { + log.WithFields(f).WithError(err).Warnf("DocuSign error while voiding the envelope - regardless, continuing on..., error: %s", err) + } + } + + // # Not sure what should be put in as documentId. + // document_id = uuid.uuid4().int & (1 << 16) - 1 # Random 16bit integer -.pylint: disable=no-member + + randomUuid := uuid.Must(uuid.NewV4()).String() + + documentID := int(randomUuid[0])<<8 + int(randomUuid[1]) + log.WithFields(f).Debugf("documentID: %d", documentID) + tab := getTabsFromDocument(&document, strconv.Itoa(documentID), defaultValues) + + // # Create the envelope request object + + if sendAsEmail { + log.WithFields(f).Warnf("assigning signatory name/email: %s/%s", authorityOrSignatoryName, authorityOrSignatoryEmail) + signatoryEmail := authorityOrSignatoryEmail + signatoryName := authorityOrSignatoryName + + var projectName string + var companyName string + + if project != nil { + projectName = project.ProjectName + } + + if companyModel != nil { + companyName = companyModel.CompanyName + } + + pcgs, err := s.projectClaGroupsRepo.GetProjectsIdsForClaGroup(ctx, project.ProjectID) + if err != nil { + log.WithFields(f).Debugf("problem fetching project cla groups by id :%s, err: %+v", project.ProjectID) + return err + } + + if len(pcgs) == 0 { + log.WithFields(f).Debugf("no project cla groups found for project id :%s", project.ProjectID) + return errors.New("no project cla groups found for project id") + } + + var projectNames []string + for _, pcg := range pcgs { + projectNames = append(projectNames, pcg.ProjectName) + } + + if len(projectNames) == 0 { + projectNames = []string{projectName} + } + + claSignatoryParams := &ClaSignatoryEmailParams{ + ClaGroupName: project.ProjectName, + SignatoryName: signatoryName, + CompanyName: companyName, + ProjectNames: projectNames, + ProjectVersion: project.Version, + ClaManagerName: claManagerName, + ClaManagerEmail: claManagerEmail, + } + + log.WithFields(f).Debugf("claSignatoryParams: %+v", claSignatoryParams) + emailSubject, emailBody = claSignatoryEmailContent(*claSignatoryParams) + log.WithFields(f).Debugf("subject: %s, body: %s", emailSubject, emailBody) + + signer = DocuSignRecipient{ + Email: signatoryEmail, + Name: signatoryName, + Tabs: tab, + RecipientId: "1", + RoleName: "signer", + } + + } else { + // This will be the Initial CLA Manager + signatoryName := userSignatureName + signatoryEmail := userSignatureEmail + + // Assigning a clientUserId does not send an email. + // It assumes that the user handles the communication with the client. + // In this case, the user opened the docusign document to manually sign it. + // Thus the email does not need to be sent. + + log.WithFields(f).Debugf("signatoryName: %s, signatoryEmail: %s", signatoryName, signatoryEmail) + + // # Max length for emailSubject is 100 characters - guard/truncate if necessary + emailSubject = fmt.Sprintf("EasyCLA: CLA Signature Request for %s", project.ProjectName) + if len(emailSubject) > 100 { + emailSubject = emailSubject[:97] + "..." + } + + // # Update Signed for label according to signature_type (company or name) + var userIdentifier string + if signatureReferenceType == utils.SignatureReferenceTypeCompany { + userIdentifier = companyModel.CompanyName + } else { + if signatoryName == "Unknown" || signatoryName == "" { + userIdentifier = signatoryEmail + } else { + userIdentifier = signatoryName + } + } + + log.WithFields(f).Debugf("userIdentifier: %s", userIdentifier) + + emailBody = fmt.Sprintf("CLA Sign Request for %s", userIdentifier) + + signer = DocuSignRecipient{ + Email: signatoryEmail, + Name: signatoryName, + Tabs: tab, + RecipientId: "1", + ClientUserId: latestSignature.SignatureID, + RoleName: "signer", + } + } + return nil } -func (s *service) createDefaultIndiviualValues(user *v1Models.User, preferredEmail string) (map[string]interface{}, error) { +// Helper function to extract the docusign tabs from the document +func getTabsFromDocument(document *v1Models.ClaGroupDocument, documentID string, defaultValues map[string]interface{}) DocuSignTab { + var docTab DocuSignTab + f := logrus.Fields{ + "functionName": "sign.getTabsFromDocument", + "documentID": documentID, + } + log.WithFields(f).Debugf("getting tabs from document...") + for _, tab := range document.DocumentTabs { + var args DocuSignTabDetails + args.DocumentId = documentID + args.PageNumber = strconv.FormatInt(tab.DocumentTabPage, 10) + args.XPosition = strconv.FormatInt(tab.DocumentTabPositionx, 10) + args.YPosition = strconv.FormatInt(tab.DocumentTabPositiony, 10) + args.Width = strconv.FormatInt(tab.DocumentTabWidth, 10) + args.Height = strconv.FormatInt(tab.DocumentTabHeight, 10) + args.CustomTabId = tab.DocumentTabID + args.TabLabel = tab.DocumentTabID + args.Name = tab.DocumentTabName + + if tab.DocumentTabAnchorString != "" { + args.AnchorString = tab.DocumentTabAnchorString + args.AnchorIgnoreIfNotPresent = strconv.FormatBool(tab.DocumentTabAnchorIgnoreIfNotPresent) + args.AnchorXOffset = strconv.FormatInt(tab.DocumentTabAnchorxOffset, 10) + args.AnchorYOffset = strconv.FormatInt(tab.DocumentTabAnchoryOffset, 10) + } + + if defaultValues != nil { + if value, ok := defaultValues[tab.DocumentTabID]; ok { + args.Value = value.(string) + } + } + + switch tab.DocumentTabType { + case "text": + docTab.TextTabs = append(docTab.TextTabs, args) + case "text_unlocked": + args.Locked = "false" + docTab.TextTabs = append(docTab.TextTabs, args) + case "text_optional": + args.Required = "false" + docTab.TextOptionalTabs = append(docTab.TextOptionalTabs, args) + case "number": + docTab.NumberTabs = append(docTab.NumberTabs, args) + case "sign": + docTab.SignHereTabs = append(docTab.SignHereTabs, args) + case "sign_optional": + args.Optional = "true" + docTab.SignHereOptionalTabs = append(docTab.SignHereOptionalTabs, args) + case "date": + docTab.DateSignedTabs = append(docTab.DateSignedTabs, args) + default: + log.WithFields(f).Warnf("unknown document tab type: %s", tab.DocumentTabType) + continue + } + } + + return docTab +} + +// helper function to get user email +func getUserEmail(user *v1Models.User, preferredEmail string) string { + if preferredEmail != "" { + if utils.StringInSlice(preferredEmail, user.Emails) || user.LfEmail == strfmt.Email(preferredEmail) { + return preferredEmail + } + } + if user.LfEmail != "" { + return string(user.LfEmail) + } + if len(user.Emails) > 0 { + return user.Emails[0] + } + return "" +} + +func getActiveSignatureReturnURL(metadata map[string]interface{}) (string, error) { + + f := logrus.Fields{ + "functionName": "sign.getActiveSignatureReturnURL", + } + + var returnURL string + var err error + + if _, ok := metadata["merge_request_id"]; ok { + return "", nil + + } else if _, ok := metadata["pull_request_id"]; ok { + log.WithFields(f).Debugf("pull request id found") + installationID := metadata["installation_id"].(int64) + repositoryID := metadata["repository_id"].(int64) + pullRequestID := metadata["pull_request_id"].(int) + returnURL, err = github.GetReturnURL(context.Background(), installationID, repositoryID, pullRequestID) + } else { + return "", errors.New("invalid metadata") + } + + if err != nil { + return "", err + } + + return returnURL, nil + +} + +func (s *service) createDefaultIndividualValues(user *v1Models.User, preferredEmail string) (map[string]interface{}, error) { f := logrus.Fields{ "functionName": "sign.createDefaultIndiviualValues", } @@ -847,3 +1165,17 @@ func prepareUserForSigning(ctx context.Context, userEmail string, companySFID, p return nil } + + +func claSignatoryEmailContent(params ClaSignatoryEmailParams) (string, string) { + projectNamesList := strings.Join(params.ProjectNames, ", ") + + emailSubject := fmt.Sprintf("EasyCLA: CLA Signature Request for %s", params.ClaGroupName) + emailBody := fmt.Sprintf("

Hello %s,

", params.SignatoryName) + emailBody += fmt.Sprintf("

This is a notification email from EasyCLA regarding the project(s) %s associated with the CLA Group %s. %s has designated you as an authorized signatory for the organization %s. In order for employees of your company to contribute to any of the above project(s), they must do so under a Contributor License Agreement signed by someone with authority on behalf of your company.

", projectNamesList, params.ClaGroupName, params.ClaManagerName, params.CompanyName) + emailBody += fmt.Sprintf("

After you sign, %s (as the initial CLA Manager for your company) will be able to maintain the list of specific employees authorized to contribute to the project(s) under this signed CLA.

", params.ClaManagerName) + emailBody += fmt.Sprintf("

If you are authorized to sign on your company’s behalf, and if you approve %s as your initial CLA Manager, please review the document and sign the CLA. If you have questions, or if you are not an authorized signatory of this company, please contact the requester at %s.

", params.ClaManagerName, params.ClaManagerEmail) + // You would need to implement the appendEmailHelpSignOffContent function in Go separately + + return emailSubject, emailBody +} \ No newline at end of file