Transient sessions: UX improvements

Closes: #24279

Signed-off-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
Hynek Mlnarik 2023-11-07 09:17:11 +01:00 committed by Hynek Mlnařík
parent bbe69fce0b
commit 5ec394b258
8 changed files with 74 additions and 6 deletions

View file

@ -211,7 +211,7 @@ jobs:
- name: Start Keycloak server - name: Start Keycloak server
run: | run: |
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz 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: env:
KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin KEYCLOAK_ADMIN_PASSWORD: admin
@ -294,7 +294,7 @@ jobs:
- name: Start Keycloak server - name: Start Keycloak server
run: | run: |
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz 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: env:
KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin KEYCLOAK_ADMIN_PASSWORD: admin

View file

@ -337,6 +337,15 @@ describe("Identity provider test", () => {
advancedSettings.clickTrustEmailSwitch(); advancedSettings.clickTrustEmailSwitch();
advancedSettings.clickAccountLinkingOnlySwitch(); advancedSettings.clickAccountLinkingOnlySwitch();
advancedSettings.clickHideOnLoginPageSwitch(); 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.clickEssentialClaimSwitch();
advancedSettings.typeClaimNameInput("claim-name"); advancedSettings.typeClaimNameInput("claim-name");
advancedSettings.typeClaimValueInput("claim-value"); advancedSettings.typeClaimValueInput("claim-value");

View file

@ -65,6 +65,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
#promptSelect = "#prompt"; #promptSelect = "#prompt";
#disableUserInfoSwitch = "#disableUserInfo"; #disableUserInfoSwitch = "#disableUserInfo";
#trustEmailSwitch = "#trustEmail"; #trustEmailSwitch = "#trustEmail";
#doNotStoreUsers = "#doNotStoreUsers";
#accountLinkingOnlySwitch = "#accountLinkingOnly"; #accountLinkingOnlySwitch = "#accountLinkingOnly";
#hideOnLoginPageSwitch = "#hideOnLoginPage"; #hideOnLoginPageSwitch = "#hideOnLoginPage";
#firstLoginFlowSelect = "#firstBrokerLoginFlowAlias"; #firstLoginFlowSelect = "#firstBrokerLoginFlowAlias";
@ -149,6 +150,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this; return this;
} }
public clickdoNotStoreUsersSwitch() {
cy.get(this.#doNotStoreUsers).parent().click();
return this;
}
public typeClaimNameInput(text: string) { public typeClaimNameInput(text: string) {
cy.get(this.#claimNameInput).type(text).blur(); cy.get(this.#claimNameInput).type(text).blur();
return this; return this;
@ -273,6 +279,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this; return this;
} }
public assertDoNotImportUsersSwitchTurnedOn(isOn: boolean) {
super.assertSwitchStateOn(cy.get(this.#doNotStoreUsers).parent(), isOn);
return this;
}
public assertEssentialClaimSwitchTurnedOn(isOn: boolean) { public assertEssentialClaimSwitchTurnedOn(isOn: boolean) {
super.assertSwitchStateOn( super.assertSwitchStateOn(
cy.get(this.#essentialClaimSwitch).parent(), cy.get(this.#essentialClaimSwitch).parent(),
@ -310,6 +321,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this; return this;
} }
public assertSyncModeShown(isShown: boolean) {
cy.get(this.#syncModeSelect).should(isShown ? "exist" : "not.exist");
return this;
}
public assertClientAssertSigAlgSelectOptionEqual( public assertClientAssertSigAlgSelectOptionEqual(
clientAssertionSigningAlg: ClientAssertionSigningAlg, clientAssertionSigningAlg: ClientAssertionSigningAlg,
) { ) {

View file

@ -2890,6 +2890,8 @@ startBySearchingAUser=Start by searching for users
times.days=Days times.days=Days
selectALocale=Select a locale selectALocale=Select a locale
clientsClientScopesHelp=The scopes associated with this resource. 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-empty=Please specify value of '{{0}}'.
error-invalid-blank=Please specify value of '{{0}}'. error-invalid-blank=Please specify value of '{{0}}'.
error-invalid-date='{{0}}' is invalid date. error-invalid-date='{{0}}' is invalid date.

View file

@ -10,6 +10,7 @@ import { CubesIcon } from "@patternfly/react-icons";
import { ReactNode, useMemo, useState } from "react"; import { ReactNode, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useMatch, useNavigate } from "react-router-dom";
import { adminClient } from "../admin-client"; import { adminClient } from "../admin-client";
import { toClient } from "../clients/routes/Client"; import { toClient } from "../clients/routes/Client";
@ -25,7 +26,9 @@ import {
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { useWhoAmI } from "../context/whoami/WhoAmI"; import { useWhoAmI } from "../context/whoami/WhoAmI";
import { keycloak } from "../keycloak"; 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"; import useFormatDate from "../utils/useFormatDate";
export type ColumnName = export type ColumnName =
@ -80,11 +83,13 @@ export default function SessionsTable({
}: SessionsTableProps) { }: SessionsTableProps) {
const { realm } = useRealm(); const { realm } = useRealm();
const { whoAmI } = useWhoAmI(); const { whoAmI } = useWhoAmI();
const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const { addError } = useAlerts(); const { addError } = useAlerts();
const formatDate = useFormatDate(); const formatDate = useFormatDate();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey((value) => value + 1); const refresh = () => setKey((value) => value + 1);
const isOnUserPage = !!useMatch(UserRoute.path);
const columns = useMemo(() => { const columns = useMemo(() => {
const defaultColumns: Field<UserSessionRepresentation>[] = [ const defaultColumns: Field<UserSessionRepresentation>[] = [
@ -130,7 +135,11 @@ export default function SessionsTable({
onConfirm: async () => { onConfirm: async () => {
try { try {
await adminClient.users.logout({ id: logoutUser! }); await adminClient.users.logout({ id: logoutUser! });
refresh(); if (isOnUserPage && isLightweightUser(logoutUser)) {
navigate(toUsers({ realm: realm }));
} else {
refresh();
}
} catch (error) { } catch (error) {
addError("logoutAllSessionsError", error); addError("logoutAllSessionsError", error);
} }
@ -142,6 +151,8 @@ export default function SessionsTable({
if (session.userId === whoAmI.getUserId()) { if (session.userId === whoAmI.getUserId()) {
await keycloak.logout({ redirectUri: "" }); await keycloak.logout({ redirectUri: "" });
} else if (isOnUserPage && isLightweightUser(session.userId)) {
navigate(toUsers({ realm: realm }));
} else { } else {
refresh(); refresh();
} }

View file

@ -5,10 +5,13 @@ import {
AlertVariant, AlertVariant,
ButtonVariant, ButtonVariant,
DropdownItem, DropdownItem,
Label,
PageSection, PageSection,
Tab, Tab,
TabTitleText, TabTitleText,
Tooltip,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { InfoCircleIcon } from "@patternfly/react-icons";
import { useState } from "react"; import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -44,6 +47,7 @@ import {
} from "./form-state"; } from "./form-state";
import { UserParams, UserTab, toUser } from "./routes/User"; import { UserParams, UserTab, toUser } from "./routes/User";
import { toUsers } from "./routes/Users"; import { toUsers } from "./routes/Users";
import { isLightweightUser } from "./utils";
import "./user-section.css"; import "./user-section.css";
@ -63,6 +67,7 @@ export default function EditUser() {
useState<UserProfileMetadata>(); useState<UserProfileMetadata>();
const [refreshCount, setRefreshCount] = useState(0); const [refreshCount, setRefreshCount] = useState(0);
const refresh = () => setRefreshCount((count) => count + 1); const refresh = () => setRefreshCount((count) => count + 1);
const lightweightUser = isLightweightUser(user?.id);
const toTab = (tab: UserTab) => const toTab = (tab: UserTab) =>
toUser({ toUser({
@ -141,7 +146,11 @@ export default function EditUser() {
continueButtonVariant: ButtonVariant.danger, continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => { onConfirm: async () => {
try { 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); addAlert(t("userDeletedSuccess"), AlertVariant.success);
navigate(toUsers({ realm: realmName })); navigate(toUsers({ realm: realmName }));
} catch (error) { } catch (error) {
@ -183,6 +192,24 @@ export default function EditUser() {
titleKey={user.username!} titleKey={user.username!}
className="kc-username-view-header" className="kc-username-view-header"
divider={false} divider={false}
badges={
lightweightUser
? [
{
text: (
<Tooltip content={t("transientUserTooltip")}>
<Label
data-testid="user-details-label-transient-user"
icon={<InfoCircleIcon />}
>
{t("transientUser")}
</Label>
</Tooltip>
),
},
]
: []
}
dropdownItems={[ dropdownItems={[
<DropdownItem <DropdownItem
key="impersonate" key="impersonate"

View file

@ -25,3 +25,6 @@ export const fieldName = (attribute: UserProfileAttributeMetadata) =>
isRootAttribute(attribute.name) isRootAttribute(attribute.name)
? attribute.name ? attribute.name
: `attributes.${attribute.name}`; : `attributes.${attribute.name}`;
export const isLightweightUser = (userId?: string) =>
userId?.startsWith("lightweight-");

View file

@ -40,7 +40,7 @@ async function startServer() {
[ [
"start-dev", "start-dev",
"--http-port=8180", "--http-port=8180",
"--features=account3,admin-fine-grained-authz,declarative-user-profile", "--features=account3,admin-fine-grained-authz,declarative-user-profile,transient-users",
...keycloakArgs, ...keycloakArgs,
], ],
{ {