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 { 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 { ForbiddenSection } from "./ForbiddenSection";
|
||||
|
||||
const AppContexts = ({ children }: { children: ReactNode }) => (
|
||||
<WhoAmIContextProvider>
|
||||
<RealmContextProvider>
|
||||
<AccessContextProvider>
|
||||
<Help>
|
||||
<AlertProvider>
|
||||
<ServerInfoProvider>{children}</ServerInfoProvider>
|
||||
</AlertProvider>
|
||||
</Help>
|
||||
</AccessContextProvider>
|
||||
</RealmContextProvider>
|
||||
</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 = () => {
|
||||
return (
|
||||
<AppContexts>
|
||||
|
@ -37,7 +53,11 @@ export const App = () => {
|
|||
>
|
||||
<Switch>
|
||||
{routes(() => {}).map((route, i) => (
|
||||
<Route key={i} {...route} exact />
|
||||
<Route
|
||||
key={i}
|
||||
path={route.path}
|
||||
component={() => <SecuredRoute route={route} />}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</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 { DataLoader } from "./components/data-loader/DataLoader";
|
||||
import { HttpClientContext } from "./context/http-service/HttpClientContext";
|
||||
import { useAccess } from "./context/access/Access";
|
||||
import { RealmRepresentation } from "./realm/models/Realm";
|
||||
import { routes } from "./route-config";
|
||||
|
||||
export const PageNav: React.FunctionComponent = () => {
|
||||
const { t } = useTranslation("common");
|
||||
const { hasAccess, hasSomeAccess } = useAccess();
|
||||
const httpClient = useContext(HttpClientContext)!;
|
||||
const realmLoader = async () => {
|
||||
const response = await httpClient.doGet<RealmRepresentation[]>(
|
||||
|
@ -43,18 +46,36 @@ export const PageNav: React.FunctionComponent = () => {
|
|||
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 (
|
||||
<NavItem
|
||||
id={"nav-item-" + path}
|
||||
to={"/" + path}
|
||||
isActive={activeItem === "/" + path}
|
||||
id={"nav-item" + path.replace("/", "-")}
|
||||
to={path}
|
||||
isActive={activeItem === path}
|
||||
>
|
||||
{t(title)}
|
||||
</NavItem>
|
||||
);
|
||||
};
|
||||
|
||||
const showManage = hasSomeAccess(
|
||||
"view-realm",
|
||||
"query-groups",
|
||||
"query-users",
|
||||
"view-events"
|
||||
);
|
||||
|
||||
const showConfigure = hasSomeAccess(
|
||||
"view-realm",
|
||||
"query-clients",
|
||||
"view-identity-providers"
|
||||
);
|
||||
|
||||
return (
|
||||
<DataLoader loader={realmLoader}>
|
||||
{(realmList) => (
|
||||
|
@ -66,22 +87,29 @@ export const PageNav: React.FunctionComponent = () => {
|
|||
<RealmSelector realmList={realmList.data || []} />
|
||||
</NavItem>
|
||||
</NavList>
|
||||
{showManage && (
|
||||
<NavGroup title={t("manage")}>
|
||||
{makeNavItem("clients", "clients")}
|
||||
{makeNavItem("clientScopes", "client-scopes")}
|
||||
{makeNavItem("realmRoles", "roles")}
|
||||
{makeNavItem("users", "users")}
|
||||
{makeNavItem("groups", "groups")}
|
||||
{makeNavItem("sessions", "sessions")}
|
||||
{makeNavItem("events", "events")}
|
||||
<LeftNav title="clients" path="/clients" />
|
||||
<LeftNav title="clientScopes" path="/client-scopes" />
|
||||
<LeftNav title="realmRoles" path="/roles" />
|
||||
<LeftNav title="users" path="/users" />
|
||||
<LeftNav title="groups" path="/groups" />
|
||||
<LeftNav title="sessions" path="/sessions" />
|
||||
<LeftNav title="events" path="/events" />
|
||||
</NavGroup>
|
||||
)}
|
||||
|
||||
{showConfigure && (
|
||||
<NavGroup title={t("configure")}>
|
||||
{makeNavItem("realmSettings", "realm-settings")}
|
||||
{makeNavItem("authentication", "authentication")}
|
||||
{makeNavItem("identityProviders", "identity-providers")}
|
||||
{makeNavItem("userFederation", "user-federation")}
|
||||
<LeftNav title="realmSettings" path="/realm-settings" />
|
||||
<LeftNav title="authentication" path="/authentication" />
|
||||
<LeftNav
|
||||
title="identityProviders"
|
||||
path="/identity-providers"
|
||||
/>
|
||||
<LeftNav title="userFederation" path="/user-federation" />
|
||||
</NavGroup>
|
||||
)}
|
||||
</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 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 { KeycloakContext } from "../auth/KeycloakContext";
|
||||
|
@ -40,7 +40,9 @@ export class WhoAmI {
|
|||
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 {};
|
||||
|
||||
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 {
|
||||
userId: string;
|
||||
realm: string;
|
||||
displayName: string;
|
||||
locale: string;
|
||||
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 { 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",
|
||||
component: NewRealmForm,
|
||||
breadcrumb: t("realm:createRealm"),
|
||||
access: "manage-realm",
|
||||
},
|
||||
{
|
||||
path: "/clients",
|
||||
component: ClientsSection,
|
||||
breadcrumb: t("clients:clientList"),
|
||||
access: "query-clients",
|
||||
},
|
||||
{
|
||||
path: "/clients/:id",
|
||||
component: ClientSettings,
|
||||
breadcrumb: t("clients:clientSettings"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/add-client",
|
||||
component: NewClientForm,
|
||||
breadcrumb: t("clients:createClient"),
|
||||
access: "manage-clients",
|
||||
},
|
||||
{
|
||||
path: "/import-client",
|
||||
component: ImportForm,
|
||||
breadcrumb: t("clients:importClient"),
|
||||
access: "manage-clients",
|
||||
},
|
||||
{
|
||||
path: "/client-scopes",
|
||||
component: ClientScopesSection,
|
||||
breadcrumb: t("client-scopes:clientScopeList"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/client-scopes/add-client-scopes",
|
||||
component: ClientScopeForm,
|
||||
breadcrumb: t("client-scopes:createClientScope"),
|
||||
access: "manage-clients",
|
||||
},
|
||||
{
|
||||
path: "/client-scopes/:id",
|
||||
component: ClientScopeForm,
|
||||
breadcrumb: t("client-scopes:clientScopeDetails"),
|
||||
access: "view-clients",
|
||||
},
|
||||
{
|
||||
path: "/roles",
|
||||
component: RealmRolesSection,
|
||||
breadcrumb: t("roles:roleList"),
|
||||
access: "view-realm",
|
||||
},
|
||||
{
|
||||
path: "/add-role",
|
||||
component: NewRoleForm,
|
||||
breadcrumb: t("roles:createRole"),
|
||||
access: "manage-realm",
|
||||
},
|
||||
{
|
||||
path: "/users",
|
||||
component: UsersSection,
|
||||
breadcrumb: t("users:title"),
|
||||
access: "query-users",
|
||||
},
|
||||
{
|
||||
path: "/groups",
|
||||
component: GroupsSection,
|
||||
breadcrumb: t("groups"),
|
||||
access: "query-groups",
|
||||
},
|
||||
{
|
||||
path: "/sessions",
|
||||
component: SessionsSection,
|
||||
breadcrumb: t("sessions:title"),
|
||||
access: "view-realm",
|
||||
},
|
||||
{
|
||||
path: "/events",
|
||||
component: EventsSection,
|
||||
breadcrumb: t("events:title"),
|
||||
access: "view-events",
|
||||
},
|
||||
{
|
||||
path: "/realm-settings",
|
||||
component: RealmSettingsSection,
|
||||
breadcrumb: t("realmSettings"),
|
||||
access: "view-realm",
|
||||
},
|
||||
{
|
||||
path: "/authentication",
|
||||
component: AuthenticationSection,
|
||||
breadcrumb: t("authentication"),
|
||||
access: "view-realm",
|
||||
},
|
||||
{
|
||||
path: "/identity-providers",
|
||||
component: IdentityProvidersSection,
|
||||
breadcrumb: t("identityProviders"),
|
||||
access: "view-identity-providers",
|
||||
},
|
||||
{
|
||||
path: "/user-federation",
|
||||
component: UserFederationSection,
|
||||
breadcrumb: t("userFederation"),
|
||||
access: "view-realm",
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: ClientsSection,
|
||||
breadcrumb: t("common:home"),
|
||||
access: "anyone",
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: PageNotFoundSection,
|
||||
breadcrumb: "",
|
||||
access: "anyone",
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Reference in a new issue