Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion cla-backend-go/emails/contact_cla_manager_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,32 @@ type ContactClaManagerTemplateParams struct {
RequestAction string
ContributorName string
ContributorIdentity string
ContributorEmail string
CompanyName string
ProjectName string
CLAGroupName string
OptionalMessage string
ContactOnly bool
}

const (
// ContactClaManagerTemplateName is email template name for ContactClaManagerTemplate
ContactClaManagerTemplateName = "ContactClaManagerTemplate"
// ContactClaManagerTemplate is the email sent to the selected CLA managers when a
// contributor requests removal from or (re-)approval under the company CCLA
// contributor requests removal/approval or sends a contact-only message
ContactClaManagerTemplate = `
<p>Hello CLA Manager,</p>
<p>This is a notification email from EasyCLA regarding the project {{.ProjectName}} and CLA Group {{.CLAGroupName}}.</p>
{{if .ContactOnly}}
<p>{{.ContributorName}} ({{.ContributorIdentity}}) has sent you a message about their employee acknowledgement
under the {{.CompanyName}} corporate CLA. You are receiving this message as a CLA Manager from {{.CompanyName}} for {{.ProjectName}}.</p>
<p>The contributor's message:</p>
<blockquote style="white-space: pre-wrap;">{{.OptionalMessage}}</blockquote>
{{if .ContributorEmail}}
<p>You can reply to the contributor at {{.ContributorEmail}}.</p>
{{end}}
<p>This is a message only - no change was requested and none has been made.</p>
{{else}}
<p>{{.ContributorName}} ({{.ContributorIdentity}}) has requested {{.RequestAction}} for their employee acknowledgement
under the {{.CompanyName}} corporate CLA. You are receiving this message as a CLA Manager from {{.CompanyName}} for {{.ProjectName}}.</p>
{{if .OptionalMessage}}
Expand All @@ -34,6 +46,7 @@ under the {{.CompanyName}} corporate CLA. You are receiving this message as a CL
{{end}}
<p>To act on this request, please log into the EasyCLA Corporate Console and update the Approved List for {{.CompanyName}} accordingly.
No change has been made automatically.</p>
{{end}}
`
)

