Skip to content
Merged
3 changes: 2 additions & 1 deletion cla-backend-go/approval_list/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package approval_list
import (
"errors"
"fmt"
"strings"

models2 "github.com/linuxfoundation/easycla/cla-backend-go/project/models"

Expand Down Expand Up @@ -86,7 +87,7 @@ func (repo repository) AddCclaApprovalRequest(company *models.Company, project *
addStringAttribute(input.Item, "project_id", project.ProjectID)
addStringAttribute(input.Item, "project_name", project.ProjectName)
addStringAttribute(input.Item, "user_id", user.UserID)
addStringSliceAttribute(input.Item, "user_emails", []string{requesterEmail})
addStringSliceAttribute(input.Item, "user_emails", []string{strings.ToLower(strings.TrimSpace(requesterEmail))})
addStringAttribute(input.Item, "user_name", requesterName)
Comment thread
lukaszgryglicki marked this conversation as resolved.
addStringAttribute(input.Item, "user_github_id", user.GithubID)
addStringAttribute(input.Item, "user_github_username", user.GithubUsername)
Expand Down
2 changes: 1 addition & 1 deletion cla-backend-go/signatures/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -3345,7 +3345,7 @@ func (repo repository) UpdateApprovalList(ctx context.Context, claManager *model
var iclas []*models.IclaSignature
var eclas []*models.Signature
Comment thread
lukaszgryglicki marked this conversation as resolved.
log.WithFields(f).Debugf("getting cla user record for email: %s ", email)
userSearch, userErr := repo.usersRepo.SearchUsers("user_emails", email, false)
userSearch, userErr := repo.usersRepo.SearchUsers("user_emails", strings.ToLower(strings.TrimSpace(email)), false)
if userErr != nil || userSearch == nil {
log.WithFields(f).Debugf("error getting user by email: %s ", email)
return
Comment thread
lukaszgryglicki marked this conversation as resolved.
Expand Down
46 changes: 41 additions & 5 deletions cla-backend-go/users/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ func (repo repository) CreateUser(user *models.User) (*models.User, error) {
}
}

if len(user.Emails) > 0 {
if normalized := normalizeEmails(user.Emails); len(normalized) > 0 {
attributes["user_emails"] = &dynamodb.AttributeValue{
SS: utils.ArrayStringPointer(user.Emails),
SS: utils.ArrayStringPointer(normalized),
}
}
Comment thread
lukaszgryglicki marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -386,9 +386,15 @@ func (repo repository) Save(user *models.UserUpdate) (*models.User, error) {
}

if user.Emails != nil {
log.WithFields(f).Debugf("building query - adding user_emails: %v", user.Emails)
// Preserve the prior nil-vs-set semantics: a nil slice leaves user_emails
// untouched, while a non-nil slice writes it (normalized to lower-case). An
// explicitly empty/blank slice still produces an empty String Set, which
// DynamoDB rejects — surfacing an error to the caller rather than silently
// dropping the update.
normalized := normalizeEmails(user.Emails)
log.WithFields(f).Debugf("building query - adding user_emails: %v", normalized)
expressionAttributeNames["#UES"] = aws.String("user_emails")
expressionAttributeValues[":ues"] = &dynamodb.AttributeValue{SS: aws.StringSlice(user.Emails)}
expressionAttributeValues[":ues"] = &dynamodb.AttributeValue{SS: aws.StringSlice(normalized)}
Comment thread
lukaszgryglicki marked this conversation as resolved.
updateExpression = updateExpression + " #UES = :ues, "
Comment thread
lukaszgryglicki marked this conversation as resolved.
}
Comment thread
lukaszgryglicki marked this conversation as resolved.

Expand Down Expand Up @@ -808,6 +814,15 @@ func (repo repository) GetUsersByEmail(userEmail string) ([]*models.User, error)
"userEmail": userEmail,
}

// user_emails are stored lower-cased (as lf_email is), so look up with a lower-cased
// address regardless of how the caller cased it.
userEmail = strings.ToLower(strings.TrimSpace(userEmail))
if userEmail == "" {
// Nothing can match an empty address, and an empty value in a DynamoDB
// expression raises a ValidationException — return no results instead.
return []*models.User{}, nil
}

// This is the filter we want to match
filter := expression.Name("user_emails").Contains(userEmail)

Expand All @@ -817,7 +832,7 @@ func (repo repository) GetUsersByEmail(userEmail string) ([]*models.User, error)
// Use the nice builder to create the expression
expr, err := expression.NewBuilder().WithFilter(filter).WithProjection(projection).Build()
if err != nil {
log.WithFields(f).Warnf("error building expression for lf_email : %s, error: %v", userEmail, err)
log.WithFields(f).Warnf("error building expression for user_emails : %s, error: %v", userEmail, err)
return nil, err
}

Expand Down Expand Up @@ -883,6 +898,27 @@ func (repo repository) GetUsersByEmail(userEmail string) ([]*models.User, error)
return users, nil
}

// normalizeEmails lower-cases and trims each address and drops empties/duplicates. Emails
// are stored normalized (so lookups by a lower-cased address match) and de-duplication is
// required because a DynamoDB String Set rejects duplicate members (which lower-casing can
// otherwise introduce, e.g. "A@x.com" and "a@x.com").
func normalizeEmails(emails []string) []string {
seen := make(map[string]struct{}, len(emails))
out := make([]string, 0, len(emails))
for _, email := range emails {
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
continue
}
if _, ok := seen[email]; ok {
continue
}
seen[email] = struct{}{}
out = append(out, email)
}
return out
}

// GetUsersByLFEmail fetches the user record by email
func (repo repository) GetUsersByLFEmail(userEmail string) ([]*models.User, error) {
f := logrus.Fields{
Expand Down
4 changes: 2 additions & 2 deletions cla-backend-legacy/internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2338,7 +2338,7 @@ func (h *Handlers) InviteCompanyAdminV2(w http.ResponseWriter, r *http.Request)
"project_name": &types.AttributeValueMemberS{Value: projectName},
"user_github_id": &types.AttributeValueMemberS{Value: contributorID},
"user_github_username": &types.AttributeValueMemberS{Value: contributorName},
"user_emails": &types.AttributeValueMemberSS{Value: []string{contributorEmail}},
"user_emails": &types.AttributeValueMemberSS{Value: []string{strings.ToLower(strings.TrimSpace(contributorEmail))}},
"request_status": &types.AttributeValueMemberS{Value: "pending"},
"date_created": &types.AttributeValueMemberS{Value: now},
"date_modified": &types.AttributeValueMemberS{Value: now},
Expand Down Expand Up @@ -2525,7 +2525,7 @@ func (h *Handlers) RequestCompanyCclaV2(w http.ResponseWriter, r *http.Request)
"request_id": &types.AttributeValueMemberS{Value: reqID},
"company_name": &types.AttributeValueMemberS{Value: companyName},
"project_name": &types.AttributeValueMemberS{Value: projectName},
"user_emails": &types.AttributeValueMemberSS{Value: []string{userEmail}},
"user_emails": &types.AttributeValueMemberSS{Value: []string{strings.ToLower(strings.TrimSpace(userEmail))}},
"request_status": &types.AttributeValueMemberS{Value: "pending"},
"date_created": &types.AttributeValueMemberS{Value: now},
"date_modified": &types.AttributeValueMemberS{Value: now},
Expand Down
28 changes: 28 additions & 0 deletions utils/downcase_emails.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

STAGE=${STAGE:-dev}
PROFILE="lfproduct-${STAGE}"
REGION=us-east-1
TABLE="cla-${STAGE}-users"
APPLY="${APPLY:-0}"

aws dynamodb scan --profile "$PROFILE" --region "$REGION" --table-name "$TABLE" --projection-expression 'user_id, user_emails' --filter-expression 'attribute_exists(user_emails)' --output json > "${STAGE}_user_emails.json"
cat "${STAGE}_user_emails.json" | jq -c '.Items[] | select(.user_emails.SS != null) | select(([.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique) != (.user_emails.SS | sort))' \
| while IFS= read -r item; do
Comment thread
lukaszgryglicki marked this conversation as resolved.
uid=$(jq -r '.user_id.S' <<<"$item")
newss=$(jq -c '[.user_emails.SS[] | ascii_downcase | gsub("^\\s+|\\s+$";"") | select(length > 0)] | unique' <<<"$item") # lower + trim + drop-empty + dedupe
if [ "$newss" = "[]" ]
Comment thread
lukaszgryglicki marked this conversation as resolved.
Outdated
then
echo "skip $uid (no valid emails after normalize)" >&2
continue
fi
echo "user $uid -> $newss"
if [ "$APPLY" = "1" ]
then
aws dynamodb update-item --profile "$PROFILE" --region "$REGION" --table-name "$TABLE" \
--key "{\"user_id\":{\"S\":\"$uid\"}}" \
--update-expression 'SET user_emails = :e' \
--expression-attribute-values "{\":e\":{\"SS\":$newss}}" && echo "ok"
fi
done
Loading