From 14a7e4946843ffc03c0304a810f961fe4c928b87 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Tue, 6 Dec 2022 05:05:41 -0500 Subject: [PATCH] Add federated user link (#3928) --- .../public/resources/en/users-help.json | 3 +- apps/admin-ui/public/resources/en/users.json | 1 + apps/admin-ui/src/user/FederatedUserLink.tsx | 55 +++++++++++++++++++ apps/admin-ui/src/user/UserForm.tsx | 14 +++++ .../user-credentials/FederatedCredentials.tsx | 50 +++-------------- 5 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 apps/admin-ui/src/user/FederatedUserLink.tsx diff --git a/apps/admin-ui/public/resources/en/users-help.json b/apps/admin-ui/public/resources/en/users-help.json index 1c5092ab29..24436aa7c0 100644 --- a/apps/admin-ui/public/resources/en/users-help.json +++ b/apps/admin-ui/public/resources/en/users-help.json @@ -5,5 +5,6 @@ "requiredUserActions": "Require an action when the user logs in. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure OTP' requires setup of a mobile password generator.", "groups": "Groups where the user has membership. To leave a group, select it and click Leave.", "userIdHelperText": "Enter the unique ID of the user for this identity provider.", - "usernameHelperText": "Enter the username of the user for this identity provider." + "usernameHelperText": "Enter the username of the user for this identity provider.", + "federationLink": "UserStorageProvider this locally stored user was imported from." } \ No newline at end of file diff --git a/apps/admin-ui/public/resources/en/users.json b/apps/admin-ui/public/resources/en/users.json index dfe702a9e6..053fec8671 100644 --- a/apps/admin-ui/public/resources/en/users.json +++ b/apps/admin-ui/public/resources/en/users.json @@ -45,6 +45,7 @@ "emailInvalid": "You must enter a valid email.", "notVerified": "Not verified", "requiredUserActions": "Required user actions", + "federationLink": "Federation link", "addUser": "Add user", "impersonate": "Impersonate", "impersonateConfirm": "Impersonate user?", diff --git a/apps/admin-ui/src/user/FederatedUserLink.tsx b/apps/admin-ui/src/user/FederatedUserLink.tsx new file mode 100644 index 0000000000..15a0f18603 --- /dev/null +++ b/apps/admin-ui/src/user/FederatedUserLink.tsx @@ -0,0 +1,55 @@ +import { Button } from "@patternfly/react-core"; +import { useState } from "react"; +import { Link } from "react-router-dom-v5-compat"; + +import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; +import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; +import { useAccess } from "../context/access/Access"; +import { useAdminClient, useFetch } from "../context/auth/AdminClient"; +import { useRealm } from "../context/realm-context/RealmContext"; +import { toUserFederationLdap } from "../user-federation/routes/UserFederationLdap"; + +type FederatedUserLinkProps = { + user: UserRepresentation; +}; + +export const FederatedUserLink = ({ user }: FederatedUserLinkProps) => { + const access = useAccess(); + const { realm } = useRealm(); + const { adminClient } = useAdminClient(); + + const [component, setComponent] = useState(); + + useFetch( + () => + access.hasAccess("view-realm") + ? adminClient.components.findOne({ + id: (user.federationLink || user.origin)!, + }) + : adminClient.userStorageProvider.name({ + id: (user.federationLink || user.origin)!, + }), + setComponent, + [] + ); + + if (!component) return null; + + return ( + + ); +}; diff --git a/apps/admin-ui/src/user/UserForm.tsx b/apps/admin-ui/src/user/UserForm.tsx index 8389d4e49b..92f5a47fb3 100644 --- a/apps/admin-ui/src/user/UserForm.tsx +++ b/apps/admin-ui/src/user/UserForm.tsx @@ -31,6 +31,7 @@ import type RequiredActionProviderRepresentation from "@keycloak/keycloak-admin- import { useAccess } from "../context/access/Access"; import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled"; import { UserProfileFields } from "./UserProfileFields"; +import { FederatedUserLink } from "./FederatedUserLink"; export type BruteForced = { isBruteForceProtected?: boolean; @@ -242,6 +243,19 @@ export const UserForm = ({ )} /> + {(user?.federationLink || user?.origin) && ( + + } + > + + + )} {isFeatureEnabled(Feature.DeclarativeUserProfile) && realm?.attributes?.userProfileEnabled === "true" ? ( diff --git a/apps/admin-ui/src/user/user-credentials/FederatedCredentials.tsx b/apps/admin-ui/src/user/user-credentials/FederatedCredentials.tsx index d2dd61a664..fa99594e76 100644 --- a/apps/admin-ui/src/user/user-credentials/FederatedCredentials.tsx +++ b/apps/admin-ui/src/user/user-credentials/FederatedCredentials.tsx @@ -1,5 +1,3 @@ -import { useState } from "react"; -import { useTranslation } from "react-i18next"; import { Button, PageSection, @@ -13,15 +11,13 @@ import { Thead, Tr, } from "@patternfly/react-table"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; -import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation"; -import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; -import { useAccess } from "../../context/access/Access"; -import { toUserFederationLdap } from "../../user-federation/routes/UserFederationLdap"; -import { useRealm } from "../../context/realm-context/RealmContext"; -import { Link } from "react-router-dom"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; +import { FederatedUserLink } from "../FederatedUserLink"; type FederatedCredentialsProps = { user: UserRepresentation; @@ -34,32 +30,16 @@ export const FederatedCredentials = ({ }: FederatedCredentialsProps) => { const { t } = useTranslation("users"); const { adminClient } = useAdminClient(); - const { realm } = useRealm(); const [credentialTypes, setCredentialTypes] = useState(); - const [component, setComponent] = useState(); - const access = useAccess(); useFetch( - () => - Promise.all([ - adminClient.users.getUserStorageCredentialTypes({ id: user.id! }), - access.hasAccess("view-realm") - ? adminClient.components.findOne({ - id: (user.federationLink || user.origin)!, - }) - : adminClient.userStorageProvider.name({ - id: (user.federationLink || user.origin)!, - }), - ]), - ([credentialTypes, component]) => { - setCredentialTypes(credentialTypes); - setComponent(component); - }, + () => adminClient.users.getUserStorageCredentialTypes({ id: user.id! }), + setCredentialTypes, [] ); - if (!credentialTypes || !component) { + if (!credentialTypes) { return ; } @@ -80,21 +60,7 @@ export const FederatedCredentials = ({ {credential} - + {credential === "password" && (