Skip to content

Instantly share code, notes, and snippets.

@brandonbryant12
Created February 15, 2025 21:32
Show Gist options
  • Save brandonbryant12/0df5bb3ad8053d78c980ebcefcadb2ae to your computer and use it in GitHub Desktop.
Save brandonbryant12/0df5bb3ad8053d78c980ebcefcadb2ae to your computer and use it in GitHub Desktop.
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