From acc7215172621b79d631128c9af2b0d5ff5d4d19 Mon Sep 17 00:00:00 2001 From: Harold Wanyama Date: Fri, 29 Sep 2023 16:01:01 +0300 Subject: [PATCH] [#4002] Feature/Docusign flow in Golang - Ported python flow for icla and ccla sign to golang - Handled the new docusign auth flow Signed-off-by: Harold Wanyama --- cla-backend-go/cmd/server.go | 4 +- cla-backend-go/project/common/helpers.go | 41 +- cla-backend-go/project/service/service.go | 4 +- cla-backend-go/signatures/converters.go | 1 + cla-backend-go/signatures/dbmodels.go | 2 + cla-backend-go/signatures/repository.go | 112 ++++ cla-backend-go/signatures/service.go | 60 +++ cla-backend-go/swagger/cla.v1.yaml | 3 + cla-backend-go/swagger/cla.v2.yaml | 49 +- .../common/cla-group-document-tab.yaml | 47 ++ .../swagger/common/cla-group-document.yaml | 5 + cla-backend-go/swagger/common/signature.yaml | 13 + cla-backend-go/users/service.go | 32 ++ cla-backend-go/v2/main/main.go | 6 +- cla-backend-go/v2/repositories/service.go | 91 ++++ cla-backend-go/v2/sign/docusign.go | 168 +++++- cla-backend-go/v2/sign/handlers.go | 58 +- cla-backend-go/v2/sign/jwt.go | 41 +- cla-backend-go/v2/sign/models.go | 95 ++++ cla-backend-go/v2/sign/service.go | 509 +++++++++++++++++- cla-backend-go/v2/store/repository.go | 56 ++ 21 files changed, 1328 insertions(+), 69 deletions(-) create mode 100644 cla-backend-go/swagger/common/cla-group-document-tab.yaml create mode 100644 cla-backend-go/v2/sign/models.go diff --git a/cla-backend-go/cmd/server.go b/cla-backend-go/cmd/server.go index 150e607a5..e8477ee1b 100644 --- a/cla-backend-go/cmd/server.go +++ b/cla-backend-go/cmd/server.go @@ -318,7 +318,7 @@ func server(localMode bool) http.Handler { v2GithubActivityService := v2GithubActivity.NewService(gitV1Repository, githubOrganizationsRepo, eventsService, autoEnableService, emailService) v2ClaGroupService := cla_groups.NewService(v1ProjectService, templateService, v1ProjectClaGroupRepo, v1ClaManagerService, v1SignaturesService, metricsRepo, gerritService, v1RepositoriesService, eventsService) - v2SignService := sign.NewService(configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService) + v2SignService := sign.NewService(configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService, usersService, v1SignaturesService, storeRepository, v2RepositoriesService, githubOrganizationsService, gitlabOrganizationRepo) sessionStore, err := dynastore.New(dynastore.Path("/"), dynastore.HTTPOnly(), dynastore.TableName(configFile.SessionStoreTableName), dynastore.DynamoDB(dynamodb.New(awsSession))) if err != nil { @@ -363,7 +363,7 @@ func server(localMode bool) http.Handler { v2Company.Configure(v2API, v2CompanyService, v1ProjectClaGroupRepo, configFile.LFXPortalURL, configFile.CorporateConsoleV1URL) cla_manager.Configure(api, v1ClaManagerService, v1CompanyService, v1ProjectService, usersService, v1SignaturesService, eventsService, emailTemplateService) v2ClaManager.Configure(v2API, v2ClaManagerService, v1CompanyService, configFile.LFXPortalURL, configFile.CorporateConsoleV2URL, v1ProjectClaGroupRepo, userRepo) - sign.Configure(v2API, v2SignService) + sign.Configure(v2API, v2SignService, v2RepositoriesService, userRepo) cla_groups.Configure(v2API, v2ClaGroupService, v1ProjectService, v1ProjectClaGroupRepo, eventsService) v2GithubActivity.Configure(v2API, v2GithubActivityService) diff --git a/cla-backend-go/project/common/helpers.go b/cla-backend-go/project/common/helpers.go index 4a6e8a5a5..bbc8e825f 100644 --- a/cla-backend-go/project/common/helpers.go +++ b/cla-backend-go/project/common/helpers.go @@ -102,8 +102,7 @@ func GetCurrentDocument(ctx context.Context, docs []models.ClaGroupDocument) (mo continue } - // No previous, use the first... - if currentDoc == (models.ClaGroupDocument{}) { + if AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { currentDoc = doc currentDocVersion = version currentDocDateTime = dateTime @@ -127,3 +126,41 @@ func GetCurrentDocument(ctx context.Context, docs []models.ClaGroupDocument) (mo return currentDoc, nil } + +// AreClaGroupDocumentsEqual compares two cla group document models +func AreClaGroupDocumentsEqual(doc1, doc2 models.ClaGroupDocument) bool { + // Compare each field individually, including the DocumentTabs slice + if doc1.DocumentAuthorName != doc2.DocumentAuthorName { + return false + } + if doc1.DocumentContentType != doc2.DocumentContentType { + return false + } + if doc1.DocumentCreationDate != doc2.DocumentCreationDate { + return false + } + if doc1.DocumentFileID != doc2.DocumentFileID { + return false + } + if doc1.DocumentLegalEntityName != doc2.DocumentLegalEntityName { + return false + } + if doc1.DocumentMajorVersion != doc2.DocumentMajorVersion { + return false + } + if doc1.DocumentMinorVersion != doc2.DocumentMinorVersion { + return false + } + if doc1.DocumentName != doc2.DocumentName { + return false + } + if doc1.DocumentPreamble != doc2.DocumentPreamble { + return false + } + if doc1.DocumentS3URL != doc2.DocumentS3URL { + return false + } + + // If all comparisons passed, the structs are equal + return true +} diff --git a/cla-backend-go/project/service/service.go b/cla-backend-go/project/service/service.go index df4fc8c07..743cacc6d 100644 --- a/cla-backend-go/project/service/service.go +++ b/cla-backend-go/project/service/service.go @@ -212,7 +212,7 @@ func (s ProjectService) GetCLAGroupCurrentICLATemplateURLByID(ctx context.Contex } } - if currentDoc == (models.ClaGroupDocument{}) { + if common.AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { log.WithFields(f).WithError(err).Warn("problem determining current ICLA for this CLA Group") return "", &utils.CLAGroupICLANotConfigured{ CLAGroupID: claGroupID, @@ -288,7 +288,7 @@ func (s ProjectService) GetCLAGroupCurrentCCLATemplateURLByID(ctx context.Contex } } - if currentDoc == (models.ClaGroupDocument{}) { + if common.AreClaGroupDocumentsEqual(currentDoc, models.ClaGroupDocument{}) { log.WithFields(f).WithError(err).Warn("problem determining current CCLA for this CLA Group") return "", &utils.CLAGroupCCLANotConfigured{ CLAGroupID: claGroupID, diff --git a/cla-backend-go/signatures/converters.go b/cla-backend-go/signatures/converters.go index c09ccb0a4..f293f8de6 100644 --- a/cla-backend-go/signatures/converters.go +++ b/cla-backend-go/signatures/converters.go @@ -74,6 +74,7 @@ func (repo repository) buildProjectSignatureModels(ctx context.Context, results SignatureReferenceNameLower: dbSignature.SignatureReferenceNameLower, SignatureSigned: dbSignature.SignatureSigned, SignatureApproved: dbSignature.SignatureApproved, + SignatureSignURL: dbSignature.SignatureSignURL, SignatureMajorVersion: dbSignature.SignatureDocumentMajorVersion, SignatureMinorVersion: dbSignature.SignatureDocumentMinorVersion, Version: dbSignature.SignatureDocumentMajorVersion + "." + dbSignature.SignatureDocumentMinorVersion, diff --git a/cla-backend-go/signatures/dbmodels.go b/cla-backend-go/signatures/dbmodels.go index 8f606a067..2cbfb7979 100644 --- a/cla-backend-go/signatures/dbmodels.go +++ b/cla-backend-go/signatures/dbmodels.go @@ -17,8 +17,10 @@ type ItemSignature struct { SignatureReferenceNameLower string `json:"signature_reference_name_lower"` SignatureProjectID string `json:"signature_project_id"` SignatureReferenceType string `json:"signature_reference_type"` + SignatureEnvelopeID string `json:"signature_envelope_id"` SignatureType string `json:"signature_type"` SignatureUserCompanyID string `json:"signature_user_ccla_company_id"` + SignatureSignURL string `json:"signature_sign_url"` EmailApprovalList []string `json:"email_whitelist"` EmailDomainApprovalList []string `json:"domain_whitelist"` GitHubUsernameApprovalList []string `json:"github_whitelist"` diff --git a/cla-backend-go/signatures/repository.go b/cla-backend-go/signatures/repository.go index c9483dfbe..afe1dade0 100644 --- a/cla-backend-go/signatures/repository.go +++ b/cla-backend-go/signatures/repository.go @@ -97,6 +97,8 @@ type SignatureRepository interface { EclaAutoCreate(ctx context.Context, signatureID string, autoCreateECLA bool) error ActivateSignature(ctx context.Context, signatureID string) error GetGitLabActiveMergeRequestMetadata(ctx context.Context, gitLabAuthorUsername, gitLabAuthorEmail string) (*ActiveGitLabPullRequest, error) + GetSignaturesByReference(ctx context.Context, referenceID, referenceType, projectID, userCCLACompanyID string, signatureSigned, signatureApproved bool) ([]*models.Signature, error) + CreateSignature(ctx context.Context, signature *models.Signature) (*models.Signature, error) } type iclaSignatureWithDetails struct { @@ -132,6 +134,116 @@ func NewRepository(awsSession *session.Session, stage string, companyRepo compan } } +func (repo repository) CreateSignature(ctx context.Context, signature *models.Signature) (*models.Signature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.CreateSignature", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "signature": signature, + } + + av, err := dynamodbattribute.MarshalMap(signature) + + if err != nil { + log.WithFields(f).Warnf("error marshalling signature model, error: %v", err) + return nil, err + } + + input := &dynamodb.PutItemInput{ + Item: av, + TableName: aws.String(repo.signatureTableName), + } + + _, err = repo.dynamoDBClient.PutItem(input) + + if err != nil { + log.WithFields(f).Warnf("error adding signature, error: %v", err) + return nil, err + } + + return signature, nil + +} + +// GetSignaturesByReference returns signatures by reference +func (repo repository) GetSignaturesByReference(ctx context.Context, referenceID, referenceType, projectID, userCCLACompanyID string, signatureSigned, signatureApproved bool) ([]*models.Signature, error) { + f := logrus.Fields{ + "functionName": "v1.signatures.repository.GetSignaturesByReference", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "referenceID": referenceID, + "referenceType": referenceType, + "projectID": projectID, + "userCCLACompanyID": userCCLACompanyID, + "signatureSigned": signatureSigned, + "signatureApproved": signatureApproved, + } + + log.WithFields(f).Debug("querying signature by reference...") + var filterAdded bool + + // These are the keys we want to match for an ICLA Signature with a given CLA Group and User ID + condition := expression.Key("signature_reference_id").Equal(expression.Value(referenceID)). + And(expression.Key("signature_reference_type").Equal(expression.Value(referenceType))) + + var filter expression.ConditionBuilder + filter = addAndCondition(filter, expression.Name("signature_project_id").Equal(expression.Value(projectID)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_user_ccla_company_id").Equal(expression.Value(userCCLACompanyID)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(signatureSigned)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(signatureApproved)), &filterAdded) + + // If no query option was provided for approved and signed and our configuration default is to only show active signatures then we add the required query filters + if signatureSigned == false && signatureApproved == false && config.GetConfig().SignatureQueryDefault == utils.SignatureQueryDefaultActive { + filterAdded = true + log.WithFields(f).Debug("adding filter signature_approved: true and signature_signed: true") + filter = addAndCondition(filter, expression.Name("signature_approved").Equal(expression.Value(true)), &filterAdded) + filter = addAndCondition(filter, expression.Name("signature_signed").Equal(expression.Value(true)), &filterAdded) + } + + // Use the nice builder to create the expression + expr, err := expression.NewBuilder(). + WithKeyCondition(condition). + WithFilter(filter). + WithProjection(buildProjection()). + Build() + + if err != nil { + log.WithFields(f).Warnf("error building expression for signature query, error: %v", err) + return nil, err + } + + // Assemble the query input parameters + queryInput := &dynamodb.QueryInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + KeyConditionExpression: expr.KeyCondition(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(repo.signatureTableName), + IndexName: aws.String(SignatureReferenceIndex), + } + + // Make the DynamoDB Query API call + results, queryErr := repo.dynamoDBClient.Query(queryInput) + if queryErr != nil { + log.WithFields(f).Warnf("error retrieving signature by reference, error: %v", queryErr) + return nil, queryErr + } + + // No match, didn't find it + if *results.Count == 0 { + return nil, nil + } + + // Convert the list of DB models to a list of response models + signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, "", LoadACLDetails) + if modelErr != nil { + log.WithFields(f).Warnf("error converting DB model to response model for signature, error: %v", modelErr) + return nil, modelErr + } + + return signatureList, nil + +} + // GetGithubOrganizationsFromApprovalList returns a list of GH organizations stored in the approval list func (repo repository) GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string) ([]models.GithubOrg, error) { f := logrus.Fields{ diff --git a/cla-backend-go/signatures/service.go b/cla-backend-go/signatures/service.go index 5d7a6b709..820fec7eb 100644 --- a/cla-backend-go/signatures/service.go +++ b/cla-backend-go/signatures/service.go @@ -57,6 +57,7 @@ type SignatureService interface { GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]SignatureCompanyID, error) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams) (*models.Signatures, error) InvalidateProjectRecords(ctx context.Context, projectID, note string) (int, error) + GetSignaturesByReference(ctx context.Context, referenceID, referenceType, projectID, userCCLACompanyID string, signatureSigned, signatureApproved bool) ([]*models.Signature, error) GetGithubOrganizationsFromApprovalList(ctx context.Context, signatureID string, githubAccessToken string) ([]models.GithubOrg, error) AddGithubOrganizationToApprovalList(ctx context.Context, signatureID string, approvalListParams models.GhOrgWhitelist, githubAccessToken string) ([]models.GithubOrg, error) @@ -69,9 +70,11 @@ type SignatureService interface { GetClaGroupICLASignatures(ctx context.Context, claGroupID string, searchTerm *string, approved, signed *bool, pageSize int64, nextKey string, withExtraDetails bool) (*models.IclaSignatures, error) GetClaGroupCCLASignatures(ctx context.Context, claGroupID string, approved, signed *bool) (*models.Signatures, error) GetClaGroupCorporateContributors(ctx context.Context, claGroupID string, companyID *string, pageSize *int64, nextKey *string, searchTerm *string) (*models.CorporateContributorList, error) + GetLatestSignature(ctx context.Context, userID string, companyID string, projectID string) (*models.Signature, error) createOrGetEmployeeModels(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) CreateOrUpdateEmployeeSignature(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error) + CreateSignature(ctx context.Context, signature *models.Signature) (*models.Signature, error) handleGitHubStatusUpdate(ctx context.Context, employeeUserModel *models.User) error } @@ -117,6 +120,58 @@ func NewService(repo SignatureRepository, companyService company.IService, users } } +// GetLatestSignatures returns the latest signatures for the specified user +func (s service) GetLatestSignature(ctx context.Context, userID string, companyID string, projectID string) (*models.Signature, error) { + + f := logrus.Fields{ + "functionName": "GetLatestSignature", + "userID": userID, + "companyID": companyID, + "projectID": projectID, + } + + log.WithFields(f).Debug("querying for user signatures...") + + if userID == "" || companyID == "" || projectID == "" { + return nil, errors.New("userID, companyID, and projectID cannot be empty") + } + signatures, err := s.GetSignaturesByReference(ctx, userID, "user", projectID, companyID, true, true) + if err != nil { + return nil, err + } + + latest := &models.Signature{} + + for _, sig := range signatures { + if latest == nil { + latest = sig + continue + } + if sig.SignatureMajorVersion > latest.SignatureMajorVersion { + latest = sig + continue + } + if sig.SignatureMajorVersion == latest.SignatureMajorVersion && sig.SignatureMinorVersion > latest.SignatureMinorVersion { + latest = sig + continue + } + } + + if latest == nil { + return nil, errors.New("unable to locate latest signature") + } + + log.WithFields(f).Debugf("latest signature: %+v", latest) + + return latest, nil + +} + +// CreateSignature creates a new signature +func (s service) CreateSignature(ctx context.Context, signature *models.Signature) (*models.Signature, error) { + return s.repo.CreateSignature(ctx, signature) +} + // GetSignature returns the signature associated with the specified signature ID func (s service) GetSignature(ctx context.Context, signatureID string) (*models.Signature, error) { return s.repo.GetSignature(ctx, signatureID) @@ -143,6 +198,11 @@ func (s service) GetProjectSignatures(ctx context.Context, params signatures.Get return projectSignatures, nil } +// GetSignaturesByReference returns the list of signatures associated with the specified reference +func (s service) GetSignaturesByReference(ctx context.Context, referenceID, referenceType, projectID, userCCLACompanyID string, signatureSigned, signatureApproved bool) ([]*models.Signature, error) { + return s.repo.GetSignaturesByReference(ctx, referenceID, referenceType, projectID, userCCLACompanyID, signatureSigned, signatureApproved) +} + // CreateProjectSummaryReport generates a project summary report based on the specified input func (s service) CreateProjectSummaryReport(ctx context.Context, params signatures.CreateProjectSummaryReportParams) (*models.SignatureReport, error) { diff --git a/cla-backend-go/swagger/cla.v1.yaml b/cla-backend-go/swagger/cla.v1.yaml index beaa11886..c1b3ba69c 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' + + cla-group-document-tab: + $ref: './common/cla-group-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 64eb14c05..3d6625960 100644 --- a/cla-backend-go/swagger/cla.v2.yaml +++ b/cla-backend-go/swagger/cla.v2.yaml @@ -4137,7 +4137,7 @@ paths: - name: input in: body schema: - $ref: '#/definitions/icla-signature-input' + $ref: '#/definitions/individual-signature-input' required: true responses: '200': @@ -4746,6 +4746,9 @@ definitions: cla-group-document: $ref: './common/cla-group-document.yaml' + + cla-group-document-tab: + $ref: './common/cla-group-document-tab.yaml' meta-field: $ref: './common/meta-field.yaml' @@ -5527,37 +5530,27 @@ definitions: corporate-contributor: $ref: './common/corporate-contributor.yaml' - icla-signature-input: + individual-signature-input: type: object - required: - - project_sfid - - company_sfid properties: - project_sfid: + project_id: + description: 'The CLA group ID' type: string - example: 'a0941000005ouJFAAY' - description: salesforce id of the project - company_sfid: + example: 'a1b86c26-d8e8-4fd8-9f8d-5c723d5dac9f' + user_id: + description: 'The User ID' type: string - example: '0014100000Te0fMAAR' - description: salesforce id of the company - send_as_email: - type: boolean - example: false - description: send signing request as email. This should be set to true when requestor is not signatory. - authority_name: + example: 'a1b86c26-d8e8-4fd8-9f8d-5c723d5dac9f' + return_url_type: + description: 'The return URL type of the repository' type: string - example: "Derk Miyamoto" - description: the name of the CLA signatory - minLength: 2 - maxLength: 255 - authority_email: - $ref: './common/properties/email.yaml' - description: the email of the CLA Signatory + enum: + - Gerrit + - Github + - GitLab return_url: + description: 'The URL to return the user to after signing is complete.' type: string - example: 'https://corporate.dev.lfcla.com/#/company/eb4d7d71-693f-4047-bf8d-10d0e7764969' - description: on signing the document, page will get redirected to this url. This is valid only when send_as_email is false format: uri @@ -5614,6 +5607,12 @@ definitions: signature_id: type: string description: id of the signature + user_id: + type: string + description: uuid for the user + project_id: + type: string + description: the cla group ID sign_url: type: string description: signing url diff --git a/cla-backend-go/swagger/common/cla-group-document-tab.yaml b/cla-backend-go/swagger/common/cla-group-document-tab.yaml new file mode 100644 index 000000000..2cfcb5f8d --- /dev/null +++ b/cla-backend-go/swagger/common/cla-group-document-tab.yaml @@ -0,0 +1,47 @@ + type: object + x-nullable: false + title: CLA Group Document Tab + properties: + documentTabType: + description: Type of the document tab (e.g., "sign", "text", etc.) + type: string + documentTabId: + description: ID for the document tab + type: string + documentTabName: + description: Name of the document tab + type: string + documentTabPage: + description: Page number where the document tab is located + type: integer + documentTabPositionX: + description: X-coordinate position of the document tab + type: integer + documentTabPositionY: + description: Y-coordinate position of the document tab + type: integer + documentTabWidth: + description: Width of the document tab + type: integer + documentTabHeight: + description: Height of the document tab + type: integer + documentTabIsLocked: + description: Indicates whether the document tab is locked (default is false) + type: string + documentTabIsRequired: + description: Indicates whether the document tab is required (default is true) + type: boolean + documentTabAnchorString: + description: Anchor string for the document tab (if applicable) + type: string + documentTabAnchorIgnoreIfNotPresent: + description: Indicates whether to ignore the tab if the anchor string is not present (default is true) + type: boolean + documentTabAnchorXOffset: + description: X-coordinate offset for the anchor (if applicable) + type: integer + documentTabAnchorYOffset: + description: Y-coordinate offset for the anchor (if applicable) + type: integer + \ No newline at end of file diff --git a/cla-backend-go/swagger/common/cla-group-document.yaml b/cla-backend-go/swagger/common/cla-group-document.yaml index 8f6d9e8bb..41ab09a65 100644 --- a/cla-backend-go/swagger/common/cla-group-document.yaml +++ b/cla-backend-go/swagger/common/cla-group-document.yaml @@ -46,3 +46,8 @@ properties: description: the document creation date example: '2019-08-01T06:55:09Z' type: string + documentTabs: + description: An array of document tab objects + type: array + items: + $ref: '#/definitions/cla-group-document-tab' diff --git a/cla-backend-go/swagger/common/signature.yaml b/cla-backend-go/swagger/common/signature.yaml index 57c764893..a3bfd31cd 100644 --- a/cla-backend-go/swagger/common/signature.yaml +++ b/cla-backend-go/swagger/common/signature.yaml @@ -28,6 +28,19 @@ properties: example: '2019-05-03T18:59:13.082304+0000' minLength: 18 maxLength: 64 + signatureSignUrl: + type: string + format: url + signatureReturnUrl: + type: string + format: uri + signatureCallbackUrl: + type: string + format: uri + signatureReturnUrlType: + type: string + signatureEnvelopeId: + type: string signatureSigned: type: boolean description: the signature signed flag - true or false value diff --git a/cla-backend-go/users/service.go b/cla-backend-go/users/service.go index 1a01605b5..8faf203dc 100644 --- a/cla-backend-go/users/service.go +++ b/cla-backend-go/users/service.go @@ -8,7 +8,11 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/events" "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" "github.com/communitybridge/easycla/cla-backend-go/user" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/go-openapi/strfmt" + "github.com/sirupsen/logrus" ) // Service interface for users @@ -26,6 +30,7 @@ type Service interface { GetUserByGitLabUsername(gitlabUsername string) (*models.User, error) SearchUsers(field string, searchTerm string, fullMatch bool) (*models.Users, error) UpdateUserCompanyID(userID, companyID, note string) error + GetUserEmail(user *models.User, preferredEmail string) (string, error) } type service struct { @@ -41,6 +46,33 @@ func NewService(repo UserRepository, events events.Service) Service { } } +func (s service) GetUserEmail(user *models.User, preferredEmail string) (string, error) { + f := logrus.Fields{ + "functionName": "GetUserEmail", + "userID": user.UserID, + "preferredEmail": preferredEmail, + } + + if preferredEmail != "" && user.LfEmail != "" && user.LfEmail == strfmt.Email(preferredEmail) { + log.WithFields(f).Debug("user email matches preferred email") + return user.LfEmail.String(), nil + } + + if preferredEmail != "" && utils.StringInSlice(preferredEmail, user.Emails) { + log.WithFields(f).Debug("user email matches preferred email") + return preferredEmail, nil + } + + if len(user.Emails) > 0 { + log.WithFields(f).Debug("returning first user email") + return user.Emails[0], nil + } + + log.WithFields(f).Debug("unable to find user email") + return "", errors.New("unable to find user email") + +} + // CreateUser attempts to create a new user based on the specified model func (s service) CreateUser(user *models.User, claUser *user.CLAUser) (*models.User, error) { userModel, err := s.repo.CreateUser(user) diff --git a/cla-backend-go/v2/main/main.go b/cla-backend-go/v2/main/main.go index d220a74c4..2dbcc99c8 100644 --- a/cla-backend-go/v2/main/main.go +++ b/cla-backend-go/v2/main/main.go @@ -3,13 +3,13 @@ package main import ( "fmt" - "github.com/communitybridge/easycla/cla-backend-go/v2/docusign_auth" + docusignauth "github.com/communitybridge/easycla/cla-backend-go/v2/docusign_auth" ) func main() { integrationKey := "557677f2-1e2f-4955-aaa6-1ef44630e01d" userID := "3a1d118f-3083-4c25-8306-11b7400f7c03" - privateKey :=` + privateKey := ` -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAh0M2mIGaJjP8S/FxZR7nRsatCR/KpCPBFBbxalZffykqtTID KNeDhJ5RvJKAVoJlLaLoUYSYloVaeSAwQdbn4F+Lsnll3mCGocwdl/W8998Lc/Ln @@ -44,4 +44,4 @@ func main() { } fmt.Println(token) -} \ No newline at end of file +} diff --git a/cla-backend-go/v2/repositories/service.go b/cla-backend-go/v2/repositories/service.go index a9dfe6385..13ba2777a 100644 --- a/cla-backend-go/v2/repositories/service.go +++ b/cla-backend-go/v2/repositories/service.go @@ -5,8 +5,11 @@ package repositories import ( "context" + "encoding/json" "errors" "fmt" + "io" + "net/http" "strconv" "github.com/communitybridge/easycla/cla-backend-go/events" @@ -37,6 +40,12 @@ import ( v2ProjectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" ) +type Email struct { + Email string `json:"email"` + Verified bool `json:"verified"` + Primary bool `json:"primary"` +} + // ServiceInterface contains functions of the repositories service type ServiceInterface interface { // GitHub @@ -51,6 +60,7 @@ type ServiceInterface interface { GitHubDisableCLAGroupRepositories(ctx context.Context, claGroupID string) error GitHubGetProtectedBranch(ctx context.Context, projectSFID, repositoryID, branchName string) (*v2Models.GithubRepositoryBranchProtection, error) GitHubUpdateProtectedBranch(ctx context.Context, projectSFID, repositoryID string, input *v2Models.GithubRepositoryBranchProtectionInput) (*v2Models.GithubRepositoryBranchProtection, error) + GithubGetPrimaryUserEmail(ctx context.Context, request *http.Request) (*string, error) // GitLab @@ -84,6 +94,20 @@ type GitLabOrgRepo interface { DeleteGitLabOrganizationByFullPath(ctx context.Context, projectSFID, gitlabOrgFullPath string) error } +type contextKey string + +func (c contextKey) String() string { + return string(c) +} + +func GetRequestSession(r *http.Request) (map[string]interface{}, error) { + session := r.Context().Value(contextKey("session")) + if session == nil { + return nil, errors.New("session not found") + } + return session.(map[string]interface{}), nil +} + // Service is the service model/structure type Service struct { gitV1Repository v1Repositories.RepositoryInterface @@ -114,6 +138,73 @@ func NewService(gitV1Repository *v1Repositories.Repository, gitV2Repository Repo } } +func fetchGithubEmails(session map[string]interface{}) ([]Email, error) { + f := logrus.Fields{ + "functionName": "fetchGithubEmails", + } + log.WithFields(f).Debugf("fetching user emails from token") + + token, ok := session["github_oauth2_token"].(string) + if !ok || token == "" { + return nil, errors.New("invalid github oauth2 token") + } + client := &http.Client{} + req, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil) + if err != nil { + return nil, errors.New("error creating request") + } + + req.Header.Add("Authorization", "Bearer "+token) + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + + if err != nil { + log.WithFields(f).Debug("could not read response body :%s", err) + return nil, nil + } + var emails []Email + err = json.Unmarshal(body, &emails) + if err != nil { + log.Debugf("unable to unmarshal response body") + return emails, err + } + + return emails, nil +} + +func (s *Service) GithubGetPrimaryUserEmail(ctx context.Context, request *http.Request) (*string, error) { + f := logrus.Fields{ + "functionName": "v2.repositories.service.GithubGetPrimaryUserEmail", + } + session, err := GetRequestSession(request) + + if err != nil { + log.WithFields(f).Debug("failed to get session") + return nil, nil + } + emails, err := fetchGithubEmails(session) + + if err != nil { + log.WithFields(f).Debug("failed to fetch github emails") + return nil, nil + } + + for _, email := range emails { + if email.Verified && email.Primary { + return &email.Email, nil + } + } + + log.WithFields(f).Debug("unable to get emails") + + return nil, nil +} + // GitHubAddRepositories adds the specified GitHub repository to the specified project func (s *Service) GitHubAddRepositories(ctx context.Context, projectSFID string, input *models.GithubRepositoryInput) ([]*v1Models.GithubRepository, error) { f := logrus.Fields{ diff --git a/cla-backend-go/v2/sign/docusign.go b/cla-backend-go/v2/sign/docusign.go index 394a332a6..0a35ba664 100644 --- a/cla-backend-go/v2/sign/docusign.go +++ b/cla-backend-go/v2/sign/docusign.go @@ -5,14 +5,174 @@ package sign import ( "context" - "log" + "encoding/json" + "io" + "net/http" + "strconv" + "strings" + + v1Models "github.com/communitybridge/easycla/cla-backend-go/gen/v1/models" + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" + "github.com/sirupsen/logrus" ) +func (s *service) getDocumentTabsFromDocument(ctx context.Context, document v1Models.ClaGroupDocument, documentID string, defaultValues map[string]interface{}) ([]*DocuSignTabDetails, error) { + f := logrus.Fields{ + "functionName": "sign.getDocumentTabsFromDocument", + "documentID": documentID, + } + log.WithFields(f).Debug("getting document tabs from document") + + tabs := []*DocuSignTabDetails{} + + for _, tab := range document.DocumentTabs { + + args := &DocuSignTabDetails{ + DocumentId: documentID, + TabLabel: tab.DocumentTabID, + PageNumber: strconv.Itoa(int(tab.DocumentTabPage)), + XPosition: strconv.Itoa(int(tab.DocumentTabPositionX)), + YPosition: strconv.Itoa(int(tab.DocumentTabPositionY)), + Width: strconv.Itoa(int(tab.DocumentTabWidth)), + Height: strconv.Itoa(int(tab.DocumentTabHeight)), + Name: tab.DocumentTabName, + TabType: tab.DocumentTabType, + } + + if tab.DocumentTabAnchorString != "" { + args.AnchorString = tab.DocumentTabAnchorString + args.AnchorIgnoreIfNotPresent = strconv.FormatBool(tab.DocumentTabAnchorIgnoreIfNotPresent) + args.AnchorXOffset = strconv.Itoa(int(tab.DocumentTabAnchorXOffset)) + args.AnchorYOffset = strconv.Itoa(int(tab.DocumentTabAnchorYOffset)) + } + + if value, ok := defaultValues[tab.DocumentTabID]; ok { + args.Value = value.(string) + } + + tabs = append(tabs, args) + } + + return tabs, nil +} + +func (s *service) voidDocument(ctx context.Context, documentID, message string) error { + f := logrus.Fields{ + "functionName": "sign.voidDocument", + "documentID": documentID, + } + log.WithFields(f).Debug("voiding document") + + accessToken, err := s.getAccessToken(ctx) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to get access token") + return err + } + + url := utils.GetProperty("DOCUSIGN_ROOT_URL") + "/v2.1/accounts/" + utils.GetProperty("DOCUSIGN_ACCOUNT_ID") + "/envelopes/" + documentID + "/views/recipient" + + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to create request") + return err + } + + req.Header.Add("Authorization", "Bearer "+accessToken) + + // Send the request using the http client + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to send request") + return err + } + + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("unable to close response body") + } + }() + + if resp.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("non-200 response code from docusign: %d", resp.StatusCode) + return err + } + + return nil +} + +// getAccessToken returns an access token for the docusign api func (s *service) getAccessToken(ctx context.Context) (string, error) { f := logrus.Fields{ "functionName": "sign.getAccessToken", } + jwtAssertion, err := jwtToken() + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to generate jwt token") + return "", err + } + + tokenRequestBody := DocuSignGetTokenRequest{ + GrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", + Assertion: jwtAssertion, + } + + tokenRequestBodyString, marshallErr := json.Marshal(tokenRequestBody) + if marshallErr != nil { + log.WithFields(f).WithError(marshallErr).Warn("unable to marshal token request body") + return "", marshallErr + } + + // Create the request + url := utils.GetProperty("DOCUSIGN_AUTH_SERVER") + "/oauth/token" + + req, err := http.NewRequest("POST", url, strings.NewReader(string(tokenRequestBodyString))) + + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to create request") + return "", err + } + + // Set the content type header, as well as the expected response type + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + + // Send the request using the http client + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to send request") + return "", err + } + + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil { + log.WithFields(f).WithError(closeErr).Warn("unable to close response body") + } + }() + + // Parse the response + responsePayload, readErr := io.ReadAll(resp.Body) + if readErr != nil { + log.WithFields(f).WithError(readErr).Warn("unable to read response body") + return "", readErr + } + + if resp.StatusCode != http.StatusOK { + log.WithFields(f).Warnf("non-200 response code from docusign: %d", resp.StatusCode) + return "", err + } + + var tokenResponse DocuSignGetTokenResponse + unmarshallErr := json.Unmarshal(responsePayload, &tokenResponse) + if unmarshallErr != nil { + log.WithFields(f).WithError(unmarshallErr).Warn("unable to unmarshal response body") + return "", unmarshallErr + } - // Get the access token - jwtAssertion, jwterr := jwtToken() -} \ No newline at end of file + return tokenResponse.AccessToken, nil +} diff --git a/cla-backend-go/v2/sign/handlers.go b/cla-backend-go/v2/sign/handlers.go index d3c743acc..7d8b7a37c 100644 --- a/cla-backend-go/v2/sign/handlers.go +++ b/cla-backend-go/v2/sign/handlers.go @@ -4,12 +4,12 @@ package sign import ( - "context" "errors" "fmt" "strings" log "github.com/communitybridge/easycla/cla-backend-go/logging" + userMod "github.com/communitybridge/easycla/cla-backend-go/user" "github.com/sirupsen/logrus" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" @@ -20,12 +20,12 @@ import ( "github.com/communitybridge/easycla/cla-backend-go/gen/v2/restapi/operations/sign" "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service/client/organizations" + v2Repos "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" "github.com/go-openapi/runtime/middleware" - ) // Configure API call -func Configure(api *operations.EasyclaAPI, service Service) { +func Configure(api *operations.EasyclaAPI, service Service, repoService v2Repos.ServiceInterface, userService userMod.RepositoryService) { // Retrieve a list of available templates api.SignRequestCorporateSignatureHandler = sign.RequestCorporateSignatureHandlerFunc( func(params sign.RequestCorporateSignatureParams, user *auth.User) middleware.Responder { @@ -80,21 +80,51 @@ func Configure(api *operations.EasyclaAPI, service Service) { api.SignRequestIndividualSignatureHandler = sign.RequestIndividualSignatureHandlerFunc( func(params sign.RequestIndividualSignatureParams) middleware.Responder { - reqId := utils.GetRequestID(params.XREQUESTID) - ctx := context.WithValue(params.HTTPRequest.Context(), utils.XREQUESTID, reqId) + reqID := utils.GetRequestID(params.XREQUESTID) + ctx := utils.NewContext() f := logrus.Fields{ "functionName": "v2.sign.handlers.SignRequestIndividualSignatureHandler", utils.XREQUESTID: ctx.Value(utils.XREQUESTID), - "CompanyID": params.Input.CompanySfid, - "ProjectSFID": params.Input.ProjectSfid, - "authorityName": params.Input.AuthorityName, - "authorityEmail": params.Input.AuthorityEmail, + "userID": params.Input.UserID, + "projectID": params.Input.ProjectID, + "repoType": params.Input.ReturnURLType, + "repoURL": params.Input.ReturnURL, } - log.WithFields(f).Debug("processing request") - resp, err := service.RequestIndividualSignature(ctx, params.Input) - if err != nil { - log.WithFields(f).WithError(err).Warn("problem requesting individual signature") - return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqId, err)) + + log.WithFields(f).Debug("request individual signature") + var resp *models.IndividualSignatureOutput + var err error + if params.Input.ReturnURLType != "" { + switch strings.ToLower(params.Input.ReturnURLType) { + case "gerrit": + log.WithFields(f).Debug("request individual signature - gerrit") + resp, err = service.RequestIndividualSignatureGerrit(ctx, params.Input.ProjectID, params.Input.UserID, params.Input.ReturnURL.String()) + + case "github", "gitlab": + var primaryUserEmail *string + log.WithFields(f).Debug("request individual signature - github/gitlab") + if strings.ToLower(params.Input.ReturnURLType) == "github" { + primaryUserEmail, err = repoService.GithubGetPrimaryUserEmail(ctx, params.HTTPRequest) + if err != nil { + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqID, err)) + } + } else { + user, err := userService.GetUser(params.Input.UserID) + if err != nil { + log.WithFields(f).Debugf("unable to lookup user by ID: %s, error: %+v", params.Input.UserID, err) + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqID, err)) + } + log.WithFields(f).Debugf("user lookup by ID: %s, result: %+v", params.Input.UserID, user) + primaryUserEmail = &user.UserEmails[0] + } + resp, err = service.RequestIndividualSignature(ctx, params.Input.ProjectID, params.Input.UserID, params.Input.ReturnURL.String(), params.Input.ReturnURLType, *primaryUserEmail) + if err != nil { + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqID, err)) + } + } + if err != nil { + return sign.NewRequestIndividualSignatureBadRequest().WithPayload(errorResponse(reqID, err)) + } } return sign.NewRequestIndividualSignatureOK().WithPayload(resp) }) diff --git a/cla-backend-go/v2/sign/jwt.go b/cla-backend-go/v2/sign/jwt.go index ac954d3e8..2c000bb4c 100644 --- a/cla-backend-go/v2/sign/jwt.go +++ b/cla-backend-go/v2/sign/jwt.go @@ -4,14 +4,47 @@ package sign import ( + "os" + "time" + + log "github.com/communitybridge/easycla/cla-backend-go/logging" + "github.com/communitybridge/easycla/cla-backend-go/utils" "github.com/golang-jwt/jwt" "github.com/sirupsen/logrus" ) -const - func jwtToken() (string, error) { + f := logrus.Fields{ + "functionName": "sign.jwtToken", + } + log.WithFields(f).Debug("generating jwt token") + + now := time.Now() claims := jwt.MapClaims{ - "iss": , + "iss": utils.GetProperty("DOCUSIGN_INTEGRATOR_KEY"), + "sub": utils.GetProperty("DOCUSIGN_USER_ID"), + "aud": utils.GetProperty("DOCUSIGN_AUTH_SERVER"), + "iat": now.Unix(), + "exp": now.Add(time.Hour).Unix(), + "scope": "signature impersonation", } -} \ No newline at end of file + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + token.Header["alg"] = "RS256" + token.Header["typ"] = "JWT" + + key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(os.Getenv("DOCUSIGN_PRIVATE_KEY"))) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to parse rsa private key") + return "", err + } + + signedToken, err := token.SignedString(key) + if err != nil { + log.WithFields(f).WithError(err).Warn("unable to sign jwt token") + return "", err + } + + return signedToken, nil +} diff --git a/cla-backend-go/v2/sign/models.go b/cla-backend-go/v2/sign/models.go new file mode 100644 index 000000000..805d7e12d --- /dev/null +++ b/cla-backend-go/v2/sign/models.go @@ -0,0 +1,95 @@ +// Copyright The Linux Foundation and each contributor to CommunityBridge. +// SPDX-License-Identifier: MIT + +package sign + +// DocuSignGetTokenRequest is the request body for getting a token from DocuSign +type DocuSignGetTokenRequest struct { + GrantType string `json:"grant_type"` + Assertion string `json:"assertion"` +} + +// DocuSignGetTokenResponse is the response body for getting a token from DocuSign +type DocuSignGetTokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +// DocuSignTab is the data model for a tab from DocuSign +type DocuSignTab struct { + ApproveTabs []DocuSignTabDetails `json:"approveTabs,omitempty"` + CheckBoxTabs []DocuSignTabDetails `json:"checkboxTabs,omitempty"` + CommentThreadTabs []DocuSignTabDetails `json:"commentThreadTabs,omitempty"` + CommissionCountyTabs []DocuSignTabDetails `json:"commissionCountyTabs,omitempty"` + CommissionExpirationTabs []DocuSignTabDetails `json:"commissionExpirationTabs,omitempty"` + CommissionNumberTabs []DocuSignTabDetails `json:"commissionNumberTabs,omitempty"` + CommissionStateTabs []DocuSignTabDetails `json:"commissionStateTabs,omitempty"` + CompanyTabs []DocuSignTabDetails `json:"companyTabs,omitempty"` + DateSignedTabs []DocuSignTabDetails `json:"dateSignedTabs,omitempty"` + DateTabs []DocuSignTabDetails `json:"dateTabs,omitempty"` + DeclinedTabs []DocuSignTabDetails `json:"declineTabs,omitempty"` + DrawTabs []DocuSignTabDetails `json:"drawTabs,omitempty"` + EmailAddressTabs []DocuSignTabDetails `json:"emailAddressTabs,omitempty"` + EmailTabs []DocuSignTabDetails `json:"emailTabs,omitempty"` + EnvelopeIdTabs []DocuSignTabDetails `json:"envelopeIdTabs,omitempty"` + FirstNameTabs []DocuSignTabDetails `json:"firstNameTabs,omitempty"` + FormulaTabs []DocuSignTabDetails `json:"formulaTab,omitempty"` + FullNameTabs []DocuSignTabDetails `json:"fullNameTabs,omitempty"` + InitialHereTabs []DocuSignTabDetails `json:"initialHereTabs,omitempty"` + LastNameTabs []DocuSignTabDetails `json:"lastNameTabs,omitempty"` + ListTabs []DocuSignTabDetails `json:"listTabs,omitempty"` + NotarizeTabs []DocuSignTabDetails `json:"notarizeTabs,omitempty"` + NotarySealTabs []DocuSignTabDetails `json:"notarySealTabs,omitempty"` + NoteTabs []DocuSignTabDetails `json:"noteTabs,omitempty"` + NumberTabs []DocuSignTabDetails `json:"numberTabs,omitempty"` + NumericalTabs []DocuSignTabDetails `json:"numericalTabs,omitempty"` + PhoneNumberTabs []DocuSignTabDetails `json:"phoneNumberTabs,omitempty"` + PolyLineOverlayTabs []DocuSignTabDetails `json:"polyLineOverlayTabs,omitempty"` + PrefillTabs []DocuSignTabDetails `json:"prefillTabs,omitempty"` + RadioGroupTabs []DocuSignTabDetails `json:"radioGroupTabs,omitempty"` + SignerAttachmentTabs []DocuSignTabDetails `json:"signerAttachmentTabs,omitempty"` + SignHereTabs []DocuSignTabDetails `json:"signHereTabs,omitempty"` + SmartSectionTabs []DocuSignTabDetails `json:"smartSectionTabs,omitempty"` + SSNTabs []DocuSignTabDetails `json:"ssnTabs,omitempty"` + TabGroups []DocuSignTabDetails `json:"tabGroupTabs,omitempty"` + TextTabs []DocuSignTabDetails `json:"textTabs,omitempty"` + TitleTabs []DocuSignTabDetails `json:"titleTabs,omitempty"` + ViewTabs []DocuSignTabDetails `json:"viewTabs,omitempty"` + ZipTabs []DocuSignTabDetails `json:"zipTabs,omitempty"` +} + +// DocuSignTabDetails is the data model for a tab from DocuSign +type DocuSignTabDetails struct { + AnchorCaseSensitive string `json:"anchorCaseSensitive,omitempty"` // anchor case sensitive flag, "true" or "false" + AnchorIgnoreIfNotPresent string `json:"anchorIgnoreIfNotPresent,omitempty"` // When true, this tab is ignored if the anchorString is not found in the document. + AnchorHorizontalAlignment string `json:"anchorHorizontalAlignment,omitempty"` // This property controls how anchor tabs are aligned in relation to the anchor text. Possible values are : left: Aligns the left side of the tab with the beginning of the first character of the matching anchor word. This is the default value. right: Aligns the tab’s left side with the last character of the matching anchor word. + AnchorMatchWholeWord string `json:"anchorMatchWholeWord,omitempty"` // When true, the text string in a document must match the value of the anchorString property in its entirety for an anchor tab to be created. The default value is false. For example, when set to true, if the input is man then man will match but manpower, fireman, and penmanship will not. When false, if the input is man then man, manpower, fireman, and penmanship will all match. + AnchorString string `json:"anchorString,omitempty"` // Specifies the string to find in the document and use as the basis for tab placement + AnchorUnits string `json:"anchorUnits,omitempty"` // anchor units, pixels, cms, mms + AnchorXOffset string `json:"anchorXOffset,omitempty"` // anchor x offset + AnchorYOffset string `json:"anchorYOffset,omitempty"` // anchor y offset + Bold string `json:"bold,omitempty"` // bold flag, "true" or "false" + DocumentId string `json:"documentId,omitempty"` // Specifies the document ID number that the tab is placed on. This must refer to an existing Document's ID attribute. + Font string `json:"font,omitempty"` // font + FontSize string `json:"fontSize,omitempty"` // font size + Height string `json:"height,omitempty"` // The height of the tab in pixels. Must be an integer. + Locked string `json:"locked,omitempty"` // locked flag, "true" or "false" + MinNumericalValue string `json:"minNumericalValue,omitempty"` // minimum numerical value, such as "0", used for validation of numerical tabs + MaxNumericalValue string `json:"maxNumericalValue,omitempty"` // maximum numerical value, such as "100", used for validation of numerical tabs + Name string `json:"name,omitempty"` // The name of the tab. For example, Sign Here or Initial Here. If the tooltip attribute is not set, this value will be displayed as the custom tooltip text. + Optional string `json:"optional,omitempty"` // When true, the recipient does not need to complete this tab to complete the signing process + PageNumber string `json:"pageNumber,omitempty"` // Specifies the page number on which the tab is located. Must be 1 for supplemental documents. + Required string `json:"required,omitempty"` // When true, the signer is required to fill out this tab + TabId string `json:"tabId,omitempty"` // tab idj + TabLabel string `json:"tabLabel,omitempty"` // label + TabOrder string `json:"tabOrder,omitempty"` // A positive integer that sets the order the tab is navigated to during signing. Tabs on a page are navigated to in ascending order, starting with the lowest number and moving to the highest. If two or more tabs have the same tabOrder value, the normal auto-navigation setting behavior for the envelope is used. + TabType string `json:"tabType,omitempty"` // Indicates type of tab (for example: signHere or initialHere) + ToolTip string `json:"toolTip,omitempty"` // The text of a tooltip that appears when a user hovers over a form field or tab. + Width string `json:"width,omitempty"` // The width of the tab in pixels. Must be an integer. This is not applicable to Sign Here tab. + XPosition string `json:"xPosition,omitempty"` // x position + YPosition string `json:"yPosition,omitempty"` // x position + ValidationType string `json:"validationType,omitempty"` // validation type, "string", "number", "date", "zipcode", "currency" + Value string `json:"value,omitempty"` +} diff --git a/cla-backend-go/v2/sign/service.go b/cla-backend-go/v2/sign/service.go index 99b282687..f2c83486b 100644 --- a/cla-backend-go/v2/sign/service.go +++ b/cla-backend-go/v2/sign/service.go @@ -12,10 +12,17 @@ import ( "io" "net/http" "os" + "path" + "strconv" "strings" + "time" + "github.com/communitybridge/easycla/cla-backend-go/project/common" "github.com/communitybridge/easycla/cla-backend-go/projects_cla_groups" "github.com/communitybridge/easycla/cla-backend-go/v2/cla_groups" + "github.com/communitybridge/easycla/cla-backend-go/v2/store" + "github.com/go-openapi/strfmt" + "github.com/gofrs/uuid" "github.com/sirupsen/logrus" @@ -24,7 +31,12 @@ import ( organizationService "github.com/communitybridge/easycla/cla-backend-go/v2/organization-service" + ghOrgService "github.com/communitybridge/easycla/cla-backend-go/github_organizations" + signatureService "github.com/communitybridge/easycla/cla-backend-go/signatures" + claUser "github.com/communitybridge/easycla/cla-backend-go/users" + gitlabOrgService "github.com/communitybridge/easycla/cla-backend-go/v2/gitlab_organizations" projectService "github.com/communitybridge/easycla/cla-backend-go/v2/project-service" + repositoryService "github.com/communitybridge/easycla/cla-backend-go/v2/repositories" userService "github.com/communitybridge/easycla/cla-backend-go/v2/user-service" log "github.com/communitybridge/easycla/cla-backend-go/logging" @@ -33,13 +45,14 @@ import ( 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/utils" - "github.com/communitybridge/easycla/cla-backend-go/v2/docusign_auth" + // docusignauth "github.com/communitybridge/easycla/cla-backend-go/v2/docusign_auth" ) var ( integrationKey = os.Getenv("DOCUSIGN_INTEGRATOR_KEY") userGUID = os.Getenv("DOCUSIGN_USER_ID") privateKey = os.Getenv("DOCUSIGN_PRIVATE_KEY") + apiBasePath = os.Getenv("API_BASE_PATH") ) // constants @@ -61,8 +74,13 @@ type ProjectRepo interface { // Service interface defines the sign service methods type Service interface { + getAccessToken(ctx context.Context) (string, error) + getDocumentTabsFromDocument(ctx context.Context, document v1Models.ClaGroupDocument, documentID string, defaultValues map[string]interface{}) ([]*DocuSignTabDetails, error) + voidDocument(ctx context.Context, documentID, message string) error + RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) - RequestIndividualSignature(ctx context.Context, input *models.IclaSignatureInput) (*models.IndividualSignatureOutput, error) + RequestIndividualSignature(ctx context.Context, projectID, userID, returnURL, returnURLType, preferredEmail string) (*models.IndividualSignatureOutput, error) + RequestIndividualSignatureGerrit(ctx context.Context, projectID, userID, returnURL string) (*models.IndividualSignatureOutput, error) } // service @@ -73,10 +91,16 @@ type service struct { projectClaGroupsRepo projects_cla_groups.Repository companyService company.IService claGroupService cla_groups.Service + claUserService claUser.Service + signatureService signatureService.SignatureService + storeRepo store.Repository + repositoryService repositoryService.ServiceInterface + ghOrgService ghOrgService.Service + gitlabOrgService gitlabOrgService.RepositoryInterface } // NewService returns an instance of v2 project service -func NewService(apiURL string, compRepo company.IRepository, projectRepo ProjectRepo, pcgRepo projects_cla_groups.Repository, compService company.IService, claGroupService cla_groups.Service) Service { +func NewService(apiURL string, compRepo company.IRepository, projectRepo ProjectRepo, pcgRepo projects_cla_groups.Repository, compService company.IService, claGroupService cla_groups.Service, claUserService claUser.Service, signatureService signatureService.SignatureService, storeRepo store.Repository, repositoryService repositoryService.ServiceInterface, ghorgService ghOrgService.Service, gitlabOrgService gitlabOrgService.RepositoryInterface) Service { return &service{ ClaV1ApiURL: apiURL, companyRepo: compRepo, @@ -84,6 +108,11 @@ func NewService(apiURL string, compRepo company.IRepository, projectRepo Project projectClaGroupsRepo: pcgRepo, companyService: compService, claGroupService: claGroupService, + claUserService: claUserService, + signatureService: signatureService, + storeRepo: storeRepo, + ghOrgService: ghorgService, + gitlabOrgService: gitlabOrgService, } } @@ -135,6 +164,47 @@ func validateCorporateSignatureInput(input *models.CorporateSignatureInput) erro return nil } +func getLatestVersion(documents []v1Models.ClaGroupDocument) *v1Models.ClaGroupDocument { + f := logrus.Fields{ + "functionName": "getLatestVersion", + } + var latest *v1Models.ClaGroupDocument + var latestMajor int64 + var latestMinor int64 + var latestDate string + for _, doc := range documents { + currentMajor, err := strconv.ParseInt(doc.DocumentMajorVersion, 10, 64) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to parse document major version: %s", doc.DocumentMajorVersion) + continue + } + currentMinor, err := strconv.ParseInt(doc.DocumentMinorVersion, 10, 64) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to parse document minor version: %s", doc.DocumentMinorVersion) + continue + } + + if currentMajor > latestMajor { + latest = &doc + latestMajor = currentMajor + latestMinor = currentMinor + continue + } + + if currentMajor == latestMajor && currentMinor > latestMinor { + latest = &doc + latestMinor = currentMinor + } + + if latestDate == "" || doc.DocumentCreationDate > latestDate { + latest = &doc + latestDate = doc.DocumentCreationDate + } + + } + return latest +} + func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername string, authorizationHeader string, input *models.CorporateSignatureInput) (*models.CorporateSignatureOutput, error) { // nolint f := logrus.Fields{ "functionName": "sign.RequestCorporateSignature", @@ -311,25 +381,438 @@ func (s *service) RequestCorporateSignature(ctx context.Context, lfUsername stri return out.toModel(), nil } -func (s *service) RequestIndividualSignature(ctx context.Context, input *models.IclaSignatureInput) (*models.IndividualSignatureOutput, error) { +func (s *service) RequestIndividualSignature(ctx context.Context, projectID, userID, returnURL, returnURLType, preferredEmail string) (*models.IndividualSignatureOutput, error) { f := logrus.Fields{ - "functionName": "sign.RequestIndividualSignature", - "authorityEmail": input.AuthorityEmail, - "authorityName": input.AuthorityName, - "companySFID": input.CompanySfid, - "projectSFID": input.ProjectSfid, + "functionName": "sign.RequestIndividualSignature", + "projectID": projectID, + "userID": userID, + "returnURL": returnURL, + "returnURLType": returnURLType, + "preferredEmail": preferredEmail, + } + + var signatureOutput *models.IndividualSignatureOutput + + // 1. ensure this is a valid user + log.WithFields(f).Debugf("getting user by id: %s", userID) + user, err := s.claUserService.GetUser(userID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get user by id: %s", userID) + return nil, err + } + if user.UserID == "" { + log.WithFields(f).WithError(err).Warnf("unable to get user by id: %s", userID) + return nil, errors.New("user not found") + } + + // 2. ensure this is a valid project + log.WithFields(f).Debugf("getting project by id: %s", projectID) + project, err := s.projectRepo.GetCLAGroupByID(ctx, projectID, DontLoadRepoDetails) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get project by id: %s", projectID) + return nil, err + } + + if project == nil { + log.WithFields(f).WithError(err).Warnf("unable to get project by id: %s", projectID) + return nil, errors.New("cla group not found") + } + + // 3. get latest signature + log.WithFields(f).Debugf("checking for active signature object with this project: %s", projectID) + latestSignature, err := s.signatureService.GetLatestSignature(ctx, userID, "", projectID) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get latest signature by project id: %s", projectID) + return nil, err + } + + proj, err := s.projectRepo.GetCLAGroupByID(ctx, projectID, DontLoadRepoDetails) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get project by id: %s", projectID) + return nil, err + } + + if proj == nil { + log.WithFields(f).WithError(err).Warnf("unable to get project by id: %s", projectID) + return nil, errors.New("cla group not found") + } + + lastDocument, err := common.GetCurrentDocument(ctx, proj.ProjectIndividualDocuments) + + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get latest document by project id: %s", projectID) + return nil, err } - log.WithFields(f).Debug("Get Access Token for DocuSign") - accessToken, err := docusignauth.GetAccessToken(integrationKey, userGUID, privateKey) + log.WithFields(f).Debugf("latest_document: %+v", lastDocument) + + defaultCLAValues := s.createDefaultValues(ctx, user, preferredEmail) + log.WithFields(f).Debugf("defaultCLAValues: %+v", defaultCLAValues) + + // 4 check for active signature object with this project + signatureMetadata, err := s.storeRepo.GetActiveSignatureMetaData(ctx, userID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature metadata by user id: %s", userID) + return nil, err + } + + if signatureMetadata == nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature metadata by user id: %s", userID) + return nil, errors.New("signature metadata not found") + } + + log.WithFields(f).Debugf("signatureMetadata: %+v", signatureMetadata) + + var callBackURL string + + if strings.ToLower(returnURLType) == "github" { + callBackURL, err = s.getSignatureCallbackURL(ctx, userID, signatureMetadata) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signature callback url by user id: %s", userID) + return nil, err + } + } else if strings.ToLower(returnURLType) == "gitlab" { + callBackURL, err = s.getSignatureCallbackURLGitLab(ctx, userID, signatureMetadata) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to get signature callback url by user id: %s", userID) + return nil, err + } + } + + log.WithFields(f).Debugf("callBackURL: %+v", callBackURL) + + if latestSignature != nil && &lastDocument != nil && lastDocument.DocumentMajorVersion == latestSignature.SignatureMajorVersion { + log.WithFields(f).Debugf("signature already exist for this project: %s", projectID) + + // regenerate and set the signing url - this will update the signature record + err := s.populateSignURL(ctx, latestSignature, callBackURL, "", "", false, "", "", defaultCLAValues, preferredEmail) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url by user id: %s", userID) + return nil, err + } + + signatureOutput = &models.IndividualSignatureOutput{ + SignURL: latestSignature.SignatureSignURL, + SignatureID: latestSignature.SignatureID, + UserID: userID, + ProjectID: projectID, + } + + return signatureOutput, nil + } + + // 5. get signature return url + if returnURL == "" { + log.WithFields(f).Debugf("return url is empty, setting default return url") + returnURL = "" + } + + if returnURL == "" { + log.WithFields(f).Debug("No active signature found for user - cannot generate returnURL without knowing where the user came from") + return &models.IndividualSignatureOutput{ + UserID: userID, + ProjectID: projectID, + }, errors.New("no active signature found for user - cannot generate returnURL without knowing where the user came from") + } + + // 6. get latest document + document := getLatestVersion(proj.ProjectIndividualDocuments) + if document == nil { + log.WithFields(f).Debugf("document not found for project: %s", projectID) + return nil, errors.New("document not found for project") + } + + aclUser := v1Models.User{} + + if returnURLType == "github" { + aclUser.GithubID = user.GithubID + } else if returnURLType == "gitlab" { + aclUser.GitlabID = user.GitlabID + } + + log.WithFields(f).Debugf("creating new signature object for user: %+v", aclUser) + + // 7. create new signature object + signature := &v1Models.Signature{ + SignatureID: uuid.Must(uuid.NewV4()).String(), + SignatureReferenceID: userID, + SignatureReferenceName: user.Username, + SignatureReferenceType: utils.SignatureReferenceTypeUser, + SignatureType: utils.SignatureTypeCLA, + SignatureReturnURL: strfmt.URI(returnURL), + SignatureReturnURLType: returnURLType, + ProjectID: projectID, + SignatureMajorVersion: document.DocumentMajorVersion, + SignatureMinorVersion: document.DocumentMinorVersion, + SignatureCreated: utils.TimeToString(time.Now()), + SignatureModified: utils.TimeToString(time.Now()), + SignatureCallbackURL: strfmt.URI(callBackURL), + SignatureSigned: false, + SignatureApproved: true, + SignatureACL: []v1Models.User{ + aclUser, + }, + } + + created, err := s.signatureService.CreateSignature(ctx, signature) if err != nil { - log.WithFields(f).WithError(err).Warn("unable to get access token for DocuSign") + log.WithFields(f).WithError(err).Warnf("unable to create signature by user id: %s", userID) return nil, err } - log.WithFields(f).Debugf("access token: %s", accessToken) + log.WithFields(f).Debugf("created signature: %+v", created) + + // 8. populate sign url + err = s.populateSignURL(ctx, created, callBackURL, "", "", false, "", "", defaultCLAValues, preferredEmail) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to populate sign url by user id: %s", userID) + return nil, err + } + + return &models.IndividualSignatureOutput{ + SignURL: created.SignatureSignURL, + SignatureID: created.SignatureID, + UserID: userID, + ProjectID: projectID, + }, nil + +} + +func (s service) populateSignURL(ctx context.Context, signature *v1Models.Signature, callbackURL, + authorityOrSignatoryName, authorityOrSignatoryEmail string, sendAsEmail bool, claManagerName, claManagerEmail string, + defaultValues IndividualValues, preferredEmail string) error { + + f := logrus.Fields{ + "functionName": "sign.populateSignURL", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "callbackURL": callbackURL, + "authorityOrSignatoryName": authorityOrSignatoryName, + "authorityOrSignatoryEmail": authorityOrSignatoryEmail, + "sendAsEmail": sendAsEmail, + "claManagerName": claManagerName, + "claManagerEmail": claManagerEmail, + "preferredEmail": preferredEmail, + } + + log.WithFields(f).Debugf("populateSignURL - signature: %+v", signature) + + userSignatureName := "Unknown" + userSignatureEmail := "Unknown" + signatureType := signature.SignatureReferenceType + + if signatureType == "company" { + userSignatureName = claManagerName + userSignatureEmail = claManagerEmail + log.WithFields(f).Debugf("company signature - userSignatureName: %s, userSignatureEmail: %s", userSignatureName, userSignatureEmail) + + //Grab the company id from the signature + company, err := s.companyRepo.GetCompany(ctx, signature.SignatureReferenceID) + if err != nil { + return err + } + log.WithFields(f).Debugf("loaded company: %+v", company) + } else if signatureType == "user" { + user, err := s.claUserService.GetUser(signature.SignatureReferenceID) + if err != nil { + return err + } + if user != nil { + userSignatureName = user.Username + userSignatureEmail = getUserEmail(user, preferredEmail) + } + log.WithFields(f).Debugf("user signature - userSignatureName: %s, userSignatureEmail: %s", userSignatureName, userSignatureEmail) + } else { + return errors.New("invalid signature type") + } + + // Fetch the document template to sign + claGroup, err := s.claGroupService.GetCLAGroup(ctx, signature.ProjectID) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s", signature.ProjectID) + return err + } + if claGroup == nil { + log.WithFields(f).WithError(err).Warnf("unable to lookup CLA Group by ID: %s", signature.ProjectID) + return errors.New("invalid CLA Group ID") + } + + var document *v1Models.ClaGroupDocument + + // Load the appropriate document template + if signatureType == "company" { + if len(claGroup.ProjectCorporateDocuments) == 0 { + log.WithFields(f).Warnf("company signature - missing corporate documents in the CLA Group configuration") + return errors.New("missing corporate documents in the CLA Group configuration") + } + document = getLatestVersion(claGroup.ProjectCorporateDocuments) + if document == nil { + log.WithFields(f).Debugf("company signature - document not found for project: %s", signature.ProjectID) + return errors.New("document not found for project") + } + log.WithFields(f).Debugf("company signature - document: %+v", document) + + } else if signatureType == "user" { + if len(claGroup.ProjectIndividualDocuments) == 0 { + log.WithFields(f).Warnf("user signature - missing individual documents in the CLA Group configuration") + return errors.New("missing individual documents in the CLA Group configuration") + } + document = getLatestVersion(claGroup.ProjectIndividualDocuments) + if document == nil { + log.WithFields(f).Debugf("user signature - document not found for project: %s", signature.ProjectID) + return errors.New("document not found for project") + } + log.WithFields(f).Debugf("user signature - document: %+v", document) + } + + // Void the existing envelope to prevent multiple envelopes from being created + envelopeID := signature.SignatureEnvelopeID + if envelopeID != "" { + msg := fmt.Sprintf("You are getting this message because youd Docusign session for project: %s expired. A new session has been created for you.", claGroup.ProjectName) + log.WithFields(f).Debug(msg) + // docusign void envelope + err = s.voidDocument(ctx, envelopeID, msg) + if err != nil { + return err + } + } + + return nil +} +func (s service) getSignatureCallbackURL(ctx context.Context, userID string, signatureMetadata *store.Data) (string, error) { + f := logrus.Fields{ + "functionName": "sign.getSignatureCallbackURL", + "userID": userID, + "signatureMetadata": signatureMetadata, + } + + var err error + if signatureMetadata == nil { + signatureMetadata, err = s.storeRepo.GetActiveSignatureMetaData(ctx, userID) + if err != nil { + return "", err + } + } + + if signatureMetadata == nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature metadata by user id: %s", userID) + return "", errors.New("signature metadata not found") + } + + // Get Github ID from metadata + repositoryID := signatureMetadata.RepositoryID + + // Get installationID + repository, err := s.repositoryService.GitHubGetRepositoryByExternalID(ctx, repositoryID) + if err != nil { + return "", err + } + + ghOrg, err := s.ghOrgService.GetGitHubOrganizationByName(ctx, repository.RepositoryOrganizationName) + if err != nil { + return "", err + } + + installationID := strconv.FormatInt(ghOrg.OrganizationInstallationID, 10) + pullRequestID := signatureMetadata.PullRequestID + + return path.Join(apiBasePath, "v2/signed/individual", installationID, repositoryID, pullRequestID), nil +} + +func (s service) getSignatureCallbackURLGitLab(ctx context.Context, userID string, signatureMetadata *store.Data) (string, error) { + f := logrus.Fields{ + "functionName": "sign.getSignatureCallbackURLGitLab", + "userID": userID, + "signatureMetadata": signatureMetadata, + } + + var err error + if signatureMetadata == nil { + signatureMetadata, err = s.storeRepo.GetActiveSignatureMetaData(ctx, userID) + if err != nil { + return "", err + } + } + + if signatureMetadata == nil { + log.WithFields(f).WithError(err).Warnf("unable to get active signature metadata by user id: %s", userID) + return "", errors.New("signature metadata not found") + } + + // format repositoryID to int64 + repositoryID, err := strconv.ParseInt(signatureMetadata.RepositoryID, 10, 64) + if err != nil { + log.WithFields(f).WithError(err).Warnf("unable to convert repositoryID to int: %s", signatureMetadata.RepositoryID) + return "", err + } + + gitlabRepository, err := s.repositoryService.GitLabGetRepositoryByExternalID(ctx, repositoryID) + if err != nil { + return "", err + } + + gitlabOrg, err := s.gitlabOrgService.GetGitLabOrganizationByName(ctx, gitlabRepository.RepositoryOrganizationName) + if err != nil { + return "", err + } + + if gitlabOrg.OrganizationID == "" { + return "", errors.New("gitlab organization not found") + } + + return path.Join(apiBasePath, "v2/signed/gitlab/individual", userID, gitlabOrg.OrganizationID, signatureMetadata.RepositoryID, signatureMetadata.MergeRequestID), nil + +} + +type IndividualValues struct { + FullName string + Email string + PublicName string +} + +func (s *service) RequestIndividualSignatureGerrit(ctx context.Context, projectID, userID, returnURL string) (*models.IndividualSignatureOutput, error) { return nil, nil +} + +func (s *service) createDefaultValues(ctx context.Context, user *v1Models.User, prefferedEmail string) IndividualValues { + individualValues := IndividualValues{} + if user == nil { + return individualValues + } + + if user.Username != "" { + individualValues.PublicName = user.Username + individualValues.FullName = user.Username + } + + email := getUserEmail(user, prefferedEmail) + if email != "" { + individualValues.Email = email + } + + return individualValues +} + +// getUserEmail returns the user email +func getUserEmail(user *v1Models.User, prefferedEmail string) string { + if user == nil { + return "" + } + + if prefferedEmail != "" && utils.StringInSlice(prefferedEmail, user.Emails) { + return prefferedEmail + } + + if user.LfEmail != "" { + return user.LfEmail.String() + } + + if len(user.Emails) > 0 { + return user.Emails[0] + } + return "" } func requestCorporateSignature(authToken string, apiURL string, input *requestCorporateSignatureInput) (*requestCorporateSignatureOutput, error) { diff --git a/cla-backend-go/v2/store/repository.go b/cla-backend-go/v2/store/repository.go index 1d58861d6..d96b76382 100644 --- a/cla-backend-go/v2/store/repository.go +++ b/cla-backend-go/v2/store/repository.go @@ -5,6 +5,7 @@ package store import ( "context" + "encoding/json" "fmt" "github.com/sirupsen/logrus" @@ -24,9 +25,19 @@ type DBStore struct { Expire int64 `dynamodbav:"expire"` } +type Data struct { + GithubAuthorUsername string `json:"github_author_username"` + GithubAuthorEmail string `json:"github_author_email"` + ClaGroupID string `json:"cla_group_id"` + RepositoryID string `json:"repository_id"` + PullRequestID string `json:"pull_request_id"` + MergeRequestID string `json:"merge_request_id"` +} + // Repository interface type Repository interface { SetActiveSignatureMetaData(ctx context.Context, key string, expire int64, value string) error + GetActiveSignatureMetaData(ctx context.Context, key string) (*Data, error) } type repo struct { @@ -85,3 +96,48 @@ func (r repo) SetActiveSignatureMetaData(ctx context.Context, key string, expire return nil } + +func (r repo) GetActiveSignatureMetaData(ctx context.Context, key string) (*Data, error) { + f := logrus.Fields{ + "functionName": "v2.store.repository.GetActiveSignatureMetaData", + utils.XREQUESTID: ctx.Value(utils.XREQUESTID), + "key": key, + } + + result, err := r.dynamoDBClient.GetItem(&dynamodb.GetItemInput{ + TableName: &r.storeTableName, + Key: map[string]*dynamodb.AttributeValue{ + "key": { + S: &key, + }, + }, + }) + + if err != nil { + log.WithFields(f).WithError(err).Warn("problem getting store record") + return nil, err + } + + if result.Item == nil { + log.WithFields(f).Debug("no store record found") + return nil, nil + } + + store := DBStore{} + err = dynamodbattribute.UnmarshalMap(result.Item, &store) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling store record") + return nil, err + } + + data := Data{} + err = json.Unmarshal([]byte(store.Value), &data) + if err != nil { + log.WithFields(f).WithError(err).Warn("problem unmarshalling store record value") + return nil, err + } + + log.WithFields(f).Debugf("Signature meta record data retrieved: %+v ", store) + + return &data, nil +}