[PM-19232] Implement externalId handling in PatchUserCommand with validation#6998
[PM-19232] Implement externalId handling in PatchUserCommand with validation#6998
Conversation
|
Great job! No new security vulnerabilities introduced in this pull request |
|
Claude finished @JaredScar's task in 4m 34s —— View job Review: PM-19232 Implement externalId handling in PatchUserCommand with validationOverviewThis PR adds Findings
Previous Finding Status
What Looks Good
Checklist
|
| { | ||
| // Replace operations | ||
| if (operation.Op?.ToLowerInvariant() == "replace") | ||
| if (operation.Op?.ToLowerInvariant() == PatchOps.Replace) |
There was a problem hiding this comment.
Non-blocker: I know you're just following the existing pattern, so the code as is good, but consider abstracting this logic into its own method and using the early return pattern to reduce nested conditionals when possible.
// Replace operations
if (operation.Op?.ToLowerInvariant() == PatchOps.Replace)
{
// Active from path
if (operation.Path?.ToLowerInvariant() == "active")
{
var active = operation.Value.ToString()?.ToLowerInvariant();
var handled = await HandleActiveOperationAsync(orgUser, active == "true");
if (!operationHandled)
{
operationHandled = handled;
}
}
// Active from value object
else if (string.IsNullOrWhiteSpace(operation.Path) &&
operation.Value.TryGetProperty("active", out var activeProperty))
{
var handled = await HandleActiveOperationAsync(orgUser, activeProperty.GetBoolean());
if (!operationHandled)
{
operationHandled = handled;
}
}
// ExternalId from path
else if (operation.Path?.ToLowerInvariant() == PatchPaths.ExternalId)
{
var newExternalId = operation.Value.GetString();
await HandleExternalIdOperationAsync(orgUser, newExternalId);
operationHandled = true;
}
// ExternalId from value object
else if (string.IsNullOrWhiteSpace(operation.Path) &&
operation.Value.TryGetProperty("externalId", out var externalIdProperty))
{
var newExternalId = externalIdProperty.GetString();
await HandleExternalIdOperationAsync(orgUser, newExternalId);
operationHandled = true;
}
}Methods are normally used for DRY, but I think the ability to keep the scope small and let the method’s name provide a quick summary of what this group of code is doing makes it easier to understand at a glance. Also, the early return pattern makes it easier to follow.
JimmyVo16
left a comment
There was a problem hiding this comment.
Ping me again when you need a reapproval after fixing the tests.
…https://github.com/bitwarden/server into ac/pm-19232-entraID-reports-successful-PATCH-ops-fix
bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs
Outdated
Show resolved
Hide resolved
…ported operations instead of throwing exceptions. Update method names for clarity and adjust assertions in test cases accordingly.
| // ExternalId from path | ||
| else if (operation.Path?.ToLowerInvariant() == PatchPaths.ExternalId) | ||
| { | ||
| var newExternalId = operation.Value.GetString(); | ||
| await HandleExternalIdOperationAsync(orgUser, newExternalId); | ||
| operationHandled = true; | ||
| } | ||
| // ExternalId from value object | ||
| else if (string.IsNullOrWhiteSpace(operation.Path) && | ||
| operation.Value.TryGetProperty("externalId", out var externalIdProperty)) | ||
| { | ||
| var newExternalId = externalIdProperty.GetString(); | ||
| await HandleExternalIdOperationAsync(orgUser, newExternalId); | ||
| operationHandled = true; | ||
| } |
There was a problem hiding this comment.
externalId
The else if chain between the value-object branches (lines 55 and 72) means that when a SCIM provider sends a single replace operation with no path and a value object containing both active and externalId properties, only active is processed and the externalId update is silently dropped.
Example request that triggers this:
{
"op": "replace",
"value": { "active": false, "externalId": "new-id" }
}This is valid per SCIM RFC 7644 Section 3.5.2.2 (replace with no path targets the resource itself with all sub-attributes in the value). SCIM providers like EntraID could reasonably send combined property updates in a single operation.
Suggested fix: For the value-object branches (no path specified), check for each property independently rather than using else if:
// Value object with no path - check for each supported property
if (string.IsNullOrWhiteSpace(operation.Path))
{
if (operation.Value.TryGetProperty("active", out var activeProperty))
{
var handled = await HandleActiveOperationAsync(orgUser, activeProperty.GetBoolean());
if (!operationHandled)
{
operationHandled = handled;
}
}
if (operation.Value.TryGetProperty("externalId", out var externalIdProperty))
{
var newExternalId = externalIdProperty.GetString();
await HandleExternalIdOperationAsync(orgUser, newExternalId);
operationHandled = true;
}
}This preserves the existing behavior for path-based operations while correctly handling combined value objects.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6998 +/- ##
==========================================
+ Coverage 56.28% 56.36% +0.07%
==========================================
Files 1986 1990 +4
Lines 87667 87861 +194
Branches 7816 7840 +24
==========================================
+ Hits 49345 49524 +179
- Misses 36491 36502 +11
- Partials 1831 1835 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|




🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-19232
📔 Objective
Fit EntraID handling