Skip to content

Commit

Permalink
feat: [WD-16894] Add bulk deletion and group modification of TLS Users (
Browse files Browse the repository at this point in the history
#1008)

## Done

- [✔] Allow selection of tls identities in the permission- > identity
list
- [✔] Use the new DELETE 1.0/auth/identities/tls/:id endpoint for the
bulk delete
- [✔] Unhide “delete” and “modify groups” button for tls users of the
new type in the identities list
- [✔] Modify bulk actions "delete" and "modify groups" on the identity
list page to also allow inclusion of tls users
- [✔] Legacy TLS users should be disabled, not deleted.

## 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:

**Create "new" TLS Fine grained identities**
- To add a new TLS Identity Client (Pending) please follow the steps for
creating a pending fine-grained TLS identity in the '[Authenticate with
the LXD
server](https://documentation.ubuntu.com/lxd/en/latest/howto/server_expose/#authenticate-with-the-lxd-server)'
section of the LXD documentation.
- Be sure to use the API instructions to create pending fine grained
identities, as the CLI instructions require a remote to be specified.
- Be sure to authenticate the client by following the instructions
through step 2.
 
**Delete TLS Users**
- Navigate to the Permissions > Identities 
- Attempt to delete individual TLS Client (Pending) identities using the
inline delete identity button.
- Attempt to delete bulk TLS Client (Pending) identities.
- Verify that Legacy TLS identities (Client Certificate (Unrestricted)
identities) cannot be selected via the checkbox or modified/deleted via
inline buttons.

**Add TLS users users to a group**
- Navigate to Permissions > Groups
- Create a new group and add permissions to the group.
- Navigate to the Permissions > Identities 
- Attempt to add an individual TLS identity to a group using the inline
modify groups button.
- Attempt to add several TLS identities (Pending) to a group.

**Test TLS user permissions / Login as a TLS User**
- Concatenate the .key and .crt files to create a .pem file using the
following command:
- ```cat <KEY-FILE> <CRT-FILE> > <PEM-FILE-NAME>.pem ```
- Change lines 11 and 22 of the _haproxy-dev.cfg_ file to the following,
respectively.
- ``` bind 0.0.0.0:8407 ssl verify optional crt <PEM-FILE-PATH> ca-file
<CRT-FILE-PATH> ```
- ``` server lxd_https LXD_UI_BACKEND_IP:8443 ssl verify none crt
<PEM-FILE-PATH> ```
- TLS Fine grained identities have been tested with the following
permissions:

![image](https://github.com/user-attachments/assets/5b361b91-5b01-4b7c-9fa2-1b87c66acf48)


## Screenshots

![image](https://github.com/user-attachments/assets/7c28d019-5e13-41e2-8df7-16f008317c61)
  • Loading branch information
Kxiru authored Dec 12, 2024
2 parents 54551b5 + a33ba9f commit 87d33b5
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 52 deletions.
17 changes: 9 additions & 8 deletions src/api/auth-identities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,24 @@ export const updateIdentities = (
});
};

export const deleteOIDCIdentity = (identity: LxdIdentity) => {
export const deleteIdentity = (identity: LxdIdentity) => {
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identities/oidc/${identity.id}`, {
method: "DELETE",
})
fetch(
`/1.0/auth/identities/${identity.authentication_method}/${identity.id}`,
{
method: "DELETE",
},
)
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

export const deleteOIDCIdentities = (
identities: LxdIdentity[],
): Promise<void> => {
export const deleteIdentities = (identities: LxdIdentity[]): Promise<void> => {
return new Promise((resolve, reject) => {
void Promise.allSettled(
identities.map((identity) => deleteOIDCIdentity(identity)),
identities.map((identity) => deleteIdentity(identity)),
)
.then(handleSettledResult)
.then(resolve)
Expand Down
22 changes: 22 additions & 0 deletions src/components/IdentityResource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FC } from "react";
import { LxdIdentity } from "types/permissions";
import ResourceLabel from "./ResourceLabel";

interface Props {
identity: LxdIdentity;
truncate?: boolean;
}

const IdentityResource: FC<Props> = ({ identity, truncate }) => {
const identityIconType =
identity.authentication_method == "tls" ? "certificate" : "oidc-identity";

return (
<ResourceLabel
type={identityIconType}
value={identity.type}
truncate={truncate}
/>
);
};
export default IdentityResource;
6 changes: 4 additions & 2 deletions src/components/ResourceLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { FC } from "react";
import ResourceIcon, { ResourceIconType } from "./ResourceIcon";
import classNames from "classnames";

interface Props {
type: ResourceIconType;
value: string;
bold?: boolean;
truncate?: boolean;
}

const ResourceLabel: FC<Props> = ({ type, value, bold }) => {
const ResourceLabel: FC<Props> = ({ type, value, bold, truncate = true }) => {
const ValueWrapper = bold ? "strong" : "span";
return (
<span className="resource-label u-truncate">
<span className={classNames("resource-label", { "u-truncate": truncate })}>
<ResourceIcon type={type} />
<ValueWrapper>{value}</ValueWrapper>
</span>
Expand Down
33 changes: 17 additions & 16 deletions src/pages/permissions/PermissionIdentities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import Tag from "components/Tag";
import BulkDeleteIdentitiesBtn from "./actions/BulkDeleteIdentitiesBtn";
import DeleteIdentityBtn from "./actions/DeleteIdentityBtn";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import { isUnrestricted } from "util/helpers";
import IdentityResource from "components/IdentityResource";

const PermissionIdentities: FC = () => {
const notify = useNotify();
Expand All @@ -52,12 +54,10 @@ const PermissionIdentities: FC = () => {
const { hasAccessManagementTLS } = useSupportedFeatures();

useEffect(() => {
const validIdentities = new Set(
identities.map((identity) => identity.name),
);
const validIdentityIds = new Set(identities.map((identity) => identity.id));

const validSelections = selectedIdentityIds.filter((identity) =>
validIdentities.has(identity),
validIdentityIds.has(identity),
);

if (validSelections.length !== selectedIdentityIds.length) {
Expand Down Expand Up @@ -114,9 +114,9 @@ const PermissionIdentities: FC = () => {

const rows = filteredIdentities.map((identity) => {
const isLoggedInIdentity = settings?.auth_user_name === identity.id;
const isTlsIdentity = identity.authentication_method === "tls";

return {
name: isTlsIdentity ? "" : identity.id,
name: isUnrestricted(identity) ? "" : identity.id,
key: identity.id,
className: "u-row",
columns: [
Expand Down Expand Up @@ -144,7 +144,7 @@ const PermissionIdentities: FC = () => {
"aria-label": "Auth method",
},
{
content: identity.type,
content: <IdentityResource identity={identity} truncate={false} />,
role: "cell",
"aria-label": "Type",
className: "u-truncate",
Expand All @@ -157,10 +157,11 @@ const PermissionIdentities: FC = () => {
"aria-label": "Groups for this identity",
},
{
content: !isTlsIdentity && (
content: !isUnrestricted(identity) && (
<>
<Button
appearance="base"
className="u-no-margin--bottom"
hasIcon
dense
onClick={() => {
Expand Down Expand Up @@ -198,10 +199,8 @@ const PermissionIdentities: FC = () => {
defaultSort: "name",
});

// NOTE: tls user group membership cannot be modified, this will be supported in the future
const nonTlsUsers = identities.filter((identity) => {
const isTlsIdentity = identity.authentication_method === "tls";
return !isTlsIdentity;
const fineGrainedIdentities = identities.filter((identity) => {
return !isUnrestricted(identity);
});

if (isLoading) {
Expand All @@ -218,11 +217,11 @@ const PermissionIdentities: FC = () => {
if (selectedIdentityIds.length > 0) {
return (
<SelectedTableNotification
totalCount={nonTlsUsers.length ?? 0}
itemName="OIDC identity"
totalCount={fineGrainedIdentities.length ?? 0}
itemName="identity"
selectedNames={selectedIdentityIds}
setSelectedNames={setSelectedIdentityIds}
filteredNames={nonTlsUsers.map((item) => item.id)}
filteredNames={fineGrainedIdentities.map((item) => item.id)}
hideActions={!!panelParams.panel}
/>
);
Expand Down Expand Up @@ -294,7 +293,9 @@ const PermissionIdentities: FC = () => {
selectedNames={selectedIdentityIds}
setSelectedNames={setSelectedIdentityIds}
processingNames={[]}
filteredNames={nonTlsUsers.map((identity) => identity.id)}
filteredNames={fineGrainedIdentities.map(
(identity) => identity.id,
)}
disableSelect={!!panelParams.panel}
/>
</TablePagination>
Expand Down
11 changes: 8 additions & 3 deletions src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { FC } from "react";
import { FC, useState } from "react";
import {
ButtonProps,
ConfirmationButton,
useNotify,
} from "@canonical/react-components";
import { LxdIdentity } from "types/permissions";
import { deleteOIDCIdentities } from "api/auth-identities";
import { deleteIdentities } from "api/auth-identities";
import { useQueryClient } from "@tanstack/react-query";
import { useToastNotification } from "context/toastNotificationProvider";
import { queryKeys } from "util/queryKeys";
Expand All @@ -24,10 +24,12 @@ const BulkDeleteIdentitiesBtn: FC<Props & ButtonProps> = ({
const notify = useNotify();
const toastNotify = useToastNotification();
const buttonText = `Delete ${pluralize("identity", identities.length)}`;
const [isLoading, setLoading] = useState(false);
const successMessage = `${identities.length} ${pluralize("identity", identities.length)} successfully deleted`;

const handleDelete = () => {
deleteOIDCIdentities(identities)
setLoading(true);
deleteIdentities(identities)
.then(() => {
void queryClient.invalidateQueries({
predicate: (query) => {
Expand All @@ -37,10 +39,12 @@ const BulkDeleteIdentitiesBtn: FC<Props & ButtonProps> = ({
},
});
toastNotify.success(successMessage);
setLoading(false);
close();
})
.catch((e) => {
notify.failure(`Identity deletion failed`, e);
setLoading(false);
});
};

Expand All @@ -50,6 +54,7 @@ const BulkDeleteIdentitiesBtn: FC<Props & ButtonProps> = ({
appearance=""
aria-label="Delete identities"
className={className}
loading={isLoading}
confirmationModalProps={{
title: "Confirm delete",
children: (
Expand Down
22 changes: 13 additions & 9 deletions src/pages/permissions/actions/DeleteIdentityBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import {
Expand All @@ -9,8 +9,8 @@ import {
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";
import { deleteIdentity } from "api/auth-identities";
import IdentityResource from "components/IdentityResource";

interface Props {
identity: LxdIdentity;
Expand All @@ -20,9 +20,11 @@ const DeleteIdentityBtn: FC<Props> = ({ identity }) => {
const queryClient = useQueryClient();
const notify = useNotify();
const toastNotify = useToastNotification();
const [isDeleting, setDeleting] = useState(false);

const handleDelete = () => {
deleteOIDCIdentity(identity)
setDeleting(true);
deleteIdentity(identity)
.then(() => {
void queryClient.invalidateQueries({
predicate: (query) => {
Expand All @@ -33,27 +35,28 @@ const DeleteIdentityBtn: FC<Props> = ({ identity }) => {
});
toastNotify.success(
<>
Identity <ResourceLabel type={"idp-group"} value={identity.name} />{" "}
deleted.
Identity <IdentityResource identity={identity} /> deleted.
</>,
);
setDeleting(false);
close();
})
.catch((e) => {
setDeleting(false);
notify.failure(
`Identity deletion failed`,
e,
<ResourceLabel type={"idp-group"} value={identity.name} />,
<IdentityResource identity={identity} />,
);
});
};

return (
<ConfirmationButton
onHoverText={"Delete identity"}
onHoverText="Delete identity"
appearance="base"
aria-label="Delete identity"
className={"has-icon"}
className="has-icon u-no-margin--bottom"
confirmationModalProps={{
title: "Confirm delete",
children: (
Expand All @@ -68,6 +71,7 @@ const DeleteIdentityBtn: FC<Props> = ({ identity }) => {
}}
shiftClickEnabled
showShiftClickHint
loading={isDeleting}
>
<Icon name="delete" />
</ConfirmationButton>
Expand Down
15 changes: 8 additions & 7 deletions src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import PermissionIdentitiesFilter, {
import NotificationRow from "components/NotificationRow";
import ScrollableContainer from "components/ScrollableContainer";
import useSortTableData from "util/useSortTableData";
import { isUnrestricted } from "util/helpers";

type IdentityEditHistory = {
identitiesAdded: Set<string>;
Expand Down Expand Up @@ -75,15 +76,15 @@ const EditGroupIdentitiesPanel: FC<Props> = ({ groups }) => {
}
}, [groups]);

const nonTlsIdentities = identities.filter(
(identity) => identity.authentication_method !== "tls",
const fineGrainedIdentities = identities.filter(
(identity) => !isUnrestricted(identity),
);

const {
identityIdsInAllGroups,
identityIdsInNoGroups,
identityIdsInSomeGroups,
} = getCurrentIdentitiesForGroups(groups, nonTlsIdentities);
} = getCurrentIdentitiesForGroups(groups, fineGrainedIdentities);

const selectedIdentities = new Set<string>(desiredState.identitiesAdded);
for (const identity of identityIdsInAllGroups) {
Expand Down Expand Up @@ -131,7 +132,7 @@ const EditGroupIdentitiesPanel: FC<Props> = ({ groups }) => {
saveToPanelHistory({
identitiesAdded: new Set(),
identitiesRemoved: new Set(
nonTlsIdentities.map((identity) => identity.id),
fineGrainedIdentities.map((identity) => identity.id),
),
});
} else {
Expand Down Expand Up @@ -191,7 +192,7 @@ const EditGroupIdentitiesPanel: FC<Props> = ({ groups }) => {
authMethod: searchParams.getAll(AUTH_METHOD),
};

const filteredIdentities = nonTlsIdentities.filter((identity) => {
const filteredIdentities = fineGrainedIdentities.filter((identity) => {
if (
!filters.queries.every(
(q) =>
Expand Down Expand Up @@ -269,7 +270,7 @@ const EditGroupIdentitiesPanel: FC<Props> = ({ groups }) => {
selectedNames={Array.from(selectedIdentities)}
setSelectedNames={modifyIdentities}
processingNames={[]}
filteredNames={nonTlsIdentities.map((identity) => identity.id)}
filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
indeterminateNames={Array.from(indeterminateIdentities)}
onToggleRow={toggleRow}
hideContextualMenu
Expand Down Expand Up @@ -334,7 +335,7 @@ const EditGroupIdentitiesPanel: FC<Props> = ({ groups }) => {
selectedGroups={groups}
addedIdentities={desiredState.identitiesAdded}
removedIdentities={desiredState.identitiesRemoved}
allIdentities={nonTlsIdentities}
allIdentities={fineGrainedIdentities}
/>
)}
</>
Expand Down
9 changes: 5 additions & 4 deletions src/pages/permissions/panels/EditIdentitiesForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SelectableMainTable from "components/SelectableMainTable";
import { fetchIdentities } from "api/auth-identities";
import useSortTableData from "util/useSortTableData";
import { LxdIdentity } from "types/permissions";
import { isUnrestricted } from "util/helpers";

export type FormIdentity = LxdIdentity & {
isRemoved?: boolean;
Expand Down Expand Up @@ -36,8 +37,8 @@ const EditIdentitiesForm: FC<Props> = ({
notify.failure("Loading details failed", error);
}

const nonTlsIdentities = identities.filter(
(identity) => identity.authentication_method !== "tls",
const fineGrainedIdentities = identities.filter(
(identity) => !isUnrestricted(identity),
);

const toggleRow = (id: string) => {
Expand Down Expand Up @@ -104,7 +105,7 @@ const EditIdentitiesForm: FC<Props> = ({
},
];

const filteredIdentities = nonTlsIdentities.filter((identity) => {
const filteredIdentities = fineGrainedIdentities.filter((identity) => {
if (filter) {
return identity.name.toLowerCase().includes(filter.toLowerCase());
}
Expand Down Expand Up @@ -168,7 +169,7 @@ const EditIdentitiesForm: FC<Props> = ({
.map((identity) => identity.id)}
setSelectedNames={bulkSelect}
processingNames={[]}
filteredNames={nonTlsIdentities.map((identity) => identity.id)}
filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
indeterminateNames={[]}
onToggleRow={toggleRow}
hideContextualMenu
Expand Down
Loading

0 comments on commit 87d33b5

Please sign in to comment.