Implement basic access control. (#183)
* Implement basic access control. * Fix formatting
This commit is contained in:
parent
34723a0ebf
commit
49284a0f11
7 changed files with 176 additions and 30 deletions
24
src/App.tsx
24
src/App.tsx
|
@ -11,20 +11,36 @@ import { WhoAmIContextProvider } from "./context/whoami/WhoAmI";
|
||||||
import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
|
import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
|
||||||
import { AlertProvider } from "./components/alert/Alerts";
|
import { AlertProvider } from "./components/alert/Alerts";
|
||||||
|
|
||||||
import { routes } from "./route-config";
|
import { AccessContextProvider, useAccess } from "./context/access/Access";
|
||||||
|
import { routes, RouteDef } from "./route-config";
|
||||||
import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
|
import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
|
||||||
|
import { ForbiddenSection } from "./ForbiddenSection";
|
||||||
|
|
||||||
const AppContexts = ({ children }: { children: ReactNode }) => (
|
const AppContexts = ({ children }: { children: ReactNode }) => (
|
||||||
<WhoAmIContextProvider>
|
<WhoAmIContextProvider>
|
||||||
<RealmContextProvider>
|
<RealmContextProvider>
|
||||||
|
<AccessContextProvider>
|
||||||
<Help>
|
<Help>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
<ServerInfoProvider>{children}</ServerInfoProvider>
|
<ServerInfoProvider>{children}</ServerInfoProvider>
|
||||||
</AlertProvider>
|
</AlertProvider>
|
||||||
</Help>
|
</Help>
|
||||||
|
</AccessContextProvider>
|
||||||
</RealmContextProvider>
|
</RealmContextProvider>
|
||||||
</WhoAmIContextProvider>
|
</WhoAmIContextProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If someone tries to go directly to a route they don't
|
||||||
|
// have access to, show forbidden page.
|
||||||
|
type SecuredRouteProps = { route: RouteDef };
|
||||||
|
const SecuredRoute = ({ route }: SecuredRouteProps) => {
|
||||||
|
const { hasAccess } = useAccess();
|
||||||
|
|
||||||
|
if (hasAccess(route.access)) return <route.component />;
|
||||||
|
|
||||||
|
return <ForbiddenSection />;
|
||||||
|
};
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
return (
|
return (
|
||||||
<AppContexts>
|
<AppContexts>
|
||||||
|
@ -37,7 +53,11 @@ export const App = () => {
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
{routes(() => {}).map((route, i) => (
|
{routes(() => {}).map((route, i) => (
|
||||||
<Route key={i} {...route} exact />
|
<Route
|
||||||
|
key={i}
|
||||||
|
path={route.path}
|
||||||
|
component={() => <SecuredRoute route={route} />}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Switch>
|
</Switch>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
5
src/ForbiddenSection.tsx
Normal file
5
src/ForbiddenSection.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const ForbiddenSection = () => {
|
||||||
|
return <>Forbidden</>;
|
||||||
|
};
|
|
@ -11,10 +11,13 @@ import {
|
||||||
import { RealmSelector } from "./components/realm-selector/RealmSelector";
|
import { RealmSelector } from "./components/realm-selector/RealmSelector";
|
||||||
import { DataLoader } from "./components/data-loader/DataLoader";
|
import { DataLoader } from "./components/data-loader/DataLoader";
|
||||||
import { HttpClientContext } from "./context/http-service/HttpClientContext";
|
import { HttpClientContext } from "./context/http-service/HttpClientContext";
|
||||||
|
import { useAccess } from "./context/access/Access";
|
||||||
import { RealmRepresentation } from "./realm/models/Realm";
|
import { RealmRepresentation } from "./realm/models/Realm";
|
||||||
|
import { routes } from "./route-config";
|
||||||
|
|
||||||
export const PageNav: React.FunctionComponent = () => {
|
export const PageNav: React.FunctionComponent = () => {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
|
const { hasAccess, hasSomeAccess } = useAccess();
|
||||||
const httpClient = useContext(HttpClientContext)!;
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
const realmLoader = async () => {
|
const realmLoader = async () => {
|
||||||
const response = await httpClient.doGet<RealmRepresentation[]>(
|
const response = await httpClient.doGet<RealmRepresentation[]>(
|
||||||
|
@ -43,18 +46,36 @@ export const PageNav: React.FunctionComponent = () => {
|
||||||
item.event.preventDefault();
|
item.event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeNavItem = (title: string, path: string) => {
|
type LeftNavProps = { title: string; path: string };
|
||||||
|
const LeftNav = ({ title, path }: LeftNavProps) => {
|
||||||
|
const route = routes(() => {}).find((route) => route.path === path);
|
||||||
|
console.log(`hasAccess(${route!.access})=` + hasAccess(route!.access));
|
||||||
|
if (!route || !hasAccess(route.access)) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavItem
|
<NavItem
|
||||||
id={"nav-item-" + path}
|
id={"nav-item" + path.replace("/", "-")}
|
||||||
to={"/" + path}
|
to={path}
|
||||||
isActive={activeItem === "/" + path}
|
isActive={activeItem === path}
|
||||||
>
|
>
|
||||||
{t(title)}
|
{t(title)}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showManage = hasSomeAccess(
|
||||||
|
"view-realm",
|
||||||
|
"query-groups",
|
||||||
|
"query-users",
|
||||||
|
"view-events"
|
||||||
|
);
|
||||||
|
|
||||||
|
const showConfigure = hasSomeAccess(
|
||||||
|
"view-realm",
|
||||||
|
"query-clients",
|
||||||
|
"view-identity-providers"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataLoader loader={realmLoader}>
|
<DataLoader loader={realmLoader}>
|
||||||
{(realmList) => (
|
{(realmList) => (
|
||||||
|
@ -66,22 +87,29 @@ export const PageNav: React.FunctionComponent = () => {
|
||||||
<RealmSelector realmList={realmList.data || []} />
|
<RealmSelector realmList={realmList.data || []} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</NavList>
|
</NavList>
|
||||||
|
{showManage && (
|
||||||
<NavGroup title={t("manage")}>
|
<NavGroup title={t("manage")}>
|
||||||
{makeNavItem("clients", "clients")}
|
<LeftNav title="clients" path="/clients" />
|
||||||
{makeNavItem("clientScopes", "client-scopes")}
|
<LeftNav title="clientScopes" path="/client-scopes" />
|
||||||
{makeNavItem("realmRoles", "roles")}
|
<LeftNav title="realmRoles" path="/roles" />
|
||||||
{makeNavItem("users", "users")}
|
<LeftNav title="users" path="/users" />
|
||||||
{makeNavItem("groups", "groups")}
|
<LeftNav title="groups" path="/groups" />
|
||||||
{makeNavItem("sessions", "sessions")}
|
<LeftNav title="sessions" path="/sessions" />
|
||||||
{makeNavItem("events", "events")}
|
<LeftNav title="events" path="/events" />
|
||||||
</NavGroup>
|
</NavGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showConfigure && (
|
||||||
<NavGroup title={t("configure")}>
|
<NavGroup title={t("configure")}>
|
||||||
{makeNavItem("realmSettings", "realm-settings")}
|
<LeftNav title="realmSettings" path="/realm-settings" />
|
||||||
{makeNavItem("authentication", "authentication")}
|
<LeftNav title="authentication" path="/authentication" />
|
||||||
{makeNavItem("identityProviders", "identity-providers")}
|
<LeftNav
|
||||||
{makeNavItem("userFederation", "user-federation")}
|
title="identityProviders"
|
||||||
|
path="/identity-providers"
|
||||||
|
/>
|
||||||
|
<LeftNav title="userFederation" path="/user-federation" />
|
||||||
</NavGroup>
|
</NavGroup>
|
||||||
|
)}
|
||||||
</Nav>
|
</Nav>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
39
src/context/access/Access.tsx
Normal file
39
src/context/access/Access.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||||
|
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
|
||||||
|
import { AccessType } from "../../context/whoami/who-am-i-model";
|
||||||
|
|
||||||
|
type AccessContextProps = {
|
||||||
|
hasAccess: (...types: AccessType[]) => boolean;
|
||||||
|
hasSomeAccess: (...types: AccessType[]) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AccessContext = createContext<AccessContextProps>({
|
||||||
|
hasAccess: () => false,
|
||||||
|
hasSomeAccess: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useAccess = () => useContext(AccessContext);
|
||||||
|
|
||||||
|
type AccessProviderProps = { children: React.ReactNode };
|
||||||
|
export const AccessContextProvider = ({ children }: AccessProviderProps) => {
|
||||||
|
const whoami = useContext(WhoAmIContext);
|
||||||
|
const realmCtx = useContext(RealmContext);
|
||||||
|
|
||||||
|
const access = () => whoami.getRealmAccess()[realmCtx.realm];
|
||||||
|
|
||||||
|
const hasAccess = (...types: AccessType[]) => {
|
||||||
|
return types.every((type) => type === "anyone" || access().includes(type));
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSomeAccess = (...types: AccessType[]) => {
|
||||||
|
return types.some((type) => type === "anyone" || access().includes(type));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessContext.Provider value={{ hasAccess, hasSomeAccess }}>
|
||||||
|
{children}
|
||||||
|
</AccessContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import i18n from "../../i18n";
|
import i18n from "../../i18n";
|
||||||
|
|
||||||
import WhoAmIRepresentation from "./who-am-i-model";
|
import WhoAmIRepresentation, { AccessType } from "./who-am-i-model";
|
||||||
|
|
||||||
import { HttpClientContext } from "../http-service/HttpClientContext";
|
import { HttpClientContext } from "../http-service/HttpClientContext";
|
||||||
import { KeycloakContext } from "../auth/KeycloakContext";
|
import { KeycloakContext } from "../auth/KeycloakContext";
|
||||||
|
@ -40,7 +40,9 @@ export class WhoAmI {
|
||||||
return this.me !== undefined && this.me.createRealm;
|
return this.me !== undefined && this.me.createRealm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRealmAccess(): Readonly<{ [key: string]: ReadonlyArray<string> }> {
|
public getRealmAccess(): Readonly<{
|
||||||
|
[key: string]: ReadonlyArray<AccessType>;
|
||||||
|
}> {
|
||||||
if (this.me === undefined) return {};
|
if (this.me === undefined) return {};
|
||||||
|
|
||||||
return this.me.realm_access;
|
return this.me.realm_access;
|
||||||
|
|
|
@ -1,8 +1,29 @@
|
||||||
|
export type AccessType =
|
||||||
|
| "view-realm"
|
||||||
|
| "view-identity-providers"
|
||||||
|
| "manage-identity-providers"
|
||||||
|
| "impersonation"
|
||||||
|
| "create-client"
|
||||||
|
| "manage-users"
|
||||||
|
| "query-realms"
|
||||||
|
| "view-authorization"
|
||||||
|
| "query-clients"
|
||||||
|
| "query-users"
|
||||||
|
| "manage-events"
|
||||||
|
| "manage-realm"
|
||||||
|
| "view-events"
|
||||||
|
| "view-users"
|
||||||
|
| "view-clients"
|
||||||
|
| "manage-authorization"
|
||||||
|
| "manage-clients"
|
||||||
|
| "query-groups"
|
||||||
|
| "anyone";
|
||||||
|
|
||||||
export default interface WhoAmIRepresentation {
|
export default interface WhoAmIRepresentation {
|
||||||
userId: string;
|
userId: string;
|
||||||
realm: string;
|
realm: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
createRealm: boolean;
|
createRealm: boolean;
|
||||||
realm_access: { [key: string]: string[] };
|
realm_access: { [key: string]: AccessType[] };
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,105 +18,136 @@ import { SessionsSection } from "./sessions/SessionsSection";
|
||||||
import { UserFederationSection } from "./user-federation/UserFederationSection";
|
import { UserFederationSection } from "./user-federation/UserFederationSection";
|
||||||
import { UsersSection } from "./user/UsersSection";
|
import { UsersSection } from "./user/UsersSection";
|
||||||
|
|
||||||
export const routes = (t: TFunction) => [
|
import { AccessType } from "./context/whoami/who-am-i-model";
|
||||||
|
|
||||||
|
export type RouteDef = {
|
||||||
|
path: string;
|
||||||
|
component: () => JSX.Element;
|
||||||
|
breadcrumb: TFunction | "";
|
||||||
|
access: AccessType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RoutesFn = (t: TFunction) => RouteDef[];
|
||||||
|
|
||||||
|
export const routes: RoutesFn = (t: TFunction) => [
|
||||||
{
|
{
|
||||||
path: "/add-realm",
|
path: "/add-realm",
|
||||||
component: NewRealmForm,
|
component: NewRealmForm,
|
||||||
breadcrumb: t("realm:createRealm"),
|
breadcrumb: t("realm:createRealm"),
|
||||||
|
access: "manage-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/clients",
|
path: "/clients",
|
||||||
component: ClientsSection,
|
component: ClientsSection,
|
||||||
breadcrumb: t("clients:clientList"),
|
breadcrumb: t("clients:clientList"),
|
||||||
|
access: "query-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/clients/:id",
|
path: "/clients/:id",
|
||||||
component: ClientSettings,
|
component: ClientSettings,
|
||||||
breadcrumb: t("clients:clientSettings"),
|
breadcrumb: t("clients:clientSettings"),
|
||||||
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/add-client",
|
path: "/add-client",
|
||||||
component: NewClientForm,
|
component: NewClientForm,
|
||||||
breadcrumb: t("clients:createClient"),
|
breadcrumb: t("clients:createClient"),
|
||||||
|
access: "manage-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/import-client",
|
path: "/import-client",
|
||||||
component: ImportForm,
|
component: ImportForm,
|
||||||
breadcrumb: t("clients:importClient"),
|
breadcrumb: t("clients:importClient"),
|
||||||
|
access: "manage-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/client-scopes",
|
path: "/client-scopes",
|
||||||
component: ClientScopesSection,
|
component: ClientScopesSection,
|
||||||
breadcrumb: t("client-scopes:clientScopeList"),
|
breadcrumb: t("client-scopes:clientScopeList"),
|
||||||
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/client-scopes/add-client-scopes",
|
path: "/client-scopes/add-client-scopes",
|
||||||
component: ClientScopeForm,
|
component: ClientScopeForm,
|
||||||
breadcrumb: t("client-scopes:createClientScope"),
|
breadcrumb: t("client-scopes:createClientScope"),
|
||||||
|
access: "manage-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/client-scopes/:id",
|
path: "/client-scopes/:id",
|
||||||
component: ClientScopeForm,
|
component: ClientScopeForm,
|
||||||
breadcrumb: t("client-scopes:clientScopeDetails"),
|
breadcrumb: t("client-scopes:clientScopeDetails"),
|
||||||
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/roles",
|
path: "/roles",
|
||||||
component: RealmRolesSection,
|
component: RealmRolesSection,
|
||||||
breadcrumb: t("roles:roleList"),
|
breadcrumb: t("roles:roleList"),
|
||||||
|
access: "view-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/add-role",
|
path: "/add-role",
|
||||||
component: NewRoleForm,
|
component: NewRoleForm,
|
||||||
breadcrumb: t("roles:createRole"),
|
breadcrumb: t("roles:createRole"),
|
||||||
|
access: "manage-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/users",
|
path: "/users",
|
||||||
component: UsersSection,
|
component: UsersSection,
|
||||||
breadcrumb: t("users:title"),
|
breadcrumb: t("users:title"),
|
||||||
|
access: "query-users",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/groups",
|
path: "/groups",
|
||||||
component: GroupsSection,
|
component: GroupsSection,
|
||||||
breadcrumb: t("groups"),
|
breadcrumb: t("groups"),
|
||||||
|
access: "query-groups",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/sessions",
|
path: "/sessions",
|
||||||
component: SessionsSection,
|
component: SessionsSection,
|
||||||
breadcrumb: t("sessions:title"),
|
breadcrumb: t("sessions:title"),
|
||||||
|
access: "view-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events",
|
path: "/events",
|
||||||
component: EventsSection,
|
component: EventsSection,
|
||||||
breadcrumb: t("events:title"),
|
breadcrumb: t("events:title"),
|
||||||
|
access: "view-events",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/realm-settings",
|
path: "/realm-settings",
|
||||||
component: RealmSettingsSection,
|
component: RealmSettingsSection,
|
||||||
breadcrumb: t("realmSettings"),
|
breadcrumb: t("realmSettings"),
|
||||||
|
access: "view-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/authentication",
|
path: "/authentication",
|
||||||
component: AuthenticationSection,
|
component: AuthenticationSection,
|
||||||
breadcrumb: t("authentication"),
|
breadcrumb: t("authentication"),
|
||||||
|
access: "view-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/identity-providers",
|
path: "/identity-providers",
|
||||||
component: IdentityProvidersSection,
|
component: IdentityProvidersSection,
|
||||||
breadcrumb: t("identityProviders"),
|
breadcrumb: t("identityProviders"),
|
||||||
|
access: "view-identity-providers",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user-federation",
|
path: "/user-federation",
|
||||||
component: UserFederationSection,
|
component: UserFederationSection,
|
||||||
breadcrumb: t("userFederation"),
|
breadcrumb: t("userFederation"),
|
||||||
|
access: "view-realm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
component: ClientsSection,
|
component: ClientsSection,
|
||||||
breadcrumb: t("common:home"),
|
breadcrumb: t("common:home"),
|
||||||
|
access: "anyone",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
component: PageNotFoundSection,
|
component: PageNotFoundSection,
|
||||||
breadcrumb: "",
|
breadcrumb: "",
|
||||||
|
access: "anyone",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in a new issue