mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 03:36:59 +00:00
feat(rbac): make stack-scoped grants node-specific (#1727)
* feat(rbac): make stack-scoped grants node-specific Qualify stack role assignments as (nodeId, stackName), migrate legacy rows to the default node, and forward bound multi-action evidence on Proxy/Pilot hops so scoped users keep least-privilege remote access without shipping the full grant table. * fix: mirror scoped-stack-auth-evidence capability to frontend, sanitize node id in role assignment log Backend added the scoped-stack-auth-evidence capability without the matching frontend entry, failing the capability parity test. The role assignment log also interpolated the node id without sanitizeForLog, unlike the rest of the line. * fix(rbac): honor node-wide scopes and fix proxied DELETE cleanup Node-scoped grants now authorize that role's stack actions on the same node in the backend resolver, frontend can(), and remote evidence. Proxied DELETE cleanup uses the gate-stashed route because pathRewrite mutates req.path before proxyRes. Add proxy integration coverage and drop the stale scoped-permissions screenshot. * fix(rbac): preserve node-qualified grants during repair
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||
import { markMilestone } from '@/lib/hydrationTiming';
|
||||
import { resolveCan } from '@/lib/resolveCan';
|
||||
|
||||
type AppStatus = 'loading' | 'needsSetup' | 'notAuthenticated' | 'mfaChallenge' | 'authenticated';
|
||||
|
||||
@@ -33,7 +34,7 @@ interface AuthContextType {
|
||||
permissions: PermissionsData | null;
|
||||
permissionsStatus: PermissionsStatus;
|
||||
permissionsReady: boolean;
|
||||
can: (action: PermissionAction, resourceType?: string, resourceId?: string) => boolean;
|
||||
can: (action: PermissionAction, resourceType?: string, resourceId?: string, nodeId?: number | null) => boolean;
|
||||
login: (username: string, password: string, remember?: boolean) => Promise<{ success: boolean; error?: string; mfaRequired?: boolean }>;
|
||||
ssoLdapLogin: (username: string, password: string, remember?: boolean) => Promise<{ success: boolean; error?: string; mfaRequired?: boolean }>;
|
||||
submitMfa: (code: string, opts?: { isBackupCode?: boolean }) => Promise<{ success: boolean; error?: string; retryAfter?: number }>;
|
||||
@@ -128,20 +129,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
return () => window.removeEventListener('sencho-unauthorized', handleUnauthorized);
|
||||
}, []);
|
||||
|
||||
const can = useCallback((action: PermissionAction, resourceType?: string, resourceId?: string): boolean => {
|
||||
if (!permissions) return false;
|
||||
|
||||
if (permissions.globalRole === 'admin') return true;
|
||||
|
||||
if (permissions.globalPermissions.includes(action)) return true;
|
||||
|
||||
if (resourceType && resourceId) {
|
||||
const key = `${resourceType}:${resourceId}`;
|
||||
return permissions.scopedPermissions[key]?.includes(action) ?? false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [permissions]);
|
||||
const can = useCallback((
|
||||
action: PermissionAction,
|
||||
resourceType?: string,
|
||||
resourceId?: string,
|
||||
nodeId?: number | null,
|
||||
): boolean => resolveCan(permissions, action, resourceType, resourceId, nodeId), [permissions]);
|
||||
|
||||
const login = async (username: string, password: string, remember = false): Promise<{ success: boolean; error?: string; mfaRequired?: boolean }> => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user