garronej 2023-09-03 07:14:57 +02:00
parent 402c6fc64a
commit 39ff7913d6
51 changed files with 4267 additions and 277 deletions

View File

@ -1,73 +0,0 @@
{
"allOf": [
{
"$ref": "https://json.schemastore.org/package.json"
},
{
"$ref": "keycloakifyPackageJsonSchema"
}
],
"$ref": "#/definitions/keycloakifyPackageJsonSchema",
"definitions": {
"keycloakifyPackageJsonSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"homepage": {
"type": "string"
},
"keycloakify": {
"type": "object",
"properties": {
"extraPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraThemeProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"areAppAndKeycloakServerSharingSameDomain": {
"type": "boolean"
},
"artifactId": {
"type": "string"
},
"groupId": {
"type": "string"
},
"bundler": {
"type": "string",
"enum": ["mvn", "keycloakify", "none"]
},
"keycloakVersionDefaultAssets": {
"type": "string"
},
"reactAppBuildDirPath": {
"type": "string"
},
"keycloakifyBuildDirPath": {
"type": "string"
},
"themeName": {
"type": "string"
}
},
"additionalProperties": false
}
},
"required": ["name", "version"],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@ -13,7 +13,7 @@
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java",
"test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit",

View File

@ -1,6 +1,7 @@
import type { AccountThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { type ThemeType } from "keycloakify/bin/constants";
export type KcContext = KcContext.Password | KcContext.Account;

View File

@ -3,9 +3,8 @@ import { deepAssign } from "keycloakify/tools/deepAssign";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { resources_common } from "keycloakify/bin/constants";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
@ -89,11 +88,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
return { "kcContext": realKcContext as any };
}

View File

@ -1,18 +1,20 @@
import "minimal-polyfills/Object.fromEntries";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"themeName": "my-theme-name",
"url": {
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
resourcesPath,
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",

7
src/bin/constants.ts Normal file
View File

@ -0,0 +1,7 @@
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number];

View File

@ -1,39 +1,56 @@
#!/usr/bin/env node
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { basenameOfKeycloakDirInPublicDir } from "./mockTestingResourcesPath";
import { join as pathJoin, relative as pathRelative, isAbsolute as pathIsAbsolute } from "path";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { themeTypes } from "./keycloakify/generateFtl";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
import * as fs from "fs";
(async () => {
const projectDirPath = process.cwd();
const cwd = process.cwd();
const projectDirPath = cwd;
const publicDirPath = (() => {
from_env_var: {
const value = process.env["PUBLIC_DIR_PATH"];
if (value === undefined) {
break from_env_var;
}
return pathIsAbsolute(value) ? value : pathJoin(cwd, value);
}
return pathJoin(projectDirPath, "public");
})();
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2),
"projectDirPath": process.cwd()
});
const keycloakDirInPublicDir = pathJoin(process.env["PUBLIC_DIR_PATH"] || pathJoin(projectDirPath, "public"), basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} already exists.`);
return;
}
const reservedDirPath = pathJoin(publicDirPath, keycloak_resources);
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeType": themeType,
"themeDirPath": keycloakDirInPublicDir,
"keycloakVersion": (() => {
switch (themeType) {
case "login":
return buildOptions.loginThemeDefaultResourcesFromKeycloakVersion;
case "account":
return lastKeycloakVersionWithAccountV1;
}
})(),
themeType,
"themeDirPath": reservedDirPath,
"usedResources": undefined
});
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"),
pathJoin(reservedDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
@ -43,7 +60,7 @@ import * as fs from "fs";
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
fs.writeFileSync(pathJoin(publicDirPath, "keycloak-resources", ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} directory created.`);
console.log(`${pathRelative(projectDirPath, reservedDirPath)} directory created.`);
})();

View File

