Skip to content

Commit 16ba7d1

Browse files
committed
fix(audit-logs): keep an unresolved workspace scope from widening the feed
A workspace id in the URL that no longer resolves — deleted since, or never one of ours — dropped the filter, so a request for one workspace's history was answered with the whole organization's, under a URL that still claimed to be scoped. The CSV export followed the same filter and would have carried the same widening. Every other deep-linked id in the app degrades to the unfiltered view, which is right where the fallback shows less than was asked for. An audit feed is the one place where widening is the dangerous direction, so it now stays closed and says so, with the filter chip still rendered so the scope can be cleared.
1 parent 44b2463 commit 16ba7d1

2 files changed

Lines changed: 51 additions & 15 deletions

File tree

apps/sim/ee/audit-logs/components/audit-logs.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,12 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
257257
/**
258258
* Resolved, not merely present. Only the id lives in the URL, and the filter is
259259
* applied once it matches a workspace the organization actually owns — a stale id
260-
* from an old link would otherwise silently narrow the feed to nothing under a
261-
* chip labelled with the bare uuid.
260+
* from an old link would otherwise be shown under a chip labelled with a bare uuid.
262261
*/
263-
const orgWorkspaces = useOrganizationWorkspaces(organizationId, Boolean(urlFilters.workspace))
264-
const filteredWorkspace = urlFilters.workspace
265-
? orgWorkspaces.data?.find((entry) => entry.id === urlFilters.workspace)
262+
const workspaceScope = urlFilters.workspace
263+
const orgWorkspaces = useOrganizationWorkspaces(organizationId, Boolean(workspaceScope))
264+
const scopedWorkspace = workspaceScope
265+
? orgWorkspaces.data?.find((entry) => entry.id === workspaceScope)
266266
: undefined
267267

268268
const [datePickerOpen, setDatePickerOpen] = useState(false)
@@ -289,7 +289,7 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
289289
const filters: AuditLogFilters = {
290290
search: debouncedSearch || undefined,
291291
resourceType: selectedTypes.length > 0 ? selectedTypes.join(',') : undefined,
292-
workspaceId: filteredWorkspace?.id,
292+
workspaceId: scopedWorkspace?.id,
293293
startDate: getStartDateFromTimeRange(timeRange, customStartDate)?.toISOString(),
294294
endDate: getEndDateFromTimeRange(timeRange, customEndDate)?.toISOString(),
295295
}
@@ -300,7 +300,20 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
300300
* immediately refetches it narrowed — two requests, with a flash of rows the link
301301
* did not ask for in between.
302302
*/
303-
const isWorkspaceFilterPending = Boolean(urlFilters.workspace) && orgWorkspaces.isPending
303+
const isWorkspaceScopePending = Boolean(workspaceScope) && orgWorkspaces.isPending
304+
305+
/**
306+
* The link named a workspace this organization cannot resolve — deleted since, or
307+
* never one of ours.
308+
*
309+
* The feed stays closed rather than falling back to the organization. Every other
310+
* deep-linked id in the app degrades to the unfiltered view, but an audit feed is
311+
* the one place where widening is the dangerous direction: dropping the filter
312+
* would answer a request for one workspace's history with everybody's, under a URL
313+
* that still claims to be scoped, and the CSV export would follow.
314+
*/
315+
const isWorkspaceScopeUnresolved =
316+
Boolean(workspaceScope) && !isWorkspaceScopePending && !scopedWorkspace
304317
const {
305318
data,
306319
isLoading,
@@ -309,7 +322,7 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
309322
hasNextPage,
310323
fetchNextPage,
311324
refetch,
312-
} = useAuditLogs(organizationId, filters, !isWorkspaceFilterPending)
325+
} = useAuditLogs(organizationId, filters, !isWorkspaceScopePending && !isWorkspaceScopeUnresolved)
313326

