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",
|
||||
"appliedByProviders": "Applied by the following providers",
|
||||
"appliedByClients": "Applied by the following clients",
|
||||
"specificProviders": "Specific providers",
|
||||
"specificClients": "Specific clients",
|
||||
"default": "Default",
|
||||
"notInUse": "Not in use",
|
||||
"used": {
|
||||
"SPECIFIC_PROVIDERS": "Specific providers",
|
||||
"SPECIFIC_CLIENTS": "Specific clients",
|
||||
"DEFAULT": "Default",
|
||||
"notInUse": "Not in use"
|
||||
},
|
||||
"duplicate": "Duplicate",
|
||||
"bindFlow": "Bind flow",
|
||||
"chooseBindingType": "Choose binding type",
|
||||
"flow": {
|
||||
"browserFlow": "Browser flow",
|
||||
"registrationFlow": "Registration flow",
|
||||
"directGrantFlow": "Direct grant flow",
|
||||
"resetCredentialsFlow": "Reset credentials flow",
|
||||
"clientAuthenticationFlow": "Client authentication flow",
|
||||
"dockerAuthenticationFlow": "Docker authentication flow"
|
||||
"browser": "Browser flow",
|
||||
"registration": "Registration flow",
|
||||
"direct grant": "Direct grant flow",
|
||||
"reset credentials": "Reset credentials flow",
|
||||
"clients": "Client authentication flow",
|
||||
"docker auth": "Docker authentication flow"
|
||||
},
|
||||
"editInfo": "Edit info",
|
||||
"editFlow": "Edit flow",
|
||||
|
|
|
@ -36,23 +36,25 @@ import {
|
|||
RoutableTabs,
|
||||
} from "../components/routable-tabs/RoutableTabs";
|
||||
import { AuthenticationTab, toAuthentication } from "./routes/Authentication";
|
||||
import { addTrailingSlash } from "../util";
|
||||
import { getAuthorizationHeaders } from "../utils/getAuthorizationHeaders";
|
||||
|
||||
import "./authentication-section.css";
|
||||
|
||||
type UsedBy = "specificClients" | "default" | "specificProviders";
|
||||
type UsedBy = "SPECIFIC_CLIENTS" | "SPECIFIC_PROVIDERS" | "DEFAULT";
|
||||
|
||||
export type AuthenticationType = AuthenticationFlowRepresentation & {
|
||||
usedBy: { type?: UsedBy; values: string[] };
|
||||
usedBy?: { type?: UsedBy; values: string[] };
|
||||
};
|
||||
|
||||
export const REALM_FLOWS = [
|
||||
"browserFlow",
|
||||
"registrationFlow",
|
||||
"directGrantFlow",
|
||||
"resetCredentialsFlow",
|
||||
"clientAuthenticationFlow",
|
||||
"dockerAuthenticationFlow",
|
||||
];
|
||||
export const REALM_FLOWS = new Map<string, string>([
|
||||
["browserFlow", "browser"],
|
||||
["registrationFlow", "registration"],
|
||||
["directGrantFlow", "direct grant"],
|
||||
["resetCredentialsFlow", "reset credentials"],
|
||||
["clientAuthenticationFlow", "clients"],
|
||||
["dockerAuthenticationFlow", "docker auth"],
|
||||
]);
|
||||
|
||||
export default function AuthenticationSection() {
|
||||
const { t } = useTranslation("authentication");
|
||||
|
@ -68,54 +70,18 @@ export default function AuthenticationSection() {
|
|||
const [bindFlowOpen, toggleBindFlow] = useToggle();
|
||||
|
||||
const loader = async () => {
|
||||
const [allClients, allIdps, realmRep, flows] = await Promise.all([
|
||||
adminClient.clients.find(),
|
||||
adminClient.identityProviders.find(),
|
||||
adminClient.realms.findOne({ realm }),
|
||||
adminClient.authenticationManagement.getFlows(),
|
||||
]);
|
||||
if (!realmRep) {
|
||||
throw new Error(t("common:notFound"));
|
||||
}
|
||||
|
||||
const defaultFlows = Object.entries(realmRep).filter(([key]) =>
|
||||
REALM_FLOWS.includes(key)
|
||||
const flowsRequest = await fetch(
|
||||
`${addTrailingSlash(
|
||||
adminClient.baseUrl
|
||||
)}admin/realms/${realm}/admin-ui-authentication-management/flows`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: getAuthorizationHeaders(await adminClient.getAccessToken()),
|
||||
}
|
||||
);
|
||||
const flows = await flowsRequest.json();
|
||||
|
||||
for (const flow of flows as AuthenticationType[]) {
|
||||
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);
|
||||
return sortBy(flows as AuthenticationType[], (flow) => flow.usedBy?.type);
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
|
@ -156,7 +122,7 @@ export default function AuthenticationSection() {
|
|||
to={toFlow({
|
||||
realm,
|
||||
id: id!,
|
||||
usedBy: usedBy.type || "notInUse",
|
||||
usedBy: usedBy?.type || "notInUse",
|
||||
builtIn: builtIn ? "builtIn" : undefined,
|
||||
})}
|
||||
key={`link-${id}`}
|
||||
|
@ -236,7 +202,7 @@ export default function AuthenticationSection() {
|
|||
setSelectedFlow(data);
|
||||
},
|
||||
},
|
||||
...(data.usedBy.type !== "default" &&
|
||||
...(data.usedBy?.type !== "DEFAULT" &&
|
||||
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"),
|
||||
|
|
|
@ -84,7 +84,7 @@ export const BindFlowDialog = ({ flowAlias, onClose }: BindFlowDialogProps) => {
|
|||
<FormGroup label={t("chooseBindingType")} fieldId="chooseBindingType">
|
||||
<Controller
|
||||
name="bindingType"
|
||||
defaultValue={REALM_FLOWS[0]}
|
||||
defaultValue={"browserFlow"}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
|
@ -100,17 +100,20 @@ export const BindFlowDialog = ({ flowAlias, onClose }: BindFlowDialogProps) => {
|
|||
isOpen={open}
|
||||
menuAppendTo="parent"
|
||||
>
|
||||
{REALM_FLOWS.filter(
|
||||
(f) => f !== "dockerAuthenticationFlow"
|
||||
).map((flow) => (
|
||||
<SelectOption
|
||||
selected={flow === value}
|
||||
key={flow}
|
||||
value={flow}
|
||||
>
|
||||
{t(`flow.${flow}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
{[...REALM_FLOWS.keys()]
|
||||
.filter((f) => f !== "dockerAuthenticationFlow")
|
||||
.map((key) => {
|
||||
const value = REALM_FLOWS.get(key);
|
||||
return (
|
||||
<SelectOption
|
||||
selected={key === REALM_FLOWS.get(key)}
|
||||
key={key}
|
||||
value={key}
|
||||
>
|
||||
{t(`flow.${value}`)}
|
||||
</SelectOption>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -303,7 +303,7 @@ export default function FlowDetails() {
|
|||
<ViewHeader
|
||||
titleKey={flow?.alias || ""}
|
||||
badges={[
|
||||
{ text: <Label>{t(usedBy)}</Label> },
|
||||
{ text: <Label>{t(`used.${usedBy}`)}</Label> },
|
||||
builtIn
|
||||
? {
|
||||
text: (
|
||||
|
|
|
@ -10,9 +10,11 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
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 useToggle from "../../utils/useToggle";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { fetchUsedBy } from "../../components/role-mapping/resource";
|
||||
|
||||
import "./used-by.css";
|
||||
|
||||
|
@ -28,17 +30,31 @@ const Label = ({ label }: { label: string }) => (
|
|||
);
|
||||
|
||||
type UsedByModalProps = {
|
||||
values: string[];
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
isSpecificClient: boolean;
|
||||
};
|
||||
|
||||
const UsedByModal = ({
|
||||
values,
|
||||
isSpecificClient,
|
||||
onClose,
|
||||
}: UsedByModalProps) => {
|
||||
const UsedByModal = ({ id, isSpecificClient, onClose }: UsedByModalProps) => {
|
||||
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 (
|
||||
<Modal
|
||||
header={
|
||||
|
@ -66,7 +82,8 @@ const UsedByModal = ({
|
|||
]}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
loader={values.map((value) => ({ name: value }))}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="authentication:usedBy"
|
||||
searchPlaceholderKey="common:search"
|
||||
columns={[
|
||||
|
@ -79,12 +96,7 @@ const UsedByModal = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const UsedBy = ({
|
||||
authType: {
|
||||
id,
|
||||
usedBy: { type, values },
|
||||
},
|
||||
}: UsedByProps) => {
|
||||
export const UsedBy = ({ authType: { id, usedBy } }: UsedByProps) => {
|
||||
const { t } = useTranslation("authentication");
|
||||
const [open, toggle] = useToggle();
|
||||
|
||||
|
@ -92,26 +104,29 @@ export const UsedBy = ({
|
|||
<>
|
||||
{open && (
|
||||
<UsedByModal
|
||||
values={values}
|
||||
id={id!}
|
||||
onClose={toggle}
|
||||
isSpecificClient={type === "specificClients"}
|
||||
isSpecificClient={usedBy?.type === "SPECIFIC_CLIENTS"}
|
||||
/>
|
||||
)}
|
||||
{(type === "specificProviders" || type === "specificClients") &&
|
||||
(values.length < 8 ? (
|
||||
{(usedBy?.type === "SPECIFIC_PROVIDERS" ||
|
||||
usedBy?.type === "SPECIFIC_CLIENTS") &&
|
||||
(usedBy.values.length <= 8 ? (
|
||||
<Popover
|
||||
key={id}
|
||||
aria-label={t("usedBy")}
|
||||
bodyContent={
|
||||
<div key={`usedBy-${id}-${values}`}>
|
||||
<div key={`usedBy-${id}-${usedBy.values}`}>
|
||||
{t(
|
||||
"appliedBy" +
|
||||
(type === "specificClients" ? "Clients" : "Providers")
|
||||
(usedBy.type === "SPECIFIC_CLIENTS"
|
||||
? "Clients"
|
||||
: "Providers")
|
||||
)}{" "}
|
||||
{values.map((used, index) => (
|
||||
{usedBy.values.map((used, index) => (
|
||||
<>
|
||||
<strong>{used}</strong>
|
||||
{index < values.length - 1 ? ", " : ""}
|
||||
{index < usedBy.values.length - 1 ? ", " : ""}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
|
@ -121,7 +136,7 @@ export const UsedBy = ({
|
|||
variant="link"
|
||||
className="keycloak__used-by__popover-button"
|
||||
>
|
||||
<Label label={t(type!)} />
|
||||
<Label label={t(`used.${usedBy.type}`)} />
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
|
@ -130,11 +145,19 @@ export const UsedBy = ({
|
|||
className="keycloak__used-by__popover-button"
|
||||
onClick={toggle}
|
||||
>
|
||||
<Label label={t(type!)} />
|
||||
<Label label={t(`used.${usedBy.type}`)} />
|
||||
</Button>
|
||||
))}
|
||||
{type === "default" && <Label label={t(`flow.${values[0]}`)} />}
|
||||
{!type && t("notInUse")}
|
||||
{usedBy?.type === "DEFAULT" && (
|
||||
<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 { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
|
||||
|
||||
type BaseClientRolesQuery = {
|
||||
type BaseQuery = {
|
||||
adminClient: KeycloakAdminClient;
|
||||
id: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type AvailableClientRolesQuery = BaseClientRolesQuery & {
|
||||
type PaginatingQuery = BaseQuery & {
|
||||
first: number;
|
||||
max: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
type EffectiveClientRolesQuery = BaseClientRolesQuery;
|
||||
type EffectiveClientRolesQuery = BaseQuery;
|
||||
|
||||
type Query = Partial<Omit<AvailableClientRolesQuery, "adminClient">> & {
|
||||
type Query = Partial<Omit<PaginatingQuery, "adminClient">> & {
|
||||
adminClient: KeycloakAdminClient;
|
||||
endpoint: string;
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ type ClientRole = {
|
|||
clientId: string;
|
||||
};
|
||||
|
||||
const fetchRoles = async ({
|
||||
const fetchEndpoint = async ({
|
||||
adminClient,
|
||||
id,
|
||||
type,
|
||||
|
@ -36,7 +36,7 @@ const fetchRoles = async ({
|
|||
max,
|
||||
search,
|
||||
endpoint,
|
||||
}: Query): Promise<ClientRole[]> => {
|
||||
}: Query): Promise<any> => {
|
||||
return fetchAdminUI(adminClient, `/admin-ui-${endpoint}/${type}/${id}`, {
|
||||
first: (first || 0).toString(),
|
||||
max: (max || 10).toString(),
|
||||
|
@ -44,14 +44,15 @@ const fetchRoles = async ({
|
|||
});
|
||||
};
|
||||
|
||||
export const getAvailableClientRoles = async (
|
||||
query: AvailableClientRolesQuery
|
||||
): Promise<ClientRole[]> => {
|
||||
return fetchRoles({ ...query, endpoint: "available-roles" });
|
||||
};
|
||||
export const getAvailableClientRoles = (
|
||||
query: PaginatingQuery
|
||||
): Promise<ClientRole[]> =>
|
||||
fetchEndpoint({ ...query, endpoint: "available-roles" });
|
||||
|
||||
export const getEffectiveClientRoles = async (
|
||||
export const getEffectiveClientRoles = (
|
||||
query: EffectiveClientRolesQuery
|
||||
): Promise<ClientRole[]> => {
|
||||
return fetchRoles({ ...query, endpoint: "effective-roles" });
|
||||
};
|
||||
): Promise<ClientRole[]> =>
|
||||
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;
|
||||
|
||||
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.function.Predicate;
|
||||
|
@ -13,22 +13,21 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
|
||||
public abstract class RoleMappingResource {
|
||||
private RealmModel realm;
|
||||
private 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());
|
||||
}
|
||||
private final RealmModel realm;
|
||||
private final AdminPermissionEvaluator auth;
|
||||
|
||||
public RoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||
this.realm = realm;
|
||||
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;
|
||||
|
||||
public final class ClientRole {
|
||||
@Schema(required = true) private final String id;
|
||||
@Schema(required = true) private final String role;
|
||||
@Schema(required = true) private String client;
|
||||
@Schema(required = true) private String clientId;
|
||||
@Schema(required = true)
|
||||
private final String id;
|
||||
@Schema(required = true)
|
||||
private final String role;
|
||||
@Schema(required = true)
|
||||
private String client;
|
||||
@Schema(required = true)
|
||||
private String clientId;
|
||||
private String description;
|
||||
|
||||
public String getId() {
|
||||
|
|
|
@ -6,7 +6,7 @@ import org.keycloak.models.RoleModel;
|
|||
|
||||
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());
|
||||
ClientModel clientModel = clients.filter(c -> roleModel.getContainerId().equals(c.getId())).findFirst()
|
||||
.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);
|
||||
}
|
||||
}
|
|
@ -17,4 +17,5 @@
|
|||
|
||||
org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
|
||||
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