Added default group tab to user registration (#1965)

This commit is contained in:
Erik Jan de Wit 2022-02-09 12:46:58 +01:00 committed by GitHub
parent f9d3f9dfa7
commit 90199db346
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 386 additions and 14 deletions

View file

@ -0,0 +1,54 @@
import ListingPage from "../support/pages/admin_console/ListingPage";
import UserRegistration, {
GroupPickerDialog,
} from "../support/pages/admin_console/manage/realm_settings/UserRegistration";
import Masthead from "../support/pages/admin_console/Masthead";
import SidebarPage from "../support/pages/admin_console/SidebarPage";
import LoginPage from "../support/pages/LoginPage";
import AdminClient from "../support/util/AdminClient";
import {
keycloakBefore,
keycloakBeforeEach,
} from "../support/util/keycloak_hooks";
describe("Realm settings - User registration tab", () => {
const loginPage = new LoginPage();
const sidebarPage = new SidebarPage();
const masthead = new Masthead();
const adminClient = new AdminClient();
const listingPage = new ListingPage();
const groupPicker = new GroupPickerDialog();
const userRegistration = new UserRegistration();
const groupName = "The default group";
before(() => {
adminClient.createGroup(groupName);
keycloakBefore();
loginPage.logIn();
});
beforeEach(() => {
keycloakBeforeEach();
sidebarPage.goToRealmSettings();
userRegistration.goToTab();
});
after(() => adminClient.deleteGroups());
it("add default role", () => {
const role = "admin";
userRegistration.addRoleButtonClick();
userRegistration.selectRow(role).assign();
masthead.checkNotificationMessage("Associated roles have been added");
listingPage.searchItem(role, false).itemExist(role);
});
it("add default role", () => {
userRegistration.goToDefaultGroupTab().addDefaultGroupClick();
groupPicker.checkTitle("Add default groups").clickRow(groupName).clickAdd();
masthead.checkNotificationMessage("New group added to the default groups");
listingPage.itemExist(groupName);
});
});

View file

@ -0,0 +1,68 @@
export default class UserRegistration {
private userRegistrationTab = "rs-userRegistration-tab";
private defaultGroupTab = "#pf-tab-20-groups";
private addRoleButton = "add-role-button";
private addDefaultGroup = "no-default-groups-empty-action";
private namesColumn = 'td[data-label="Role name"]:visible';
private addBtn = "add-associated-roles-button";
goToTab() {
cy.findByTestId(this.userRegistrationTab).click();
return this;
}
goToDefaultGroupTab() {
cy.get(this.defaultGroupTab).click();
return this;
}
addRoleButtonClick() {
cy.findByTestId(this.addRoleButton).click();
return this;
}
addDefaultGroupClick() {
cy.findByTestId(this.addDefaultGroup).click();
return this;
}
selectRow(name: string) {
cy.get(this.namesColumn)
.contains(name)
.parent()
.within(() => {
cy.get("input").click();
});
return this;
}
assign() {
cy.findByTestId(this.addBtn).click();
return this;
}
}
export class GroupPickerDialog {
private addButton = "common:add-button";
private title = ".pf-c-modal-box__title";
clickRow(groupName: string) {
cy.findByTestId(groupName).within(() => cy.get("input").click());
return this;
}
clickRoot() {
cy.get(".pf-c-breadcrumb__item > button").click();
return this;
}
checkTitle(title: string) {
cy.get(this.title).should("have.text", title);
return this;
}
clickAdd() {
cy.findByTestId(this.addButton).click();
return this;
}
}

View file

@ -200,7 +200,7 @@ export const WebauthnPolicy = ({
<PageSection variant="light"> <PageSection variant="light">
{enabled && ( {enabled && (
<Popover bodyContent={t(`authentication-help:${namePrefix}FormHelp`)}> <Popover bodyContent={t(`authentication-help:${namePrefix}FormHelp`)}>
<TextContent className="keycloak__webauthn_policies__intro"> <TextContent className="keycloak__section_intro__help">
<Text> <Text>
<QuestionCircleIcon /> {t("authentication-help:webauthnIntro")} <QuestionCircleIcon /> {t("authentication-help:webauthnIntro")}
</Text> </Text>

View file

@ -1,9 +1,3 @@
.keycloak__webauthn_policies__intro {
padding: var(--pf-global--spacer--md) 0 var(--pf-global--spacer--lg);
color: var(--pf-global--primary-color--100);
width: fit-content;
}
@media (min-width: 768px) { @media (min-width: 768px) {
.keycloak__webauthn_policies_authentication__form .pf-c-form__group { .keycloak__webauthn_policies_authentication__form .pf-c-form__group {
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 10rem; --pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 10rem;

View file

@ -36,6 +36,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { prettyPrintJSON } from "../../util"; import { prettyPrintJSON } from "../../util";
import "./evaluate.css"; import "./evaluate.css";
export type EvaluateScopesProps = { export type EvaluateScopesProps = {
@ -242,7 +243,7 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
<> <>
<PageSection variant="light"> <PageSection variant="light">
{enabled && ( {enabled && (
<TextContent className="keycloak__scopes_evaluate__intro"> <TextContent className="keycloak__section_intro__help">
<Text> <Text>
<QuestionCircleIcon /> {t("clients-help:evaluateExplain")} <QuestionCircleIcon /> {t("clients-help:evaluateExplain")}
</Text> </Text>

View file

@ -1,7 +1,3 @@
.keycloak__scopes_evaluate__intro {
padding: var(--pf-global--spacer--md) 0 var(--pf-global--spacer--lg);
color: var(--pf-global--primary-color--100);
}
.keycloak__scopes_evaluate__clipboard-copy input { .keycloak__scopes_evaluate__clipboard-copy input {
display: none; display: none;
} }

View file

@ -59,3 +59,9 @@ td.pf-c-table__check > input[type="checkbox"] {
.kc-time-select-dropdown { .kc-time-select-dropdown {
min-width: 170px; min-width: 170px;
} }
.keycloak__section_intro__help {
padding: var(--pf-global--spacer--md) 0 var(--pf-global--spacer--lg);
color: var(--pf-global--primary-color--100);
width: fit-content;
}

View file

@ -0,0 +1,232 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import {
AlertVariant,
Button,
ButtonVariant,
Dropdown,
DropdownItem,
KebabToggle,
Popover,
Text,
TextContent,
ToolbarItem,
} from "@patternfly/react-core";
import { QuestionCircleIcon } from "@patternfly/react-icons";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import useToggle from "../utils/useToggle";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../components/alert/Alerts";
import { toUserFederation } from "../user-federation/routes/UserFederation";
import { useRealm } from "../context/realm-context/RealmContext";
import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { useHelp } from "../components/help-enabler/HelpHeader";
export const DefaultsGroupsTab = () => {
const { t } = useTranslation("realm-settings");
const [isKebabOpen, toggleKebab] = useToggle();
const [isGroupPickerOpen, toggleGroupPicker] = useToggle();
const [defaultGroups, setDefaultGroups] = useState<GroupRepresentation[]>();
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
const [key, setKey] = useState(0);
const [load, setLoad] = useState(0);
const reload = () => setLoad(load + 1);
const adminClient = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const { enabled } = useHelp();
useFetch(
() => adminClient.realms.getDefaultGroups({ realm }),
(groups) => {
setDefaultGroups(groups);
setKey(key + 1);
},
[load]
);
const loader = () => Promise.resolve(defaultGroups!);
const removeGroup = async () => {
try {
await Promise.all(
selectedRows.map((group) =>
adminClient.realms.removeDefaultGroup({
realm,
id: group.id!,
})
)
);
addAlert(
t("groupRemove", { count: selectedRows.length }),
AlertVariant.success
);
setSelectedRows([]);
} catch (error) {
addError("realm-settings:groupRemoveError", error);
}
reload();
};
const addGroups = async (groups: GroupRepresentation[]) => {
try {
await Promise.all(
groups.map((group) =>
adminClient.realms.addDefaultGroup({
realm,
id: group.id!,
})
)
);
addAlert(
t("defaultGroupAdded", { count: groups.length }),
AlertVariant.success
);
} catch (error) {
addError("realm-settings:defaultGroupAddedError", error);
}
reload();
};
const [toggleRemoveDialog, RemoveDialog] = useConfirmDialog({
titleKey: t("removeConfirmTitle", { count: selectedRows.length }),
messageKey: t("removeConfirm", { count: selectedRows.length }),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: removeGroup,
});
if (!defaultGroups) {
return <KeycloakSpinner />;
}
return (
<>
<RemoveDialog />
{isGroupPickerOpen && (
<GroupPickerDialog
type="selectMany"
text={{
title: "realm-settings:addDefaultGroups",
ok: "common:add",
}}
onConfirm={(groups) => {
addGroups(groups);
toggleGroupPicker();
}}
onClose={toggleGroupPicker}
/>
)}
{enabled && (
<Popover
bodyContent={
<Trans i18nKey="realm-settings-help:defaultGroups">
{" "}
<Link to={toUserFederation({ realm })} />.
</Trans>
}
>
<TextContent
className="keycloak__section_intro__help"
style={{
paddingLeft: "var(--pf-c-page__main-section--PaddingLeft)",
}}
>
<Text>
<QuestionCircleIcon /> {t("whatIsDefaultGroups")}
</Text>
</TextContent>
</Popover>
)}
<KeycloakDataTable
key={key}
canSelectAll
onSelect={(rows) => setSelectedRows([...rows])}
loader={loader}
ariaLabelKey="realm-settings:defaultGroups"
searchPlaceholderKey="realm-settings:searchForGroups"
toolbarItem={
<>
<ToolbarItem>
<Button
data-testid="openCreateGroupModal"
variant="primary"
onClick={toggleGroupPicker}
>
{t("addGroups")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Dropdown
toggle={
<KebabToggle
onToggle={toggleKebab}
isDisabled={selectedRows!.length === 0}
/>
}
isOpen={isKebabOpen}
isPlain
dropdownItems={[
<DropdownItem
key="action"
component="button"
onClick={() => {
toggleRemoveDialog();
toggleKebab();
}}
>
{t("common:remove")}
</DropdownItem>,
]}
/>
</ToolbarItem>
</>
}
actions={[
{
title: t("common:remove"),
onRowClick: (group: GroupRepresentation) => {
setSelectedRows([group]);
toggleRemoveDialog();
return Promise.resolve(false);
},
},
]}
columns={[
{
name: "name",
displayKey: "groups:groupName",
},
{
name: "path",
displayKey: "groups:path",
},
]}
emptyState={
<ListEmptyState
hasIcon
message={t("noDefaultGroups")}
instructions={
<Trans i18nKey="realm-settings:noDefaultGroupsInstructions">
{" "}
<Link to={toUserFederation({ realm })} />
Add groups...
</Trans>
}
primaryActionText={t("addGroups")}
onPrimaryAction={toggleGroupPicker}
/>
}
/>
</>
);
};

View file

@ -7,6 +7,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { AssociatedRolesTab } from "../realm-roles/AssociatedRolesTab"; import { AssociatedRolesTab } from "../realm-roles/AssociatedRolesTab";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { DefaultsGroupsTab } from "./DefaultGroupsTab";
export const UserRegistration = () => { export const UserRegistration = () => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
@ -29,11 +30,11 @@ export const UserRegistration = () => {
return ( return (
<Tabs <Tabs
key={key}
activeKey={activeTab} activeKey={activeTab}
onSelect={(_, key) => setActiveTab(key as number)} onSelect={(_, key) => setActiveTab(key as number)}
> >
<Tab <Tab
key={key}
id="roles" id="roles"
eventKey={10} eventKey={10}
title={<TabTitleText>{t("defaultRoles")}</TabTitleText>} title={<TabTitleText>{t("defaultRoles")}</TabTitleText>}
@ -48,7 +49,7 @@ export const UserRegistration = () => {
eventKey={20} eventKey={20}
title={<TabTitleText>{t("defaultGroups")}</TabTitleText>} title={<TabTitleText>{t("defaultGroups")}</TabTitleText>}
> >
<h1>Work in progress</h1> <DefaultsGroupsTab />
</Tab> </Tab>
</Tabs> </Tabs>
); );

View file

@ -143,5 +143,7 @@ export default {
"The condition checks the role of the entity who tries to create/update the client to determine whether the policy is applied.", "The condition checks the role of the entity who tries to create/update the client to determine whether the policy is applied.",
clientUpdaterSourceRolesTooltip: clientUpdaterSourceRolesTooltip:
"The condition is checked during client registration/update requests and it evaluates to true if the entity (usually user), who is creating/updating client is member of the specified role. For reference the realm role, you can use the realm role name like 'my_realm_role' . For reference client role, you can use the client_id.role_name for example 'my_client.my_client_role' will refer to client role 'my_client_role' of client 'my_client'. ", "The condition is checked during client registration/update requests and it evaluates to true if the entity (usually user), who is creating/updating client is member of the specified role. For reference the realm role, you can use the realm role name like 'my_realm_role' . For reference client role, you can use the client_id.role_name for example 'my_client.my_client_role' will refer to client role 'my_client_role' of client 'my_client'. ",
defaultGroups:
"Default groups allow you to automatically assign groups membership whenever any new user is created or imported through <1>identity brokering</1>.",
}, },
}; };

View file

@ -726,6 +726,24 @@ export default {
"You can edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale.", "You can edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale.",
defaultRoles: "Default roles", defaultRoles: "Default roles",
defaultGroups: "Default groups", defaultGroups: "Default groups",
whatIsDefaultGroups: "What is the function of default groups?",
searchForGroups: "Search group",
addDefaultGroups: "Add default groups",
removeConfirmTitle_one: "Remove group?",
removeConfirmTitle_other: "Remove groups?",
removeConfirm_one: "Are you sure you want to remove this group",
removeConfirm_other: "Are you sure you want to remove these groups.",
groupRemove_one: "Group removed",
groupRemove_other: "Groups removed",
groupRemoveError: "Error removing group {error}",
defaultGroupAdded_one: "New group added to the default groups",
defaultGroupAdded_other: "Added {{count}} groups to the default groups",
defaultGroupAddedError:
"Error adding group(s) to the default group {error}",
noDefaultGroups: "No default groups",
noDefaultGroupsInstructions:
"Default groups allow you to automatically assign group membership whenever any new user is created or imported throughout <1>identity brokering</1>. Add default groups to get started",
addGroups: "Add groups",
securityDefences: "Security defenses", securityDefences: "Security defenses",
headers: "Headers", headers: "Headers",
bruteForceDetection: "Brute force detection", bruteForceDetection: "Brute force detection",