Support OR condition for forms + authz (#24879)
Closes: #24586 Signed-off-by: Hynek Mlnarik <hmlnarik@redhat.com>
This commit is contained in:
parent
ab3758842c
commit
c03c2e953a
28 changed files with 389 additions and 160 deletions
|
@ -195,6 +195,80 @@ describe("Client authentication subtab", () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("Client authorization tab access for view-realm-authorization", () => {
|
||||
const clientId = "realm-view-authz-client-" + uuid();
|
||||
|
||||
beforeEach(async () => {
|
||||
const [, testUser] = await Promise.all([
|
||||
adminClient.createRealm("realm-view-authz"),
|
||||
adminClient.createUser({
|
||||
// Create user in master realm
|
||||
username: "test-view-authz-user",
|
||||
enabled: true,
|
||||
credentials: [{ type: "password", value: "password" }],
|
||||
}),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
adminClient.addClientRoleToUser(
|
||||
testUser.id!,
|
||||
"realm-view-authz-realm",
|
||||
["view-realm", "view-users", "view-authorization", "view-clients"],
|
||||
),
|
||||
adminClient.createClient({
|
||||
realm: "realm-view-authz",
|
||||
clientId,
|
||||
authorizationServicesEnabled: true,
|
||||
serviceAccountsEnabled: true,
|
||||
standardFlowEnabled: true,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() =>
|
||||
Promise.all([
|
||||
adminClient.deleteClient(clientId),
|
||||
adminClient.deleteUser("test-view-authz-user"),
|
||||
adminClient.deleteRealm("realm-view-authz"),
|
||||
]),
|
||||
);
|
||||
|
||||
it("Should view autorization tab", () => {
|
||||
sidebarPage.waitForPageLoad();
|
||||
masthead.signOut();
|
||||
|
||||
sidebarPage.waitForPageLoad();
|
||||
loginPage.logIn("test-view-authz-user", "password");
|
||||
keycloakBefore();
|
||||
|
||||
sidebarPage
|
||||
.waitForPageLoad()
|
||||
.goToRealm("realm-view-authz")
|
||||
.waitForPageLoad()
|
||||
.goToClients();
|
||||
|
||||
listingPage
|
||||
.searchItem(clientId, true, "realm-view-authz")
|
||||
.goToItemDetails(clientId);
|
||||
clientDetailsPage.goToAuthorizationTab();
|
||||
|
||||
authenticationTab.goToResourcesSubTab();
|
||||
sidebarPage.waitForPageLoad();
|
||||
listingPage.goToItemDetails("Resource");
|
||||
sidebarPage.waitForPageLoad();
|
||||
cy.go("back");
|
||||
|
||||
authenticationTab.goToScopesSubTab();
|
||||
sidebarPage.waitForPageLoad();
|
||||
authenticationTab.goToPoliciesSubTab();
|
||||
sidebarPage.waitForPageLoad();
|
||||
authenticationTab.goToPermissionsSubTab();
|
||||
sidebarPage.waitForPageLoad();
|
||||
authenticationTab.goToEvaluateSubTab();
|
||||
sidebarPage.waitForPageLoad();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility tests for client authorization", () => {
|
||||
beforeEach(() => {
|
||||
loginPage.logIn();
|
||||
|
|
|
@ -89,9 +89,9 @@ export default class ListingPage extends CommonElements {
|
|||
return this;
|
||||
}
|
||||
|
||||
searchItem(searchValue: string, wait = true) {
|
||||
searchItem(searchValue: string, wait = true, realm = "master") {
|
||||
if (wait) {
|
||||
const searchUrl = `/admin/realms/master/**/*${searchValue}*`;
|
||||
const searchUrl = `/admin/realms/${realm}/**/*${searchValue}*`;
|
||||
cy.intercept(searchUrl).as("search");
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,11 @@ class AdminClient {
|
|||
await this.#client.realms.del({ realm });
|
||||
}
|
||||
|
||||
async createClient(client: ClientRepresentation) {
|
||||
async createClient(
|
||||
client: ClientRepresentation & {
|
||||
realm?: string;
|
||||
},
|
||||
) {
|
||||
await this.#login();
|
||||
await this.#client.clients.create(client);
|
||||
}
|
||||
|
@ -68,6 +72,11 @@ class AdminClient {
|
|||
}
|
||||
}
|
||||
|
||||
async getClient(clientName: string) {
|
||||
await this.#login();
|
||||
return (await this.#client.clients.find({ clientId: clientName }))[0];
|
||||
}
|
||||
|
||||
async createGroup(groupName: string) {
|
||||
await this.#login();
|
||||
return await this.#client.groups.create({ name: groupName });
|
||||
|
@ -149,6 +158,30 @@ class AdminClient {
|
|||
});
|
||||
}
|
||||
|
||||
async addClientRoleToUser(
|
||||
userId: string,
|
||||
clientId: string,
|
||||
roleNames: string[],
|
||||
) {
|
||||
await this.#login();
|
||||
|
||||
const client = await this.#client.clients.find({ clientId });
|
||||
const clientRoles = await Promise.all(
|
||||
roleNames.map(
|
||||
async (roleName) =>
|
||||
(await this.#client.clients.findRole({
|
||||
id: client[0].id!,
|
||||
roleName: roleName,
|
||||
})) as RoleMappingPayload,
|
||||
),
|
||||
);
|
||||
await this.#client.users.addClientRoleMappings({
|
||||
id: userId,
|
||||
clientUniqueId: client[0].id!,
|
||||
roles: clientRoles,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUser(username: string) {
|
||||
await this.#login();
|
||||
const user = await this.#client.users.find({ username });
|
||||
|
|
|
@ -11,11 +11,14 @@ export const ForbiddenSection = ({
|
|||
permissionNeeded,
|
||||
}: ForbiddenSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const count = Array.isArray(permissionNeeded) ? permissionNeeded.length : 1;
|
||||
const permissionNeededArray = Array.isArray(permissionNeeded)
|
||||
? permissionNeeded
|
||||
: [permissionNeeded];
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
{t("forbidden", { count })} {permissionNeeded}
|
||||
{t("forbidden", { count: permissionNeededArray.length })}{" "}
|
||||
{permissionNeededArray.map((p) => p.toString())}
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -195,11 +195,13 @@ export default function ClientDetails() {
|
|||
const isFeatureEnabled = useIsFeatureEnabled();
|
||||
|
||||
const hasManageAuthorization = hasAccess("manage-authorization");
|
||||
const hasViewAuthorization = hasAccess("view-authorization");
|
||||
const hasManageClients = hasAccess("manage-clients");
|
||||
const hasViewClients = hasAccess("view-clients");
|
||||
const hasViewUsers = hasAccess("view-users");
|
||||
const permissionsEnabled =
|
||||
isFeatureEnabled(Feature.AdminFineGrainedAuthz) && hasManageAuthorization;
|
||||
isFeatureEnabled(Feature.AdminFineGrainedAuthz) &&
|
||||
(hasManageAuthorization || hasViewAuthorization);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -530,7 +532,8 @@ export default function ClientDetails() {
|
|||
</RoutableTabs>
|
||||
</Tab>
|
||||
)}
|
||||
{client!.authorizationServicesEnabled && hasManageAuthorization && (
|
||||
{client!.authorizationServicesEnabled &&
|
||||
(hasManageAuthorization || hasViewAuthorization) && (
|
||||
<Tab
|
||||
id="authorization"
|
||||
data-testid="authorizationTab"
|
||||
|
@ -560,7 +563,10 @@ export default function ClientDetails() {
|
|||
title={<TabTitleText>{t("resources")}</TabTitleText>}
|
||||
{...authorizationResourcesTab}
|
||||
>
|
||||
<AuthorizationResources clientId={clientId} />
|
||||
<AuthorizationResources
|
||||
clientId={clientId}
|
||||
isDisabled={!hasManageAuthorization}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="scopes"
|
||||
|
@ -568,7 +574,10 @@ export default function ClientDetails() {
|
|||
title={<TabTitleText>{t("scopes")}</TabTitleText>}
|
||||
{...authorizationScopesTab}
|
||||
>
|
||||
<AuthorizationScopes clientId={clientId} />
|
||||
<AuthorizationScopes
|
||||
clientId={clientId}
|
||||
isDisabled={!hasManageAuthorization}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="policies"
|
||||
|
@ -576,7 +585,10 @@ export default function ClientDetails() {
|
|||
title={<TabTitleText>{t("policies")}</TabTitleText>}
|
||||
{...authorizationPoliciesTab}
|
||||
>
|
||||
<AuthorizationPolicies clientId={clientId} />
|
||||
<AuthorizationPolicies
|
||||
clientId={clientId}
|
||||
isDisabled={!hasManageAuthorization}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
id="permissions"
|
||||
|
@ -584,7 +596,10 @@ export default function ClientDetails() {
|
|||
title={<TabTitleText>{t("permissions")}</TabTitleText>}
|
||||
{...authorizationPermissionsTab}
|
||||
>
|
||||
<AuthorizationPermissions clientId={clientId} />
|
||||
<AuthorizationPermissions
|
||||
clientId={clientId}
|
||||
isDisabled={!hasManageAuthorization}
|
||||
/>
|
||||
</Tab>
|
||||
{hasViewUsers && (
|
||||
<Tab
|
||||
|
@ -596,6 +611,7 @@ export default function ClientDetails() {
|
|||
<AuthorizationEvaluate client={client} save={save} />
|
||||
</Tab>
|
||||
)}
|
||||
{hasAccess("manage-authorization") && (
|
||||
<Tab
|
||||
id="export"
|
||||
data-testid="authorizationExport"
|
||||
|
@ -604,6 +620,7 @@ export default function ClientDetails() {
|
|||
>
|
||||
<AuthorizationExport />
|
||||
</Tab>
|
||||
)}
|
||||
</RoutableTabs>
|
||||
</Tab>
|
||||
)}
|
||||
|
|
|
@ -8,11 +8,13 @@ const DECISION_STRATEGY = ["UNANIMOUS", "AFFIRMATIVE", "CONSENSUS"] as const;
|
|||
|
||||
type DecisionStrategySelectProps = {
|
||||
helpLabel?: string;
|
||||
isDisabled?: boolean;
|
||||
isLimited?: boolean;
|
||||
};
|
||||
|
||||
export const DecisionStrategySelect = ({
|
||||
helpLabel,
|
||||
isDisabled = false,
|
||||
isLimited = false,
|
||||
}: DecisionStrategySelectProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -46,6 +48,7 @@ export const DecisionStrategySelect = ({
|
|||
key={strategy}
|
||||
data-testid={strategy}
|
||||
isChecked={field.value === strategy}
|
||||
isDisabled={isDisabled}
|
||||
name="decisionStrategy"
|
||||
onChange={() => field.onChange(strategy)}
|
||||
label={t(`decisionStrategies.${strategy}`)}
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from "../routes/PermissionDetails";
|
||||
import { ResourcesPolicySelect } from "./ResourcesPolicySelect";
|
||||
import { ScopeSelect } from "./ScopeSelect";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
|
||||
type FormFields = PolicyRepresentation & {
|
||||
resourceType: string;
|
||||
|
@ -64,6 +65,9 @@ export default function PermissionDetails() {
|
|||
const { addAlert, addError } = useAlerts();
|
||||
const [permission, setPermission] = useState<PolicyRepresentation>();
|
||||
const [applyToResourceTypeFlag, setApplyToResourceTypeFlag] = useState(false);
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const isDisabled = !hasAccess("manage-authorization");
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
|
@ -192,6 +196,7 @@ export default function PermissionDetails() {
|
|||
<DropdownItem
|
||||
key="delete"
|
||||
data-testid="delete-resource"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => toggleDeleteDialog()}
|
||||
>
|
||||
{t("delete")}
|
||||
|
@ -203,7 +208,7 @@ export default function PermissionDetails() {
|
|||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="view-clients"
|
||||
role="manage-authorization"
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
|
@ -376,6 +381,7 @@ export default function PermissionDetails() {
|
|||
key={strategy}
|
||||
data-testid={strategy}
|
||||
isChecked={field.value === strategy}
|
||||
isDisabled={isDisabled}
|
||||
name="decisionStrategies"
|
||||
onChange={() => field.onChange(strategy)}
|
||||
label={t(`decisionStrategies.${strategy}`)}
|
||||
|
|
|
@ -46,6 +46,7 @@ import "./permissions.css";
|
|||
|
||||
type PermissionsProps = {
|
||||
clientId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
type ExpandablePolicyRepresentation = PolicyRepresentation & {
|
||||
|
@ -66,7 +67,10 @@ const AssociatedPoliciesRenderer = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
||||
export const AuthorizationPermissions = ({
|
||||
clientId,
|
||||
isDisabled = false,
|
||||
}: PermissionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
@ -204,6 +208,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
|||
toggle={
|
||||
<DropdownToggle
|
||||
onToggle={toggleCreate}
|
||||
isDisabled={isDisabled}
|
||||
isPrimary
|
||||
data-testid="permissionCreateDropdown"
|
||||
>
|
||||
|
@ -215,7 +220,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
|||
<DropdownItem
|
||||
data-testid="create-resource"
|
||||
key="createResourceBasedPermission"
|
||||
isDisabled={disabledCreate?.resources}
|
||||
isDisabled={isDisabled || disabledCreate?.resources}
|
||||
component="button"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
|
@ -233,7 +238,7 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
|||
<DropdownItem
|
||||
data-testid="create-scope"
|
||||
key="createScopeBasedPermission"
|
||||
isDisabled={disabledCreate?.scopes}
|
||||
isDisabled={isDisabled || disabledCreate?.scopes}
|
||||
component="button"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
|
@ -366,8 +371,8 @@ export const AuthorizationPermissions = ({ clientId }: PermissionsProps) => {
|
|||
{noData && !searching && (
|
||||
<EmptyPermissionsState
|
||||
clientId={clientId}
|
||||
isResourceEnabled={disabledCreate?.resources}
|
||||
isScopeEnabled={disabledCreate?.scopes}
|
||||
isResourceEnabled={!isDisabled && disabledCreate?.resources}
|
||||
isScopeEnabled={!isDisabled && disabledCreate?.scopes}
|
||||
/>
|
||||
)}
|
||||
{noData && searching && (
|
||||
|
|
|
@ -41,6 +41,7 @@ import { SearchDropdown, SearchForm } from "./SearchDropdown";
|
|||
|
||||
type PoliciesProps = {
|
||||
clientId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
type ExpandablePolicyRepresentation = PolicyRepresentation & {
|
||||
|
@ -61,7 +62,10 @@ const DependentPoliciesRenderer = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
||||
export const AuthorizationPolicies = ({
|
||||
clientId,
|
||||
isDisabled = false,
|
||||
}: PoliciesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
|
@ -201,7 +205,11 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
|||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button data-testid="createPolicy" onClick={toggleDialog}>
|
||||
<Button
|
||||
data-testid="createPolicy"
|
||||
onClick={toggleDialog}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{t("createPolicy")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
|
@ -254,12 +262,13 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
|||
<DependentPoliciesRenderer row={policy} />
|
||||
</Td>
|
||||
<Td>{policy.description}</Td>
|
||||
{!isDisabled && (
|
||||
<Td
|
||||
actions={{
|
||||
items: [
|
||||
{
|
||||
title: t("delete"),
|
||||
onClick: async () => {
|
||||
onClick: () => {
|
||||
setSelectedPolicy(policy);
|
||||
toggleDeleteDialog();
|
||||
},
|
||||
|
@ -267,13 +276,14 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
|||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tr>
|
||||
<Tr
|
||||
key={`child-${policy.id}`}
|
||||
isExpanded={policy.isExpanded}
|
||||
>
|
||||
<Td />
|
||||
<Td colSpan={4}>
|
||||
<Td colSpan={3 + (isDisabled ? 0 : 1)}>
|
||||
<ExpandableRowContent>
|
||||
{policy.isExpanded && (
|
||||
<DescriptionList
|
||||
|
@ -308,6 +318,7 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
|||
{noData && searching && (
|
||||
<ListEmptyState
|
||||
isSearchVariant
|
||||
isDisabled={isDisabled}
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
/>
|
||||
|
@ -330,6 +341,7 @@ export const AuthorizationPolicies = ({ clientId }: PoliciesProps) => {
|
|||
<ListEmptyState
|
||||
message={t("emptyPolicies")}
|
||||
instructions={t("emptyPoliciesInstructions")}
|
||||
isDisabled={isDisabled}
|
||||
primaryActionText={t("createPolicy")}
|
||||
onPrimaryAction={toggleDialog}
|
||||
/>
|
||||
|
|
|
@ -37,6 +37,7 @@ import { ResourceDetailsParams, toResourceDetails } from "../routes/Resource";
|
|||
import { ScopePicker } from "./ScopePicker";
|
||||
|
||||
import "./resource-details.css";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
|
||||
type SubmittedResource = Omit<
|
||||
ResourceRepresentation,
|
||||
|
@ -72,6 +73,10 @@ export default function ResourceDetails() {
|
|||
convertToFormValues(resource, setValue);
|
||||
};
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const isDisabled = !hasAccess("manage-authorization");
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
Promise.all([
|
||||
|
@ -174,6 +179,7 @@ export default function ResourceDetails() {
|
|||
<DropdownItem
|
||||
key="delete"
|
||||
data-testid="delete-resource"
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => toggleDeleteDialog()}
|
||||
>
|
||||
{t("delete")}
|
||||
|
@ -186,7 +192,7 @@ export default function ResourceDetails() {
|
|||
<FormProvider {...form}>
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="view-clients"
|
||||
role="manage-authorization"
|
||||
className="keycloak__resource-details__form"
|
||||
onSubmit={handleSubmit(submit)}
|
||||
>
|
||||
|
@ -316,7 +322,7 @@ export default function ResourceDetails() {
|
|||
}
|
||||
fieldId="resourceAttribute"
|
||||
>
|
||||
<KeyValueInput name="attributes" />
|
||||
<KeyValueInput name="attributes" isDisabled={isDisabled} />
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<div className="pf-u-mt-md">
|
||||
|
|
|
@ -37,6 +37,7 @@ import { SearchDropdown, SearchForm } from "./SearchDropdown";
|
|||
|
||||
type ResourcesProps = {
|
||||
clientId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
type ExpandableResourceRepresentation = ResourceRepresentation & {
|
||||
|
@ -49,7 +50,10 @@ const UriRenderer = ({ row }: { row: ResourceRepresentation }) => (
|
|||
</>
|
||||
);
|
||||
|
||||
export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
||||
export const AuthorizationResources = ({
|
||||
clientId,
|
||||
isDisabled = false,
|
||||
}: ResourcesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
@ -168,6 +172,7 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
|||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="createResource"
|
||||
isDisabled={isDisabled}
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
|
@ -191,8 +196,12 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
|||
<Th>{t("type")}</Th>
|
||||
<Th>{t("owner")}</Th>
|
||||
<Th>{t("uris")}</Th>
|
||||
{!isDisabled && (
|
||||
<>
|
||||
<Th aria-hidden="true" />
|
||||
<Th aria-hidden="true" />
|
||||
</>
|
||||
)}
|
||||
</Tr>
|
||||
</Thead>
|
||||
{resources.map((resource, rowIndex) => (
|
||||
|
@ -232,6 +241,8 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
|||
<Td>
|
||||
<UriRenderer row={resource} />
|
||||
</Td>
|
||||
{!isDisabled && (
|
||||
<>
|
||||
<Td width={10}>
|
||||
<Button
|
||||
variant="link"
|
||||
|
@ -266,6 +277,8 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
|||
],
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Tr>
|
||||
<Tr
|
||||
key={`child-${resource._id}`}
|
||||
|
@ -301,6 +314,7 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
|
|||
<ListEmptyState
|
||||
message={t("emptyResources")}
|
||||
instructions={t("emptyResourcesInstructions")}
|
||||
isDisabled={isDisabled}
|
||||
primaryActionText={t("createResource")}
|
||||
onPrimaryAction={() =>
|
||||
navigate(toCreateResource({ realm, id: clientId }))
|
||||
|
|
|
@ -125,7 +125,7 @@ export default function ScopeDetails() {
|
|||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="view-clients"
|
||||
role="manage-authorization"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormGroup
|
||||
|
|
|
@ -36,6 +36,7 @@ import { DetailDescriptionLink } from "./DetailDescription";
|
|||
|
||||
type ScopesProps = {
|
||||
clientId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type PermissionScopeRepresentation = ScopeRepresentation & {
|
||||
|
@ -48,7 +49,10 @@ type ExpandableRow = {
|
|||
isExpanded: boolean;
|
||||
};
|
||||
|
||||
export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
|
||||
export const AuthorizationScopes = ({
|
||||
clientId,
|
||||
isDisabled = false,
|
||||
}: ScopesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { realm } = useRealm();
|
||||
|
@ -305,6 +309,7 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
|
|||
<ListEmptyState
|
||||
message={t("emptyAuthorizationScopes")}
|
||||
instructions={t("emptyAuthorizationInstructions")}
|
||||
isDisabled={isDisabled}
|
||||
onPrimaryAction={() => navigate(toNewScope({ id: clientId, realm }))}
|
||||
primaryActionText={t("createAuthorizationScope")}
|
||||
/>
|
||||
|
@ -312,6 +317,7 @@ export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
|
|||
{noData && searching && (
|
||||
<ListEmptyState
|
||||
isSearchVariant
|
||||
isDisabled={isDisabled}
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
/>
|
||||
|
|
|
@ -22,6 +22,7 @@ import useToggle from "../../utils/useToggle";
|
|||
import { DecisionStrategySelect } from "./DecisionStrategySelect";
|
||||
import { ImportDialog } from "./ImportDialog";
|
||||
import { useFetch } from "../../utils/useFetch";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
|
||||
const POLICY_ENFORCEMENT_MODES = [
|
||||
"ENFORCING",
|
||||
|
@ -43,6 +44,9 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
|||
const { control, reset, handleSubmit } = form;
|
||||
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { hasAccess } = useAccess();
|
||||
|
||||
const isDisabled = !hasAccess("manage-authorization");
|
||||
|
||||
useFetch(
|
||||
() => adminClient.clients.getResourceServer({ id: clientId }),
|
||||
|
@ -88,7 +92,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
|||
/>
|
||||
)}
|
||||
<FormAccess
|
||||
role="view-clients"
|
||||
role="manage-authorization"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
|
@ -128,6 +132,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
|||
key={mode}
|
||||
data-testid={mode}
|
||||
isChecked={field.value === mode}
|
||||
isDisabled={isDisabled}
|
||||
name="policyEnforcementMode"
|
||||
onChange={() => field.onChange(mode)}
|
||||
label={t(`policyEnforcementModes.${mode}`)}
|
||||
|
|
|
@ -25,7 +25,8 @@ export const AuthorizationRoute: AppRouteObject = {
|
|||
element: <ClientDetails />,
|
||||
breadcrumb: (t) => t("clientSettings"),
|
||||
handle: {
|
||||
access: "manage-authorization",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("view-authorization", "manage-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@ export const NewPermissionRoute: AppRouteObject = {
|
|||
element: <PermissionDetails />,
|
||||
breadcrumb: (t) => t("createPermission"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "manage-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ export const NewPolicyRoute: AppRouteObject = {
|
|||
element: <PolicyDetails />,
|
||||
breadcrumb: (t) => t("createPolicy"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "manage-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ export const NewResourceRoute: AppRouteObject = {
|
|||
element: <ResourceDetails />,
|
||||
breadcrumb: (t) => t("createResource"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "manage-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ export const NewScopeRoute: AppRouteObject = {
|
|||
element: <ScopeDetails />,
|
||||
breadcrumb: (t) => t("createAuthorizationScope"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "manage-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,8 @@ export const PermissionDetailsRoute: AppRouteObject = {
|
|||
element: <PermissionDetails />,
|
||||
breadcrumb: (t) => t("permissionDetails"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "view-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ export const PolicyDetailsRoute: AppRouteObject = {
|
|||
element: <PolicyDetails />,
|
||||
breadcrumb: (t) => t("policyDetails"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "view-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ export const ResourceDetailsRoute: AppRouteObject = {
|
|||
element: <ResourceDetails />,
|
||||
breadcrumb: (t) => t("resourceDetails"),
|
||||
handle: {
|
||||
access: "view-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "view-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ export const ScopeDetailsRoute: AppRouteObject = {
|
|||
element: <ScopeDetails />,
|
||||
breadcrumb: (t) => t("authorizationScopeDetails"),
|
||||
handle: {
|
||||
access: "manage-clients",
|
||||
access: (accessChecker) =>
|
||||
accessChecker.hasAny("manage-clients", "view-authorization"),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
import { Controller } from "react-hook-form";
|
||||
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { FixedButtonsGroup } from "./FixedButtonGroup";
|
||||
|
||||
export type FormAccessProps = FormProps & {
|
||||
/**
|
||||
|
@ -89,7 +90,13 @@ export const FormAccess = ({
|
|||
element.props.children,
|
||||
newProps,
|
||||
);
|
||||
if (child.type === TextArea) {
|
||||
switch (child.type) {
|
||||
case FixedButtonsGroup:
|
||||
return cloneElement(child, {
|
||||
isActive: !newProps.isDisabled,
|
||||
children,
|
||||
} as any);
|
||||
case TextArea:
|
||||
return cloneElement(child, {
|
||||
readOnly: newProps.isDisabled,
|
||||
children,
|
||||
|
|
|
@ -32,11 +32,13 @@ export type DefaultValue = {
|
|||
type KeyValueInputProps = {
|
||||
name: string;
|
||||
defaultKeyValue?: DefaultValue[];
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const KeyValueInput = ({
|
||||
name,
|
||||
defaultKeyValue,
|
||||
isDisabled = false,
|
||||
}: KeyValueInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
|
@ -89,6 +91,7 @@ export const KeyValueInput = ({
|
|||
{...register(`${name}.${index}.key`, { required: true })}
|
||||
validated={keyError ? "error" : "default"}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
{keyError && (
|
||||
|
@ -115,6 +118,7 @@ export const KeyValueInput = ({
|
|||
{...register(`${name}.${index}.value`, { required: true })}
|
||||
validated={valueError ? "error" : "default"}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
{valueError && (
|
||||
|
@ -131,6 +135,7 @@ export const KeyValueInput = ({
|
|||
title={t("removeAttribute")}
|
||||
onClick={() => remove(index)}
|
||||
data-testid={`${name}-remove`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
<MinusCircleIcon />
|
||||
</Button>
|
||||
|
@ -147,6 +152,7 @@ export const KeyValueInput = ({
|
|||
variant="link"
|
||||
icon={<PlusCircleIcon />}
|
||||
onClick={appendNew}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{t("addAttribute")}
|
||||
</Button>
|
||||
|
@ -166,6 +172,7 @@ export const KeyValueInput = ({
|
|||
icon={<PlusCircleIcon />}
|
||||
isSmall
|
||||
onClick={appendNew}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{t("addAttribute")}
|
||||
</Button>
|
||||
|
|
|
@ -26,6 +26,7 @@ export type ListEmptyStateProps = {
|
|||
icon?: ComponentClass<SVGIconProps>;
|
||||
isSearchVariant?: boolean;
|
||||
secondaryActions?: Action[];
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const ListEmptyState = ({
|
||||
|
@ -37,6 +38,7 @@ export const ListEmptyState = ({
|
|||
primaryActionText,
|
||||
secondaryActions,
|
||||
icon,
|
||||
isDisabled = false,
|
||||
}: ListEmptyStateProps) => {
|
||||
return (
|
||||
<EmptyState data-testid="empty-state" variant="large">
|
||||
|
@ -56,6 +58,7 @@ export const ListEmptyState = ({
|
|||
.toLowerCase()}-empty-action`}
|
||||
variant="primary"
|
||||
onClick={onPrimaryAction}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{primaryActionText}
|
||||
</Button>
|
||||
|
@ -70,6 +73,7 @@ export const ListEmptyState = ({
|
|||
.toLowerCase()}-empty-action`}
|
||||
variant={action.type || ButtonVariant.secondary}
|
||||
onClick={action.onClick}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{action.text}
|
||||
</Button>
|
||||
|
|
|
@ -27,12 +27,24 @@ export const AccessContextProvider = ({ children }: PropsWithChildren) => {
|
|||
}
|
||||
}, [whoAmI, realm]);
|
||||
|
||||
const hasAccess = (...types: AccessType[]) => {
|
||||
return types.every((type) => type === "anyone" || access.includes(type));
|
||||
const hasAccess = (...types: AccessType[]): boolean => {
|
||||
return types.every(
|
||||
(type) =>
|
||||
type === "anyone" ||
|
||||
(typeof type === "function" &&
|
||||
type({ hasAll: hasAccess, hasAny: hasSomeAccess })) ||
|
||||
access.includes(type),
|
||||
);
|
||||
};
|
||||
|
||||
const hasSomeAccess = (...types: AccessType[]) => {
|
||||
return types.some((type) => type === "anyone" || access.includes(type));
|
||||
const hasSomeAccess = (...types: AccessType[]): boolean => {
|
||||
return types.some(
|
||||
(type) =>
|
||||
type === "anyone" ||
|
||||
(typeof type === "function" &&
|
||||
type({ hasAll: hasAccess, hasAny: hasSomeAccess })) ||
|
||||
access.includes(type),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
export type AccessChecker = {
|
||||
hasAll: (...types: AccessType[]) => boolean;
|
||||
hasAny: (...types: AccessType[]) => boolean;
|
||||
};
|
||||
export type AccessTypeFunc = (accessChecker: AccessChecker) => boolean;
|
||||
export type AccessType =
|
||||
| "view-realm"
|
||||
| "view-identity-providers"
|
||||
|
@ -17,7 +22,8 @@ export type AccessType =
|
|||
| "manage-authorization"
|
||||
| "manage-clients"
|
||||
| "query-groups"
|
||||
| "anyone";
|
||||
| "anyone"
|
||||
| AccessTypeFunc;
|
||||
|
||||
export default interface WhoAmIRepresentation {
|
||||
userId: string;
|
||||
|
|
Loading…
Reference in a new issue