Harden log export CSV and fix profile loader boundary
Load SCIM profile emails via Identities in the export layer, drop the cross-table join, sanitize formula-leading cells, and fall back to validated userName when profile email is missing. Drop redundant gid array casts in LoadByIDs. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
@@ -407,7 +407,7 @@ SELECT
|
|||||||
FROM
|
FROM
|
||||||
identities
|
identities
|
||||||
WHERE
|
WHERE
|
||||||
id = ANY(@identity_ids::text[])
|
id = ANY(@identity_ids)
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"identity_ids": identityIDs}
|
args := pgx.StrictNamedArgs{"identity_ids": identityIDs}
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ SELECT
|
|||||||
p.id,
|
p.id,
|
||||||
p.identity_id,
|
p.identity_id,
|
||||||
p.organization_id,
|
p.organization_id,
|
||||||
i.email_address,
|
''::citext AS email_address,
|
||||||
p.source,
|
p.source,
|
||||||
p.state,
|
p.state,
|
||||||
p.full_name,
|
p.full_name,
|
||||||
@@ -540,8 +540,6 @@ SELECT
|
|||||||
p.updated_at
|
p.updated_at
|
||||||
FROM
|
FROM
|
||||||
iam_membership_profiles p
|
iam_membership_profiles p
|
||||||
INNER JOIN identities i
|
|
||||||
ON i.id = p.identity_id
|
|
||||||
WHERE
|
WHERE
|
||||||
p.%s
|
p.%s
|
||||||
AND p.organization_id = @organization_id
|
AND p.organization_id = @organization_id
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ SELECT
|
|||||||
FROM
|
FROM
|
||||||
iam_personal_api_keys
|
iam_personal_api_keys
|
||||||
WHERE
|
WHERE
|
||||||
id = ANY(@api_key_ids::text[])
|
id = ANY(@api_key_ids)
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"api_key_ids": apiKeyIDs}
|
args := pgx.StrictNamedArgs{"api_key_ids": apiKeyIDs}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/mail"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -206,7 +207,7 @@ func auditLogEntryCSVRow(
|
|||||||
entry *coredata.AuditLogEntry,
|
entry *coredata.AuditLogEntry,
|
||||||
actor auditLogActorExportInfo,
|
actor auditLogActorExportInfo,
|
||||||
) []string {
|
) []string {
|
||||||
return []string{
|
return csvExportRow(
|
||||||
organizationName,
|
organizationName,
|
||||||
entry.ID.String(),
|
entry.ID.String(),
|
||||||
entry.CreatedAt.Format(time.RFC3339),
|
entry.CreatedAt.Format(time.RFC3339),
|
||||||
@@ -217,7 +218,7 @@ func auditLogEntryCSVRow(
|
|||||||
entry.Action,
|
entry.Action,
|
||||||
entry.ResourceType,
|
entry.ResourceType,
|
||||||
entry.ResourceID.String(),
|
entry.ResourceID.String(),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scimEventCSVRow(
|
func scimEventCSVRow(
|
||||||
@@ -227,19 +228,24 @@ func scimEventCSVRow(
|
|||||||
) []string {
|
) []string {
|
||||||
profile := profilesByUserName[strings.ToLower(event.UserName)]
|
profile := profilesByUserName[strings.ToLower(event.UserName)]
|
||||||
|
|
||||||
return []string{
|
email := profile.email
|
||||||
|
if email == "" {
|
||||||
|
email = scimEmailFromUserName(event.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return csvExportRow(
|
||||||
organizationName,
|
organizationName,
|
||||||
event.ID.String(),
|
event.ID.String(),
|
||||||
event.CreatedAt.Format(time.RFC3339),
|
event.CreatedAt.Format(time.RFC3339),
|
||||||
event.Method,
|
event.Method,
|
||||||
event.Path,
|
event.Path,
|
||||||
event.UserName,
|
event.UserName,
|
||||||
profile.email,
|
email,
|
||||||
profile.fullName,
|
profile.fullName,
|
||||||
strconv.Itoa(event.StatusCode),
|
strconv.Itoa(event.StatusCode),
|
||||||
stringPtrValue(event.ErrorMessage),
|
stringPtrValue(event.ErrorMessage),
|
||||||
event.IPAddress.String(),
|
event.IPAddress.String(),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAuditLogActorExportInfo(
|
func loadAuditLogActorExportInfo(
|
||||||
@@ -312,6 +318,21 @@ func loadSCIMProfileExportInfo(
|
|||||||
return nil, fmt.Errorf("cannot load SCIM profile export info: %w", err)
|
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))
|
result := make(map[string]scimProfileExportInfo, len(profiles))
|
||||||
for _, profile := range profiles {
|
for _, profile := range profiles {
|
||||||
if profile.UserName == nil {
|
if profile.UserName == nil {
|
||||||
@@ -320,7 +341,7 @@ func loadSCIMProfileExportInfo(
|
|||||||
|
|
||||||
key := strings.ToLower(*profile.UserName)
|
key := strings.ToLower(*profile.UserName)
|
||||||
result[key] = scimProfileExportInfo{
|
result[key] = scimProfileExportInfo{
|
||||||
email: profile.EmailAddress.String(),
|
email: emailByIdentityID[profile.IdentityID],
|
||||||
fullName: profileFullName(profile),
|
fullName: profileFullName(profile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,6 +381,41 @@ func uniqueNonEmptyStrings(values []string) []string {
|
|||||||
return out
|
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 {
|
func profileFullName(profile *coredata.MembershipProfile) string {
|
||||||
if profile.FormattedName != nil && *profile.FormattedName != "" {
|
if profile.FormattedName != nil && *profile.FormattedName != "" {
|
||||||
return *profile.FormattedName
|
return *profile.FormattedName
|
||||||
|
|||||||
@@ -32,3 +32,21 @@ func TestUniqueNonEmptyStrings(t *testing.T) {
|
|||||||
got := uniqueNonEmptyStrings([]string{"a", "A", "", "b", "a"})
|
got := uniqueNonEmptyStrings([]string{"a", "A", "", "b", "a"})
|
||||||
assert.Equal(t, []string{"a", "b"}, got)
|
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(""))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user