Skip to content

Commit

Permalink
feat: [WD-15753] Delete OIDC User (#972)
Browse files Browse the repository at this point in the history
## Done

- Created API(s) to delete OIDC identities.
- Created DeleteIdentitiesBtn
- Added an inline delete identity button.

Fixes [list issues/bugs if needed]

## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
- Navigate to Permissions -> Identities and attempt to delete one or
many identities through the inline delete button or the bulk delete
button.

## Screenshots

![image](https://github.com/user-attachments/assets/89afb1d7-04c6-47e0-a0e5-1cb8560ff023)
  • Loading branch information
Kxiru authored Nov 5, 2024
2 parents dbfa314 + c5fc9f1 commit b33fd83
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 15 deletions.
24 changes: 24 additions & 0 deletions src/api/auth-identities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
return new Promise((resolve, reject) => {
void Promise.allSettled(
identities.map((identity) => deleteOIDCIdentity(identity)),
)
.then(handleSettledResult)
.then(resolve)
.catch(reject);
});
};
1 change: 1 addition & 0 deletions src/context/useSupportedFeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
59 changes: 44 additions & 15 deletions src/pages/permissions/PermissionIdentities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -46,6 +49,21 @@ const PermissionIdentities: FC = () => {
const panelParams = usePanelParams();
const [searchParams] = useSearchParams();
const [selectedIdentityIds, setSelectedIdentityIds] = useState<string[]>([]);
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);
Expand Down Expand Up @@ -140,20 +158,25 @@ const PermissionIdentities: FC = () => {
},
{
content: !isTlsIdentity && (
<Button
appearance="base"
hasIcon
dense
onClick={() => {
panelParams.openIdentityGroups(identity.id);
setSelectedIdentityIds([identity.id]);
}}
type="button"
aria-label="Manage groups"
title="Manage groups"
>
<Icon name="user-group" />
</Button>
<>
<Button
appearance="base"
hasIcon
dense
onClick={() => {
panelParams.openIdentityGroups(identity.id);
setSelectedIdentityIds([identity.id]);
}}
type="button"
aria-label="Manage groups"
title="Manage groups"
>
<Icon name="user-group" />
</Button>
{hasAccessManagementTLS && (
<DeleteIdentityBtn identity={identity} />
)}
</>
),
className: "actions u-align--right",
role: "cell",
Expand Down Expand Up @@ -234,6 +257,12 @@ const PermissionIdentities: FC = () => {
className="u-no-margin--bottom"
/>
)}
{!!selectedIdentityIds.length && hasAccessManagementTLS && (
<BulkDeleteIdentitiesBtn
identities={selectedIdentities}
className="u-no-margin--bottom"
/>
)}
</PageHeader.Left>
</PageHeader>
}
Expand Down
78 changes: 78 additions & 0 deletions src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
Original file line number Diff line number Diff line change
@@ -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<Props & ButtonProps> = ({
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 (
<ConfirmationButton
onHoverText={buttonText}
appearance=""
aria-label="Delete identities"
className={className}
confirmationModalProps={{
title: "Confirm delete",
children: (
<p>
This will permanently delete the following identities:
<ul>
{identities.map((identity) => (
<li key={identity.name}>{identity.name}</li>
))}
</ul>
This action cannot be undone, and can result in data loss.
</p>
),
confirmButtonLabel: "Delete",
onConfirm: handleDelete,
}}
disabled={!identities.length}
shiftClickEnabled
showShiftClickHint
>
{buttonText}
</ConfirmationButton>
);
};

export default BulkDeleteIdentitiesBtn;
77 changes: 77 additions & 0 deletions src/pages/permissions/actions/DeleteIdentityBtn.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ 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 <ResourceLabel type={"idp-group"} value={identity.name} />{" "}
deleted.
</>,
);
close();
})
.catch((e) => {
notify.failure(
`Identity deletion failed`,
e,
<ResourceLabel type={"idp-group"} value={identity.name} />,
);
});
};

return (
<ConfirmationButton
onHoverText={"Delete identity"}
appearance="base"
aria-label="Delete identity"
className={"has-icon"}
confirmationModalProps={{
title: "Confirm delete",
children: (
<p>
This will permanently delete <ItemName item={identity} bold />.
<br />
This action cannot be undone, and can result in data loss.
</p>
),
confirmButtonLabel: "Delete",
onConfirm: handleDelete,
}}
shiftClickEnabled
showShiftClickHint
>
<Icon name="delete" />
</ConfirmationButton>
);
};

export default DeleteIdentityBtn;
1 change: 1 addition & 0 deletions tests/permission-identities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit b33fd83

Please sign in to comment.