Created add service account roles assign screen (#465)
* service account dialog * create test * fixed types * fixed realm roles selection * disable when no rows are selected Co-authored-by: Eugenia <32821331+jenny-s51@users.noreply.github.com> Co-authored-by: Eugenia <32821331+jenny-s51@users.noreply.github.com>
This commit is contained in:
parent
a0faba0f97
commit
84bf7925a6
11 changed files with 814 additions and 380 deletions
|
@ -8,6 +8,7 @@ import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedT
|
||||||
import AdminClient from "../support/util/AdminClient";
|
import AdminClient from "../support/util/AdminClient";
|
||||||
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
|
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
|
||||||
import { keycloakBefore } from "../support/util/keycloak_before";
|
import { keycloakBefore } from "../support/util/keycloak_before";
|
||||||
|
import ServiceAccountTab from "../support/pages/admin_console/manage/clients/ServiceAccountTab";
|
||||||
|
|
||||||
let itemId = "client_crud";
|
let itemId = "client_crud";
|
||||||
const loginPage = new LoginPage();
|
const loginPage = new LoginPage();
|
||||||
|
@ -162,4 +163,39 @@ describe("Clients test", function () {
|
||||||
advancedTab.checkAccessTokenSignatureAlgorithm(algorithm);
|
advancedTab.checkAccessTokenSignatureAlgorithm(algorithm);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Service account tab test", () => {
|
||||||
|
const serviceAccountTab = new ServiceAccountTab();
|
||||||
|
const serviceAccountName = "service-account-client";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
keycloakBefore();
|
||||||
|
loginPage.logIn();
|
||||||
|
sidebarPage.goToClients();
|
||||||
|
});
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await new AdminClient().createClient({
|
||||||
|
protocol: "openid-connect",
|
||||||
|
clientId: serviceAccountName,
|
||||||
|
publicClient: false,
|
||||||
|
authorizationServicesEnabled: true,
|
||||||
|
serviceAccountsEnabled: true,
|
||||||
|
standardFlowEnabled: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
new AdminClient().deleteClient(serviceAccountName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list", () => {
|
||||||
|
listingPage
|
||||||
|
.searchItem(serviceAccountName)
|
||||||
|
.goToItemDetails(serviceAccountName);
|
||||||
|
serviceAccountTab
|
||||||
|
.goToTab()
|
||||||
|
.checkRoles(["manage-account", "offline_access", "uma_authorization"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,6 +64,13 @@ export default class CreateClientPage {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeSwitches(switches: string[]) {
|
||||||
|
for (const uiSwitch of switches) {
|
||||||
|
cy.getId(uiSwitch).check({ force: true });
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
checkClientTypeRequiredMessage(exist = true) {
|
checkClientTypeRequiredMessage(exist = true) {
|
||||||
cy.get(this.clientTypeError).should((!exist ? "not." : "") + "exist");
|
cy.get(this.clientTypeError).should((!exist ? "not." : "") + "exist");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import KeycloakAdminClient from "keycloak-admin";
|
import KeycloakAdminClient from "keycloak-admin";
|
||||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||||
|
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||||
|
|
||||||
export default class AdminClient {
|
export default class AdminClient {
|
||||||
private client: KeycloakAdminClient;
|
private client: KeycloakAdminClient;
|
||||||
|
@ -24,6 +25,10 @@ export default class AdminClient {
|
||||||
await this.client.realms.del({ realm });
|
await this.client.realms.del({ realm });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createClient(client: ClientRepresentation) {
|
||||||
|
await this.login();
|
||||||
|
await this.client.clients.create(client);
|
||||||
|
}
|
||||||
async deleteClient(clientName: string) {
|
async deleteClient(clientName: string) {
|
||||||
await this.login();
|
await this.login();
|
||||||
const client = (
|
const client = (
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ButtonVariant,
|
ButtonVariant,
|
||||||
ExpandableSection,
|
ExpandableSection,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
PageSection,
|
||||||
Split,
|
Split,
|
||||||
SplitItem,
|
SplitItem,
|
||||||
Text,
|
Text,
|
||||||
|
@ -176,257 +177,261 @@ export const AdvancedTab = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollForm sections={sections}>
|
<PageSection variant="light">
|
||||||
<>
|
<ScrollForm sections={sections}>
|
||||||
<Text className="pf-u-py-lg">
|
|
||||||
<Trans i18nKey="clients-help:notBeforeIntro">
|
|
||||||
In order to successfully push setup url on
|
|
||||||
<Link to={`/${realm}/clients/${id}/settings`}>{t("settings")}</Link>
|
|
||||||
tab
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<FormAccess role="manage-clients" isHorizontal>
|
|
||||||
<FormGroup
|
|
||||||
label={t("notBefore")}
|
|
||||||
fieldId="kc-not-before"
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem
|
|
||||||
helpText="clients-help:notBefore"
|
|
||||||
forLabel={t("notBefore")}
|
|
||||||
forID="kc-not-before"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
type="text"
|
|
||||||
id="kc-not-before"
|
|
||||||
name="notBefore"
|
|
||||||
isReadOnly
|
|
||||||
value={formatDate()}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<ActionGroup>
|
|
||||||
<Button
|
|
||||||
id="setToNow"
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={() => {
|
|
||||||
setNotBefore(moment.now() / 1000, "notBeforeSetToNow");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("setToNow")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="clear"
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={() => {
|
|
||||||
setNotBefore(0, "notBeforeNowClear");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("clear")}
|
|
||||||
</Button>
|
|
||||||
<Button id="push" variant="secondary" onClick={push}>
|
|
||||||
{t("push")}
|
|
||||||
</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
</FormAccess>
|
|
||||||
</>
|
|
||||||
<>
|
|
||||||
<FormAccess role="manage-clients" isHorizontal>
|
|
||||||
<FormGroup
|
|
||||||
label={t("nodeReRegistrationTimeout")}
|
|
||||||
fieldId="kc-node-reregistration-timeout"
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem
|
|
||||||
helpText="clients-help:nodeReRegistrationTimeout"
|
|
||||||
forLabel={t("nodeReRegistrationTimeout")}
|
|
||||||
forID="nodeReRegistrationTimeout"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Split hasGutter>
|
|
||||||
<SplitItem>
|
|
||||||
<Controller
|
|
||||||
name="nodeReRegistrationTimeout"
|
|
||||||
defaultValue=""
|
|
||||||
control={control}
|
|
||||||
render={({ onChange, value }) => (
|
|
||||||
<TimeSelector value={value} onChange={onChange} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SplitItem>
|
|
||||||
<SplitItem>
|
|
||||||
<Button
|
|
||||||
variant={ButtonVariant.secondary}
|
|
||||||
onClick={() => save()}
|
|
||||||
>
|
|
||||||
{t("common:save")}
|
|
||||||
</Button>
|
|
||||||
</SplitItem>
|
|
||||||
</Split>
|
|
||||||
</FormGroup>
|
|
||||||
</FormAccess>
|
|
||||||
<>
|
<>
|
||||||
<DeleteNodeConfirm />
|
<Text className="pf-u-py-lg">
|
||||||
<AddHostDialog
|
<Trans i18nKey="clients-help:notBeforeIntro">
|
||||||
clientId={id!}
|
In order to successfully push setup url on
|
||||||
isOpen={addNodeOpen}
|
<Link to={`/${realm}/clients/${id}/settings`}>
|
||||||
onAdded={(node) => {
|
{t("settings")}
|
||||||
nodes[node] = moment.now() / 1000;
|
</Link>
|
||||||
refresh();
|
tab
|
||||||
}}
|
</Trans>
|
||||||
onClose={() => setAddNodeOpen(false)}
|
</Text>
|
||||||
/>
|
<FormAccess role="manage-clients" isHorizontal>
|
||||||
<ExpandableSection
|
<FormGroup
|
||||||
toggleText={t("registeredClusterNodes")}
|
label={t("notBefore")}
|
||||||
onToggle={() => setExpanded(!expanded)}
|
fieldId="kc-not-before"
|
||||||
isExpanded={expanded}
|
labelIcon={
|
||||||
>
|
<HelpItem
|
||||||
<KeycloakDataTable
|
helpText="clients-help:notBefore"
|
||||||
key={key}
|
forLabel={t("notBefore")}
|
||||||
ariaLabelKey="registeredClusterNodes"
|
forID="kc-not-before"
|
||||||
loader={() =>
|
/>
|
||||||
Promise.resolve(
|
|
||||||
Object.entries(nodes || {}).map((entry) => {
|
|
||||||
return { host: entry[0], registration: entry[1] };
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
toolbarItem={
|
>
|
||||||
<>
|
<TextInput
|
||||||
<ToolbarItem>
|
type="text"
|
||||||
<Button
|
id="kc-not-before"
|
||||||
id="testClusterAvailability"
|
name="notBefore"
|
||||||
onClick={testCluster}
|
isReadOnly
|
||||||
variant={ButtonVariant.secondary}
|
value={formatDate()}
|
||||||
isDisabled={Object.keys(nodes).length === 0}
|
/>
|
||||||
>
|
</FormGroup>
|
||||||
{t("testClusterAvailability")}
|
<ActionGroup>
|
||||||
</Button>
|
<Button
|
||||||
</ToolbarItem>
|
id="setToNow"
|
||||||
<ToolbarItem>
|
variant="tertiary"
|
||||||
<Button
|
onClick={() => {
|
||||||
id="registerNodeManually"
|
setNotBefore(moment.now() / 1000, "notBeforeSetToNow");
|
||||||
onClick={() => setAddNodeOpen(true)}
|
}}
|
||||||
variant={ButtonVariant.tertiary}
|
>
|
||||||
>
|
{t("setToNow")}
|
||||||
{t("registerNodeManually")}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
</ToolbarItem>
|
id="clear"
|
||||||
</>
|
variant="tertiary"
|
||||||
}
|
onClick={() => {
|
||||||
actions={[
|
setNotBefore(0, "notBeforeNowClear");
|
||||||
{
|
}}
|
||||||
title: t("common:delete"),
|
>
|
||||||
onRowClick: (node) => {
|
{t("clear")}
|
||||||
setSelectedNode(node.host);
|
</Button>
|
||||||
toggleDeleteNodeConfirm();
|
<Button id="push" variant="secondary" onClick={push}>
|
||||||
},
|
{t("push")}
|
||||||
},
|
</Button>
|
||||||
]}
|
</ActionGroup>
|
||||||
columns={[
|
</FormAccess>
|
||||||
{
|
</>
|
||||||
name: "host",
|
<>
|
||||||
displayKey: "clients:nodeHost",
|
<FormAccess role="manage-clients" isHorizontal>
|
||||||
},
|
<FormGroup
|
||||||
{
|
label={t("nodeReRegistrationTimeout")}
|
||||||
name: "registration",
|
fieldId="kc-node-reregistration-timeout"
|
||||||
displayKey: "clients:lastRegistration",
|
labelIcon={
|
||||||
cellFormatters: [
|
<HelpItem
|
||||||
(value) =>
|
helpText="clients-help:nodeReRegistrationTimeout"
|
||||||
value
|
forLabel={t("nodeReRegistrationTimeout")}
|
||||||
? moment(parseInt(value.toString()) * 1000).format(
|
forID="nodeReRegistrationTimeout"
|
||||||
"LLL"
|
/>
|
||||||
)
|
}
|
||||||
: "",
|
>
|
||||||
],
|
<Split hasGutter>
|
||||||
},
|
<SplitItem>
|
||||||
]}
|
<Controller
|
||||||
/>
|
name="nodeReRegistrationTimeout"
|
||||||
</ExpandableSection>
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<TimeSelector value={value} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.secondary}
|
||||||
|
onClick={() => save()}
|
||||||
|
>
|
||||||
|
{t("common:save")}
|
||||||
|
</Button>
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</FormGroup>
|
||||||
|
</FormAccess>
|
||||||
|
<>
|
||||||
|
<DeleteNodeConfirm />
|
||||||
|
<AddHostDialog
|
||||||
|
clientId={id!}
|
||||||
|
isOpen={addNodeOpen}
|
||||||
|
onAdded={(node) => {
|
||||||
|
nodes[node] = moment.now() / 1000;
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
onClose={() => setAddNodeOpen(false)}
|
||||||
|
/>
|
||||||
|
<ExpandableSection
|
||||||
|
toggleText={t("registeredClusterNodes")}
|
||||||
|
onToggle={() => setExpanded(!expanded)}
|
||||||
|
isExpanded={expanded}
|
||||||
|
>
|
||||||
|
<KeycloakDataTable
|
||||||
|
key={key}
|
||||||
|
ariaLabelKey="registeredClusterNodes"
|
||||||
|
loader={() =>
|
||||||
|
Promise.resolve(
|
||||||
|
Object.entries(nodes || {}).map((entry) => {
|
||||||
|
return { host: entry[0], registration: entry[1] };
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toolbarItem={
|
||||||
|
<>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
id="testClusterAvailability"
|
||||||
|
onClick={testCluster}
|
||||||
|
variant={ButtonVariant.secondary}
|
||||||
|
isDisabled={Object.keys(nodes).length === 0}
|
||||||
|
>
|
||||||
|
{t("testClusterAvailability")}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button
|
||||||
|
id="registerNodeManually"
|
||||||
|
onClick={() => setAddNodeOpen(true)}
|
||||||
|
variant={ButtonVariant.tertiary}
|
||||||
|
>
|
||||||
|
{t("registerNodeManually")}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
title: t("common:delete"),
|
||||||
|
onRowClick: (node) => {
|
||||||
|
setSelectedNode(node.host);
|
||||||
|
toggleDeleteNodeConfirm();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
name: "host",
|
||||||
|
displayKey: "clients:nodeHost",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "registration",
|
||||||
|
displayKey: "clients:lastRegistration",
|
||||||
|
cellFormatters: [
|
||||||
|
(value) =>
|
||||||
|
value
|
||||||
|
? moment(parseInt(value.toString()) * 1000).format(
|
||||||
|
"LLL"
|
||||||
|
)
|
||||||
|
: "",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ExpandableSection>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
<>
|
||||||
|
{protocol === openIdConnect && (
|
||||||
|
<>
|
||||||
|
<Text className="pf-u-py-lg">
|
||||||
|
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
||||||
|
</Text>
|
||||||
|
<FineGrainOpenIdConnect
|
||||||
|
control={control}
|
||||||
|
save={() => save()}
|
||||||
|
reset={() =>
|
||||||
|
convertToFormValues(attributes, "attributes", setValue)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{protocol !== openIdConnect && (
|
||||||
|
<>
|
||||||
|
<Text className="pf-u-py-lg">
|
||||||
|
{t("clients-help:fineGrainSamlEndpointConfig")}
|
||||||
|
</Text>
|
||||||
|
<FineGrainSamlEndpointConfig
|
||||||
|
control={control}
|
||||||
|
save={() => save()}
|
||||||
|
reset={() =>
|
||||||
|
convertToFormValues(attributes, "attributes", setValue)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
</>
|
|
||||||
<>
|
|
||||||
{protocol === openIdConnect && (
|
{protocol === openIdConnect && (
|
||||||
<>
|
<>
|
||||||
<Text className="pf-u-py-lg">
|
<Text className="pf-u-py-lg">
|
||||||
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
{t("clients-help:openIdConnectCompatibilityModes")}
|
||||||
</Text>
|
</Text>
|
||||||
<FineGrainOpenIdConnect
|
<OpenIdConnectCompatibilityModes
|
||||||
control={control}
|
control={control}
|
||||||
save={() => save()}
|
save={() => save()}
|
||||||
reset={() =>
|
reset={() =>
|
||||||
convertToFormValues(attributes, "attributes", setValue)
|
resetFields(["exclude-session-state-from-auth-response"])
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{protocol !== openIdConnect && (
|
|
||||||
<>
|
|
||||||
<Text className="pf-u-py-lg">
|
|
||||||
{t("clients-help:fineGrainSamlEndpointConfig")}
|
|
||||||
</Text>
|
|
||||||
<FineGrainSamlEndpointConfig
|
|
||||||
control={control}
|
|
||||||
save={() => save()}
|
|
||||||
reset={() =>
|
|
||||||
convertToFormValues(attributes, "attributes", setValue)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
{protocol === openIdConnect && (
|
|
||||||
<>
|
<>
|
||||||
<Text className="pf-u-py-lg">
|
<Text className="pf-u-py-lg">
|
||||||
{t("clients-help:openIdConnectCompatibilityModes")}
|
{t("clients-help:advancedSettings" + toUpperCase(protocol!))}
|
||||||
</Text>
|
</Text>
|
||||||
<OpenIdConnectCompatibilityModes
|
<AdvancedSettings
|
||||||
|
protocol={protocol}
|
||||||
control={control}
|
control={control}
|
||||||
save={() => save()}
|
save={() => save()}
|
||||||
reset={() =>
|
reset={() => {
|
||||||
resetFields(["exclude-session-state-from-auth-response"])
|
resetFields([
|
||||||
}
|
"saml-assertion-lifespan",
|
||||||
|
"access-token-lifespan",
|
||||||
|
"tls-client-certificate-bound-access-tokens",
|
||||||
|
"pkce-code-challenge-method",
|
||||||
|
]);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
<>
|
||||||
<>
|
<Text className="pf-u-py-lg">
|
||||||
<Text className="pf-u-py-lg">
|
{t("clients-help:authenticationOverrides")}
|
||||||
{t("clients-help:advancedSettings" + toUpperCase(protocol!))}
|
</Text>
|
||||||
</Text>
|
<AuthenticationOverrides
|
||||||
<AdvancedSettings
|
protocol={protocol}
|
||||||
protocol={protocol}
|
control={control}
|
||||||
control={control}
|
save={() => save()}
|
||||||
save={() => save()}
|
reset={() => {
|
||||||
reset={() => {
|
setValue(
|
||||||
resetFields([
|
"authenticationFlowBindingOverrides.browser",
|
||||||
"saml-assertion-lifespan",
|
authenticationFlowBindingOverrides?.browser
|
||||||
"access-token-lifespan",
|
);
|
||||||
"tls-client-certificate-bound-access-tokens",
|
setValue(
|
||||||
"pkce-code-challenge-method",
|
"authenticationFlowBindingOverrides.direct_grant",
|
||||||
]);
|
authenticationFlowBindingOverrides?.direct_grant
|
||||||
}}
|
);
|
||||||
/>
|
}}
|
||||||
</>
|
/>
|
||||||
<>
|
</>
|
||||||
<Text className="pf-u-py-lg">
|
</ScrollForm>
|
||||||
{t("clients-help:authenticationOverrides")}
|
</PageSection>
|
||||||
</Text>
|
|
||||||
<AuthenticationOverrides
|
|
||||||
protocol={protocol}
|
|
||||||
control={control}
|
|
||||||
save={() => save()}
|
|
||||||
reset={() => {
|
|
||||||
setValue(
|
|
||||||
"authenticationFlowBindingOverrides.browser",
|
|
||||||
authenticationFlowBindingOverrides?.browser
|
|
||||||
);
|
|
||||||
setValue(
|
|
||||||
"authenticationFlowBindingOverrides.direct_grant",
|
|
||||||
authenticationFlowBindingOverrides?.direct_grant
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</ScrollForm>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const CapabilityConfig = ({
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="authentication"
|
||||||
id="kc-authentication"
|
id="kc-authentication"
|
||||||
name="publicClient"
|
name="publicClient"
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
|
@ -65,6 +66,7 @@ export const CapabilityConfig = ({
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="authorization"
|
||||||
id="kc-authorization"
|
id="kc-authorization"
|
||||||
name="authorizationServicesEnabled"
|
name="authorizationServicesEnabled"
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
|
@ -95,6 +97,7 @@ export const CapabilityConfig = ({
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
data-testid="standard"
|
||||||
label={t("standardFlow")}
|
label={t("standardFlow")}
|
||||||
id="kc-flow-standard"
|
id="kc-flow-standard"
|
||||||
name="standardFlowEnabled"
|
name="standardFlowEnabled"
|
||||||
|
@ -118,6 +121,7 @@ export const CapabilityConfig = ({
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
data-testid="direct"
|
||||||
label={t("directAccess")}
|
label={t("directAccess")}
|
||||||
id="kc-flow-direct"
|
id="kc-flow-direct"
|
||||||
name="directAccessGrantsEnabled"
|
name="directAccessGrantsEnabled"
|
||||||
|
@ -141,6 +145,7 @@ export const CapabilityConfig = ({
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
data-testid="implicit"
|
||||||
label={t("implicitFlow")}
|
label={t("implicitFlow")}
|
||||||
id="kc-flow-implicit"
|
id="kc-flow-implicit"
|
||||||
name="implicitFlowEnabled"
|
name="implicitFlowEnabled"
|
||||||
|
@ -164,6 +169,7 @@ export const CapabilityConfig = ({
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
data-testid="service-account"
|
||||||
label={t("serviceAccount")}
|
label={t("serviceAccount")}
|
||||||
id="kc-flow-service-account"
|
id="kc-flow-service-account"
|
||||||
name="serviceAccountsEnabled"
|
name="serviceAccountsEnabled"
|
||||||
|
@ -207,6 +213,7 @@ export const CapabilityConfig = ({
|
||||||
defaultValue="false"
|
defaultValue="false"
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="encrypt"
|
||||||
id="kc-encrypt"
|
id="kc-encrypt"
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
labelOff={t("common:off")}
|
labelOff={t("common:off")}
|
||||||
|
@ -233,6 +240,7 @@ export const CapabilityConfig = ({
|
||||||
defaultValue="false"
|
defaultValue="false"
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="client-signature"
|
||||||
id="kc-client-signature"
|
id="kc-client-signature"
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
labelOff={t("common:off")}
|
labelOff={t("common:off")}
|
||||||
|
|
|
@ -105,6 +105,14 @@
|
||||||
"directAccess": "Direct access",
|
"directAccess": "Direct access",
|
||||||
"serviceAccount": "Service account roles",
|
"serviceAccount": "Service account roles",
|
||||||
"enableServiceAccount": "Enable service account roles",
|
"enableServiceAccount": "Enable service account roles",
|
||||||
|
"assignRolesTo": "Assign roles to {{client}} account",
|
||||||
|
"searchByRoleName": "Search by role name",
|
||||||
|
"filterByOrigin": "Filter by Origin",
|
||||||
|
"realmRoles": "Realm roles",
|
||||||
|
"clients": "Clients",
|
||||||
|
"assign": "Assign",
|
||||||
|
"roleMappingUpdatedSuccess": "Role mapping updated",
|
||||||
|
"roleMappingUpdatedError": "Could not update role mapping {{error}}",
|
||||||
"displayOnClient": "Display client on screen",
|
"displayOnClient": "Display client on screen",
|
||||||
"consentScreenText": "Client consent screen text",
|
"consentScreenText": "Client consent screen text",
|
||||||
"loginSettings": "Login settings",
|
"loginSettings": "Login settings",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
Grid,
|
Grid,
|
||||||
GridItem,
|
GridItem,
|
||||||
|
PageSection,
|
||||||
Select,
|
Select,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
SelectVariant,
|
SelectVariant,
|
||||||
|
@ -253,92 +254,95 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextContent className="keycloak__scopes_evaluate__intro">
|
<PageSection variant="light">
|
||||||
<Text>
|
<TextContent className="keycloak__scopes_evaluate__intro">
|
||||||
<QuestionCircleIcon /> {t("clients-help:evaluateExplain")}
|
<Text>
|
||||||
</Text>
|
<QuestionCircleIcon /> {t("clients-help:evaluateExplain")}
|
||||||
</TextContent>
|
</Text>
|
||||||
<Form isHorizontal>
|
</TextContent>
|
||||||
<FormGroup
|
<Form isHorizontal>
|
||||||
label={t("scopeParameter")}
|
<FormGroup
|
||||||
fieldId="scopeParameter"
|
label={t("scopeParameter")}
|
||||||
labelIcon={
|
fieldId="scopeParameter"
|
||||||
<HelpItem
|
labelIcon={
|
||||||
helpText="clients-help:scopeParameter"
|
<HelpItem
|
||||||
forLabel={t("scopeParameter")}
|
helpText="clients-help:scopeParameter"
|
||||||
forID="scopeParameter"
|
forLabel={t("scopeParameter")}
|
||||||
/>
|
forID="scopeParameter"
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
<Split hasGutter>
|
>
|
||||||
<SplitItem isFilled>
|
<Split hasGutter>
|
||||||
<Select
|
<SplitItem isFilled>
|
||||||
toggleId="scopeParameter"
|
<Select
|
||||||
variant={SelectVariant.typeaheadMulti}
|
toggleId="scopeParameter"
|
||||||
typeAheadAriaLabel={t("scopeParameter")}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
onToggle={() => setIsScopeOpen(!isScopeOpen)}
|
typeAheadAriaLabel={t("scopeParameter")}
|
||||||
isOpen={isScopeOpen}
|
onToggle={() => setIsScopeOpen(!isScopeOpen)}
|
||||||
selections={selected}
|
isOpen={isScopeOpen}
|
||||||
onSelect={(_, value) => {
|
selections={selected}
|
||||||
const option = value as string;
|
onSelect={(_, value) => {
|
||||||
if (selected.includes(option)) {
|
const option = value as string;
|
||||||
if (option !== prefix) {
|
if (selected.includes(option)) {
|
||||||
setSelected(selected.filter((item) => item !== option));
|
if (option !== prefix) {
|
||||||
|
setSelected(selected.filter((item) => item !== option));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelected([...selected, option]);
|
||||||
}
|
}
|
||||||
} else {
|
}}
|
||||||
setSelected([...selected, option]);
|
aria-labelledby={t("scopeParameter")}
|
||||||
}
|
placeholderText={t("scopeParameterPlaceholder")}
|
||||||
}}
|
>
|
||||||
aria-labelledby={t("scopeParameter")}
|
{selectableScopes.map((option, index) => (
|
||||||
placeholderText={t("scopeParameterPlaceholder")}
|
<SelectOption key={index} value={option.name} />
|
||||||
>
|
))}
|
||||||
{selectableScopes.map((option, index) => (
|
</Select>
|
||||||
<SelectOption key={index} value={option.name} />
|
</SplitItem>
|
||||||
))}
|
<SplitItem>
|
||||||
</Select>
|
<ClipboardCopy className="keycloak__scopes_evaluate__clipboard-copy">
|
||||||
</SplitItem>
|
{selected.join(" ")}
|
||||||
<SplitItem>
|
</ClipboardCopy>
|
||||||
<ClipboardCopy className="keycloak__scopes_evaluate__clipboard-copy">
|
</SplitItem>
|
||||||
{selected.join(" ")}
|
</Split>
|
||||||
</ClipboardCopy>
|
</FormGroup>
|
||||||
</SplitItem>
|
<FormGroup
|
||||||
</Split>
|
label={t("user")}
|
||||||
</FormGroup>
|
fieldId="user"
|
||||||
<FormGroup
|
labelIcon={
|
||||||
label={t("user")}
|
<HelpItem
|
||||||
fieldId="user"
|
helpText="clients-help:user"
|
||||||
labelIcon={
|
forLabel={t("user")}
|
||||||
<HelpItem
|
forID="user"
|
||||||
helpText="clients-help:user"
|
/>
|
||||||
forLabel={t("user")}
|
}
|
||||||
forID="user"
|
>
|
||||||
|
<Select
|
||||||
|
toggleId="user"
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={t("user")}
|
||||||
|
onToggle={() => setIsUserOpen(!isUserOpen)}
|
||||||
|
onFilter={(e) => {
|
||||||
|
const value = e?.target.value || "";
|
||||||
|
setUserSearch(value);
|
||||||
|
return userItems;
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setUser(undefined);
|
||||||
|
setUserSearch("");
|
||||||
|
}}
|
||||||
|
selections={[user]}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
setUser(value as UserRepresentation);
|
||||||
|
setUserSearch("");
|
||||||
|
setIsUserOpen(false);
|
||||||
|
}}
|
||||||
|
isOpen={isUserOpen}
|
||||||
/>
|
/>
|
||||||
}
|
</FormGroup>
|
||||||
>
|
</Form>
|
||||||
<Select
|
</PageSection>
|
||||||
toggleId="user"
|
|
||||||
variant={SelectVariant.typeahead}
|
|
||||||
typeAheadAriaLabel={t("user")}
|
|
||||||
onToggle={() => setIsUserOpen(!isUserOpen)}
|
|
||||||
onFilter={(e) => {
|
|
||||||
const value = e?.target.value || "";
|
|
||||||
setUserSearch(value);
|
|
||||||
return userItems;
|
|
||||||
}}
|
|
||||||
onClear={() => {
|
|
||||||
setUser(undefined);
|
|
||||||
setUserSearch("");
|
|
||||||
}}
|
|
||||||
selections={[user]}
|
|
||||||
onSelect={(_, value) => {
|
|
||||||
setUser(value as UserRepresentation);
|
|
||||||
setUserSearch("");
|
|
||||||
setIsUserOpen(false);
|
|
||||||
}}
|
|
||||||
isOpen={isUserOpen}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
<Grid hasGutter className="keycloak__scopes_evaluate__tabs">
|
<Grid hasGutter className="keycloak__scopes_evaluate__tabs">
|
||||||
<GridItem span={8}>
|
<GridItem span={8}>
|
||||||
<TabContent
|
<TabContent
|
||||||
|
|
269
src/clients/service-account/AddServiceAccountModal.tsx
Normal file
269
src/clients/service-account/AddServiceAccountModal.tsx
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
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 "../../components/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";
|
||||||
|
|
||||||
|
type AddServiceAccountModalProps = {
|
||||||
|
clientId: string;
|
||||||
|
serviceAccountId: string;
|
||||||
|
onAssign: (rows: Row[]) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClientRole = ClientRepresentation & {
|
||||||
|
numberOfRoles: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const realmRole = {
|
||||||
|
name: "realmRoles",
|
||||||
|
} as ClientRepresentation;
|
||||||
|
|
||||||
|
export const AddServiceAccountModal = ({
|
||||||
|
clientId,
|
||||||
|
serviceAccountId,
|
||||||
|
onAssign,
|
||||||
|
onClose,
|
||||||
|
}: AddServiceAccountModalProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const errorHandler = useErrorHandler();
|
||||||
|
|
||||||
|
const [clients, setClients] = useState<ClientRole[]>([]);
|
||||||
|
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();
|
||||||
|
return (
|
||||||
|
await Promise.all(
|
||||||
|
clients.map(async (client) => {
|
||||||
|
const roles = await adminClient.users.listAvailableClientRoleMappings(
|
||||||
|
{
|
||||||
|
id: serviceAccountId,
|
||||||
|
clientUniqueId: 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const realmRoles = (
|
||||||
|
await adminClient.users.listAvailableRealmRoleMappings({
|
||||||
|
id: serviceAccountId,
|
||||||
|
})
|
||||||
|
).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) =>
|
||||||
|
(
|
||||||
|
await adminClient.users.listAvailableClientRoleMappings({
|
||||||
|
id: serviceAccountId,
|
||||||
|
clientUniqueId: client.id!,
|
||||||
|
})
|
||||||
|
).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}
|
||||||
|
title={t("assignRolesTo", { client: clientId })}
|
||||||
|
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="secondary"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,25 +1,50 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core";
|
import {
|
||||||
|
AlertVariant,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
ToolbarItem,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
import RoleRepresentation, {
|
||||||
|
RoleMappingPayload,
|
||||||
|
} from "keycloak-admin/lib/defs/roleRepresentation";
|
||||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
import { RealmContext } from "../../context/realm-context/RealmContext";
|
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
import { emptyFormatter } from "../../util";
|
import { emptyFormatter } from "../../util";
|
||||||
|
import { AddServiceAccountModal } from "./AddServiceAccountModal";
|
||||||
|
|
||||||
import "./service-account.css";
|
import "./service-account.css";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
|
||||||
type ServiceAccountProps = {
|
type ServiceAccountProps = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Row = {
|
export type Row = {
|
||||||
client: ClientRepresentation;
|
client?: ClientRepresentation;
|
||||||
role: CompositeRole;
|
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 & {
|
type CompositeRole = RoleRepresentation & {
|
||||||
parent: RoleRepresentation;
|
parent: RoleRepresentation;
|
||||||
};
|
};
|
||||||
|
@ -28,13 +53,20 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { realm } = useContext(RealmContext);
|
const { realm } = useContext(RealmContext);
|
||||||
|
const { addAlert } = useAlerts();
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const refresh = () => setKey(new Date().getTime());
|
||||||
|
|
||||||
const [hide, setHide] = useState(false);
|
const [hide, setHide] = useState(false);
|
||||||
|
const [serviceAccountId, setServiceAccountId] = useState("");
|
||||||
|
const [showAssign, setShowAssign] = useState(false);
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
||||||
id: clientId,
|
id: clientId,
|
||||||
});
|
});
|
||||||
|
setServiceAccountId(serviceAccount.id!);
|
||||||
const effectiveRoles = await adminClient.users.listCompositeRealmRoleMappings(
|
const effectiveRoles = await adminClient.users.listCompositeRealmRoleMappings(
|
||||||
{ id: serviceAccount.id! }
|
{ id: serviceAccount.id! }
|
||||||
);
|
);
|
||||||
|
@ -65,7 +97,6 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
||||||
console.log(clientRolesFlat);
|
|
||||||
|
|
||||||
const addInherentData = await (async () =>
|
const addInherentData = await (async () =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
|
@ -99,59 +130,90 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoleLink = ({ role, client }: Row) => (
|
const assignRoles = async (rows: Row[]) => {
|
||||||
|
try {
|
||||||
|
const realmRoles = rows
|
||||||
|
.filter((row) => row.client === undefined)
|
||||||
|
.map((row) => row.role as RoleMappingPayload)
|
||||||
|
.flat();
|
||||||
|
adminClient.users.addRealmRoleMappings({
|
||||||
|
id: serviceAccountId,
|
||||||
|
roles: realmRoles,
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
rows
|
||||||
|
.filter((row) => row.client !== undefined)
|
||||||
|
.map((row) =>
|
||||||
|
adminClient.users.addClientRoleMappings({
|
||||||
|
id: serviceAccountId,
|
||||||
|
clientUniqueId: row.client!.id!,
|
||||||
|
roles: [row.role as RoleMappingPayload],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
addAlert(t("roleMappingUpdatedSuccess"), AlertVariant.success);
|
||||||
|
refresh();
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(
|
||||||
|
t("roleMappingUpdatedError", {
|
||||||
|
error: error.response?.data?.errorMessage || error,
|
||||||
|
}),
|
||||||
|
AlertVariant.danger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
{client && (
|
{showAssign && (
|
||||||
<Badge
|
<AddServiceAccountModal
|
||||||
key={client.id}
|
clientId={clientId}
|
||||||
isRead
|
serviceAccountId={serviceAccountId}
|
||||||
className="keycloak-admin--service-account__client-name"
|
onAssign={assignRoles}
|
||||||
>
|
onClose={() => setShowAssign(false)}
|
||||||
{client.clientId}
|
/>
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
{role.name}
|
<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()],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
|
||||||
<KeycloakDataTable
|
|
||||||
loader={loader}
|
|
||||||
onSelect={() => {}}
|
|
||||||
searchPlaceholderKey="clients:searchByName"
|
|
||||||
ariaLabelKey="clients:clientScopeList"
|
|
||||||
toolbarItem={
|
|
||||||
<>
|
|
||||||
<ToolbarItem>
|
|
||||||
<Checkbox
|
|
||||||
label={t("hideInheritedRoles")}
|
|
||||||
id="hideInheritedRoles"
|
|
||||||
isChecked={hide}
|
|
||||||
onChange={setHide}
|
|
||||||
/>
|
|
||||||
</ToolbarItem>
|
|
||||||
<ToolbarItem>
|
|
||||||
<Button>{t("assignRole")}</Button>
|
|
||||||
</ToolbarItem>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
name: "role.name",
|
|
||||||
displayKey: t("name"),
|
|
||||||
cellRenderer: RoleLink,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "role.parent.name",
|
|
||||||
displayKey: t("inherentFrom"),
|
|
||||||
cellFormatters: [emptyFormatter()],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "role.description",
|
|
||||||
displayKey: t("description"),
|
|
||||||
cellFormatters: [emptyFormatter()],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,10 +43,12 @@ function DataTable<T>({
|
||||||
ariaLabelKey,
|
ariaLabelKey,
|
||||||
onSelect,
|
onSelect,
|
||||||
canSelectAll,
|
canSelectAll,
|
||||||
|
...props
|
||||||
}: DataTableProps<T>) {
|
}: DataTableProps<T>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
|
{...props}
|
||||||
variant={TableVariant.compact}
|
variant={TableVariant.compact}
|
||||||
onSelect={
|
onSelect={
|
||||||
onSelect
|
onSelect
|
||||||
|
@ -130,6 +132,7 @@ export function KeycloakDataTable<T>({
|
||||||
searchTypeComponent,
|
searchTypeComponent,
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
emptyState,
|
emptyState,
|
||||||
|
...props
|
||||||
}: DataListProps<T>) {
|
}: DataListProps<T>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selected, setSelected] = useState<T[]>([]);
|
const [selected, setSelected] = useState<T[]>([]);
|
||||||
|
@ -281,6 +284,7 @@ export function KeycloakDataTable<T>({
|
||||||
>
|
>
|
||||||
{!loading && (filteredData || rows).length > 0 && (
|
{!loading && (filteredData || rows).length > 0 && (
|
||||||
<DataTable
|
<DataTable
|
||||||
|
{...props}
|
||||||
canSelectAll={canSelectAll}
|
canSelectAll={canSelectAll}
|
||||||
onSelect={onSelect ? _onSelect : undefined}
|
onSelect={onSelect ? _onSelect : undefined}
|
||||||
actions={convertAction()}
|
actions={convertAction()}
|
||||||
|
@ -290,14 +294,17 @@ export function KeycloakDataTable<T>({
|
||||||
ariaLabelKey={ariaLabelKey}
|
ariaLabelKey={ariaLabelKey}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!loading && rows.length === 0 && search !== "" && (
|
{!loading &&
|
||||||
<ListEmptyState
|
rows.length === 0 &&
|
||||||
hasIcon={true}
|
search !== "" &&
|
||||||
isSearchVariant={true}
|
searchPlaceholderKey && (
|
||||||
message={t("noSearchResults")}
|
<ListEmptyState
|
||||||
instructions={t("noSearchResultsInstructions")}
|
hasIcon={true}
|
||||||
/>
|
isSearchVariant={true}
|
||||||
)}
|
message={t("noSearchResults")}
|
||||||
|
instructions={t("noSearchResultsInstructions")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{loading && <Loading />}
|
{loading && <Loading />}
|
||||||
</PaginatingTableToolbar>
|
</PaginatingTableToolbar>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in a new issue