Fix role-based authorization in Clients section. (#2632)
This commit is contained in:
parent
0350a2ccf4
commit
f4cfb23a3c
10 changed files with 244 additions and 151 deletions
|
@ -71,6 +71,7 @@ import { AuthorizationExport } from "./authorization/AuthorizationExport";
|
|||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
|
||||
import { keyValueToArray } from "../components/key-value-form/key-value-convert";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
|
||||
type ClientDetailHeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
|
@ -125,6 +126,9 @@ const ClientDetailHeader = ({
|
|||
return [{ text }];
|
||||
}, [client, t]);
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-clients");
|
||||
|
||||
const dropdownItems = [
|
||||
<DropdownItem key="download" onClick={toggleDownloadDialog}>
|
||||
{t("downloadAdapterConfig")}
|
||||
|
@ -132,7 +136,7 @@ const ClientDetailHeader = ({
|
|||
<DropdownItem key="export" onClick={() => exportClient(client)}>
|
||||
{t("common:export")}
|
||||
</DropdownItem>,
|
||||
...(!isRealmClient(client)
|
||||
...(!isRealmClient(client) && isManager
|
||||
? [
|
||||
<Divider key="divider" />,
|
||||
<DropdownItem
|
||||
|
@ -154,6 +158,7 @@ const ClientDetailHeader = ({
|
|||
subKey="clients:clientsExplain"
|
||||
badges={badges}
|
||||
divider={false}
|
||||
isReadOnly={!isManager}
|
||||
helpTextKey="clients-help:enableDisable"
|
||||
dropdownItems={dropdownItems}
|
||||
isEnabled={value}
|
||||
|
@ -182,6 +187,14 @@ export default function ClientDetails() {
|
|||
const { realm } = useRealm();
|
||||
const { profileInfo } = useServerInfo();
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-clients");
|
||||
const canViewPermissions = hasAccess(
|
||||
"manage-authorization",
|
||||
"manage-clients"
|
||||
);
|
||||
const canViewServiceAccountRoles = hasAccess("view-users");
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [downloadDialogOpen, toggleDownloadDialogOpen] = useToggle();
|
||||
|
@ -419,6 +432,7 @@ export default function ClientDetails() {
|
|||
loader={loader}
|
||||
paginated={false}
|
||||
messageBundle="clients"
|
||||
isReadOnly={!isManager}
|
||||
/>
|
||||
</Tab>
|
||||
{!isRealmClient(client) && !client.bearerOnly && (
|
||||
|
@ -550,7 +564,7 @@ export default function ClientDetails() {
|
|||
</RoutableTabs>
|
||||
</Tab>
|
||||
)}
|
||||
{client!.serviceAccountsEnabled && (
|
||||
{client!.serviceAccountsEnabled && canViewServiceAccountRoles && (
|
||||
<Tab
|
||||
id="serviceAccount"
|
||||
data-testid="serviceAccountTab"
|
||||
|
@ -563,7 +577,7 @@ export default function ClientDetails() {
|
|||
{!profileInfo?.disabledFeatures?.includes(
|
||||
"ADMIN_FINE_GRAINED_AUTHZ"
|
||||
) &&
|
||||
client.access?.manage && (
|
||||
canViewPermissions && (
|
||||
<Tab
|
||||
id="permissions"
|
||||
data-testid="permissionsTab"
|
||||
|
|
|
@ -26,6 +26,7 @@ import { SamlConfig } from "./add/SamlConfig";
|
|||
import { SamlSignature } from "./add/SamlSignature";
|
||||
import environment from "../environment";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
|
||||
type ClientSettingsProps = {
|
||||
client: ClientRepresentation;
|
||||
|
@ -47,6 +48,9 @@ export const ClientSettings = ({
|
|||
const { t } = useTranslation("clients");
|
||||
const { realm } = useRealm();
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-clients");
|
||||
|
||||
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
|
||||
const loginThemes = useServerInfo().themes!["login"];
|
||||
const consentRequired = watch("consentRequired");
|
||||
|
@ -246,6 +250,7 @@ export const ClientSettings = ({
|
|||
name="settings"
|
||||
save={save}
|
||||
reset={reset}
|
||||
isActive={!isManager}
|
||||
/>
|
||||
)}
|
||||
</FormAccess>
|
||||
|
@ -527,6 +532,7 @@ export const ClientSettings = ({
|
|||
name="settings"
|
||||
save={save}
|
||||
reset={reset}
|
||||
isActive={isManager}
|
||||
/>
|
||||
</FormAccess>
|
||||
</ScrollForm>
|
||||
|
|
|
@ -32,6 +32,7 @@ import { toClient } from "./routes/Client";
|
|||
import { toImportClient } from "./routes/ImportClient";
|
||||
import { isRealmClient, getProtocolName } from "./utils";
|
||||
import helpUrls from "../help-urls";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
|
||||
export default function ClientsSection() {
|
||||
const { t } = useTranslation("clients");
|
||||
|
@ -45,6 +46,9 @@ export default function ClientsSection() {
|
|||
const refresh = () => setKey(new Date().getTime());
|
||||
const [selectedClient, setSelectedClient] = useState<ClientRepresentation>();
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-clients");
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const params: ClientQuery = {
|
||||
first: first!,
|
||||
|
@ -95,6 +99,34 @@ export default function ClientsSection() {
|
|||
</TableText>
|
||||
);
|
||||
|
||||
const ToolbarItems = () => {
|
||||
if (!isManager) return <span />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
component={(props) => (
|
||||
<Link {...props} to={toAddClient({ realm })} />
|
||||
)}
|
||||
>
|
||||
{t("createClient")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
component={(props) => (
|
||||
<Link {...props} to={toImportClient({ realm })} />
|
||||
)}
|
||||
variant="link"
|
||||
>
|
||||
{t("importClient")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader
|
||||
|
@ -117,29 +149,7 @@ export default function ClientsSection() {
|
|||
isPaginated
|
||||
ariaLabelKey="clients:clientList"
|
||||
searchPlaceholderKey="clients:searchForClient"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
component={(props) => (
|
||||
<Link {...props} to={toAddClient({ realm })} />
|
||||
)}
|
||||
>
|
||||
{t("createClient")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
component={(props) => (
|
||||
<Link {...props} to={toImportClient({ realm })} />
|
||||
)}
|
||||
variant="link"
|
||||
>
|
||||
{t("importClient")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
toolbarItem={<ToolbarItems />}
|
||||
actionResolver={(rowData: IRowData) => {
|
||||
const client: ClientRepresentation = rowData.data;
|
||||
const actions: Action<ClientRepresentation>[] = [
|
||||
|
@ -151,7 +161,7 @@ export default function ClientsSection() {
|
|||
},
|
||||
];
|
||||
|
||||
if (!isRealmClient(client)) {
|
||||
if (!isRealmClient(client) && isManager) {
|
||||
actions.push({
|
||||
title: t("common:delete"),
|
||||
onClick() {
|
||||
|
|
|
@ -6,16 +6,29 @@ type SaveResetProps = ActionGroupProps & {
|
|||
name: string;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const SaveReset = ({ name, save, reset, ...rest }: SaveResetProps) => {
|
||||
export const SaveReset = ({
|
||||
name,
|
||||
save,
|
||||
reset,
|
||||
isActive = true,
|
||||
...rest
|
||||
}: SaveResetProps) => {
|
||||
console.log(isActive);
|
||||
const { t } = useTranslation("common");
|
||||
return (
|
||||
<ActionGroup {...rest}>
|
||||
<Button data-testid={name + "Save"} onClick={save}>
|
||||
<Button isDisabled={!isActive} data-testid={name + "Save"} onClick={save}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button data-testid={name + "Revert"} variant="link" onClick={reset}>
|
||||
<Button
|
||||
isDisabled={!isActive}
|
||||
data-testid={name + "Revert"}
|
||||
variant="link"
|
||||
onClick={reset}
|
||||
>
|
||||
{t("revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
|
|
|
@ -82,7 +82,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
|||
closeDialog={toggleImportDialog}
|
||||
/>
|
||||
)}
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormAccess role="view-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("import")}
|
||||
fieldId="import"
|
||||
|
@ -166,6 +166,7 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
|
|||
name="authenticationSettings"
|
||||
save={() => handleSubmit(save)()}
|
||||
reset={() => reset(resource)}
|
||||
isActive
|
||||
/>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
|
|
|
@ -40,6 +40,8 @@ import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
|
|||
|
||||
import "./client-scopes.css";
|
||||
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
|
||||
export type ClientScopesProps = {
|
||||
clientId: string;
|
||||
protocol: string;
|
||||
|
@ -80,6 +82,9 @@ export const ClientScopes = ({
|
|||
const refresh = () => setKey(key + 1);
|
||||
const isDedicatedRow = (value: Row) => value.id === DEDICATED_ROW;
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-clients");
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const defaultClientScopes =
|
||||
await adminClient.clients.listDefaultClientScopes({ id: clientId });
|
||||
|
@ -141,7 +146,7 @@ export const ClientScopes = ({
|
|||
|
||||
const TypeSelector = (scope: Row) => (
|
||||
<CellDropdown
|
||||
isDisabled={isDedicatedRow(scope)}
|
||||
isDisabled={isDedicatedRow(scope) || !isManager}
|
||||
clientScope={scope}
|
||||
type={scope.type}
|
||||
onSelect={async (value) => {
|
||||
|
@ -162,6 +167,65 @@ export const ClientScopes = ({
|
|||
/>
|
||||
);
|
||||
|
||||
const ManagerToolbarItems = () => {
|
||||
if (!isManager) return <span />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
{t("addClientScope")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<ChangeTypeDropdown
|
||||
clientId={clientId}
|
||||
selectedRows={selectedRows}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
toggle={<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />}
|
||||
isOpen={kebabOpen}
|
||||
isPlain
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="deleteAll"
|
||||
isDisabled={selectedRows.length === 0}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map(async (row) => {
|
||||
await removeClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
{ ...row },
|
||||
row.type as ClientScope
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
setKebabOpen(false);
|
||||
addAlert(
|
||||
t("clients:clientScopeRemoveSuccess"),
|
||||
AlertVariant.success
|
||||
);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("clients:clientScopeRemoveError", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("common:remove")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{rest && (
|
||||
|
@ -220,58 +284,7 @@ export const ClientScopes = ({
|
|||
refresh();
|
||||
}}
|
||||
/>
|
||||
<ToolbarItem>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
{t("addClientScope")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<ChangeTypeDropdown
|
||||
clientId={clientId}
|
||||
selectedRows={selectedRows}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
toggle={
|
||||
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
|
||||
}
|
||||
isOpen={kebabOpen}
|
||||
isPlain
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="deleteAll"
|
||||
isDisabled={selectedRows.length === 0}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map(async (row) => {
|
||||
await removeClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
{ ...row },
|
||||
row.type as ClientScope
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
setKebabOpen(false);
|
||||
addAlert(
|
||||
t("clients:clientScopeRemoveSuccess"),
|
||||
AlertVariant.success
|
||||
);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("clients:clientScopeRemoveError", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("common:remove")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ManagerToolbarItems />
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
|
@ -296,7 +309,9 @@ export const ClientScopes = ({
|
|||
},
|
||||
{ name: "description" },
|
||||
]}
|
||||
actions={[
|
||||
actions={
|
||||
isManager
|
||||
? [
|
||||
{
|
||||
title: t("common:remove"),
|
||||
onRowClick: async (row) => {
|
||||
|
@ -318,7 +333,9 @@ export const ClientScopes = ({
|
|||
return true;
|
||||
},
|
||||
},
|
||||
]}
|
||||
]
|
||||
: []
|
||||
}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("clients:emptyClientScopes")}
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { toUser } from "../../user/routes/User";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
|
||||
import "./service-account.css";
|
||||
|
||||
|
@ -33,6 +34,9 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
|||
const [hide, setHide] = useState(false);
|
||||
const [serviceAccount, setServiceAccount] = useState<UserRepresentation>();
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-clients");
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
adminClient.clients.getServiceAccountUser({
|
||||
|
@ -124,6 +128,7 @@ export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
|||
name={client.clientId!}
|
||||
id={serviceAccount.id!}
|
||||
type="users"
|
||||
isManager={isManager}
|
||||
loader={loader}
|
||||
save={assignRoles}
|
||||
onHideRolesToggle={() => setHide(!hide)}
|
||||
|
|
|
@ -79,6 +79,7 @@ type RoleMappingProps = {
|
|||
name: string;
|
||||
id: string;
|
||||
type: ResourcesKey;
|
||||
isManager?: boolean;
|
||||
loader: () => Promise<Row[]>;
|
||||
save: (rows: Row[]) => Promise<void>;
|
||||
onHideRolesToggle: () => void;
|
||||
|
@ -160,6 +161,7 @@ export const RoleMapping = ({
|
|||
name,
|
||||
id,
|
||||
type,
|
||||
isManager = true,
|
||||
loader,
|
||||
save,
|
||||
onHideRolesToggle,
|
||||
|
@ -216,6 +218,30 @@ export const RoleMapping = ({
|
|||
},
|
||||
});
|
||||
|
||||
const ManagerToolbarItems = () => {
|
||||
if (!isManager) return <span />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button data-testid="assignRole" onClick={() => setShowAssign(true)}>
|
||||
{t("common:assignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="unAssignRole"
|
||||
onClick={toggleDeleteDialog}
|
||||
isDisabled={selected.length === 0}
|
||||
>
|
||||
{t("common:unAssignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{showAssign && (
|
||||
|
@ -253,27 +279,12 @@ export const RoleMapping = ({
|
|||
}}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="assignRole"
|
||||
onClick={() => setShowAssign(true)}
|
||||
>
|
||||
{t("common:assignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="link"
|
||||
data-testid="unAssignRole"
|
||||
onClick={toggleDeleteDialog}
|
||||
isDisabled={selected.length === 0}
|
||||
>
|
||||
{t("common:unAssignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ManagerToolbarItems />
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
actions={
|
||||
isManager
|
||||
? [
|
||||
{
|
||||
title: t("common:unAssignRole"),
|
||||
onRowClick: async (role) => {
|
||||
|
@ -282,7 +293,9 @@ export const RoleMapping = ({
|
|||
return false;
|
||||
},
|
||||
},
|
||||
]}
|
||||
]
|
||||
: []
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "role.name",
|
||||
|
|
|
@ -44,6 +44,7 @@ export type ViewHeaderProps = {
|
|||
onToggle?: (value: boolean) => void;
|
||||
divider?: boolean;
|
||||
helpTextKey?: string;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
export type ViewHeaderBadge = {
|
||||
|
@ -68,6 +69,7 @@ export const ViewHeader = ({
|
|||
onToggle,
|
||||
divider = true,
|
||||
helpTextKey,
|
||||
isReadOnly = false,
|
||||
}: ViewHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { enabled } = useHelp();
|
||||
|
@ -124,6 +126,7 @@ export const ViewHeader = ({
|
|||
label={t("common:enabled")}
|
||||
labelOff={t("common:disabled")}
|
||||
className="pf-u-mr-lg"
|
||||
isDisabled={isReadOnly}
|
||||
isChecked={isEnabled}
|
||||
onChange={(value) => {
|
||||
onToggle(value);
|
||||
|
|
|
@ -25,6 +25,7 @@ type RolesListProps = {
|
|||
paginated?: boolean;
|
||||
parentRoleId?: string;
|
||||
messageBundle?: string;
|
||||
isReadOnly?: boolean;
|
||||
loader?: (
|
||||
first?: number,
|
||||
max?: number,
|
||||
|
@ -51,6 +52,7 @@ export const RolesList = ({
|
|||
paginated = true,
|
||||
parentRoleId,
|
||||
messageBundle = "roles",
|
||||
isReadOnly = false,
|
||||
}: RolesListProps) => {
|
||||
const { t } = useTranslation(messageBundle);
|
||||
const history = useHistory();
|
||||
|
@ -129,21 +131,30 @@ export const RolesList = ({
|
|||
searchPlaceholderKey="roles:searchFor"
|
||||
isPaginated={paginated}
|
||||
toolbarItem={
|
||||
!isReadOnly && (
|
||||
<Button data-testid="create-role" onClick={goToCreate}>
|
||||
{t("createRole")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
actions={[
|
||||
actions={
|
||||
isReadOnly
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: (role) => {
|
||||
setSelectedRole(role);
|
||||
if (role.name === realm!.defaultRole!.name) {
|
||||
addAlert(t("defaultRoleDeleteError"), AlertVariant.danger);
|
||||
addAlert(
|
||||
t("defaultRoleDeleteError"),
|
||||
AlertVariant.danger
|
||||
);
|
||||
} else toggleDeleteDialog();
|
||||
},
|
||||
},
|
||||
]}
|
||||
]
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
|
@ -165,8 +176,8 @@ export const RolesList = ({
|
|||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
message={t("noRoles")}
|
||||
instructions={t("noRolesInstructions")}
|
||||
primaryActionText={t("createRole")}
|
||||
instructions={isReadOnly ? "" : t("noRolesInstructions")}
|
||||
primaryActionText={isReadOnly ? "" : t("createRole")}
|
||||
onPrimaryAction={goToCreate}
|
||||
/>
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue