created endpoint to query authentication section with used by information (#3225)
* created endpoint to query authentication section with used by information * buildIn !== builtIn
This commit is contained in:
parent
eb24eaa386
commit
33b9b95bd7
15 changed files with 470 additions and 143 deletions
|
@ -73,20 +73,22 @@
|
||||||
"buildIn": "Built-in",
|
"buildIn": "Built-in",
|
||||||
"appliedByProviders": "Applied by the following providers",
|
"appliedByProviders": "Applied by the following providers",
|
||||||
"appliedByClients": "Applied by the following clients",
|
"appliedByClients": "Applied by the following clients",
|
||||||
"specificProviders": "Specific providers",
|
"used": {
|
||||||
"specificClients": "Specific clients",
|
"SPECIFIC_PROVIDERS": "Specific providers",
|
||||||
"default": "Default",
|
"SPECIFIC_CLIENTS": "Specific clients",
|
||||||
"notInUse": "Not in use",
|
"DEFAULT": "Default",
|
||||||
|
"notInUse": "Not in use"
|
||||||
|
},
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"bindFlow": "Bind flow",
|
"bindFlow": "Bind flow",
|
||||||
"chooseBindingType": "Choose binding type",
|
"chooseBindingType": "Choose binding type",
|
||||||
"flow": {
|
"flow": {
|
||||||
"browserFlow": "Browser flow",
|
"browser": "Browser flow",
|
||||||
"registrationFlow": "Registration flow",
|
"registration": "Registration flow",
|
||||||
"directGrantFlow": "Direct grant flow",
|
"direct grant": "Direct grant flow",
|
||||||
"resetCredentialsFlow": "Reset credentials flow",
|
"reset credentials": "Reset credentials flow",
|
||||||
"clientAuthenticationFlow": "Client authentication flow",
|
"clients": "Client authentication flow",
|
||||||
"dockerAuthenticationFlow": "Docker authentication flow"
|
"docker auth": "Docker authentication flow"
|
||||||
},
|
},
|
||||||
"editInfo": "Edit info",
|
"editInfo": "Edit info",
|
||||||
"editFlow": "Edit flow",
|
"editFlow": "Edit flow",
|
||||||
|
|
|
@ -36,23 +36,25 @@ import {
|
||||||
RoutableTabs,
|
RoutableTabs,
|
||||||
} from "../components/routable-tabs/RoutableTabs";
|
} from "../components/routable-tabs/RoutableTabs";
|
||||||
import { AuthenticationTab, toAuthentication } from "./routes/Authentication";
|
import { AuthenticationTab, toAuthentication } from "./routes/Authentication";
|
||||||
|
import { addTrailingSlash } from "../util";
|
||||||
|
import { getAuthorizationHeaders } from "../utils/getAuthorizationHeaders";
|
||||||
|
|
||||||
import "./authentication-section.css";
|
import "./authentication-section.css";
|
||||||
|
|
||||||
type UsedBy = "specificClients" | "default" | "specificProviders";
|
type UsedBy = "SPECIFIC_CLIENTS" | "SPECIFIC_PROVIDERS" | "DEFAULT";
|
||||||
|
|
||||||
export type AuthenticationType = AuthenticationFlowRepresentation & {
|
export type AuthenticationType = AuthenticationFlowRepresentation & {
|
||||||
usedBy: { type?: UsedBy; values: string[] };
|
usedBy?: { type?: UsedBy; values: string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const REALM_FLOWS = [
|
export const REALM_FLOWS = new Map<string, string>([
|
||||||
"browserFlow",
|
["browserFlow", "browser"],
|
||||||
"registrationFlow",
|
["registrationFlow", "registration"],
|
||||||
"directGrantFlow",
|
["directGrantFlow", "direct grant"],
|
||||||
"resetCredentialsFlow",
|
["resetCredentialsFlow", "reset credentials"],
|
||||||
"clientAuthenticationFlow",
|
["clientAuthenticationFlow", "clients"],
|
||||||
"dockerAuthenticationFlow",
|
["dockerAuthenticationFlow", "docker auth"],
|
||||||
];
|
]);
|
||||||
|
|
||||||
export default function AuthenticationSection() {
|
export default function AuthenticationSection() {
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
|
@ -68,54 +70,18 @@ export default function AuthenticationSection() {
|
||||||
const [bindFlowOpen, toggleBindFlow] = useToggle();
|
const [bindFlowOpen, toggleBindFlow] = useToggle();
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const [allClients, allIdps, realmRep, flows] = await Promise.all([
|
const flowsRequest = await fetch(
|
||||||
adminClient.clients.find(),
|
`${addTrailingSlash(
|
||||||
adminClient.identityProviders.find(),
|
adminClient.baseUrl
|
||||||
adminClient.realms.findOne({ realm }),
|
)}admin/realms/${realm}/admin-ui-authentication-management/flows`,
|
||||||
adminClient.authenticationManagement.getFlows(),
|
{
|
||||||
]);
|
method: "GET",
|
||||||
if (!realmRep) {
|
headers: getAuthorizationHeaders(await adminClient.getAccessToken()),
|
||||||
throw new Error(t("common:notFound"));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFlows = Object.entries(realmRep).filter(([key]) =>
|
|
||||||
REALM_FLOWS.includes(key)
|
|
||||||
);
|
);
|
||||||
|
const flows = await flowsRequest.json();
|
||||||
|
|
||||||
for (const flow of flows as AuthenticationType[]) {
|
return sortBy(flows as AuthenticationType[], (flow) => flow.usedBy?.type);
|
||||||
flow.usedBy = { values: [] };
|
|
||||||
const clients = allClients.filter(
|
|
||||||
(client) =>
|
|
||||||
client.authenticationFlowBindingOverrides &&
|
|
||||||
(client.authenticationFlowBindingOverrides["direct_grant"] ===
|
|
||||||
flow.id ||
|
|
||||||
client.authenticationFlowBindingOverrides["browser"] === flow.id)
|
|
||||||
);
|
|
||||||
if (clients.length > 0) {
|
|
||||||
flow.usedBy.type = "specificClients";
|
|
||||||
flow.usedBy.values = clients.map(({ clientId }) => clientId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
const idps = allIdps.filter(
|
|
||||||
(idp) =>
|
|
||||||
idp.firstBrokerLoginFlowAlias === flow.alias ||
|
|
||||||
idp.postBrokerLoginFlowAlias === flow.alias
|
|
||||||
);
|
|
||||||
if (idps.length > 0) {
|
|
||||||
flow.usedBy.type = "specificProviders";
|
|
||||||
flow.usedBy.values = idps.map(({ alias }) => alias!);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultFlow = defaultFlows.find(
|
|
||||||
([, alias]) => flow.alias === alias
|
|
||||||
);
|
|
||||||
if (defaultFlow) {
|
|
||||||
flow.usedBy.type = "default";
|
|
||||||
flow.usedBy.values.push(defaultFlow[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortBy(flows as AuthenticationType[], (flow) => flow.usedBy.type);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
@ -156,7 +122,7 @@ export default function AuthenticationSection() {
|
||||||
to={toFlow({
|
to={toFlow({
|
||||||
realm,
|
realm,
|
||||||
id: id!,
|
id: id!,
|
||||||
usedBy: usedBy.type || "notInUse",
|
usedBy: usedBy?.type || "notInUse",
|
||||||
builtIn: builtIn ? "builtIn" : undefined,
|
builtIn: builtIn ? "builtIn" : undefined,
|
||||||
})}
|
})}
|
||||||
key={`link-${id}`}
|
key={`link-${id}`}
|
||||||
|
@ -236,7 +202,7 @@ export default function AuthenticationSection() {
|
||||||
setSelectedFlow(data);
|
setSelectedFlow(data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(data.usedBy.type !== "default" &&
|
...(data.usedBy?.type !== "DEFAULT" &&
|
||||||
data.providerId !== "client-flow"
|
data.providerId !== "client-flow"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
@ -248,7 +214,7 @@ export default function AuthenticationSection() {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(!data.builtIn && data.usedBy.values.length === 0
|
...(!data.builtIn && !data.usedBy
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: t("common:delete"),
|
title: t("common:delete"),
|
||||||
|
|
|
@ -84,7 +84,7 @@ export const BindFlowDialog = ({ flowAlias, onClose }: BindFlowDialogProps) => {
|
||||||
<FormGroup label={t("chooseBindingType")} fieldId="chooseBindingType">
|
<FormGroup label={t("chooseBindingType")} fieldId="chooseBindingType">
|
||||||
<Controller
|
<Controller
|
||||||
name="bindingType"
|
name="bindingType"
|
||||||
defaultValue={REALM_FLOWS[0]}
|
defaultValue={"browserFlow"}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Select
|
<Select
|
||||||
|
@ -100,17 +100,20 @@ export const BindFlowDialog = ({ flowAlias, onClose }: BindFlowDialogProps) => {
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
menuAppendTo="parent"
|
menuAppendTo="parent"
|
||||||
>
|
>
|
||||||
{REALM_FLOWS.filter(
|
{[...REALM_FLOWS.keys()]
|
||||||
(f) => f !== "dockerAuthenticationFlow"
|
.filter((f) => f !== "dockerAuthenticationFlow")
|
||||||
).map((flow) => (
|
.map((key) => {
|
||||||
<SelectOption
|
const value = REALM_FLOWS.get(key);
|
||||||
selected={flow === value}
|
return (
|
||||||
key={flow}
|
<SelectOption
|
||||||
value={flow}
|
selected={key === REALM_FLOWS.get(key)}
|
||||||
>
|
key={key}
|
||||||
{t(`flow.${flow}`)}
|
value={key}
|
||||||
</SelectOption>
|
>
|
||||||
))}
|
{t(`flow.${value}`)}
|
||||||
|
</SelectOption>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -303,7 +303,7 @@ export default function FlowDetails() {
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey={flow?.alias || ""}
|
titleKey={flow?.alias || ""}
|
||||||
badges={[
|
badges={[
|
||||||
{ text: <Label>{t(usedBy)}</Label> },
|
{ text: <Label>{t(`used.${usedBy}`)}</Label> },
|
||||||
builtIn
|
builtIn
|
||||||
? {
|
? {
|
||||||
text: (
|
text: (
|
||||||
|
|
|
@ -10,9 +10,11 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { CheckCircleIcon } from "@patternfly/react-icons";
|
import { CheckCircleIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
import type { AuthenticationType } from "../AuthenticationSection";
|
import { AuthenticationType, REALM_FLOWS } from "../AuthenticationSection";
|
||||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
import useToggle from "../../utils/useToggle";
|
import useToggle from "../../utils/useToggle";
|
||||||
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
|
import { fetchUsedBy } from "../../components/role-mapping/resource";
|
||||||
|
|
||||||
import "./used-by.css";
|
import "./used-by.css";
|
||||||
|
|
||||||
|
@ -28,17 +30,31 @@ const Label = ({ label }: { label: string }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
type UsedByModalProps = {
|
type UsedByModalProps = {
|
||||||
values: string[];
|
id: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isSpecificClient: boolean;
|
isSpecificClient: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UsedByModal = ({
|
const UsedByModal = ({ id, isSpecificClient, onClose }: UsedByModalProps) => {
|
||||||
values,
|
|
||||||
isSpecificClient,
|
|
||||||
onClose,
|
|
||||||
}: UsedByModalProps) => {
|
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
|
const { adminClient } = useAdminClient();
|
||||||
|
|
||||||
|
const loader = async (
|
||||||
|
first?: number,
|
||||||
|
max?: number,
|
||||||
|
search?: string
|
||||||
|
): Promise<{ name: string }[]> => {
|
||||||
|
const result = await fetchUsedBy({
|
||||||
|
adminClient,
|
||||||
|
id,
|
||||||
|
type: isSpecificClient ? "clients" : "idp",
|
||||||
|
first: first || 0,
|
||||||
|
max: max || 10,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
return result.map((p) => ({ name: p }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
header={
|
header={
|
||||||
|
@ -66,7 +82,8 @@ const UsedByModal = ({
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<KeycloakDataTable
|
<KeycloakDataTable
|
||||||
loader={values.map((value) => ({ name: value }))}
|
loader={loader}
|
||||||
|
isPaginated
|
||||||
ariaLabelKey="authentication:usedBy"
|
ariaLabelKey="authentication:usedBy"
|
||||||
searchPlaceholderKey="common:search"
|
searchPlaceholderKey="common:search"
|
||||||
columns={[
|
columns={[
|
||||||
|
@ -79,12 +96,7 @@ const UsedByModal = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UsedBy = ({
|
export const UsedBy = ({ authType: { id, usedBy } }: UsedByProps) => {
|
||||||
authType: {
|
|
||||||
id,
|
|
||||||
usedBy: { type, values },
|
|
||||||
},
|
|
||||||
}: UsedByProps) => {
|
|
||||||
const { t } = useTranslation("authentication");
|
const { t } = useTranslation("authentication");
|
||||||
const [open, toggle] = useToggle();
|
const [open, toggle] = useToggle();
|
||||||
|
|
||||||
|
@ -92,26 +104,29 @@ export const UsedBy = ({
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
<UsedByModal
|
<UsedByModal
|
||||||
values={values}
|
id={id!}
|
||||||
onClose={toggle}
|
onClose={toggle}
|
||||||
isSpecificClient={type === "specificClients"}
|
isSpecificClient={usedBy?.type === "SPECIFIC_CLIENTS"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(type === "specificProviders" || type === "specificClients") &&
|
{(usedBy?.type === "SPECIFIC_PROVIDERS" ||
|
||||||
(values.length < 8 ? (
|
usedBy?.type === "SPECIFIC_CLIENTS") &&
|
||||||
|
(usedBy.values.length <= 8 ? (
|
||||||
<Popover
|
<Popover
|
||||||
key={id}
|
key={id}
|
||||||
aria-label={t("usedBy")}
|
aria-label={t("usedBy")}
|
||||||
bodyContent={
|
bodyContent={
|
||||||
<div key={`usedBy-${id}-${values}`}>
|
<div key={`usedBy-${id}-${usedBy.values}`}>
|
||||||
{t(
|
{t(
|
||||||
"appliedBy" +
|
"appliedBy" +
|
||||||
(type === "specificClients" ? "Clients" : "Providers")
|
(usedBy.type === "SPECIFIC_CLIENTS"
|
||||||
|
? "Clients"
|
||||||
|
: "Providers")
|
||||||
)}{" "}
|
)}{" "}
|
||||||
{values.map((used, index) => (
|
{usedBy.values.map((used, index) => (
|
||||||
<>
|
<>
|
||||||
<strong>{used}</strong>
|
<strong>{used}</strong>
|
||||||
{index < values.length - 1 ? ", " : ""}
|
{index < usedBy.values.length - 1 ? ", " : ""}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -121,7 +136,7 @@ export const UsedBy = ({
|
||||||
variant="link"
|
variant="link"
|
||||||
className="keycloak__used-by__popover-button"
|
className="keycloak__used-by__popover-button"
|
||||||
>
|
>
|
||||||
<Label label={t(type!)} />
|
<Label label={t(`used.${usedBy.type}`)} />
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : (
|
) : (
|
||||||
|
@ -130,11 +145,19 @@ export const UsedBy = ({
|
||||||
className="keycloak__used-by__popover-button"
|
className="keycloak__used-by__popover-button"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<Label label={t(type!)} />
|
<Label label={t(`used.${usedBy.type}`)} />
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
{type === "default" && <Label label={t(`flow.${values[0]}`)} />}
|
{usedBy?.type === "DEFAULT" && (
|
||||||
{!type && t("notInUse")}
|
<Label
|
||||||
|
label={t(
|
||||||
|
[...REALM_FLOWS.values()].includes(usedBy.values[0])
|
||||||
|
? `flow.${usedBy.values[0]}`
|
||||||
|
: usedBy.values[0]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!usedBy?.type && t("used.notInUse")}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||||
import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
|
import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
|
||||||
|
|
||||||
type BaseClientRolesQuery = {
|
type BaseQuery = {
|
||||||
adminClient: KeycloakAdminClient;
|
adminClient: KeycloakAdminClient;
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AvailableClientRolesQuery = BaseClientRolesQuery & {
|
type PaginatingQuery = BaseQuery & {
|
||||||
first: number;
|
first: number;
|
||||||
max: number;
|
max: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EffectiveClientRolesQuery = BaseClientRolesQuery;
|
type EffectiveClientRolesQuery = BaseQuery;
|
||||||
|
|
||||||
type Query = Partial<Omit<AvailableClientRolesQuery, "adminClient">> & {
|
type Query = Partial<Omit<PaginatingQuery, "adminClient">> & {
|
||||||
adminClient: KeycloakAdminClient;
|
adminClient: KeycloakAdminClient;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
};
|
};
|
||||||
|
@ -28,7 +28,7 @@ type ClientRole = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchRoles = async ({
|
const fetchEndpoint = async ({
|
||||||
adminClient,
|
adminClient,
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
|
@ -36,7 +36,7 @@ const fetchRoles = async ({
|
||||||
max,
|
max,
|
||||||
search,
|
search,
|
||||||
endpoint,
|
endpoint,
|
||||||
}: Query): Promise<ClientRole[]> => {
|
}: Query): Promise<any> => {
|
||||||
return fetchAdminUI(adminClient, `/admin-ui-${endpoint}/${type}/${id}`, {
|
return fetchAdminUI(adminClient, `/admin-ui-${endpoint}/${type}/${id}`, {
|
||||||
first: (first || 0).toString(),
|
first: (first || 0).toString(),
|
||||||
max: (max || 10).toString(),
|
max: (max || 10).toString(),
|
||||||
|
@ -44,14 +44,15 @@ const fetchRoles = async ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAvailableClientRoles = async (
|
export const getAvailableClientRoles = (
|
||||||
query: AvailableClientRolesQuery
|
query: PaginatingQuery
|
||||||
): Promise<ClientRole[]> => {
|
): Promise<ClientRole[]> =>
|
||||||
return fetchRoles({ ...query, endpoint: "available-roles" });
|
fetchEndpoint({ ...query, endpoint: "available-roles" });
|
||||||
};
|
|
||||||
|
|
||||||
export const getEffectiveClientRoles = async (
|
export const getEffectiveClientRoles = (
|
||||||
query: EffectiveClientRolesQuery
|
query: EffectiveClientRolesQuery
|
||||||
): Promise<ClientRole[]> => {
|
): Promise<ClientRole[]> =>
|
||||||
return fetchRoles({ ...query, endpoint: "effective-roles" });
|
fetchEndpoint({ ...query, endpoint: "effective-roles" });
|
||||||
};
|
|
||||||
|
export const fetchUsedBy = (query: PaginatingQuery): Promise<string[]> =>
|
||||||
|
fetchEndpoint({ ...query, endpoint: "authentication-management" });
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
|
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider;
|
||||||
|
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
|
public final class AuthenticationManagementProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
|
||||||
|
public AdminRealmResourceProvider create(KeycloakSession session) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return "admin-ui-authentication-management";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
|
return new AuthenticationManagementResource(realm, auth);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||||
|
import org.keycloak.admin.ui.rest.model.Authentication;
|
||||||
|
import org.keycloak.admin.ui.rest.model.AuthenticationMapper;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@Consumes({"application/json"})
|
||||||
|
@Produces({"application/json"})
|
||||||
|
public class AuthenticationManagementResource extends RoleMappingResource {
|
||||||
|
@Context
|
||||||
|
private KeycloakSession session;
|
||||||
|
|
||||||
|
private RealmModel realm;
|
||||||
|
private AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public AuthenticationManagementResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
super(realm, auth);
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/flows")
|
||||||
|
@Operation(
|
||||||
|
summary = "List all authentication flows for this realm",
|
||||||
|
description = "This endpoint returns all the authentication flows and lists if there they are used."
|
||||||
|
)
|
||||||
|
@APIResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "",
|
||||||
|
content = {@Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = Authentication.class,
|
||||||
|
type = SchemaType.ARRAY
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
public final List<Authentication> listIdentityProviders() {
|
||||||
|
auth.realm().requireViewAuthenticationFlows();
|
||||||
|
|
||||||
|
return realm.getAuthenticationFlowsStream()
|
||||||
|
.filter(flow -> flow.isTopLevel() && !Objects.equals(flow.getAlias(), DefaultAuthenticationFlows.SAML_ECP_FLOW))
|
||||||
|
.map(flow -> AuthenticationMapper.convertToModel(flow, realm))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{type}/{id}")
|
||||||
|
@Operation(
|
||||||
|
summary = "List all clients or identity providers that this flow is used by",
|
||||||
|
description = "List all the clients or identity providers this flow is used by as a paginated list"
|
||||||
|
)
|
||||||
|
@APIResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "",
|
||||||
|
content = {@Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = String.class,
|
||||||
|
type = SchemaType.ARRAY
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
public final List<String> listUsed(@PathParam("id") String id, @PathParam("type") String type, @QueryParam("first") @DefaultValue("0") long first,
|
||||||
|
@QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||||
|
auth.realm().requireViewAuthenticationFlows();
|
||||||
|
|
||||||
|
final AuthenticationFlowModel flow = realm.getAuthenticationFlowsStream().filter(f -> id.equals(f.getId())).collect(Collectors.toList()).get(0);
|
||||||
|
|
||||||
|
if ("clients".equals(type)) {
|
||||||
|
final Stream<ClientModel> clients = realm.getClientsStream();
|
||||||
|
return clients.filter(
|
||||||
|
c -> c.getAuthenticationFlowBindingOverrides().get("browser") != null && c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("browser").equals(flow.getId()) || c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("direct_grant") != null && c.getAuthenticationFlowBindingOverrides().get("direct_grant").equals(flow.getId()))
|
||||||
|
.map(ClientModel::getClientId).filter(f -> f.contains(search))
|
||||||
|
.skip("".equals(search) ? first : 0).limit(max).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("idp".equals(type)) {
|
||||||
|
final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream();
|
||||||
|
return identityProviders.filter(idp -> idp.getFirstBrokerLoginFlowId().equals(flow.getId()))
|
||||||
|
.map(IdentityProviderModel::getAlias).filter(f -> f.contains(search))
|
||||||
|
.skip("".equals(search) ? first : 0).limit(max).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Invalid type");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package org.keycloak.admin.ui.rest;
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToRepresentation;
|
import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToModel;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
@ -13,22 +13,21 @@ import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
public abstract class RoleMappingResource {
|
public abstract class RoleMappingResource {
|
||||||
private RealmModel realm;
|
private final RealmModel realm;
|
||||||
private AdminPermissionEvaluator auth;
|
private final AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
public final Stream<ClientRole> mapping(Predicate<RoleModel> predicate) {
|
|
||||||
return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate)
|
|
||||||
.filter(auth.roles()::canMapClientScope).map(roleModel -> convertToRepresentation(roleModel, realm.getClientsStream()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public final List<ClientRole> mapping(Predicate<RoleModel> predicate, long first, long max, final String search) {
|
|
||||||
|
|
||||||
return mapping(predicate).filter(clientRole -> clientRole.getClient().contains(search) || clientRole.getRole().contains(search))
|
|
||||||
.skip(first).limit(max).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public RoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
public RoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final Stream<ClientRole> mapping(Predicate<RoleModel> predicate) {
|
||||||
|
return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate)
|
||||||
|
.filter(auth.roles()::canMapClientScope).map(roleModel -> convertToModel(roleModel, realm.getClientsStream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public final List<ClientRole> mapping(Predicate<RoleModel> predicate, long first, long max, final String search) {
|
||||||
|
return mapping(predicate).filter(clientRole -> clientRole.getClient().contains(search) || clientRole.getRole().contains(search))
|
||||||
|
.skip(first).limit(max).collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||||
|
|
||||||
|
public class Authentication {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String alias;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private boolean builtIn;
|
||||||
|
|
||||||
|
private UsedBy usedBy;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
public UsedBy getUsedBy() {
|
||||||
|
return usedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsedBy( UsedBy usedBy) {
|
||||||
|
this.usedBy = usedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBuiltIn() {
|
||||||
|
return builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuiltIn(boolean builtIn) {
|
||||||
|
this.builtIn = builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
|
Authentication that = (Authentication) o;
|
||||||
|
return builtIn == that.builtIn && Objects.equals(usedBy, that.usedBy) && Objects.equals(id, that.id) && Objects.equals(alias,
|
||||||
|
that.alias) && Objects.equals(description, that.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(usedBy, id, builtIn, alias, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toString() {
|
||||||
|
return "Authentication{" + "usedBy=" + usedBy + ", id='" + id + '\'' + ", buildIn=" + builtIn + ", alias='" + alias + '\'' + ", description='" + description + '\'' + '}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
public class AuthenticationMapper {
|
||||||
|
private static final int MAX_USED_BY = 9;
|
||||||
|
|
||||||
|
public static Authentication convertToModel(AuthenticationFlowModel flow, RealmModel realm) {
|
||||||
|
|
||||||
|
final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream();
|
||||||
|
final Stream<ClientModel> clients = realm.getClientsStream();
|
||||||
|
|
||||||
|
final Authentication authentication = new Authentication();
|
||||||
|
authentication.setId(flow.getId());
|
||||||
|
authentication.setAlias(flow.getAlias());
|
||||||
|
authentication.setBuiltIn(flow.isBuiltIn());
|
||||||
|
authentication.setDescription(flow.getDescription());
|
||||||
|
|
||||||
|
final List<String> usedByIdp = identityProviders.filter(idp -> idp.getFirstBrokerLoginFlowId().equals(flow.getId()))
|
||||||
|
.map(IdentityProviderModel::getAlias).limit(MAX_USED_BY).collect(Collectors.toList());
|
||||||
|
if (!usedByIdp.isEmpty()) {
|
||||||
|
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_PROVIDERS, usedByIdp));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> usedClients = clients.filter(
|
||||||
|
c -> c.getAuthenticationFlowBindingOverrides().get("browser") != null && c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("browser").equals(flow.getId()) || c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("direct_grant") != null && c.getAuthenticationFlowBindingOverrides().get("direct_grant").equals(flow.getId()))
|
||||||
|
.map(ClientModel::getClientId).limit(MAX_USED_BY).collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!usedClients.isEmpty()) {
|
||||||
|
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> useAsDefault = Stream.of(realm.getBrowserFlow(), realm.getRegistrationFlow(), realm.getDirectGrantFlow(),
|
||||||
|
realm.getResetCredentialsFlow(), realm.getClientAuthenticationFlow(), realm.getDockerAuthenticationFlow())
|
||||||
|
.filter(f -> flow.getAlias().equals(f.getAlias())).map(AuthenticationFlowModel::getAlias).collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!useAsDefault.isEmpty()) {
|
||||||
|
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.DEFAULT, useAsDefault));
|
||||||
|
}
|
||||||
|
|
||||||
|
return authentication;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,10 +4,14 @@ import java.util.Objects;
|
||||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||||
|
|
||||||
public final class ClientRole {
|
public final class ClientRole {
|
||||||
@Schema(required = true) private final String id;
|
@Schema(required = true)
|
||||||
@Schema(required = true) private final String role;
|
private final String id;
|
||||||
@Schema(required = true) private String client;
|
@Schema(required = true)
|
||||||
@Schema(required = true) private String clientId;
|
private final String role;
|
||||||
|
@Schema(required = true)
|
||||||
|
private String client;
|
||||||
|
@Schema(required = true)
|
||||||
|
private String clientId;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import org.keycloak.models.RoleModel;
|
||||||
|
|
||||||
public class RoleMapper {
|
public class RoleMapper {
|
||||||
|
|
||||||
public static ClientRole convertToRepresentation(RoleModel roleModel, Stream<ClientModel> clients) {
|
public static ClientRole convertToModel(RoleModel roleModel, Stream<ClientModel> clients) {
|
||||||
ClientRole clientRole = new ClientRole(roleModel.getId(), roleModel.getName(), roleModel.getDescription());
|
ClientRole clientRole = new ClientRole(roleModel.getId(), roleModel.getName(), roleModel.getDescription());
|
||||||
ClientModel clientModel = clients.filter(c -> roleModel.getContainerId().equals(c.getId())).findFirst()
|
ClientModel clientModel = clients.filter(c -> roleModel.getContainerId().equals(c.getId())).findFirst()
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Could not find referenced client"));
|
.orElseThrow(() -> new IllegalArgumentException("Could not find referenced client"));
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class UsedBy {
|
||||||
|
public UsedBy(UsedByType type, List<String> values) {
|
||||||
|
this.type = type;
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UsedByType {
|
||||||
|
SPECIFIC_CLIENTS, SPECIFIC_PROVIDERS, DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
private UsedByType type;
|
||||||
|
private List<String> values;
|
||||||
|
|
||||||
|
public UsedByType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(UsedByType type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValues(List<String> values) {
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
|
UsedBy usedBy = (UsedBy) o;
|
||||||
|
return type == usedBy.type && Objects.equals(values, usedBy.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(type, values);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,3 +18,4 @@
|
||||||
org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
|
org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
|
||||||
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider
|
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider
|
||||||
org.keycloak.admin.ui.rest.GroupsResourceProvider
|
org.keycloak.admin.ui.rest.GroupsResourceProvider
|
||||||
|
org.keycloak.admin.ui.rest.AuthenticationManagementProvider
|
Loading…
Reference in a new issue