diff --git a/.github/workflows/js-ci.yml b/.github/workflows/js-ci.yml index 3610bb21db..56c30d634d 100644 --- a/.github/workflows/js-ci.yml +++ b/.github/workflows/js-ci.yml @@ -211,7 +211,7 @@ jobs: - name: Start Keycloak server run: | tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz - keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=declarative-user-profile &> ~/server.log & + keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=declarative-user-profile,transient-users &> ~/server.log & env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin @@ -294,7 +294,7 @@ jobs: - name: Start Keycloak server run: | tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz - keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,declarative-user-profile &> ~/server.log & + keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,declarative-user-profile,transient-users &> ~/server.log & env: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin diff --git a/js/apps/admin-ui/cypress/e2e/identity_providers_test.spec.ts b/js/apps/admin-ui/cypress/e2e/identity_providers_test.spec.ts index 6f1c61fe81..5662d0f1da 100644 --- a/js/apps/admin-ui/cypress/e2e/identity_providers_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/identity_providers_test.spec.ts @@ -337,6 +337,15 @@ describe("Identity provider test", () => { advancedSettings.clickTrustEmailSwitch(); advancedSettings.clickAccountLinkingOnlySwitch(); advancedSettings.clickHideOnLoginPageSwitch(); + advancedSettings.assertDoNotImportUsersSwitchTurnedOn(false); + advancedSettings.assertSyncModeShown(true); + advancedSettings.clickdoNotStoreUsersSwitch(); + advancedSettings.assertDoNotImportUsersSwitchTurnedOn(true); + advancedSettings.assertSyncModeShown(false); + advancedSettings.clickdoNotStoreUsersSwitch(); + advancedSettings.assertDoNotImportUsersSwitchTurnedOn(false); + advancedSettings.assertSyncModeShown(true); + advancedSettings.clickEssentialClaimSwitch(); advancedSettings.typeClaimNameInput("claim-name"); advancedSettings.typeClaimValueInput("claim-value"); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/ProviderBaseAdvancedSettingsPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/ProviderBaseAdvancedSettingsPage.ts index fc34006615..a75494a42f 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/ProviderBaseAdvancedSettingsPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/ProviderBaseAdvancedSettingsPage.ts @@ -65,6 +65,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject { #promptSelect = "#prompt"; #disableUserInfoSwitch = "#disableUserInfo"; #trustEmailSwitch = "#trustEmail"; + #doNotStoreUsers = "#doNotStoreUsers"; #accountLinkingOnlySwitch = "#accountLinkingOnly"; #hideOnLoginPageSwitch = "#hideOnLoginPage"; #firstLoginFlowSelect = "#firstBrokerLoginFlowAlias"; @@ -149,6 +150,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject { return this; } + public clickdoNotStoreUsersSwitch() { + cy.get(this.#doNotStoreUsers).parent().click(); + return this; + } + public typeClaimNameInput(text: string) { cy.get(this.#claimNameInput).type(text).blur(); return this; @@ -273,6 +279,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject { return this; } + public assertDoNotImportUsersSwitchTurnedOn(isOn: boolean) { + super.assertSwitchStateOn(cy.get(this.#doNotStoreUsers).parent(), isOn); + return this; + } + public assertEssentialClaimSwitchTurnedOn(isOn: boolean) { super.assertSwitchStateOn( cy.get(this.#essentialClaimSwitch).parent(), @@ -310,6 +321,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject { return this; } + public assertSyncModeShown(isShown: boolean) { + cy.get(this.#syncModeSelect).should(isShown ? "exist" : "not.exist"); + return this; + } + public assertClientAssertSigAlgSelectOptionEqual( clientAssertionSigningAlg: ClientAssertionSigningAlg, ) { diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 82db8e695b..b0e8534191 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -2890,6 +2890,8 @@ startBySearchingAUser=Start by searching for users times.days=Days selectALocale=Select a locale clientsClientScopesHelp=The scopes associated with this resource. +transientUser=Transient +transientUserTooltip=This user not stored in Keycloak database. It is constructed solely from data provided by the originating identity provider. error-empty=Please specify value of '{{0}}'. error-invalid-blank=Please specify value of '{{0}}'. error-invalid-date='{{0}}' is invalid date. diff --git a/js/apps/admin-ui/src/sessions/SessionsTable.tsx b/js/apps/admin-ui/src/sessions/SessionsTable.tsx index d9b88cdd2b..8426fb9e3a 100644 --- a/js/apps/admin-ui/src/sessions/SessionsTable.tsx +++ b/js/apps/admin-ui/src/sessions/SessionsTable.tsx @@ -10,6 +10,7 @@ import { CubesIcon } from "@patternfly/react-icons"; import { ReactNode, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; +import { useMatch, useNavigate } from "react-router-dom"; import { adminClient } from "../admin-client"; import { toClient } from "../clients/routes/Client"; @@ -25,7 +26,9 @@ import { import { useRealm } from "../context/realm-context/RealmContext"; import { useWhoAmI } from "../context/whoami/WhoAmI"; import { keycloak } from "../keycloak"; -import { toUser } from "../user/routes/User"; +import { toUser, UserRoute } from "../user/routes/User"; +import { toUsers } from "../user/routes/Users"; +import { isLightweightUser } from "../user/utils"; import useFormatDate from "../utils/useFormatDate"; export type ColumnName = @@ -80,11 +83,13 @@ export default function SessionsTable({ }: SessionsTableProps) { const { realm } = useRealm(); const { whoAmI } = useWhoAmI(); + const navigate = useNavigate(); const { t } = useTranslation(); const { addError } = useAlerts(); const formatDate = useFormatDate(); const [key, setKey] = useState(0); const refresh = () => setKey((value) => value + 1); + const isOnUserPage = !!useMatch(UserRoute.path); const columns = useMemo(() => { const defaultColumns: Field[] = [ @@ -130,7 +135,11 @@ export default function SessionsTable({ onConfirm: async () => { try { await adminClient.users.logout({ id: logoutUser! }); - refresh(); + if (isOnUserPage && isLightweightUser(logoutUser)) { + navigate(toUsers({ realm: realm })); + } else { + refresh(); + } } catch (error) { addError("logoutAllSessionsError", error); } @@ -142,6 +151,8 @@ export default function SessionsTable({ if (session.userId === whoAmI.getUserId()) { await keycloak.logout({ redirectUri: "" }); + } else if (isOnUserPage && isLightweightUser(session.userId)) { + navigate(toUsers({ realm: realm })); } else { refresh(); } diff --git a/js/apps/admin-ui/src/user/EditUser.tsx b/js/apps/admin-ui/src/user/EditUser.tsx index 27af4c62ba..9fe743ffec 100644 --- a/js/apps/admin-ui/src/user/EditUser.tsx +++ b/js/apps/admin-ui/src/user/EditUser.tsx @@ -5,10 +5,13 @@ import { AlertVariant, ButtonVariant, DropdownItem, + Label, PageSection, Tab, TabTitleText, + Tooltip, } from "@patternfly/react-core"; +import { InfoCircleIcon } from "@patternfly/react-icons"; import { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -44,6 +47,7 @@ import { } from "./form-state"; import { UserParams, UserTab, toUser } from "./routes/User"; import { toUsers } from "./routes/Users"; +import { isLightweightUser } from "./utils"; import "./user-section.css"; @@ -63,6 +67,7 @@ export default function EditUser() { useState(); const [refreshCount, setRefreshCount] = useState(0); const refresh = () => setRefreshCount((count) => count + 1); + const lightweightUser = isLightweightUser(user?.id); const toTab = (tab: UserTab) => toUser({ @@ -141,7 +146,11 @@ export default function EditUser() { continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { - await adminClient.users.del({ id: user!.id! }); + if (lightweightUser) { + await adminClient.users.logout({ id: user!.id! }); + } else { + await adminClient.users.del({ id: user!.id! }); + } addAlert(t("userDeletedSuccess"), AlertVariant.success); navigate(toUsers({ realm: realmName })); } catch (error) { @@ -183,6 +192,24 @@ export default function EditUser() { titleKey={user.username!} className="kc-username-view-header" divider={false} + badges={ + lightweightUser + ? [ + { + text: ( + + + + ), + }, + ] + : [] + } dropdownItems={[ isRootAttribute(attribute.name) ? attribute.name : `attributes.${attribute.name}`; + +export const isLightweightUser = (userId?: string) => + userId?.startsWith("lightweight-"); diff --git a/js/apps/keycloak-server/scripts/start-server.js b/js/apps/keycloak-server/scripts/start-server.js index 515ec5b5a7..2407cd6bff 100755 --- a/js/apps/keycloak-server/scripts/start-server.js +++ b/js/apps/keycloak-server/scripts/start-server.js @@ -40,7 +40,7 @@ async function startServer() { [ "start-dev", "--http-port=8180", - "--features=account3,admin-fine-grained-authz,declarative-user-profile", + "--features=account3,admin-fine-grained-authz,declarative-user-profile,transient-users", ...keycloakArgs, ], {