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:
Anso
2026-08-03 08:42:46 -04:00
committed by GitHub
parent 5240957ff3
commit 5f5cd5ea51
11 changed files with 183 additions and 73 deletions
+6 -16
View File
@@ -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();
});
});
+12 -2
View File
@@ -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 });
+2 -1
View File
@@ -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;