2022-01-04 09:17:43 +00:00
|
|
|
import React, { useState } from "react";
|
|
|
|
import { Link, useHistory } from "react-router-dom";
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import {
|
|
|
|
Button,
|
|
|
|
DescriptionList,
|
|
|
|
PageSection,
|
|
|
|
ToolbarItem,
|
|
|
|
} from "@patternfly/react-core";
|
|
|
|
import {
|
|
|
|
ExpandableRowContent,
|
|
|
|
TableComposable,
|
|
|
|
Tbody,
|
|
|
|
Td,
|
|
|
|
Th,
|
|
|
|
Thead,
|
|
|
|
Tr,
|
|
|
|
} from "@patternfly/react-table";
|
|
|
|
|
|
|
|
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
|
|
|
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
|
|
|
|
|
|
|
|
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
|
|
|
import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar";
|
|
|
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
|
|
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
|
|
|
import { MoreLabel } from "./MoreLabel";
|
|
|
|
import { toScopeDetails } from "../routes/Scope";
|
|
|
|
import { toNewScope } from "../routes/NewScope";
|
|
|
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
|
|
|
import useToggle from "../../utils/useToggle";
|
|
|
|
import { DeleteScopeDialog } from "./DeleteScopeDialog";
|
2022-03-21 06:45:49 +00:00
|
|
|
import { DetailDescriptionLink } from "./DetailDescription";
|
2022-01-19 10:11:40 +00:00
|
|
|
import { toNewPermission } from "../routes/NewPermission";
|
2022-03-21 06:45:49 +00:00
|
|
|
import { toResourceDetails } from "../routes/Resource";
|
|
|
|
import { toPermissionDetails } from "../routes/PermissionDetails";
|
2022-01-04 09:17:43 +00:00
|
|
|
|
|
|
|
type ScopesProps = {
|
|
|
|
clientId: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type ExpandableScopeRepresentation = ScopeRepresentation & {
|
|
|
|
permissions?: PolicyRepresentation[];
|
|
|
|
isExpanded: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
|
|
|
|
const { t } = useTranslation("clients");
|
|
|
|
const history = useHistory();
|
|
|
|
const adminClient = useAdminClient();
|
|
|
|
const { realm } = useRealm();
|
|
|
|
|
|
|
|
const [deleteDialog, toggleDeleteDialog] = useToggle();
|
|
|
|
const [scopes, setScopes] = useState<ExpandableScopeRepresentation[]>();
|
|
|
|
const [selectedScope, setSelectedScope] =
|
|
|
|
useState<ExpandableScopeRepresentation>();
|
|
|
|
|
|
|
|
const [key, setKey] = useState(0);
|
|
|
|
const refresh = () => setKey(key + 1);
|
|
|
|
|
|
|
|
const [max, setMax] = useState(10);
|
|
|
|
const [first, setFirst] = useState(0);
|
2022-03-16 13:57:50 +00:00
|
|
|
const [search, setSearch] = useState("");
|
2022-01-04 09:17:43 +00:00
|
|
|
|
|
|
|
useFetch(
|
|
|
|
async () => {
|
|
|
|
const params = {
|
|
|
|
first,
|
2022-04-21 14:53:30 +00:00
|
|
|
max: max + 1,
|
2022-01-04 09:17:43 +00:00
|
|
|
deep: false,
|
2022-03-16 13:57:50 +00:00
|
|
|
name: search,
|
2022-01-04 09:17:43 +00:00
|
|
|
};
|
|
|
|
const scopes = await adminClient.clients.listAllScopes({
|
|
|
|
...params,
|
|
|
|
id: clientId,
|
|
|
|
});
|
|
|
|
|
|
|
|
return await Promise.all(
|
|
|
|
scopes.map(async (scope) => {
|
|
|
|
const options = { id: clientId, scopeId: scope.id! };
|
|
|
|
const [resources, permissions] = await Promise.all([
|
|
|
|
adminClient.clients.listAllResourcesByScope(options),
|
|
|
|
adminClient.clients.listAllPermissionsByScope(options),
|
|
|
|
]);
|
|
|
|
|
|
|
|
return {
|
|
|
|
...scope,
|
|
|
|
resources,
|
|
|
|
permissions,
|
|
|
|
isExpanded: false,
|
|
|
|
};
|
|
|
|
})
|
|
|
|
);
|
|
|
|
},
|
|
|
|
setScopes,
|
2022-04-21 14:53:30 +00:00
|
|
|
[key, search, first, max]
|
2022-01-04 09:17:43 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
const ResourceRenderer = ({
|
|
|
|
row,
|
|
|
|
}: {
|
|
|
|
row: ExpandableScopeRepresentation;
|
|
|
|
}) => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{row.resources?.[0]?.name} <MoreLabel array={row.resources} />
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const PermissionsRenderer = ({
|
|
|
|
row,
|
|
|
|
}: {
|
|
|
|
row: ExpandableScopeRepresentation;
|
|
|
|
}) => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{row.permissions?.[0]?.name} <MoreLabel array={row.permissions} />
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!scopes) {
|
|
|
|
return <KeycloakSpinner />;
|
|
|
|
}
|
|
|
|
|
2022-03-16 13:57:50 +00:00
|
|
|
const noData = scopes.length === 0;
|
|
|
|
const searching = search !== "";
|
2022-01-04 09:17:43 +00:00
|
|
|
return (
|
|
|
|
<PageSection variant="light" className="pf-u-p-0">
|
|
|
|
<DeleteScopeDialog
|
|
|
|
clientId={clientId}
|
|
|
|
open={deleteDialog}
|
|
|
|
toggleDialog={toggleDeleteDialog}
|
|
|
|
selectedScope={selectedScope}
|
|
|
|
refresh={refresh}
|
|
|
|
/>
|
2022-03-16 13:57:50 +00:00
|
|
|
{(!noData || searching) && (
|
2022-01-04 09:17:43 +00:00
|
|
|
<PaginatingTableToolbar
|
|
|
|
count={scopes.length}
|
|
|
|
first={first}
|
|
|
|
max={max}
|
|
|
|
onNextClick={setFirst}
|
|
|
|
onPreviousClick={setFirst}
|
|
|
|
onPerPageSelect={(first, max) => {
|
|
|
|
setFirst(first);
|
|
|
|
setMax(max);
|
|
|
|
}}
|
2022-03-16 13:57:50 +00:00
|
|
|
inputGroupName="search"
|
|
|
|
inputGroupPlaceholder={t("searchByName")}
|
|
|
|
inputGroupOnEnter={setSearch}
|
2022-01-04 09:17:43 +00:00
|
|
|
toolbarItem={
|
|
|
|
<ToolbarItem>
|
|
|
|
<Button
|
|
|
|
data-testid="createAuthorizationScope"
|
|
|
|
component={(props) => (
|
|
|
|
<Link {...props} to={toNewScope({ realm, id: clientId })} />
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{t("createAuthorizationScope")}
|
|
|
|
</Button>
|
|
|
|
</ToolbarItem>
|
|
|
|
}
|
|
|
|
>
|
2022-03-16 13:57:50 +00:00
|
|
|
{!noData && (
|
|
|
|
<TableComposable aria-label={t("scopes")} variant="compact">
|
|
|
|
<Thead>
|
2022-01-04 09:17:43 +00:00
|
|
|
<Tr>
|
2022-03-16 13:57:50 +00:00
|
|
|
<Th />
|
|
|
|
<Th>{t("common:name")}</Th>
|
|
|
|
<Th>{t("resources")}</Th>
|
|
|
|
<Th>{t("permissions")}</Th>
|
|
|
|
<Th />
|
|
|
|
<Th />
|
2022-01-04 09:17:43 +00:00
|
|
|
</Tr>
|
2022-03-16 13:57:50 +00:00
|
|
|
</Thead>
|
|
|
|
{scopes.map((scope, rowIndex) => (
|
|
|
|
<Tbody key={scope.id} isExpanded={scope.isExpanded}>
|
|
|
|
<Tr>
|
|
|
|
<Td
|
|
|
|
expand={{
|
|
|
|
rowIndex,
|
|
|
|
isExpanded: scope.isExpanded,
|
|
|
|
onToggle: (_, rowIndex) => {
|
|
|
|
const rows = scopes.map((resource, index) =>
|
|
|
|
index === rowIndex
|
|
|
|
? {
|
|
|
|
...resource,
|
|
|
|
isExpanded: !resource.isExpanded,
|
|
|
|
}
|
|
|
|
: resource
|
|
|
|
);
|
|
|
|
setScopes(rows);
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<Td data-testid={`name-column-${scope.name}`}>
|
|
|
|
<Link
|
|
|
|
to={toScopeDetails({
|
|
|
|
realm,
|
|
|
|
id: clientId,
|
|
|
|
scopeId: scope.id!,
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{scope.name}
|
|
|
|
</Link>
|
|
|
|
</Td>
|
|
|
|
<Td>
|
|
|
|
<ResourceRenderer row={scope} />
|
|
|
|
</Td>
|
|
|
|
<Td>
|
|
|
|
<PermissionsRenderer row={scope} />
|
|
|
|
</Td>
|
|
|
|
<Td width={10}>
|
|
|
|
<Button
|
|
|
|
variant="link"
|
|
|
|
component={(props) => (
|
|
|
|
<Link
|
|
|
|
{...props}
|
|
|
|
to={toNewPermission({
|
|
|
|
realm,
|
|
|
|
id: clientId,
|
|
|
|
permissionType: "scope",
|
|
|
|
selectedId: scope.id,
|
|
|
|
})}
|
2022-01-12 16:01:54 +00:00
|
|
|
/>
|
2022-03-16 13:57:50 +00:00
|
|
|
)}
|
|
|
|
>
|
|
|
|
{t("createPermission")}
|
|
|
|
</Button>
|
|
|
|
</Td>
|
|
|
|
<Td
|
|
|
|
isActionCell
|
|
|
|
actions={{
|
|
|
|
items: [
|
|
|
|
{
|
|
|
|
title: t("common:delete"),
|
|
|
|
onClick: () => {
|
|
|
|
setSelectedScope(scope);
|
|
|
|
toggleDeleteDialog();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Tr>
|
|
|
|
<Tr key={`child-${scope.id}`} isExpanded={scope.isExpanded}>
|
|
|
|
<Td />
|
|
|
|
<Td colSpan={4}>
|
|
|
|
<ExpandableRowContent>
|
|
|
|
{scope.isExpanded && (
|
|
|
|
<DescriptionList
|
|
|
|
isHorizontal
|
|
|
|
className="keycloak_resource_details"
|
|
|
|
>
|
2022-03-21 06:45:49 +00:00
|
|
|
<DetailDescriptionLink
|
2022-03-16 13:57:50 +00:00
|
|
|
name="resources"
|
|
|
|
array={scope.resources}
|
|
|
|
convert={(r) => r.name!}
|
2022-03-21 06:45:49 +00:00
|
|
|
link={(r) =>
|
|
|
|
toResourceDetails({
|
|
|
|
id: clientId,
|
|
|
|
realm,
|
|
|
|
resourceId: r._id!,
|
|
|
|
})
|
|
|
|
}
|
2022-03-16 13:57:50 +00:00
|
|
|
/>
|
2022-03-21 06:45:49 +00:00
|
|
|
<DetailDescriptionLink
|
2022-03-16 13:57:50 +00:00
|
|
|
name="associatedPermissions"
|
|
|
|
array={scope.permissions}
|
|
|
|
convert={(p) => p.name!}
|
2022-03-21 06:45:49 +00:00
|
|
|
link={(p) =>
|
|
|
|
toPermissionDetails({
|
|
|
|
id: clientId,
|
|
|
|
realm,
|
|
|
|
permissionId: p.id!,
|
|
|
|
permissionType: p.type!,
|
|
|
|
})
|
|
|
|
}
|
2022-03-16 13:57:50 +00:00
|
|
|
/>
|
|
|
|
</DescriptionList>
|
|
|
|
)}
|
|
|
|
</ExpandableRowContent>
|
|
|
|
</Td>
|
|
|
|
</Tr>
|
|
|
|
</Tbody>
|
|
|
|
))}
|
|
|
|
</TableComposable>
|
|
|
|
)}
|
2022-01-04 09:17:43 +00:00
|
|
|
</PaginatingTableToolbar>
|
|
|
|
)}
|
2022-03-16 13:57:50 +00:00
|
|
|
{noData && !searching && (
|
2022-01-04 09:17:43 +00:00
|
|
|
<ListEmptyState
|
|
|
|
message={t("emptyAuthorizationScopes")}
|
|
|
|
instructions={t("emptyAuthorizationInstructions")}
|
|
|
|
onPrimaryAction={() =>
|
|
|
|
history.push(toNewScope({ id: clientId, realm }))
|
|
|
|
}
|
|
|
|
primaryActionText={t("createAuthorizationScope")}
|
|
|
|
/>
|
|
|
|
)}
|
2022-03-16 13:57:50 +00:00
|
|
|
{noData && searching && (
|
|
|
|
<ListEmptyState
|
|
|
|
isSearchVariant
|
|
|
|
message={t("common:noSearchResults")}
|
|
|
|
instructions={t("common:noSearchResultsInstructions")}
|
|
|
|
/>
|
|
|
|
)}
|
2022-01-04 09:17:43 +00:00
|
|
|
</PageSection>
|
|
|
|
);
|
|
|
|
};
|