Skip to content

Commit 841d77c

Browse files
refactor(ui): drop the client-side Registry eligibility lease
- Remove the eligibility provider, access boundary, and lease plumbing - Route access-denied action results straight to the profile page - Seed the sidebar Registry entry from one server-side evaluation - Align the e2e fixture with the renamed registry artifacts endpoints
1 parent 5fff469 commit 841d77c

23 files changed

Lines changed: 214 additions & 737 deletions

ui/actions/registry/registry.test.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
refreshRegistryCollections,
2525
removeRegistryArtifact,
2626
refreshRegistryCredential,
27-
refreshRegistryEligibility,
2827
submitRegistryCredential,
2928
} from "./registry";
3029

@@ -92,10 +91,7 @@ const catalogResponse = () =>
9291
beforeEach(() => {
9392
vi.stubGlobal("fetch", fetchMock);
9493
authMock.mockResolvedValue({ accessToken: "access-token" });
95-
evaluateAccessMock.mockResolvedValue({
96-
status: "eligible",
97-
leaseDurationMs: 30_000,
98-
});
94+
evaluateAccessMock.mockResolvedValue({ status: "eligible" });
9995
fetchMock.mockReset();
10096
pollTaskUntilSettledMock.mockReset();
10197
});
@@ -150,20 +146,6 @@ describe("Registry guarded reads", () => {
150146
},
151147
);
152148

