From 90199db3469d2884fbd40533f861440105ca98be Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Wed, 9 Feb 2022 12:46:58 +0100 Subject: [PATCH] Added default group tab to user registration (#1965) --- .../realm_user_registration.spec.ts | 54 ++++ .../manage/realm_settings/UserRegistration.ts | 68 +++++ .../policies/WebauthnPolicy.tsx | 2 +- .../policies/webauthn-policy.css | 6 - src/clients/scopes/EvaluateScopes.tsx | 3 +- src/clients/scopes/evaluate.css | 4 - src/index.css | 6 + src/realm-settings/DefaultGroupsTab.tsx | 232 ++++++++++++++++++ src/realm-settings/UserRegistration.tsx | 5 +- src/realm-settings/help.ts | 2 + src/realm-settings/messages.ts | 18 ++ 11 files changed, 386 insertions(+), 14 deletions(-) create mode 100644 cypress/integration/realm_user_registration.spec.ts create mode 100644 cypress/support/pages/admin_console/manage/realm_settings/UserRegistration.ts create mode 100644 src/realm-settings/DefaultGroupsTab.tsx diff --git a/cypress/integration/realm_user_registration.spec.ts b/cypress/integration/realm_user_registration.spec.ts new file mode 100644 index 0000000000..36fc498450 --- /dev/null +++ b/cypress/integration/realm_user_registration.spec.ts @@ -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); + }); +}); diff --git a/cypress/support/pages/admin_console/manage/realm_settings/UserRegistration.ts b/cypress/support/pages/admin_console/manage/realm_settings/UserRegistration.ts new file mode 100644 index 0000000000..dec8975ec5 --- /dev/null +++ b/cypress/support/pages/admin_console/manage/realm_settings/UserRegistration.ts @@ -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; + } +} diff --git a/src/authentication/policies/WebauthnPolicy.tsx b/src/authentication/policies/WebauthnPolicy.tsx index e27af513c8..2072f0145d 100644 --- a/src/authentication/policies/WebauthnPolicy.tsx +++ b/src/authentication/policies/WebauthnPolicy.tsx @@ -200,7 +200,7 @@ export const WebauthnPolicy = ({ {enabled && ( - + {t("authentication-help:webauthnIntro")} diff --git a/src/authentication/policies/webauthn-policy.css b/src/authentication/policies/webauthn-policy.css index 363e287107..0251c0016c 100644 --- a/src/authentication/policies/webauthn-policy.css +++ b/src/authentication/policies/webauthn-policy.css @@ -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) { .keycloak__webauthn_policies_authentication__form .pf-c-form__group { --pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 10rem; diff --git a/src/clients/scopes/EvaluateScopes.tsx b/src/clients/scopes/EvaluateScopes.tsx index b3f3ed5c8b..e8b526bdc5 100644 --- a/src/clients/scopes/EvaluateScopes.tsx +++ b/src/clients/scopes/EvaluateScopes.tsx @@ -36,6 +36,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useRealm } from "../../context/realm-context/RealmContext"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; import { prettyPrintJSON } from "../../util"; + import "./evaluate.css"; export type EvaluateScopesProps = { @@ -242,7 +243,7 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => { <> {enabled && ( - + {t("clients-help:evaluateExplain")} diff --git a/src/clients/scopes/evaluate.css b/src/clients/scopes/evaluate.css index a930812bf1..f75bef0fdc 100644 --- a/src/clients/scopes/evaluate.css +++ b/src/clients/scopes/evaluate.css @@ -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 { display: none; } diff --git a/src/index.css b/src/index.css index 0da5ce088e..ebc9e9b1ac 100644 --- a/src/index.css +++ b/src/index.css @@ -59,3 +59,9 @@ td.pf-c-table__check > input[type="checkbox"] { .kc-time-select-dropdown { 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; +} \ No newline at end of file diff --git a/src/realm-settings/DefaultGroupsTab.tsx b/src/realm-settings/DefaultGroupsTab.tsx new file mode 100644 index 0000000000..a113115e03 --- /dev/null +++ b/src/realm-settings/DefaultGroupsTab.tsx @@ -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(); + const [selectedRows, setSelectedRows] = useState([]); + + 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 ; + } + + return ( + <> + + {isGroupPickerOpen && ( + { + addGroups(groups); + toggleGroupPicker(); + }} + onClose={toggleGroupPicker} + /> + )} + {enabled && ( + + {" "} + . + + } + > + + + {t("whatIsDefaultGroups")} + + + + )} + setSelectedRows([...rows])} + loader={loader} + ariaLabelKey="realm-settings:defaultGroups" + searchPlaceholderKey="realm-settings:searchForGroups" + toolbarItem={ + <> + + + + + + } + isOpen={isKebabOpen} + isPlain + dropdownItems={[ + { + toggleRemoveDialog(); + toggleKebab(); + }} + > + {t("common:remove")} + , + ]} + /> + + + } + 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={ + + {" "} + + Add groups... + + } + primaryActionText={t("addGroups")} + onPrimaryAction={toggleGroupPicker} + /> + } + /> + + ); +}; diff --git a/src/realm-settings/UserRegistration.tsx b/src/realm-settings/UserRegistration.tsx index 4afabc41fc..5faec59191 100644 --- a/src/realm-settings/UserRegistration.tsx +++ b/src/realm-settings/UserRegistration.tsx @@ -7,6 +7,7 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { AssociatedRolesTab } from "../realm-roles/AssociatedRolesTab"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; +import { DefaultsGroupsTab } from "./DefaultGroupsTab"; export const UserRegistration = () => { const { t } = useTranslation("realm-settings"); @@ -29,11 +30,11 @@ export const UserRegistration = () => { return ( setActiveTab(key as number)} > {t("defaultRoles")}} @@ -48,7 +49,7 @@ export const UserRegistration = () => { eventKey={20} title={{t("defaultGroups")}} > -

Work in progress

+
); diff --git a/src/realm-settings/help.ts b/src/realm-settings/help.ts index 6d965aced4..44f137984c 100644 --- a/src/realm-settings/help.ts +++ b/src/realm-settings/help.ts @@ -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.", 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'. ", + defaultGroups: + "Default groups allow you to automatically assign groups membership whenever any new user is created or imported through <1>identity brokering.", }, }; diff --git a/src/realm-settings/messages.ts b/src/realm-settings/messages.ts index 9b7fc2797c..87f9ee6a43 100644 --- a/src/realm-settings/messages.ts +++ b/src/realm-settings/messages.ts @@ -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.", defaultRoles: "Default roles", 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. Add default groups to get started", + addGroups: "Add groups", securityDefences: "Security defenses", headers: "Headers", bruteForceDetection: "Brute force detection",