diff --git a/pkg/coredata/identity.go b/pkg/coredata/identity.go index 97345814c..d3c6e987c 100644 --- a/pkg/coredata/identity.go +++ b/pkg/coredata/identity.go @@ -407,7 +407,7 @@ SELECT FROM identities WHERE - id = ANY(@identity_ids::text[]) + id = ANY(@identity_ids) ` args := pgx.StrictNamedArgs{"identity_ids": identityIDs} diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index 49854d563..c1919552a 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -507,7 +507,7 @@ SELECT p.id, p.identity_id, p.organization_id, - i.email_address, + ''::citext AS email_address, p.source, p.state, p.full_name, @@ -540,8 +540,6 @@ SELECT p.updated_at FROM iam_membership_profiles p -INNER JOIN identities i - ON i.id = p.identity_id WHERE p.%s AND p.organization_id = @organization_id diff --git a/pkg/coredata/personal_api_key.go b/pkg/coredata/personal_api_key.go index 6b863b385..ace5855b3 100644 --- a/pkg/coredata/personal_api_key.go +++ b/pkg/coredata/personal_api_key.go @@ -320,7 +320,7 @@ SELECT FROM iam_personal_api_keys WHERE - id = ANY(@api_key_ids::text[]) + id = ANY(@api_key_ids) ` args := pgx.StrictNamedArgs{"api_key_ids": apiKeyIDs} diff --git a/pkg/iam/log_export_csv.go b/pkg/iam/log_export_csv.go index bfcd63bd1..c650201d6 100644 --- a/pkg/iam/log_export_csv.go +++ b/pkg/iam/log_export_csv.go @@ -31,6 +31,7 @@ import ( "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" ) @@ -206,7 +207,7 @@ func auditLogEntryCSVRow( entry *coredata.AuditLogEntry, actor auditLogActorExportInfo, ) []string { - return []string{ + return csvExportRow( organizationName, entry.ID.String(), entry.CreatedAt.Format(time.RFC3339), @@ -217,7 +218,7 @@ func auditLogEntryCSVRow( entry.Action, entry.ResourceType, entry.ResourceID.String(), - } + ) } func scimEventCSVRow( @@ -227,19 +228,24 @@ func scimEventCSVRow( ) []string { profile := profilesByUserName[strings.ToLower(event.UserName)] - return []string{ + email := profile.email + if email == "" { + email = scimEmailFromUserName(event.UserName) + } + + return csvExportRow( organizationName, event.ID.String(), event.CreatedAt.Format(time.RFC3339), event.Method, event.Path, event.UserName, - profile.email, + email, profile.fullName, strconv.Itoa(event.StatusCode), stringPtrValue(event.ErrorMessage), event.IPAddress.String(), - } + ) } func loadAuditLogActorExportInfo( @@ -312,6 +318,21 @@ func loadSCIMProfileExportInfo( return nil, fmt.Errorf("cannot load SCIM profile export info: %w", err) } + identityIDs := make([]gid.GID, 0, len(profiles)) + for _, profile := range profiles { + identityIDs = append(identityIDs, profile.IdentityID) + } + + var identities coredata.Identities + if err := identities.LoadByIDs(ctx, conn, identityIDs); err != nil { + return nil, fmt.Errorf("cannot load SCIM profile identity emails: %w", err) + } + + emailByIdentityID := make(map[gid.GID]string, len(identities)) + for _, identity := range identities { + emailByIdentityID[identity.ID] = identity.EmailAddress.String() + } + result := make(map[string]scimProfileExportInfo, len(profiles)) for _, profile := range profiles { if profile.UserName == nil { @@ -320,7 +341,7 @@ func loadSCIMProfileExportInfo( key := strings.ToLower(*profile.UserName) result[key] = scimProfileExportInfo{ - email: profile.EmailAddress.String(), + email: emailByIdentityID[profile.IdentityID], fullName: profileFullName(profile), } } @@ -360,6 +381,41 @@ func uniqueNonEmptyStrings(values []string) []string { return out } +func csvExportRow(fields ...string) []string { + row := make([]string, len(fields)) + for i, field := range fields { + row[i] = csvSafeCell(field) + } + + return row +} + +func csvSafeCell(value string) string { + if value == "" { + return value + } + + switch value[0] { + case '=', '+', '-', '@', '\t', '\r': + return "'" + value + default: + return value + } +} + +func scimEmailFromUserName(userName string) string { + userName = strings.TrimSpace(userName) + if userName == "" { + return "" + } + + if _, err := mail.ParseAddr(userName); err == nil { + return userName + } + + return "" +} + func profileFullName(profile *coredata.MembershipProfile) string { if profile.FormattedName != nil && *profile.FormattedName != "" { return *profile.FormattedName diff --git a/pkg/iam/log_export_csv_test.go b/pkg/iam/log_export_csv_test.go index 303061350..0dde378f5 100644 --- a/pkg/iam/log_export_csv_test.go +++ b/pkg/iam/log_export_csv_test.go @@ -32,3 +32,21 @@ func TestUniqueNonEmptyStrings(t *testing.T) { got := uniqueNonEmptyStrings([]string{"a", "A", "", "b", "a"}) assert.Equal(t, []string{"a", "b"}, got) } + +func TestCsvSafeCell(t *testing.T) { + t.Parallel() + + assert.Equal(t, "plain", csvSafeCell("plain")) + assert.Equal(t, "'=1+1", csvSafeCell("=1+1")) + assert.Equal(t, "'+cmd", csvSafeCell("+cmd")) + assert.Equal(t, "'-2", csvSafeCell("-2")) + assert.Equal(t, "'@sum", csvSafeCell("@sum")) +} + +func TestScimEmailFromUserName(t *testing.T) { + t.Parallel() + + assert.Equal(t, "user@example.com", scimEmailFromUserName("user@example.com")) + assert.Equal(t, "", scimEmailFromUserName("not-an-email")) + assert.Equal(t, "", scimEmailFromUserName("")) +}