Skip to content

Commit

Permalink
change role logic to handle selector state and wizard access (#192)
Browse files Browse the repository at this point in the history
* change role logic to handle when to various states

* stop executiion on match

* avoid infinite redirects

* update picker visibility
  • Loading branch information
pnzrr authored Jul 16, 2024
1 parent 91f043a commit 7e31295
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 75 deletions.
21 changes: 8 additions & 13 deletions src/app/components/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,16 @@ import { PATHS } from "@app/routes";
const Dashboard: React.FunctionComponent = () => {
useTitle("Dashboard | Phase Two");
const { navigateToAccessDenied } = useRoleAccess();
const { hasIDPRoles } = useRoleAccess();
let { realm } = useParams();
const { data: featureFlags, isLoading } = useGetFeatureFlagsQuery();
const { data: featureFlags, isLoading: isLoadingFeatureFlags } =
useGetFeatureFlagsQuery();

if (isLoading) {
if (isLoadingFeatureFlags) {
return <Loading />;
}

if (!isLoading && !featureFlags?.enableDashboard) {
navigateToAccessDenied();
return <Loading />;
}

if (!hasIDPRoles()) {
return <Navigate to={generatePath(PATHS.accessDenied, { realm })} />;
} else {
if (!featureFlags?.enableDashboard) {
navigateToAccessDenied();
return <Loading />;
}
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,10 @@ import { useRoleAccess } from "@app/hooks";
import { useGetFeatureFlagsQuery } from "@app/services";

export const IdPProtocolSelector: FC = ({}) => {
const { hasIDPRoles } = useRoleAccess();
const { provider, realm } = useParams<keyof RouterParams>() as RouterParams;
let navigate = useNavigate();
const { data: featureFlags } = useGetFeatureFlagsQuery();

if (!hasIDPRoles()) {
return <Navigate to={generatePath(PATHS.accessDenied, { realm })} />;
}

const currentProvider = IdentityProviders.find((i) => i.id === provider)!;

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,17 @@ import { useTitle } from "react-use";
import { useHostname } from "@app/hooks/useHostname";
import { useGetFeatureFlagsQuery } from "@app/services";
import { MainNav } from "@app/components/navigation/main-nav";
import { useRoleAccess } from "@app/hooks";
import { useAppSelector, useOrganization, useRoleAccess } from "@app/hooks";

export const IdentityProviderSelector: FC = () => {
const { hasIDPRoles } = useRoleAccess();
useTitle("Select your Identity Provider | Phase Two");

let { realm } = useParams();
const hostname = useHostname();
const { data: featureFlags } = useGetFeatureFlagsQuery();
const { getCurrentOrgName } = useOrganization();
const currentOrgName = getCurrentOrgName();

if (!hasIDPRoles()) {
return <Navigate to={generatePath(PATHS.accessDenied, { realm })} />;
}
const { data: featureFlags } = useGetFeatureFlagsQuery();

return (
<PageSection variant={PageSectionVariants.light}>
Expand All @@ -41,7 +39,10 @@ export const IdentityProviderSelector: FC = () => {
<div className="container">
<div className="vertical-center">
<h1>Select your Identity Provider</h1>
<h2>This is how users will sign in to {hostname}</h2>
<h2>
This is how users will sign in to{" "}
<b>{currentOrgName === "Global" ? "realms" : currentOrgName}</b>
</h2>
<div className="selection-container">
{IdentityProviders.filter((idp) => idp.active)
.sort((a, b) =>
Expand Down
6 changes: 0 additions & 6 deletions src/app/components/IdentityProviderWizard/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ const Provider = () => {
keyof RouterParams
>() as RouterParams;

const { hasIDPRoles } = useRoleAccess();

if (!hasIDPRoles()) {
return <Navigate to={generatePath(PATHS.accessDenied, { realm })} />;
}

const providers = [...IdentityProviders, ...GenericIdentityProviders];

useTitle(`${providers.find((ip) => ip.id === provider)?.name} | Phase Two`);
Expand Down
7 changes: 5 additions & 2 deletions src/app/components/navigation/app-launcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ const AppLauncher: React.FC<Props> = ({ toggleOrgPicker }) => {
const hasAdminRole = hasOrganizationRoles("admin", orgId);
if (hasAdminRole) return orgId;
});
const hasNoOrgsToPick = orgsToPick.length <= 1 || !hasRealmRoles();

let pickerVisibility = orgsToPick.length;
if (hasRealmRoles()) pickerVisibility++;
const showOrgPicker = pickerVisibility <= 1;

const AppLauncherItems: React.ReactElement[] = [
<ApplicationLauncherItem
Expand Down Expand Up @@ -91,7 +94,7 @@ const AppLauncher: React.FC<Props> = ({ toggleOrgPicker }) => {
onClick={() => toggleOrgPicker(true)}
title="Change the active organization for IdP creation."
className={cs({
"pf-u-display-none": hasNoOrgsToPick,
"pf-u-display-none": showOrgPicker,
})}
>
Switch Organization
Expand Down
6 changes: 5 additions & 1 deletion src/app/components/navigation/org-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ const OrgPicker: React.FC<Props> = ({
For which Organization are you configuring an Identity Provider?
</div>
<br />
<div>{OrgRadioGroups.map((grp) => grp)}</div>
<div>
{OrgRadioGroups.map((grp, index) =>
React.cloneElement(grp, { key: index })
)}
</div>
{hasRealmRoles() && (
<>
<Divider className="pf-u-mt-lg pf-u-mb-lg" />
Expand Down
38 changes: 14 additions & 24 deletions src/app/hooks/useRoleAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { useEffect, useState } from "react";
import { generatePath, useParams } from "react-router-dom";
import { useAppSelector } from "./hooks";

export const requiredIdpRoles = [
"view-identity-providers",
"manage-identity-providers",
];
// Logic for role checks
// 1. Check if the user has the required roles for the realm admin. This sets
// access for ability to set Realm level IDP
// 2. Check if the user has the required roles for the organization admin. This sets
// access for ability to set Organization level IDP
// 3. If the user has access to more than 1 org or to 1 org and realm level, show the selector
// for the org picker to allow the choice.
// Choice is saved locally. On keycloak token refreshes, access is rechecked.

export const requiredOrganizationResourceRoles = [
"view-identity-providers",
Expand Down Expand Up @@ -45,9 +49,11 @@ export function useRoleAccess() {
const [hasOrgAccess, setHasOrgAccess] = useState<null | boolean>(null);

function navigateToAccessDenied() {
window.location.replace(
if (window.location.pathname.endsWith("access-denied")) return;

window.location.assign(
generatePath(PATHS.accessDenied, {
realm,
realm: realm || window.location.pathname.split("/")[3],
})
);
}
Expand Down Expand Up @@ -102,15 +108,8 @@ export function useRoleAccess() {
return !roleAccess.includes(false);
}

function hasIDPRoles() {
let roleAccess: boolean[] = [];
requiredIdpRoles.map((role) => {
return roleAccess.push(hasOrganizationRole(role, currentOrg));
});

return !roleAccess.includes(false);
}

// TODO: is this fully sufficient to refresh the choice of org?
// may need to force refresh
function checkAccess() {
if (currentOrg === "global") {
setHasOrgAccess(true);
Expand Down Expand Up @@ -144,19 +143,10 @@ export function useRoleAccess() {
checkAccess();
}, []);

// Can't use this here in order to get correct role access for use with
// the Org Selector
// useEffect(() => {
// if (hasOrgAccess === false && realm) {
// navigateToAccessDenied();
// }
// }, [hasOrgAccess, realm]);

return {
hasOrgAccess,
hasOrganizationRoles,
hasRealmRoles,
navigateToAccessDenied,
hasIDPRoles,
};
}
45 changes: 28 additions & 17 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { useGetFeatureFlagsQuery } from "@app/services";

const App: React.FC = () => {
const dispatch = useAppDispatch();
const { hasOrganizationRoles, hasRealmRoles } = useRoleAccess();
const { hasOrganizationRoles, hasRealmRoles, navigateToAccessDenied } =
useRoleAccess();
const { keycloak } = useKeycloak();
const { data: featureFlags } = useGetFeatureFlagsQuery();
const apiMode = useAppSelector((state) => state.settings.apiMode);
Expand All @@ -31,35 +32,45 @@ const App: React.FC = () => {
}, [featureFlags]);

// Organization setup and selection
const kcTPOrgId = keycloak?.tokenParsed?.org_id;
const orgsObj = keycloak?.tokenParsed?.organizations || {};
const orgsArr = Object.keys(orgsObj)
.map((orgId) => {
const orgsArr: string[] = Object.keys(orgsObj)
.map((orgId): string | false => {
const hasRealmRoles = hasOrganizationRoles("admin", orgId);
return hasRealmRoles ? orgId : false;
})
.filter((orgId) => orgId);
.filter((orgId): orgId is string => Boolean(orgId));

useEffect(() => {
if (organization) {
// Organization already selected and is still locally saved
// and still in token
if (organization && orgsArr.includes(organization)) {
return;
}
if (kcTPOrgId) {
const hasAdminRole = hasOrganizationRoles("admin", kcTPOrgId);
if (hasAdminRole) {
dispatch(setOrganization(keycloak?.tokenParsed.org_id));
}

// No org available but has Realm admin role
if (orgsArr.length === 0 && hasRealmRoles()) {
dispatch(setOrganization("global"));
return;
}
// Naively grab the first orgId that has the right permissions
if (orgsArr.length > 0) {
dispatch(setMustPickOrg(true));

// Organizations array is only 1
// Set organization to that specific org
if (orgsArr.length === 1) {
dispatch(setOrganization(orgsArr[0]));
dispatch(setMustPickOrg(false));
return;
}
// No org available but has Realm admin role
if (orgsArr.length === 0 && hasRealmRoles()) {
dispatch(setOrganization("global"));

// Must pick an org or global
// Global option presented in picker
if (orgsArr.length > 1) {
dispatch(setMustPickOrg(true));
return;
}

// Did not have Realm Role and No orgs available
// Redirect to Access Denied
return navigateToAccessDenied();
}, []);

return (
Expand Down

0 comments on commit 7e31295

Please sign in to comment.