Skip to content

Commit f681f0b

Browse files
committed
fix: guard service account landing redirect
1 parent 58efb9c commit f681f0b

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

frontend/src/app/workspaces/[workspaceId]/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default function WorkspacePage() {
4343
const canViewServiceAccounts = useScopeCheck("workspace:service_account:read")
4444
const canViewMembers = useScopeCheck("workspace:member:read")
4545
const canViewInbox = useScopeCheck("inbox:read")
46+
const canReadWorkspace = useScopeCheck("workspace:read")
4647

4748
const { hasEntitlement, isLoading: entitlementsLoading } = useEntitlements()
4849
const agentAddonsEnabled = hasEntitlement("agent_addons")
@@ -60,6 +61,7 @@ export default function WorkspacePage() {
6061
canViewServiceAccounts,
6162
canViewMembers,
6263
canViewInbox,
64+
canReadWorkspace,
6365
].some((value) => value === undefined)
6466

6567
const landingPath = useMemo(() => {
@@ -88,7 +90,7 @@ export default function WorkspacePage() {
8890
if (canViewIntegrations === true) {
8991
return `${basePath}/integrations`
9092
}
91-
if (canViewServiceAccounts === true) {
93+
if (canViewServiceAccounts === true && canReadWorkspace === true) {
9294
return `${basePath}/service-accounts`
9395
}
9496
if (canViewMembers === true) {
@@ -106,6 +108,7 @@ export default function WorkspacePage() {
106108
canViewIntegrations,
107109
canViewMembers,
108110
canViewSecrets,
111+
canReadWorkspace,
109112
canViewTables,
110113
canViewVariables,
111114
canViewWorkflows,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import { render, screen, waitFor } from "@testing-library/react"
6+
import WorkspacePage from "@/app/workspaces/[workspaceId]/page"
7+
8+
const mockRouterReplace = jest.fn()
9+
const mockUseScopeCheck = jest.fn<boolean | undefined, [string]>()
10+
let mockScopes: Record<string, boolean | undefined> = {}
11+
12+
jest.mock("next/navigation", () => ({
13+
useRouter: () => ({ replace: mockRouterReplace }),
14+
}))
15+
16+
jest.mock("next/image", () => ({
17+
__esModule: true,
18+
default: (props: {
19+
src: string
20+
alt: string
21+
className?: string
22+
}): JSX.Element => <img alt={props.alt} className={props.className} />,
23+
}))
24+
25+
jest.mock("@/components/auth/scope-guard", () => ({
26+
useScopeCheck: (scope: string) => mockUseScopeCheck(scope),
27+
}))
28+
29+
jest.mock("@/components/loading/spinner", () => ({
30+
CenteredSpinner: () => <div>Loading</div>,
31+
}))
32+
33+
jest.mock("@/hooks", () => ({
34+
useEntitlements: () => ({
35+
hasEntitlement: () => false,
36+
isLoading: false,
37+
}),
38+
}))
39+
40+
jest.mock("@/providers/workspace-id", () => ({
41+
useWorkspaceId: () => "workspace-1",
42+
}))
43+
44+
describe("WorkspacePage", () => {
45+
beforeEach(() => {
46+
mockRouterReplace.mockReset()
47+
mockUseScopeCheck.mockReset()
48+
mockScopes = {}
49+
mockUseScopeCheck.mockImplementation((scope) => mockScopes[scope] ?? false)
50+
})
51+
52+
it("does not redirect service-account-only users into the workspace shell", () => {
53+
mockScopes = {
54+
"workspace:service_account:read": true,
55+
"workspace:read": false,
56+
}
57+
58+
render(<WorkspacePage />)
59+
60+
expect(mockRouterReplace).not.toHaveBeenCalled()
61+
expect(screen.getByText("No accessible pages")).toBeInTheDocument()
62+
})
63+
64+
it("redirects to service accounts when the workspace shell is readable", async () => {
65+
mockScopes = {
66+
"workspace:service_account:read": true,
67+
"workspace:read": true,
68+
}
69+
70+
render(<WorkspacePage />)
71+
72+
await waitFor(() => {
73+
expect(mockRouterReplace).toHaveBeenCalledWith(
74+
"/workspaces/workspace-1/service-accounts"
75+
)
76+
})
77+
})
78+
})

0 commit comments

Comments
 (0)