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:
Silo Server Migration
2026-05-26 08:34:17 -04:00
parent 8f818ac0c9
commit ef1c6accb6
3 changed files with 66 additions and 12 deletions
+12 -7
View File
@@ -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
+50
View File
@@ -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}