Created
February 15, 2025 21:32
-
-
Save brandonbryant12/0df5bb3ad8053d78c980ebcefcadb2ae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useState } from 'react'; | |
import { useApi, identityApiRef } from '@backstage/core-plugin-api'; | |
import { useAsyncEntity } from '@backstage/plugin-catalog-react'; | |
import { useEntityOwnership } from '@backstage/plugin-catalog-react'; | |
import jwtDecode from 'jwt-decode'; | |
import { accessCheckerRegistry, AccessCondition } from '../accessCheckers'; | |
export interface AccessState { | |
loading: boolean; | |
hasAccess: boolean; | |
error?: Error; | |
} | |
/** | |
* Custom hook that checks whether the current user has access to a given entity, | |
* based on one or more conditions. | |
* | |
* @param conditions - An array of AccessCondition strings. | |
* @returns An object with { loading, hasAccess, error }. | |
*/ | |
export function useClientEntityAccessCheck(conditions: AccessCondition[]): AccessState { | |
const [state, setState] = useState<AccessState>({ loading: true, hasAccess: false }); | |
const identityApi = useApi(identityApiRef); | |
const { entity, loading: entityLoading, error: entityError } = useAsyncEntity(); | |
const { isOwnedEntity } = useEntityOwnership(); | |
useEffect(() => { | |
let cancelled = false; // cancellation flag to avoid setting state after unmount | |
// If the entity is still loading, we remain in a loading state. | |
if (entityLoading) { | |
setState({ loading: true, hasAccess: false }); | |
return; | |
} | |
// If loading is finished but there's no entity or an error occurred, update state with the error. | |
if (entityError || !entity) { | |
setState({ | |
loading: false, | |
hasAccess: false, | |
error: entityError || new Error('Entity not available'), | |
}); | |
return; | |
} | |
// At this point the entity is loaded and available. | |
setState({ loading: true, hasAccess: false }); | |
identityApi | |
.getCredentials() | |
.then(credentials => { | |
if (cancelled) return; // skip if the component unmounted | |
const token = credentials.token; | |
if (!token) { | |
setState({ | |
loading: false, | |
hasAccess: false, | |
error: new Error('No token available'), | |
}); | |
return; | |
} | |
let decoded; | |
try { | |
decoded = jwtDecode(token); | |
} catch (decodeError) { | |
setState({ | |
loading: false, | |
hasAccess: false, | |
error: new Error('Failed to decode token'), | |
}); | |
return; | |
} | |
// Build an array of Promises for each condition. | |
const checks = conditions.map(cond => { | |
const checker = accessCheckerRegistry[cond]; | |
if (!checker) { | |
return Promise.resolve(false); | |
} | |
return Promise.resolve( | |
checker.check({ | |
entity, | |
userClaims: decoded, | |
isOwnedEntity: isOwnedEntity?.(entity), | |
}) | |
); | |
}); | |
return Promise.all(checks).then(results => { | |
if (cancelled) return; | |
const hasAccess = results.some(result => result === true); | |
setState({ loading: false, hasAccess }); | |
}); | |
}) | |
.catch(err => { | |
if (!cancelled) { | |
setState({ | |
loading: false, | |
hasAccess: false, | |
error: err as Error, | |
}); | |
} | |
}); | |
// Cleanup function: mark as cancelled when unmounted or dependencies change. | |
return () => { | |
cancelled = true; | |
}; | |
}, [conditions, identityApi, entity, entityLoading, entityError, isOwnedEntity]); | |
return state; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment