From c5fc9f1299486793dc829cb8a7cbf49999b2e9bd Mon Sep 17 00:00:00 2001
From: Nkeiruka
Date: Fri, 25 Oct 2024 18:13:44 +0100
Subject: [PATCH] feat: [WD-15753] Delete OIDC User
Signed-off-by: Nkeiruka
---
src/api/auth-identities.tsx | 24 ++++++
src/context/useSupportedFeatures.tsx | 1 +
.../permissions/PermissionIdentities.tsx | 59 ++++++++++----
.../actions/BulkDeleteIdentitiesBtn.tsx | 78 +++++++++++++++++++
.../permissions/actions/DeleteIdentityBtn.tsx | 77 ++++++++++++++++++
tests/permission-identities.spec.ts | 1 +
6 files changed, 225 insertions(+), 15 deletions(-)
create mode 100644 src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
create mode 100644 src/pages/permissions/actions/DeleteIdentityBtn.tsx
diff --git a/src/api/auth-identities.tsx b/src/api/auth-identities.tsx
index d0501ea819..3ba6b52c6a 100644
--- a/src/api/auth-identities.tsx
+++ b/src/api/auth-identities.tsx
@@ -50,3 +50,27 @@ export const updateIdentities = (
.catch(reject);
});
};
+
+export const deleteOIDCIdentity = (identity: LxdIdentity) => {
+ return new Promise((resolve, reject) => {
+ fetch(`/1.0/auth/identities/oidc/${identity.id}`, {
+ method: "DELETE",
+ })
+ .then(handleResponse)
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
+export const deleteOIDCIdentities = (
+ identities: LxdIdentity[],
+): Promise => {
+ return new Promise((resolve, reject) => {
+ void Promise.allSettled(
+ identities.map((identity) => deleteOIDCIdentity(identity)),
+ )
+ .then(handleSettledResult)
+ .then(resolve)
+ .catch(reject);
+ });
+};
diff --git a/src/context/useSupportedFeatures.tsx b/src/context/useSupportedFeatures.tsx
index 49b262afed..0245f74a2f 100644
--- a/src/context/useSupportedFeatures.tsx
+++ b/src/context/useSupportedFeatures.tsx
@@ -24,6 +24,7 @@ export const useSupportedFeatures = () => {
(!!serverVersion && serverMajor >= 5 && serverMinor >= 20) ||
serverMajor > 5,
hasAccessManagement: apiExtensions.has("access_management"),
+ hasAccessManagementTLS: apiExtensions.has("access_management_tls"),
hasExplicitTrustToken: apiExtensions.has("explicit_trust_token"),
hasInstanceCreateStart: apiExtensions.has("instance_create_start"),
hasInstanceImportConversion: apiExtensions.has(
diff --git a/src/pages/permissions/PermissionIdentities.tsx b/src/pages/permissions/PermissionIdentities.tsx
index 19eec377e4..b07858ea8f 100644
--- a/src/pages/permissions/PermissionIdentities.tsx
+++ b/src/pages/permissions/PermissionIdentities.tsx
@@ -11,7 +11,7 @@ import Loader from "components/Loader";
import ScrollableTable from "components/ScrollableTable";
import SelectableMainTable from "components/SelectableMainTable";
import SelectedTableNotification from "components/SelectedTableNotification";
-import { FC, useState } from "react";
+import { FC, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import { queryKeys } from "util/queryKeys";
import useSortTableData from "util/useSortTableData";
@@ -30,6 +30,9 @@ import HelpLink from "components/HelpLink";
import { useDocs } from "context/useDocs";
import EditIdentityGroupsPanel from "./panels/EditIdentityGroupsPanel";
import Tag from "components/Tag";
+import BulkDeleteIdentitiesBtn from "./actions/BulkDeleteIdentitiesBtn";
+import DeleteIdentityBtn from "./actions/DeleteIdentityBtn";
+import { useSupportedFeatures } from "context/useSupportedFeatures";
const PermissionIdentities: FC = () => {
const notify = useNotify();
@@ -46,6 +49,21 @@ const PermissionIdentities: FC = () => {
const panelParams = usePanelParams();
const [searchParams] = useSearchParams();
const [selectedIdentityIds, setSelectedIdentityIds] = useState([]);
+ const { hasAccessManagementTLS } = useSupportedFeatures();
+
+ useEffect(() => {
+ const validIdentities = new Set(
+ identities.map((identity) => identity.name),
+ );
+
+ const validSelections = selectedIdentityIds.filter((identity) =>
+ validIdentities.has(identity),
+ );
+
+ if (validSelections.length !== selectedIdentityIds.length) {
+ setSelectedIdentityIds(validSelections);
+ }
+ }, [identities]);
if (error) {
notify.failure("Loading identities failed", error);
@@ -140,20 +158,25 @@ const PermissionIdentities: FC = () => {
},
{
content: !isTlsIdentity && (
-
+ <>
+
+ {hasAccessManagementTLS && (
+
+ )}
+ >
),
className: "actions u-align--right",
role: "cell",
@@ -234,6 +257,12 @@ const PermissionIdentities: FC = () => {
className="u-no-margin--bottom"
/>
)}
+ {!!selectedIdentityIds.length && hasAccessManagementTLS && (
+
+ )}
}
diff --git a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
new file mode 100644
index 0000000000..551963d819
--- /dev/null
+++ b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
@@ -0,0 +1,78 @@
+import { FC } from "react";
+import {
+ ButtonProps,
+ ConfirmationButton,
+ useNotify,
+} from "@canonical/react-components";
+import { LxdIdentity } from "types/permissions";
+import { deleteOIDCIdentities } from "api/auth-identities";
+import { useQueryClient } from "@tanstack/react-query";
+import { useToastNotification } from "context/toastNotificationProvider";
+import { queryKeys } from "util/queryKeys";
+import { pluralize } from "util/instanceBulkActions";
+
+interface Props {
+ identities: LxdIdentity[];
+ className?: string;
+}
+
+const BulkDeleteIdentitiesBtn: FC = ({
+ identities,
+ className,
+}) => {
+ const queryClient = useQueryClient();
+ const notify = useNotify();
+ const toastNotify = useToastNotification();
+ const buttonText = `Delete ${pluralize("identity", identities.length)}`;
+ const successMessage = `${identities.length} ${pluralize("identity", identities.length)} successfully deleted`;
+
+ const handleDelete = () => {
+ deleteOIDCIdentities(identities)
+ .then(() => {
+ void queryClient.invalidateQueries({
+ predicate: (query) => {
+ return [queryKeys.identities, queryKeys.authGroups].includes(
+ query.queryKey[0] as string,
+ );
+ },
+ });
+ toastNotify.success(successMessage);
+ close();
+ })
+ .catch((e) => {
+ notify.failure(`Identity deletion failed`, e);
+ });
+ };
+
+ return (
+
+ This will permanently delete the following identities:
+
+ {identities.map((identity) => (
+ - {identity.name}
+ ))}
+
+ This action cannot be undone, and can result in data loss.
+
+ ),
+ confirmButtonLabel: "Delete",
+ onConfirm: handleDelete,
+ }}
+ disabled={!identities.length}
+ shiftClickEnabled
+ showShiftClickHint
+ >
+ {buttonText}
+
+ );
+};
+
+export default BulkDeleteIdentitiesBtn;
diff --git a/src/pages/permissions/actions/DeleteIdentityBtn.tsx b/src/pages/permissions/actions/DeleteIdentityBtn.tsx
new file mode 100644
index 0000000000..e46de90309
--- /dev/null
+++ b/src/pages/permissions/actions/DeleteIdentityBtn.tsx
@@ -0,0 +1,77 @@
+import { FC } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "util/queryKeys";
+import {
+ ConfirmationButton,
+ Icon,
+ useNotify,
+} from "@canonical/react-components";
+import { useToastNotification } from "context/toastNotificationProvider";
+import { LxdIdentity } from "types/permissions";
+import ItemName from "components/ItemName";
+import { deleteOIDCIdentity } from "api/auth-identities";
+import ResourceLabel from "components/ResourceLabel";
+
+interface Props {
+ identity: LxdIdentity;
+}
+
+const DeleteIdentityBtn: FC = ({ identity }) => {
+ const queryClient = useQueryClient();
+ const notify = useNotify();
+ const toastNotify = useToastNotification();
+
+ const handleDelete = () => {
+ deleteOIDCIdentity(identity)
+ .then(() => {
+ void queryClient.invalidateQueries({
+ predicate: (query) => {
+ return [queryKeys.identities, queryKeys.authGroups].includes(
+ query.queryKey[0] as string,
+ );
+ },
+ });
+ toastNotify.success(
+ <>
+ Identity {" "}
+ deleted.
+ >,
+ );
+ close();
+ })
+ .catch((e) => {
+ notify.failure(
+ `Identity deletion failed`,
+ e,
+ ,
+ );
+ });
+ };
+
+ return (
+
+ This will permanently delete .
+
+ This action cannot be undone, and can result in data loss.
+
+ ),
+ confirmButtonLabel: "Delete",
+ onConfirm: handleDelete,
+ }}
+ shiftClickEnabled
+ showShiftClickHint
+ >
+
+
+ );
+};
+
+export default DeleteIdentityBtn;
diff --git a/tests/permission-identities.spec.ts b/tests/permission-identities.spec.ts
index f288b5dd91..2a90133713 100644
--- a/tests/permission-identities.spec.ts
+++ b/tests/permission-identities.spec.ts
@@ -92,6 +92,7 @@ test("manage groups for many identities", async ({ page, lxdVersion }) => {
);
await page.getByRole("button", { name: "Confirm changes" }).click();
await page.waitForSelector(`text=Updated groups for 2 identities`);
+ await selectIdentitiesToModify(page, [identityFoo, identityBar]);
await page.getByLabel("Modify groups").click();
await toggleGroupsForIdentities(page, [groupOne, groupTwo]);
await assertTextVisible(page, "2 groups will be modified");