Add scope tab to client scope detail page (#514)
* initial version of the scope tab * fixed assign * moved form logic added test * added unassign * fixed merge error * fixed labels
This commit is contained in:
parent
3332bd1a01
commit
b86db32ba8
16 changed files with 1071 additions and 435 deletions
|
@ -4,6 +4,7 @@ import ListingPage from "../support/pages/admin_console/ListingPage";
|
|||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||
import CreateClientScopePage from "../support/pages/admin_console/manage/client_scopes/CreateClientScopePage";
|
||||
import { keycloakBefore } from "../support/util/keycloak_before";
|
||||
import RoleMappingTab from "../support/pages/admin_console/manage/RoleMappingTab";
|
||||
|
||||
let itemId = "client_scope_crud";
|
||||
const loginPage = new LoginPage();
|
||||
|
@ -57,4 +58,23 @@ describe("Client Scopes test", function () {
|
|||
.itemExist(itemId, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scope test", () => {
|
||||
const scopeTab = new RoleMappingTab();
|
||||
const scopeName = "address";
|
||||
|
||||
beforeEach(() => {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToClientScopes();
|
||||
});
|
||||
|
||||
it("assignRole", () => {
|
||||
const role = "offline_access";
|
||||
listingPage.searchItem(scopeName, false).goToItemDetails(scopeName);
|
||||
scopeTab.goToScopeTab().clickAssignRole().selectRow(role).clickAssign();
|
||||
masthead.checkNotificationMessage("Role mapping updated");
|
||||
scopeTab.checkRoles([role]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedT
|
|||
import AdminClient from "../support/util/AdminClient";
|
||||
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
|
||||
import { keycloakBefore } from "../support/util/keycloak_before";
|
||||
import ServiceAccountTab from "../support/pages/admin_console/manage/clients/ServiceAccountTab";
|
||||
import RoleMappingTab from "../support/pages/admin_console/manage/RoleMappingTab";
|
||||
|
||||
let itemId = "client_crud";
|
||||
const loginPage = new LoginPage();
|
||||
|
@ -165,7 +165,7 @@ describe("Clients test", function () {
|
|||
});
|
||||
|
||||
describe("Service account tab test", () => {
|
||||
const serviceAccountTab = new ServiceAccountTab();
|
||||
const serviceAccountTab = new RoleMappingTab();
|
||||
const serviceAccountName = "service-account-client";
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -194,7 +194,7 @@ describe("Clients test", function () {
|
|||
.searchItem(serviceAccountName)
|
||||
.goToItemDetails(serviceAccountName);
|
||||
serviceAccountTab
|
||||
.goToTab()
|
||||
.goToServiceAccountTab()
|
||||
.checkRoles(["manage-account", "offline_access", "uma_authorization"]);
|
||||
});
|
||||
});
|
||||
|
|
51
cypress/support/pages/admin_console/manage/RoleMappingTab.ts
Normal file
51
cypress/support/pages/admin_console/manage/RoleMappingTab.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
const expect = chai.expect;
|
||||
export default class RoleMappingTab {
|
||||
private tab = "#pf-tab-serviceAccount-serviceAccount";
|
||||
private scopeTab = "scopeTab";
|
||||
private assignRole = "assignRole";
|
||||
private assign = "assign";
|
||||
private assignedRolesTable = "assigned-roles";
|
||||
private namesColumn = 'td[data-label="Name"]:visible';
|
||||
|
||||
goToServiceAccountTab() {
|
||||
cy.get(this.tab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
goToScopeTab() {
|
||||
cy.getId(this.scopeTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickAssignRole() {
|
||||
cy.getId(this.assignRole).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickAssign() {
|
||||
cy.getId(this.assign).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
selectRow(name: string) {
|
||||
cy.get(this.namesColumn)
|
||||
.contains(name)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get("input").click();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
checkRoles(roleNames: string[]) {
|
||||
cy.getId(this.assignedRolesTable)
|
||||
.get(this.namesColumn)
|
||||
.should((roles) => {
|
||||
for (let index = 0; index < roleNames.length; index++) {
|
||||
const roleName = roleNames[index];
|
||||
expect(roles).to.contain(roleName);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
const expect = chai.expect;
|
||||
export default class ServiceAccountTab {
|
||||
private tab = "#pf-tab-serviceAccount-serviceAccount";
|
||||
private assignedRolesTable = "assigned-roles";
|
||||
private namesColumn = 'td[data-label="Name"]:visible';
|
||||
|
||||
goToTab() {
|
||||
cy.get(this.tab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkRoles(roleNames: string[]) {
|
||||
cy.getId(this.assignedRolesTable)
|
||||
.get(this.namesColumn)
|
||||
.should((roles) => {
|
||||
for (let index = 0; index < roleNames.length; index++) {
|
||||
const roleName = roleNames[index];
|
||||
expect(roles).to.contain(roleName);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
"@patternfly/react-table": "4.24.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"i18next": "^19.6.2",
|
||||
"keycloak-admin": "1.14.10",
|
||||
"keycloak-admin": "1.14.11",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.1",
|
||||
"react": "^16.8.5",
|
||||
|
|
255
src/client-scopes/details/ScopeForm.tsx
Normal file
255
src/client-scopes/details/ScopeForm.tsx
Normal file
|
@ -0,0 +1,255 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import {
|
||||
Form,
|
||||
FormGroup,
|
||||
ValidatedOptions,
|
||||
TextInput,
|
||||
Select,
|
||||
SelectVariant,
|
||||
SelectOption,
|
||||
Switch,
|
||||
ActionGroup,
|
||||
Button,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
|
||||
import { convertToFormValues } from "../../util";
|
||||
|
||||
type ScopeFormProps = {
|
||||
clientScope: ClientScopeRepresentation;
|
||||
save: (clientScope: ClientScopeRepresentation) => void;
|
||||
};
|
||||
|
||||
export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const { register, control, handleSubmit, errors, setValue } = useForm();
|
||||
const history = useHistory();
|
||||
const providers = useLoginProviders();
|
||||
const [open, isOpen] = useState(false);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
Object.entries(clientScope).map((entry) => {
|
||||
if (entry[0] === "attributes") {
|
||||
convertToFormValues(entry[1], "attributes", setValue);
|
||||
}
|
||||
setValue(entry[0], entry[1]);
|
||||
});
|
||||
}, [clientScope]);
|
||||
|
||||
return (
|
||||
<Form isHorizontal onSubmit={handleSubmit(save)} className="pf-u-mt-md">
|
||||
<FormGroup
|
||||
label={t("common:name")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:name"
|
||||
forLabel={t("common:name")}
|
||||
forID="kc-name"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-name"
|
||||
isRequired
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
id="kc-name"
|
||||
name="name"
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("common:description")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:description"
|
||||
forLabel={t("common:description")}
|
||||
forID="kc-description"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-description"
|
||||
validated={
|
||||
errors.description ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:maxLength", { length: 255 })}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({
|
||||
maxLength: 255,
|
||||
})}
|
||||
validated={
|
||||
errors.description
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
type="text"
|
||||
id="kc-description"
|
||||
name="description"
|
||||
/>
|
||||
</FormGroup>
|
||||
{!id && (
|
||||
<FormGroup
|
||||
label={t("protocol")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:protocol"
|
||||
forLabel="protocol"
|
||||
forID="kc-protocol"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-protocol"
|
||||
>
|
||||
<Controller
|
||||
name="protocol"
|
||||
defaultValue={providers[0]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="kc-protocol"
|
||||
required
|
||||
onToggle={() => isOpen(!open)}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value as string);
|
||||
isOpen(false);
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("selectEncryptionType")}
|
||||
isOpen={open}
|
||||
>
|
||||
{providers.map((option) => (
|
||||
<SelectOption
|
||||
selected={option === value}
|
||||
key={option}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("displayOnConsentScreen")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:displayOnConsentScreen"
|
||||
forLabel={t("displayOnConsentScreen")}
|
||||
forID="kc-display.on.consent.screen"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-display.on.consent.screen"
|
||||
>
|
||||
<Controller
|
||||
name="attributes.display-on-consent-screen"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-display.on.consent.screen"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("consentScreenText")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:consentScreenText"
|
||||
forLabel={t("consentScreenText")}
|
||||
forID="kc-consent-screen-text"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-consent-screen-text"
|
||||
>
|
||||
<TextInput
|
||||
ref={register}
|
||||
type="text"
|
||||
id="kc-consent-screen-text"
|
||||
name="attributes.consent-screen-text"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("includeInTokenScope")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:includeInTokenScope"
|
||||
forLabel={t("includeInTokenScope")}
|
||||
forID="includeInTokenScope"
|
||||
/>
|
||||
}
|
||||
fieldId="includeInTokenScope"
|
||||
>
|
||||
<Controller
|
||||
name="attributes.include-in-token-scope"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="includeInTokenScope"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("guiOrder")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:guiOrder"
|
||||
forLabel={t("guiOrder")}
|
||||
forID="kc-gui-order"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-gui-order"
|
||||
helperTextInvalid={t("shouldBeANumber")}
|
||||
validated={
|
||||
errors.attributes && errors.attributes["gui_order"]
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ pattern: /^([0-9]*)$/ })}
|
||||
type="text"
|
||||
id="kc-gui-order"
|
||||
name="attributes.gui-order"
|
||||
validated={
|
||||
errors.attributes && errors.attributes["gui_order"]
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button variant="primary" type="submit">
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={() => history.push("/client-scopes/")}>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
);
|
||||
};
|
|
@ -1,52 +1,38 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Switch,
|
||||
Spinner,
|
||||
Tab,
|
||||
TabTitleText,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||
import {
|
||||
useAdminClient,
|
||||
asyncStateFetch,
|
||||
} from "../../context/auth/AdminClient";
|
||||
import { KeycloakTabs } from "../../components/keycloak-tabs/KeycloakTabs";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
|
||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
||||
import { convertFormValuesToObject } from "../../util";
|
||||
import { MapperList } from "../details/MapperList";
|
||||
import { ScopeForm } from "../details/ScopeForm";
|
||||
import { RoleMapping, Row } from "../../components/role-mapping/RoleMapping";
|
||||
import { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
|
||||
export const ClientScopeForm = () => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const { register, control, handleSubmit, errors, setValue } = useForm<
|
||||
ClientScopeRepresentation
|
||||
>();
|
||||
const history = useHistory();
|
||||
const [clientScope, setClientScope] = useState<ClientScopeRepresentation>();
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const handleError = useErrorHandler();
|
||||
const providers = useLoginProviders();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const [open, isOpen] = useState(false);
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
|
@ -56,23 +42,53 @@ export const ClientScopeForm = () => {
|
|||
return asyncStateFetch(
|
||||
async () => {
|
||||
if (id) {
|
||||
const data = await adminClient.clientScopes.findOne({ id });
|
||||
if (data) {
|
||||
Object.entries(data).map((entry) => {
|
||||
if (entry[0] === "attributes") {
|
||||
convertToFormValues(entry[1], "attributes", setValue);
|
||||
}
|
||||
setValue(entry[0], entry[1]);
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
return await adminClient.clientScopes.findOne({ id });
|
||||
}
|
||||
},
|
||||
(data) => setClientScope(data),
|
||||
(clientScope) => {
|
||||
setClientScope(clientScope);
|
||||
},
|
||||
handleError
|
||||
);
|
||||
}, [key]);
|
||||
}, [key, id]);
|
||||
|
||||
const loader = async () => {
|
||||
const assignedRoles = hide
|
||||
? await adminClient.clientScopes.listRealmScopeMappings({ id })
|
||||
: await adminClient.clientScopes.listCompositeRealmScopeMappings({ id });
|
||||
const clients = await adminClient.clients.find();
|
||||
|
||||
const clientRoles = (
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const clientScope = hide
|
||||
? await adminClient.clientScopes.listClientScopeMappings({
|
||||
id,
|
||||
client: client.id!,
|
||||
})
|
||||
: await adminClient.clientScopes.listCompositeClientScopeMappings({
|
||||
id,
|
||||
client: client.id!,
|
||||
});
|
||||
return clientScope.map((scope) => {
|
||||
return {
|
||||
client,
|
||||
role: scope,
|
||||
};
|
||||
});
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
|
||||
return [
|
||||
...assignedRoles.map((role) => {
|
||||
return {
|
||||
role,
|
||||
};
|
||||
}),
|
||||
...clientRoles,
|
||||
];
|
||||
};
|
||||
|
||||
const save = async (clientScopes: ClientScopeRepresentation) => {
|
||||
try {
|
||||
|
@ -94,6 +110,50 @@ export const ClientScopeForm = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const assignRoles = async (rows: Row[]) => {
|
||||
try {
|
||||
const realmRoles = rows
|
||||
.filter((row) => row.client === undefined)
|
||||
.map((row) => row.role as RoleMappingPayload)
|
||||
.flat();
|
||||
await adminClient.clientScopes.addRealmScopeMappings(
|
||||
{
|
||||
id,
|
||||
},
|
||||
realmRoles
|
||||
);
|
||||
await Promise.all(
|
||||
rows
|
||||
.filter((row) => row.client !== undefined)
|
||||
.map((row) =>
|
||||
adminClient.clientScopes.addClientScopeMappings(
|
||||
{
|
||||
id,
|
||||
client: row.client!.id!,
|
||||
},
|
||||
[row.role as RoleMappingPayload]
|
||||
)
|
||||
)
|
||||
);
|
||||
addAlert(t("roleMappingUpdatedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("roleMappingUpdatedError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (id && !clientScope) {
|
||||
return (
|
||||
<div className="pf-u-text-align-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader
|
||||
|
@ -102,246 +162,47 @@ export const ClientScopeForm = () => {
|
|||
}
|
||||
subKey="client-scopes:clientScopeExplain"
|
||||
badge={clientScope ? clientScope.protocol : undefined}
|
||||
divider={!id}
|
||||
/>
|
||||
|
||||
<PageSection variant="light">
|
||||
<KeycloakTabs isBox>
|
||||
<Tab
|
||||
eventKey="settings"
|
||||
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
||||
>
|
||||
<Form
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
className="pf-u-mt-md"
|
||||
<PageSection variant="light" className="pf-u-p-0">
|
||||
{!id && (
|
||||
<PageSection variant="light">
|
||||
<ScopeForm save={save} clientScope={{}} />
|
||||
</PageSection>
|
||||
)}
|
||||
{id && clientScope && (
|
||||
<KeycloakTabs isBox>
|
||||
<Tab
|
||||
eventKey="settings"
|
||||
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
||||
>
|
||||
<PageSection variant="light">
|
||||
<ScopeForm save={save} clientScope={clientScope} />
|
||||
</PageSection>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="mappers"
|
||||
title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
|
||||
>
|
||||
<FormGroup
|
||||
label={t("common:name")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:name"
|
||||
forLabel={t("common:name")}
|
||||
forID="kc-name"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-name"
|
||||
isRequired
|
||||
validated={
|
||||
errors.name
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ required: true })}
|
||||
type="text"
|
||||
id="kc-name"
|
||||
name="name"
|
||||
validated={
|
||||
errors.name
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("common:description")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:description"
|
||||
forLabel={t("common:description")}
|
||||
forID="kc-description"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-description"
|
||||
validated={
|
||||
errors.description
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
helperTextInvalid={t("common:maxLength", { length: 255 })}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({
|
||||
maxLength: 255,
|
||||
})}
|
||||
validated={
|
||||
errors.description
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
type="text"
|
||||
id="kc-description"
|
||||
name="description"
|
||||
/>
|
||||
</FormGroup>
|
||||
{!id && (
|
||||
<FormGroup
|
||||
label={t("protocol")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:protocol"
|
||||
forLabel="protocol"
|
||||
forID="kc-protocol"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-protocol"
|
||||
>
|
||||
<Controller
|
||||
name="protocol"
|
||||
defaultValue={providers[0]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="kc-protocol"
|
||||
required
|
||||
onToggle={() => isOpen(!open)}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value as string);
|
||||
isOpen(false);
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("selectEncryptionType")}
|
||||
isOpen={open}
|
||||
>
|
||||
{providers.map((option) => (
|
||||
<SelectOption
|
||||
selected={option === value}
|
||||
key={option}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("displayOnConsentScreen")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:displayOnConsentScreen"
|
||||
forLabel={t("displayOnConsentScreen")}
|
||||
forID="kc-display.on.consent.screen"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-display.on.consent.screen"
|
||||
>
|
||||
<Controller
|
||||
name="attributes.display-on-consent-screen"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-display.on.consent.screen"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("consentScreenText")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:consentScreenText"
|
||||
forLabel={t("consentScreenText")}
|
||||
forID="kc-consent-screen-text"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-consent-screen-text"
|
||||
>
|
||||
<TextInput
|
||||
ref={register}
|
||||
type="text"
|
||||
id="kc-consent-screen-text"
|
||||
name="attributes.consent-screen-text"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
hasNoPaddingTop
|
||||
label={t("includeInTokenScope")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:includeInTokenScope"
|
||||
forLabel={t("includeInTokenScope")}
|
||||
forID="includeInTokenScope"
|
||||
/>
|
||||
}
|
||||
fieldId="includeInTokenScope"
|
||||
>
|
||||
<Controller
|
||||
name="attributes.include-in-token-scope"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="includeInTokenScope"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("guiOrder")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="client-scopes-help:guiOrder"
|
||||
forLabel={t("guiOrder")}
|
||||
forID="kc-gui-order"
|
||||
/>
|
||||
}
|
||||
fieldId="kc-gui-order"
|
||||
helperTextInvalid={t("shouldBeANumber")}
|
||||
validated={
|
||||
errors.attributes && errors.attributes["gui_order"]
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={register({ pattern: /^([0-9]*)$/ })}
|
||||
type="text"
|
||||
id="kc-gui-order"
|
||||
name="attributes.gui-order"
|
||||
validated={
|
||||
errors.attributes && errors.attributes["gui_order"]
|
||||
? ValidatedOptions.error
|
||||
: ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button variant="primary" type="submit">
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => history.push("/client-scopes/")}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</Form>
|
||||
</Tab>
|
||||
<Tab
|
||||
isHidden={!id}
|
||||
eventKey="mappers"
|
||||
title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
|
||||
>
|
||||
{clientScope && (
|
||||
<MapperList clientScope={clientScope} refresh={refresh} />
|
||||
)}
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
</Tab>
|
||||
<Tab
|
||||
data-testid="scopeTab"
|
||||
eventKey="scope"
|
||||
title={<TabTitleText>{t("scope")}</TabTitleText>}
|
||||
>
|
||||
<RoleMapping
|
||||
id={id}
|
||||
name={clientScope.name!}
|
||||
type={"client-scope"}
|
||||
loader={loader}
|
||||
save={assignRoles}
|
||||
onHideRolesToggle={() => setHide(!hide)}
|
||||
/>
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -54,6 +54,9 @@
|
|||
"predefinedMappingDescription": "Choose one of the predefined mappings from this table",
|
||||
"mappingTable": "Table with predefined mapping",
|
||||
"roleGroup": "Use a realm role from:",
|
||||
"clientGroup": "Use a client role from:"
|
||||
"clientGroup": "Use a client role from:",
|
||||
"scope": "Scope",
|
||||
"roleMappingUpdatedSuccess": "Role mapping updated",
|
||||
"roleMappingUpdatedError": "Could not update role mapping {{error}}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,10 @@
|
|||
"evaluate": "Evaluate",
|
||||
"changeTypeTo": "Change type to",
|
||||
"assignRole": "Assign role",
|
||||
"unAssignRole": "Unassign",
|
||||
"removeMappingTitle": "Remove mapping?",
|
||||
"removeMappingConfirm": "Are you sure you want to remove this mapping?",
|
||||
"removeMappingConfirm_plural": "Are you sure you want to remove {{count}} mappings",
|
||||
"clientScopeSearch": {
|
||||
"client": "Client scope",
|
||||
"assigned": "Assigned type"
|
||||
|
|
|
@ -1,66 +1,32 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertVariant,
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { AlertVariant } from "@patternfly/react-core";
|
||||
|
||||
import RoleRepresentation, {
|
||||
RoleMappingPayload,
|
||||
} from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { emptyFormatter } from "../../util";
|
||||
import { AddServiceAccountModal } from "./AddServiceAccountModal";
|
||||
|
||||
import "./service-account.css";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import {
|
||||
CompositeRole,
|
||||
RoleMapping,
|
||||
Row,
|
||||
} from "../../components/role-mapping/RoleMapping";
|
||||
|
||||
type ServiceAccountProps = {
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
export type Row = {
|
||||
client?: ClientRepresentation;
|
||||
role: CompositeRole | RoleRepresentation;
|
||||
};
|
||||
|
||||
export const ServiceRole = ({ role, client }: Row) => (
|
||||
<>
|
||||
{client && (
|
||||
<Badge
|
||||
key={`${client.id}-${role.id}`}
|
||||
isRead
|
||||
className="keycloak-admin--service-account__client-name"
|
||||
>
|
||||
{client.clientId}
|
||||
</Badge>
|
||||
)}
|
||||
{role.name}
|
||||
</>
|
||||
);
|
||||
|
||||
type CompositeRole = RoleRepresentation & {
|
||||
parent: RoleRepresentation;
|
||||
};
|
||||
|
||||
export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useContext(RealmContext);
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [hide, setHide] = useState(false);
|
||||
const [serviceAccountId, setServiceAccountId] = useState("");
|
||||
const [showAssign, setShowAssign] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const loader = async () => {
|
||||
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
||||
|
@ -75,6 +41,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
});
|
||||
|
||||
const clients = await adminClient.clients.find();
|
||||
setName(clients.find((c) => c.id === clientId)?.clientId!);
|
||||
const clientRoles = (
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
|
@ -152,7 +119,6 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
)
|
||||
);
|
||||
addAlert(t("roleMappingUpdatedSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("roleMappingUpdatedError", {
|
||||
|
@ -163,57 +129,13 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{showAssign && (
|
||||
<AddServiceAccountModal
|
||||
clientId={clientId}
|
||||
serviceAccountId={serviceAccountId}
|
||||
onAssign={assignRoles}
|
||||
onClose={() => setShowAssign(false)}
|
||||
/>
|
||||
)}
|
||||
<KeycloakDataTable
|
||||
data-testid="assigned-roles"
|
||||
key={key}
|
||||
loader={loader}
|
||||
onSelect={() => {}}
|
||||
searchPlaceholderKey="clients:searchByName"
|
||||
ariaLabelKey="clients:clientScopeList"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
label={t("hideInheritedRoles")}
|
||||
id="hideInheritedRoles"
|
||||
isChecked={hide}
|
||||
onChange={setHide}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button onClick={() => setShowAssign(true)}>
|
||||
{t("assignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "role.name",
|
||||
displayKey: t("name"),
|
||||
cellRenderer: ServiceRole,
|
||||
},
|
||||
{
|
||||
name: "role.parent.name",
|
||||
displayKey: t("inherentFrom"),
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "role.description",
|
||||
displayKey: t("description"),
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
<RoleMapping
|
||||
name={name}
|
||||
id={serviceAccountId}
|
||||
type={"service-account"}
|
||||
loader={loader}
|
||||
save={assignRoles}
|
||||
onHideRolesToggle={() => setHide(!hide)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,18 +17,22 @@ import {
|
|||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { KeycloakDataTable } from "../table-toolbar/KeycloakDataTable";
|
||||
import {
|
||||
asyncStateFetch,
|
||||
useAdminClient,
|
||||
} from "../../context/auth/AdminClient";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { FilterIcon } from "@patternfly/react-icons";
|
||||
import { Row, ServiceRole } from "./ServiceAccount";
|
||||
import { Row, ServiceRole } from "./RoleMapping";
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
|
||||
type AddServiceAccountModalProps = {
|
||||
clientId: string;
|
||||
serviceAccountId: string;
|
||||
export type MappingType = "service-account" | "client-scope";
|
||||
|
||||
type AddRoleMappingModalProps = {
|
||||
id: string;
|
||||
type: MappingType;
|
||||
name: string;
|
||||
onAssign: (rows: Row[]) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
@ -41,18 +45,18 @@ const realmRole = {
|
|||
name: "realmRoles",
|
||||
} as ClientRepresentation;
|
||||
|
||||
export const AddServiceAccountModal = ({
|
||||
clientId,
|
||||
serviceAccountId,
|
||||
export const AddRoleMappingModal = ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
onAssign,
|
||||
onClose,
|
||||
}: AddServiceAccountModalProps) => {
|
||||
}: AddRoleMappingModalProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const errorHandler = useErrorHandler();
|
||||
|
||||
const [clients, setClients] = useState<ClientRole[]>([]);
|
||||
const [name, setName] = useState<string>();
|
||||
const [searchToggle, setSearchToggle] = useState(false);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
|
@ -66,16 +70,25 @@ export const AddServiceAccountModal = ({
|
|||
asyncStateFetch(
|
||||
async () => {
|
||||
const clients = await adminClient.clients.find();
|
||||
setName(clients.find((client) => client.id === clientId)?.clientId);
|
||||
return (
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const roles = await adminClient.users.listAvailableClientRoleMappings(
|
||||
{
|
||||
id: serviceAccountId,
|
||||
clientUniqueId: client.id!,
|
||||
}
|
||||
);
|
||||
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!,
|
||||
}
|
||||
);
|
||||
}
|
||||
return {
|
||||
roles,
|
||||
client,
|
||||
|
@ -114,11 +127,18 @@ export const AddServiceAccountModal = ({
|
|||
(client) => client.name !== "realmRoles"
|
||||
);
|
||||
}
|
||||
const realmRoles = (
|
||||
await adminClient.users.listAvailableRealmRoleMappings({
|
||||
id: serviceAccountId,
|
||||
})
|
||||
).map((role) => {
|
||||
|
||||
let availableRoles: RoleRepresentation[] = [];
|
||||
if (type === "service-account") {
|
||||
availableRoles = await adminClient.users.listAvailableRealmRoleMappings({
|
||||
id,
|
||||
});
|
||||
} else if (type === "client-scope") {
|
||||
availableRoles = await adminClient.clientScopes.listAvailableRealmScopeMappings(
|
||||
{ id }
|
||||
);
|
||||
}
|
||||
const realmRoles = availableRoles.map((role) => {
|
||||
return {
|
||||
role,
|
||||
client: undefined,
|
||||
|
@ -132,19 +152,27 @@ export const AddServiceAccountModal = ({
|
|||
|
||||
const roles = (
|
||||
await Promise.all(
|
||||
allClients.map(async (client) =>
|
||||
(
|
||||
await adminClient.users.listAvailableClientRoleMappings({
|
||||
id: serviceAccountId,
|
||||
clientUniqueId: client.id!,
|
||||
})
|
||||
).map((role) => {
|
||||
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! }
|
||||
);
|
||||
}
|
||||
return clientAvailableRoles.map((role) => {
|
||||
return {
|
||||
role,
|
||||
client,
|
||||
};
|
||||
})
|
||||
)
|
||||
});
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
|
||||
|
@ -173,9 +201,7 @@ export const AddServiceAccountModal = ({
|
|||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
title={t("assignRolesTo", {
|
||||
client: name,
|
||||
})}
|
||||
title={t("assignRolesTo", { client: name })}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
actions={[
|
307
src/components/role-mapping/AddRoleMappingModal.tsx.orig
Normal file
307
src/components/role-mapping/AddRoleMappingModal.tsx.orig
Normal file
|
@ -0,0 +1,307 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Divider,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { KeycloakDataTable } from "../table-toolbar/KeycloakDataTable";
|
||||
import {
|
||||
asyncStateFetch,
|
||||
useAdminClient,
|
||||
} from "../../context/auth/AdminClient";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { FilterIcon } from "@patternfly/react-icons";
|
||||
import { Row, ServiceRole } from "./RoleMapping";
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
|
||||
export type MappingType = "service-account" | "client-scope";
|
||||
|
||||
type AddRoleMappingModalProps = {
|
||||
id: string;
|
||||
type: MappingType;
|
||||
name: string;
|
||||
onAssign: (rows: Row[]) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type ClientRole = ClientRepresentation & {
|
||||
numberOfRoles: number;
|
||||
};
|
||||
|
||||
const realmRole = {
|
||||
name: "realmRoles",
|
||||
} as ClientRepresentation;
|
||||
|
||||
export const AddRoleMappingModal = ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
onAssign,
|
||||
onClose,
|
||||
}: AddRoleMappingModalProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const errorHandler = useErrorHandler();
|
||||
|
||||
const [clients, setClients] = useState<ClientRole[]>([]);
|
||||
const [name, setName] = useState<string>();
|
||||
const [searchToggle, setSearchToggle] = useState(false);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [selectedClients, setSelectedClients] = useState<ClientRole[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
asyncStateFetch(
|
||||
async () => {
|
||||
const clients = await adminClient.clients.find();
|
||||
setName(clients.find((client) => client.id === clientId)?.clientId);
|
||||
return (
|
||||
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!,
|
||||
}
|
||||
);
|
||||
}
|
||||
return {
|
||||
roles,
|
||||
client,
|
||||
};
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter((row) => row.roles.length !== 0)
|
||||
.map((row) => {
|
||||
return { ...row.client, numberOfRoles: row.roles.length };
|
||||
});
|
||||
},
|
||||
(clients) => {
|
||||
setClients(clients);
|
||||
},
|
||||
errorHandler
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(refresh, [searchToggle]);
|
||||
|
||||
const removeClient = (client: ClientRole) => {
|
||||
setSelectedClients(selectedClients.filter((item) => item.id !== client.id));
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const realmRolesSelected = _.findIndex(
|
||||
selectedClients,
|
||||
(client) => client.name === "realmRoles"
|
||||
);
|
||||
let selected = selectedClients;
|
||||
if (realmRolesSelected !== -1) {
|
||||
selected = selectedClients.filter(
|
||||
(client) => client.name !== "realmRoles"
|
||||
);
|
||||
}
|
||||
|
||||
let availableRoles: RoleRepresentation[] = [];
|
||||
if (type === "service-account") {
|
||||
availableRoles = await adminClient.users.listAvailableRealmRoleMappings({
|
||||
id,
|
||||
});
|
||||
} else if (type === "client-scope") {
|
||||
availableRoles = await adminClient.clientScopes.listAvailableRealmScopeMappings(
|
||||
{ id }
|
||||
);
|
||||
}
|
||||
const realmRoles = availableRoles.map((role) => {
|
||||
return {
|
||||
role,
|
||||
client: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const allClients =
|
||||
selectedClients.length !== 0
|
||||
? selected
|
||||
: await adminClient.clients.find();
|
||||
|
||||
const roles = (
|
||||
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! }
|
||||
);
|
||||
}
|
||||
return clientAvailableRoles.map((role) => {
|
||||
return {
|
||||
role,
|
||||
client,
|
||||
};
|
||||
});
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
|
||||
return [
|
||||
...(realmRolesSelected !== -1 || selected.length === 0 ? realmRoles : []),
|
||||
...roles,
|
||||
];
|
||||
};
|
||||
|
||||
const createSelectGroup = (clients: ClientRepresentation[]) => [
|
||||
<SelectGroup key="role" label={t("realmRoles")}>
|
||||
<SelectOption key="realmRoles" value={realmRole}>
|
||||
{t("realmRoles")}
|
||||
</SelectOption>
|
||||
</SelectGroup>,
|
||||
<Divider key="divider" />,
|
||||
<SelectGroup key="group" label={t("clients")}>
|
||||
{clients.map((client) => (
|
||||
<SelectOption key={client.id} value={client}>
|
||||
{client.clientId}
|
||||
</SelectOption>
|
||||
))}
|
||||
</SelectGroup>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
<<<<<<< HEAD:src/clients/service-account/AddServiceAccountModal.tsx
|
||||
title={t("assignRolesTo", {
|
||||
client: name,
|
||||
})}
|
||||
=======
|
||||
title={t("assignRolesTo", { client: name })}
|
||||
>>>>>>> 0f6f6ab (fixed assign):src/components/role-mapping/AddRoleMappingModal.tsx
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="assign"
|
||||
key="confirm"
|
||||
isDisabled={selectedRows?.length === 0}
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
onAssign(selectedRows);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("assign")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
key="cancel"
|
||||
variant="link"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
toggleId="role"
|
||||
onToggle={() => setSearchToggle(!searchToggle)}
|
||||
isOpen={searchToggle}
|
||||
variant={SelectVariant.checkbox}
|
||||
hasInlineFilter
|
||||
menuAppendTo="parent"
|
||||
placeholderText={
|
||||
<>
|
||||
<FilterIcon /> {t("filterByOrigin")}
|
||||
</>
|
||||
}
|
||||
isGrouped
|
||||
onFilter={(evt) => {
|
||||
const value = evt?.target.value || "";
|
||||
return createSelectGroup(
|
||||
clients.filter((client) => client.clientId?.includes(value))
|
||||
);
|
||||
}}
|
||||
selections={selectedClients}
|
||||
onClear={() => setSelectedClients([])}
|
||||
onSelect={(_, selection) => {
|
||||
const client = selection as ClientRole;
|
||||
if (selectedClients.includes(client)) {
|
||||
removeClient(client);
|
||||
} else {
|
||||
setSelectedClients([...selectedClients, client]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{createSelectGroup(clients)}
|
||||
</Select>
|
||||
<ToolbarItem variant="chip-group">
|
||||
<ChipGroup>
|
||||
{selectedClients.map((client) => (
|
||||
<Chip
|
||||
key={`chip-${client.id}`}
|
||||
onClick={() => {
|
||||
removeClient(client);
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
{client.clientId || t("realmRoles")}
|
||||
<Badge isRead={true}>{client.numberOfRoles}</Badge>
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</ToolbarItem>
|
||||
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
searchPlaceholderKey="clients:searchByRoleName"
|
||||
canSelectAll={false}
|
||||
loader={loader}
|
||||
ariaLabelKey="clients:roles"
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
cellRenderer: ServiceRole,
|
||||
},
|
||||
{
|
||||
name: "role.description",
|
||||
displayKey: t("description"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
211
src/components/role-mapping/RoleMapping.tsx
Normal file
211
src/components/role-mapping/RoleMapping.tsx
Normal file
|
@ -0,0 +1,211 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertVariant,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Checkbox,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import { AddRoleMappingModal, MappingType } from "./AddRoleMappingModal";
|
||||
import { KeycloakDataTable } from "../table-toolbar/KeycloakDataTable";
|
||||
import { emptyFormatter } from "../../util";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export type Row = {
|
||||
client?: ClientRepresentation;
|
||||
role: CompositeRole | RoleRepresentation;
|
||||
};
|
||||
|
||||
export const ServiceRole = ({ role, client }: Row) => (
|
||||
<>
|
||||
{client && (
|
||||
<Badge
|
||||
key={`${client.id}-${role.id}`}
|
||||
isRead
|
||||
className="keycloak-admin--role-mapping__client-name"
|
||||
>
|
||||
{client.clientId}
|
||||
</Badge>
|
||||
)}
|
||||
{role.name}
|
||||
</>
|
||||
);
|
||||
|
||||
type RoleMappingProps = {
|
||||
name: string;
|
||||
id: string;
|
||||
type: MappingType;
|
||||
loader: () => Promise<Row[]>;
|
||||
save: (rows: Row[]) => Promise<void>;
|
||||
onHideRolesToggle: () => void;
|
||||
};
|
||||
|
||||
export const RoleMapping = ({
|
||||
name,
|
||||
id,
|
||||
type,
|
||||
loader,
|
||||
save,
|
||||
onHideRolesToggle,
|
||||
}: RoleMappingProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [hide, setHide] = useState(false);
|
||||
const [showAssign, setShowAssign] = useState(false);
|
||||
const [selected, setSelected] = useState<Row[]>([]);
|
||||
|
||||
const assignRoles = async (rows: Row[]) => {
|
||||
await save(rows);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:removeMappingTitle",
|
||||
messageKey: t("removeMappingConfirm", { count: selected.length }),
|
||||
continueButtonLabel: "common:remove",
|
||||
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(
|
||||
{
|
||||
id,
|
||||
client: row.client!.id!,
|
||||
},
|
||||
[role]
|
||||
);
|
||||
} else {
|
||||
return adminClient.clientScopes.delRealmScopeMappings(
|
||||
{
|
||||
id,
|
||||
},
|
||||
[role]
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(t("clientScopeRemoveError", { error }), AlertVariant.danger);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAssign && (
|
||||
<AddRoleMappingModal
|
||||
id={id}
|
||||
type={type}
|
||||
name={name}
|
||||
onAssign={assignRoles}
|
||||
onClose={() => setShowAssign(false)}
|
||||
/>
|
||||
)}
|
||||
<DeleteConfirm />
|
||||
<KeycloakDataTable
|
||||
data-testid="assigned-roles"
|
||||
key={key}
|
||||
loader={loader}
|
||||
canSelectAll={hide}
|
||||
onSelect={hide ? (rows) => setSelected(rows) : undefined}
|
||||
searchPlaceholderKey="clients:searchByName"
|
||||
ariaLabelKey="clients:clientScopeList"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
label={t("hideInheritedRoles")}
|
||||
id="hideInheritedRoles"
|
||||
isChecked={hide}
|
||||
onChange={(check) => {
|
||||
setHide(check);
|
||||
onHideRolesToggle();
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="assignRole"
|
||||
onClick={() => setShowAssign(true)}
|
||||
>
|
||||
{t("assignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="unAssignRole"
|
||||
onClick={toggleDeleteDialog}
|
||||
isDisabled={selected.length === 0}
|
||||
>
|
||||
{t("unAssignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "role.name",
|
||||
displayKey: t("name"),
|
||||
cellRenderer: ServiceRole,
|
||||
},
|
||||
{
|
||||
name: "role.parent.name",
|
||||
displayKey: t("inherentFrom"),
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "role.description",
|
||||
displayKey: t("description"),
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
|
||||
.keycloak-admin--service-account__client-name {
|
||||
.keycloak-admin--role-mapping__client-name {
|
||||
margin-right: var(--pf-global--spacer--sm);
|
||||
}
|
|
@ -173,10 +173,9 @@ export function KeycloakDataTable<T>({
|
|||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
return asyncStateFetch(
|
||||
async () => {
|
||||
setLoading(true);
|
||||
|
||||
let data = unPaginatedData || (await loader(first, max, search));
|
||||
|
||||
if (!isPaginated) {
|
||||
|
|
|
@ -13471,10 +13471,10 @@ junk@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
|
||||
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
|
||||
|
||||
keycloak-admin@1.14.10:
|
||||
version "1.14.10"
|
||||
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.10.tgz#e44903826896262b3655303db46795b84a5f9b08"
|
||||
integrity sha512-WhEA+FkcPikN/Oqh7L0puVkPU1cm3bB+15VOoPdESZknQ9poS0Ohz3Rg1flRfmMdqoMgcy+prigUPtHy6gOAUg==
|
||||
keycloak-admin@1.14.11:
|
||||
version "1.14.11"
|
||||
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.11.tgz#71415395eeb014f5a8675c951b23596ba33b6f35"
|
||||
integrity sha512-s0NNLdJ27oAx52pXsvJgm8O/KDb0dbPsnbc+f4uTaz/Gzh6QN6GJPCgAYJEZj/Re+oOm+OVRHTx8bhhlrom5hA==
|
||||
dependencies:
|
||||
axios "^0.21.0"
|
||||
camelize "^1.0.0"
|
||||
|
|
Loading…
Reference in a new issue