mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-10 10:49:35 +00:00
feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community (#1758)
* feat(fleet-secrets): graduate encrypted fleet-wide environment bundles to Community * fix(fleet-secrets): update reachability test for Secrets community graduation * fix(fleet-secrets): address review findings * fix(fleet-secrets): add HTTP-level Community admin push/import tests and non-admin tab-hidden test
This commit is contained in:
@@ -72,7 +72,6 @@ export function FleetView({
|
||||
const containerLabelsEnabled = hasCapability('container-label-inventory');
|
||||
// Visual fail-closed while /meta loads; paid/admin gates still apply when on.
|
||||
const canDiscoverRouting = experimentalReady && experimental && isPaid;
|
||||
const canDiscoverSecrets = experimentalReady && experimental && isPaid && isAdmin;
|
||||
|
||||
const { prefs, updatePrefs } = useFleetPreferences();
|
||||
const updateStatus = useFleetUpdateStatus();
|
||||
@@ -104,9 +103,9 @@ export function FleetView({
|
||||
if (controlledTab === undefined) setInternalTab(tab);
|
||||
};
|
||||
|
||||
// Fall back only after experimental readiness settles. When experimental is
|
||||
// on, also wait for license (and admin for secrets) so a paid deep link is
|
||||
// not rewritten to Overview while isPaid is still the cold-load false.
|
||||
// Fall back Routing deep links when experimental/license gates resolve false.
|
||||
// Wait for license during cold load so a paid deep link is not rewritten to
|
||||
// Overview while isPaid is still the cold-load false.
|
||||
useEffect(() => {
|
||||
if (!experimentalReady) return;
|
||||
if (activeTab === 'routing') {
|
||||
@@ -116,19 +115,10 @@ export function FleetView({
|
||||
}
|
||||
if (licenseStatus !== 'ready') return;
|
||||
if (!isPaid) setActiveTab('overview');
|
||||
return;
|
||||
}
|
||||
if (activeTab === 'secrets') {
|
||||
if (!experimental) {
|
||||
setActiveTab('overview');
|
||||
return;
|
||||
}
|
||||
if (licenseStatus !== 'ready') return;
|
||||
if (!isPaid || !isAdmin) setActiveTab('overview');
|
||||
}
|
||||
// setActiveTab closes over onFleetActiveTabChange; listing deps explicitly.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [experimentalReady, experimental, licenseStatus, isPaid, isAdmin, activeTab]);
|
||||
}, [experimentalReady, experimental, licenseStatus, isPaid, activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fleetUpdatesIntent) {
|
||||
@@ -217,7 +207,7 @@ export function FleetView({
|
||||
<Wrench className="w-4 h-4 mr-1.5" />Actions
|
||||
</TabsTrigger>
|
||||
</TabsHighlightItem>
|
||||
{canDiscoverSecrets && (
|
||||
{isAdmin && (
|
||||
<TabsHighlightItem value="secrets">
|
||||
<TabsTrigger value="secrets">
|
||||
<KeyRound className="w-4 h-4 mr-1.5" />Secrets
|
||||
@@ -347,7 +337,7 @@ export function FleetView({
|
||||
unfiltered node list rather than the overview-filtered view. */}
|
||||
<FleetActionsTab nodes={overview.nodes} />
|
||||
</TabsContent>
|
||||
{canDiscoverSecrets && (
|
||||
{isAdmin && (
|
||||
<TabsContent value="secrets">
|
||||
<SecretsTab />
|
||||
</TabsContent>
|
||||
|
||||
@@ -8,10 +8,11 @@ vi.mock('@/hooks/useExperimental', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@/context/LicenseContext', () => ({
|
||||
useLicense: () => ({ isPaid: true }),
|
||||
useLicense: () => ({ isPaid: true, licenseStatus: 'ready' as const }),
|
||||
}));
|
||||
const useAuthMock = vi.fn(() => ({ isAdmin: true as boolean, can: () => true as boolean }));
|
||||
vi.mock('@/context/AuthContext', () => ({
|
||||
useAuth: () => ({ isAdmin: true, can: () => true }),
|
||||
useAuth: () => useAuthMock(),
|
||||
}));
|
||||
vi.mock('@/context/NodeContext', () => ({
|
||||
useNodes: () => ({ hasCapability: () => false }),
|
||||
@@ -100,22 +101,23 @@ vi.mock('../fleet/DependencyMapTab', () => ({ DependencyMapTab: () => null }));
|
||||
vi.mock('../fleet/ContainerLabelsTab', () => ({ ContainerLabelsTab: () => null }));
|
||||
vi.mock('../PaidGate', () => ({ PaidGate: ({ children }: { children: React.ReactNode }) => <>{children}</> }));
|
||||
|
||||
describe('FleetView experimental discovery', () => {
|
||||
describe('FleetView tab discovery and deep-link fallback', () => {
|
||||
beforeEach(() => {
|
||||
useExperimentalMock.mockReturnValue({ experimental: true, experimentalReady: true });
|
||||
useAuthMock.mockReturnValue({ isAdmin: true, can: () => true });
|
||||
});
|
||||
|
||||
it('shows Routing and Secrets when experimental discovery is on for paid admin', () => {
|
||||
it('shows Routing when experimental is on; Secrets always visible for Admin', () => {
|
||||
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
|
||||
expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides Routing and Secrets when experimental discovery is off', () => {
|
||||
it('hides Routing when experimental off; Secrets stays visible for Admin', () => {
|
||||
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
|
||||
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
|
||||
expect(screen.queryByRole('tab', { name: /routing/i })).toBeNull();
|
||||
expect(screen.queryByRole('tab', { name: /secrets/i })).toBeNull();
|
||||
expect(screen.getByRole('tab', { name: /secrets/i })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /federation/i })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: /actions/i })).toBeTruthy();
|
||||
@@ -173,4 +175,29 @@ describe('FleetView experimental discovery', () => {
|
||||
await waitFor(() => expect(onTab).toHaveBeenCalledWith('overview'));
|
||||
expect(onTab).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not rewrite a Secrets deep link for Admin when experimental is off', () => {
|
||||
useExperimentalMock.mockReturnValue({ experimental: false, experimentalReady: true });
|
||||
const onTab = vi.fn();
|
||||
render(
|
||||
<FleetView
|
||||
onNavigateToNode={vi.fn()}
|
||||
onOpenNodeNetworking={vi.fn()}
|
||||
fleetActiveTab="secrets"
|
||||
onFleetActiveTabChange={onTab}
|
||||
/>,
|
||||
);
|
||||
// No fallback rewrite; Secrets is always available to Admin
|
||||
expect(onTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides Secrets tab for non-admin', () => {
|
||||
useAuthMock.mockReturnValue({ isAdmin: false, can: () => false });
|
||||
render(<FleetView onNavigateToNode={vi.fn()} onOpenNodeNetworking={vi.fn()} />);
|
||||
expect(screen.queryByRole('tab', { name: /secrets/i })).toBeNull();
|
||||
// Routing still visible (gated on experimental + paid, not admin)
|
||||
expect(screen.getByRole('tab', { name: /routing/i })).toBeTruthy();
|
||||
// Unrelated tabs still visible
|
||||
expect(screen.getByRole('tab', { name: /deployments/i })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,19 +90,29 @@ describe('reachability', () => {
|
||||
expect(isViewHidden('audit-log', ctx({ isPaid: true, can: () => false }))).toBe(true);
|
||||
});
|
||||
|
||||
it('hides routing and secrets fleet tabs only after experimentalReady when off', () => {
|
||||
it('hides routing fleet tab only after experimentalReady when off; secrets always visible for admin', () => {
|
||||
const loading = ctx({ experimental: false, experimentalReady: false });
|
||||
expect(isFleetTabHidden('routing', loading)).toBe(false);
|
||||
expect(isFleetTabHidden('secrets', loading)).toBe(false);
|
||||
|
||||
const off = ctx({ experimental: false, experimentalReady: true });
|
||||
expect(isFleetTabHidden('routing', off)).toBe(true);
|
||||
expect(isFleetTabHidden('secrets', off)).toBe(true);
|
||||
expect(isFleetTabHidden('secrets', off)).toBe(false);
|
||||
expect(isFleetTabHidden('deployments', off)).toBe(false);
|
||||
expect(isFleetTabHidden('federation', off)).toBe(false);
|
||||
expect(isFleetTabHidden('actions', off)).toBe(false);
|
||||
});
|
||||
|
||||
it('hides secrets fleet tab for non-admin after authz ready', () => {
|
||||
// Cold load: permissions not ready, don't hide yet (deep link survives)
|
||||
const loading = ctx({ isAdmin: false, permissionsStatus: 'loading' });
|
||||
expect(isFleetTabHidden('secrets', loading)).toBe(false);
|
||||
|
||||
// Permissions settled: non-admin deep link normalizes to overview
|
||||
const ready = ctx({ isAdmin: false, permissionsStatus: 'ready' });
|
||||
expect(isFleetTabHidden('secrets', ready)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not hide fleet-mesh settings for experimental off', () => {
|
||||
const off = ctx({ experimental: false, experimentalReady: true,
|
||||
scheduledOpsAccessible: false, isAdmin: true });
|
||||
|
||||
@@ -65,8 +65,9 @@ export function isViewCapabilityLocked(view: ActiveView, ctx: ReachabilityContex
|
||||
export function isFleetTabHidden(tab: FleetTab, ctx: ReachabilityContext): boolean {
|
||||
if (!authzReady(ctx)) return false;
|
||||
if (tab === 'container-labels' && !ctx.containerLabelsEnabled) return true;
|
||||
if (tab === 'secrets' && !ctx.isAdmin) return true;
|
||||
// Defer experimental hide until ready so deep links survive cold load.
|
||||
if ((tab === 'routing' || tab === 'secrets') && experimentalDiscoveryReady(ctx) && !ctx.experimental) {
|
||||
if (tab === 'routing' && experimentalDiscoveryReady(ctx) && !ctx.experimental) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user