Implement basic access control. (#183)

* Implement basic access control.

* Fix formatting
This commit is contained in:
Stan Silvert 2020-10-21 07:31:41 -04:00 committed by GitHub
parent 34723a0ebf
commit 49284a0f11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 176 additions and 30 deletions

View file

@ -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
View file

@ -0,0 +1,5 @@
import React from "react";
export const ForbiddenSection = () => {
return <>Forbidden</>;
};

View file

@ -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>
}
/>

View 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>
);
};

View file

@ -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;

View file

@ -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[] };
}

View file

@ -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",
},
];