153-
it("returns only the current eligibility result without Registry I/O", async () => {
154-
// Given
155-
const access = { status: "unknown" } as const;
156-
evaluateAccessMock.mockResolvedValue(access);
157-
158-
// When
159-
const result = await refreshRegistryEligibility();
160-
161-
// Then
162-
expect(result).toEqual(access);
163-
expect(evaluateAccessMock).toHaveBeenCalledWith("access-token");
164-
expect(fetchMock).not.toHaveBeenCalled();
165-
});
166-
167149
it("bootstraps in credential, tenant-artifact, providers, then complete-catalog order", async () => {
168150
// Given
169151
fetchMock
@@ -176,9 +158,9 @@ describe("Registry guarded reads", () => {
176158
const result = await getRegistryBootstrap();
177159

178160
// Then
161+
expect(result).not.toHaveProperty("leaseDurationMs");
179162
expect(result).toEqual({
180163
status: "ready",
181-
leaseDurationMs: 30_000,
182164
state: {
183165
status: "ready",
184166
credential: activeCredential,
@@ -229,9 +211,9 @@ describe("Registry guarded reads", () => {
229211
const result = await getRegistryBootstrap();
230212

231213
// Then
214+
expect(result).not.toHaveProperty("leaseDurationMs");
232215
expect(result).toEqual({
233216
status: "ready",
234-
leaseDurationMs: 30_000,
235217
state: {
236218
status: expectedStatus,
237219
credential,
@@ -369,7 +351,7 @@ describe("Registry guarded reads", () => {
369351
it("rechecks access between separate actions after permission revocation", async () => {
370352
// Given
371353
evaluateAccessMock
372-
.mockResolvedValueOnce({ status: "eligible", leaseDurationMs: 30_000 })
354+
.mockResolvedValueOnce({ status: "eligible" })
373355
.mockResolvedValueOnce({ status: "ineligible" });
374356
fetchMock.mockResolvedValueOnce(credentialResponse());
375357

ui/actions/registry/registry.ts

Lines changed: 31 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,16 @@ import {
3333
RegistryCatalogPageError,
3434
} from "./registry.adapter";
3535

36-
type GuardedRegistryAccess = { accessToken: string; leaseDurationMs: number };
37-
3836
interface RegistryAddArtifactInput {
3937
normalizedName: string;
4038
versionSpec?: string;
4139
}
4240

43-
async function getRegistryAccess(): Promise<GuardedRegistryAccess | null> {
41+
async function getRegistryAccess(): Promise<string | null> {
4442
const accessToken = (await auth())?.accessToken;
4543
const access = await evaluateRegistryAccess(accessToken);
4644
return access.status === REGISTRY_ACCESS.ELIGIBLE && accessToken?.trim()
47-
? { accessToken, leaseDurationMs: access.leaseDurationMs }
45+
? accessToken
4846
: null;
4947
}
5048

@@ -228,54 +226,42 @@ async function confirmRegistryMutation(
228226
}
229227

230228
function bootstrapReady(
231-
access: GuardedRegistryAccess,
232229
state: RegistryBootstrapState,
233230
): RegistryBootstrapResult {
234-
return {
235-
status: REGISTRY_BOOTSTRAP_STATE.READY,
236-
leaseDurationMs: access.leaseDurationMs,
237-
state,
238-
};
231+
return { status: REGISTRY_BOOTSTRAP_STATE.READY, state };
239232
}
240233

241234
function bootstrapFailure(
242-
access: GuardedRegistryAccess,
243235
failure: RegistryFailureResult,
244236
): RegistryBootstrapResult {
245237
if (failure.status === REGISTRY_FAILURE.ACCESS_DENIED) {
246238
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
247239
}
248-
return bootstrapReady(access, {
240+
return bootstrapReady({
249241
status:
250242
failure.status === REGISTRY_FAILURE.ONBOARDING
251243
? REGISTRY_BOOTSTRAP_STATE.ERROR
252244
: failure.status,
253245
});
254246
}
255247

256-
export async function refreshRegistryEligibility() {
257-
return evaluateRegistryAccess((await auth())?.accessToken);
258-
}
259-
260248
export async function getRegistryBootstrap(): Promise<RegistryBootstrapResult> {
261-
const access = await getRegistryAccess();
262-
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
249+
const accessToken = await getRegistryAccess();
250+
if (!accessToken) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
263251

264-
const credentialRead = await readRegistryCredential(access.accessToken);
252+
const credentialRead = await readRegistryCredential(accessToken);
265253
if (credentialRead.status !== REGISTRY_CREDENTIAL_READ.STATUS) {
266-
return bootstrapFailure(access, credentialRead);
254+
return bootstrapFailure(credentialRead);
267255
}
268-
const tenantArtifactsRead = await readRegistryTenantArtifacts(
269-
access.accessToken,
270-
);
256+
const tenantArtifactsRead = await readRegistryTenantArtifacts(accessToken);
271257
if (tenantArtifactsRead.status !== "ready") {
272-
return bootstrapFailure(access, tenantArtifactsRead);
258+
return bootstrapFailure(tenantArtifactsRead);
273259
}
274260

275261
const { credential } = credentialRead;
276262
const { tenantArtifacts } = tenantArtifactsRead;
277263
if (!hasActiveRegistryCredential(credential)) {
278-
return bootstrapReady(access, {
264+
return bootstrapReady({
279265
status: credential.validationPending
280266
? REGISTRY_BOOTSTRAP_STATE.VALIDATION_PENDING
281267
: REGISTRY_BOOTSTRAP_STATE.ONBOARDING,
@@ -284,26 +270,23 @@ export async function getRegistryBootstrap(): Promise<RegistryBootstrapResult> {
284270
});
285271
}
286272

287-
const providers = await readRegistryProviders(access.accessToken, credential);
288-
if (providers.status !== "ready") return bootstrapFailure(access, providers);
289-
const catalog = await readCompleteRegistryCatalog(
290-
access.accessToken,
291-
credential,
292-
);
273+
const providers = await readRegistryProviders(accessToken, credential);
274+
if (providers.status !== "ready") return bootstrapFailure(providers);
275+
const catalog = await readCompleteRegistryCatalog(accessToken, credential);
293276
if (catalog.status === REGISTRY_FAILURE.ACCESS_DENIED) {
294277
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
295278
}
296279
if (catalog.status === REGISTRY_CATALOG.INCOMPLETE) {
297-
return bootstrapReady(access, {
280+
return bootstrapReady({
298281
status: REGISTRY_BOOTSTRAP_STATE.INCOMPLETE,
299282
catalog,
300283
});
301284
}
302285
if (catalog.status !== REGISTRY_CATALOG.COMPLETE) {
303-
return bootstrapFailure(access, catalog);
286+
return bootstrapFailure(catalog);
304287
}
305288

306-
return bootstrapReady(access, {
289+
return bootstrapReady({
307290
status: REGISTRY_BOOTSTRAP_STATE.READY,
308291
credential,
309292
catalog,
@@ -314,20 +297,18 @@ export async function getRegistryBootstrap(): Promise<RegistryBootstrapResult> {
314297
export async function refreshRegistryCredential(): Promise<RegistryCredentialReadResult> {
315298
const access = await getRegistryAccess();
316299
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
317-
return readRegistryCredential(access.accessToken);
300+
return readRegistryCredential(access);
318301
}
319302

320303
export async function refreshRegistryCollections(): Promise<RegistryCollectionsResult> {
321304
const access = await getRegistryAccess();
322305
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
323306

324-
const providers = await readRegistryProviders(access.accessToken, null);
307+
const providers = await readRegistryProviders(access, null);
325308
if (providers.status !== "ready") return providers;
326-
const catalog = await readCompleteRegistryCatalog(access.accessToken, null);
309+
const catalog = await readCompleteRegistryCatalog(access, null);
327310
if (catalog.status !== REGISTRY_CATALOG.COMPLETE) return catalog;
328-
const tenantArtifactsRead = await readRegistryTenantArtifacts(
329-
access.accessToken,
330-
);
311+
const tenantArtifactsRead = await readRegistryTenantArtifacts(access);
331312
return tenantArtifactsRead.status === "ready"
332313
? {
333314
status: REGISTRY_CATALOG.COMPLETE,
@@ -353,7 +334,7 @@ export async function addRegistryArtifact({
353334
headers: {
354335
Accept: "application/vnd.api+json",
355336
"Content-Type": "application/vnd.api+json",
356-
Authorization: `Bearer ${access.accessToken}`,
337+
Authorization: `Bearer ${access}`,
357338
},
358339
body: JSON.stringify({
359340
data: {
@@ -379,7 +360,7 @@ export async function addRegistryArtifact({
379360
);
380361
}
381362

382-
return confirmRegistryMutation(access.accessToken, normalizedName, true);
363+
return confirmRegistryMutation(access, normalizedName, true);
383364
}
384365

385366
export async function removeRegistryArtifact(
@@ -397,7 +378,7 @@ export async function removeRegistryArtifact(
397378
cache: "no-store",
398379
headers: {
399380
Accept: "application/vnd.api+json",
400-
Authorization: `Bearer ${access.accessToken}`,
381+
Authorization: `Bearer ${access}`,
401382
},
402383
},
403384
);
@@ -409,7 +390,7 @@ export async function removeRegistryArtifact(
409390
}
410391
if (!response.ok) return { status: REGISTRY_FAILURE.ERROR };
411392

412-
return confirmRegistryMutation(access.accessToken, normalizedName, false);
393+
return confirmRegistryMutation(access, normalizedName, false);
413394
}
414395

415396
export async function submitRegistryCredential(
@@ -418,7 +399,7 @@ export async function submitRegistryCredential(
418399
const access = await getRegistryAccess();
419400
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
420401

421-
const priorCredential = await readRegistryCredential(access.accessToken);
402+
const priorCredential = await readRegistryCredential(access);
422403
if (priorCredential.status !== REGISTRY_CREDENTIAL_READ.STATUS) {
423404
return priorCredential;
424405
}
@@ -431,7 +412,7 @@ export async function submitRegistryCredential(
431412
headers: {
432413
Accept: "application/vnd.api+json",
433414
"Content-Type": "application/vnd.api+json",
434-
Authorization: `Bearer ${access.accessToken}`,
415+
Authorization: `Bearer ${access}`,
435416
},
436417
body: JSON.stringify({
437418
data: { type: "registry-credentials", attributes: { api_key: key } },
@@ -448,7 +429,7 @@ export async function submitRegistryCredential(
448429
const taskCompleted =
449430
submission.status === "pending" &&
450431
(await credentialTaskCompleted(submission.taskId));
451-
const credential = await readRegistryCredential(access.accessToken);
432+
const credential = await readRegistryCredential(access);
452433
if (credential.status !== REGISTRY_CREDENTIAL_READ.STATUS) return credential;
453434

454435
return credentialActionResult(
@@ -470,7 +451,7 @@ export async function disconnectRegistryCredential(): Promise<RegistryCredential
470451
cache: "no-store",
471452
headers: {
472453
Accept: "application/vnd.api+json",
473-
Authorization: `Bearer ${access.accessToken}`,
454+
Authorization: `Bearer ${access}`,
474455
},
475456
});
476457
} catch {
@@ -480,8 +461,8 @@ export async function disconnectRegistryCredential(): Promise<RegistryCredential
480461
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
481462
}
482463

483-
const credential = await readRegistryCredential(access.accessToken);
484-
const tenantArtifacts = await readRegistryTenantArtifacts(access.accessToken);
464+
const credential = await readRegistryCredential(access);
465+
const tenantArtifacts = await readRegistryTenantArtifacts(access);
485466
if (credential.status !== REGISTRY_CREDENTIAL_READ.STATUS) return credential;
486467
if (tenantArtifacts.status !== "ready") return tenantArtifacts;
487468
if (!response.ok) return { status: REGISTRY_FAILURE.ERROR };

ui/app/(prowler)/layout.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { ReactNode, Suspense } from "react";
66

77
import { getProviders } from "@/actions/providers";
88
import { getScansByState } from "@/actions/scans/scans";
9+
import { auth } from "@/auth.config";
910
import MainLayout from "@/components/layout/main-layout/main-layout";
1011
import {
1112
OnboardingCheckpointWatcher,
1213
OnboardingGate,
1314
OnboardingSequenceBanner,
1415
} from "@/components/onboarding";
15-
import { RegistryEligibilityProvider } from "@/components/registry/registry-eligibility-provider";
1616
import { RuntimePublicConfig } from "@/components/runtime-config/runtime-public-config";
1717
import { NavigationProgress } from "@/components/shadcn/navigation-progress";
1818
import { Toaster } from "@/components/shadcn/toast";
@@ -21,6 +21,8 @@ import { GlobalSidePanel } from "@/components/side-panel";
2121
import { FeedbackSurvey } from "@/components/survey/feedback-survey";
2222
import { fontMono, fontSans } from "@/config/fonts";
2323
import { siteConfig } from "@/config/site";
24+
import { REGISTRY_ACCESS } from "@/lib/registry/access";
25+
import { evaluateRegistryAccess } from "@/lib/registry/access.server";
2426
import { isCloud } from "@/lib/shared/env";
2527
import { cn } from "@/lib/utils";
2628
import { StoreInitializer } from "@/store/ui/store-initializer";
@@ -57,6 +59,13 @@ export default async function RootLayout({
5759
// Skip Cloud-only onboarding fetches and orchestrators in OSS.
5860
const cloudEnabled = isCloud();
5961

62+
// One-time server-side Registry gate per request: only an ELIGIBLE answer
63+
// shows the sidebar entry; UNKNOWN and INELIGIBLE both hide it. Started
64+
// here so it resolves in parallel with the Cloud onboarding fetches.
65+
const registryAccessPromise = auth().then((session) =>
66+
evaluateRegistryAccess(session?.accessToken),
67+
);
68+
6069
// Fail-open: unknown scan state is treated as "has data" so the banner never blocks
6170
// progression on a fetch error.
6271
let hasCompletedScan = true;
@@ -79,6 +88,9 @@ export default async function RootLayout({
7988
: undefined;
8089
}
8190

91+
const registryEligible =
92+
(await registryAccessPromise).status === REGISTRY_ACCESS.ELIGIBLE;
93+
8294
return (
8395
<html suppressHydrationWarning lang="en">
8496
<head>
@@ -99,7 +111,9 @@ export default async function RootLayout({
99111
<NavigationProgress />
100112
</Suspense>
101113
{/* Store uses boolean; gate receives tri-state to fail open on fetch errors. */}
102-
<StoreInitializer values={{ hasProviders: hasProviders ?? false }} />
114+
<StoreInitializer
115+
values={{ hasProviders: hasProviders ?? false, registryEligible }}
116+
/>
103117
{cloudEnabled && (
104118
<>
105119
<OnboardingGate hasProviders={hasProviders} />
@@ -109,9 +123,7 @@ export default async function RootLayout({
109123
<OnboardingSequenceBanner hasCompletedScan={hasCompletedScan} />
110124
</>
111125
)}
112-
<RegistryEligibilityProvider>
113-
<MainLayout>{children}</MainLayout>
114-
</RegistryEligibilityProvider>
126+
<MainLayout>{children}</MainLayout>
115127
{cloudEnabled && <FeedbackSurvey />}
116128
{/* Always mounted: it hosts the detail (finding/resource) views in
117129
every deployment; the AI tab inside is cloud-gated on its own. */}

ui/app/(prowler)/registry/page.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { redirect } from "next/navigation";
22

33
import { getRegistryBootstrap } from "@/actions/registry/registry";
4-
import { RegistryAccessBoundary } from "@/components/registry/registry-access-boundary";
54
import { RegistryExplorer } from "@/components/registry/registry-explorer";
65
import { ContentLayout } from "@/components/shadcn/content-layout/content-layout";
76
import { REGISTRY_FAILURE } from "@/types/registry";
@@ -14,11 +13,7 @@ export default async function RegistryPage() {
1413

1514
return (
1615
<ContentLayout title="Registry">
17-
<RegistryAccessBoundary
18-
initialLeaseDurationMs={bootstrap.leaseDurationMs}
19-
>
20-
<RegistryExplorer initialState={bootstrap.state} />
21-
</RegistryAccessBoundary>
16+
<RegistryExplorer initialState={bootstrap.state} />
2217
</ContentLayout>
2318
);
2419
}

0 commit comments

Comments
 (0)