8000 Link authentication flow UsedBy clients to client settings by singhvishalkr · Pull Request #48841 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 79 additions & 13 deletions js/apps/admin-ui/src/authentication/components/UsedBy.tsx
8000
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,47 @@ import {
TextVariants,
} from "@patternfly/react-core";
import { CheckCircleIcon } from "@patternfly/react-icons";
import { Fragment, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useAdminClient } from "../../admin-client";
import { fetchUsedBy } from "../../components/role-mapping/resource";
import {
fetchUsedBy,
type UsedByClientRef,
type UsedByReference as FlowUsedByRow,
} from "../../components/role-mapping/resource";
import { useRealm } from "../../context/realm-context/RealmContext";
import { toClient } from "../../clients/routes/Client";
import useToggle from "../../utils/useToggle";
import { AuthenticationType, REALM_FLOWS } from "../constants";

import style from "./used-by.module.css";

const ClientUsedByLink = ({
id,
clientId,
}: {
id?: string;
clientId: string;
}) => {
const { realm } = useRealm();
const label = <strong>{clientId}</strong>;
if (!id) {
return label;
}
return (
<Link
to={toClient({
realm,
clientId: id,
tab: "settings",
})}
>
{label}
</Link>
);
};

type UsedByProps = {
authType: AuthenticationType;
};
Expand All @@ -43,15 +75,14 @@ const UsedByModal = ({ id, isSpecificClient, onClose }: UsedByModalProps) => {
first?: number,
max?: number,
search?: string,
): Promise<{ name: string }[]> => {
const result = await fetchUsedBy(adminClient, {
): Promise<FlowUsedByRow[]> => {
return fetchUsedBy(adminClient, {
id,
type: isSpecificClient ? "clients" : "idp",
first: first || 0,
max: max || 10,
search,
});
return result.map((p) => ({ name: p }));
};

return (
Expand Down Expand Up @@ -86,9 +117,23 @@ const UsedByModal = ({ id, isSpecificClient, onClose }: UsedByModalProps) => {
ariaLabelKey="usedBy"
searchPlaceholderKey="search"
columns={[
{
name: "name",
},
isSpecificClient
? {
name: "name",
displayKey: "name",
cellRenderer: (row: FlowUsedByRow) => (
<ClientUsedByLink
id={row.id ?? undefined}
clientId={row.label}
/>
),
}
: {
name: "name",
cellRenderer: (row: FlowUsedByRow) => (
<strong>{row.label}</strong>
),
},
]}
/>
</Modal>
Expand All @@ -100,6 +145,17 @@ export const UsedBy = ({ authType: { id, usedBy } }: UsedByProps) => {
const { realmRepresentation: realm } = useRealm();
const [open, toggle] = useToggle();

const clientRefs = usedBy?.clientRefs;
const clientsOrFallback: UsedByClientRef[] = useMemo(() => {
if (!usedBy || usedBy.type !== "SPECIFIC_CLIENTS") {
return [];
}
if (clientRefs && clientRefs.length > 0) {
return clientRefs;
}
return usedBy.values.map((clientId) => ({ clientId }));
}, [usedBy, clientRefs]);
Comment on lines +148 to +157

const key = Object.entries(realm!).find(
(e) => e[1] === usedBy?.values[0],
)?.[0];
Expand Down Expand Up @@ -127,12 +183,22 @@ export const UsedBy = ({ authType: { id, usedBy } }: UsedByProps) => {
? "Clients"
: "Providers"),
)}{" "}
{usedBy.values.map((used, index) => (
<>
<strong>{used}</strong>
{index < usedBy.values.length - 1 ? ", " : ""}
</>
))}
{usedBy.type === "SPECIFIC_CLIENTS"
? clientsOrFallback.map((ref, index) => (
<Fragment key={`${id}-used-${ref.clientId}-${index}`}>
<ClientUsedByLink
id={ref.id ?? undefined}
clientId={ref.clientId}
/>
{index < clientsOrFallback.length - 1 ? ", " : ""}
</Fragment>
))
: usedBy.values.map((used, index) => (
<Fragment key={`${id}-used-${used}-${index}`}>
<strong>{used}</strong>
{index < usedBy.values.length - 1 ? ", " : ""}
</Fragment>
))}
</div>
}
>
Expand Down
10 changes: 9 additions & 1 deletion js/apps/admin-ui/src/authentication/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";

import type { UsedByClientRef } from "../components/role-mapping/resource";

export type { UsedByClientRef };

type UsedBy = "SPECIFIC_CLIENTS" | "SPECIFIC_PROVIDERS" | "DEFAULT";

export type AuthenticationType = AuthenticationFlowRepresentation & {
usedBy?: { type?: UsedBy; values: string[] };
usedBy?: {
type?: UsedBy;
values: string[];
clientRefs?: UsedByClientRef[];
};
realm: RealmRepresentation;
};

Expand Down
12 changes: 11 additions & 1 deletion js/apps/admin-ui/src/components/role-mapping/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,20 @@ export const findUsers = (
query as Record<string, string>,
);

export type UsedByClientRef = {
id?: string | null;
clientId: string;
};

export type UsedByReference = {
id?: string | null;
label: string;
};

export const fetchUsedBy = (
adminClient: KeycloakAdminClient,
query: PaginatingQuery,
): Promise<string[]> =>
): Promise<UsedByReference[]> =>
fetchEndpoint(adminClient, {
...query,
endpoint: "authentication-management",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.keycloak.admin.ui.rest.model.Authentication;
import org.keycloak.admin.ui.rest.model.AuthenticationMapper;
import org.keycloak.admin.ui.rest.model.ConfigurableRequiredActionProviderRepresentation;
import org.keycloak.admin.ui.rest.model.UsedByReference;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.models.AuthenticationFlowModel;
Expand Down Expand Up @@ -89,12 +90,12 @@ public final List<Authentication> listIdentityProviders() {
description = "",
content = {@Content(
schema = @Schema(
implementation = String.class,
implementation = UsedByReference.class,
type = SchemaType.ARRAY
)
)}
)
public final List<String> listUsed(@PathParam("id") String id, @PathParam("type") String type, @QueryParam("first") @DefaultValue("0") int first,
public final List<UsedByReference> listUsed(@PathParam("id") String id, @PathParam("type") String type, @QueryParam("first") @DefaultValue("0") int first,
@QueryParam("max") @DefaultValue("10") int max, @QueryParam("search") @DefaultValue("") String search) {
auth.realm().requireViewAuthenticationFlows();

Expand All @@ -106,12 +107,13 @@ public final List<String> listUsed(@PathParam("id") String id, @PathParam("type"
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());
.map(c -> new UsedByReference(c.getId(), c.getClientId())).filter(ref -> ref.getLabel().contains(search))
.skip(first).limit(max).collect(Collectors.toList());
}

if ("idp".equals(type)) {
return session.identityProviders().getByFlow(flow.getId(), search, first, max).toList();
return session.identityProviders().getByFlow(flow.getId(), search, first, max)
.map(alias -> new UsedByReference(null, alias)).collect(Collectors.toList());
}

throw new IllegalArgumentException("Invalid type");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.keycloak.admin.ui.rest.model;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -29,12 +31,16 @@ public static Authentication convertToModel(KeycloakSession session, Authenticat

Stream<ClientModel> browserFlowOverridingClients = realm.searchClientByAuthenticationFlowBindingOverrides(Collections.singletonMap("browser", flow.getId()), 0, MAX_USED_BY);
Stream<ClientModel> directGrantFlowOverridingClients = realm.searchClientByAuthenticationFlowBindingOverrides(Collections.singletonMap("direct_grant", flow.getId()), 0, MAX_USED_BY);
final List<String> usedClients = Stream.concat(browserFlowOverridingClients, directGrantFlowOverridingClients)
.limit(MAX_USED_BY)
.map(ClientModel::getClientId).collect(Collectors.toList());
Map<String, ClientModel> clientsByInternalId = new LinkedHashMap<>();
Stream.concat(browserFlowOverridingClients, directGrantFlowOverridingClients).forEach(c -> clientsByInternalId.putIfAbsent(c.getId(), c));

final List<ClientModel> clientModels = clientsByInternalId.values().stream().limit(MAX_USED_BY).collect(Collectors.toList());
final List<String> usedClients = clientModels.stream().map(ClientModel::getClientId).collect(Collectors.toList());
final List<UsedByClientRef> clientRefs = clientModels.stream()
.map(c -> new UsedByClientRef(c.getId(), c.getClientId())).collect(Collectors.toList());

if (!usedClients.isEmpty()) {
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients));
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients, clientRefs));
}

final List<String> useAsDefault = Stream.of(realm.getBrowserFlow(), realm.getRegistrationFlow(), realm.getDirectGrantFlow(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonInclude;

public class UsedBy {
public UsedBy(UsedByType type, List<String> values) {
this(type, values, null);
}

public UsedBy(UsedByType type, List<String> values, List<UsedByClientRef> clientRefs) {
this.type = type;
this.values = values;
this.clientRefs = clientRefs;
}

public enum UsedByType {
Expand All @@ -16,6 +23,12 @@ public enum UsedByType {
private UsedByType type;
private List<String> values;

/**
* Populated for {@link UsedByType#SPECIFIC_CLIENTS} flows so the admin UI can link to client settings without resolving clientId to internal id client-side.
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<UsedByClientRef> clientRefs;

public UsedByType getType() {
return type;
}
Expand All @@ -32,18 +45,27 @@ public void setValues(List<String> values) {
this.values = values;
}

public List<UsedByClientRef> getClientRefs() {
return clientRefs;
}

public void setClientRefs(List<UsedByClientRef> clientRefs) {
this.clientRefs = clientRefs;
}

@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);
return type == usedBy.type && Objects.equals(values, usedBy.values)
&& Objects.equals(clientRefs, usedBy.clientRefs);
}

@Override
public int hashCode() {
return Objects.hash(type, values);
return Objects.hash(type, values, clientRefs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.keycloak.admin.ui.rest.model;

import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonInclude;

public class UsedByClientRef {

@JsonInclude(JsonInclude.Include.NON_NULL)
private String id;
private String clientId;

public UsedByClientRef() {
}

public UsedByClientRef(String id, String clientId) {
this.id = id;
this.clientId = clientId;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getClientId() {
return clientId;
}

public void setClientId(String clientId) {
this.clientId = clientId;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UsedByClientRef that = (UsedByClientRef) o;
return Objects.equals(id, that.id) && Objects.equals(clientId, that.clientId);
}

@Override
public int hashCode() {
return Objects.hash(id, clientId);
}
}
Loading
Loading
0