Expand Down
52 changes: 52 additions & 0 deletions cla-backend-go/events/event_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ type GitHubProjectDeletedEventData struct {
// SignatureProjectInvalidatedEventData data model
type SignatureProjectInvalidatedEventData struct {
InvalidatedCount int
SignatureID string
InvalidatedBy string
Reason string
InvalidationNote string
}

// SignatureInvalidatedApprovalRejectionEventData data model
Expand Down Expand Up @@ -173,6 +177,9 @@ type CompanyACLUserAddedEventData struct {
UserLFID string
}

// CompanySanctionedEventData data model
type CompanySanctionedEventData struct{}

// CLATemplateCreatedEventData data model
type CLATemplateCreatedEventData struct {
TemplateName string
Expand Down Expand Up @@ -793,6 +800,13 @@ func (ed *CompanyACLUserAddedEventData) GetEventDetailsString(args *LogEventArgs
return data, true
}

// GetEventDetailsString returns the details string for this event
func (ed *CompanySanctionedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) {
data := fmt.Sprintf("The company %s was flagged as sanctioned by sanctions screening", args.CompanyName)
data = data + "."
return data, true
}

// GetEventDetailsString returns the details string for this event
func (ed *CLATemplateCreatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) {
data := "A CLA Group template was created or updated" // nolint
Expand Down Expand Up @@ -1475,6 +1489,9 @@ func (ed *GitHubProjectDeletedEventData) GetEventDetailsString(args *LogEventArg

// GetEventDetailsString returns the details string for this event
func (ed *SignatureProjectInvalidatedEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) {
if ed.SignatureID != "" {
return ed.singleSignatureText(args, true), true
}
data := fmt.Sprintf("%d Signatures were invalidated (approved set to false) due to CLA Group/Project: %s deletion",
ed.InvalidatedCount, args.ProjectName)
if args.UserName != "" {
Expand All @@ -1484,6 +1501,32 @@ func (ed *SignatureProjectInvalidatedEventData) GetEventDetailsString(args *LogE
return data, true
}

// singleSignatureText renders the admin ICLA invalidation wording (SignatureID set) shared by
// the details and summary strings
func (ed *SignatureProjectInvalidatedEventData) singleSignatureText(args *LogEventArgs, capitalized bool) string {
lead := "the signature"
if capitalized {
lead = "The signature"
}
data := fmt.Sprintf("%s %s was invalidated (approved set to false)", lead, ed.SignatureID)
if args.UserName != "" {
data = data + fmt.Sprintf(" for the user %s", args.UserName)
}
if args.ProjectName != "" {
data = data + fmt.Sprintf(" for the project %s", args.ProjectName)
}
if ed.InvalidatedBy != "" {
data = data + fmt.Sprintf(" by the administrator %s", ed.InvalidatedBy)
}
if ed.Reason != "" {
data = data + fmt.Sprintf(", reason: %s", ed.Reason)
}
if ed.InvalidationNote != "" {
data = data + fmt.Sprintf(", note: %s", ed.InvalidationNote)
}
return data + "."
}

// GetEventDetailsString returns the details string for this event
func (ed *SignatureInvalidatedApprovalRejectionEventData) GetEventDetailsString(args *LogEventArgs) (string, bool) {
reason := noReason
Expand Down Expand Up @@ -1904,6 +1947,12 @@ func (ed *CompanyACLUserAddedEventData) GetEventSummaryString(args *LogEventArgs
return data, true
}

// GetEventSummaryString returns the summary string for this event
func (ed *CompanySanctionedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) {
data := fmt.Sprintf("The company %s was flagged as sanctioned by sanctions screening.", args.CompanyName)
return data, true
}

// GetEventSummaryString returns the summary string for this event
func (ed *CLATemplateCreatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) {
// Same output as the details
Expand Down Expand Up @@ -2651,6 +2700,9 @@ func (ed *GitHubProjectDeletedEventData) GetEventSummaryString(args *LogEventArg

// GetEventSummaryString returns the summary string for this event
func (ed *SignatureProjectInvalidatedEventData) GetEventSummaryString(args *LogEventArgs) (string, bool) {
if ed.SignatureID != "" {
return ed.singleSignatureText(args, false), true
}
data := fmt.Sprintf("%d signatures were invalidated (approved set to false) due to CLA Group/Project %s deletion",
ed.InvalidatedCount, args.ProjectName)
if args.CLAGroupName != "" {
Expand Down
37 changes: 37 additions & 0 deletions cla-backend-go/events/event_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,40 @@ func TestContactCLAManagerRequestCreatedEventData(t *testing.T) {
summary, _ = eventData.GetEventSummaryString(args)
assert.Contains(t, summary, "with a message")
}

func TestSignatureProjectInvalidatedEventDataSingleSignature(t *testing.T) {
bulk := &SignatureProjectInvalidatedEventData{InvalidatedCount: 3}
args := &LogEventArgs{UserName: testUser, ProjectName: "My Project"}

details, containsPII := bulk.GetEventDetailsString(args)
assert.True(t, containsPII)
assert.Contains(t, details, "3 Signatures were invalidated (approved set to false) due to CLA Group/Project: My Project deletion")

single := &SignatureProjectInvalidatedEventData{
SignatureID: "sig-1",
InvalidatedBy: "admin-user",
Reason: "compliance",
InvalidationNote: "per legal review",
}
details, containsPII = single.GetEventDetailsString(args)
assert.True(t, containsPII)
assert.Equal(t, "The signature sig-1 was invalidated (approved set to false) for the user john for the project My Project by the administrator admin-user, reason: compliance, note: per legal review.", details)
summary, _ := single.GetEventSummaryString(args)
assert.Equal(t, "the signature sig-1 was invalidated (approved set to false) for the user john for the project My Project by the administrator admin-user, reason: compliance, note: per legal review.", summary)

bare := &SignatureProjectInvalidatedEventData{SignatureID: "sig-2"}
details, _ = bare.GetEventDetailsString(&LogEventArgs{})
assert.Equal(t, "The signature sig-2 was invalidated (approved set to false).", details)
}

func TestCompanySanctionedEventData(t *testing.T) {
eventData := &CompanySanctionedEventData{}
args := &LogEventArgs{CompanyName: "Flagged Corp"}

details, containsPII := eventData.GetEventDetailsString(args)
assert.True(t, containsPII)
assert.Equal(t, "The company Flagged Corp was flagged as sanctioned by sanctions screening.", details)
summary, containsPII := eventData.GetEventSummaryString(args)
assert.True(t, containsPII)
assert.Equal(t, "The company Flagged Corp was flagged as sanctioned by sanctions screening.", summary)
}
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 @@ -59,6 +59,8 @@ const (
CompanyACLRequestApproved = "company_acl.request_approved"
CompanyACLRequestDenied = "company_acl.request_denied"

CompanySanctioned = "company.sanctioned"

ContactCLAManagerRequestCreated = "contact_cla_manager_request.created"

CCLAApprovalListRequestCreated = "ccla_approval_list_request.created"
Expand Down
4 changes: 4 additions & 0 deletions cla-backend-go/signatures/dbmodels.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ type ItemSignature struct {
UserDocusignDateSigned string `json:"user_docusign_date_signed,omitempty"`
AutoCreateECLA bool `json:"auto_create_ecla,omitempty"`
UserDocusignRawXML string `json:"user_docusign_raw_xml,omitempty"`
DateInvalidated string `json:"date_invalidated,omitempty"`
InvalidatedBy string `json:"invalidated_by,omitempty"`
InvalidationReason string `json:"invalidation_reason,omitempty"`
InvalidationNote string `json:"invalidation_note,omitempty"`
}

// DBManagersModel is a database model for only the ACL/Manager column
Expand Down
15 changes: 15 additions & 0 deletions cla-backend-go/signatures/mocks/mock_repo.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cla-backend-go/signatures/mocks/mock_service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 91 additions & 0 deletions cla-backend-go/signatures/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type SignatureRepository interface {
DeleteGithubOrganizationFromApprovalList(ctx context.Context, signatureID, githubOrganizationID string) ([]models.GithubOrg, error)
ValidateProjectRecord(ctx context.Context, signatureID, note string) error
InvalidateProjectRecord(ctx context.Context, signatureID, note string) error
InvalidateProjectRecordWithMetadata(ctx context.Context, signatureID, note string, metadata *InvalidationMetadata) 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
Expand Down Expand Up @@ -2085,6 +2086,13 @@ func (repo repository) ProjectSignatures(ctx context.Context, projectID string)
}, nil
}

// InvalidationMetadata carries the invalidation attribution stored on the signature record
type InvalidationMetadata struct {
InvalidatedBy string
Reason string
Note string
}

// InvalidateProjectRecord invalidates the specified project record by setting the signature_approved flag to false
func (repo repository) InvalidateProjectRecord(ctx context.Context, signatureID, note string) error {
f := logrus.Fields{
Expand Down Expand Up @@ -2129,6 +2137,89 @@ func (repo repository) InvalidateProjectRecord(ctx context.Context, signatureID,
return nil
}

// InvalidateProjectRecordWithMetadata invalidates the specified project record by setting the
// signature_approved flag to false and records the invalidation attribution. The attribution
// attributes (date_invalidated, invalidated_by, invalidation_reason, invalidation_note) are
// first-write-wins so a re-invalidation never destroys the record of a prior invalidation;
// attributes missing on pre-feature records are still populated.
func (repo repository) InvalidateProjectRecordWithMetadata(ctx context.Context, signatureID, note string, metadata *InvalidationMetadata) error {
f := logrus.Fields{
"functionName": "v1.signatures.repository.InvalidateProjectRecordWithMetadata",
utils.XREQUESTID: ctx.Value(utils.XREQUESTID),
"signatureID": signatureID,
}

signatureTableName := fmt.Sprintf("cla-%s-signatures", repo.stage)

_, now := utils.CurrentTime()

expressionAttributeNames, expressionAttributeValues, updateExpression := invalidationUpdateExpression(note, now, metadata)

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

_, updateErr := repo.dynamoDBClient.UpdateItem(input)
if updateErr != nil {
log.WithFields(f).Warnf("error updating signature_approved for signature_id : %s error : %v ", signatureID, updateErr)
return updateErr
}

return nil
}

// invalidationUpdateExpression assembles the invalidation update: approval revoked, note replaced,
// every attribution attribute first-write-wins via if_not_exists, date_modified refreshed.
func invalidationUpdateExpression(note, now string, metadata *InvalidationMetadata) (map[string]*string, map[string]*dynamodb.AttributeValue, string) {
expressionAttributeNames := map[string]*string{}
expressionAttributeValues := map[string]*dynamodb.AttributeValue{}
updateExpression := "SET " // nolint

expressionAttributeNames["#A"] = aws.String("signature_approved")
expressionAttributeValues[":a"] = &dynamodb.AttributeValue{BOOL: aws.Bool(false)}
updateExpression = updateExpression + " #A = :a,"

expressionAttributeNames["#S"] = aws.String("note")
expressionAttributeValues[":s"] = &dynamodb.AttributeValue{S: aws.String(note)}
updateExpression = updateExpression + " #S = :s,"

expressionAttributeNames["#DI"] = aws.String("date_invalidated")
expressionAttributeValues[":di"] = &dynamodb.AttributeValue{S: aws.String(now)}
updateExpression = updateExpression + " #DI = if_not_exists(#DI, :di),"

if metadata != nil {
if metadata.InvalidatedBy != "" {
expressionAttributeNames["#IB"] = aws.String("invalidated_by")
expressionAttributeValues[":ib"] = &dynamodb.AttributeValue{S: aws.String(metadata.InvalidatedBy)}
updateExpression = updateExpression + " #IB = if_not_exists(#IB, :ib),"
}
if metadata.Reason != "" {
expressionAttributeNames["#IR"] = aws.String("invalidation_reason")
expressionAttributeValues[":ir"] = &dynamodb.AttributeValue{S: aws.String(metadata.Reason)}
updateExpression = updateExpression + " #IR = if_not_exists(#IR, :ir),"
}
if metadata.Note != "" {
expressionAttributeNames["#IN"] = aws.String("invalidation_note")
expressionAttributeValues[":in"] = &dynamodb.AttributeValue{S: aws.String(metadata.Note)}
updateExpression = updateExpression + " #IN = if_not_exists(#IN, :in),"
}
}
Comment thread
lukaszgryglicki marked this conversation as resolved.

expressionAttributeNames["#M"] = aws.String("date_modified")
expressionAttributeValues[":m"] = &dynamodb.AttributeValue{S: aws.String(now)}
updateExpression = updateExpression + " #M = :m"

return expressionAttributeNames, expressionAttributeValues, updateExpression
}

// ValidateProjectRecord validates the specified project record by setting the signature_approved flag to true
func (repo repository) ValidateProjectRecord(ctx context.Context, signatureID, note string) error {
f := logrus.Fields{
Expand Down
Loading
Loading