@ -2,14 +2,7 @@
import { getProjectRoot } from "./tools/getProjectRoot";
import cliSelect from "cli-select";
import {
loginThemePageIds,
accountThemePageIds,
type LoginThemePageId,
type AccountThemePageId,
themeTypes,
type ThemeType
} from "./keycloakify/generateFtl";
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./keycloakify/generateFtl";
import { capitalize } from "tsafe/capitalize";
import { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
@ -17,6 +10,7 @@ import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./getSrcDirPath";
import { themeTypes, type ThemeType } from "./constants";
(async () => {
console.log("Select a theme type");

View File

@ -2,7 +2,7 @@ import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./keycloakify/generateFtl";
import { themeTypes } from "./constants";
const themeSrcDirBasename = "keycloak-theme";

View File

@ -17,7 +17,7 @@ export type BuildOptions = {
groupId: string;
artifactId: string;
bundler: Bundler;
keycloakVersionDefaultAssets: string;
loginThemeDefaultResourcesFromKeycloakVersion: string;
/** Directory of your built react project. Defaults to {cwd}/build */
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
@ -42,7 +42,15 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
const {
extraThemeProperties,
groupId,
artifactId,
bundler,
keycloakVersionDefaultAssets,
loginThemeDefaultResourcesFromKeycloakVersion,
extraThemeNames = []
} = keycloakify ?? {};
const themeName =
keycloakify.themeName ??
@ -83,7 +91,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
extraThemeProperties,
"isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
"loginThemeDefaultResourcesFromKeycloakVersion": loginThemeDefaultResourcesFromKeycloakVersion ?? keycloakVersionDefaultAssets ?? "11.0.3",
"reactAppBuildDirPath": (() => {
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};

View File

@ -8,10 +8,7 @@ import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number];
import type { ThemeType } from "../../constants";
export type BuildOptionsLike = {
themeName: string;

View File

@ -1,84 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl";
export type BuildOptionsLike = {
themeName: string;
extraThemeNames: string[];
groupId: string;
artifactId: string;
themeVersion: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string;
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const {
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
implementedThemeTypes
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${themeVersion}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [themeName, ...extraThemeNames].map(themeName => ({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}))
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
};
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public enum AccountPages {
ACCOUNT,
PASSWORD,
TOTP,
FEDERATED_IDENTITY,
LOG,
SESSIONS,
APPLICATIONS,
RESOURCES,
RESOURCE_DETAIL;
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.util.List;
import org.keycloak.events.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AccountProvider extends Provider {
AccountProvider setUriInfo(UriInfo uriInfo);
AccountProvider setHttpHeaders(HttpHeaders httpHeaders);
Response createResponse(AccountPages page);
AccountProvider setError(Response.Status status, String message, Object... parameters);
AccountProvider setErrors(Response.Status status, List<FormMessage> messages);
AccountProvider setSuccess(String message, Object... parameters);
AccountProvider setWarning(String message, Object... parameters);
AccountProvider setUser(UserModel user);
AccountProvider setProfileFormData(MultivaluedMap<String, String> formData);
AccountProvider setRealm(RealmModel realm);
AccountProvider setReferrer(String[] referrer);
AccountProvider setEvents(List<Event> events);
AccountProvider setSessions(List<UserSessionModel> sessions);
AccountProvider setPasswordSet(boolean passwordSet);
AccountProvider setStateChecker(String stateChecker);
AccountProvider setIdTokenHint(String idTokenHint);
AccountProvider setFeatures(
boolean social,
boolean events,
boolean passwordUpdateSupported,
boolean authorizationSupported);
AccountProvider setAttribute(String key, String value);
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface AccountProviderFactory extends ProviderFactory<AccountProvider> {}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account;
import com.google.auto.service.AutoService;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@AutoService(Spi.class)
public class AccountSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "account";
}
@Override
public Class<? extends Provider> getProviderClass() {
return AccountProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return AccountProviderFactory.class;
}
}

View File

@ -0,0 +1,424 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import org.jboss.logging.Logger;
import org.keycloak.events.Event;
import org.keycloak.forms.account.AccountPages;
import org.keycloak.forms.account.AccountProvider;
import org.keycloak.forms.account.freemarker.model.AccountBean;
import org.keycloak.forms.account.freemarker.model.AccountFederatedIdentityBean;
import org.keycloak.forms.account.freemarker.model.ApplicationsBean;
import org.keycloak.forms.account.freemarker.model.AuthorizationBean;
import org.keycloak.forms.account.freemarker.model.FeaturesBean;
import org.keycloak.forms.account.freemarker.model.LogBean;
import org.keycloak.forms.account.freemarker.model.PasswordBean;
import org.keycloak.forms.account.freemarker.model.RealmBean;
import org.keycloak.forms.account.freemarker.model.ReferrerBean;
import org.keycloak.forms.account.freemarker.model.SessionsBean;
import org.keycloak.forms.account.freemarker.model.TotpBean;
import org.keycloak.forms.account.freemarker.model.UrlBean;
import org.keycloak.forms.login.MessageType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.theme.FreeMarkerException;
import org.keycloak.theme.Theme;
import org.keycloak.theme.beans.AdvancedMessageFormatterMethod;
import org.keycloak.theme.beans.LocaleBean;
import org.keycloak.theme.beans.MessageBean;
import org.keycloak.theme.beans.MessageFormatterMethod;
import org.keycloak.theme.beans.MessagesPerFieldBean;
import org.keycloak.theme.freemarker.FreeMarkerProvider;
import org.keycloak.utils.MediaType;
import org.keycloak.utils.StringUtil;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FreeMarkerAccountProvider implements AccountProvider {
private static final Logger logger = Logger.getLogger(FreeMarkerAccountProvider.class);
protected UserModel user;
protected MultivaluedMap<String, String> profileFormData;
protected Response.Status status = Response.Status.OK;
protected RealmModel realm;
protected String[] referrer;
protected List<Event> events;
protected String stateChecker;
protected String idTokenHint;
protected List<UserSessionModel> sessions;
protected boolean identityProviderEnabled;
protected boolean eventsEnabled;
protected boolean passwordUpdateSupported;
protected boolean passwordSet;
protected KeycloakSession session;
protected FreeMarkerProvider freeMarker;
protected HttpHeaders headers;
protected Map<String, Object> attributes;
protected UriInfo uriInfo;
protected List<FormMessage> messages = null;
protected MessageType messageType = MessageType.ERROR;
private boolean authorizationSupported;
public FreeMarkerAccountProvider(KeycloakSession session) {
this.session = session;
this.freeMarker = session.getProvider(FreeMarkerProvider.class);
}
public AccountProvider setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
@Override
public AccountProvider setHttpHeaders(HttpHeaders httpHeaders) {
this.headers = httpHeaders;
return this;
}
@Override
public Response createResponse(AccountPages page) {
Map<String, Object> attributes = new HashMap<>();
if (this.attributes != null) {
attributes.putAll(this.attributes);
}
Theme theme;
try {
theme = getTheme();
} catch (IOException e) {
logger.error("Failed to create theme", e);
return Response.serverError().build();
}
Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale, attributes);
URI baseUri = uriInfo.getBaseUri();
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
for (Map.Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
baseUriBuilder.queryParam(e.getKey(), e.getValue().toArray());
}
URI baseQueryUri = baseUriBuilder.build();
if (stateChecker != null) {
attributes.put("stateChecker", stateChecker);
}
handleMessages(locale, messagesBundle, attributes);
if (referrer != null) {
attributes.put("referrer", new ReferrerBean(referrer));
}
if (realm != null) {
attributes.put("realm", new RealmBean(realm));
}
attributes.put(
"url",
new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), idTokenHint));
if (realm.isInternationalizationEnabled()) {
UriBuilder b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath());
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
attributes.put(
"features",
new FeaturesBean(
identityProviderEnabled,
eventsEnabled,
passwordUpdateSupported,
authorizationSupported));
attributes.put("account", new AccountBean(user, profileFormData));
switch (page) {
case TOTP:
attributes.put("totp", new TotpBean(session, realm, user, uriInfo.getRequestUriBuilder()));
break;
case FEDERATED_IDENTITY:
attributes.put(
"federatedIdentity",
new AccountFederatedIdentityBean(
session, realm, user, uriInfo.getBaseUri(), stateChecker));
break;
case LOG:
attributes.put("log", new LogBean(events));
break;
case SESSIONS:
attributes.put("sessions", new SessionsBean(realm, sessions));
break;
case APPLICATIONS:
attributes.put("applications", new ApplicationsBean(session, realm, user));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
break;
case PASSWORD:
attributes.put("password", new PasswordBean(passwordSet));
break;
case RESOURCES:
if (!realm.isUserManagedAccessAllowed()) {
return Response.status(Status.FORBIDDEN).build();
}
attributes.put("authorization", new AuthorizationBean(session, realm, user, uriInfo));
case RESOURCE_DETAIL:
if (!realm.isUserManagedAccessAllowed()) {
return Response.status(Status.FORBIDDEN).build();
}
attributes.put("authorization", new AuthorizationBean(session, realm, user, uriInfo));
}
return processTemplate(theme, page, attributes, locale);
}
/**
* Get Theme used for page rendering.
*
* @return theme for page rendering, never null
* @throws IOException in case of Theme loading problem
*/
protected Theme getTheme() throws IOException {
return session.theme().getTheme(Theme.Type.ACCOUNT);
}
/**
* Load message bundle and place it into <code>msg</code> template attribute. Also load Theme
* properties and place them into <code>properties</code> template attribute.
*
* @param theme actual Theme to load bundle from
* @param locale to load bundle for
* @param attributes template attributes to add resources to
* @return message bundle for other use
*/
protected Properties handleThemeResources(
Theme theme, Locale locale, Map<String, Object> attributes) {
Properties messagesBundle = new Properties();
try {
if (!StringUtil.isNotBlank(realm.getDefaultLocale())) {
messagesBundle.putAll(realm.getRealmLocalizationTextsByLocale(realm.getDefaultLocale()));
}
messagesBundle.putAll(theme.getMessages(locale));
messagesBundle.putAll(realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag()));
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messagesBundle = new Properties();
}
try {
attributes.put("properties", theme.getProperties());
} catch (IOException e) {
logger.warn("Failed to load properties", e);
}
return messagesBundle;
}
/**
* Handle messages to be shown on the page - set them to template attributes
*
* @param locale to be used for message text loading
* @param messagesBundle to be used for message text loading
* @param attributes template attributes to messages related info to
* @see #messageType
* @see #messages
*/
protected void handleMessages(
Locale locale, Properties messagesBundle, Map<String, Object> attributes) {
MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
if (messages != null) {
MessageBean wholeMessage = new MessageBean(null, messageType);
for (FormMessage message : this.messages) {
String formattedMessageText = formatMessage(message, messagesBundle, locale);
if (formattedMessageText != null) {
wholeMessage.appendSummaryLine(formattedMessageText);
messagesPerField.addMessage(message.getField(), formattedMessageText, messageType);
}
}
attributes.put("message", wholeMessage);
}
attributes.put("messagesPerField", messagesPerField);
}
/**
* Process FreeMarker template and prepare Response. Some fields are used for rendering also.
*
* @param theme to be used (provided by <code>getTheme()</code>)
* @param page to be rendered
* @param attributes pushed to the template
* @param locale to be used
* @return Response object to be returned to the browser, never null
*/
protected Response processTemplate(
Theme theme, AccountPages page, Map<String, Object> attributes, Locale locale) {
try {
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
Response.ResponseBuilder builder =
Response.status(status)
.type(MediaType.TEXT_HTML_UTF_8_TYPE)
.language(locale)
.entity(result);
builder.cacheControl(CacheControlUtil.noCache());
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
return Response.serverError().build();
}
}
public AccountProvider setPasswordSet(boolean passwordSet) {
this.passwordSet = passwordSet;
return this;
}
protected void setMessage(MessageType type, String message, Object... parameters) {
messageType = type;
messages = new ArrayList<>();
messages.add(new FormMessage(null, message, parameters));
}
protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) {
if (message == null) return null;
if (messagesBundle.containsKey(message.getMessage())) {
return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale)
.format(message.getParameters());
} else {
return message.getMessage();
}
}
@Override
public AccountProvider setErrors(Response.Status status, List<FormMessage> messages) {
this.status = status;
this.messageType = MessageType.ERROR;
this.messages = new ArrayList<>(messages);
return this;
}
@Override
public AccountProvider setError(Response.Status status, String message, Object... parameters) {
this.status = status;
setMessage(MessageType.ERROR, message, parameters);
return this;
}
@Override
public AccountProvider setSuccess(String message, Object... parameters) {
setMessage(MessageType.SUCCESS, message, parameters);
return this;
}
@Override
public AccountProvider setWarning(String message, Object... parameters) {
setMessage(MessageType.WARNING, message, parameters);
return this;
}
@Override
public AccountProvider setUser(UserModel user) {
this.user = user;
return this;
}
@Override
public AccountProvider setProfileFormData(MultivaluedMap<String, String> formData) {
this.profileFormData = formData;
return this;
}
@Override
public AccountProvider setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
@Override
public AccountProvider setReferrer(String[] referrer) {
this.referrer = referrer;
return this;
}
@Override
public AccountProvider setEvents(List<Event> events) {
this.events = events;
return this;
}
@Override
public AccountProvider setSessions(List<UserSessionModel> sessions) {
this.sessions = sessions;
return this;
}
@Override
public AccountProvider setStateChecker(String stateChecker) {
this.stateChecker = stateChecker;
return this;
}
@Override
public AccountProvider setIdTokenHint(String idTokenHint) {
this.idTokenHint = idTokenHint;
return this;
}
@Override
public AccountProvider setFeatures(
boolean identityProviderEnabled,
boolean eventsEnabled,
boolean passwordUpdateSupported,
boolean authorizationSupported) {
this.identityProviderEnabled = identityProviderEnabled;
this.eventsEnabled = eventsEnabled;
this.passwordUpdateSupported = passwordUpdateSupported;
this.authorizationSupported = authorizationSupported;
return this;
}
@Override
public AccountProvider setAttribute(String key, String value) {
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(key, value);
return this;
}
@Override
public void close() {}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker;
import com.google.auto.service.AutoService;
import org.keycloak.Config;
import org.keycloak.forms.account.AccountProvider;
import org.keycloak.forms.account.AccountProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@AutoService(AccountProviderFactory.class)
public class FreeMarkerAccountProviderFactory implements AccountProviderFactory {
@Override
public AccountProvider create(KeycloakSession session) {
return new FreeMarkerAccountProvider(session);
}
@Override
public void init(Config.Scope config) {}
@Override
public void postInit(KeycloakSessionFactory factory) {}
@Override
public void close() {}
@Override
public String getId() {
return "freemarker";
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker;
import org.keycloak.forms.account.AccountPages;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Templates {
public static String getTemplate(AccountPages page) {
switch (page) {
case ACCOUNT:
return "account.ftl";
case PASSWORD:
return "password.ftl";
case TOTP:
return "totp.ftl";
case FEDERATED_IDENTITY:
return "federatedIdentity.ftl";
case LOG:
return "log.ftl";
case SESSIONS:
return "sessions.ftl";
case APPLICATIONS:
return "applications.ftl";
case RESOURCES:
return "resources.ftl";
case RESOURCE_DETAIL:
return "resource-detail.ftl";
default:
throw new IllegalArgumentException();
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AccountBean {
private static final Logger logger = Logger.getLogger(AccountBean.class);
private final UserModel user;
private final MultivaluedMap<String, String> profileFormData;
// TODO: More proper multi-value attribute support
private final Map<String, String> attributes = new HashMap<>();
public AccountBean(UserModel user, MultivaluedMap<String, String> profileFormData) {
this.user = user;
this.profileFormData = profileFormData;
for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
List<String> attrValue = attr.getValue();
if (attrValue.size() > 0) {
attributes.put(attr.getKey(), attrValue.get(0));
}
if (attrValue.size() > 1) {
logger.warnf(
"There are more values for attribute '%s' of user '%s' . Will display just first value",
attr.getKey(), user.getUsername());
}
}
if (profileFormData != null) {
for (String key : profileFormData.keySet()) {
if (key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
attributes.put(attribute, profileFormData.getFirst(key));
}
}
}
}
public String getFirstName() {
return profileFormData != null ? profileFormData.getFirst("firstName") : user.getFirstName();
}
public String getLastName() {
return profileFormData != null ? profileFormData.getFirst("lastName") : user.getLastName();
}
public String getUsername() {
if (profileFormData != null && profileFormData.containsKey("username")) {
return profileFormData.getFirst("username");
} else {
return user.getUsername();
}
}
public String getEmail() {
return profileFormData != null ? profileFormData.getFirst("email") : user.getEmail();
}
public Map<String, String> getAttributes() {
return attributes;
}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrderedModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.resources.account.AccountFormService;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:velias@redhat.com">Vlastimil Elias</a>
*/
public class AccountFederatedIdentityBean {
private static OrderedModel.OrderedModelComparator<FederatedIdentityEntry>
IDP_COMPARATOR_INSTANCE = new OrderedModel.OrderedModelComparator<>();
private final List<FederatedIdentityEntry> identities;
private final boolean removeLinkPossible;
private final KeycloakSession session;
public AccountFederatedIdentityBean(
KeycloakSession session, RealmModel realm, UserModel user, URI baseUri, String stateChecker) {
this.session = session;
AtomicInteger availableIdentities = new AtomicInteger(0);
this.identities =
realm
.getIdentityProvidersStream()
.filter(IdentityProviderModel::isEnabled)
.map(
provider -> {
String providerId = provider.getAlias();
FederatedIdentityModel identity =
getIdentity(
session.users().getFederatedIdentitiesStream(realm, user), providerId);
if (identity != null) {
availableIdentities.getAndIncrement();
}
String displayName =
KeycloakModelUtils.getIdentityProviderDisplayName(session, provider);
return new FederatedIdentityEntry(
identity,
displayName,
provider.getAlias(),
provider.getAlias(),
provider.getConfig() != null ? provider.getConfig().get("guiOrder") : null);
})
.sorted(IDP_COMPARATOR_INSTANCE)
.collect(Collectors.toList());
// Removing last social provider is not possible if you don't have other possibility to
// authenticate
this.removeLinkPossible =
availableIdentities.get() > 1
|| user.getFederationLink() != null
|| AccountFormService.isPasswordSet(session, realm, user);
}
private FederatedIdentityModel getIdentity(
Stream<FederatedIdentityModel> identities, String providerId) {
return identities
.filter(
federatedIdentityModel ->
Objects.equals(federatedIdentityModel.getIdentityProvider(), providerId))
.findFirst()
.orElse(null);
}
public List<FederatedIdentityEntry> getIdentities() {
return identities;
}
public boolean isRemoveLinkPossible() {
return removeLinkPossible;
}
public static class FederatedIdentityEntry implements OrderedModel {
private FederatedIdentityModel federatedIdentityModel;
private final String providerId;
private final String providerName;
private final String guiOrder;
private final String displayName;
public FederatedIdentityEntry(
FederatedIdentityModel federatedIdentityModel,
String displayName,
String providerId,
String providerName,
String guiOrder) {
this.federatedIdentityModel = federatedIdentityModel;
this.displayName = displayName;
this.providerId = providerId;
this.providerName = providerName;
this.guiOrder = guiOrder;
}
public String getProviderId() {
return providerId;
}
public String getProviderName() {
return providerName;
}
public String getUserId() {
return federatedIdentityModel != null ? federatedIdentityModel.getUserId() : null;
}
public String getUserName() {
return federatedIdentityModel != null ? federatedIdentityModel.getUserName() : null;
}
public boolean isConnected() {
return federatedIdentityModel != null;
}
@Override
public String getGuiOrder() {
return guiOrder;
}
public String getDisplayName() {
return displayName;
}
}
}

View File

@ -0,0 +1,258 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrderedModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.storage.StorageId;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ApplicationsBean {
private List<ApplicationEntry> applications = new LinkedList<>();
public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) {
Set<ClientModel> offlineClients =
new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
this.applications =
this.getApplications(session, realm, user)
.filter(
client ->
!isAdminClient(client)
|| AdminPermissions.realms(session, realm, user).isAdmin())
.map(client -> toApplicationEntry(session, realm, user, client, offlineClients))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public static boolean isAdminClient(ClientModel client) {
return client.getClientId().equals(Constants.ADMIN_CLI_CLIENT_ID)
|| client.getClientId().equals(Constants.ADMIN_CONSOLE_CLIENT_ID);
}
private Stream<ClientModel> getApplications(
KeycloakSession session, RealmModel realm, UserModel user) {
Predicate<ClientModel> bearerOnly = ClientModel::isBearerOnly;
Stream<ClientModel> clients = realm.getClientsStream().filter(bearerOnly.negate());
Predicate<ClientModel> isLocal = client -> new StorageId(client.getId()).isLocal();
return Stream.concat(
clients,
session
.users()
.getConsentsStream(realm, user.getId())
.map(UserConsentModel::getClient)
.filter(isLocal.negate()))
.distinct();
}
private void processRoles(
Set<RoleModel> inputRoles,
List<RoleModel> realmRoles,
MultivaluedHashMap<String, ClientRoleEntry> clientRoles) {
for (RoleModel role : inputRoles) {
if (role.getContainer() instanceof RealmModel) {
realmRoles.add(role);
} else {
ClientModel currentClient = (ClientModel) role.getContainer();
ClientRoleEntry clientRole =
new ClientRoleEntry(
currentClient.getClientId(),
currentClient.getName(),
role.getName(),
role.getDescription());
clientRoles.add(currentClient.getClientId(), clientRole);
}
}
}
public List<ApplicationEntry> getApplications() {
return applications;
}
public static class ApplicationEntry {
private KeycloakSession session;
private final List<RoleModel> realmRolesAvailable;
private final MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable;
private final ClientModel client;
private final List<String> clientScopesGranted;
private final List<String> additionalGrants;
public ApplicationEntry(
KeycloakSession session,
List<RoleModel> realmRolesAvailable,
MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable,
ClientModel client,
List<String> clientScopesGranted,
List<String> additionalGrants) {
this.session = session;
this.realmRolesAvailable = realmRolesAvailable;
this.resourceRolesAvailable = resourceRolesAvailable;
this.client = client;
this.clientScopesGranted = clientScopesGranted;
this.additionalGrants = additionalGrants;
}
public List<RoleModel> getRealmRolesAvailable() {
return realmRolesAvailable;
}
public MultivaluedHashMap<String, ClientRoleEntry> getResourceRolesAvailable() {
return resourceRolesAvailable;
}
public List<String> getClientScopesGranted() {
return clientScopesGranted;
}
public String getEffectiveUrl() {
return ResolveRelative.resolveRelativeUri(
session, getClient().getRootUrl(), getClient().getBaseUrl());
}
public ClientModel getClient() {
return client;
}
public List<String> getAdditionalGrants() {
return additionalGrants;
}
}
// Same class used in OAuthGrantBean as well. Maybe should be merged into common-freemarker...
public static class ClientRoleEntry {
private final String clientId;
private final String clientName;
private final String roleName;
private final String roleDescription;
public ClientRoleEntry(
String clientId, String clientName, String roleName, String roleDescription) {
this.clientId = clientId;
this.clientName = clientName;
this.roleName = roleName;
this.roleDescription = roleDescription;
}
public String getClientId() {
return clientId;
}
public String getClientName() {
return clientName;
}
public String getRoleName() {
return roleName;
}
public String getRoleDescription() {
return roleDescription;
}
}
/**
* Constructs a {@link ApplicationEntry} from the specified parameters.
*
* @param session a reference to the {@code Keycloak} session.
* @param realm a reference to the realm.
* @param user a reference to the user.
* @param client a reference to the client that contains the applications.
* @param offlineClients a {@link Set} containing the offline clients.
* @return the constructed {@link ApplicationEntry} instance or {@code null} if the user can't
* access the applications in the specified client.
*/
private ApplicationEntry toApplicationEntry(
final KeycloakSession session,
final RealmModel realm,
final UserModel user,
final ClientModel client,
final Set<ClientModel> offlineClients) {
// Construct scope parameter with all optional scopes to see all potentially available roles
Stream<ClientScopeModel> allClientScopes =
Stream.concat(
client.getClientScopes(true).values().stream(),
client.getClientScopes(false).values().stream());
allClientScopes = Stream.concat(allClientScopes, Stream.of(client)).distinct();
Set<RoleModel> availableRoles = TokenManager.getAccess(user, client, allClientScopes);
// Don't show applications, which user doesn't have access into (any available roles)
// unless this is can be changed by approving/revoking consent
if (!isAdminClient(client) && availableRoles.isEmpty() && !client.isConsentRequired()) {
return null;
}
List<RoleModel> realmRolesAvailable = new LinkedList<>();
MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable = new MultivaluedHashMap<>();
processRoles(availableRoles, realmRolesAvailable, resourceRolesAvailable);
List<ClientScopeModel> orderedScopes = new LinkedList<>();
if (client.isConsentRequired()) {
UserConsentModel consent =
session.users().getConsentByClient(realm, user.getId(), client.getId());
if (consent != null) {
orderedScopes.addAll(consent.getGrantedClientScopes());
}
}
List<String> clientScopesGranted =
orderedScopes.stream()
.sorted(OrderedModel.OrderedModelComparator.getInstance())
.map(ClientScopeModel::getConsentScreenText)
.collect(Collectors.toList());
List<String> additionalGrants = new ArrayList<>();
if (offlineClients.contains(client)) {
additionalGrants.add("${offlineToken}");
}
return new ApplicationEntry(
session,
realmRolesAvailable,
resourceRolesAvailable,
client,
clientScopesGranted,
additionalGrants);
}
}

View File

@ -0,0 +1,515 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import jakarta.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PermissionTicketStore;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.util.ResolveRelative;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class AuthorizationBean {
private final KeycloakSession session;
private final RealmModel realm;
private final UserModel user;
private final AuthorizationProvider authorization;
private final UriInfo uriInfo;
private ResourceBean resource;
private List<ResourceBean> resources;
private Collection<ResourceBean> userSharedResources;
private Collection<ResourceBean> requestsWaitingPermission;
private Collection<ResourceBean> resourcesWaitingOthersApproval;
public AuthorizationBean(
KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) {
this.session = session;
this.realm = realm;
this.user = user;
this.uriInfo = uriInfo;
authorization = session.getProvider(AuthorizationProvider.class);
List<String> pathParameters = uriInfo.getPathParameters().get("resource_id");
if (pathParameters != null && !pathParameters.isEmpty()) {
Resource resource =
authorization
.getStoreFactory()
.getResourceStore()
.findById(realm, null, pathParameters.get(0));
if (resource != null && !resource.getOwner().equals(user.getId())) {
throw new RuntimeException(
"User [" + user.getUsername() + "] can not access resource [" + resource.getId() + "]");
}
}
}
public Collection<ResourceBean> getResourcesWaitingOthersApproval() {
if (resourcesWaitingOthersApproval == null) {
Map<PermissionTicket.FilterOption, String> filters =
new EnumMap<>(PermissionTicket.FilterOption.class);
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
resourcesWaitingOthersApproval = toResourceRepresentation(findPermissions(filters));
}
return resourcesWaitingOthersApproval;
}
public Collection<ResourceBean> getResourcesWaitingApproval() {
if (requestsWaitingPermission == null) {
Map<PermissionTicket.FilterOption, String> filters =
new EnumMap<>(PermissionTicket.FilterOption.class);
filters.put(PermissionTicket.FilterOption.OWNER, user.getId());
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.FALSE.toString());
requestsWaitingPermission = toResourceRepresentation(findPermissions(filters));
}
return requestsWaitingPermission;
}
public List<ResourceBean> getResources() {
if (resources == null) {
resources =
authorization
.getStoreFactory()
.getResourceStore()
.findByOwner(realm, null, user.getId())
.stream()
.filter(Resource::isOwnerManagedAccess)
.map(ResourceBean::new)
.collect(Collectors.toList());
}
return resources;
}
public Collection<ResourceBean> getSharedResources() {
if (userSharedResources == null) {
Map<PermissionTicket.FilterOption, String> filters =
new EnumMap<>(PermissionTicket.FilterOption.class);
filters.put(PermissionTicket.FilterOption.REQUESTER, user.getId());
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
PermissionTicketStore ticketStore =
authorization.getStoreFactory().getPermissionTicketStore();
userSharedResources =
toResourceRepresentation(ticketStore.find(realm, null, filters, null, null));
}
return userSharedResources;
}
public ResourceBean getResource() {
if (resource == null) {
String resourceId = uriInfo.getPathParameters().getFirst("resource_id");
if (resourceId != null) {
resource = getResource(resourceId);
}
}
return resource;
}
private ResourceBean getResource(String id) {
return new ResourceBean(
authorization.getStoreFactory().getResourceStore().findById(realm, null, id));
}
public static class RequesterBean {
private final Long createdTimestamp;
private final Long grantedTimestamp;
private UserModel requester;
private List<PermissionScopeBean> scopes = new ArrayList<>();
private boolean granted;
public RequesterBean(PermissionTicket ticket, AuthorizationProvider authorization) {
this.requester =
authorization
.getKeycloakSession()
.users()
.getUserById(authorization.getRealm(), ticket.getRequester());
granted = ticket.isGranted();
createdTimestamp = ticket.getCreatedTimestamp();
grantedTimestamp = ticket.getGrantedTimestamp();
}
public UserModel getRequester() {
return requester;
}
public List<PermissionScopeBean> getScopes() {
return scopes;
}
private void addScope(PermissionTicket ticket) {
if (ticket != null) {
scopes.add(new PermissionScopeBean(ticket));
}
}
public boolean isGranted() {
return (granted && scopes.isEmpty())
|| scopes.stream().filter(permissionScopeBean -> permissionScopeBean.isGranted()).count()
> 0;
}
public Date getCreatedDate() {
return Time.toDate(createdTimestamp);
}
public Date getGrantedDate() {
if (grantedTimestamp == null) {
PermissionScopeBean permission =
scopes.stream()
.filter(permissionScopeBean -> permissionScopeBean.isGranted())
.findFirst()
.orElse(null);
if (permission == null) {
return null;
}
return permission.getGrantedDate();
}
return Time.toDate(grantedTimestamp);
}
}
public static class PermissionScopeBean {
private final Scope scope;
private final PermissionTicket ticket;
public PermissionScopeBean(PermissionTicket ticket) {
this.ticket = ticket;
scope = ticket.getScope();
}
public String getId() {
return ticket.getId();
}
public Scope getScope() {
return scope;
}
public boolean isGranted() {
return ticket.isGranted();
}
private Date getGrantedDate() {
if (isGranted()) {
return Time.toDate(ticket.getGrantedTimestamp());
}
return null;
}
}
public class ResourceBean {
private final ResourceServerBean resourceServer;
private final String ownerName;
private final UserModel userOwner;
private ClientModel clientOwner;
private Resource resource;
private Map<String, RequesterBean> permissions = new HashMap<>();
private Collection<RequesterBean> shares;
public ResourceBean(Resource resource) {
RealmModel realm = authorization.getRealm();
ResourceServer resourceServerModel = resource.getResourceServer();
resourceServer =
new ResourceServerBean(
realm.getClientById(resourceServerModel.getClientId()), resourceServerModel);
this.resource = resource;
userOwner =
authorization.getKeycloakSession().users().getUserById(realm, resource.getOwner());
if (userOwner == null) {
clientOwner = realm.getClientById(resource.getOwner());
ownerName = clientOwner.getClientId();
} else if (userOwner.getEmail() != null) {
ownerName = userOwner.getEmail();
} else {
ownerName = userOwner.getUsername();
}
}
public String getId() {
return resource.getId();
}
public String getName() {
return resource.getName();
}
public String getDisplayName() {
return resource.getDisplayName();
}
public String getIconUri() {
return resource.getIconUri();
}
public String getOwnerName() {
return ownerName;
}
public UserModel getUserOwner() {
return userOwner;
}
public ClientModel getClientOwner() {
return clientOwner;
}
public List<ScopeRepresentation> getScopes() {
return resource.getScopes().stream()
.map(ModelToRepresentation::toRepresentation)
.collect(Collectors.toList());
}
public Collection<RequesterBean> getShares() {
if (shares == null) {
Map<PermissionTicket.FilterOption, String> filters =
new EnumMap<>(PermissionTicket.FilterOption.class);
filters.put(PermissionTicket.FilterOption.RESOURCE_ID, this.resource.getId());
filters.put(PermissionTicket.FilterOption.GRANTED, Boolean.TRUE.toString());
shares = toPermissionRepresentation(findPermissions(filters));
}
return shares;
}
public Collection<ManagedPermissionBean> getPolicies() {
ResourceServer resourceServer = getResourceServer().getResourceServerModel();
RealmModel realm = resourceServer.getRealm();
Map<Policy.FilterOption, String[]> filters = new EnumMap<>(Policy.FilterOption.class);
filters.put(Policy.FilterOption.TYPE, new String[] {"uma"});
filters.put(Policy.FilterOption.RESOURCE_ID, new String[] {this.resource.getId()});
if (getUserOwner() != null) {
filters.put(Policy.FilterOption.OWNER, new String[] {getUserOwner().getId()});
} else {
filters.put(Policy.FilterOption.OWNER, new String[] {getClientOwner().getId()});
}
List<Policy> policies =
authorization
.getStoreFactory()
.getPolicyStore()
.find(realm, resourceServer, filters, null, null);
if (policies.isEmpty()) {
return Collections.emptyList();
}
return policies.stream()
.filter(
policy -> {
Map<PermissionTicket.FilterOption, String> filters1 =
new EnumMap<>(PermissionTicket.FilterOption.class);
filters1.put(PermissionTicket.FilterOption.POLICY_ID, policy.getId());
return authorization
.getStoreFactory()
.getPermissionTicketStore()
.find(realm, resourceServer, filters1, -1, 1)
.isEmpty();
})
.map(ManagedPermissionBean::new)
.collect(Collectors.toList());
}
public ResourceServerBean getResourceServer() {
return resourceServer;
}
public Collection<RequesterBean> getPermissions() {
return permissions.values();
}
private void addPermission(PermissionTicket ticket, AuthorizationProvider authorization) {
permissions
.computeIfAbsent(ticket.getRequester(), key -> new RequesterBean(ticket, authorization))
.addScope(ticket);
}
}
private Collection<RequesterBean> toPermissionRepresentation(
List<PermissionTicket> permissionRequests) {
Map<String, RequesterBean> requests = new HashMap<>();
for (PermissionTicket ticket : permissionRequests) {
Resource resource = ticket.getResource();
if (!resource.isOwnerManagedAccess()) {
continue;
}
requests
.computeIfAbsent(
ticket.getRequester(), resourceId -> new RequesterBean(ticket, authorization))
.addScope(ticket);
}
return requests.values();
}
private Collection<ResourceBean> toResourceRepresentation(List<PermissionTicket> tickets) {
Map<String, ResourceBean> requests = new HashMap<>();
for (PermissionTicket ticket : tickets) {
Resource resource = ticket.getResource();
if (!resource.isOwnerManagedAccess()) {
continue;
}
requests
.computeIfAbsent(resource.getId(), resourceId -> getResource(resourceId))
.addPermission(ticket, authorization);
}
return requests.values();
}
private List<PermissionTicket> findPermissions(
Map<PermissionTicket.FilterOption, String> filters) {
return authorization
.getStoreFactory()
.getPermissionTicketStore()
.find(realm, null, filters, null, null);
}
public class ResourceServerBean {
private ClientModel clientModel;
private ResourceServer resourceServer;
public ResourceServerBean(ClientModel clientModel, ResourceServer resourceServer) {
this.clientModel = clientModel;
this.resourceServer = resourceServer;
}
public String getId() {
return resourceServer.getId();
}
public String getName() {
String name = clientModel.getName();
if (name != null) {
return name;
}
return clientModel.getClientId();
}
public String getClientId() {
return clientModel.getClientId();
}
public String getRedirectUri() {
Set<String> redirectUris = clientModel.getRedirectUris();
if (redirectUris.isEmpty()) {
return null;
}
return redirectUris.iterator().next();
}
public String getBaseUri() {
return ResolveRelative.resolveRelativeUri(
session, clientModel.getRootUrl(), clientModel.getBaseUrl());
}
public ResourceServer getResourceServerModel() {
return resourceServer;
}
}
public class ManagedPermissionBean {
private final Policy policy;
private List<ManagedPermissionBean> policies;
public ManagedPermissionBean(Policy policy) {
this.policy = policy;
}
public String getId() {
return policy.getId();
}
public Collection<ScopeRepresentation> getScopes() {
return policy.getScopes().stream()
.map(ModelToRepresentation::toRepresentation)
.collect(Collectors.toList());
}
public String getDescription() {
return this.policy.getDescription();
}
public Collection<ManagedPermissionBean> getPolicies() {
if (this.policies == null) {
this.policies =
policy.getAssociatedPolicies().stream()
.map(ManagedPermissionBean::new)
.collect(Collectors.toList());
}
return this.policies;
}
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class FeaturesBean {
private final boolean identityFederation;
private final boolean log;
private final boolean passwordUpdateSupported;
private boolean authorization;
public FeaturesBean(
boolean identityFederation,
boolean log,
boolean passwordUpdateSupported,
boolean authorization) {
this.identityFederation = identityFederation;
this.log = log;
this.passwordUpdateSupported = passwordUpdateSupported;
this.authorization = authorization;
}
public boolean isIdentityFederation() {
return identityFederation;
}
public boolean isLog() {
return log;
}
public boolean isPasswordUpdateSupported() {
return passwordUpdateSupported;
}
public boolean isAuthorization() {
return authorization;
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.keycloak.events.Event;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LogBean {
private List<EventBean> events;
public LogBean(List<Event> events) {
this.events = new LinkedList<EventBean>();
for (Event e : events) {
this.events.add(new EventBean(e));
}
}
public List<EventBean> getEvents() {
return events;
}
public static class EventBean {
private Event event;
public EventBean(Event event) {
this.event = event;
}
public Date getDate() {
return new Date(event.getTime());
}
public String getEvent() {
return event.getType().toString().toLowerCase().replace("_", " ");
}
public String getClient() {
return event.getClientId();
}
public String getIpAddress() {
return event.getIpAddress();
}
public List<DetailBean> getDetails() {
List<DetailBean> details = new LinkedList<DetailBean>();
if (event.getDetails() != null) {
for (Map.Entry<String, String> e : event.getDetails().entrySet()) {
details.add(new DetailBean(e));
}
}
return details;
}
}
public static class DetailBean {
private Map.Entry<String, String> entry;
public DetailBean(Map.Entry<String, String> entry) {
this.entry = entry;
}
public String getKey() {
return entry.getKey();
}
public String getValue() {
return entry.getValue().replace("_", " ");
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class PasswordBean {
private boolean passwordSet;
public PasswordBean(boolean passwordSet) {
this.passwordSet = passwordSet;
}
public boolean isPasswordSet() {
return passwordSet;
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import java.util.Set;
import java.util.stream.Collectors;
import org.keycloak.models.RealmModel;
/**
* @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
*/
public class RealmBean {
private RealmModel realm;
public RealmBean(RealmModel realmModel) {
realm = realmModel;
}
public String getName() {
return realm.getName();
}
public String getDisplayName() {
String displayName = realm.getDisplayName();
if (displayName != null && displayName.length() > 0) {
return displayName;
} else {
return getName();
}
}
public String getDisplayNameHtml() {
String displayNameHtml = realm.getDisplayNameHtml();
if (displayNameHtml != null && displayNameHtml.length() > 0) {
return displayNameHtml;
} else {
return getDisplayName();
}
}
public boolean isInternationalizationEnabled() {
return realm.isInternationalizationEnabled();
}
public Set<String> getSupportedLocales() {
return realm.getSupportedLocalesStream().collect(Collectors.toSet());
}
public boolean isEditUsernameAllowed() {
return realm.isEditUsernameAllowed();
}
public boolean isRegistrationEmailAsUsername() {
return realm.isRegistrationEmailAsUsername();
}
public boolean isUserManagedAccessAllowed() {
return realm.isUserManagedAccessAllowed();
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ReferrerBean {
private String[] referrer;
public ReferrerBean(String[] referrer) {
this.referrer = referrer;
}
public String getName() {
return referrer[0];
}
public String getUrl() {
return referrer[1];
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class SessionsBean {
private List<UserSessionBean> events;
private RealmModel realm;
public SessionsBean(RealmModel realm, List<UserSessionModel> sessions) {
this.events = new LinkedList<>();
for (UserSessionModel session : sessions) {
this.events.add(new UserSessionBean(realm, session));
}
}
public List<UserSessionBean> getSessions() {
return events;
}
public static class UserSessionBean {
private UserSessionModel session;
private RealmModel realm;
public UserSessionBean(RealmModel realm, UserSessionModel session) {
this.realm = realm;
this.session = session;
}
public String getId() {
return session.getId();
}
public String getIpAddress() {
return session.getIpAddress();
}
public Date getStarted() {
return Time.toDate(session.getStarted());
}
public Date getLastAccess() {
return Time.toDate(session.getLastSessionRefresh());
}
public Date getExpires() {
int maxLifespan =
session.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0
? realm.getSsoSessionMaxLifespanRememberMe()
: realm.getSsoSessionMaxLifespan();
int max = session.getStarted() + maxLifespan;
return Time.toDate(max);
}
public Set<String> getClients() {
Set<String> clients = new HashSet<>();
for (String clientUUID : session.getAuthenticatedClientSessions().keySet()) {
ClientModel client = realm.getClientById(clientUUID);
clients.add(client.getClientId());
}
return clients;
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import static org.keycloak.utils.CredentialHelper.createUserStorageCredentialRepresentation;
import jakarta.ws.rs.core.UriBuilder;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.keycloak.authentication.otp.OTPApplicationProvider;
import org.keycloak.credential.CredentialModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.utils.TotpUtils;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TotpBean {
private final RealmModel realm;
private final String totpSecret;
private final String totpSecretEncoded;
private final String totpSecretQrCode;
private final boolean enabled;
private KeycloakSession session;
private final UriBuilder uriBuilder;
private final List<CredentialModel> otpCredentials;
private final List<String> supportedApplications;
public TotpBean(
KeycloakSession session, RealmModel realm, UserModel user, UriBuilder uriBuilder) {
this.session = session;
this.uriBuilder = uriBuilder;
this.enabled = user.credentialManager().isConfiguredFor(OTPCredentialModel.TYPE);
if (enabled) {
List<CredentialModel> otpCredentials =
user.credentialManager()
.getStoredCredentialsByTypeStream(OTPCredentialModel.TYPE)
.collect(Collectors.toList());
if (otpCredentials.isEmpty()) {
// Credential is configured on userStorage side. Create the "fake" credential similar like
// we do for the new account console
CredentialRepresentation credential =
createUserStorageCredentialRepresentation(OTPCredentialModel.TYPE);
this.otpCredentials = Collections.singletonList(RepresentationToModel.toModel(credential));
} else {
this.otpCredentials = otpCredentials;
}
} else {
this.otpCredentials = Collections.EMPTY_LIST;
}
this.realm = realm;
this.totpSecret = HmacOTP.generateSecret(20);
this.totpSecretEncoded = TotpUtils.encode(totpSecret);
this.totpSecretQrCode = TotpUtils.qrCode(totpSecret, realm, user);
OTPPolicy otpPolicy = realm.getOTPPolicy();
this.supportedApplications =
session.getAllProviders(OTPApplicationProvider.class).stream()
.filter(p -> p.supports(otpPolicy))
.map(OTPApplicationProvider::getName)
.collect(Collectors.toList());
}
public boolean isEnabled() {
return enabled;
}
public String getTotpSecret() {
return totpSecret;
}
public String getTotpSecretEncoded() {
return totpSecretEncoded;
}
public String getTotpSecretQrCode() {
return totpSecretQrCode;
}
public String getManualUrl() {
return uriBuilder.replaceQueryParam("mode", "manual").build().toString();
}
public String getQrUrl() {
return uriBuilder.replaceQueryParam("mode", "qr").build().toString();
}
public OTPPolicy getPolicy() {
return realm.getOTPPolicy();
}
public List<String> getSupportedApplications() {
return supportedApplications;
}
public List<CredentialModel> getOtpCredentials() {
return otpCredentials;
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.forms.account.freemarker.model;
import java.io.IOException;
import java.net.URI;
import org.jboss.logging.Logger;
import org.keycloak.models.RealmModel;
import org.keycloak.services.AccountUrls;
import org.keycloak.theme.Theme;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class UrlBean {
private static final Logger logger = Logger.getLogger(UrlBean.class);
private String realm;
private Theme theme;
private URI baseURI;
private URI baseQueryURI;
private URI currentURI;
private String idTokenHint;
public UrlBean(
RealmModel realm,
Theme theme,
URI baseURI,
URI baseQueryURI,
URI currentURI,
String idTokenHint) {
this.realm = realm.getName();
this.theme = theme;
this.baseURI = baseURI;
this.baseQueryURI = baseQueryURI;
this.currentURI = currentURI;
this.idTokenHint = idTokenHint;
}
public String getApplicationsUrl() {
return AccountUrls.accountApplicationsPage(baseQueryURI, realm).toString();
}
public String getAccountUrl() {
return AccountUrls.accountPage(baseQueryURI, realm).toString();
}
public String getPasswordUrl() {
return AccountUrls.accountPasswordPage(baseQueryURI, realm).toString();
}
public String getSocialUrl() {
return AccountUrls.accountFederatedIdentityPage(baseQueryURI, realm).toString();
}
public String getTotpUrl() {
return AccountUrls.accountTotpPage(baseQueryURI, realm).toString();
}
public String getLogUrl() {
return AccountUrls.accountLogPage(baseQueryURI, realm).toString();
}
public String getSessionsUrl() {
return AccountUrls.accountSessionsPage(baseQueryURI, realm).toString();
}
public String getLogoutUrl() {
return AccountUrls.accountLogout(baseQueryURI, currentURI, realm, idTokenHint).toString();
}
public String getResourceUrl() {
return AccountUrls.accountResourcesPage(baseQueryURI, realm).toString();
}
public String getResourceDetailUrl(String id) {
return AccountUrls.accountResourceDetailPage(id, baseQueryURI, realm).toString();
}
public String getResourceGrant(String id) {
return AccountUrls.accountResourceGrant(id, baseQueryURI, realm).toString();
}
public String getResourceShare(String id) {
return AccountUrls.accountResourceShare(id, baseQueryURI, realm).toString();
}
public String getResourcesPath() {
URI uri = AccountUrls.themeRoot(baseURI);
return uri.getPath() + "/" + theme.getType().toString().toLowerCase() + "/" + theme.getName();
}
public String getResourcesCommonPath() {
URI uri = AccountUrls.themeRoot(baseURI);
String commonPath = "";
try {
commonPath = theme.getProperties().getProperty("import");
} catch (IOException ex) {
logger.warn("Failed to load properties", ex);
}
if (commonPath == null || commonPath.isEmpty()) {
commonPath = "/common/keycloak";
}
return uri.getPath() + "/" + commonPath;
}
}

View File

@ -0,0 +1,115 @@
package org.keycloak.services;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.OAuth2Constants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.account.AccountFormService;
@JBossLog
public class AccountUrls extends Urls {
private static UriBuilder realmLogout(URI baseUri) {
return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "logout");
}
public static UriBuilder accountBase(URI baseUri) {
return realmBase(baseUri).path(RealmsResource.class, "getAccountService");
}
private static UriBuilder tokenBase(URI baseUri) {
return realmBase(baseUri).path("{realm}/protocol/" + OIDCLoginProtocol.LOGIN_PROTOCOL);
}
public static URI accountApplicationsPage(URI baseUri, String realmName) {
return accountBase(baseUri).path(AccountFormService.class, "applicationsPage").build(realmName);
}
public static URI accountPage(URI baseUri, String realmName) {
return accountPageBuilder(baseUri).build(realmName);
}
public static UriBuilder accountPageBuilder(URI baseUri) {
return accountBase(baseUri).path(AccountFormService.class, "accountPage");
}
public static URI accountPasswordPage(URI baseUri, String realmName) {
return accountBase(baseUri).path(AccountFormService.class, "passwordPage").build(realmName);
}
public static URI accountFederatedIdentityPage(URI baseUri, String realmName) {
return accountBase(baseUri)
.path(AccountFormService.class, "federatedIdentityPage")
.build(realmName);
}
public static URI accountFederatedIdentityUpdate(URI baseUri, String realmName) {
return accountBase(baseUri)
.path(AccountFormService.class, "processFederatedIdentityUpdate")
.build(realmName);
}
public static URI accountTotpPage(URI baseUri, String realmName) {
return accountBase(baseUri).path(AccountFormService.class, "totpPage").build(realmName);
}
public static URI accountLogPage(URI baseUri, String realmName) {
return accountBase(baseUri).path(AccountFormService.class, "logPage").build(realmName);
}
public static URI accountSessionsPage(URI baseUri, String realmName) {
return accountBase(baseUri).path(AccountFormService.class, "sessionsPage").build(realmName);
}
public static URI accountLogout(
URI baseUri, URI redirectUri, String realmName, String idTokenHint) {
return realmLogout(baseUri)
.queryParam(OAuth2Constants.POST_LOGOUT_REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.ID_TOKEN_HINT, idTokenHint)
.build(realmName);
}
public static URI accountResourcesPage(URI baseUri, String realmName) {
return accountBase(baseUri).path(AccountFormService.class, "resourcesPage").build(realmName);
}
public static URI accountResourceDetailPage(String resourceId, URI baseUri, String realmName) {
return accountBase(baseUri)
.path(AccountFormService.class, "resourceDetailPage")
.build(realmName, resourceId);
}
public static URI accountResourceGrant(String resourceId, URI baseUri, String realmName) {
return accountBase(baseUri)
.path(AccountFormService.class, "grantPermission")
.build(realmName, resourceId);
}
public static URI accountResourceShare(String resourceId, URI baseUri, String realmName) {
return accountBase(baseUri)
.path(AccountFormService.class, "shareResource")
.build(realmName, resourceId);
}
public static URI loginActionUpdatePassword(URI baseUri, String realmName) {
return loginActionsBase(baseUri)
.path(LoginActionsService.class, "updatePassword")
.build(realmName);
}
public static URI loginActionUpdateTotp(URI baseUri, String realmName) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmName);
}
public static URI loginActionEmailVerification(URI baseUri, String realmName) {
return loginActionEmailVerificationBuilder(baseUri).build(realmName);
}
public static String localeCookiePath(URI baseUri, String realmName) {
return realmBase(baseUri).path(realmName).build().getRawPath();
}
}

View File

@ -0,0 +1,64 @@
package org.keycloak.services.resources.account;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.Map;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.Config.Scope;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.services.resource.AccountResourceProvider;
import org.keycloak.services.resource.AccountResourceProviderFactory;
import jakarta.ws.rs.NotFoundException;
import org.keycloak.models.Constants;
@JBossLog
@AutoService(AccountResourceProviderFactory.class)
public class AccountFormServiceFactory implements AccountResourceProviderFactory {
public static final String ID = "account-v1";
@Override
public String getId() {
return ID;
}
private ClientModel getAccountManagementClient(RealmModel realm) {
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
if (client == null || !client.isEnabled()) {
log.debug("account management not enabled");
throw new NotFoundException("account management not enabled");
}
return client;
}
@Override
public AccountResourceProvider create(KeycloakSession session) {
log.info("create");
RealmModel realm = session.getContext().getRealm();
ClientModel client = getAccountManagementClient(realm);
EventBuilder event = new EventBuilder(realm, session, session.getContext().getConnection());
return new AccountFormService(session, client, event);
}
@Override
public void init(Scope config) {
log.info("init");
}
@Override
public void postInit(KeycloakSessionFactory factory) {
log.info("postInit");
}
@Override
public void close() {
log.info("close");
}
}

View File

@ -0,0 +1,271 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "../BuildOptions";
import { type ThemeType, resources_common, lastKeycloakVersionWithAccountV1 } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
export type BuildOptionsLike = {
themeName: string;
extraThemeNames: string[];
groupId: string;
artifactId: string;
themeVersion: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export async function generateJavaStackFiles(params: {
projectDirPath: string;
keycloakThemeBuildingDirPath: string;
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): Promise<{
jarFilePath: string;
}> {
const {
projectDirPath,
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
implementedThemeTypes
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${themeVersion}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <java.version>17</java.version>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` <keycloak.version>999.0.0-SNAPSHOT</keycloak.version>`,
` <guava.version>32.0.0-jre</guava.version>`,
` <lombok.version>1.18.28</lombok.version>`,
` <auto-service.version>1.1.1</auto-service.version>`,
` </properties>`,
` <build>`,
` <plugins>`,
` <plugin>`,
` <artifactId>maven-compiler-plugin</artifactId>`,
` <version>3.11.0</version>`,
` <configuration>`,
` <source>\${java.version}</source>`,
` <target>\${java.version}</target>`,
` <compilerArgument>-Xlint:unchecked</compilerArgument>`,
` <compilerArgument>-Xlint:deprecation</compilerArgument>`,
` <useIncrementalCompilation>false</useIncrementalCompilation>`,
` <annotationProcessorPaths>`,
` <path>`,
` <groupId>com.google.auto.service</groupId>`,
` <artifactId>auto-service</artifactId>`,
` <version>\${auto-service.version}</version>`,
` </path>`,
` <path>`,
` <groupId>org.projectlombok</groupId>`,
` <artifactId>lombok</artifactId>`,
` <version>\${lombok.version}</version>`,
` </path>`,
` </annotationProcessorPaths>`,
` </configuration>`,
` </plugin>`,
` <plugin>`,
` <groupId>org.apache.maven.plugins</groupId>`,
` <artifactId>maven-jar-plugin</artifactId>`,
` <version>3.2.0</version>`,
` <configuration>`,
` <archive>`,
` <manifestEntries>`,
` <Dependencies>`,
` <![CDATA[org.keycloak.keycloak-common,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,com.google.guava]]>`,
` </Dependencies>`,
` </manifestEntries>`,
` </archive>`,
` </configuration>`,
` </plugin>`,
` <plugin>`,
` <groupId>com.spotify.fmt</groupId>`,
` <artifactId>fmt-maven-plugin</artifactId>`,
` <version>2.20</version>`,
` </plugin>`,
` </plugins>`,
` </build>`,
` <dependencies>`,
` <dependency>`,
` <groupId>org.projectlombok</groupId>`,
` <artifactId>lombok</artifactId>`,
` <version>\${lombok.version}</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` <dependency>`,
` <groupId>com.google.auto.service</groupId>`,
` <artifactId>auto-service</artifactId>`,
` <version>\${auto-service.version}</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` <dependency>`,
` <groupId>org.keycloak</groupId>`,
` <artifactId>keycloak-server-spi</artifactId>`,
` <version>\${keycloak.version}</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` <dependency>`,
` <groupId>org.keycloak</groupId>`,
` <artifactId>keycloak-server-spi-private</artifactId>`,
` <version>\${keycloak.version}</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` <dependency>`,
` <groupId>org.keycloak</groupId>`,
` <artifactId>keycloak-services</artifactId>`,
` <version>\${keycloak.version}</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` <dependency>`,
` <groupId>jakarta.ws.rs</groupId>`,
` <artifactId>jakarta.ws.rs-api</artifactId>`,
` <version>3.1.0</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` <dependency>`,
` <groupId>com.google.guava</groupId>`,
` <artifactId>guava</artifactId>`,
` <version>\${guava.version}</version>`,
` <scope>provided</scope>`,
` </dependency>`,
` </dependencies>`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
const accountV1 = "account-v1";
{
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
projectDirPath,
"destDirPath": builtinKeycloakThemeTmpDirPath,
"keycloakVersion": lastKeycloakVersionWithAccountV1
});
const accountV1DirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", accountV1, "account");
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),
"destDirPath": accountV1DirPath
});
const commonResourceFilePaths = [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css"
];
for (const relativeFilePath of commonResourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", resources_common, relativeFilePath);
fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "common", "resources", relativeFilePath), destFilePath);
}
const resourceFilePaths = ["css/account.css"];
for (const relativeFilePath of resourceFilePaths.map(path => pathJoin(...path.split("/")))) {
const destFilePath = pathJoin(accountV1DirPath, "resources", relativeFilePath);
fs.mkdirSync(pathDirname(destFilePath), { "recursive": true });
fs.cpSync(pathJoin(builtinKeycloakThemeTmpDirPath, "keycloak", "account", "resources", relativeFilePath), destFilePath);
}
fs.rmdirSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
Buffer.from(
[
"accountResourceProvider=org.keycloak.services.resources.account.AccountFormService",
"",
"locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"",
"styles=" + [...resourceFilePaths, ...commonResourceFilePaths.map(path => `resources_common/${path}`)].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
"kcButtonClass=btn",
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
"kcButtonPrimaryClass=btn-primary",
"kcButtonDefaultClass=btn-default",
"# classes defining size of the button",
"kcButtonLargeClass=btn-lg",
""
].join("\n"),
"utf8"
)
);
}
transformCodebase({
"srcDirPath": pathJoin(__dirname, "account-v1-java"),
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "java", "org", "keycloak")
});
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [
{
"name": accountV1,
"types": ["account"]
},
...[themeName, ...extraThemeNames].map(themeName => ({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}))
]
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
};
}

View File

@ -0,0 +1 @@
export * from "./generateJavaStackFiles";

View File

@ -1,6 +1,7 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
@ -8,7 +9,11 @@ export type BuildOptionsLike = {
extraThemeNames: string[];
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";

View File

@ -1,13 +1,8 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import type { ThemeType } from "../generateFtl";
import { join as pathJoin } from "path";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import {
resourcesCommonDirPathRelativeToPublicDir,
resourcesDirPathRelativeToPublicDir,
basenameOfKeycloakDirInPublicDir
} from "../../mockTestingResourcesPath";
import { resources_common, type ThemeType } from "../../constants";
import * as crypto from "crypto";
export async function downloadKeycloakStaticResources(
@ -37,9 +32,11 @@ export async function downloadKeycloakStaticResources(
"destDirPath": tmpDirPath
});
const resourcesPath = pathJoin(themeDirPath, themeType, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir)),
"destDirPath": resourcesPath,
"transformSourceCode":
usedResources === undefined
? undefined
@ -54,7 +51,7 @@ export async function downloadKeycloakStaticResources(
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir)),
"destDirPath": pathJoin(resourcesPath, resources_common),
"transformSourceCode":
usedResources === undefined
? undefined

View File

@ -1,4 +1,4 @@
import type { ThemeType } from "../generateFtl";
import type { ThemeType } from "../../constants";
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import { readFileSync } from "fs";

View File

@ -3,11 +3,11 @@ import * as fs from "fs";
import { join as pathJoin } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "../generateFtl";
import { basenameOfKeycloakDirInPublicDir } from "../../mockTestingResourcesPath";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import { themeTypes, type ThemeType, lastKeycloakVersionWithAccountV1, keycloak_resources } from "../../constants";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
@ -18,7 +18,7 @@ export type BuildOptionsLike = {
themeName: string;
extraThemeProperties: string[] | undefined;
themeVersion: string;
keycloakVersionDefaultAssets: string;
loginThemeDefaultResourcesFromKeycloakVersion: string;
urlPathname: string | undefined;
};
@ -71,7 +71,7 @@ export async function generateTheme(params: {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
"dirPath": pathJoin(reactAppBuildDirPath, keycloak_resources),
filePath
})
) {
@ -160,44 +160,16 @@ export async function generateTheme(params: {
fs.writeFileSync(propertiesFilePath, Buffer.from(propertiesFileSource, "utf8"));
});
//TODO: Remove this block we left it for now only for backward compatibility
// we now have a separate script for this
copy_keycloak_resources_to_public: {
const keycloakDirInPublicDir = pathJoin(reactAppBuildDirPath, "..", "public", basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
break copy_keycloak_resources_to_public;
}
await downloadKeycloakStaticResources({
projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeDirPath": keycloakDirInPublicDir,
themeType,
"usedResources": undefined
});
if (themeType !== themeTypes[0]) {
break copy_keycloak_resources_to_public;
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
}
await downloadKeycloakStaticResources({
projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"keycloakVersion": (() => {
switch (themeType) {
case "account":
return lastKeycloakVersionWithAccountV1;
case "login":
return buildOptions.loginThemeDefaultResourcesFromKeycloakVersion;
}
})(),
themeDirPath,
themeType,
"usedResources": readStaticResourcesUsage({
@ -209,7 +181,21 @@ export async function generateTheme(params: {
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from([`parent=keycloak`, ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
Buffer.from(
[
`parent=${(() => {
switch (themeType) {
case "account":
return "account-v1";
case "login":
return "keycloak";
}
assert<Equals<typeof themeType, never>>(false);
})()}`,
...(buildOptions.extraThemeProperties ?? [])
].join("\n\n"),
"utf8"
)
);
}

View File

@ -1,9 +1,10 @@
import { crawl } from "../../tools/crawl";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs";
import { join as pathJoin } from "path";
import type { ThemeType } from "../../constants";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params;

View File

@ -2,7 +2,7 @@ import { crawl } from "../../tools/crawl";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../generateFtl";
import type { ThemeType } from "../../constants";
/** Assumes the theme type exists */
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {

View File

@ -1,7 +1,7 @@
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../generateFtl";
import type { ThemeType } from "../../constants";
/** Assumes the theme type exists */
export function readStaticResourcesUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): {

View File

@ -49,7 +49,8 @@ export async function main() {
});
}
const { jarFilePath } = generateJavaStackFiles({
const { jarFilePath } = await generateJavaStackFiles({
projectDirPath,
"keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
"implementedThemeTypes": (() => {
const implementedThemeTypes = {

View File

@ -16,7 +16,9 @@ export type ParsedPackageJson = {
artifactId?: string;
groupId?: string;
bundler?: Bundler;
/** @deprecated: Use loginThemeDefaultResourcesFromKeycloakVersion instead */
keycloakVersionDefaultAssets?: string;
loginThemeDefaultResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string;
@ -36,6 +38,7 @@ export const zParsedPackageJson = z.object({
"groupId": z.string().optional(),
"bundler": z.enum(bundlers).optional(),
"keycloakVersionDefaultAssets": z.string().optional(),
"loginThemeDefaultResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.string().optional(),

View File

@ -1,5 +0,0 @@
import { pathJoin } from "./tools/pathJoin";
export const basenameOfKeycloakDirInPublicDir = "keycloak-resources";
export const resourcesDirPathRelativeToPublicDir = pathJoin(basenameOfKeycloakDirInPublicDir, "resources");
export const resourcesCommonDirPathRelativeToPublicDir = pathJoin(resourcesDirPathRelativeToPublicDir, "resources_common");

View File

@ -162,6 +162,7 @@ export async function downloadAndUnzip(
} & (
| {
doUseCache: true;
// TODO: Get rid of this parameter, it's a pain to pass around
projectDirPath: string;
}
| {

View File

@ -2,5 +2,5 @@ export function pathJoin(...path: string[]): string {
return path
.map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
.map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
.join("/");
.join(typeof process !== "undefined" && process.platform === "win32" ? "\\" : "/");
}

View File

@ -1,4 +1,5 @@
import type { LoginThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { type ThemeType } from "keycloakify/bin/constants";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";

View File

@ -8,9 +8,8 @@ import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { resources_common } from "keycloakify/bin/constants";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
@ -148,11 +147,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
realKcContext.url.resourcesCommonPath = pathJoin(realKcContext.url.resourcesPath, resources_common);
return { "kcContext": realKcContext as any };
}

View File

@ -1,13 +1,11 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const attributes: Attribute[] = [
{
"validators": {
@ -102,14 +100,18 @@ const attributes: Attribute[] = [
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
const resourcesPath = pathJoin(PUBLIC_URL, keycloak_resources, "login", "resources");
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"themeType": "login",
"themeName": "my-theme-name",
"url": {
"loginAction": "#",
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
resourcesPath,
"resourcesCommonPath": pathJoin(resourcesPath, resources_common),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
},