fix(webhooksync): skip events for unmapped external users
- Require explicit profile mapping instead of falling back to the default profile - Update settings UI copy to reflect that unmapped users are ignored
This commit is contained in:
@@ -255,18 +255,16 @@ func (s *Service) ProcessWebhook(ctx context.Context, secret string, r *http.Req
|
||||
slog.Warn("webhook sync: failed to upsert seen external user", "connection_id", conn.ID, "external_user_id", event.UserID, "error", err)
|
||||
}
|
||||
|
||||
profileID := conn.DefaultProfileID
|
||||
if mapping, err := s.repo.GetMappingByUser(ctx, conn.ID, event.UserID); err != nil {
|
||||
return s.failWebhook(ctx, conn.ID, result, err, "Failed to resolve profile mapping")
|
||||
} else if mapping != nil && mapping.SiloProfileID != nil && *mapping.SiloProfileID != "" {
|
||||
profileID = *mapping.SiloProfileID
|
||||
}
|
||||
result.ProfileID = profileID
|
||||
if profileID == "" {
|
||||
} else if profileID, ok := resolveWebhookProfileID(mapping); ok {
|
||||
result.ProfileID = profileID
|
||||
} else {
|
||||
result.Outcome = OutcomeSkipped
|
||||
result.Summary = "Skipped because no default or user-specific profile is configured"
|
||||
result.Summary = "Skipped because external user is not linked to a Silo profile"
|
||||
return result, nil
|
||||
}
|
||||
profileID := result.ProfileID
|
||||
|
||||
record := event.Record.toHistoryImportRecord()
|
||||
match, _, err := s.matcher.Match(ctx, record)
|
||||
@@ -432,6 +430,13 @@ func shouldSkipEvent(state *ItemState, event *CanonicalEvent) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func resolveWebhookProfileID(mapping *ProfileMapping) (string, bool) {
|
||||
if mapping == nil || mapping.SiloProfileID == nil || strings.TrimSpace(*mapping.SiloProfileID) == "" {
|
||||
return "", false
|
||||
}
|
||||
return *mapping.SiloProfileID, true
|
||||
}
|
||||
|
||||
func buildWebhookURL(baseURL, secret string) string {
|
||||
if baseURL == "" {
|
||||
return webhookSyncPathPrefix + secret
|
||||
|
||||
@@ -42,6 +42,52 @@ func TestBuildWebhookURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveWebhookProfileRequiresExplicitMapping(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
linkedProfileID := "linked-profile"
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
mapping *ProfileMapping
|
||||
want string
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "missing mapping is skipped",
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "unmapped external user is skipped",
|
||||
mapping: &ProfileMapping{},
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "empty profile mapping is skipped",
|
||||
mapping: &ProfileMapping{SiloProfileID: ptrString("")},
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "explicit profile mapping is used",
|
||||
mapping: &ProfileMapping{SiloProfileID: &linkedProfileID},
|
||||
want: linkedProfileID,
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, ok := resolveWebhookProfileID(tc.mapping)
|
||||
if ok != tc.wantOK || got != tc.want {
|
||||
t.Fatalf("resolveWebhookProfileID() = (%q, %v), want (%q, %v)", got, ok, tc.want, tc.wantOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterDiscoveredAccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -82,3 +128,7 @@ func TestFilterDiscoveredAccountsFallsBackWhenFlagsMissing(t *testing.T) {
|
||||
t.Fatalf("unexpected fallback accounts: %#v", filtered)
|
||||
}
|
||||
}
|
||||
|
||||
func ptrString(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
@@ -678,7 +678,7 @@ export default function WebhookSyncSettings() {
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<Label className="text-sm font-medium">Default profile</Label>
|
||||
<p className="text-muted-foreground text-[13px] leading-relaxed">
|
||||
Activity from unmapped users goes to this profile.
|
||||
The signed-in external user is linked to this profile when the connection is created.
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
@@ -894,7 +894,7 @@ export default function WebhookSyncSettings() {
|
||||
<SelectValue placeholder="Choose a profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={UNMAPPED_VALUE}>No fallback profile</SelectItem>
|
||||
<SelectItem value={UNMAPPED_VALUE}>No default profile</SelectItem>
|
||||
{profiles.map((candidate) => (
|
||||
<SelectItem key={candidate.id} value={candidate.id}>
|
||||
{candidate.name}
|
||||
@@ -941,8 +941,7 @@ export default function WebhookSyncSettings() {
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-sm font-medium">Profile mapping</Label>
|
||||
<p className="text-muted-foreground text-[13px] leading-relaxed">
|
||||
Map each external user to a Silo profile. Unmapped users fall back to the
|
||||
default profile above.
|
||||
Map each external user to a Silo profile. Unmapped users are ignored.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -987,7 +986,7 @@ export default function WebhookSyncSettings() {
|
||||
<SelectValue placeholder="Choose a profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={UNMAPPED_VALUE}>Use default profile</SelectItem>
|
||||
<SelectItem value={UNMAPPED_VALUE}>Ignore this user</SelectItem>
|
||||
{profiles.map((candidate) => (
|
||||
<SelectItem key={candidate.id} value={candidate.id}>
|
||||
{candidate.name}
|
||||
|
||||
Reference in New Issue
Block a user