Adds the permissions tab to the Authorization screen under clients (#1835)
* initial work on permissions * search dropdown permissions * permissions tab * added empty state * added new permssion detail route * added detail screen * fixed load * added decision strategy * added tests * fixed class name and identeded the expandable table row
This commit is contained in:
parent
4d4190f67b
commit
811131518e
20 changed files with 1299 additions and 135 deletions
99
cypress/integration/client_authorization_test.spec.ts
Normal file
99
cypress/integration/client_authorization_test.spec.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import {
|
||||||
|
keycloakBefore,
|
||||||
|
keycloakBeforeEach,
|
||||||
|
} from "../support/util/keycloak_hooks";
|
||||||
|
import AdminClient from "../support/util/AdminClient";
|
||||||
|
import LoginPage from "../support/pages/LoginPage";
|
||||||
|
import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||||
|
import Masthead from "../support/pages/admin_console/Masthead";
|
||||||
|
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||||
|
import AuthorizationTab from "../support/pages/admin_console/manage/clients/AuthorizationTab";
|
||||||
|
|
||||||
|
describe("Client authentication subtab", () => {
|
||||||
|
const adminClient = new AdminClient();
|
||||||
|
const loginPage = new LoginPage();
|
||||||
|
const listingPage = new ListingPage();
|
||||||
|
const masthead = new Masthead();
|
||||||
|
const sidebarPage = new SidebarPage();
|
||||||
|
const authenticationTab = new AuthorizationTab();
|
||||||
|
const clientId =
|
||||||
|
"client-authentication-" + (Math.random() + 1).toString(36).substring(7);
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
adminClient.createClient({
|
||||||
|
protocol: "openid-connect",
|
||||||
|
clientId,
|
||||||
|
publicClient: false,
|
||||||
|
authorizationServicesEnabled: true,
|
||||||
|
serviceAccountsEnabled: true,
|
||||||
|
standardFlowEnabled: true,
|
||||||
|
});
|
||||||
|
keycloakBefore();
|
||||||
|
loginPage.logIn();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
adminClient.deleteClient(clientId);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
keycloakBeforeEach();
|
||||||
|
sidebarPage.goToClients();
|
||||||
|
listingPage.searchItem(clientId).goToItemDetails(clientId);
|
||||||
|
authenticationTab.goToAuthenticationTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should update the resource server settings", () => {
|
||||||
|
authenticationTab.setPolicy("DISABLED").saveSettings();
|
||||||
|
masthead.checkNotificationMessage("Resource successfully updated");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should create a resource", () => {
|
||||||
|
authenticationTab.goToResourceSubTab();
|
||||||
|
authenticationTab.assertDefaultResource();
|
||||||
|
|
||||||
|
authenticationTab
|
||||||
|
.goToCreateResource()
|
||||||
|
.fillResourceForm({
|
||||||
|
name: "Resource",
|
||||||
|
displayName: "The display name",
|
||||||
|
type: "type",
|
||||||
|
uris: ["one", "two"],
|
||||||
|
})
|
||||||
|
.save();
|
||||||
|
|
||||||
|
masthead.checkNotificationMessage("Resource created successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should create a scope", () => {
|
||||||
|
authenticationTab.goToScopeSubTab();
|
||||||
|
authenticationTab
|
||||||
|
.goToCreateScope()
|
||||||
|
.fillScopeForm({
|
||||||
|
name: "The scope",
|
||||||
|
displayName: "Display something",
|
||||||
|
iconUri: "res://something",
|
||||||
|
})
|
||||||
|
.save();
|
||||||
|
|
||||||
|
masthead.checkNotificationMessage(
|
||||||
|
"Authorization scope created successfully"
|
||||||
|
);
|
||||||
|
authenticationTab.goToScopeSubTab();
|
||||||
|
listingPage.itemExist("The scope");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should create a permission", () => {
|
||||||
|
authenticationTab.goToPermissionsSubTab();
|
||||||
|
authenticationTab
|
||||||
|
.goToCreatePermission("resource")
|
||||||
|
.fillPermissionForm({
|
||||||
|
name: "Permission name",
|
||||||
|
description: "Something describing this permission",
|
||||||
|
})
|
||||||
|
.selectResource("Resource")
|
||||||
|
.save();
|
||||||
|
|
||||||
|
masthead.checkNotificationMessage("Successfully created the permission");
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,7 +5,6 @@ import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||||
import ModalUtils from "../support/util/ModalUtils";
|
import ModalUtils from "../support/util/ModalUtils";
|
||||||
import AdminClient from "../support/util/AdminClient";
|
import AdminClient from "../support/util/AdminClient";
|
||||||
import { keycloakBefore } from "../support/util/keycloak_hooks";
|
import { keycloakBefore } from "../support/util/keycloak_hooks";
|
||||||
import AuthenticationTab from "../support/pages/admin_console/manage/clients/AuthenticationTab";
|
|
||||||
|
|
||||||
const loginPage = new LoginPage();
|
const loginPage = new LoginPage();
|
||||||
const masthead = new Masthead();
|
const masthead = new Masthead();
|
||||||
|
@ -110,55 +109,4 @@ describe("Clients SAML tests", () => {
|
||||||
cy.findAllByTestId("certificate").should("have.length", 1);
|
cy.findAllByTestId("certificate").should("have.length", 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Authentication tab", () => {
|
|
||||||
const clientName = "authenticationTabClient";
|
|
||||||
const authenticationTab = new AuthenticationTab();
|
|
||||||
beforeEach(() => {
|
|
||||||
keycloakBefore();
|
|
||||||
loginPage.logIn();
|
|
||||||
sidebarPage.goToClients();
|
|
||||||
});
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
await new AdminClient().createClient({
|
|
||||||
protocol: "openid-connect",
|
|
||||||
clientId: clientName,
|
|
||||||
publicClient: false,
|
|
||||||
authorizationServicesEnabled: true,
|
|
||||||
serviceAccountsEnabled: true,
|
|
||||||
standardFlowEnabled: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
new AdminClient().deleteClient(clientName);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should update the resource server settings", () => {
|
|
||||||
listingPage.searchItem(clientName).goToItemDetails(clientName);
|
|
||||||
authenticationTab.goToAuthenticationTab();
|
|
||||||
authenticationTab.setPolicy("DISABLED").saveSettings();
|
|
||||||
|
|
||||||
masthead.checkNotificationMessage("Resource successfully updated");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should create a resource", () => {
|
|
||||||
listingPage.searchItem(clientName).goToItemDetails(clientName);
|
|
||||||
authenticationTab.goToAuthenticationTab().goToResourceSubTab();
|
|
||||||
authenticationTab.assertDefaultResource();
|
|
||||||
|
|
||||||
authenticationTab
|
|
||||||
.goToCreateResource()
|
|
||||||
.fillResourceForm({
|
|
||||||
name: "Resource",
|
|
||||||
displayName: "The display name",
|
|
||||||
type: "type",
|
|
||||||
uris: ["one", "two"],
|
|
||||||
})
|
|
||||||
.save();
|
|
||||||
|
|
||||||
masthead.checkNotificationMessage("Resource created successfully");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||||
|
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
||||||
|
|
||||||
export default class AuthenticationTab {
|
type PermissionType = "resource" | "scope";
|
||||||
|
|
||||||
|
export default class AuthorizationTab {
|
||||||
private tabName = "#pf-tab-authorization-authorization";
|
private tabName = "#pf-tab-authorization-authorization";
|
||||||
private resourcesTabName = "#pf-tab-41-resources";
|
private resourcesTabName = "#pf-tab-41-resources";
|
||||||
|
private scopeTabName = "#pf-tab-42-scopes";
|
||||||
|
private permissionsTabName = "#pf-tab-43-permissions";
|
||||||
private nameColumnPrefix = "name-column-";
|
private nameColumnPrefix = "name-column-";
|
||||||
private createResourceButton = "createResource";
|
private createResourceButton = "createResource";
|
||||||
|
private createScopeButton = "no-authorization-scopes-empty-action";
|
||||||
|
private createPermissionDropdown = "permissionCreateDropdown";
|
||||||
|
private permissionResourceDropdown = "#resources";
|
||||||
|
|
||||||
goToAuthenticationTab() {
|
goToAuthenticationTab() {
|
||||||
cy.get(this.tabName).click();
|
cy.get(this.tabName).click();
|
||||||
|
@ -16,8 +25,29 @@ export default class AuthenticationTab {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToScopeSubTab() {
|
||||||
|
cy.get(this.scopeTabName).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPermissionsSubTab() {
|
||||||
|
cy.get(this.permissionsTabName).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
goToCreateResource() {
|
goToCreateResource() {
|
||||||
cy.findAllByTestId(this.createResourceButton).click();
|
cy.findByTestId(this.createResourceButton).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToCreateScope() {
|
||||||
|
cy.findByTestId(this.createScopeButton).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToCreatePermission(type: PermissionType) {
|
||||||
|
cy.findByTestId(this.createPermissionDropdown).click();
|
||||||
|
cy.findByTestId(`create-${type}`).click();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +66,28 @@ export default class AuthenticationTab {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillScopeForm(scope: ScopeRepresentation) {
|
||||||
|
Object.entries(scope).map(([key, value]) => cy.get(`#${key}`).type(value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillPermissionForm(permission: PolicyRepresentation) {
|
||||||
|
Object.entries(permission).map(([key, value]) =>
|
||||||
|
cy.get(`#${key}`).type(value)
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectResource(name: string) {
|
||||||
|
cy.get(this.permissionResourceDropdown)
|
||||||
|
.click()
|
||||||
|
.parent()
|
||||||
|
.parent()
|
||||||
|
.findByText(name)
|
||||||
|
.click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
setPolicy(policyName: string) {
|
setPolicy(policyName: string) {
|
||||||
cy.findByTestId(policyName).click();
|
cy.findByTestId(policyName).click();
|
||||||
return this;
|
return this;
|
||||||
|
@ -52,7 +104,7 @@ export default class AuthenticationTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
pressCancel() {
|
pressCancel() {
|
||||||
cy.findAllByTestId("cancel").click();
|
cy.findByTestId("cancel").click();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ import { toMapper } from "./routes/Mapper";
|
||||||
import { AuthorizationSettings } from "./authorization/Settings";
|
import { AuthorizationSettings } from "./authorization/Settings";
|
||||||
import { AuthorizationResources } from "./authorization/Resources";
|
import { AuthorizationResources } from "./authorization/Resources";
|
||||||
import { AuthorizationScopes } from "./authorization/Scopes";
|
import { AuthorizationScopes } from "./authorization/Scopes";
|
||||||
|
import { AuthorizationPermissions } from "./authorization/Permissions";
|
||||||
|
|
||||||
type ClientDetailHeaderProps = {
|
type ClientDetailHeaderProps = {
|
||||||
onChange: (value: boolean) => void;
|
onChange: (value: boolean) => void;
|
||||||
|
@ -137,7 +138,7 @@ const ClientDetailHeader = ({
|
||||||
<>
|
<>
|
||||||
<DisableConfirm />
|
<DisableConfirm />
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey={client ? client.clientId! : ""}
|
titleKey={client.clientId!}
|
||||||
subKey="clients:clientsExplain"
|
subKey="clients:clientsExplain"
|
||||||
badges={badges}
|
badges={badges}
|
||||||
divider={false}
|
divider={false}
|
||||||
|
@ -492,6 +493,13 @@ export default function ClientDetails() {
|
||||||
>
|
>
|
||||||
<AuthorizationScopes clientId={clientId} />
|
<AuthorizationScopes clientId={clientId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
id="permissions"
|
||||||
|
eventKey={43}
|
||||||
|
title={<TabTitleText>{t("permissions")}</TabTitleText>}
|
||||||
|
>
|
||||||
|
<AuthorizationPermissions clientId={clientId} />
|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { DescriptionList } from "@patternfly/react-core";
|
||||||
import {
|
|
||||||
DescriptionList,
|
|
||||||
DescriptionListGroup,
|
|
||||||
DescriptionListTerm,
|
|
||||||
DescriptionListDescription,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
|
|
||||||
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation";
|
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation";
|
||||||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
import { DetailDescription } from "./DetailDescription";
|
||||||
|
|
||||||
import "./detail-cell.css";
|
import "./detail-cell.css";
|
||||||
|
|
||||||
|
@ -22,7 +17,6 @@ type DetailCellProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DetailCell = ({ id, clientId, uris }: DetailCellProps) => {
|
export const DetailCell = ({ id, clientId, uris }: DetailCellProps) => {
|
||||||
const { t } = useTranslation("clients");
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const [scope, setScope] = useState<Scope>();
|
const [scope, setScope] = useState<Scope>();
|
||||||
const [permissions, setPermissions] =
|
const [permissions, setPermissions] =
|
||||||
|
@ -53,39 +47,13 @@ export const DetailCell = ({ id, clientId, uris }: DetailCellProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList isHorizontal className="keycloak_resource_details">
|
<DescriptionList isHorizontal className="keycloak_resource_details">
|
||||||
<DescriptionListGroup>
|
<DetailDescription name="uris" array={uris} />
|
||||||
<DescriptionListTerm>{t("uris")}</DescriptionListTerm>
|
<DetailDescription name="scopes" array={scope} convert={(s) => s.name} />
|
||||||
<DescriptionListDescription>
|
<DetailDescription
|
||||||
{uris?.map((uri) => (
|
name="associatedPermissions"
|
||||||
<span key={uri} className="pf-u-pr-sm">
|
array={permissions}
|
||||||
{uri}
|
convert={(p) => p.name!}
|
||||||
</span>
|
/>
|
||||||
))}
|
|
||||||
{uris?.length === 0 && <i>{t("common:none")}</i>}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{t("scopes")}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>
|
|
||||||
{scope.map((scope) => (
|
|
||||||
<span key={scope.id} className="pf-u-pr-sm">
|
|
||||||
{scope.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{scope.length === 0 && <i>{t("common:none")}</i>}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>{t("associatedPermissions")}</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>
|
|
||||||
{permissions.map((permission) => (
|
|
||||||
<span key={permission.id} className="pf-u-pr-sm">
|
|
||||||
{permission.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{permissions.length === 0 && <i>{t("common:none")}</i>}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
38
src/clients/authorization/DetailDescription.tsx
Normal file
38
src/clients/authorization/DetailDescription.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
DescriptionListGroup,
|
||||||
|
DescriptionListTerm,
|
||||||
|
DescriptionListDescription,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
type DetailDescriptionProps<T> = {
|
||||||
|
name: string;
|
||||||
|
array?: string[] | T[];
|
||||||
|
convert?: (obj: T) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DetailDescription<T>({
|
||||||
|
name,
|
||||||
|
array,
|
||||||
|
convert,
|
||||||
|
}: DetailDescriptionProps<T>) {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
return (
|
||||||
|
<DescriptionListGroup>
|
||||||
|
<DescriptionListTerm>{t(name)}</DescriptionListTerm>
|
||||||
|
<DescriptionListDescription>
|
||||||
|
{array?.map((element) => {
|
||||||
|
const value =
|
||||||
|
typeof element === "string" ? element : convert!(element);
|
||||||
|
return (
|
||||||
|
<span key={value} className="pf-u-pr-sm">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{array?.length === 0 && <i>{t("common:none")}</i>}
|
||||||
|
</DescriptionListDescription>
|
||||||
|
</DescriptionListGroup>
|
||||||
|
);
|
||||||
|
}
|
104
src/clients/authorization/EmptyPermissionsState.tsx
Normal file
104
src/clients/authorization/EmptyPermissionsState.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateIcon,
|
||||||
|
Title,
|
||||||
|
EmptyStateBody,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import { PlusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
|
import { PermissionType, toNewPermission } from "../routes/NewPermission";
|
||||||
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { toUpperCase } from "../../util";
|
||||||
|
|
||||||
|
type EmptyButtonProps = {
|
||||||
|
permissionType: PermissionType;
|
||||||
|
disabled?: boolean;
|
||||||
|
clientId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmptyButton = ({
|
||||||
|
permissionType,
|
||||||
|
disabled = false,
|
||||||
|
clientId,
|
||||||
|
}: EmptyButtonProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { realm } = useRealm();
|
||||||
|
const history = useHistory();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-testid={`create-${permissionType}`}
|
||||||
|
className={
|
||||||
|
disabled ? "keycloak__permissions__empty_state " : "" + "pf-u-m-sm"
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
!disabled &&
|
||||||
|
history.push(toNewPermission({ realm, id: clientId, permissionType }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(`create${toUpperCase(permissionType)}BasedPermission`)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TooltipEmptyButton = ({
|
||||||
|
permissionType,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}: EmptyButtonProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
return disabled ? (
|
||||||
|
<Tooltip content={t(`no${toUpperCase(permissionType)}CreateHint`)}>
|
||||||
|
<EmptyButton
|
||||||
|
{...props}
|
||||||
|
disabled={disabled}
|
||||||
|
permissionType={permissionType}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<EmptyButton
|
||||||
|
{...props}
|
||||||
|
disabled={disabled}
|
||||||
|
permissionType={permissionType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmptyPermissionsStateProps = {
|
||||||
|
clientId: string;
|
||||||
|
isResourceEnabled?: boolean;
|
||||||
|
isScopeEnabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyPermissionsState = ({
|
||||||
|
clientId,
|
||||||
|
isResourceEnabled,
|
||||||
|
isScopeEnabled,
|
||||||
|
}: EmptyPermissionsStateProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
return (
|
||||||
|
<EmptyState data-testid="empty-state" variant="large">
|
||||||
|
<EmptyStateIcon icon={PlusCircleIcon} />
|
||||||
|
<Title headingLevel="h1" size="lg">
|
||||||
|
{t("emptyPermissions")}
|
||||||
|
</Title>
|
||||||
|
<EmptyStateBody>{t("emptyPermissionInstructions")}</EmptyStateBody>
|
||||||
|
<TooltipEmptyButton
|
||||||
|
permissionType="resource"
|
||||||
|
disabled={isResourceEnabled}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<TooltipEmptyButton
|
||||||
|
permissionType="scope"
|
||||||
|
disabled={isScopeEnabled}
|
||||||
|
clientId={clientId}
|
||||||
|
/>
|
||||||
|
</EmptyState>
|
||||||
|
);
|
||||||
|
};
|
349
src/clients/authorization/PermissionDetails.tsx
Normal file
349
src/clients/authorization/PermissionDetails.tsx
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Link, useHistory, useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
|
DropdownItem,
|
||||||
|
FormGroup,
|
||||||
|
PageSection,
|
||||||
|
Radio,
|
||||||
|
Switch,
|
||||||
|
TextArea,
|
||||||
|
TextInput,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||||
|
import type { NewPermissionParams } from "../routes/NewPermission";
|
||||||
|
import {
|
||||||
|
PermissionDetailsParams,
|
||||||
|
toPermissionDetails,
|
||||||
|
} from "../routes/PermissionDetails";
|
||||||
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||||
|
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { toClient } from "../routes/Client";
|
||||||
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
import { ResourcesPolicySelect } from "./ResourcesPolicySelect";
|
||||||
|
|
||||||
|
const DECISION_STRATEGIES = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const;
|
||||||
|
|
||||||
|
export default function PermissionDetails() {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
shouldUnregister: false,
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
const { register, control, reset, errors, handleSubmit } = form;
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
const { id, realm, permissionType, permissionId } = useParams<
|
||||||
|
NewPermissionParams & PermissionDetailsParams
|
||||||
|
>();
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const [permission, setPermission] = useState<PolicyRepresentation>();
|
||||||
|
const [applyToResourceTypeFlag, setApplyToResourceTypeFlag] = useState(false);
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
if (permissionId) {
|
||||||
|
const r = await Promise.all([
|
||||||
|
adminClient.clients.findOnePermission({
|
||||||
|
id,
|
||||||
|
type: permissionType,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
adminClient.clients.getAssociatedResources({
|
||||||
|
id,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
adminClient.clients.getAssociatedPolicies({
|
||||||
|
id,
|
||||||
|
permissionId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!r[0]) {
|
||||||
|
throw new Error(t("common:notFound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
permission: r[0],
|
||||||
|
resources: r[1].map((p) => p._id),
|
||||||
|
policies: r[2].map((p) => p.id!),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
({ permission, resources, policies }) => {
|
||||||
|
reset({ ...permission, resources, policies });
|
||||||
|
if (permission && "resourceType" in permission) {
|
||||||
|
setApplyToResourceTypeFlag(
|
||||||
|
!!(permission as { resourceType: string }).resourceType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setPermission({ ...permission, resources, policies });
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = async (permission: PolicyRepresentation) => {
|
||||||
|
try {
|
||||||
|
if (permissionId) {
|
||||||
|
await adminClient.clients.updatePermission(
|
||||||
|
{ id, type: permissionType, permissionId },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const result = await adminClient.clients.createPermission(
|
||||||
|
{ id, type: permissionType },
|
||||||
|
permission
|
||||||
|
);
|
||||||
|
history.push(
|
||||||
|
toPermissionDetails({
|
||||||
|
realm,
|
||||||
|
id,
|
||||||
|
permissionType,
|
||||||
|
permissionId: result.id!,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addAlert(
|
||||||
|
t((permissionId ? "update" : "create") + "PermissionSuccess"),
|
||||||
|
AlertVariant.success
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
addError("clients:permissionSaveError", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:deletePermission",
|
||||||
|
messageKey: t("deletePermissionConfirm", {
|
||||||
|
permission: permission?.name,
|
||||||
|
}),
|
||||||
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
|
continueButtonLabel: "clients:confirm",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await adminClient.clients.delPermission({
|
||||||
|
id,
|
||||||
|
type: permissionType,
|
||||||
|
permissionId: permissionId,
|
||||||
|
});
|
||||||
|
addAlert(t("permissionDeletedSuccess"), AlertVariant.success);
|
||||||
|
history.push(toClient({ realm, clientId: id, tab: "authorization" }));
|
||||||
|
} catch (error) {
|
||||||
|
addError("clients:permissionDeletedError", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteConfirm />
|
||||||
|
<ViewHeader
|
||||||
|
titleKey={permissionId ? permission?.name! : "clients:createPermission"}
|
||||||
|
dropdownItems={
|
||||||
|
permissionId
|
||||||
|
? [
|
||||||
|
<DropdownItem
|
||||||
|
key="delete"
|
||||||
|
data-testid="delete-resource"
|
||||||
|
onClick={() => toggleDeleteDialog()}
|
||||||
|
>
|
||||||
|
{t("common:delete")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PageSection variant="light">
|
||||||
|
<FormAccess
|
||||||
|
isHorizontal
|
||||||
|
role="manage-clients"
|
||||||
|
onSubmit={handleSubmit(save)}
|
||||||
|
>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<FormGroup
|
||||||
|
label={t("common:name")}
|
||||||
|
fieldId="name"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:permissionName"
|
||||||
|
fieldLabelId="name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput id="name" name="name" ref={register} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("common:description")}
|
||||||
|
fieldId="description"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:permissionDescription"
|
||||||
|
fieldLabelId="description"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
validated={errors.description ? "error" : "default"}
|
||||||
|
helperTextInvalid={errors.description?.message}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
ref={register({
|
||||||
|
maxLength: {
|
||||||
|
value: 255,
|
||||||
|
message: t("common:maxLength", { length: 255 }),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
validated={errors.description ? "error" : "default"}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("applyToResourceTypeFlag")}
|
||||||
|
fieldId="applyToResourceTypeFlag"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:applyToResourceTypeFlag"
|
||||||
|
fieldLabelId="clients:applyToResourceTypeFlag"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id="applyToResourceTypeFlag"
|
||||||
|
name="applyToResourceTypeFlag"
|
||||||
|
label={t("common:on")}
|
||||||
|
labelOff={t("common:off")}
|
||||||
|
isChecked={applyToResourceTypeFlag}
|
||||||
|
onChange={setApplyToResourceTypeFlag}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{applyToResourceTypeFlag ? (
|
||||||
|
<FormGroup
|
||||||
|
label={t("resourceType")}
|
||||||
|
fieldId="name"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:resourceType"
|
||||||
|
fieldLabelId="resourceType"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id="resourceType"
|
||||||
|
name="resourceType"
|
||||||
|
ref={register}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : (
|
||||||
|
<FormGroup
|
||||||
|
label={t("resources")}
|
||||||
|
fieldId="resources"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:permissionResources"
|
||||||
|
fieldLabelId="clients:resources"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ResourcesPolicySelect
|
||||||
|
name="resources"
|
||||||
|
searchFunction="listResources"
|
||||||
|
clientId={id}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
<FormGroup
|
||||||
|
label={t("policies")}
|
||||||
|
fieldId="policies"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:permissionPolicies"
|
||||||
|
fieldLabelId="clients:policies"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ResourcesPolicySelect
|
||||||
|
name="policies"
|
||||||
|
searchFunction="listPolicies"
|
||||||
|
clientId={id}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("decisionStrategy")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:permissionDecisionStrategy"
|
||||||
|
fieldLabelId="clients:decisionStrategy"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="policyEnforcementMode"
|
||||||
|
hasNoPaddingTop
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="decisionStrategy"
|
||||||
|
data-testid="decisionStrategy"
|
||||||
|
defaultValue={DECISION_STRATEGIES[0]}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<>
|
||||||
|
{DECISION_STRATEGIES.map((strategy) => (
|
||||||
|
<Radio
|
||||||
|
id={strategy}
|
||||||
|
key={strategy}
|
||||||
|
data-testid={strategy}
|
||||||
|
isChecked={value === strategy}
|
||||||
|
name="decisionStrategies"
|
||||||
|
onChange={() => onChange(strategy)}
|
||||||
|
label={t(`decisionStrategies.${strategy}`)}
|
||||||
|
className="pf-u-mb-md"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<ActionGroup>
|
||||||
|
<div className="pf-u-mt-md">
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.primary}
|
||||||
|
type="submit"
|
||||||
|
data-testid="save"
|
||||||
|
>
|
||||||
|
{t("common:save")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
data-testid="cancel"
|
||||||
|
component={(props) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
to={toClient({
|
||||||
|
realm,
|
||||||
|
clientId: id,
|
||||||
|
tab: "authorization",
|
||||||
|
})}
|
||||||
|
></Link>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormProvider>
|
||||||
|
</FormAccess>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
341
src/clients/authorization/Permissions.tsx
Normal file
341
src/clients/authorization/Permissions.tsx
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Link, useHistory } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
AlertVariant,
|
||||||
|
ButtonVariant,
|
||||||
|
DescriptionList,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownToggle,
|
||||||
|
PageSection,
|
||||||
|
ToolbarItem,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import {
|
||||||
|
ExpandableRowContent,
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
|
||||||
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||||
|
import type PolicyProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyProviderRepresentation";
|
||||||
|
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||||
|
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
import useToggle from "../../utils/useToggle";
|
||||||
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { SearchDropdown } from "./SearchDropdown";
|
||||||
|
import { MoreLabel } from "./MoreLabel";
|
||||||
|
import { DetailDescription } from "./DetailDescription";
|
||||||
|
import { EmptyPermissionsState } from "./EmptyPermissionsState";
|
||||||
|
import { toNewPermission } from "../routes/NewPermission";
|
||||||
|
import { toPermissionDetails } from "../routes/PermissionDetails";
|
||||||
|
|
||||||
|
import "./permissions.css";
|
||||||
|
|
||||||
|
type PermissionsProps = {
|
||||||
|
clientId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpandablePolicyRepresentation = PolicyRepresentation & {
|
||||||
|
associatedPolicies?: PolicyRepresentation[];
|
||||||
|
isExpanded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const history = useHistory();
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const { realm } = useRealm();
|
||||||
|
|
||||||
|
const [permissions, setPermissions] =
|
||||||
|
useState<ExpandablePolicyRepresentation[]>();
|
||||||
|
const [selectedPermission, setSelectedPermission] =
|
||||||
|
useState<PolicyRepresentation>();
|
||||||
|
const [policyProviders, setPolicyProviders] =
|
||||||
|
useState<PolicyProviderRepresentation[]>();
|
||||||
|
const [disabledCreate, setDisabledCreate] =
|
||||||
|
useState<{ resources: boolean; scopes: boolean }>();
|
||||||
|
const [createOpen, toggleCreate] = useToggle();
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const refresh = () => setKey(key + 1);
|
||||||
|
|
||||||
|
const [max, setMax] = useState(10);
|
||||||
|
const [first, setFirst] = useState(0);
|
||||||
|
|
||||||
|
const AssociatedPoliciesRenderer = ({
|
||||||
|
row,
|
||||||
|
}: {
|
||||||
|
row: ExpandablePolicyRepresentation;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{row.associatedPolicies?.[0]?.name}{" "}
|
||||||
|
<MoreLabel array={row.associatedPolicies} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
const permissions = await adminClient.clients.findPermissions({
|
||||||
|
first,
|
||||||
|
max,
|
||||||
|
id: clientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
permissions.map(async (permission) => {
|
||||||
|
const associatedPolicies =
|
||||||
|
await adminClient.clients.getAssociatedPolicies({
|
||||||
|
id: clientId,
|
||||||
|
permissionId: permission.id!,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...permission,
|
||||||
|
associatedPolicies,
|
||||||
|
isExpanded: false,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setPermissions,
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
const params = {
|
||||||
|
first: 0,
|
||||||
|
max: 1,
|
||||||
|
};
|
||||||
|
const [policies, resources, scopes] = await Promise.all([
|
||||||
|
adminClient.clients.listPolicyProviders({
|
||||||
|
id: clientId,
|
||||||
|
}),
|
||||||
|
adminClient.clients.listResources({ ...params, id: clientId }),
|
||||||
|
adminClient.clients.listAllScopes({ ...params, id: clientId }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
policies: policies.filter(
|
||||||
|
(p) => p.type === "resource" || p.type === "scope"
|
||||||
|
),
|
||||||
|
resources: resources.length !== 1,
|
||||||
|
scopes: scopes.length !== 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
({ policies, resources, scopes }) => {
|
||||||
|
setPolicyProviders(policies);
|
||||||
|
setDisabledCreate({ resources, scopes });
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:deletePermission",
|
||||||
|
messageKey: t("deletePermissionConfirm", {
|
||||||
|
permission: selectedPermission?.name,
|
||||||
|
}),
|
||||||
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
|
continueButtonLabel: "clients:confirm",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await adminClient.clients.delPermission({
|
||||||
|
id: clientId,
|
||||||
|
type: selectedPermission?.type!,
|
||||||
|
permissionId: selectedPermission?.id!,
|
||||||
|
});
|
||||||
|
addAlert(t("permissionDeletedSuccess"), AlertVariant.success);
|
||||||
|
refresh();
|
||||||
|
} catch (error) {
|
||||||
|
addError("clients:permissionDeletedError", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!permissions) {
|
||||||
|
return <KeycloakSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
|
<DeleteConfirm />
|
||||||
|
{permissions.length > 0 && (
|
||||||
|
<PaginatingTableToolbar
|
||||||
|
count={permissions.length}
|
||||||
|
first={first}
|
||||||
|
max={max}
|
||||||
|
onNextClick={setFirst}
|
||||||
|
onPreviousClick={setFirst}
|
||||||
|
onPerPageSelect={(first, max) => {
|
||||||
|
setFirst(first);
|
||||||
|
setMax(max);
|
||||||
|
}}
|
||||||
|
toolbarItem={
|
||||||
|
<>
|
||||||
|
<ToolbarItem>
|
||||||
|
<SearchDropdown types={policyProviders} />
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Dropdown
|
||||||
|
toggle={
|
||||||
|
<DropdownToggle
|
||||||
|
onToggle={toggleCreate}
|
||||||
|
isPrimary
|
||||||
|
data-testid="permissionCreateDropdown"
|
||||||
|
>
|
||||||
|
{t("createPermission")}
|
||||||
|
</DropdownToggle>
|
||||||
|
}
|
||||||
|
isOpen={createOpen}
|
||||||
|
dropdownItems={[
|
||||||
|
<DropdownItem
|
||||||
|
data-testid="create-resource"
|
||||||
|
key="createResourceBasedPermission"
|
||||||
|
isDisabled={disabledCreate?.resources}
|
||||||
|
component="button"
|
||||||
|
onClick={() =>
|
||||||
|
history.push(
|
||||||
|
toNewPermission({
|
||||||
|
realm,
|
||||||
|
id: clientId,
|
||||||
|
permissionType: "resource",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("createResourceBasedPermission")}
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
data-testid="create-scope"
|
||||||
|
key="createScopeBasedPermission"
|
||||||
|
isDisabled={disabledCreate?.scopes}
|
||||||
|
component="button"
|
||||||
|
onClick={() =>
|
||||||
|
history.push(
|
||||||
|
toNewPermission({
|
||||||
|
realm,
|
||||||
|
id: clientId,
|
||||||
|
permissionType: "scope",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("createScopeBasedPermission")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TableComposable aria-label={t("resources")} variant="compact">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th />
|
||||||
|
<Th>{t("common:name")}</Th>
|
||||||
|
<Th>{t("common:type")}</Th>
|
||||||
|
<Th>{t("associatedPolicy")}</Th>
|
||||||
|
<Th>{t("common:description")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
{permissions.map((permission, rowIndex) => (
|
||||||
|
<Tbody key={permission.id} isExpanded={permission.isExpanded}>
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
expand={{
|
||||||
|
rowIndex,
|
||||||
|
isExpanded: permission.isExpanded,
|
||||||
|
onToggle: (_, rowIndex) => {
|
||||||
|
const rows = permissions.map((p, index) =>
|
||||||
|
index === rowIndex
|
||||||
|
? { ...p, isExpanded: !p.isExpanded }
|
||||||
|
: p
|
||||||
|
);
|
||||||
|
setPermissions(rows);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Td data-testid={`name-column-${permission.name}`}>
|
||||||
|
<Link
|
||||||
|
to={toPermissionDetails({
|
||||||
|
realm,
|
||||||
|
id: clientId,
|
||||||
|
permissionType: permission.type!,
|
||||||
|
permissionId: permission.id!,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{permission.name}
|
||||||
|
</Link>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{
|
||||||
|
policyProviders?.find((p) => p.type === permission.type)
|
||||||
|
?.name
|
||||||
|
}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<AssociatedPoliciesRenderer row={permission} />
|
||||||
|
</Td>
|
||||||
|
<Td>{permission.description}</Td>
|
||||||
|
<Td
|
||||||
|
actions={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: t("common:delete"),
|
||||||
|
onClick: async () => {
|
||||||
|
setSelectedPermission(permission);
|
||||||
|
toggleDeleteDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
></Td>
|
||||||
|
</Tr>
|
||||||
|
<Tr
|
||||||
|
key={`child-${permission.id}`}
|
||||||
|
isExpanded={permission.isExpanded}
|
||||||
|
>
|
||||||
|
<Td />
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<ExpandableRowContent>
|
||||||
|
{permission.isExpanded && (
|
||||||
|
<DescriptionList
|
||||||
|
isHorizontal
|
||||||
|
className="keycloak_resource_details"
|
||||||
|
>
|
||||||
|
<DetailDescription
|
||||||
|
name="associatedPolicy"
|
||||||
|
array={permission.associatedPolicies}
|
||||||
|
convert={(p) => p.name!}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
)}
|
||||||
|
</ExpandableRowContent>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
))}
|
||||||
|
</TableComposable>
|
||||||
|
</PaginatingTableToolbar>
|
||||||
|
)}
|
||||||
|
{permissions.length === 0 && (
|
||||||
|
<EmptyPermissionsState
|
||||||
|
clientId={clientId}
|
||||||
|
isResourceEnabled={disabledCreate?.resources}
|
||||||
|
isScopeEnabled={disabledCreate?.scopes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
88
src/clients/authorization/ResourcesPolicySelect.tsx
Normal file
88
src/clients/authorization/ResourcesPolicySelect.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||||
|
import type { Clients } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||||
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
|
||||||
|
type ResourcesPolicySelectProps = {
|
||||||
|
name: string;
|
||||||
|
clientId: string;
|
||||||
|
searchFunction: keyof Pick<Clients, "listPolicies" | "listResources">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ResourcesPolicySelect = ({
|
||||||
|
name,
|
||||||
|
searchFunction,
|
||||||
|
clientId,
|
||||||
|
}: ResourcesPolicySelectProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
const { control } = useFormContext<PolicyRepresentation>();
|
||||||
|
const [items, setItems] = useState<JSX.Element[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () =>
|
||||||
|
(
|
||||||
|
await adminClient.clients[searchFunction](
|
||||||
|
Object.assign(
|
||||||
|
{ id: clientId, first: 0, max: 10 },
|
||||||
|
search === "" ? null : { name: search }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).map((p) => ({
|
||||||
|
id: "_id" in p ? p._id : "id" in p ? p.id : undefined,
|
||||||
|
name: p.name,
|
||||||
|
})),
|
||||||
|
(policies) =>
|
||||||
|
setItems(
|
||||||
|
policies.map((p) => (
|
||||||
|
<SelectOption key={p.id} value={p.id}>
|
||||||
|
{p.name}
|
||||||
|
</SelectOption>
|
||||||
|
))
|
||||||
|
),
|
||||||
|
[search]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
defaultValue={[]}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId={name}
|
||||||
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
onToggle={setOpen}
|
||||||
|
onFilter={(_, filter) => {
|
||||||
|
setSearch(filter);
|
||||||
|
return items;
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
onChange([]);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
selections={value}
|
||||||
|
onSelect={(_, selectedValue) => {
|
||||||
|
const option = selectedValue.toString();
|
||||||
|
const changedValue = value.find((p: string) => p === option)
|
||||||
|
? value.filter((p: string) => p !== option)
|
||||||
|
: [...value, option];
|
||||||
|
onChange(changedValue);
|
||||||
|
setSearch("");
|
||||||
|
}}
|
||||||
|
isOpen={open}
|
||||||
|
aria-labelledby={t("policies")}
|
||||||
|
>
|
||||||
|
{items}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,9 +4,6 @@ import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
DescriptionListDescription,
|
|
||||||
DescriptionListGroup,
|
|
||||||
DescriptionListTerm,
|
|
||||||
PageSection,
|
PageSection,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
@ -33,6 +30,7 @@ import { toNewScope } from "../routes/NewScope";
|
||||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||||
import useToggle from "../../utils/useToggle";
|
import useToggle from "../../utils/useToggle";
|
||||||
import { DeleteScopeDialog } from "./DeleteScopeDialog";
|
import { DeleteScopeDialog } from "./DeleteScopeDialog";
|
||||||
|
import { DetailDescription } from "./DetailDescription";
|
||||||
|
|
||||||
type ScopesProps = {
|
type ScopesProps = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
|
@ -225,39 +223,16 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
|
||||||
isHorizontal
|
isHorizontal
|
||||||
className="keycloak_resource_details"
|
className="keycloak_resource_details"
|
||||||
>
|
>
|
||||||
<DescriptionListGroup>
|
<DetailDescription
|
||||||
<DescriptionListTerm>
|
name="resources"
|
||||||
{t("resources")}
|
array={scope.resources}
|
||||||
</DescriptionListTerm>
|
convert={(r) => r.name!}
|
||||||
<DescriptionListDescription>
|
/>
|
||||||
{scope.resources?.map((resource) => (
|
<DetailDescription
|
||||||
<span key={resource._id} className="pf-u-pr-sm">
|
name="associatedPermissions"
|
||||||
{resource.name}
|
array={scope.permissions}
|
||||||
</span>
|
convert={(p) => p.name!}
|
||||||
))}
|
/>
|
||||||
{scope.resources?.length === 0 && (
|
|
||||||
<i>{t("common:none")}</i>
|
|
||||||
)}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
<DescriptionListGroup>
|
|
||||||
<DescriptionListTerm>
|
|
||||||
{t("associatedPermissions")}
|
|
||||||
</DescriptionListTerm>
|
|
||||||
<DescriptionListDescription>
|
|
||||||
{scope.permissions?.map((permission) => (
|
|
||||||
<span
|
|
||||||
key={permission.id}
|
|
||||||
className="pf-u-pr-sm"
|
|
||||||
>
|
|
||||||
{permission.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{scope.permissions?.length === 0 && (
|
|
||||||
<i>{t("common:none")}</i>
|
|
||||||
)}
|
|
||||||
</DescriptionListDescription>
|
|
||||||
</DescriptionListGroup>
|
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
)}
|
)}
|
||||||
</ExpandableRowContent>
|
</ExpandableRowContent>
|
||||||
|
|
93
src/clients/authorization/SearchDropdown.tsx
Normal file
93
src/clients/authorization/SearchDropdown.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownToggle,
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
TextInput,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type PolicyProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyProviderRepresentation";
|
||||||
|
import useToggle from "../../utils/useToggle";
|
||||||
|
|
||||||
|
import "./search-dropdown.css";
|
||||||
|
|
||||||
|
type SearchDropdownProps = {
|
||||||
|
types?: PolicyProviderRepresentation[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchDropdown = ({ types }: SearchDropdownProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { register, control } = useForm();
|
||||||
|
|
||||||
|
const [open, toggle] = useToggle();
|
||||||
|
const [typeOpen, toggleType] = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
data-testid="searchdropdown_dorpdown"
|
||||||
|
className="pf-u-ml-md"
|
||||||
|
toggle={
|
||||||
|
<DropdownToggle
|
||||||
|
onToggle={toggle}
|
||||||
|
className="keycloak__client_authentication__searchdropdown"
|
||||||
|
>
|
||||||
|
{t("searchForPermission")}
|
||||||
|
</DropdownToggle>
|
||||||
|
}
|
||||||
|
isOpen={open}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
isHorizontal
|
||||||
|
className="keycloak__client_authentication__searchdropdown_form"
|
||||||
|
>
|
||||||
|
<FormGroup label={t("common:name")} fieldId="name">
|
||||||
|
<TextInput
|
||||||
|
ref={register}
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
data-testid="searchdropdown_name"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={t("common:type")} fieldId="type">
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="type"
|
||||||
|
onToggle={toggleType}
|
||||||
|
onSelect={(event, value) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onChange(value);
|
||||||
|
toggleType();
|
||||||
|
}}
|
||||||
|
selections={value.name}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
aria-label={t("common:type")}
|
||||||
|
isOpen={typeOpen}
|
||||||
|
>
|
||||||
|
{types?.map((type) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={type.type === value.type}
|
||||||
|
key={type.type}
|
||||||
|
value={type}
|
||||||
|
>
|
||||||
|
{type.name}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -114,7 +114,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
<Controller
|
<Controller
|
||||||
name="policyEnforcementMode"
|
name="policyEnforcementMode"
|
||||||
data-testid="policyEnforcementMode"
|
data-testid="policyEnforcementMode"
|
||||||
defaultValue={DECISION_STRATEGY[0]}
|
defaultValue={POLICY_ENFORCEMENT_MODES[0]}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<>
|
<>
|
||||||
|
|
5
src/clients/authorization/permissions.css
Normal file
5
src/clients/authorization/permissions.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.pf-c-button.keycloak__permissions__empty_state {
|
||||||
|
color: var(--pf-c-button--disabled--Color);
|
||||||
|
background-color: var(--pf-c-button--disabled--BackgroundColor);
|
||||||
|
--pf-c-button--after--BorderWidth: 0;
|
||||||
|
}
|
9
src/clients/authorization/search-dropdown.css
Normal file
9
src/clients/authorization/search-dropdown.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.keycloak__client_authentication__searchdropdown {
|
||||||
|
--pf-c-dropdown__toggle--MinWidth: 21rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keycloak__client_authentication__searchdropdown_form {
|
||||||
|
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 5rem;
|
||||||
|
--pf-c-form--m-horizontal__group-control--md--GridColumnWidth: 24rem;
|
||||||
|
margin: 0 var(--pf-global--spacer--lg) var(--pf-global--spacer--lg) var(--pf-global--spacer--lg);
|
||||||
|
}
|
|
@ -180,5 +180,16 @@ export default {
|
||||||
"A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.",
|
"A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.",
|
||||||
scopeDisplayName:
|
scopeDisplayName:
|
||||||
"A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.",
|
"A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.",
|
||||||
|
|
||||||
|
permissionName: "The name of this permission.",
|
||||||
|
permissionDescription: "A description for this permission.",
|
||||||
|
applyToResourceTypeFlag:
|
||||||
|
"Specifies if this permission should be applied to all resources with a given type. In this case, this permission will be evaluated for all instances of a given resource type.",
|
||||||
|
permissionResources:
|
||||||
|
"Specifies that this permission must be applied to a specific resource instance.",
|
||||||
|
permissionType:
|
||||||
|
"Specifies that this permission must be applied to all resources instances of a given type.",
|
||||||
|
permissionDecisionStrategy:
|
||||||
|
"The decision strategy dictates how the policies associated with a given permission are evaluated and how a final decision is obtained. 'Affirmative' means that at least one policy must evaluate to a positive decision in order for the final decision to be also positive. 'Unanimous' means that all policies must evaluate to a positive decision in order for the final decision to be also positive. 'Consensus' means that the number of positive decisions must be greater than the number of negative decisions. If the number of positive and negative is the same, the final decision will be negative.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default {
|
||||||
decisionStrategies: {
|
decisionStrategies: {
|
||||||
UNANIMOUS: "Unanimous",
|
UNANIMOUS: "Unanimous",
|
||||||
AFFIRMATIVE: "Affirmative",
|
AFFIRMATIVE: "Affirmative",
|
||||||
|
CONSENSUS: "Consensus",
|
||||||
},
|
},
|
||||||
importResources: "The following settings and data will be imported:",
|
importResources: "The following settings and data will be imported:",
|
||||||
importWarning:
|
importWarning:
|
||||||
|
@ -67,7 +68,15 @@ export default {
|
||||||
importResourceSuccess: "The resource was successfully imported",
|
importResourceSuccess: "The resource was successfully imported",
|
||||||
importResourceError: "Could not import the resource due to {{error}}",
|
importResourceError: "Could not import the resource due to {{error}}",
|
||||||
createResource: "Create resource",
|
createResource: "Create resource",
|
||||||
|
emptyPermissions: "No permissions",
|
||||||
|
emptyPermissionInstructions:
|
||||||
|
"If you want to create a permission, please click the button below to create a resource-based or scope-based permission.",
|
||||||
|
noScopeCreateHint:
|
||||||
|
"There is no authorization scope you can't create scope-based permission",
|
||||||
|
noResourceCreateHint:
|
||||||
|
"There are no resources you can't create resource-based permission",
|
||||||
createResourceBasedPermission: "Create resource-based permission",
|
createResourceBasedPermission: "Create resource-based permission",
|
||||||
|
createScopeBasedPermission: "Create scope-based permission",
|
||||||
displayName: "Display name",
|
displayName: "Display name",
|
||||||
type: "Type",
|
type: "Type",
|
||||||
addUri: "Add URI",
|
addUri: "Add URI",
|
||||||
|
@ -86,6 +95,7 @@ export default {
|
||||||
scopes: "Scopes",
|
scopes: "Scopes",
|
||||||
policies: "Policies",
|
policies: "Policies",
|
||||||
createPermission: "Create permission",
|
createPermission: "Create permission",
|
||||||
|
permissionDetails: "Permission details",
|
||||||
deleteResource: "Permanently delete resource?",
|
deleteResource: "Permanently delete resource?",
|
||||||
deleteResourceConfirm:
|
deleteResourceConfirm:
|
||||||
"If you delete this resource, some permissions will be affected.",
|
"If you delete this resource, some permissions will be affected.",
|
||||||
|
@ -93,6 +103,8 @@ export default {
|
||||||
"The permissions below will be removed when they are no longer used by other resources:",
|
"The permissions below will be removed when they are no longer used by other resources:",
|
||||||
resourceDeletedSuccess: "The resource successfully deleted",
|
resourceDeletedSuccess: "The resource successfully deleted",
|
||||||
resourceDeletedError: "Could not remove the resource {{error}}",
|
resourceDeletedError: "Could not remove the resource {{error}}",
|
||||||
|
permissions: "Permissions",
|
||||||
|
searchForPermission: "Search for permission",
|
||||||
deleteScope: "Permanently delete authorization scope?",
|
deleteScope: "Permanently delete authorization scope?",
|
||||||
deleteScopeConfirm:
|
deleteScopeConfirm:
|
||||||
"If you delete this authorization scope, some permissions will be affected.",
|
"If you delete this authorization scope, some permissions will be affected.",
|
||||||
|
@ -101,8 +113,18 @@ export default {
|
||||||
resourceScopeSuccess: "The authorization scope successfully deleted",
|
resourceScopeSuccess: "The authorization scope successfully deleted",
|
||||||
resourceScopeError:
|
resourceScopeError:
|
||||||
"Could not remove the authorization scope due to {{error}}",
|
"Could not remove the authorization scope due to {{error}}",
|
||||||
|
associatedPolicy: "Associated policy",
|
||||||
|
deletePermission: "Permanently delete permission?",
|
||||||
|
deletePermissionConfirm:
|
||||||
|
"Are you sure you want to delete the permission {{permission}}",
|
||||||
|
permissionDeletedSuccess: "Successfully deleted permission",
|
||||||
|
permissionDeletedError: "Could not delete permission due to {{error}}",
|
||||||
|
applyToResourceTypeFlag: "Apply to resource type",
|
||||||
|
resourceType: "Resource type",
|
||||||
|
createPermissionSuccess: "Successfully created the permission",
|
||||||
|
updatePermissionSuccess: "Successfully updated the permission",
|
||||||
|
permissionSaveError: "Could not update the permission due to {{error}}",
|
||||||
createAuthorizationScope: "Create authorization scope",
|
createAuthorizationScope: "Create authorization scope",
|
||||||
permissions: "Permissions",
|
|
||||||
emptyAuthorizationScopes: "No authorization scopes",
|
emptyAuthorizationScopes: "No authorization scopes",
|
||||||
emptyAuthorizationInstructions:
|
emptyAuthorizationInstructions:
|
||||||
"If you want to create authorization scopes, please click the button below to create the authorization scope",
|
"If you want to create authorization scopes, please click the button below to create the authorization scope",
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { NewResourceRoute } from "./routes/NewResource";
|
||||||
import { ResourceDetailsRoute } from "./routes/Resource";
|
import { ResourceDetailsRoute } from "./routes/Resource";
|
||||||
import { NewScopeRoute } from "./routes/NewScope";
|
import { NewScopeRoute } from "./routes/NewScope";
|
||||||
import { ScopeDetailsRoute } from "./routes/Scope";
|
import { ScopeDetailsRoute } from "./routes/Scope";
|
||||||
|
import { NewPermissionRoute } from "./routes/NewPermission";
|
||||||
|
import { PermissionDetailsRoute } from "./routes/PermissionDetails";
|
||||||
|
|
||||||
const routes: RouteDef[] = [
|
const routes: RouteDef[] = [
|
||||||
AddClientRoute,
|
AddClientRoute,
|
||||||
|
@ -21,6 +23,8 @@ const routes: RouteDef[] = [
|
||||||
ResourceDetailsRoute,
|
ResourceDetailsRoute,
|
||||||
NewScopeRoute,
|
NewScopeRoute,
|
||||||
ScopeDetailsRoute,
|
ScopeDetailsRoute,
|
||||||
|
NewPermissionRoute,
|
||||||
|
PermissionDetailsRoute,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
25
src/clients/routes/NewPermission.ts
Normal file
25
src/clients/routes/NewPermission.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
export type PermissionType = "resource" | "scope";
|
||||||
|
|
||||||
|
export type NewPermissionParams = {
|
||||||
|
realm: string;
|
||||||
|
id: string;
|
||||||
|
permissionType: PermissionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewPermissionRoute: RouteDef = {
|
||||||
|
path: "/:realm/clients/:id/authorization/permission/new/:permissionType",
|
||||||
|
component: lazy(() => import("../authorization/PermissionDetails")),
|
||||||
|
breadcrumb: (t) => t("clients:createPermission"),
|
||||||
|
access: "manage-clients",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toNewPermission = (
|
||||||
|
params: NewPermissionParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(NewPermissionRoute.path, params),
|
||||||
|
});
|
25
src/clients/routes/PermissionDetails.ts
Normal file
25
src/clients/routes/PermissionDetails.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import { lazy } from "react";
|
||||||
|
import type { PermissionType } from "./NewPermission";
|
||||||
|
|
||||||
|
export type PermissionDetailsParams = {
|
||||||
|
realm: string;
|
||||||
|
id: string;
|
||||||
|
permissionType: string | PermissionType;
|
||||||
|
permissionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PermissionDetailsRoute: RouteDef = {
|
||||||
|
path: "/:realm/clients/:id/authorization/permission/:permissionType/:permissionId",
|
||||||
|
component: lazy(() => import("../authorization/PermissionDetails")),
|
||||||
|
breadcrumb: (t) => t("clients:permissionDetails"),
|
||||||
|
access: "manage-clients",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toPermissionDetails = (
|
||||||
|
params: PermissionDetailsParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(PermissionDetailsRoute.path, params),
|
||||||
|
});
|
Loading…
Reference in a new issue