diff --git a/cypress/integration/user_fed_ldap_mapper_test.spec.ts b/cypress/integration/user_fed_ldap_mapper_test.spec.ts index dc83ae6d78..7f214a3587 100644 --- a/cypress/integration/user_fed_ldap_mapper_test.spec.ts +++ b/cypress/integration/user_fed_ldap_mapper_test.spec.ts @@ -3,6 +3,7 @@ import SidebarPage from "../support/pages/admin_console/SidebarPage"; import ListingPage from "../support/pages/admin_console/ListingPage"; import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal"; import ProviderPage from "../support/pages/admin_console/manage/providers/ProviderPage"; +import CreateClientPage from "../support/pages/admin_console/manage/clients/CreateClientPage"; import Masthead from "../support/pages/admin_console/Masthead"; import ModalUtils from "../support/util/ModalUtils"; import { keycloakBefore } from "../support/util/keycloak_before"; @@ -12,6 +13,7 @@ const masthead = new Masthead(); const sidebarPage = new SidebarPage(); const listingPage = new ListingPage(); const groupModal = new GroupModal(); +const createClientPage = new CreateClientPage(); const providersPage = new ProviderPage(); const modalUtils = new ModalUtils(); @@ -41,8 +43,15 @@ const providerDeleteSuccess = "The user federation provider has been deleted."; const providerDeleteTitle = "Delete user federation provider?"; const mapperDeletedSuccess = "Mapping successfully deleted"; const mapperDeleteTitle = "Delete mapping?"; - -const groupName = "my-mappers-group"; +const groupDeleteTitle = "Delete group?"; +const groupCreatedSuccess = "Group created"; +const groupDeletedSuccess = "Group deleted"; +const clientCreatedSuccess = "Client created successfully"; +const clientDeletedSuccess = "The client has been deleted"; +const roleCreatedSuccess = "Role created"; +const groupName = "aa-uf-mappers-group"; +const clientName = "aa-uf-mappers-client"; +const roleName = "aa-uf-mappers-role"; // mapperType variables const msadUserAcctMapper = "msad-user-account-control-mapper"; @@ -53,9 +62,10 @@ const certLdapMapper = "certificate-ldap-mapper"; const fullNameLdapMapper = "full-name-ldap-mapper"; const hcLdapGroupMapper = "hardcoded-ldap-group-mapper"; const hcLdapAttMapper = "hardcoded-ldap-attribute-mapper"; -// const groupLdapMapper = "group-ldap-mapper"; -// const roleMapper = "role-ldap-mapper"; -// const hcLdapRoleMapper = "hardcoded-ldap-role-mapper"; + +const groupLdapMapper = "group-ldap-mapper"; +const roleLdapMapper = "role-ldap-mapper"; +const hcLdapRoleMapper = "hardcoded-ldap-role-mapper"; const creationDateMapper = "creation date"; const emailMapper = "email"; @@ -96,9 +106,7 @@ describe("User Fed LDAP mapper tests", () => { firstUuidLdapAtt, firstUserObjClasses ); - providersPage.save(provider); - masthead.checkNotificationMessage(providerCreatedSuccess); sidebarPage.goToUserFederation(); }); @@ -111,7 +119,26 @@ describe("User Fed LDAP mapper tests", () => { .fillGroupForm(groupName) .clickCreate(); - masthead.checkNotificationMessage("Group created"); + masthead.checkNotificationMessage(groupCreatedSuccess); + }); + + // create a new client and then new role for that client + it("Create client and role", () => { + sidebarPage.goToClients(); + listingPage.goToCreateItem(); + createClientPage + .selectClientType("openid-connect") + .fillClientData(clientName) + .continue() + .continue(); + + masthead.checkNotificationMessage(clientCreatedSuccess); + + providersPage.createRole(roleName); + masthead.checkNotificationMessage(roleCreatedSuccess); + + sidebarPage.goToClients(); + listingPage.searchItem(clientName).itemExist(clientName); }); // delete default mappers @@ -156,6 +183,7 @@ describe("User Fed LDAP mapper tests", () => { masthead.checkNotificationMessage(mapperDeletedSuccess); }); + // mapper CRUD tests // create mapper it("Create certificate ldap mapper", () => { providersPage.clickExistingCard(ldapName); @@ -188,7 +216,7 @@ describe("User Fed LDAP mapper tests", () => { masthead.checkNotificationMessage(mapperDeletedSuccess); }); - // create one of every kind of non-group/role mapper (8) + // create one of each mapper type it("Create user account control mapper", () => { providersPage.clickExistingCard(ldapName); providersPage.goToMappers(); @@ -261,17 +289,51 @@ describe("User Fed LDAP mapper tests", () => { listingPage.itemExist(hcLdapAttMapper, true); }); - // *** test cleanup *** - it("Cleanup - delete group", () => { - sidebarPage.goToGroups(); - listingPage.deleteItem(groupName); - modalUtils.confirmModal(); - masthead.checkNotificationMessage("Group deleted"); + it("Create group ldap mapper", () => { + providersPage.clickExistingCard(ldapName); + providersPage.goToMappers(); + providersPage.createNewMapper(groupLdapMapper); + providersPage.save("ldap-mapper"); + masthead.checkNotificationMessage(mapperCreatedSuccess); + listingPage.itemExist(groupLdapMapper, true); + + it("Create hardcoded ldap role mapper", () => { + providersPage.clickExistingCard(ldapName); + providersPage.goToMappers(); + providersPage.createNewMapper(hcLdapRoleMapper); + providersPage.save("ldap-mapper"); + masthead.checkNotificationMessage(mapperCreatedSuccess); + listingPage.itemExist(hcLdapRoleMapper, true); + }); + + it("Create role ldap mapper", () => { + providersPage.clickExistingCard(ldapName); + providersPage.goToMappers(); + providersPage.createNewMapper(roleLdapMapper); + providersPage.save("ldap-mapper"); + masthead.checkNotificationMessage(mapperCreatedSuccess); + listingPage.itemExist(roleLdapMapper, true); + }); }); + // *** test cleanup *** it("Cleanup - delete LDAP provider", () => { providersPage.deleteCardFromMenu(provider, ldapName); modalUtils.checkModalTitle(providerDeleteTitle).confirmModal(); masthead.checkNotificationMessage(providerDeleteSuccess); }); + + it("Cleanup - delete group", () => { + sidebarPage.goToGroups(); + listingPage.deleteItem(groupName); + modalUtils.checkModalTitle(groupDeleteTitle).confirmModal(); + masthead.checkNotificationMessage(groupDeletedSuccess); + }); + + it("Cleanup - delete client", () => { + sidebarPage.goToClients(); + listingPage.deleteItem(clientName); + modalUtils.checkModalTitle(`Delete ${clientName} ?`).confirmModal(); + masthead.checkNotificationMessage(clientDeletedSuccess); + }); }); diff --git a/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts b/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts index 37d02925ce..62465aef5f 100644 --- a/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts +++ b/cypress/support/pages/admin_console/manage/providers/ProviderPage.ts @@ -44,6 +44,7 @@ export default class ProviderPage { private ldapAttNameInput = "data-testid=mapper-ldapAttributeName-fld"; private ldapAttValueInput = "data-testid=mapper-ldapAttributeValue-fld"; private groupInput = "data-testid=mapper-group-fld"; + private ldapDnInput = "data-testid=ldap-dn"; // mapper types private msadUserAcctMapper = "msad-user-account-control-mapper"; @@ -54,11 +55,28 @@ export default class ProviderPage { private fullNameLdapMapper = "full-name-ldap-mapper"; private hcLdapAttMapper = "hardcoded-ldap-attribute-mapper"; private hcLdapGroupMapper = "hardcoded-ldap-group-mapper"; - // this.groupLdapMapper = "group-ldap-mapper"; - // this.roleMapper = "role-ldap-mapper"; - // this.hcLdapRoleMapper = "hardcoded-ldap-role-mapper"; + private groupLdapMapper = "group-ldap-mapper"; + private roleLdapMapper = "role-ldap-mapper"; + private hcLdapRoleMapper = "hardcoded-ldap-role-mapper"; - private groupName = "my-mappers-group"; + private tab = "#pf-tab-serviceAccount-serviceAccount"; + private scopeTab = "scopeTab"; + private assignRole = "assignRole"; + private unAssign = "unAssignRole"; + private assign = "assign"; + private hide = "#hideInheritedRoles"; + private assignedRolesTable = "assigned-roles"; + private namesColumn = 'td[data-label="Name"]:visible'; + + private rolesTab = "#pf-tab-roles-roles"; + private createRoleBtn = "data-testid=empty-primary-action"; + private realmRolesSaveBtn = "data-testid=realm-roles-save-button"; + private roleNameField = "#kc-name"; + private clientIdSelect = "#kc-client-id"; + + private groupName = "aa-uf-mappers-group"; + private clientName = "aa-uf-mappers-client"; + private roleName = "aa-uf-mappers-role"; changeCacheTime(unit: string, time: string) { switch (unit) { @@ -183,9 +201,21 @@ export default class ProviderPage { cy.get(`[data-testid="ldap-mappers-tab"]`).click(); } + createRole(roleName: string) { + cy.get(this.rolesTab).click(); + cy.wait(1000); + cy.get(`[${this.createRoleBtn}]`).click(); + cy.wait(1000); + cy.get(this.roleNameField).type(roleName); + cy.wait(1000); + cy.get(`[${this.realmRolesSaveBtn}]`).click(); + cy.wait(1000); + } + createNewMapper(mapperType: string) { const userModelAttValue = "firstName"; const ldapAttValue = "cn"; + const ldapDnValue = "ou=groups"; cy.get(`[data-testid="add-mapper-btn"]`).click(); cy.wait(1000); @@ -218,12 +248,29 @@ export default class ProviderPage { case this.hcLdapGroupMapper: cy.get(`[${this.groupInput}]`).type(this.groupName); break; - // case this.groupLdapMapper: - // break; - // case this.roleMapper: - // break; - // case this.hcLdapRoleMapper: - // break; + case this.groupLdapMapper: + cy.get(`[${this.ldapDnInput}]`).type(ldapDnValue); + break; + + case this.roleLdapMapper: + cy.get(`[${this.ldapDnInput}]`).type(ldapDnValue); + // cy select clientID dropdown and choose clientName (var) + cy.get(this.clientIdSelect).click(); + cy.get("button").contains(this.clientName).click({ force: true }); + break; + + case this.hcLdapRoleMapper: + cy.get(`[data-testid="selectRole"]`).click(); + cy.wait(2000); + cy.get(this.namesColumn) + .contains(this.clientName) + .parent() + .parent() + .within(() => { + cy.get('input[name="radioGroup"]').click(); + }); + cy.getId(this.assign).click(); + break; default: console.log("Invalid mapper type."); break; @@ -261,14 +308,6 @@ export default class ProviderPage { cy.get(`[${this.ldapAttValueInput}]`).clear; cy.get(`[${this.ldapAttValueInput}]`).type(ldapAttValue); break; - // case this.hcLdapGroupMapper: - // break; - // case this.groupLdapMapper: - // break; - // case this.roleMapper: - // break; - // case this.hcLdapRoleMapper: - // break; default: console.log("Invalid mapper name."); break; diff --git a/src/components/role-mapping/AddRoleMappingModal.tsx b/src/components/role-mapping/AddRoleMappingModal.tsx index 907e22b544..aa7223f619 100644 --- a/src/components/role-mapping/AddRoleMappingModal.tsx +++ b/src/components/role-mapping/AddRoleMappingModal.tsx @@ -23,12 +23,13 @@ import { FilterIcon } from "@patternfly/react-icons"; import { Row, ServiceRole } from "./RoleMapping"; import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation"; -export type MappingType = "service-account" | "client-scope"; +export type MappingType = "service-account" | "client-scope" | "user-fed"; type AddRoleMappingModalProps = { id: string; type: MappingType; - name: string; + name?: string; + isRadio?: boolean; onAssign: (rows: Row[]) => void; onClose: () => void; }; @@ -45,6 +46,7 @@ export const AddRoleMappingModal = ({ id, name, type, + isRadio = false, onAssign, onClose, }: AddRoleMappingModalProps) => { @@ -67,18 +69,28 @@ export const AddRoleMappingModal = ({ await Promise.all( clients.map(async (client) => { let roles: RoleRepresentation[] = []; - if (type === "service-account") { - roles = await adminClient.users.listAvailableClientRoleMappings({ - id: id, - clientUniqueId: client.id!, - }); - } else if (type === "client-scope") { - roles = await adminClient.clientScopes.listAvailableClientScopeMappings( - { - id, - client: client.id!, - } - ); + + switch (type) { + case "service-account": + roles = await adminClient.users.listAvailableClientRoleMappings( + { + id: id, + clientUniqueId: client.id!, + } + ); + break; + + case "client-scope": + roles = await adminClient.clientScopes.listAvailableClientScopeMappings( + { + id, + client: client.id!, + } + ); + break; + case "user-fed": + roles = await adminClient.roles.find(); + break; } return { roles, @@ -118,15 +130,25 @@ export const AddRoleMappingModal = ({ } let availableRoles: RoleRepresentation[] = []; - if (type === "service-account") { - availableRoles = await adminClient.users.listAvailableRealmRoleMappings({ - id, - }); - } else if (type === "client-scope") { - availableRoles = await adminClient.clientScopes.listAvailableRealmScopeMappings( - { id } - ); + + switch (type) { + case "service-account": + availableRoles = await adminClient.users.listAvailableRealmRoleMappings( + { + id, + } + ); + break; + case "client-scope": + availableRoles = await adminClient.clientScopes.listAvailableRealmScopeMappings( + { id } + ); + break; + case "user-fed": + availableRoles = await adminClient.roles.find(); + break; } + const realmRoles = availableRoles.map((role) => { return { role, @@ -143,18 +165,28 @@ export const AddRoleMappingModal = ({ await Promise.all( allClients.map(async (client) => { let clientAvailableRoles: RoleRepresentation[] = []; - if (type === "service-account") { - clientAvailableRoles = await adminClient.users.listAvailableClientRoleMappings( - { - id, - clientUniqueId: client.id!, - } - ); - } else if (type === "client-scope") { - clientAvailableRoles = await adminClient.clientScopes.listAvailableClientScopeMappings( - { id, client: client.id! } - ); + + switch (type) { + case "service-account": + clientAvailableRoles = await adminClient.users.listAvailableClientRoleMappings( + { + id, + clientUniqueId: client.id!, + } + ); + break; + case "client-scope": + clientAvailableRoles = await adminClient.clientScopes.listAvailableClientScopeMappings( + { id, client: client.id! } + ); + break; + case "user-fed": + clientAvailableRoles = await adminClient.clients.listRoles({ + id: client.id!, + }); + break; } + return clientAvailableRoles.map((role) => { return { role, @@ -220,7 +252,7 @@ export const AddRoleMappingModal = ({ toggleId="role" onToggle={() => setSearchToggle(!searchToggle)} isOpen={searchToggle} - variant={SelectVariant.checkbox} + variant={isRadio ? SelectVariant.single : SelectVariant.checkbox} hasInlineFilter menuAppendTo="parent" placeholderText={ @@ -270,6 +302,7 @@ export const AddRoleMappingModal = ({ onSelect={(rows) => setSelectedRows([...rows])} searchPlaceholderKey="clients:searchByRoleName" canSelectAll={false} + isRadio={isRadio} loader={loader} ariaLabelKey="clients:roles" columns={[ diff --git a/src/components/role-mapping/RoleMapping.tsx b/src/components/role-mapping/RoleMapping.tsx index 3a1c410459..435a32da01 100644 --- a/src/components/role-mapping/RoleMapping.tsx +++ b/src/components/role-mapping/RoleMapping.tsx @@ -19,7 +19,6 @@ import "./role-mapping.css"; import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog"; import { useAdminClient } from "../../context/auth/AdminClient"; import { useAlerts } from "../alert/Alerts"; -import _ from "lodash"; export type CompositeRole = RoleRepresentation & { parent: RoleRepresentation; @@ -85,46 +84,49 @@ export const RoleMapping = ({ continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { - if (type === "service-account") { - await Promise.all( - selected.map((row) => { - const role = { id: row.role.id!, name: row.role.name! }; - if (row.client) { - return adminClient.users.delClientRoleMappings({ - id, - clientUniqueId: row.client!.id!, - roles: [role], - }); - } else { - return adminClient.users.delRealmRoleMappings({ - id, - roles: [role], - }); - } - }) - ); - } else if (type === "client-scope") { - await Promise.all( - selected.map((row) => { - const role = { id: row.role.id!, name: row.role.name! }; - if (row.client) { - return adminClient.clientScopes.delClientScopeMappings( - { + switch (type) { + case "service-account": + await Promise.all( + selected.map((row) => { + const role = { id: row.role.id!, name: row.role.name! }; + if (row.client) { + return adminClient.users.delClientRoleMappings({ id, - client: row.client!.id!, - }, - [role] - ); - } else { - return adminClient.clientScopes.delRealmScopeMappings( - { + clientUniqueId: row.client!.id!, + roles: [role], + }); + } else { + return adminClient.users.delRealmRoleMappings({ id, - }, - [role] - ); - } - }) - ); + roles: [role], + }); + } + }) + ); + break; + case "client-scope": + await Promise.all( + selected.map((row) => { + const role = { id: row.role.id!, name: row.role.name! }; + if (row.client) { + return adminClient.clientScopes.delClientScopeMappings( + { + id, + client: row.client!.id!, + }, + [role] + ); + } else { + return adminClient.clientScopes.delRealmScopeMappings( + { + id, + }, + [role] + ); + } + }) + ); + break; } addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success); refresh(); diff --git a/src/components/table-toolbar/KeycloakDataTable.tsx b/src/components/table-toolbar/KeycloakDataTable.tsx index b41af329a2..a447d2416e 100644 --- a/src/components/table-toolbar/KeycloakDataTable.tsx +++ b/src/components/table-toolbar/KeycloakDataTable.tsx @@ -54,6 +54,7 @@ type DataTableProps = { onCollapse?: (isOpen: boolean, rowIndex: number) => void; canSelectAll: boolean; isNotCompact?: boolean; + isRadio?: boolean; }; function DataTable({ @@ -66,6 +67,7 @@ function DataTable({ onCollapse, canSelectAll, isNotCompact, + isRadio, ...props }: DataTableProps) { const { t } = useTranslation(); @@ -83,6 +85,7 @@ function DataTable({ ? (_, rowIndex, isOpen) => onCollapse(isOpen, rowIndex) : undefined } + selectVariant={isRadio ? "radio" : "checkbox"} canSelectAll={canSelectAll} cells={columns.map((column) => { return { ...column, title: t(column.displayKey || column.name) }; @@ -133,6 +136,7 @@ export type DataListProps = { emptyState?: ReactNode; icon?: React.ComponentClass; isNotCompact?: boolean; + isRadio?: boolean; }; /** @@ -165,6 +169,7 @@ export function KeycloakDataTable({ onSelect, canSelectAll = false, isNotCompact, + isRadio, detailColumns, isRowDisabled, loader, @@ -394,6 +399,7 @@ export function KeycloakDataTable({ rows={filteredData || rows} columns={columns} isNotCompact={isNotCompact} + isRadio={isRadio} ariaLabelKey={ariaLabelKey} /> )} diff --git a/src/user-federation/ldap/mappers/LdapMapperDetails.tsx b/src/user-federation/ldap/mappers/LdapMapperDetails.tsx index 0d256e111d..fcc0721a15 100644 --- a/src/user-federation/ldap/mappers/LdapMapperDetails.tsx +++ b/src/user-federation/ldap/mappers/LdapMapperDetails.tsx @@ -10,6 +10,7 @@ import { SelectOption, SelectVariant, TextInput, + ValidatedOptions, } from "@patternfly/react-core"; import { convertFormValuesToObject, convertToFormValues } from "../../../util"; import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; @@ -158,7 +159,12 @@ export const LdapMapperDetails = () => { id="kc-ldap-mapper-name" data-testid="ldap-mapper-name" name="name" - ref={form.register} + ref={form.register({ required: true })} + validated={ + form.errors.name + ? ValidatedOptions.error + : ValidatedOptions.default + } />