Skip to content

Commit

Permalink
Merge pull request #4196 from nickmango/feature/docsign-webhook
Browse files Browse the repository at this point in the history
Feature/Docusign webhook
  • Loading branch information
nickmango authored Nov 29, 2023
2 parents 0ebaf1b + cd939f8 commit ac83188
Show file tree
Hide file tree
Showing 20 changed files with 1,038 additions and 60 deletions.
2 changes: 1 addition & 1 deletion cla-backend-go/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.ClaAPIV4Base, configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService, configFile.DocuSignPrivateKey, usersService, v1SignaturesService, storeRepository, v1RepositoriesService, githubOrganizationsService, gitlabOrganizationsService)
v2SignService := sign.NewService(configFile.ClaAPIV4Base, configFile.ClaV1ApiURL, v1CompanyRepo, v1CLAGroupRepo, v1ProjectClaGroupRepo, v1CompanyService, v2ClaGroupService, configFile.DocuSignPrivateKey, usersService, v1SignaturesService, storeRepository, v1RepositoriesService, githubOrganizationsService, gitlabOrganizationsService, configFile.CLALandingPage, configFile.CLALogoURL, emailService, eventsService)

sessionStore, err := dynastore.New(dynastore.Path("/"), dynastore.HTTPOnly(), dynastore.TableName(configFile.SessionStoreTableName), dynastore.DynamoDB(dynamodb.New(awsSession)))
if err != nil {
Expand Down
47 changes: 47 additions & 0 deletions cla-backend-go/emails/docusign_templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright The Linux Foundation and each contributor to CommunityBridge.
// SPDX-License-Identifier: MIT

package emails

type DocumentSignedTemplateParams struct {
CommonEmailParams
CLAGroupTemplateParams
ICLA bool
PdfLink string
}

const (
// DocumentSignedTemplateName is email template name for DocumentSignedTemplate
DocumentSignedTemplateName = "DocumentSignedTemplate"

// DocumentSignedTemplate is email template for
DocumentSignedICLATemplate = `
<p>Hello {{.RecipientName}},</p>
<p>This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.</p>
<p>The CLA has now been signed. You can download the signed CLA as a PDF <a href="{{.PdfLink}}" target="_blank" alt="ICLA Document Link">here</a>.</p>
`

DocumentSignedCCLATemplate = `
<p>Hello {{.RecipientName}},</p>
<p>This is a notification email from EasyCLA regarding the project {{.Project.ExternalProjectName}}.</p>
<p>The CLA has now been signed. You can download the signed CLA as a PDF <a href="{{.PdfLink}}" target="_blank" alt="CCLA Document Link">here</a>, or from within the <a href="{{.CorporateConsole}}" target="_blank"> EasyCLA CLA Manager console </a>.</p>
`
)

// RenderDocumentSignedTemplate renders RenderDocumentSignedTemplate
func RenderDocumentSignedTemplate(svc EmailTemplateService, claGroupModelVersion, projectSFID string, params DocumentSignedTemplateParams) (string, error) {
claGroupParams, err := svc.GetCLAGroupTemplateParamsFromProjectSFID(claGroupModelVersion, projectSFID)
if err != nil {
return "", err
}

params.CLAGroupTemplateParams = claGroupParams
var template string
if params.ICLA {
template = DocumentSignedICLATemplate
} else {
template = DocumentSignedCCLATemplate
}

return RenderTemplate(claGroupModelVersion, DocumentSignedTemplateName, template, params)
}
18 changes: 18 additions & 0 deletions cla-backend-go/events/event_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,12 @@ type SignatureAutoCreateECLAUpdatedEventData struct {
AutoCreateECLA bool
}

type IndividualSignatureSignedEventData struct {
ProjectName string
Username string
ProjectID string
}

// GetEventDetailsString returns the details string for this event
func (ed *SignatureAutoCreateECLAUpdatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) {

Expand Down Expand Up @@ -2725,3 +2731,15 @@ func (ed *SignatureAutoCreateECLAUpdatedEventData) GetEventSummaryString(args *L
data = data + "."
return data, false
}

func (ed *IndividualSignatureSignedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) {
data := fmt.Sprintf("The user %s signed an individual CLA for project %s with project ID: %s",
args.LfUsername, ed.ProjectName, ed.ProjectID)
return data, false
}

func (ed *IndividualSignatureSignedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) {
data := fmt.Sprintf("The user %s signed an individual CLA for project %s",
args.LfUsername, ed.ProjectName)
return data, false
}
2 changes: 2 additions & 0 deletions cla-backend-go/events/event_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,6 @@ const (
ProjectServiceCLAEnabled = "project.service.cla.enabled"
ProjectServiceCLADisabled = "project.service.cla.disabled"
SignatureAutoCreateECLAUpdated = "signature.auto_create_ecla.updated"

IndividualSignatureSigned = "individual.signature.signed"
)
1 change: 1 addition & 0 deletions cla-backend-go/signatures/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func (repo repository) buildProjectSignatureModels(ctx context.Context, results
go func() {
defer swg.Done()
for _, userName := range sigACL {
log.WithFields(f).Debugf("looking up user by user name: %s", userName)
if loadACLDetails {
userModel, userErr := repo.usersRepo.GetUserByUserName(userName, true)
if userErr != nil {
Expand Down
1 change: 1 addition & 0 deletions cla-backend-go/signatures/dbmodels.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ItemSignature struct {
UserDocusignName string `json:"user_docusign_name"`
UserDocusignDateSigned string `json:"user_docusign_date_signed"`
AutoCreateECLA bool `json:"auto_create_ecla"`
UserDocusignRawXML string `json:"user_docusign_raw_xml"`
}

// DBManagersModel is a database model for only the ACL/Manager column
Expand Down
2 changes: 1 addition & 1 deletion cla-backend-go/signatures/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ func Configure(api *operations.ClaAPI, service SignatureService, sessionStore *d
api.SignaturesGetUserSignaturesHandler = signatures.GetUserSignaturesHandlerFunc(func(params signatures.GetUserSignaturesParams, claUser *user.CLAUser) middleware.Responder {
reqID := utils.GetRequestID(params.XREQUESTID)
ctx := context.WithValue(context.Background(), utils.XREQUESTID, reqID) // nolint
userSignatures, err := service.GetUserSignatures(ctx, params)
userSignatures, err := service.GetUserSignatures(ctx, params, nil)
if err != nil {
log.Warnf("error retrieving user signatures for userID: %s, error: %+v", params.UserID, err)
return signatures.NewGetUserSignaturesBadRequest().WithXRequestID(reqID).WithPayload(errorResponse(err))
Expand Down
83 changes: 80 additions & 3 deletions cla-backend-go/signatures/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type SignatureRepository interface {
InvalidateProjectRecord(ctx context.Context, signatureID, note string) error
UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error)
CreateSignature(ctx context.Context, signature *ItemSignature) error
UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error

GetSignature(ctx context.Context, signatureID string) (*models.Signature, error)
GetActivePullRequestMetadata(ctx context.Context, gitHubAuthorUsername, gitHubAuthorEmail string) (*ActivePullRequest, error)
Expand All @@ -89,7 +90,7 @@ type SignatureRepository interface {
CreateProjectCompanyEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, employeeUserModel *models.User) error
GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams, pageSize int64, loadACL bool) (*models.Signatures, error)
GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]SignatureCompanyID, error)
GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64) (*models.Signatures, error)
GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64, projectID *string) (*models.Signatures, error)
ProjectSignatures(ctx context.Context, projectID string) (*models.Signatures, error)
UpdateApprovalList(ctx context.Context, claManager *models.User, claGroupModel *models.ClaGroup, companyID string, params *models.ApprovalList, eventArgs *events.LogEventArgs) (*models.Signature, error)
AddCLAManager(ctx context.Context, signatureID, claManagerID string) (*models.Signature, error)
Expand Down Expand Up @@ -166,6 +167,73 @@ func (repo repository) CreateSignature(ctx context.Context, signature *ItemSigna

}

// UpdateSignature updates an existing signature
func (repo repository) UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error {
f := logrus.Fields{
"functionName": "v1.signatures.repository.UpdateSignature",
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
"signatureID": signatureID,
}

if len(updates) == 0 {
log.WithFields(f).Warnf("no updates provided")
return errors.New("no updates provided")
}

var updateExpression strings.Builder
updateExpression.WriteString("SET ")
attributeValues := make(map[string]*dynamodb.AttributeValue)
expressionAttributeNames := make(map[string]*string)

count := 1
for attr, val := range updates {
attrPlaceholder := fmt.Sprintf("#A%d", count)
valPlaceholder := fmt.Sprintf(":v%d", count)

if count > 1 && count <= len(updates) {
updateExpression.WriteString(", ")
}
updateExpression.WriteString(fmt.Sprintf("%s = %s", attrPlaceholder, valPlaceholder))

expressionAttributeNames[attrPlaceholder] = aws.String(attr)
av, err := dynamodbattribute.Marshal(val)
if err != nil {
return err
}
attributeValues[valPlaceholder] = av

count++
}

log.WithFields(f).Debugf("updating signature using expression: %s", updateExpression.String())
log.WithFields(f).Debugf("expression attribute names : %+v", expressionAttributeNames)

input := &dynamodb.UpdateItemInput{
ExpressionAttributeValues: attributeValues,
TableName: aws.String(repo.signatureTableName),
Key: map[string]*dynamodb.AttributeValue{
"signature_id": {
S: aws.String(signatureID),
},
},
UpdateExpression: aws.String(updateExpression.String()),
ExpressionAttributeNames: expressionAttributeNames,
ReturnValues: aws.String("UPDATED_NEW"),
}

// perform the update
_, err := repo.dynamoDBClient.UpdateItem(input)
if err != nil {
log.WithFields(f).Warnf("error updating signature, error: %v", err)
return err
}

log.WithFields(f).Debugf("successfully updated signature")

return 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{
Expand Down Expand Up @@ -2659,16 +2727,23 @@ func (repo repository) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Co
}

// GetUserSignatures returns a list of user signatures for the specified user
func (repo repository) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64) (*models.Signatures, error) {
func (repo repository) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, pageSize int64, projectID *string) (*models.Signatures, error) {
f := logrus.Fields{
"functionName": "v1.signatures.repository.GetUserSignatures",
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
}
// This is the keys we want to match
condition := expression.Key("signature_reference_id").Equal(expression.Value(params.UserID))

expressionBuilder := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection())

if projectID != nil {
filterExpression := expression.Name("signature_project_id").Equal(expression.Value(*projectID))
expressionBuilder = expressionBuilder.WithFilter(filterExpression)
}

// Use the nice builder to create the expression
expr, err := expression.NewBuilder().WithKeyCondition(condition).WithProjection(buildProjection()).Build()
expr, err := expressionBuilder.Build()
if err != nil {
log.WithFields(f).Warnf("error building expression for user signature query, userID: %s, error: %v",
params.UserID, err)
Expand Down Expand Up @@ -2715,6 +2790,8 @@ func (repo repository) GetUserSignatures(ctx context.Context, params signatures.
return nil, errQuery
}

log.WithFields(f).Debugf("query results count: %d", len(results.Items))

// Convert the list of DB models to a list of response models
signatureList, modelErr := repo.buildProjectSignatureModels(ctx, results, "", LoadACLDetails)
if modelErr != nil {
Expand Down
17 changes: 12 additions & 5 deletions cla-backend-go/signatures/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ type SignatureService interface {
GetProjectCompanyEmployeeSignatures(ctx context.Context, params signatures.GetProjectCompanyEmployeeSignaturesParams, criteria *ApprovalCriteria) (*models.Signatures, error)
GetCompanySignatures(ctx context.Context, params signatures.GetCompanySignaturesParams) (*models.Signatures, error)
GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context, claGroupID string) ([]SignatureCompanyID, error)
GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams) (*models.Signatures, error)
GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, projectID *string) (*models.Signatures, error)
InvalidateProjectRecords(ctx context.Context, projectID, note string) (int, error)
CreateSignature(ctx context.Context, signature *ItemSignature) error
UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) 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)
Expand All @@ -72,6 +73,7 @@ type SignatureService interface {
CreateOrUpdateEmployeeSignature(ctx context.Context, claGroupModel *models.ClaGroup, companyModel *models.Company, corporateSignatureModel *models.Signature) ([]*models.User, error)
UpdateEnvelopeDetails(ctx context.Context, signatureID, envelopeID string, signURL *string) (*models.Signature, error)
handleGitHubStatusUpdate(ctx context.Context, employeeUserModel *models.User) error
ProcessEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, user *models.User) (*bool, error)
}

type service struct {
Expand Down Expand Up @@ -112,6 +114,11 @@ func (s service) GetSignature(ctx context.Context, signatureID string) (*models.
return s.repo.GetSignature(ctx, signatureID)
}

// UpdateSignature updates the specified signature
func (s service) UpdateSignature(ctx context.Context, signatureID string, updates map[string]interface{}) error {
return s.repo.UpdateSignature(ctx, signatureID, updates)
}

// GetIndividualSignature returns the signature associated with the specified CLA Group and User ID
func (s service) GetIndividualSignature(ctx context.Context, claGroupID, userID string, approved, signed *bool) (*models.Signature, error) {
return s.repo.GetIndividualSignature(ctx, claGroupID, userID, approved, signed)
Expand Down Expand Up @@ -228,15 +235,15 @@ func (s service) GetCompanyIDsWithSignedCorporateSignatures(ctx context.Context,
}

// GetUserSignatures returns the list of user signatures associated with the specified user
func (s service) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams) (*models.Signatures, error) {
func (s service) GetUserSignatures(ctx context.Context, params signatures.GetUserSignaturesParams, projectID *string) (*models.Signatures, error) {

const defaultPageSize int64 = 10
var pageSize = defaultPageSize
if params.PageSize != nil {
pageSize = *params.PageSize
}

userSignatures, err := s.repo.GetUserSignatures(ctx, params, pageSize)
userSignatures, err := s.repo.GetUserSignatures(ctx, params, pageSize, projectID)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1160,7 +1167,7 @@ func (s service) hasUserSigned(ctx context.Context, user *models.User, projectID
return &hasSigned, &companyAffiliation, claGroupModelErr
}

employeeSigned, err := s.processEmployeeSignature(ctx, companyModel, claGroupModel, user)
employeeSigned, err := s.ProcessEmployeeSignature(ctx, companyModel, claGroupModel, user)

if err != nil {
log.WithFields(f).WithError(err).Warnf("problem looking up employee signature for company: %s", companyID)
Expand All @@ -1177,7 +1184,7 @@ func (s service) hasUserSigned(ctx context.Context, user *models.User, projectID
return &hasSigned, &companyAffiliation, nil
}

func (s service) processEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, user *models.User) (*bool, error) {
func (s service) ProcessEmployeeSignature(ctx context.Context, companyModel *models.Company, claGroupModel *models.ClaGroup, user *models.User) (*bool, error) {
f := logrus.Fields{
"functionName": "v2.signatures.service.processEmployeeSignature",
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
Expand Down
Loading

0 comments on commit ac83188

Please sign in to comment.