314327
const allEntries = useMemo(() => {
315328
if (!data?.pages) return []
@@ -437,7 +450,7 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
437450
allOptionLabel='All types'
438451
align='start'
439452
/>
440-
{filteredWorkspace && (
453+
{workspaceScope && (
441454
/*
442455
A deep-linked scope, not a picker: the organization can hold hundreds of
443456
workspaces, so this narrows the feed only when a link asks it to and
@@ -448,11 +461,13 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
448461
<Chip
449462
rightIcon={X}
450463
onClick={() => void setUrlFilters({ workspace: null })}
451-
aria-label={`Clear the ${filteredWorkspace.name} workspace filter`}
464+
aria-label='Clear the workspace filter'
452465
className='max-w-[280px] shrink-0'
453466
>
467+
{/* Rendered for an unresolved scope too, or a bad link would leave the
468+
feed closed with no control to reopen it. */}
454469
<OverflowText
455-
label={`Workspace: ${filteredWorkspace.name}`}
470+
label={`Workspace: ${scopedWorkspace?.name ?? 'not found'}`}
456471
className='block min-w-0'
457472
/>
458473
</Chip>
@@ -516,7 +531,11 @@ export function AuditLogs({ organizationId }: AuditLogsProps) {
516531
<ActivityLog
517532
entries={allEntries.map(toActivityEntry)}
518533
emptyState={
519-
isLoading || isWorkspaceFilterPending ? undefined : debouncedSearch ? (
534+
isLoading || isWorkspaceScopePending ? undefined : isWorkspaceScopeUnresolved ? (
535+
<SettingsEmptyState>
536+
That workspace is not part of this organization.
537+
</SettingsEmptyState>
538+
) : debouncedSearch ? (
520539
<SettingsEmptyState variant='inline'>
521540
No results for "{debouncedSearch}"
522541
</SettingsEmptyState>

apps/sim/ee/audit-logs/hooks/audit-logs.test.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,13 @@ let queryClient: QueryClient
5353
function AuditProbe({
5454
organizationId,
5555
workspaceId,
56+
enabled = true,
5657
}: {
5758
organizationId: string
5859
workspaceId?: string
60+
enabled?: boolean
5961
}) {
60-
const auditLogs = useAuditLogs(organizationId, { workspaceId })
62+
const auditLogs = useAuditLogs(organizationId, { workspaceId }, enabled)
6163
const entries = auditLogs.data?.pages.flatMap((page) => page.data) ?? []
6264

6365
return (
@@ -68,11 +70,11 @@ function AuditProbe({
6870
)
6971
}
7072

71-
function renderAuditLogs(organizationId: string, workspaceId?: string) {
73+
function renderAuditLogs(organizationId: string, workspaceId?: string, enabled = true) {
7274
act(() => {
7375
root.render(
7476
<QueryClientProvider client={queryClient}>
75-
<AuditProbe organizationId={organizationId} workspaceId={workspaceId} />
77+
<AuditProbe organizationId={organizationId} workspaceId={workspaceId} enabled={enabled} />
7678
</QueryClientProvider>
7779
)
7880
})
@@ -156,4 +158,19 @@ describe('useAuditLogs identity transitions', () => {
156158

157159
expect(container).toHaveTextContent('Updated Organization A')
158160
})
161+
162+
/**
163+
* The scope a link asks for is a ceiling, not a hint. A workspace id that no longer
164+
* resolves must leave the feed closed rather than answering with the whole
165+
* organization's history under a URL that still claims to be scoped.
166+
*/
167+
it('never queries unscoped while a workspace scope is unresolved', async () => {
168+
mockRequestJson.mockResolvedValue(AUDIT_PAGE_A)
169+
170+
renderAuditLogs('org-a', undefined, false)
171+
await flushQueries()
172+
173+
expect(mockRequestJson).not.toHaveBeenCalled()
174+
expect(container).not.toHaveTextContent('Updated Organization A')
175+
})
159176
})

0 commit comments

Comments
 (0)