feat(rbac): add Deployer & Node Admin roles with scoped permissions (Team Pro) (#253)

* feat(rbac): add Deployer & Node Admin roles with scoped permissions (Team Pro)

Add intermediate RBAC roles gated to Team Pro tier:
- Deployer: can deploy/restart/stop/start stacks but cannot edit compose files, delete stacks, or access system settings
- Node Admin: full stack and node management within scope, no system settings access
- Scoped permissions: assign roles per-stack or per-node for fine-grained access control
- Permission engine with checkPermission/requirePermission guards replacing requireAdmin on stack/node routes
- Frontend can() function with /api/permissions/me endpoint for client-side permission checks
- User management UI updated with 4-role selector and scoped permission editor
- Documentation updated with permission matrix, scoped permission docs, and screenshots

* fix(rbac): remove unused RoleAssignment import to fix lint error
This commit is contained in:
Anso
2026-03-29 17:02:56 -04:00
committed by GitHub
parent 37701d5281
commit 8380fbad4b
10 changed files with 636 additions and 88 deletions
+7 -7
View File
@@ -78,7 +78,7 @@ const formatBytes = (bytes: number) => {
};
export default function EditorLayout() {
const { isAdmin } = useAuth();
const { isAdmin, can } = useAuth();
const { isPro, license } = useLicense();
const { nodes, activeNode, setActiveNode } = useNodes();
// Stable ref so notification callbacks always read the latest nodes list
@@ -1128,7 +1128,7 @@ export default function EditorLayout() {
)}
{/* Create Stack Button */}
{isAdmin && <div className="p-4">
{can('stack:create') && <div className="p-4">
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogTrigger asChild>
<Button className="w-full rounded-lg">
@@ -1234,7 +1234,7 @@ export default function EditorLayout() {
<Download className="h-4 w-4 mr-2" />
Update
</DropdownMenuItem>
{isAdmin && (
{can('stack:delete', 'stack', file.replace(/\.(yml|yaml)$/, '')) && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -1282,7 +1282,7 @@ export default function EditorLayout() {
<Download className="h-4 w-4 mr-2" />
Update
</ContextMenuItem>
{isAdmin && (
{can('stack:delete', 'stack', file.replace(/\.(yml|yaml)$/, '')) && (
<>
<ContextMenuSeparator />
<ContextMenuItem
@@ -1490,7 +1490,7 @@ export default function EditorLayout() {
{/* Stack Name */}
<CardTitle className="text-2xl font-bold">{stackName}</CardTitle>
{/* Action Bar */}
{isAdmin && (
{can('stack:deploy', 'stack', stackName) && (
<div className="flex items-center gap-2 flex-wrap">
{isRunning ? (
<>
@@ -1712,7 +1712,7 @@ export default function EditorLayout() {
</Select>
)}
</div>
{isAdmin && (
{can('stack:edit', 'stack', stackName) && (
<div className="flex gap-2">
{!isEditing ? (
<Button size="sm" variant="default" className="rounded-lg" onClick={enterEditMode}>
@@ -1767,7 +1767,7 @@ export default function EditorLayout() {
fontSize: 14,
padding: { top: 10 },
scrollBeyondLastLine: false,
readOnly: !isEditing || !isAdmin,
readOnly: !isEditing || !can('stack:edit', 'stack', stackName),
}}
/>
)}