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:
Erik Jan de Wit 2022-09-13 14:36:41 +02:00 committed by GitHub
parent eb24eaa386
commit 33b9b95bd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 470 additions and 143 deletions

View file

@ -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",

View file

@ -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"),

View file

@ -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) => {
const value = REALM_FLOWS.get(key);
return (
<SelectOption <SelectOption
selected={flow === value} selected={key === REALM_FLOWS.get(key)}
key={flow} key={key}
value={flow} value={key}
> >
{t(`flow.${flow}`)} {t(`flow.${value}`)}
</SelectOption> </SelectOption>
))} );
})}
</Select> </Select>
)} )}
/> />

View file

@ -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: (

View file

@ -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")}
</> </>
); );
}; };

View file

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

View file

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

View file

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

View file

@ -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());
}
} }

View file

@ -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 + '\'' + '}';
}
}

View file

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

View file

@ -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() {

View file

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

View file

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

View file

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