fix(stacks): require stack:read on file explorer GET routes (#1200)

The four file-explorer GET endpoints (list, content, download,
permissions) previously relied on auth alone. Their write-side siblings
required stack:edit, so the read path was the only file-explorer surface
without an explicit capability check. The shipped roles all carry
stack:read globally so behaviour is unchanged today, but adding the
guard prevents a future role definition from silently inheriting
unrestricted file reads, and it brings the file-explorer reads in line
with gitSources and stackActivity which already gate on stack:read.

The frontend Files tab trigger, panel, and the anatomy-panel "Open
Files" affordance now render only when the user holds stack:read for
the active stack. An effect canonicalises activeTab back to 'compose'
if the user lands on 'files' without permission, so a denied user
cannot end up staring at an empty panel.
This commit is contained in:
Anso
2026-05-24 22:58:28 -04:00
committed by GitHub
parent c67478b50d
commit 4c28b37a59
3 changed files with 56 additions and 8 deletions
@@ -319,6 +319,13 @@ export function EditorView({
const safeContent = content || '';
const safeEnvContent = envContent || '';
const isRunning = safeContainers.some(c => c.State === 'running');
const canRead = can('stack:read', 'stack', stackName);
useEffect(() => {
if (activeTab === 'files' && !canRead) {
setActiveTab('compose');
}
}, [activeTab, canRead, setActiveTab]);
return (
<ErrorBoundary>
@@ -712,12 +719,14 @@ export function EditorView({
<TabsHighlightItem value="env">
<TabsTrigger value="env" disabled={!envExists}>.env</TabsTrigger>
</TabsHighlightItem>
<TabsHighlightItem value="files">
<TabsTrigger value="files">
<FolderOpen className="w-3.5 h-3.5 mr-1" strokeWidth={1.5} />
Files
</TabsTrigger>
</TabsHighlightItem>
{canRead && (
<TabsHighlightItem value="files">
<TabsTrigger value="files">
<FolderOpen className="w-3.5 h-3.5 mr-1" strokeWidth={1.5} />
Files
</TabsTrigger>
</TabsHighlightItem>
)}
</TabsHighlight>
</TabsList>
</Tabs>
@@ -801,7 +810,7 @@ export function EditorView({
</div>
</div>
<div className="flex-1 min-h-0 flex flex-col">
{activeTab === 'files' ? (
{activeTab === 'files' && canRead ? (
<StackFileExplorer
stackName={stackName}
canEdit={can('stack:edit', 'stack', stackName)}
@@ -864,7 +873,7 @@ export function EditorView({
selectedEnvFile={selectedEnvFile}
gitSourcePending={Boolean(gitSourcePendingMap[stackName])}
onEditCompose={() => { setEditingCompose(true); setActiveTab('compose'); }}
onOpenFiles={() => { setEditingCompose(true); setActiveTab('files'); }}
onOpenFiles={canRead ? () => { setEditingCompose(true); setActiveTab('files'); } : undefined}
onOpenGitSource={() => setGitSourceOpen(true)}
onApplyUpdate={() => { void updateStack(); }}
canEdit={can('stack:edit', 'stack', stackName)}