mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Hide admin operations from public demo
This commit is contained in:
@@ -432,6 +432,11 @@ needs them, but `/api/license/commercial-posture`,
|
||||
public demo mode and those lifecycle flows must not depend on licensed
|
||||
identity, plan labels, upgrade reasons, checkout handoff state, or observed
|
||||
usage counts surviving the public-demo contract.
|
||||
That same demo-hidden API boundary also keeps runtime-admin operations out of
|
||||
public lifecycle flows: `/api/diagnostics`,
|
||||
`/api/diagnostics/docker/prepare-token`, and `/api/logs/*` must return `404`
|
||||
in demo mode instead of exposing runtime bundles, log streams, or diagnostics
|
||||
payloads through a nominally read-only preview account.
|
||||
Lifecycle-owned browser shells must also defer any commercial helper reads
|
||||
until that presentation policy resolves so demo suppression stays fail-closed
|
||||
during first render instead of racing hidden commercial endpoints from shared
|
||||
|
||||
@@ -228,6 +228,10 @@ organization chrome: `frontend-modern/src/App.tsx` and
|
||||
`frontend-modern/src/AppLayout.tsx` may hide org switchers or demo-only org
|
||||
labels, but they must not couple assistant visibility, session reset, or
|
||||
drawer-open behavior to that organization presentation state.
|
||||
That same shell boundary also owns demo-only Operations suppression:
|
||||
`frontend-modern/src/AppLayout.tsx` may remove the top-level Operations route
|
||||
from the public demo shell, but assistant availability and reset behavior must
|
||||
stay independent of that utility-tab presentation choice.
|
||||
Authenticated `/login` recovery belongs to that same route shell boundary:
|
||||
once login succeeds, `frontend-modern/src/App.tsx` must resolve `/login`
|
||||
through the canonical post-auth landing route instead of leaving the
|
||||
|
||||
@@ -1348,6 +1348,12 @@ That same diagnostics boundary must also backfill canonical fallback reasons
|
||||
when a raw snapshot reaches the API layer without one, so
|
||||
`buildMemorySourceDiagnostics` stays self-consistent even if a caller bypasses
|
||||
`GetDiagnosticSnapshots()` and hands diagnostics a legacy alias directly.
|
||||
That same public-demo API boundary must also hide runtime-admin operations
|
||||
surfaces instead of treating them as harmless reads. Demo sessions must receive
|
||||
`404` for `/api/diagnostics`, `/api/diagnostics/docker/prepare-token`, and the
|
||||
shared `/api/logs/*` endpoints, so the preview shell cannot expose runtime
|
||||
diagnostics, log streams, or downloadable log bundles behind a supposedly
|
||||
read-only demo account.
|
||||
That shared `InfrastructureOperationsController.tsx` boundary now also preserves copied shell
|
||||
command payload continuity: any privilege-escalation wrapper applied at the
|
||||
settings surface must keep the full canonical installer argument list intact
|
||||
|
||||
@@ -228,6 +228,10 @@ trial urgency, observed usage counts, or checkout handoff state. The
|
||||
commercial posture store and billing-entitlements store must also fail closed
|
||||
locally until the shared presentation policy resolves, then stay fail-closed
|
||||
in demo mode so hidden routes are not probed from the browser shell.
|
||||
That same browser-shell boundary also owns utility-nav suppression:
|
||||
`frontend-modern/src/AppLayout.tsx` must drop the top-level Operations tab in
|
||||
public demo mode instead of leaving diagnostics or system-log shells
|
||||
discoverable after commercial surfaces are hidden.
|
||||
Deep-linkable commercial panels must consume the same resolved presentation
|
||||
policy directly, not rely only on settings navigation to keep public-demo
|
||||
browsers away from commercial routes. `ProLicensePanel` may instantiate its
|
||||
|
||||
@@ -179,6 +179,11 @@ work extends shared components instead of creating new local variants.
|
||||
posture: `/alerts` may continue exposing reporting tabs such as overview and
|
||||
history, but activation controls plus configuration routes must collapse out
|
||||
of the public-demo shell instead of advertising blocked management actions.
|
||||
That same public-demo presentation boundary also owns top-level Operations
|
||||
posture: the authenticated demo shell must not advertise the Operations
|
||||
utility tab, and `/operations` deep links must hand back to the dashboard
|
||||
instead of surfacing diagnostics or system-log chrome that the backend hides
|
||||
for demo sessions.
|
||||
3. Add feature-specific presentation only when no shared primitive should own it
|
||||
4. Add guardrail tests when a new shared pattern is introduced
|
||||
5. Keep shared platform-connections shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, and `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source instead of creating provider-local summary fetches or VMware-only shell vocabulary.
|
||||
@@ -1183,7 +1188,11 @@ the tabbed operations surface, and
|
||||
`frontend-modern/src/features/operations/operationsPageModel.ts` owns the tab
|
||||
and path contract. The operations route must keep its navigation routed through
|
||||
the shared `frontend-modern/src/components/shared/Subtabs.tsx` primitive rather
|
||||
than rebuilding a bespoke page-local tab bar.
|
||||
than rebuilding a bespoke page-local tab bar. When the session presentation
|
||||
policy marks the operator as a public demo viewer, that same route owner must
|
||||
suppress the surface entirely and hand the browser back to the canonical
|
||||
dashboard route instead of rendering diagnostics, reporting, or logs shells
|
||||
that are unavailable in demo mode.
|
||||
|
||||
The dashboard overview route now follows that same feature-owner pattern for
|
||||
its dashboard-specific summary surfaces. `frontend-modern/src/pages/Dashboard.tsx`
|
||||
|
||||
@@ -104,6 +104,11 @@ querying, and the operator-facing storage health presentation layer.
|
||||
payloads must read through `GetUnifiedReadStateOrSnapshot()` so storage and
|
||||
recovery consumers stay aligned with the canonical mock unified snapshot
|
||||
instead of slipping onto the live store graph.
|
||||
That same `internal/api/` demo boundary must keep runtime-admin operations
|
||||
hidden from public preview sessions: `/api/diagnostics`,
|
||||
`/api/diagnostics/docker/prepare-token`, and `/api/logs/*` must not remain
|
||||
readable side channels while storage or recovery demo routes are otherwise
|
||||
presented as read-only product surfaces.
|
||||
Storage and recovery consumers must also inherit the hook's canonical
|
||||
`ResourceType` normalization for route/query filters, so storage subtypes
|
||||
such as `physical_disk` stay on the same cache-backed snapshot instead of
|
||||
|
||||
@@ -40,7 +40,10 @@ import { getKioskModePreference, setKioskMode } from '@/utils/url';
|
||||
import { updateStore } from '@/stores/updates';
|
||||
import { aiChatStore } from '@/stores/aiChat';
|
||||
import { isPro } from '@/stores/licenseCommercial';
|
||||
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
|
||||
import {
|
||||
presentationPolicyHidesUpgradePrompts,
|
||||
presentationPolicyIsDemoMode,
|
||||
} from '@/stores/sessionPresentationPolicy';
|
||||
import type { AppConnectionStatus } from '@/useAppRuntimeState';
|
||||
|
||||
const ROOT_INFRASTRUCTURE_PATH = buildInfrastructurePath();
|
||||
@@ -422,7 +425,10 @@ export function AppLayout(props: AppLayoutProps) {
|
||||
breakdown: undefined,
|
||||
icon: <PulsePatrolLogo class="w-4 h-4 shrink-0" />,
|
||||
},
|
||||
{
|
||||
];
|
||||
|
||||
if (!presentationPolicyIsDemoMode()) {
|
||||
tabs.push({
|
||||
id: 'operations',
|
||||
label: 'Operations',
|
||||
route: '/operations',
|
||||
@@ -431,8 +437,8 @@ export function AppLayout(props: AppLayoutProps) {
|
||||
count: undefined,
|
||||
breakdown: undefined,
|
||||
icon: <ActivityIcon class="w-4 h-4 shrink-0" />,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (hasSettingsAccess) {
|
||||
tabs.push({
|
||||
|
||||
@@ -75,6 +75,8 @@ describe('App architecture', () => {
|
||||
expect(appLayoutSource).not.toContain('sessionPresentationPolicyResolved');
|
||||
expect(appLayoutSource).not.toContain('presentationPolicyHidesCommercialSurfaces');
|
||||
expect(appLayoutSource).not.toContain('presentationPolicyHidesOrganizationSurfaces');
|
||||
expect(appLayoutSource).toContain('presentationPolicyIsDemoMode');
|
||||
expect(appLayoutSource).toContain("if (!presentationPolicyIsDemoMode()) {");
|
||||
expect(appLayoutSource).toContain(
|
||||
'aiChatStore.enabled === true && !aiChatStore.isOpenSignal() && !kioskMode()',
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Suspense, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { Show, Suspense, createEffect, createMemo, type Component, type JSX } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import ActivityIcon from 'lucide-solid/icons/activity';
|
||||
import FileTextIcon from 'lucide-solid/icons/file-text';
|
||||
@@ -7,9 +7,12 @@ import { DiagnosticsPanel } from '@/components/Settings/DiagnosticsPanel';
|
||||
import { ReportingPanel } from '@/components/Settings/ReportingPanel';
|
||||
import { SystemLogsPanel } from '@/components/Settings/SystemLogsPanel';
|
||||
import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs';
|
||||
import { DASHBOARD_PATH } from '@/routing/resourceLinks';
|
||||
import { presentationPolicyIsDemoMode } from '@/stores/sessionPresentationPolicy';
|
||||
import {
|
||||
buildOperationsPath,
|
||||
getOperationsTabFromPath,
|
||||
operationsSurfaceHiddenInDemoMode,
|
||||
OPERATIONS_TABS,
|
||||
type OperationsTabId,
|
||||
} from '@/features/operations/operationsPageModel';
|
||||
@@ -24,21 +27,32 @@ export function OperationsPageSurface() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const hiddenInDemoMode = createMemo(() =>
|
||||
operationsSurfaceHiddenInDemoMode(presentationPolicyIsDemoMode()),
|
||||
);
|
||||
const activeTab = createMemo(() => getOperationsTabFromPath(location.pathname));
|
||||
|
||||
createEffect(() => {
|
||||
if (hiddenInDemoMode()) {
|
||||
navigate(DASHBOARD_PATH, { replace: true });
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = createMemo<SubtabOption[]>(() =>
|
||||
OPERATIONS_TABS.map((tab) => {
|
||||
const Icon = operationsTabIcons[tab.id];
|
||||
return {
|
||||
value: tab.id,
|
||||
label: (
|
||||
<span class="inline-flex items-center gap-2.5" title={tab.description}>
|
||||
<Icon class="h-4 w-4" />
|
||||
<span>{tab.label}</span>
|
||||
</span>
|
||||
) satisfies JSX.Element,
|
||||
};
|
||||
}),
|
||||
hiddenInDemoMode()
|
||||
? []
|
||||
: OPERATIONS_TABS.map((tab) => {
|
||||
const Icon = operationsTabIcons[tab.id];
|
||||
return {
|
||||
value: tab.id,
|
||||
label: (
|
||||
<span class="inline-flex items-center gap-2.5" title={tab.description}>
|
||||
<Icon class="h-4 w-4" />
|
||||
<span>{tab.label}</span>
|
||||
</span>
|
||||
) satisfies JSX.Element,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
@@ -46,33 +60,35 @@ export function OperationsPageSurface() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<div class="mb-6">
|
||||
<Subtabs
|
||||
value={activeTab()}
|
||||
onChange={handleTabChange}
|
||||
tabs={tabs()}
|
||||
ariaLabel="Operations"
|
||||
class="rounded-md border border-border bg-surface-alt p-1.5 sm:w-max"
|
||||
listClass="gap-2 overflow-x-auto scrollbar-hide"
|
||||
tabClass="min-h-10 whitespace-nowrap rounded-md border border-transparent px-4 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Show when={!hiddenInDemoMode()}>
|
||||
<div class="space-y-6">
|
||||
<div class="mb-6">
|
||||
<Subtabs
|
||||
value={activeTab()}
|
||||
onChange={handleTabChange}
|
||||
tabs={tabs()}
|
||||
ariaLabel="Operations"
|
||||
class="rounded-md border border-border bg-surface-alt p-1.5 sm:w-max"
|
||||
listClass="gap-2 overflow-x-auto scrollbar-hide"
|
||||
tabClass="min-h-10 whitespace-nowrap rounded-md border border-transparent px-4 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 animate-fade-in animate-duration-200">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex justify-center p-6">
|
||||
<div class="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{activeTab() === 'diagnostics' && <DiagnosticsPanel />}
|
||||
{activeTab() === 'reporting' && <ReportingPanel />}
|
||||
{activeTab() === 'logs' && <SystemLogsPanel />}
|
||||
</Suspense>
|
||||
<div class="mt-4 animate-fade-in animate-duration-200">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="flex justify-center p-6">
|
||||
<div class="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{activeTab() === 'diagnostics' && <DiagnosticsPanel />}
|
||||
{activeTab() === 'reporting' && <ReportingPanel />}
|
||||
{activeTab() === 'logs' && <SystemLogsPanel />}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { OperationsPageSurface } from '@/features/operations/OperationsPageSurface';
|
||||
|
||||
const navigateSpy = vi.hoisted(() => vi.fn());
|
||||
const presentationPolicyIsDemoModeMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const locationState = vi.hoisted(() => ({
|
||||
pathname: '/operations',
|
||||
hash: '',
|
||||
search: '',
|
||||
query: {},
|
||||
}));
|
||||
|
||||
vi.mock('@solidjs/router', async () => {
|
||||
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
|
||||
return {
|
||||
...actual,
|
||||
useLocation: () => locationState,
|
||||
useNavigate: () => navigateSpy,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/stores/sessionPresentationPolicy', () => ({
|
||||
presentationPolicyIsDemoMode: () => presentationPolicyIsDemoModeMock(),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Settings/DiagnosticsPanel', () => ({
|
||||
DiagnosticsPanel: () => <div data-testid="diagnostics-panel">Diagnostics</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Settings/ReportingPanel', () => ({
|
||||
ReportingPanel: () => <div data-testid="reporting-panel">Reporting</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Settings/SystemLogsPanel', () => ({
|
||||
SystemLogsPanel: () => <div data-testid="system-logs-panel">Logs</div>,
|
||||
}));
|
||||
|
||||
describe('OperationsPageSurface demo mode', () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
navigateSpy.mockReset();
|
||||
presentationPolicyIsDemoModeMock.mockReset();
|
||||
presentationPolicyIsDemoModeMock.mockReturnValue(false);
|
||||
locationState.pathname = '/operations';
|
||||
locationState.hash = '';
|
||||
locationState.search = '';
|
||||
});
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('redirects demo sessions back to the dashboard and hides operations chrome', async () => {
|
||||
presentationPolicyIsDemoModeMock.mockReturnValue(true);
|
||||
|
||||
render(() => <OperationsPageSurface />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/dashboard', { replace: true });
|
||||
});
|
||||
expect(screen.queryByText('Diagnostics & Health')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('diagnostics-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps operations tabs available outside demo mode', async () => {
|
||||
render(() => <OperationsPageSurface />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Diagnostics & Health')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('diagnostics-panel')).toBeInTheDocument();
|
||||
expect(navigateSpy).not.toHaveBeenCalledWith('/dashboard', { replace: true });
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,10 @@ export const OPERATIONS_TABS: readonly OperationsTabDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export function operationsSurfaceHiddenInDemoMode(demoMode: boolean): boolean {
|
||||
return demoMode;
|
||||
}
|
||||
|
||||
export function getOperationsTabFromPath(pathname: string): OperationsTabId {
|
||||
const lastPathSegment = pathname.split('/').pop() || '';
|
||||
if (lastPathSegment === 'reporting') return 'reporting';
|
||||
|
||||
@@ -17,8 +17,10 @@ describe('operations page route shell', () => {
|
||||
expect(operationsPageSurfaceSource).toContain('@/components/shared/Subtabs');
|
||||
expect(operationsPageSurfaceSource).toContain('getOperationsTabFromPath');
|
||||
expect(operationsPageSurfaceSource).toContain('buildOperationsPath');
|
||||
expect(operationsPageSurfaceSource).toContain('operationsSurfaceHiddenInDemoMode');
|
||||
expect(operationsPageSurfaceSource).not.toContain('-webkit-overflow-scrolling');
|
||||
expect(operationsPageModelSource).toContain('export const OPERATIONS_TABS');
|
||||
expect(operationsPageModelSource).toContain('export function operationsSurfaceHiddenInDemoMode');
|
||||
expect(operationsPageModelSource).toContain('export function getOperationsTabFromPath');
|
||||
expect(operationsPageModelSource).toContain('export function buildOperationsPath');
|
||||
});
|
||||
|
||||
@@ -6193,6 +6193,12 @@ func TestContract_DemoModeCommercialSurfacePolicy(t *testing.T) {
|
||||
{method: http.MethodPut, path: "/api/admin/orgs/t-tenant/billing-state"},
|
||||
{method: http.MethodGet, path: "/api/upgrade-metrics/stats"},
|
||||
{method: http.MethodPost, path: "/api/upgrade-metrics/events"},
|
||||
{method: http.MethodGet, path: "/api/diagnostics"},
|
||||
{method: http.MethodPost, path: "/api/diagnostics/docker/prepare-token"},
|
||||
{method: http.MethodGet, path: "/api/logs/stream"},
|
||||
{method: http.MethodGet, path: "/api/logs/download"},
|
||||
{method: http.MethodGet, path: "/api/logs/level"},
|
||||
{method: http.MethodPost, path: "/api/logs/level"},
|
||||
{method: http.MethodGet, path: licensePurchaseStartPath},
|
||||
{method: http.MethodGet, path: "/auth/trial-activate"},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ func DemoModeMiddleware(cfg *config.Config, next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
}
|
||||
if exposure, ok := publicDemoAdminOperationsPolicyForRequest(r); ok {
|
||||
if exposure == publicDemoCommercialExposureHidden {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Allow GET and HEAD requests (read-only)
|
||||
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
|
||||
|
||||
@@ -64,6 +64,12 @@ func TestDemoModeMiddleware(t *testing.T) {
|
||||
{"demo on hidden truenas saved preview", true, http.MethodPost, "/api/truenas/connections/conn-1/preview", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden vmware draft preview", true, http.MethodPost, "/api/vmware/connections/preview", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden vmware saved preview", true, http.MethodPost, "/api/vmware/connections/conn-1/preview", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden diagnostics", true, http.MethodGet, "/api/diagnostics", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden diagnostics token prepare", true, http.MethodPost, "/api/diagnostics/docker/prepare-token", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden logs stream", true, http.MethodGet, "/api/logs/stream", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden logs download", true, http.MethodGet, "/api/logs/download", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden logs level read", true, http.MethodGet, "/api/logs/level", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden logs level write", true, http.MethodPost, "/api/logs/level", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden checkout start", true, http.MethodGet, "/auth/license-purchase-start", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden license activate", true, http.MethodPost, "/api/license/activate", "", false, http.StatusNotFound, true},
|
||||
{"demo on hidden purchase start", true, http.MethodGet, licensePurchaseStartPath, "", false, http.StatusNotFound, true},
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package api
|
||||
|
||||
import "net/http"
|
||||
|
||||
var publicDemoAdminOperationsPolicies = []publicDemoCommercialRoutePolicy{
|
||||
{
|
||||
route: "GET /api/diagnostics",
|
||||
exposure: publicDemoCommercialExposureHidden,
|
||||
matches: exactDemoCommercialMethodPath(http.MethodGet, "/api/diagnostics"),
|
||||
},
|
||||
{
|
||||
route: "POST /api/diagnostics/docker/prepare-token",
|
||||
exposure: publicDemoCommercialExposureHidden,
|
||||
matches: exactDemoCommercialMethodPath(http.MethodPost, "/api/diagnostics/docker/prepare-token"),
|
||||
},
|
||||
{
|
||||
route: "GET /api/logs/stream",
|
||||
exposure: publicDemoCommercialExposureHidden,
|
||||
matches: exactDemoCommercialMethodPath(http.MethodGet, "/api/logs/stream"),
|
||||
},
|
||||
{
|
||||
route: "GET /api/logs/download",
|
||||
exposure: publicDemoCommercialExposureHidden,
|
||||
matches: exactDemoCommercialMethodPath(http.MethodGet, "/api/logs/download"),
|
||||
},
|
||||
{
|
||||
route: "GET /api/logs/level",
|
||||
exposure: publicDemoCommercialExposureHidden,
|
||||
matches: exactDemoCommercialMethodPath(http.MethodGet, "/api/logs/level"),
|
||||
},
|
||||
{
|
||||
route: "POST /api/logs/level",
|
||||
exposure: publicDemoCommercialExposureHidden,
|
||||
matches: exactDemoCommercialMethodPath(http.MethodPost, "/api/logs/level"),
|
||||
},
|
||||
}
|
||||
|
||||
func publicDemoAdminOperationsPolicyForRequest(
|
||||
r *http.Request,
|
||||
) (publicDemoCommercialExposure, bool) {
|
||||
for _, policy := range publicDemoAdminOperationsPolicies {
|
||||
if policy.matches != nil && policy.matches(r) {
|
||||
return policy.exposure, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPublicDemoAdminOperationsPolicyForRequest(t *testing.T) {
|
||||
hiddenRoutes := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{name: "diagnostics", method: http.MethodGet, path: "/api/diagnostics"},
|
||||
{name: "diagnostics token prepare", method: http.MethodPost, path: "/api/diagnostics/docker/prepare-token"},
|
||||
{name: "logs stream", method: http.MethodGet, path: "/api/logs/stream"},
|
||||
{name: "logs download", method: http.MethodGet, path: "/api/logs/download"},
|
||||
{name: "logs level read", method: http.MethodGet, path: "/api/logs/level"},
|
||||
{name: "logs level write", method: http.MethodPost, path: "/api/logs/level"},
|
||||
}
|
||||
|
||||
for _, tc := range hiddenRoutes {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.path, nil)
|
||||
exposure, ok := publicDemoAdminOperationsPolicyForRequest(req)
|
||||
if !ok {
|
||||
t.Fatalf("%s %s did not match public demo admin operations policy", tc.method, tc.path)
|
||||
}
|
||||
if exposure != publicDemoCommercialExposureHidden {
|
||||
t.Fatalf("%s %s exposure=%q, want %q", tc.method, tc.path, exposure, publicDemoCommercialExposureHidden)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
allowedRoutes := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{name: "health", method: http.MethodGet, path: "/api/health"},
|
||||
{name: "resources", method: http.MethodGet, path: "/api/resources"},
|
||||
{name: "runtime capabilities", method: http.MethodGet, path: "/api/license/runtime-capabilities"},
|
||||
}
|
||||
|
||||
for _, tc := range allowedRoutes {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.path, nil)
|
||||
if exposure, ok := publicDemoAdminOperationsPolicyForRequest(req); ok {
|
||||
t.Fatalf("%s %s unexpectedly matched public demo admin operations policy with exposure=%q", tc.method, tc.path, exposure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user