Parse environment information as JSON (#851)

* Parse environment information as JSON

* Pass 'isRunningAsTheme' from injected environment

* Update comment to use index.ftl instead of html

Co-authored-by: Stan Silvert <ssilvert@redhat.com>

* Update realm param comment

* Update version param

* add more sensible defaults

Co-authored-by: Stan Silvert <ssilvert@redhat.com>
This commit is contained in:
Jon Koops 2021-07-15 11:50:01 +02:00 committed by GitHub
parent d67b37e48e
commit 8236528a07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 100 additions and 62 deletions

View file

@ -68,18 +68,23 @@
<value>href="${resourceUrl}/</value> <value>href="${resourceUrl}/</value>
</replacement> </replacement>
<replacement> <replacement>
<token>&lt;head&gt;</token> <token><![CDATA[</body>]]></token>
<value> <value xml:space="preserve">
&lt;head&gt; <![CDATA[
&lt;script type="text/javascript"&gt; <script id="environment" type="application/json">
var loginRealm = "${loginRealm}"; {
var authServerUrl = "${authServerUrl}"; "loginRealm": "${loginRealm}",
var authUrl = "${authUrl}"; "authServerUrl": "${authServerUrl}",
var consoleBaseUrl = "${consoleBaseUrl}"; "authUrl": "${authUrl}",
var resourceUrl = "${resourceUrl}"; "consoleBaseUrl": "${consoleBaseUrl}",
var masterRealm = "${masterRealm}"; "resourceUrl": "${resourceUrl}",
var resourceVersion = "${resourceVersion}"; "masterRealm": "${masterRealm}",
&lt;/script&gt; "resourceVersion": "${resourceVersion}",
"isRunningAsTheme": true
}
</script>
</body>
]]>
</value> </value>
</replacement> </replacement>
</replacements> </replacements>

View file

@ -18,7 +18,7 @@ import { WhoAmIContext } from "./context/whoami/WhoAmI";
import { HelpContext, HelpHeader } from "./components/help-enabler/HelpHeader"; import { HelpContext, HelpHeader } from "./components/help-enabler/HelpHeader";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { useAdminClient } from "./context/auth/AdminClient"; import { useAdminClient } from "./context/auth/AdminClient";
import { resourceUri } from "./util"; import environment from "./environment";
export const Header = () => { export const Header = () => {
const adminClient = useAdminClient(); const adminClient = useAdminClient();
@ -127,7 +127,10 @@ export const Header = () => {
<UserDropdown /> <UserDropdown />
</PageHeaderToolsItem> </PageHeaderToolsItem>
</PageHeaderToolsGroup> </PageHeaderToolsGroup>
<Avatar src={resourceUri + "/img_avatar.svg"} alt="Avatar image" /> <Avatar
src={environment.resourceUrl + "/img_avatar.svg"}
alt="Avatar image"
/>
</PageHeaderTools> </PageHeaderTools>
); );
}; };
@ -181,7 +184,7 @@ export const Header = () => {
logo={ logo={
<Link to="/"> <Link to="/">
<Brand <Brand
src={resourceUri + "/logo.svg"} src={environment.resourceUrl + "/logo.svg"}
id="masthead-logo" id="masthead-logo"
alt="Logo" alt="Logo"
className="keycloak__pageheader_brand" className="keycloak__pageheader_brand"

View file

@ -1,5 +1,5 @@
import KcAdminClient from "keycloak-admin"; import KcAdminClient from "keycloak-admin";
import { homeRealm, isDevMode, authUri } from "../../util"; import environment from "../../environment";
export default async function (): Promise<KcAdminClient> { export default async function (): Promise<KcAdminClient> {
const kcAdminClient = new KcAdminClient(); const kcAdminClient = new KcAdminClient();
@ -7,16 +7,16 @@ export default async function (): Promise<KcAdminClient> {
await kcAdminClient.init( await kcAdminClient.init(
{ onLoad: "check-sso", pkceMethod: "S256" }, { onLoad: "check-sso", pkceMethod: "S256" },
{ {
url: authUri(), url: environment.authUrl,
realm: homeRealm(), realm: environment.loginRealm,
clientId: isDevMode clientId: environment.isRunningAsTheme
? "security-admin-console-v2" ? "security-admin-console"
: "security-admin-console", : "security-admin-console-v2",
} }
); );
kcAdminClient.setConfig({ realmName: homeRealm() }); kcAdminClient.setConfig({ realmName: environment.loginRealm });
kcAdminClient.baseUrl = authUri(); kcAdminClient.baseUrl = environment.authUrl;
} catch (error) { } catch (error) {
alert("failed to initialize keycloak"); alert("failed to initialize keycloak");
} }

View file

@ -4,7 +4,7 @@ import _ from "lodash";
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation"; import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { RecentUsed } from "../../components/realm-selector/recent-used"; import { RecentUsed } from "../../components/realm-selector/recent-used";
import { useAdminClient, useFetch } from "../auth/AdminClient"; import { useAdminClient, useFetch } from "../auth/AdminClient";
import { WhoAmIContext } from "../whoami/WhoAmI"; import environment from "../../environment";
type RealmContextType = { type RealmContextType = {
realm: string; realm: string;
@ -25,8 +25,7 @@ type RealmContextProviderProps = { children: React.ReactNode };
export const RealmContextProvider = ({ export const RealmContextProvider = ({
children, children,
}: RealmContextProviderProps) => { }: RealmContextProviderProps) => {
const { whoAmI } = useContext(WhoAmIContext); const [realm, setRealm] = useState(environment.loginRealm);
const [realm, setRealm] = useState(whoAmI.getHomeRealm());
const [realms, setRealms] = useState<RealmRepresentation[]>([]); const [realms, setRealms] = useState<RealmRepresentation[]>([]);
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const recentUsed = new RecentUsed(); const recentUsed = new RecentUsed();

View file

@ -4,7 +4,6 @@ import i18n from "../../i18n";
import type WhoAmIRepresentation from "keycloak-admin/lib/defs/whoAmIRepresentation"; import type WhoAmIRepresentation from "keycloak-admin/lib/defs/whoAmIRepresentation";
import type { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation"; import type { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation";
import { useAdminClient, useFetch } from "../auth/AdminClient"; import { useAdminClient, useFetch } from "../auth/AdminClient";
import { homeRealm } from "../../util";
export class WhoAmI { export class WhoAmI {
constructor(private me?: WhoAmIRepresentation) { constructor(private me?: WhoAmIRepresentation) {
@ -27,13 +26,6 @@ export class WhoAmI {
return this.me.userId; return this.me.userId;
} }
/**
* Return the realm I am signed in to.
*/
public getHomeRealm(): string {
return homeRealm();
}
public canCreateRealm(): boolean { public canCreateRealm(): boolean {
return this.me !== undefined && this.me.createRealm; return this.me !== undefined && this.me.createRealm;
} }

View file

@ -7,11 +7,6 @@ test("returns display name", () => {
expect(whoami.getDisplayName()).toEqual("Stan Silvert"); expect(whoami.getDisplayName()).toEqual("Stan Silvert");
}); });
test("returns correct home realm in dev mode", () => {
const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation);
expect(whoami.getHomeRealm()).toEqual("master");
});
test("can not create realm", () => { test("can not create realm", () => {
const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation); const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation);
expect(whoami.canCreateRealm()).toEqual(false); expect(whoami.canCreateRealm()).toEqual(false);

View file

@ -28,8 +28,9 @@ import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import "./dashboard.css"; import "./dashboard.css";
import { toUpperCase, resourceUri } from "../util"; import { toUpperCase } from "../util";
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import environment from "../environment";
const EmptyDashboard = () => { const EmptyDashboard = () => {
const { t } = useTranslation("dashboard"); const { t } = useTranslation("dashboard");
@ -38,7 +39,7 @@ const EmptyDashboard = () => {
<PageSection variant="light"> <PageSection variant="light">
<EmptyState variant="large"> <EmptyState variant="large">
<Brand <Brand
src={resourceUri + "/icon.svg"} src={environment.resourceUrl + "/icon.svg"}
alt="Keycloak icon" alt="Keycloak icon"
className="keycloak__dashboard_icon" className="keycloak__dashboard_icon"
/> />

64
src/environment.ts Normal file
View file

@ -0,0 +1,64 @@
export type Environment = {
/** The realm which should be used when signing into the application. */
loginRealm: string;
/** The URL to the root of the auth server. */
authServerUrl: string;
/** The URL to the path of the auth server where client requests can be sent. */
authUrl: string;
/** The URL to the base of the admin console. */
consoleBaseUrl: string;
resourceUrl: string;
/** The name of the master realm. */
masterRealm: string;
/** The version hash of the auth sever. */
resourceVersion: string;
/** Indicates if the application is running as a Keycloak theme. */
isRunningAsTheme: boolean;
};
// During development the realm can be passed as a query parameter when redirecting back from Keycloak.
const realm =
new URLSearchParams(window.location.search).get("realm") ?? "master";
// The default environment, used during development.
const defaultEnvironment: Environment = {
loginRealm: realm,
authServerUrl: "http://localhost:8180/auth",
authUrl: "http://localhost:8180/auth",
consoleBaseUrl: "/auth/admin/master/console/",
resourceUrl: ".",
masterRealm: "master",
resourceVersion: "unknown",
isRunningAsTheme: false,
};
// Merge the default and injected environment variables together.
const environment: Environment = {
...defaultEnvironment,
...getInjectedEnvironment(),
};
export default environment;
/**
* Extracts the environment variables that are passed if the application is running as a Keycloak theme.
* These variables are injected by Keycloak into the `index.ftl` as a script tag, the contents of which can be parsed as JSON.
*/
function getInjectedEnvironment(): Record<string, string | number | boolean> {
const element = document.getElementById("environment");
// If the element cannot be found, return an empty record.
if (!element?.textContent) {
return {};
}
// Attempt to parse the contents as JSON and return its value.
try {
return JSON.parse(element.textContent);
} catch (error) {
console.error("Unable to parse environment variables.");
}
// Otherwise, return an empty record.
return {};
}

View file

@ -5,27 +5,6 @@ import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentat
import type { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation"; import type { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import type KeycloakAdminClient from "keycloak-admin"; import type KeycloakAdminClient from "keycloak-admin";
// if we are running on Keycloak server, resourceUrl will be passed from index.ftl
declare const resourceUrl: string;
export const isDevMode = typeof resourceUrl === "undefined";
export const resourceUri = isDevMode ? "." : resourceUrl;
// if we are running on Keycloak server, loginRealm will be passed from index.ftl
declare const loginRealm: string;
export const homeRealm = () => {
if (typeof loginRealm !== "undefined") return loginRealm;
return new URLSearchParams(window.location.search).get("realm") || "master";
};
// if we are running on Keycloak server, authUrl will be passed from index.ftl
declare const authUrl: string;
export const authUri = () => {
if (typeof authUrl !== "undefined") return authUrl;
return "http://localhost:8180/auth";
};
export const sortProviders = (providers: { export const sortProviders = (providers: {
[index: string]: ProviderRepresentation; [index: string]: ProviderRepresentation;
}) => { }) => {