mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-07 17:34:23 +00:00
117f590332
* fix(security): gate admin-only scan affordances on isAdmin Backend already required admin for SBOM, SARIF, scan policies, Trivy install/update/uninstall, the auto-update toggle, CVE suppressions, and misconfig acknowledgements. The matching frontend surfaces were gated only on isPaid (or only on isReplica), so non-admin users at the same tier saw buttons that returned 403 on click. Threads isAdmin from useAuth() into SecuritySection, SuppressionsPanel, and MisconfigAckPanel. Updates the scan-result sheet caller in ResourcesView so SBOM and SARIF render only when paid AND admin; passes canManageSuppressions to the stack-misconfig sheet so admins can ack misconfigs from that surface too. Read paths remain visible to non-admins (policy list, suppression list, ack list, scan history) since the GET routes are auth-only on both sides. * fix(security): close 3 remaining scan-sheet parity gaps from review Code review surfaced three sites missed in the first pass: 1. SecurityHistoryView opened the scan sheet with canGenerateSbom set only on isPaid, so Skipper non-admins saw SBOM and SARIF buttons even though the backend requires admin+paid. Now ANDed with isAdmin. 2. ResourcesView passed onRescan unconditionally, and the sheet renders a Re-scan primary action whenever onRescan is defined. Non-admins reaching the sheet via the severity-badge shortcut saw the button; clicking it called POST /security/scan, which the backend requires admin for. onRescan is now undefined for non-admins. 3. ShellOverlays and ResourcesView passed canManageSuppressions=isAdmin without considering the replica gate, so a replica admin saw suppress and ack columns whose backend writes blockIfReplica. The sheet now probes /fleet/role internally and ANDs !isReplica into the effective canManageSuppressions, so the column hides on a replica regardless of how the caller wired the prop. * fix(security): clear isReplica state on every scan-sheet probe The previous probe only flipped the state to true on a replica response and never wrote false on a control, non-OK, or skipped probe. With the sheet kept mounted by ResourcesView, SecurityHistoryView, and ShellOverlays, an admin who first viewed a scan on a replica would keep suppress/ack controls hidden even after switching to a control instance, because the stale true value persisted across re-opens. The effect now resets isReplica to false at the start of every probe and assigns the result of /fleet/role directly. Probe failures and skips leave the state at false, so the UI is permissive and the backend blockIfReplica guard remains the source of truth.
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import { lazy, Suspense } from 'react';
|
|
import BashExecModal from '../BashExecModal';
|
|
import LazyBoundary from '../LazyBoundary';
|
|
import { PolicyBlockDialog } from '../stack/PolicyBlockDialog';
|
|
import { DeleteStackDialog } from './DeleteStackDialog';
|
|
import { UnsavedChangesDialog } from './UnsavedChangesDialog';
|
|
import { StackAlertSheet } from '../StackAlertSheet';
|
|
import { GitSourcePanel } from '../stack/GitSourcePanel';
|
|
import { LogViewer } from '../LogViewer';
|
|
import { VulnerabilityScanSheet } from '../VulnerabilityScanSheet';
|
|
import { ComposeDiffPreviewDialog } from '@/components/ComposeDiffPreviewDialog';
|
|
import type { OverlayState } from './hooks/useOverlayState';
|
|
import type { StackActionsHook } from './hooks/useStackActions';
|
|
import type { PermissionAction } from '@/context/AuthContext';
|
|
|
|
// SecurityHistoryView is the only lazy-loaded view that lives outside
|
|
// the ViewRouter switch -- it renders as an overlay sheet wired into the
|
|
// settings flow, not as a top-level tab. The other tab-level lazy views
|
|
// (HostConsole, FleetView, AuditLogView, etc.) live inside ViewRouter.
|
|
const SecurityHistoryView = lazy(() =>
|
|
import('../SecurityHistoryView').then(m => ({ default: m.SecurityHistoryView })),
|
|
);
|
|
|
|
interface ShellOverlaysProps {
|
|
overlayState: OverlayState;
|
|
stackActions: StackActionsHook;
|
|
isDarkMode: boolean;
|
|
isAdmin: boolean;
|
|
can: (action: PermissionAction, resourceType?: string, resourceId?: string) => boolean;
|
|
selectedFile: string | null;
|
|
stackName: string;
|
|
gitSourceOpen: boolean;
|
|
setGitSourceOpen: (open: boolean) => void;
|
|
securityHistoryOpen: boolean;
|
|
setSecurityHistoryOpen: (open: boolean) => void;
|
|
}
|
|
|
|
export function ShellOverlays({
|
|
overlayState,
|
|
stackActions,
|
|
isDarkMode,
|
|
isAdmin,
|
|
can,
|
|
selectedFile,
|
|
stackName,
|
|
gitSourceOpen,
|
|
setGitSourceOpen,
|
|
securityHistoryOpen,
|
|
setSecurityHistoryOpen,
|
|
}: ShellOverlaysProps) {
|
|
const {
|
|
deleteDialogOpen, closeDeleteDialog, stackToDelete,
|
|
pendingUnsavedLoad,
|
|
bashModalOpen, selectedContainer,
|
|
logViewerOpen, logContainer,
|
|
stackMonitor, closeStackMonitor,
|
|
policyBlock, setPolicyBlock, policyBypassing,
|
|
stackMisconfigScanId, setStackMisconfigScanId,
|
|
diffPreview, setDiffPreview, diffPreviewConfirming, setDiffPreviewConfirming,
|
|
} = overlayState;
|
|
|
|
return (
|
|
<>
|
|
<DeleteStackDialog
|
|
open={deleteDialogOpen}
|
|
onOpenChange={(open) => { if (!open) closeDeleteDialog(); }}
|
|
stackName={stackToDelete}
|
|
onConfirm={stackActions.deleteStack}
|
|
/>
|
|
|
|
<UnsavedChangesDialog
|
|
open={!!pendingUnsavedLoad}
|
|
onCancel={stackActions.cancelPendingUnsavedLoad}
|
|
onConfirm={stackActions.discardAndLoadPending}
|
|
/>
|
|
|
|
{/* Bash Exec Modal */}
|
|
{selectedContainer && (
|
|
<BashExecModal
|
|
isOpen={bashModalOpen}
|
|
onClose={stackActions.closeBashModal}
|
|
containerId={selectedContainer.id}
|
|
containerName={selectedContainer.name}
|
|
/>
|
|
)}
|
|
|
|
{/* LogViewer Modal */}
|
|
{logContainer && (
|
|
<LogViewer
|
|
isOpen={logViewerOpen}
|
|
onClose={stackActions.closeLogViewer}
|
|
containerId={logContainer.id}
|
|
containerName={logContainer.name}
|
|
/>
|
|
)}
|
|
|
|
{/* Stack monitor (alerts + auto-heal as tabs) */}
|
|
<StackAlertSheet
|
|
open={stackMonitor !== null}
|
|
onOpenChange={(open) => { if (!open) closeStackMonitor(); }}
|
|
stackName={stackMonitor?.stackName ?? ''}
|
|
initialTab={stackMonitor?.tab ?? 'alerts'}
|
|
/>
|
|
|
|
{/* Pre-deploy policy block */}
|
|
<PolicyBlockDialog
|
|
open={policyBlock !== null}
|
|
payload={policyBlock?.payload ?? null}
|
|
stackName={policyBlock?.stackName ?? ''}
|
|
canBypass={isAdmin}
|
|
bypassing={policyBypassing}
|
|
onClose={() => setPolicyBlock(null)}
|
|
onBypass={stackActions.bypassPolicyAndDeploy}
|
|
/>
|
|
|
|
{/* Git Source Panel */}
|
|
{stackName && (
|
|
<GitSourcePanel
|
|
open={gitSourceOpen}
|
|
onOpenChange={setGitSourceOpen}
|
|
stackName={stackName}
|
|
canEdit={can('stack:edit', 'stack', stackName)}
|
|
isDarkMode={isDarkMode}
|
|
onSourceChanged={stackActions.refreshGitSourcePending}
|
|
/>
|
|
)}
|
|
|
|
{/* Stack config misconfig scan results */}
|
|
<VulnerabilityScanSheet
|
|
scanId={stackMisconfigScanId}
|
|
onClose={() => setStackMisconfigScanId(null)}
|
|
canManageSuppressions={isAdmin}
|
|
/>
|
|
|
|
{/* Compose diff preview */}
|
|
<ComposeDiffPreviewDialog
|
|
open={diffPreview !== null}
|
|
onOpenChange={(open) => { if (!open && !diffPreviewConfirming) setDiffPreview(null); }}
|
|
stackName={selectedFile ? selectedFile.replace(/\.(yml|yaml)$/, '') : ''}
|
|
fileName={diffPreview?.fileName ?? ''}
|
|
language={diffPreview?.language ?? 'yaml'}
|
|
original={diffPreview?.original ?? ''}
|
|
modified={diffPreview?.modified ?? ''}
|
|
actionLabel={diffPreview?.mode === 'save-and-deploy' ? 'Save & deploy' : 'Save'}
|
|
confirming={diffPreviewConfirming}
|
|
isDarkMode={isDarkMode}
|
|
onConfirm={async () => {
|
|
const snapshot = diffPreview;
|
|
setDiffPreviewConfirming(true);
|
|
try {
|
|
if (snapshot?.mode === 'save-and-deploy') {
|
|
const saved = await stackActions.saveFile();
|
|
if (saved) await stackActions.deployStack();
|
|
} else {
|
|
await stackActions.saveFile();
|
|
}
|
|
} finally {
|
|
setDiffPreviewConfirming(false);
|
|
setDiffPreview(null);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* Scan history overlay. Conditionally mounted so the lazy chunk
|
|
only fetches when the user opens the overlay; an always-mounted
|
|
lazy component would fetch on EditorLayout's first render and
|
|
defeat the split. The overlay has no internal state that needs
|
|
to persist across opens. */}
|
|
{securityHistoryOpen ? (
|
|
<LazyBoundary>
|
|
<Suspense fallback={null}>
|
|
<SecurityHistoryView
|
|
open
|
|
onClose={() => setSecurityHistoryOpen(false)}
|
|
/>
|
|
</Suspense>
|
|
</LazyBoundary>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|