Fix permissions infinite failed request
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { type ReactNode, useState, useEffect } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { isAuthorized } from "./permissions";
|
import { usePermissions } from "./permissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
entity: string;
|
entity: string;
|
||||||
@@ -38,41 +38,9 @@ export function Authorized({
|
|||||||
fallback = null,
|
fallback = null,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { organizationId } = useParams();
|
const { organizationId } = useParams();
|
||||||
const [hasAccess, setHasAccess] = useState<boolean | null>(null);
|
const { loading, error, isAuthorized } = usePermissions(organizationId || "");
|
||||||
|
|
||||||
useEffect(() => {
|
if (!organizationId || loading || error || !isAuthorized(entity, action)) {
|
||||||
if (!organizationId) {
|
|
||||||
setHasAccess(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to check authorization, catching promise throws
|
|
||||||
try {
|
|
||||||
const authorized = isAuthorized(organizationId, entity, action);
|
|
||||||
setHasAccess(authorized);
|
|
||||||
} catch (promise) {
|
|
||||||
// If a promise is thrown (Suspense pattern), wait for it
|
|
||||||
if (promise instanceof Promise) {
|
|
||||||
promise
|
|
||||||
.then(() => {
|
|
||||||
// Permissions loaded, try again
|
|
||||||
try {
|
|
||||||
const authorized = isAuthorized(organizationId, entity, action);
|
|
||||||
setHasAccess(authorized);
|
|
||||||
} catch {
|
|
||||||
setHasAccess(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setHasAccess(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setHasAccess(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [organizationId, entity, action]);
|
|
||||||
|
|
||||||
if (!organizationId || hasAccess === null || hasAccess === false) {
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// Authorization system that checks permissions from the backend
|
// Authorization system that checks permissions from the backend
|
||||||
// Permissions are fetched per organization from /authz/:organizationId/permissions
|
// Permissions are fetched per organization from /authz/:organizationId/permissions
|
||||||
// Format: { permissions: { "Document": { "node": true, "updateDocument": true }, "Organization": { "createDocument": true } }, role: "ADMIN" }
|
// Format: { permissions: { "Document": { "node": true, "updateDocument": true }, "Organization": { "createDocument": true } }, role: "ADMIN" }
|
||||||
@@ -9,33 +11,30 @@ type PermissionsResponse = {
|
|||||||
role: string;
|
role: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let cachedPermissions: EntityPermissions | null = null;
|
type PermissionsCache = {
|
||||||
let cachedRole: string | null = null;
|
[organizationId: string]: {
|
||||||
let cachePromise: Promise<PermissionsResponse> | null = null;
|
permissions: EntityPermissions;
|
||||||
let currentOrganizationId: string | null = null;
|
role: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cache: PermissionsCache = {};
|
||||||
|
const pendingRequests: Map<string, Promise<PermissionsResponse>> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch permissions for the current user's role in the organization
|
* Fetch permissions for the current user's role in the organization
|
||||||
*/
|
*/
|
||||||
function fetchPermissions(organizationId: string): Promise<PermissionsResponse> {
|
function fetchPermissions(organizationId: string): Promise<PermissionsResponse> {
|
||||||
if (cachedPermissions && cachedRole && currentOrganizationId === organizationId) {
|
if (cache[organizationId]) {
|
||||||
return Promise.resolve({ permissions: cachedPermissions, role: cachedRole });
|
return Promise.resolve(cache[organizationId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cachePromise && currentOrganizationId === organizationId) {
|
const pending = pendingRequests.get(organizationId);
|
||||||
return cachePromise;
|
if (pending) {
|
||||||
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOrganizationId !== organizationId) {
|
const promise = fetch(`/authz/${encodeURIComponent(organizationId)}/permissions`, {
|
||||||
cachedPermissions = null;
|
|
||||||
cachedRole = null;
|
|
||||||
cachePromise = null;
|
|
||||||
currentOrganizationId = organizationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestedOrgId = organizationId;
|
|
||||||
|
|
||||||
cachePromise = fetch(`/authz/${encodeURIComponent(organizationId)}/permissions`, {
|
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -45,55 +44,138 @@ function fetchPermissions(organizationId: string): Promise<PermissionsResponse>
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((data: PermissionsResponse) => {
|
.then((data: PermissionsResponse) => {
|
||||||
if (currentOrganizationId === requestedOrgId) {
|
if (!data || typeof data.permissions !== 'object' || !data.role) {
|
||||||
cachedPermissions = data.permissions;
|
throw new Error('Invalid permissions response structure');
|
||||||
cachedRole = data.role;
|
|
||||||
}
|
}
|
||||||
cachePromise = null;
|
|
||||||
|
cache[organizationId] = {
|
||||||
|
permissions: data.permissions,
|
||||||
|
role: data.role,
|
||||||
|
};
|
||||||
|
pendingRequests.delete(organizationId);
|
||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
cachePromise = null;
|
pendingRequests.delete(organizationId);
|
||||||
cachedPermissions = null;
|
|
||||||
cachedRole = null;
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
return cachePromise;
|
pendingRequests.set(organizationId, promise);
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the user has permission for an entity and action
|
* React hook to fetch and manage permissions for an organization
|
||||||
|
*
|
||||||
|
* @param organizationId - The organization ID
|
||||||
|
* @returns Object with loading, error, permissions, role, and helper functions
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { loading, error, permissions, role, isAuthorized, getAssignableRoles } = usePermissions(orgId);
|
||||||
|
*
|
||||||
|
* if (loading) return <Spinner />;
|
||||||
|
* if (error) return <ErrorMessage />;
|
||||||
|
* if (isAuthorized("Document", "updateDocument")) { ... }
|
||||||
|
*/
|
||||||
|
export function usePermissions(organizationId: string) {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
permissions: EntityPermissions | null;
|
||||||
|
role: string | null;
|
||||||
|
}>({
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
permissions: null,
|
||||||
|
role: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState({ loading: true, error: null, permissions: null, role: null });
|
||||||
|
|
||||||
|
fetchPermissions(organizationId)
|
||||||
|
.then((data) => {
|
||||||
|
setState({
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
permissions: data.permissions,
|
||||||
|
role: data.role,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setState({
|
||||||
|
loading: false,
|
||||||
|
error,
|
||||||
|
permissions: null,
|
||||||
|
role: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [organizationId]);
|
||||||
|
|
||||||
|
const checkAuthorized = (entity: string, action: string): boolean => {
|
||||||
|
if (!state.permissions) return false;
|
||||||
|
|
||||||
|
const entityPermissions = state.permissions[entity];
|
||||||
|
if (!entityPermissions) return false;
|
||||||
|
|
||||||
|
return entityPermissions[action] === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAssignableRolesList = (): string[] => {
|
||||||
|
if (!state.role) return [];
|
||||||
|
|
||||||
|
if (state.role === "OWNER" || state.role === "FULL") {
|
||||||
|
return ["OWNER", "ADMIN", "VIEWER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.role === "ADMIN") {
|
||||||
|
return ["ADMIN", "VIEWER"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: state.loading,
|
||||||
|
error: state.error,
|
||||||
|
permissions: state.permissions,
|
||||||
|
role: state.role,
|
||||||
|
isAuthorized: checkAuthorized,
|
||||||
|
getAssignableRoles: getAssignableRolesList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user has permission for an entity and action (synchronous)
|
||||||
|
* Returns false if permissions are not loaded yet
|
||||||
*
|
*
|
||||||
* @param organizationId - The organization ID
|
* @param organizationId - The organization ID
|
||||||
* @param entity - The entity name (e.g., "Document", "Organization", "Vendor")
|
* @param entity - The entity name (e.g., "Document", "Organization", "Vendor")
|
||||||
* @param action - The action/field name (e.g., "node", "updateDocument", "createDocument")
|
* @param action - The action/field name (e.g., "node", "updateDocument", "createDocument")
|
||||||
* @returns true if the user has permission
|
* @returns true if the user has permission, false otherwise
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* isAuthorized(orgId, "Document", "get") // Check if user can query Document nodes
|
* isAuthorized(orgId, "Document", "get")
|
||||||
* isAuthorized(orgId, "Document", "updateDocument") // Check if user can update documents
|
* isAuthorized(orgId, "Document", "updateDocument")
|
||||||
* isAuthorized(orgId, "Organization", "createDocument") // Check if user can create documents
|
* isAuthorized(orgId, "Organization", "createDocument")
|
||||||
*/
|
*/
|
||||||
export function isAuthorized(
|
export function isAuthorized(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
entity: string,
|
entity: string,
|
||||||
action: string
|
action: string
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!cachedPermissions || currentOrganizationId !== organizationId) {
|
const cached = cache[organizationId];
|
||||||
throw fetchPermissions(organizationId);
|
if (!cached) return false;
|
||||||
}
|
|
||||||
|
|
||||||
const entityPermissions = cachedPermissions[entity];
|
const entityPermissions = cached.permissions[entity];
|
||||||
if (!entityPermissions) {
|
if (!entityPermissions) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entityPermissions[action] === true;
|
return entityPermissions[action] === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current user's role in the organization
|
* Get the current user's role in the organization
|
||||||
|
* Returns empty string if not loaded yet
|
||||||
*
|
*
|
||||||
* @param organizationId - The organization ID
|
* @param organizationId - The organization ID
|
||||||
* @returns The user's role (e.g., "OWNER", "ADMIN", "VIEWER", "FULL")
|
* @returns The user's role (e.g., "OWNER", "ADMIN", "VIEWER", "FULL")
|
||||||
@@ -102,11 +184,8 @@ export function isAuthorized(
|
|||||||
* getUserRole(orgId) // Returns "ADMIN"
|
* getUserRole(orgId) // Returns "ADMIN"
|
||||||
*/
|
*/
|
||||||
export function getUserRole(organizationId: string): string {
|
export function getUserRole(organizationId: string): string {
|
||||||
if (!cachedRole || currentOrganizationId !== organizationId) {
|
const cached = cache[organizationId];
|
||||||
throw fetchPermissions(organizationId);
|
return cached?.role || "";
|
||||||
}
|
|
||||||
|
|
||||||
return cachedRole;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +197,7 @@ export function getUserRole(organizationId: string): string {
|
|||||||
*/
|
*/
|
||||||
export function getAssignableRoles(organizationId: string): string[] {
|
export function getAssignableRoles(organizationId: string): string[] {
|
||||||
const currentRole = getUserRole(organizationId);
|
const currentRole = getUserRole(organizationId);
|
||||||
|
if (!currentRole) return [];
|
||||||
|
|
||||||
if (currentRole === "OWNER" || currentRole === "FULL") {
|
if (currentRole === "OWNER" || currentRole === "FULL") {
|
||||||
return ["OWNER", "ADMIN", "VIEWER"];
|
return ["OWNER", "ADMIN", "VIEWER"];
|
||||||
|
|||||||
Reference in New Issue
Block a user