Added the policy screens (#1884)
* initial policies tab * added initial edit create screen * removed switch in favour of mapping list * added pickers * added groups * added regex * added role select component * added time * added js type * fixed create route * fixed details * added tests * changed table header to required * added user type * added missing validation message
This commit is contained in:
parent
3f7a912c2a
commit
109c255d90
27 changed files with 2104 additions and 44 deletions
|
@ -8,6 +8,7 @@ import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||||
import Masthead from "../support/pages/admin_console/Masthead";
|
import Masthead from "../support/pages/admin_console/Masthead";
|
||||||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||||
import AuthorizationTab from "../support/pages/admin_console/manage/clients/AuthorizationTab";
|
import AuthorizationTab from "../support/pages/admin_console/manage/clients/AuthorizationTab";
|
||||||
|
import ModalUtils from "../support/util/ModalUtils";
|
||||||
|
|
||||||
describe("Client authentication subtab", () => {
|
describe("Client authentication subtab", () => {
|
||||||
const adminClient = new AdminClient();
|
const adminClient = new AdminClient();
|
||||||
|
@ -30,6 +31,9 @@ describe("Client authentication subtab", () => {
|
||||||
});
|
});
|
||||||
keycloakBefore();
|
keycloakBefore();
|
||||||
loginPage.logIn();
|
loginPage.logIn();
|
||||||
|
sidebarPage.goToClients();
|
||||||
|
listingPage.searchItem(clientId).goToItemDetails(clientId);
|
||||||
|
authenticationTab.goToAuthenticationTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
after(() => {
|
||||||
|
@ -38,9 +42,6 @@ describe("Client authentication subtab", () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
keycloakBeforeEach();
|
keycloakBeforeEach();
|
||||||
sidebarPage.goToClients();
|
|
||||||
listingPage.searchItem(clientId).goToItemDetails(clientId);
|
|
||||||
authenticationTab.goToAuthenticationTab();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should update the resource server settings", () => {
|
it("Should update the resource server settings", () => {
|
||||||
|
@ -63,6 +64,7 @@ describe("Client authentication subtab", () => {
|
||||||
.save();
|
.save();
|
||||||
|
|
||||||
masthead.checkNotificationMessage("Resource created successfully");
|
masthead.checkNotificationMessage("Resource created successfully");
|
||||||
|
authenticationTab.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should create a scope", () => {
|
it("Should create a scope", () => {
|
||||||
|
@ -83,6 +85,55 @@ describe("Client authentication subtab", () => {
|
||||||
listingPage.itemExist("The scope");
|
listingPage.itemExist("The scope");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should create a policy", () => {
|
||||||
|
authenticationTab.goToPolicySubTab();
|
||||||
|
cy.intercept(
|
||||||
|
"GET",
|
||||||
|
"/auth/admin/realms/master/clients/*/authz/resource-server/policy/regex/*"
|
||||||
|
).as("get");
|
||||||
|
authenticationTab
|
||||||
|
.goToCreatePolicy("regex")
|
||||||
|
.fillBasePolicyForm({
|
||||||
|
name: "Regex policy",
|
||||||
|
description: "Policy for regex",
|
||||||
|
targetClaim: "I don't know",
|
||||||
|
regexPattern: ".*?",
|
||||||
|
})
|
||||||
|
.save();
|
||||||
|
|
||||||
|
cy.wait(["@get"]);
|
||||||
|
masthead.checkNotificationMessage("Successfully created the policy");
|
||||||
|
authenticationTab.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should delete a policy", () => {
|
||||||
|
authenticationTab.goToPolicySubTab();
|
||||||
|
listingPage.deleteItem("Regex policy");
|
||||||
|
new ModalUtils().confirmModal();
|
||||||
|
|
||||||
|
masthead.checkNotificationMessage("The Policy successfully deleted");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should create a client policy", () => {
|
||||||
|
authenticationTab.goToPolicySubTab();
|
||||||
|
cy.intercept(
|
||||||
|
"GET",
|
||||||
|
"/auth/admin/realms/master/clients/*/authz/resource-server/policy/client/*"
|
||||||
|
).as("get");
|
||||||
|
authenticationTab
|
||||||
|
.goToCreatePolicy("client")
|
||||||
|
.fillBasePolicyForm({
|
||||||
|
name: "Client policy",
|
||||||
|
description: "Extra client field",
|
||||||
|
})
|
||||||
|
.inputClient("master-realm")
|
||||||
|
.save();
|
||||||
|
|
||||||
|
cy.wait(["@get"]);
|
||||||
|
masthead.checkNotificationMessage("Successfully created the policy");
|
||||||
|
authenticationTab.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
it("Should create a permission", () => {
|
it("Should create a permission", () => {
|
||||||
authenticationTab.goToPermissionsSubTab();
|
authenticationTab.goToPermissionsSubTab();
|
||||||
authenticationTab
|
authenticationTab
|
||||||
|
|
|
@ -8,8 +8,11 @@ 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 scopeTabName = "#pf-tab-42-scopes";
|
||||||
private permissionsTabName = "#pf-tab-43-permissions";
|
private policyTabName = "#pf-tab-43-policies";
|
||||||
|
private permissionsTabName = "#pf-tab-44-permissions";
|
||||||
private nameColumnPrefix = "name-column-";
|
private nameColumnPrefix = "name-column-";
|
||||||
|
private emptyPolicyCreateButton = "no-policies-empty-action";
|
||||||
|
private createPolicyButton = "createPolicy";
|
||||||
private createResourceButton = "createResource";
|
private createResourceButton = "createResource";
|
||||||
private createScopeButton = "no-authorization-scopes-empty-action";
|
private createScopeButton = "no-authorization-scopes-empty-action";
|
||||||
private createPermissionDropdown = "permissionCreateDropdown";
|
private createPermissionDropdown = "permissionCreateDropdown";
|
||||||
|
@ -30,6 +33,11 @@ export default class AuthorizationTab {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToPolicySubTab() {
|
||||||
|
cy.get(this.policyTabName).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
goToPermissionsSubTab() {
|
goToPermissionsSubTab() {
|
||||||
cy.get(this.permissionsTabName).click();
|
cy.get(this.permissionsTabName).click();
|
||||||
return this;
|
return this;
|
||||||
|
@ -45,12 +53,35 @@ export default class AuthorizationTab {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToCreatePolicy(type: string, first: boolean | undefined = false) {
|
||||||
|
if (first) {
|
||||||
|
cy.findByTestId(this.emptyPolicyCreateButton).click();
|
||||||
|
} else {
|
||||||
|
cy.findByTestId(this.createPolicyButton).click();
|
||||||
|
}
|
||||||
|
cy.findByTestId(type).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
goToCreatePermission(type: PermissionType) {
|
goToCreatePermission(type: PermissionType) {
|
||||||
cy.findByTestId(this.createPermissionDropdown).click();
|
cy.findByTestId(this.createPermissionDropdown).click();
|
||||||
cy.findByTestId(`create-${type}`).click();
|
cy.findByTestId(`create-${type}`).click();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillBasePolicyForm(policy: { [key: string]: string }) {
|
||||||
|
Object.entries(policy).map(([key, value]) =>
|
||||||
|
cy.findByTestId(key).type(value)
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
inputClient(clientName: string) {
|
||||||
|
cy.get("#clients").click();
|
||||||
|
cy.get("ul li").contains(clientName).click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
fillResourceForm(resource: ResourceRepresentation) {
|
fillResourceForm(resource: ResourceRepresentation) {
|
||||||
Object.entries(resource).map(([key, value]) => {
|
Object.entries(resource).map(([key, value]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@ -98,6 +129,11 @@ export default class AuthorizationTab {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
cy.findByTestId("cancel").click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
cy.findByTestId("authenticationSettingsSave").click();
|
cy.findByTestId("authenticationSettingsSave").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 { AuthorizationPolicies } from "./authorization/Policies";
|
||||||
import { AuthorizationPermissions } from "./authorization/Permissions";
|
import { AuthorizationPermissions } from "./authorization/Permissions";
|
||||||
|
|
||||||
type ClientDetailHeaderProps = {
|
type ClientDetailHeaderProps = {
|
||||||
|
@ -494,8 +495,15 @@ export default function ClientDetails() {
|
||||||
<AuthorizationScopes clientId={clientId} />
|
<AuthorizationScopes clientId={clientId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
id="permissions"
|
id="policies"
|
||||||
eventKey={43}
|
eventKey={43}
|
||||||
|
title={<TabTitleText>{t("policies")}</TabTitleText>}
|
||||||
|
>
|
||||||
|
<AuthorizationPolicies clientId={clientId} />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
id="permissions"
|
||||||
|
eventKey={44}
|
||||||
title={<TabTitleText>{t("permissions")}</TabTitleText>}
|
title={<TabTitleText>{t("permissions")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<AuthorizationPermissions clientId={clientId} />
|
<AuthorizationPermissions clientId={clientId} />
|
||||||
|
|
60
src/clients/authorization/DecisionStragegySelect.tsx
Normal file
60
src/clients/authorization/DecisionStragegySelect.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { FormGroup, Radio } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
const DECISION_STRATEGY = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const;
|
||||||
|
|
||||||
|
type DecisionStrategySelectProps = {
|
||||||
|
helpLabel?: string;
|
||||||
|
isLimited?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DecisionStrategySelect = ({
|
||||||
|
helpLabel,
|
||||||
|
isLimited = false,
|
||||||
|
}: DecisionStrategySelectProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control } = useFormContext();
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("decisionStrategy")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={`clients-help:${helpLabel || "decisionStrategy"}`}
|
||||||
|
fieldLabelId="clients:decisionStrategy"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="decisionStrategy"
|
||||||
|
hasNoPaddingTop
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="decisionStrategy"
|
||||||
|
data-testid="decisionStrategy"
|
||||||
|
defaultValue={DECISION_STRATEGY[0]}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<>
|
||||||
|
{(isLimited
|
||||||
|
? DECISION_STRATEGY.slice(0, 2)
|
||||||
|
: DECISION_STRATEGY
|
||||||
|
).map((strategy) => (
|
||||||
|
<Radio
|
||||||
|
id={strategy}
|
||||||
|
key={strategy}
|
||||||
|
data-testid={strategy}
|
||||||
|
isChecked={value === strategy}
|
||||||
|
name="decisionStrategy"
|
||||||
|
onChange={() => onChange(strategy)}
|
||||||
|
label={t(`decisionStrategies.${strategy}`)}
|
||||||
|
className="pf-u-mb-md"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
70
src/clients/authorization/NewPolicyDialog.tsx
Normal file
70
src/clients/authorization/NewPolicyDialog.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalVariant,
|
||||||
|
TextContent,
|
||||||
|
Text,
|
||||||
|
TextVariants,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
|
||||||
|
import type PolicyProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyProviderRepresentation";
|
||||||
|
|
||||||
|
type NewPolicyDialogProps = {
|
||||||
|
policyProviders?: PolicyProviderRepresentation[];
|
||||||
|
toggleDialog: () => void;
|
||||||
|
onSelect: (provider: PolicyProviderRepresentation) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewPolicyDialog = ({
|
||||||
|
policyProviders,
|
||||||
|
onSelect,
|
||||||
|
toggleDialog,
|
||||||
|
}: NewPolicyDialogProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby={t("addPredefinedMappers")}
|
||||||
|
variant={ModalVariant.medium}
|
||||||
|
header={
|
||||||
|
<TextContent>
|
||||||
|
<Text component={TextVariants.h1}>{t("chooseAPolicyType")}</Text>
|
||||||
|
<Text>{t("chooseAPolicyTypeInstructions")}</Text>
|
||||||
|
</TextContent>
|
||||||
|
}
|
||||||
|
isOpen
|
||||||
|
onClose={toggleDialog}
|
||||||
|
>
|
||||||
|
<TableComposable aria-label={t("policies")} variant="compact">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t("common:name")}</Th>
|
||||||
|
<Th>{t("common:description")}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{policyProviders?.map((provider) => (
|
||||||
|
<Tr
|
||||||
|
key={provider.type}
|
||||||
|
data-testid={provider.type}
|
||||||
|
onRowClick={() => onSelect(provider)}
|
||||||
|
isHoverable
|
||||||
|
>
|
||||||
|
<Td>{provider.name}</Td>
|
||||||
|
<Td>{t(`policyProvider.${provider.type}`)}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
304
src/clients/authorization/Policies.tsx
Normal file
304
src/clients/authorization/Policies.tsx
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Link, useHistory } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
DescriptionList,
|
||||||
|
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 { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { toPolicyDetails } from "../routes/PolicyDetails";
|
||||||
|
import { MoreLabel } from "./MoreLabel";
|
||||||
|
import { toUpperCase } from "../../util";
|
||||||
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||||
|
import useToggle from "../../utils/useToggle";
|
||||||
|
import { NewPolicyDialog } from "./NewPolicyDialog";
|
||||||
|
import { toCreatePolicy } from "../routes/NewPolicy";
|
||||||
|
import { DetailDescription } from "./DetailDescription";
|
||||||
|
|
||||||
|
type PoliciesProps = {
|
||||||
|
clientId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpandablePolicyRepresentation = PolicyRepresentation & {
|
||||||
|
dependentPolicies?: PolicyRepresentation[];
|
||||||
|
isExpanded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const { realm } = useRealm();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [policies, setPolicies] = useState<ExpandablePolicyRepresentation[]>();
|
||||||
|
const [selectedPolicy, setSelectedPolicy] =
|
||||||
|
useState<ExpandablePolicyRepresentation>();
|
||||||
|
const [policyProviders, setPolicyProviders] =
|
||||||
|
useState<PolicyProviderRepresentation[]>();
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const refresh = () => setKey(key + 1);
|
||||||
|
|
||||||
|
const [max, setMax] = useState(10);
|
||||||
|
const [first, setFirst] = useState(0);
|
||||||
|
const [newDialog, toggleDialog] = useToggle();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
const policies = await adminClient.clients.listPolicies({
|
||||||
|
first,
|
||||||
|
max,
|
||||||
|
id: clientId,
|
||||||
|
permission: "false",
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all([
|
||||||
|
adminClient.clients.listPolicyProviders({ id: clientId }),
|
||||||
|
...policies.map(async (policy) => {
|
||||||
|
const dependentPolicies =
|
||||||
|
await adminClient.clients.listDependentPolicies({
|
||||||
|
id: clientId,
|
||||||
|
policyId: policy.id!,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...policy,
|
||||||
|
dependentPolicies,
|
||||||
|
isExpanded: false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
([providers, ...policies]) => {
|
||||||
|
setPolicyProviders(
|
||||||
|
providers.filter((p) => p.type !== "resource" && p.type !== "scope")
|
||||||
|
);
|
||||||
|
setPolicies(policies);
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
|
||||||
|
const DependentPoliciesRenderer = ({
|
||||||
|
row,
|
||||||
|
}: {
|
||||||
|
row: ExpandablePolicyRepresentation;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{row.dependentPolicies?.[0]?.name}{" "}
|
||||||
|
<MoreLabel array={row.dependentPolicies} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:deletePolicy",
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{t("deletePolicyConfirm")}
|
||||||
|
{selectedPolicy?.dependentPolicies &&
|
||||||
|
selectedPolicy.dependentPolicies.length > 0 && (
|
||||||
|
<Alert
|
||||||
|
variant="warning"
|
||||||
|
isInline
|
||||||
|
isPlain
|
||||||
|
title={t("deletePolicyWarning")}
|
||||||
|
className="pf-u-pt-lg"
|
||||||
|
>
|
||||||
|
<p className="pf-u-pt-xs">
|
||||||
|
{selectedPolicy.dependentPolicies.map((policy) => (
|
||||||
|
<strong key={policy.id} className="pf-u-pr-md">
|
||||||
|
{policy.name}
|
||||||
|
</strong>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
continueButtonLabel: "clients:confirm",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await adminClient.clients.delPolicy({
|
||||||
|
id: clientId,
|
||||||
|
policyId: selectedPolicy?.id!,
|
||||||
|
});
|
||||||
|
addAlert(t("policyDeletedSuccess"), AlertVariant.success);
|
||||||
|
refresh();
|
||||||
|
} catch (error) {
|
||||||
|
addError("clients:policyDeletedError", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!policies) {
|
||||||
|
return <KeycloakSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection variant="light" className="pf-u-p-0">
|
||||||
|
<DeleteConfirm />
|
||||||
|
{policies.length > 0 && (
|
||||||
|
<>
|
||||||
|
{newDialog && (
|
||||||
|
<NewPolicyDialog
|
||||||
|
policyProviders={policyProviders}
|
||||||
|
onSelect={(p) =>
|
||||||
|
history.push(
|
||||||
|
toCreatePolicy({ id: clientId, realm, policyType: p.type! })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toggleDialog={toggleDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginatingTableToolbar
|
||||||
|
count={policies.length}
|
||||||
|
first={first}
|
||||||
|
max={max}
|
||||||
|
onNextClick={setFirst}
|
||||||
|
onPreviousClick={setFirst}
|
||||||
|
onPerPageSelect={(first, max) => {
|
||||||
|
setFirst(first);
|
||||||
|
setMax(max);
|
||||||
|
}}
|
||||||
|
toolbarItem={
|
||||||
|
<ToolbarItem>
|
||||||
|
<Button data-testid="createPolicy" onClick={toggleDialog}>
|
||||||
|
{t("createPolicy")}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TableComposable aria-label={t("resources")} variant="compact">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th />
|
||||||
|
<Th>{t("common:name")}</Th>
|
||||||
|
<Th>{t("common:type")}</Th>
|
||||||
|
<Th>{t("dependentPermission")}</Th>
|
||||||
|
<Th>{t("common:description")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
{policies.map((policy, rowIndex) => (
|
||||||
|
<Tbody key={policy.id} isExpanded={policy.isExpanded}>
|
||||||
|
<Tr>
|
||||||
|
<Td
|
||||||
|
expand={{
|
||||||
|
rowIndex,
|
||||||
|
isExpanded: policy.isExpanded,
|
||||||
|
onToggle: (_, rowIndex) => {
|
||||||
|
const rows = policies.map((policy, index) =>
|
||||||
|
index === rowIndex
|
||||||
|
? { ...policy, isExpanded: !policy.isExpanded }
|
||||||
|
: policy
|
||||||
|
);
|
||||||
|
setPolicies(rows);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Td data-testid={`name-column-${policy.name}`}>
|
||||||
|
<Link
|
||||||
|
to={toPolicyDetails({
|
||||||
|
realm,
|
||||||
|
id: clientId,
|
||||||
|
policyType: policy.type!,
|
||||||
|
policyId: policy.id!,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{policy.name}
|
||||||
|
</Link>
|
||||||
|
</Td>
|
||||||
|
<Td>{toUpperCase(policy.type!)}</Td>
|
||||||
|
<Td>
|
||||||
|
<DependentPoliciesRenderer row={policy} />
|
||||||
|
</Td>
|
||||||
|
<Td>{policy.description}</Td>
|
||||||
|
<Td
|
||||||
|
actions={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: t("common:delete"),
|
||||||
|
onClick: async () => {
|
||||||
|
setSelectedPolicy(policy);
|
||||||
|
toggleDeleteDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tr>
|
||||||
|
<Tr key={`child-${policy.id}`} isExpanded={policy.isExpanded}>
|
||||||
|
<Td />
|
||||||
|
<Td colSpan={4}>
|
||||||
|
<ExpandableRowContent>
|
||||||
|
{policy.isExpanded && (
|
||||||
|
<DescriptionList
|
||||||
|
isHorizontal
|
||||||
|
className="keycloak_resource_details"
|
||||||
|
>
|
||||||
|
<DetailDescription
|
||||||
|
name="dependentPermission"
|
||||||
|
array={policy.dependentPolicies}
|
||||||
|
convert={(p) => p.name!}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
)}
|
||||||
|
</ExpandableRowContent>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
))}
|
||||||
|
</TableComposable>
|
||||||
|
</PaginatingTableToolbar>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{policies.length === 0 && (
|
||||||
|
<>
|
||||||
|
{newDialog && (
|
||||||
|
<NewPolicyDialog
|
||||||
|
policyProviders={policyProviders?.filter(
|
||||||
|
(p) => p.type !== "aggregate"
|
||||||
|
)}
|
||||||
|
onSelect={(p) =>
|
||||||
|
history.push(
|
||||||
|
toCreatePolicy({ id: clientId, realm, policyType: p.type! })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toggleDialog={toggleDialog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ListEmptyState
|
||||||
|
message={t("emptyPolicies")}
|
||||||
|
instructions={t("emptyPoliciesInstructions")}
|
||||||
|
primaryActionText={t("createPolicy")}
|
||||||
|
onPrimaryAction={toggleDialog}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
|
@ -20,13 +20,13 @@ import { SaveReset } from "../advanced/SaveReset";
|
||||||
import { ImportDialog } from "./ImportDialog";
|
import { ImportDialog } from "./ImportDialog";
|
||||||
import useToggle from "../../utils/useToggle";
|
import useToggle from "../../utils/useToggle";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { DecisionStrategySelect } from "./DecisionStragegySelect";
|
||||||
|
|
||||||
const POLICY_ENFORCEMENT_MODES = [
|
const POLICY_ENFORCEMENT_MODES = [
|
||||||
"ENFORCING",
|
"ENFORCING",
|
||||||
"PERMISSIVE",
|
"PERMISSIVE",
|
||||||
"DISABLED",
|
"DISABLED",
|
||||||
] as const;
|
] as const;
|
||||||
const DECISION_STRATEGY = ["UNANIMOUS", "AFFIRMATIVE"] as const;
|
|
||||||
|
|
||||||
export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
|
@ -134,40 +134,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup
|
<DecisionStrategySelect isLimited />
|
||||||
label={t("decisionStrategy")}
|
|
||||||
labelIcon={
|
|
||||||
<HelpItem
|
|
||||||
helpText="clients-help:decisionStrategy"
|
|
||||||
fieldLabelId="clients:decisionStrategy"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
fieldId="decisionStrategy"
|
|
||||||
hasNoPaddingTop
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="decisionStrategy"
|
|
||||||
data-testid="decisionStrategy"
|
|
||||||
defaultValue={DECISION_STRATEGY[0]}
|
|
||||||
control={control}
|
|
||||||
render={({ onChange, value }) => (
|
|
||||||
<>
|
|
||||||
{DECISION_STRATEGY.map((strategy) => (
|
|
||||||
<Radio
|
|
||||||
id={strategy}
|
|
||||||
key={strategy}
|
|
||||||
data-testid={strategy}
|
|
||||||
isChecked={value === strategy}
|
|
||||||
name="decisionStrategy"
|
|
||||||
onChange={() => onChange(strategy)}
|
|
||||||
label={t(`decisionStrategies.${strategy}`)}
|
|
||||||
className="pf-u-mb-md"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
hasNoPaddingTop
|
hasNoPaddingTop
|
||||||
label={t("allowRemoteResourceManagement")}
|
label={t("allowRemoteResourceManagement")}
|
||||||
|
|
36
src/clients/authorization/policy/Aggregate.tsx
Normal file
36
src/clients/authorization/policy/Aggregate.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { FormGroup } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type { PolicyDetailsParams } from "../../routes/PolicyDetails";
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { ResourcesPolicySelect } from "../ResourcesPolicySelect";
|
||||||
|
import { DecisionStrategySelect } from "../DecisionStragegySelect";
|
||||||
|
|
||||||
|
export const Aggregate = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { id } = useParams<PolicyDetailsParams>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t("applyPolicy")}
|
||||||
|
fieldId="policies"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:applyPolicy"
|
||||||
|
fieldLabelId="clients:policies"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ResourcesPolicySelect
|
||||||
|
name="policies"
|
||||||
|
searchFunction="listPolicies"
|
||||||
|
clientId={id}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<DecisionStrategySelect helpLabel="policyDecisionStagey" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
111
src/clients/authorization/policy/Client.tsx
Normal file
111
src/clients/authorization/policy/Client.tsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
SelectOption,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectVariant,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||||
|
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const Client = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control, getValues, errors } = useFormContext();
|
||||||
|
const values: string[] | undefined = getValues("clients");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [clients, setClients] = useState<ClientRepresentation[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
const params: ClientQuery = {
|
||||||
|
max: 20,
|
||||||
|
};
|
||||||
|
if (search) {
|
||||||
|
params.clientId = search;
|
||||||
|
params.search = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.length && !search) {
|
||||||
|
return await Promise.all(
|
||||||
|
values.map(
|
||||||
|
(id: string) =>
|
||||||
|
adminClient.clients.findOne({ id }) as ClientRepresentation
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await adminClient.clients.find(params);
|
||||||
|
},
|
||||||
|
setClients,
|
||||||
|
[search]
|
||||||
|
);
|
||||||
|
|
||||||
|
const convert = (clients: ClientRepresentation[]) =>
|
||||||
|
clients.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
key={option.id!}
|
||||||
|
value={option.id}
|
||||||
|
selected={values?.includes(option.id!)}
|
||||||
|
>
|
||||||
|
{option.clientId}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("clients")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:policyClient"
|
||||||
|
fieldLabelId="clients:client"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="clients"
|
||||||
|
helperTextInvalid={t("requiredClient")}
|
||||||
|
validated={errors.clients ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="clients"
|
||||||
|
defaultValue={[]}
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
validate: (value) => value.length > 0,
|
||||||
|
}}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="clients"
|
||||||
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
onToggle={(open) => setOpen(open)}
|
||||||
|
isOpen={open}
|
||||||
|
selections={value}
|
||||||
|
onFilter={(_, value) => {
|
||||||
|
setSearch(value);
|
||||||
|
return convert(clients);
|
||||||
|
}}
|
||||||
|
onSelect={(_, v) => {
|
||||||
|
const option = v.toString();
|
||||||
|
if (value.includes(option)) {
|
||||||
|
onChange(value.filter((item: string) => item !== option));
|
||||||
|
} else {
|
||||||
|
onChange([...value, option]);
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
aria-label={t("clients")}
|
||||||
|
>
|
||||||
|
{convert(clients)}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
161
src/clients/authorization/policy/ClientScope.tsx
Normal file
161
src/clients/authorization/policy/ClientScope.tsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFormContext, Controller } from "react-hook-form";
|
||||||
|
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
||||||
|
import { MinusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
|
||||||
|
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { AddScopeDialog } from "../../scopes/AddScopeDialog";
|
||||||
|
|
||||||
|
export type RequiredIdValue = {
|
||||||
|
id: string;
|
||||||
|
required: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientScope = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control, getValues, setValue, errors } =
|
||||||
|
useFormContext<{ clientScopes: RequiredIdValue[] }>();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [scopes, setScopes] = useState<ClientScopeRepresentation[]>([]);
|
||||||
|
const [selectedScopes, setSelectedScopes] = useState<
|
||||||
|
ClientScopeRepresentation[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() => adminClient.clientScopes.find(),
|
||||||
|
(scopes) => {
|
||||||
|
setSelectedScopes(
|
||||||
|
getValues("clientScopes").map((s) => scopes.find((c) => c.id === s.id)!)
|
||||||
|
);
|
||||||
|
setScopes(scopes);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("clientScopes")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:clientScopes"
|
||||||
|
fieldLabelId="clients:clientScopes"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="clientScopes"
|
||||||
|
helperTextInvalid={t("requiredClientScope")}
|
||||||
|
validated={errors.clientScopes ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="clientScopes"
|
||||||
|
control={control}
|
||||||
|
defaultValue={[]}
|
||||||
|
rules={{
|
||||||
|
validate: (value: RequiredIdValue[]) =>
|
||||||
|
value.filter((c) => c.id).length > 0,
|
||||||
|
}}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<>
|
||||||
|
{open && (
|
||||||
|
<AddScopeDialog
|
||||||
|
clientScopes={scopes.filter(
|
||||||
|
(scope) =>
|
||||||
|
!value.map((c: RequiredIdValue) => c.id).includes(scope.id!)
|
||||||
|
)}
|
||||||
|
isClientScopesConditionType
|
||||||
|
open={open}
|
||||||
|
toggleDialog={() => setOpen(!open)}
|
||||||
|
onAdd={(scopes) => {
|
||||||
|
setSelectedScopes([
|
||||||
|
...selectedScopes,
|
||||||
|
...scopes.map((s) => s.scope),
|
||||||
|
]);
|
||||||
|
onChange([
|
||||||
|
...value,
|
||||||
|
...scopes
|
||||||
|
.map((scope) => scope.scope)
|
||||||
|
.map((item) => ({ id: item.id!, required: false })),
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
data-testid="select-scope-button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("addClientScopes")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedScopes.length > 0 && (
|
||||||
|
<TableComposable>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t("clientScope")}</Th>
|
||||||
|
<Th>{t("required")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{selectedScopes.map((scope, index) => (
|
||||||
|
<Tr key={scope.id}>
|
||||||
|
<Td>{scope.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`clientScopes[${index}].required`}
|
||||||
|
defaultValue={false}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Checkbox
|
||||||
|
id="required"
|
||||||
|
data-testid="standard"
|
||||||
|
name="required"
|
||||||
|
isChecked={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="keycloak__client-authorization__policy-row-remove"
|
||||||
|
icon={<MinusCircleIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setValue("clientScopes", [
|
||||||
|
...getValues("clientScopes").filter(
|
||||||
|
(s) => s.id !== scope.id
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
setSelectedScopes([
|
||||||
|
...selectedScopes.filter((s) => s.id !== scope.id),
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
162
src/clients/authorization/policy/Group.tsx
Normal file
162
src/clients/authorization/policy/Group.tsx
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFormContext, Controller } from "react-hook-form";
|
||||||
|
import { MinusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
|
||||||
|
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog";
|
||||||
|
|
||||||
|
export type GroupValue = {
|
||||||
|
id: string;
|
||||||
|
extendChildren: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Group = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control, getValues, setValue, errors } =
|
||||||
|
useFormContext<{ groups?: GroupValue[] }>();
|
||||||
|
const values = getValues("groups");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() => {
|
||||||
|
if (values && values.length > 0)
|
||||||
|
return Promise.all(
|
||||||
|
values.map((g) => adminClient.groups.findOne({ id: g.id }))
|
||||||
|
);
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
(groups) => {
|
||||||
|
const filteredGroup = groups.filter((g) => g) as GroupRepresentation[];
|
||||||
|
setSelectedGroups(filteredGroup);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("groups")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:policyGroups"
|
||||||
|
fieldLabelId="clients:groups"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="groups"
|
||||||
|
helperTextInvalid={t("requiredGroups")}
|
||||||
|
validated={errors.groups ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="groups"
|
||||||
|
control={control}
|
||||||
|
defaultValue={[]}
|
||||||
|
rules={{
|
||||||
|
validate: (value: GroupValue[]) =>
|
||||||
|
value.filter((c) => c.id).length > 0,
|
||||||
|
}}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<>
|
||||||
|
{open && (
|
||||||
|
<GroupPickerDialog
|
||||||
|
type="selectMany"
|
||||||
|
text={{
|
||||||
|
title: "clients:addGroupsToGroupPolicy",
|
||||||
|
ok: "common:add",
|
||||||
|
}}
|
||||||
|
onConfirm={(groups) => {
|
||||||
|
onChange([
|
||||||
|
...value,
|
||||||
|
...groups.map((group) => ({ id: group.id })),
|
||||||
|
]);
|
||||||
|
setSelectedGroups([...selectedGroups, ...groups]);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
filterGroups={selectedGroups.map((g) => g.name!)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
data-testid="select-group-button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("addGroups")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedGroups.length > 0 && (
|
||||||
|
<TableComposable>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t("groups")}</Th>
|
||||||
|
<Th>{t("extendToChildren")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{selectedGroups.map((group, index) => (
|
||||||
|
<Tr key={group.id}>
|
||||||
|
<Td>{group.path}</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`groups[${index}].extendChildren`}
|
||||||
|
defaultValue={false}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Checkbox
|
||||||
|
id="extendChildren"
|
||||||
|
data-testid="standard"
|
||||||
|
name="extendChildren"
|
||||||
|
isChecked={value}
|
||||||
|
onChange={onChange}
|
||||||
|
isDisabled={group.subGroups?.length === 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="keycloak__client-authorization__policy-row-remove"
|
||||||
|
icon={<MinusCircleIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setValue("groups", [
|
||||||
|
...(values || []).filter((s) => s.id !== group.id),
|
||||||
|
]);
|
||||||
|
setSelectedGroups([
|
||||||
|
...selectedGroups.filter((s) => s.id !== group.id),
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
43
src/clients/authorization/policy/JavaScript.tsx
Normal file
43
src/clients/authorization/policy/JavaScript.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { FormGroup } from "@patternfly/react-core";
|
||||||
|
import { CodeEditor, Language } from "@patternfly/react-code-editor";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
export const JavaScript = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("code")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:policyCode"
|
||||||
|
fieldLabelId="clients:code"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="code"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="code"
|
||||||
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<CodeEditor
|
||||||
|
id="code"
|
||||||
|
data-testid="code"
|
||||||
|
type="text"
|
||||||
|
onChange={onChange}
|
||||||
|
code={value}
|
||||||
|
height="600px"
|
||||||
|
language={Language.javascript}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
47
src/clients/authorization/policy/LogicSelector.tsx
Normal file
47
src/clients/authorization/policy/LogicSelector.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { FormGroup, Radio } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
const LOGIC_TYPES = ["POSITIVE", "NEGATIVE"] as const;
|
||||||
|
|
||||||
|
export const LogicSelector = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("logic")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem helpText="clients-help:logic" fieldLabelId="clients:logic" />
|
||||||
|
}
|
||||||
|
fieldId="logic"
|
||||||
|
hasNoPaddingTop
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="logic"
|
||||||
|
data-testid="logic"
|
||||||
|
defaultValue={LOGIC_TYPES[0]}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<>
|
||||||
|
{LOGIC_TYPES.map((type) => (
|
||||||
|
<Radio
|
||||||
|
id={type}
|
||||||
|
key={type}
|
||||||
|
data-testid={type}
|
||||||
|
isChecked={value === type}
|
||||||
|
name="logic"
|
||||||
|
onChange={() => onChange(type)}
|
||||||
|
label={t(`logicType.${type.toLowerCase()}`)}
|
||||||
|
className="pf-u-mb-md"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
83
src/clients/authorization/policy/NameDescription.tsx
Normal file
83
src/clients/authorization/policy/NameDescription.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
TextArea,
|
||||||
|
TextInput,
|
||||||
|
ValidatedOptions,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
type NameDescriptionProps = {
|
||||||
|
prefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NameDescription = ({ prefix }: NameDescriptionProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { register, errors } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t("common:name")}
|
||||||
|
fieldId="kc-name"
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
validated={
|
||||||
|
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
|
}
|
||||||
|
isRequired
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={`clients-help:${prefix}-name`}
|
||||||
|
fieldLabelId="name"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="kc-name"
|
||||||
|
name="name"
|
||||||
|
data-testid="name"
|
||||||
|
ref={register({ required: true })}
|
||||||
|
validated={
|
||||||
|
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("common:description")}
|
||||||
|
fieldId="kc-description"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={`clients-help:${prefix}-description`}
|
||||||
|
fieldLabelId="description"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
validated={
|
||||||
|
errors.description ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
|
}
|
||||||
|
helperTextInvalid={errors.description?.message}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
ref={register({
|
||||||
|
maxLength: {
|
||||||
|
value: 255,
|
||||||
|
message: t("common:maxLength", { length: 255 }),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
type="text"
|
||||||
|
id="kc-description"
|
||||||
|
name="description"
|
||||||
|
data-testid="description"
|
||||||
|
validated={
|
||||||
|
errors.description
|
||||||
|
? ValidatedOptions.error
|
||||||
|
: ValidatedOptions.default
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
231
src/clients/authorization/policy/PolicyDetails.tsx
Normal file
231
src/clients/authorization/policy/PolicyDetails.tsx
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
import { Link, useHistory, useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
|
DropdownItem,
|
||||||
|
PageSection,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
||||||
|
import {
|
||||||
|
PolicyDetailsParams,
|
||||||
|
toPolicyDetails,
|
||||||
|
} from "../../routes/PolicyDetails";
|
||||||
|
import { ViewHeader } from "../../../components/view-header/ViewHeader";
|
||||||
|
import { KeycloakSpinner } from "../../../components/keycloak-spinner/KeycloakSpinner";
|
||||||
|
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import { FormAccess } from "../../../components/form-access/FormAccess";
|
||||||
|
import { useAlerts } from "../../../components/alert/Alerts";
|
||||||
|
import { toClient } from "../../routes/Client";
|
||||||
|
import { Aggregate } from "./Aggregate";
|
||||||
|
import { Client } from "./Client";
|
||||||
|
import { User } from "./User";
|
||||||
|
import { NameDescription } from "./NameDescription";
|
||||||
|
import { LogicSelector } from "./LogicSelector";
|
||||||
|
import { ClientScope, RequiredIdValue } from "./ClientScope";
|
||||||
|
import { Group, GroupValue } from "./Group";
|
||||||
|
import { Regex } from "./Regex";
|
||||||
|
import { Role } from "./Role";
|
||||||
|
import { Time } from "./Time";
|
||||||
|
import { JavaScript } from "./JavaScript";
|
||||||
|
|
||||||
|
import "./policy-details.css";
|
||||||
|
|
||||||
|
type Policy = PolicyRepresentation & {
|
||||||
|
groups?: GroupValue[];
|
||||||
|
clientScopes?: RequiredIdValue[];
|
||||||
|
roles?: RequiredIdValue[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMPONENTS: {
|
||||||
|
[index: string]: FunctionComponent;
|
||||||
|
} = {
|
||||||
|
aggregate: Aggregate,
|
||||||
|
client: Client,
|
||||||
|
user: User,
|
||||||
|
"client-scope": ClientScope,
|
||||||
|
group: Group,
|
||||||
|
regex: Regex,
|
||||||
|
role: Role,
|
||||||
|
time: Time,
|
||||||
|
js: JavaScript,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const isValidComponentType = (value: string): boolean => value in COMPONENTS;
|
||||||
|
|
||||||
|
export default function PolicyDetails() {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { id, realm, policyId, policyType } = useParams<PolicyDetailsParams>();
|
||||||
|
const history = useHistory();
|
||||||
|
const form = useForm({ shouldUnregister: false });
|
||||||
|
const { reset, handleSubmit } = form;
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
|
||||||
|
const [policy, setPolicy] = useState<PolicyRepresentation>();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
if (policyId) {
|
||||||
|
const result = await Promise.all([
|
||||||
|
adminClient.clients.findOnePolicy({
|
||||||
|
id,
|
||||||
|
type: policyType,
|
||||||
|
policyId,
|
||||||
|
}) as PolicyRepresentation | undefined,
|
||||||
|
adminClient.clients.getAssociatedPolicies({
|
||||||
|
id,
|
||||||
|
permissionId: policyId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!result[0]) {
|
||||||
|
throw new Error(t("common:notFound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
policy: result[0],
|
||||||
|
policies: result[1].map((p) => p.id),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
({ policy, policies }) => {
|
||||||
|
reset({ ...policy, policies });
|
||||||
|
setPolicy(policy);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = async (policy: Policy) => {
|
||||||
|
// remove entries that only have the boolean set and no id
|
||||||
|
policy.groups = policy.groups?.filter((g) => g.id);
|
||||||
|
policy.clientScopes = policy.clientScopes?.filter((c) => c.id);
|
||||||
|
policy.roles = policy.roles?.filter((r) => r.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (policyId) {
|
||||||
|
await adminClient.clients.updatePolicy(
|
||||||
|
{ id, type: policyType, policyId },
|
||||||
|
policy
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const result = await adminClient.clients.createPolicy(
|
||||||
|
{ id, type: policyType },
|
||||||
|
policy
|
||||||
|
);
|
||||||
|
history.push(
|
||||||
|
toPolicyDetails({
|
||||||
|
realm,
|
||||||
|
id,
|
||||||
|
policyType,
|
||||||
|
policyId: result.id!,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addAlert(
|
||||||
|
t((policyId ? "update" : "create") + "PolicySuccess"),
|
||||||
|
AlertVariant.success
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
addError("clients:policySaveError", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: "clients:deletePolicy",
|
||||||
|
messageKey: "clients:deletePolicyConfirm",
|
||||||
|
continueButtonLabel: "clients:confirm",
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await adminClient.clients.delPolicy({
|
||||||
|
id,
|
||||||
|
policyId,
|
||||||
|
});
|
||||||
|
addAlert(t("policyDeletedSuccess"), AlertVariant.success);
|
||||||
|
history.push(toClient({ realm, clientId: id, tab: "authorization" }));
|
||||||
|
} catch (error) {
|
||||||
|
addError("clients:policyDeletedError", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (policyId && !policy) {
|
||||||
|
return <KeycloakSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidComponentType(policyType)) {
|
||||||
|
throw new Error(`Not a supported ${policyType}!`);
|
||||||
|
}
|
||||||
|
const ComponentType = COMPONENTS[policyType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteConfirm />
|
||||||
|
<ViewHeader
|
||||||
|
titleKey={policyId ? policy?.name! : "clients:createPolicy"}
|
||||||
|
dropdownItems={
|
||||||
|
policyId
|
||||||
|
? [
|
||||||
|
<DropdownItem
|
||||||
|
key="delete"
|
||||||
|
data-testid="delete-policy"
|
||||||
|
onClick={() => toggleDeleteDialog()}
|
||||||
|
>
|
||||||
|
{t("common:delete")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PageSection variant="light">
|
||||||
|
<FormAccess
|
||||||
|
isHorizontal
|
||||||
|
onSubmit={handleSubmit(save)}
|
||||||
|
role="manage-clients"
|
||||||
|
>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<NameDescription prefix="policy" />
|
||||||
|
<ComponentType />
|
||||||
|
<LogicSelector />
|
||||||
|
</FormProvider>
|
||||||
|
<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",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormAccess>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
60
src/clients/authorization/policy/Regex.tsx
Normal file
60
src/clients/authorization/policy/Regex.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
export const Regex = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { register, errors } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t("targetClaim")}
|
||||||
|
fieldId="targetClaim"
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
validated={errors.targetClaim ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:targetClaim"
|
||||||
|
fieldLabelId="clients:targetClaim"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
id="targetClaim"
|
||||||
|
name="targetClaim"
|
||||||
|
data-testid="targetClaim"
|
||||||
|
ref={register({ required: true })}
|
||||||
|
validated={errors.targetClaim ? "error" : "default"}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("regexPattern")}
|
||||||
|
fieldId="pattern"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:regexPattern"
|
||||||
|
fieldLabelId="clients:regexPattern"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isRequired
|
||||||
|
validated={errors.pattern ? "error" : "default"}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={register({ required: true })}
|
||||||
|
type="text"
|
||||||
|
id="pattern"
|
||||||
|
name="pattern"
|
||||||
|
data-testid="regexPattern"
|
||||||
|
validated={errors.pattern ? "error" : "default"}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
164
src/clients/authorization/policy/Role.tsx
Normal file
164
src/clients/authorization/policy/Role.tsx
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFormContext, Controller } from "react-hook-form";
|
||||||
|
import { FormGroup, Button, Checkbox } from "@patternfly/react-core";
|
||||||
|
import { MinusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
|
||||||
|
import { Row, ServiceRole } from "../../../components/role-mapping/RoleMapping";
|
||||||
|
import type { RequiredIdValue } from "./ClientScope";
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import { AddRoleMappingModal } from "../../../components/role-mapping/AddRoleMappingModal";
|
||||||
|
|
||||||
|
export const Role = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control, getValues, setValue, errors } =
|
||||||
|
useFormContext<{ roles?: RequiredIdValue[] }>();
|
||||||
|
const values = getValues("roles");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedRoles, setSelectedRoles] = useState<Row[]>([]);
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
if (values && values.length > 0) {
|
||||||
|
const roles = await Promise.all(
|
||||||
|
values.map((r) => adminClient.roles.findOneById({ id: r.id }))
|
||||||
|
);
|
||||||
|
return Promise.all(
|
||||||
|
roles
|
||||||
|
.filter((r) => r?.clientRole)
|
||||||
|
.map(async (role) => ({
|
||||||
|
role: role!,
|
||||||
|
client: await adminClient.clients.findOne({
|
||||||
|
id: role?.containerId!,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
setSelectedRoles,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("roles")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:policyRoles"
|
||||||
|
fieldLabelId="clients:roles"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="roles"
|
||||||
|
helperTextInvalid={t("requiredRoles")}
|
||||||
|
validated={errors.roles ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="roles"
|
||||||
|
control={control}
|
||||||
|
defaultValue={[]}
|
||||||
|
rules={{
|
||||||
|
validate: (value: RequiredIdValue[]) =>
|
||||||
|
value.filter((c) => c.id).length > 0,
|
||||||
|
}}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<>
|
||||||
|
{open && (
|
||||||
|
<AddRoleMappingModal
|
||||||
|
id="role"
|
||||||
|
type="role"
|
||||||
|
onAssign={(rows) => {
|
||||||
|
onChange([
|
||||||
|
...value,
|
||||||
|
...rows.map((row) => ({ id: row.role.id })),
|
||||||
|
]);
|
||||||
|
setSelectedRoles([...selectedRoles, ...rows]);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
isLDAPmapper
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
data-testid="select-role-button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("addRoles")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedRoles.length > 0 && (
|
||||||
|
<TableComposable>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{t("roles")}</Th>
|
||||||
|
<Th>{t("required")}</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{selectedRoles.map((row, index) => (
|
||||||
|
<Tr key={row.role.id}>
|
||||||
|
<Td>
|
||||||
|
<ServiceRole role={row.role} client={row.client} />
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`roles[${index}].required`}
|
||||||
|
defaultValue={false}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Checkbox
|
||||||
|
id="required"
|
||||||
|
data-testid="standard"
|
||||||
|
name="required"
|
||||||
|
isChecked={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="keycloak__client-authorization__policy-row-remove"
|
||||||
|
icon={<MinusCircleIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setValue("roles", [
|
||||||
|
...(values || []).filter((s) => s.id !== row.role.id),
|
||||||
|
]);
|
||||||
|
setSelectedRoles([
|
||||||
|
...selectedRoles.filter(
|
||||||
|
(s) => s.role.id !== row.role.id
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
217
src/clients/authorization/policy/Time.tsx
Normal file
217
src/clients/authorization/policy/Time.tsx
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
DatePicker,
|
||||||
|
Flex,
|
||||||
|
FlexItem,
|
||||||
|
FormGroup,
|
||||||
|
NumberInput,
|
||||||
|
Radio,
|
||||||
|
Split,
|
||||||
|
SplitItem,
|
||||||
|
TimePicker,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
|
||||||
|
const DATE_TIME_FORMAT = /(\d\d\d\d-\d\d-\d\d)? (\d\d?):(\d\d?)/;
|
||||||
|
|
||||||
|
const DateTime = ({ name }: { name: string }) => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
const parseDate = (value: string, date?: Date): string => {
|
||||||
|
const parts = value.match(DATE_TIME_FORMAT);
|
||||||
|
if (date) {
|
||||||
|
const parsedDate = moment(date).format("yyyy-MM-DD");
|
||||||
|
return `${parsedDate} ${parts ? parts[2] : "00"}:${
|
||||||
|
parts ? parts[3] : "00"
|
||||||
|
}:00`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseTime = (
|
||||||
|
value: string,
|
||||||
|
hour?: number | null,
|
||||||
|
minute?: number | null
|
||||||
|
): string => {
|
||||||
|
const parts = value.match(DATE_TIME_FORMAT);
|
||||||
|
if (minute !== undefined && minute !== null) {
|
||||||
|
return `${parts ? parts[1] : ""} ${hour}:${
|
||||||
|
minute < 10 ? `0${minute}` : minute
|
||||||
|
}:00`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => {
|
||||||
|
const dateTime = value.match(DATE_TIME_FORMAT) || ["", "", "0", "00"];
|
||||||
|
return (
|
||||||
|
<Split hasGutter id={name}>
|
||||||
|
<SplitItem>
|
||||||
|
<DatePicker
|
||||||
|
value={dateTime[1]}
|
||||||
|
onChange={(_, date) => {
|
||||||
|
onChange(parseDate(value, date));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<TimePicker
|
||||||
|
time={`${dateTime[2]}:${dateTime[3]}`}
|
||||||
|
onChange={(_, hour, minute) =>
|
||||||
|
onChange(parseTime(value, hour, minute))
|
||||||
|
}
|
||||||
|
is24Hour
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type NumberControlProps = {
|
||||||
|
name: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NumberControl = ({ name, min, max }: NumberControlProps) => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
const setValue = (newValue: number) => Math.min(newValue, max);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<NumberInput
|
||||||
|
id={name}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
onPlus={() => onChange(Number(value) + 1)}
|
||||||
|
onMinus={() => onChange(Number(value) - 1)}
|
||||||
|
onChange={(event) => {
|
||||||
|
const newValue = Number(event.currentTarget.value);
|
||||||
|
onChange(setValue(!isNaN(newValue) ? newValue : 0));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FromTo = ({ name, ...rest }: NumberControlProps) => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t(name)}
|
||||||
|
fieldId={name}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText={`clients-help:${name}`}
|
||||||
|
fieldLabelId={`clients:${name}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Split hasGutter>
|
||||||
|
<SplitItem>
|
||||||
|
<NumberControl name={name} {...rest} />
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>{t("common:to")}</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<NumberControl name={`${name}End`} {...rest} />
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Time = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { getValues } = useFormContext();
|
||||||
|
const [repeat, setRepeat] = useState(getValues("month"));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t("repeat")}
|
||||||
|
fieldId="repeat"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:repeat"
|
||||||
|
fieldLabelId="clients:repeat"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex>
|
||||||
|
<FlexItem>
|
||||||
|
<Radio
|
||||||
|
id="notRepeat"
|
||||||
|
data-testid="notRepeat"
|
||||||
|
isChecked={!repeat}
|
||||||
|
name="repeat"
|
||||||
|
onChange={() => setRepeat(false)}
|
||||||
|
label={t("notRepeat")}
|
||||||
|
className="pf-u-mb-md"
|
||||||
|
/>
|
||||||
|
</FlexItem>
|
||||||
|
<FlexItem>
|
||||||
|
<Radio
|
||||||
|
id="repeat"
|
||||||
|
data-testid="repeat"
|
||||||
|
isChecked={repeat}
|
||||||
|
name="repeat"
|
||||||
|
onChange={() => setRepeat(true)}
|
||||||
|
label={t("repeat")}
|
||||||
|
className="pf-u-mb-md"
|
||||||
|
/>
|
||||||
|
</FlexItem>
|
||||||
|
</Flex>
|
||||||
|
</FormGroup>
|
||||||
|
{repeat && (
|
||||||
|
<>
|
||||||
|
<FromTo name="month" min={1} max={12} />
|
||||||
|
<FromTo name="dayMonth" min={1} max={31} />
|
||||||
|
<FromTo name="hour" min={0} max={23} />
|
||||||
|
<FromTo name="minute" min={0} max={59} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<FormGroup
|
||||||
|
label={t("startTime")}
|
||||||
|
fieldId="notBefore"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:startTime"
|
||||||
|
fieldLabelId="clients:startTime"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateTime name="notBefore" />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("expireTime")}
|
||||||
|
fieldId="notOnOrAfter"
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:expireTime"
|
||||||
|
fieldLabelId="clients:expireTime"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateTime name="notOnOrAfter" />
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
110
src/clients/authorization/policy/User.tsx
Normal file
110
src/clients/authorization/policy/User.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
SelectOption,
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectVariant,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||||
|
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export const User = () => {
|
||||||
|
const { t } = useTranslation("clients");
|
||||||
|
const { control, getValues, errors } = useFormContext();
|
||||||
|
const values: string[] | undefined = getValues("users");
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [users, setUsers] = useState<UserRepresentation[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
async () => {
|
||||||
|
const params: UserQuery = {
|
||||||
|
max: 20,
|
||||||
|
};
|
||||||
|
if (search) {
|
||||||
|
params.name = search;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.length && !search) {
|
||||||
|
return await Promise.all(
|
||||||
|
values.map(
|
||||||
|
(id: string) =>
|
||||||
|
adminClient.users.findOne({ id }) as UserRepresentation
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await adminClient.users.find(params);
|
||||||
|
},
|
||||||
|
setUsers,
|
||||||
|
[search]
|
||||||
|
);
|
||||||
|
|
||||||
|
const convert = (clients: UserRepresentation[]) =>
|
||||||
|
clients.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
key={option.id!}
|
||||||
|
value={option.id}
|
||||||
|
selected={values?.includes(option.id!)}
|
||||||
|
>
|
||||||
|
{option.username}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t("users")}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem
|
||||||
|
helpText="clients-help:policyUsers"
|
||||||
|
fieldLabelId="clients:users"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fieldId="users"
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
validated={errors.users ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="users"
|
||||||
|
defaultValue={[]}
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
validate: (value) => value.length > 0,
|
||||||
|
}}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="users"
|
||||||
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
onToggle={(open) => setOpen(open)}
|
||||||
|
isOpen={open}
|
||||||
|
selections={value}
|
||||||
|
onFilter={(_, value) => {
|
||||||
|
setSearch(value);
|
||||||
|
return convert(users);
|
||||||
|
}}
|
||||||
|
onSelect={(_, v) => {
|
||||||
|
const option = v.toString();
|
||||||
|
if (value.includes(option)) {
|
||||||
|
onChange(value.filter((item: string) => item !== option));
|
||||||
|
} else {
|
||||||
|
onChange([...value, option]);
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
aria-label={t("users")}
|
||||||
|
>
|
||||||
|
{convert(users)}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
4
src/clients/authorization/policy/policy-details.css
Normal file
4
src/clients/authorization/policy/policy-details.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
.pf-c-button.keycloak__client-authorization__policy-row-remove {
|
||||||
|
color: var(--pf-c-button--m-plain--Color);
|
||||||
|
}
|
|
@ -180,7 +180,31 @@ 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.",
|
||||||
|
"policy-name": "The name of this policy.",
|
||||||
|
"policy-description": "A description for this policy.",
|
||||||
|
policyDecisionStagey:
|
||||||
|
"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.",
|
||||||
|
applyPolicy:
|
||||||
|
"Specifies all the policies that must be applied to the scopes defined by this policy or permission.",
|
||||||
|
policyClient: "Specifies which client(s) are allowed by this policy.",
|
||||||
|
policyGroups: "Specifies which user(s) are allowed by this policy.",
|
||||||
|
targetClaim: "Specifies the target claim which the policy will fetch.",
|
||||||
|
regexPattern: "Specifies the regex pattern.",
|
||||||
|
policyRoles: "Specifies the client roles allowed by this policy.",
|
||||||
|
startTime:
|
||||||
|
"Defines the time before which the policy MUST NOT be granted. Only granted if current date/time is after or equal to this value.",
|
||||||
|
expireTime:
|
||||||
|
"Defines the time after which the policy MUST NOT be granted. Only granted if current date/time is before or equal to this value.",
|
||||||
|
month:
|
||||||
|
"Defines the month which the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current month is between or equal to the two values you provided.",
|
||||||
|
dayMonth:
|
||||||
|
"Defines the day of month when the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current day of month is between or equal to the two values you provided.",
|
||||||
|
hour: "Defines the hour when the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current hour is between or equal to the two values you provided.",
|
||||||
|
minute:
|
||||||
|
"Defines the minute when the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current minute is between or equal to the two values you provided.",
|
||||||
|
policyCode: "The JavaScript code providing the conditions for this policy.",
|
||||||
|
logic:
|
||||||
|
"The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.",
|
||||||
permissionName: "The name of this permission.",
|
permissionName: "The name of this permission.",
|
||||||
permissionDescription: "A description for this permission.",
|
permissionDescription: "A description for this permission.",
|
||||||
applyToResourceTypeFlag:
|
applyToResourceTypeFlag:
|
||||||
|
|
|
@ -156,6 +156,69 @@ export default {
|
||||||
createScopeSuccess: "Authorization scope created successfully",
|
createScopeSuccess: "Authorization scope created successfully",
|
||||||
updateScopeSuccess: "Authorization scope successfully updated",
|
updateScopeSuccess: "Authorization scope successfully updated",
|
||||||
scopeSaveError: "Could not persist authorization scope due to {{error}}",
|
scopeSaveError: "Could not persist authorization scope due to {{error}}",
|
||||||
|
createPolicy: "Create policy",
|
||||||
|
dependentPermission: "Dependent permission",
|
||||||
|
deletePolicy: "Permanently delete policy?",
|
||||||
|
deletePolicyConfirm:
|
||||||
|
"If you delete this policy, some permissions or aggregated policies will be affected.",
|
||||||
|
deletePolicyWarning:
|
||||||
|
"The aggregated polices below will be removed automatically:",
|
||||||
|
policyDeletedSuccess: "The Policy successfully deleted",
|
||||||
|
policyDeletedError: "Could not remove the resource {{error}}",
|
||||||
|
emptyPolicies: "No policies",
|
||||||
|
emptyPoliciesInstructions:
|
||||||
|
"If you want to create a policy, please click the button below to create the policy.",
|
||||||
|
chooseAPolicyType: "Choose a policy type",
|
||||||
|
chooseAPolicyTypeInstructions:
|
||||||
|
"Choose one policy type from the list below and then you can configure a new policy for authorization. There are some types and description.",
|
||||||
|
policyProvider: {
|
||||||
|
regex: "Define regex conditions for your permissions.",
|
||||||
|
role: "Define conditions for your permissions where a set of one or more roles is permitted to access an object.",
|
||||||
|
js: "Define conditions for your permissions using JavaScript. It is one of the rule-based policy types supported by Keycloak, and provides flexibility to write any policy based on the Evaluation API.",
|
||||||
|
client:
|
||||||
|
"Define conditions for your permissions where a set of one or more clients is permitted to access an object.",
|
||||||
|
time: "Define time conditions for your permissions.",
|
||||||
|
user: "Define conditions for your permissions where a set of one or more users is permitted to access an object.",
|
||||||
|
"client-scope":
|
||||||
|
"Define conditions for your permissions where a set of one or more client scopes is permitted to access an object.",
|
||||||
|
aggregate:
|
||||||
|
"Reuse existing policies to build more complex ones and keep your permissions even more decoupled from the policies that are evaluated during the processing of authorization requests.",
|
||||||
|
group:
|
||||||
|
"Define conditions for your permissions where a set of one or more groups (and their hierarchies) is permitted to access an object.",
|
||||||
|
},
|
||||||
|
applyPolicy: "Apply policy",
|
||||||
|
addClientScopes: "Add client scopes",
|
||||||
|
clientScope: "Client scope",
|
||||||
|
addGroups: "Add groups",
|
||||||
|
groups: "Groups",
|
||||||
|
users: "Users",
|
||||||
|
requiredClient: "Please add at least one client.",
|
||||||
|
requiredClientScope: "Please add at least one client scope.",
|
||||||
|
requiredGroups: "Please add at least one group.",
|
||||||
|
requiredRoles: "Please add at least one role.",
|
||||||
|
addGroupsToGroupPolicy: "Add groups to group policy",
|
||||||
|
extendToChildren: "Extend to children",
|
||||||
|
targetClaim: "Target claim",
|
||||||
|
regexPattern: "Regex pattern",
|
||||||
|
addRoles: "Add roles",
|
||||||
|
required: "Required",
|
||||||
|
startTime: "Start time",
|
||||||
|
repeat: "Repeat",
|
||||||
|
notRepeat: "Not repeat",
|
||||||
|
month: "Month",
|
||||||
|
dayMonth: "Day",
|
||||||
|
hour: "Hour",
|
||||||
|
minute: "Minute",
|
||||||
|
code: "Code",
|
||||||
|
expireTime: "Expire time",
|
||||||
|
logic: "Logic",
|
||||||
|
logicType: {
|
||||||
|
positive: "Positive",
|
||||||
|
negative: "Negative",
|
||||||
|
},
|
||||||
|
createPolicySuccess: "Successfully created the policy",
|
||||||
|
updatePolicySuccess: "Successfully updated the policy",
|
||||||
|
policySaveError: "Could not update the policy due to {{error}}",
|
||||||
assignedClientScope: "Assigned client scope",
|
assignedClientScope: "Assigned client scope",
|
||||||
assignedType: "Assigned type",
|
assignedType: "Assigned type",
|
||||||
hideInheritedRoles: "Hide inherited roles",
|
hideInheritedRoles: "Hide inherited roles",
|
||||||
|
|
|
@ -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 { NewPolicyRoute } from "./routes/NewPolicy";
|
||||||
|
import { PolicyDetailsRoute } from "./routes/PolicyDetails";
|
||||||
import { NewPermissionRoute } from "./routes/NewPermission";
|
import { NewPermissionRoute } from "./routes/NewPermission";
|
||||||
import { PermissionDetailsRoute } from "./routes/PermissionDetails";
|
import { PermissionDetailsRoute } from "./routes/PermissionDetails";
|
||||||
|
|
||||||
|
@ -23,6 +25,8 @@ const routes: RouteDef[] = [
|
||||||
ResourceDetailsRoute,
|
ResourceDetailsRoute,
|
||||||
NewScopeRoute,
|
NewScopeRoute,
|
||||||
ScopeDetailsRoute,
|
ScopeDetailsRoute,
|
||||||
|
NewPolicyRoute,
|
||||||
|
PolicyDetailsRoute,
|
||||||
NewPermissionRoute,
|
NewPermissionRoute,
|
||||||
PermissionDetailsRoute,
|
PermissionDetailsRoute,
|
||||||
];
|
];
|
||||||
|
|
19
src/clients/routes/NewPolicy.ts
Normal file
19
src/clients/routes/NewPolicy.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
export type NewPolicyParams = { realm: string; id: string; policyType: string };
|
||||||
|
|
||||||
|
export const NewPolicyRoute: RouteDef = {
|
||||||
|
path: "/:realm/clients/:id/authorization/policy/new/:policyType",
|
||||||
|
component: lazy(() => import("../authorization/policy/PolicyDetails")),
|
||||||
|
breadcrumb: (t) => t("clients:createPolicy"),
|
||||||
|
access: "manage-clients",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toCreatePolicy = (
|
||||||
|
params: NewPolicyParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(NewPolicyRoute.path, params),
|
||||||
|
});
|
24
src/clients/routes/PolicyDetails.ts
Normal file
24
src/clients/routes/PolicyDetails.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import { lazy } from "react";
|
||||||
|
|
||||||
|
export type PolicyDetailsParams = {
|
||||||
|
realm: string;
|
||||||
|
id: string;
|
||||||
|
policyId: string;
|
||||||
|
policyType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PolicyDetailsRoute: RouteDef = {
|
||||||
|
path: "/:realm/clients/:id/authorization/policy/:policyId/:policyType",
|
||||||
|
component: lazy(() => import("../authorization/policy/PolicyDetails")),
|
||||||
|
breadcrumb: (t) => t("clients:createPolicy"),
|
||||||
|
access: "manage-clients",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toPolicyDetails = (
|
||||||
|
params: PolicyDetailsParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(PolicyDetailsRoute.path, params),
|
||||||
|
});
|
|
@ -291,7 +291,6 @@ export const AddScopeDialog = ({
|
||||||
{
|
{
|
||||||
name: "name",
|
name: "name",
|
||||||
},
|
},
|
||||||
{ name: "protocol", displayKey: "Protocol" },
|
|
||||||
{
|
{
|
||||||
name: "protocol",
|
name: "protocol",
|
||||||
displayKey: "clients:protocol",
|
displayKey: "clients:protocol",
|
||||||
|
|
|
@ -12,9 +12,11 @@ export const isRealmClient = (client: ClientRepresentation) => !client.protocol;
|
||||||
export const getProtocolName = (t: TFunction<"clients">, protocol: string) => {
|
export const getProtocolName = (t: TFunction<"clients">, protocol: string) => {
|
||||||
switch (protocol) {
|
switch (protocol) {
|
||||||
case "openid-connect":
|
case "openid-connect":
|
||||||
return t("clients:protocolTypes:openIdConnect");
|
return t("clients:protocolTypes.openIdConnect");
|
||||||
case "saml":
|
case "saml":
|
||||||
return t("clients:protocolTypes:saml");
|
return t("clients:protocolTypes.saml");
|
||||||
|
default:
|
||||||
|
return protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
return protocol;
|
return protocol;
|
||||||
|
|
Loading…
Reference in a new issue