Fix role-based authorization in Clients section. (#2632)

This commit is contained in:
Stan Silvert 2022-05-17 03:52:19 -04:00 committed by GitHub
parent 0350a2ccf4
commit f4cfb23a3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 244 additions and 151 deletions

View file

@ -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"

View file

@ -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>

View file

@ -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() {

View file

@ -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>

View file

@ -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>

View file

@ -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,29 +309,33 @@ export const ClientScopes = ({
},
{ name: "description" },
]}
actions={[
{
title: t("common:remove"),
onRowClick: async (row) => {
try {
await removeClientScope(
adminClient,
clientId,
row,
row.type as ClientScope
);
addAlert(
t("clients:clientScopeRemoveSuccess"),
AlertVariant.success
);
refresh();
} catch (error) {
addError("clients:clientScopeRemoveError", error);
}
return true;
},
},
]}
actions={
isManager
? [
{
title: t("common:remove"),
onRowClick: async (row) => {
try {
await removeClientScope(
adminClient,
clientId,
row,
row.type as ClientScope
);
addAlert(
t("clients:clientScopeRemoveSuccess"),
AlertVariant.success
);
refresh();
} catch (error) {
addError("clients:clientScopeRemoveError", error);
}
return true;
},
},
]
: []
}
emptyState={
<ListEmptyState
message={t("clients:emptyClientScopes")}

View file

@ -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)}

View file

@ -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,36 +279,23 @@ 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={[
{
title: t("common:unAssignRole"),
onRowClick: async (role) => {
setSelected([role]);
toggleDeleteDialog();
return false;
},
},
]}
actions={
isManager
? [
{
title: t("common:unAssignRole"),
onRowClick: async (role) => {
setSelected([role]);
toggleDeleteDialog();
return false;
},
},
]
: []
}
columns={[
{
name: "role.name",

View file

@ -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);

View file

@ -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={
<Button data-testid="create-role" onClick={goToCreate}>
{t("createRole")}
</Button>
!isReadOnly && (
<Button data-testid="create-role" onClick={goToCreate}>
{t("createRole")}
</Button>
)
}
actions={
isReadOnly
? []
: [
{
title: t("common:delete"),
onRowClick: (role) => {
setSelectedRole(role);
if (role.name === realm!.defaultRole!.name) {
addAlert(
t("defaultRoleDeleteError"),
AlertVariant.danger
);
} else toggleDeleteDialog();
},
},
]
}
actions={[
{
title: t("common:delete"),
onRowClick: (role) => {
setSelectedRole(role);
if (role.name === realm!.defaultRole!.name) {
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}
/>
}