From c8ef8580e74af7e7e6b9b4b76d5fb6736e40312a Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 09:38:12 +0200
Subject: [PATCH 01/22] feat: disbale create tls identity button when user has
no permission
Signed-off-by: Mason Hu
---
src/pages/permissions/CreateTlsIdentityBtn.tsx | 8 ++++++++
src/util/entitlements/server.tsx | 10 ++++++++++
2 files changed, 18 insertions(+)
diff --git a/src/pages/permissions/CreateTlsIdentityBtn.tsx b/src/pages/permissions/CreateTlsIdentityBtn.tsx
index d0d9031c73..44a88de73a 100644
--- a/src/pages/permissions/CreateTlsIdentityBtn.tsx
+++ b/src/pages/permissions/CreateTlsIdentityBtn.tsx
@@ -3,10 +3,12 @@ import { useSmallScreen } from "context/useSmallScreen";
import { FC } from "react";
import { usePortal } from "@canonical/react-components";
import CreateIdentityModal from "./CreateIdentityModal";
+import { useServerEntitlements } from "util/entitlements/server";
const CreateTlsIdentityBtn: FC = () => {
const isSmallScreen = useSmallScreen();
const { openPortal, closePortal, isOpen, Portal } = usePortal();
+ const { canCreateIdentities } = useServerEntitlements();
return (
<>
@@ -20,6 +22,12 @@ const CreateTlsIdentityBtn: FC = () => {
className="u-float-right u-no-margin--bottom"
onClick={openPortal}
hasIcon={!isSmallScreen}
+ title={
+ canCreateIdentities()
+ ? ""
+ : "You do not have permission to create identities"
+ }
+ disabled={!canCreateIdentities()}
>
{!isSmallScreen && }
Create TLS Identity
diff --git a/src/util/entitlements/server.tsx b/src/util/entitlements/server.tsx
index 2c6af0dc57..cef83c28a1 100644
--- a/src/util/entitlements/server.tsx
+++ b/src/util/entitlements/server.tsx
@@ -4,6 +4,15 @@ import { hasEntitlement } from "./helpers";
export const useServerEntitlements = () => {
const { isFineGrained, serverEntitlements } = useAuth();
+ const canCreateIdentities = () =>
+ hasEntitlement(
+ isFineGrained,
+ "can_create_identities",
+ serverEntitlements,
+ ) ||
+ hasEntitlement(isFineGrained, "permission_manager", serverEntitlements) ||
+ hasEntitlement(isFineGrained, "admin", serverEntitlements);
+
const canCreateProjects = () =>
hasEntitlement(isFineGrained, "can_create_projects", serverEntitlements) ||
hasEntitlement(isFineGrained, "project_manager", serverEntitlements) ||
@@ -33,6 +42,7 @@ export const useServerEntitlements = () => {
hasEntitlement(isFineGrained, "viewer", serverEntitlements);
return {
+ canCreateIdentities,
canCreateProjects,
canCreateStoragePools,
canEditServerConfiguration,
From 845f76eaf9d1f003ff6191ba9a31dd8e5983bd0e Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 09:52:39 +0200
Subject: [PATCH 02/22] feat: enable entitlement queries for identities
Signed-off-by: Mason Hu
---
src/api/auth-identities.tsx | 14 ++++++++--
src/context/useIdentities.tsx | 28 +++++++++++++++++++
src/context/useLoggedInUser.tsx | 11 ++------
.../permissions/PermissionIdentities.tsx | 13 ++-------
.../panels/EditGroupIdentitiesPanel.tsx | 13 ++-------
.../permissions/panels/EditGroupPanel.tsx | 10 +++----
.../permissions/panels/EditIdentitiesForm.tsx | 9 ++----
.../permissions/panels/PermissionSelector.tsx | 7 ++---
src/types/permissions.d.ts | 1 +
src/util/entitlements/identities.tsx | 14 ++++++++++
10 files changed, 69 insertions(+), 51 deletions(-)
create mode 100644 src/context/useIdentities.tsx
create mode 100644 src/util/entitlements/identities.tsx
diff --git a/src/api/auth-identities.tsx b/src/api/auth-identities.tsx
index 3a0f4455a3..bfbe473a3d 100644
--- a/src/api/auth-identities.tsx
+++ b/src/api/auth-identities.tsx
@@ -1,10 +1,16 @@
import { handleResponse, handleSettledResult } from "util/helpers";
import type { LxdApiResponse } from "types/apiResponse";
import type { LxdIdentity, TlsIdentityTokenDetail } from "types/permissions";
+import { withEntitlementsQuery } from "util/entitlements/api";
-export const fetchIdentities = (): Promise => {
+export const identitiesEntitlements = ["can_edit"];
+
+export const fetchIdentities = (
+ isFineGrained: boolean | null,
+): Promise => {
+ const entitlements = `&${withEntitlementsQuery(isFineGrained, identitiesEntitlements)}`;
return new Promise((resolve, reject) => {
- fetch(`/1.0/auth/identities?recursion=1`)
+ fetch(`/1.0/auth/identities?recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse) => resolve(data.metadata))
.catch(reject);
@@ -23,9 +29,11 @@ export const fetchCurrentIdentity = (): Promise => {
export const fetchIdentity = (
id: string,
authMethod: string,
+ isFineGrained: boolean | null,
): Promise => {
+ const entitlements = `&${withEntitlementsQuery(isFineGrained, identitiesEntitlements)}`;
return new Promise((resolve, reject) => {
- fetch(`/1.0/auth/identities/${authMethod}/${id}`)
+ fetch(`/1.0/auth/identities/${authMethod}/${id}?recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse) => resolve(data.metadata))
.catch(reject);
diff --git a/src/context/useIdentities.tsx b/src/context/useIdentities.tsx
new file mode 100644
index 0000000000..e523226a19
--- /dev/null
+++ b/src/context/useIdentities.tsx
@@ -0,0 +1,28 @@
+import { useQuery } from "@tanstack/react-query";
+import { queryKeys } from "util/queryKeys";
+import { UseQueryResult } from "@tanstack/react-query";
+import { useAuth } from "./auth";
+import { LxdIdentity } from "types/permissions";
+import { fetchIdentities, fetchIdentity } from "api/auth-identities";
+
+export const useIdentities = (): UseQueryResult => {
+ const { isFineGrained } = useAuth();
+ return useQuery({
+ queryKey: [queryKeys.identities],
+ queryFn: () => fetchIdentities(isFineGrained),
+ enabled: isFineGrained !== null,
+ });
+};
+
+export const useIdentity = (
+ id: string,
+ authMethod: string,
+ enabled?: boolean,
+): UseQueryResult => {
+ const { isFineGrained } = useAuth();
+ return useQuery({
+ queryKey: [queryKeys.identities, id],
+ queryFn: () => fetchIdentity(id, authMethod, isFineGrained),
+ enabled: (enabled ?? true) && isFineGrained !== null,
+ });
+};
diff --git a/src/context/useLoggedInUser.tsx b/src/context/useLoggedInUser.tsx
index 6ed50d174f..c556cdb822 100644
--- a/src/context/useLoggedInUser.tsx
+++ b/src/context/useLoggedInUser.tsx
@@ -1,7 +1,5 @@
-import { useQuery } from "@tanstack/react-query";
import { useSettings } from "./useSettings";
-import { fetchIdentity } from "api/auth-identities";
-import { queryKeys } from "util/queryKeys";
+import { useIdentity } from "./useIdentities";
export const useLoggedInUser = () => {
const { data: settings } = useSettings();
@@ -9,11 +7,8 @@ export const useLoggedInUser = () => {
const id = settings?.auth_user_name || "";
const authMethod = settings?.auth_user_method || "";
- const { data: identity } = useQuery({
- queryKey: [queryKeys.identities, id],
- queryFn: () => fetchIdentity(id, authMethod),
- enabled: !!id && !!authMethod,
- });
+ const identityQueryEnabled = !!id && !!authMethod;
+ const { data: identity } = useIdentity(id, authMethod, identityQueryEnabled);
return {
loggedInUserName: identity?.name,
diff --git a/src/pages/permissions/PermissionIdentities.tsx b/src/pages/permissions/PermissionIdentities.tsx
index 13d0baedd8..7e9ac7a57c 100644
--- a/src/pages/permissions/PermissionIdentities.tsx
+++ b/src/pages/permissions/PermissionIdentities.tsx
@@ -5,15 +5,12 @@ import {
TablePagination,
useNotify,
} from "@canonical/react-components";
-import { useQuery } from "@tanstack/react-query";
-import { fetchIdentities } from "api/auth-identities";
import Loader from "components/Loader";
import ScrollableTable from "components/ScrollableTable";
import SelectableMainTable from "components/SelectableMainTable";
import SelectedTableNotification from "components/SelectedTableNotification";
import { FC, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
-import { queryKeys } from "util/queryKeys";
import useSortTableData from "util/useSortTableData";
import PermissionIdentitiesFilter, {
AUTH_METHOD,
@@ -36,17 +33,11 @@ import { useSupportedFeatures } from "context/useSupportedFeatures";
import { isUnrestricted } from "util/helpers";
import IdentityResource from "components/IdentityResource";
import CreateTlsIdentityBtn from "./CreateTlsIdentityBtn";
+import { useIdentities } from "context/useIdentities";
const PermissionIdentities: FC = () => {
const notify = useNotify();
- const {
- data: identities = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.identities],
- queryFn: fetchIdentities,
- });
+ const { data: identities = [], error, isLoading } = useIdentities();
const { data: settings } = useSettings();
const docBaseLink = useDocs();
const panelParams = usePanelParams();
diff --git a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
index 4605e86b2b..d85db66258 100644
--- a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
@@ -4,10 +4,8 @@ import {
Icon,
useNotify,
} from "@canonical/react-components";
-import { useQuery } from "@tanstack/react-query";
import SidePanel from "components/SidePanel";
import { FC, useEffect, useState } from "react";
-import { queryKeys } from "util/queryKeys";
import usePanelParams from "util/usePanelParams";
import ScrollableTable from "components/ScrollableTable";
import SelectableMainTable from "components/SelectableMainTable";
@@ -15,7 +13,6 @@ import { useSearchParams } from "react-router-dom";
import useEditHistory from "util/useEditHistory";
import ModifiedStatusAction from "../actions/ModifiedStatusAction";
import { pluralize } from "util/instanceBulkActions";
-import { fetchIdentities } from "api/auth-identities";
import type { LxdGroup } from "types/permissions";
import { getCurrentIdentitiesForGroups } from "util/permissionGroups";
import GroupIdentitiesPanelConfirmModal from "./GroupIdentitiesPanelConfirmModal";
@@ -28,6 +25,7 @@ import NotificationRow from "components/NotificationRow";
import ScrollableContainer from "components/ScrollableContainer";
import useSortTableData from "util/useSortTableData";
import { isUnrestricted } from "util/helpers";
+import { useIdentities } from "context/useIdentities";
type IdentityEditHistory = {
identitiesAdded: Set;
@@ -44,14 +42,7 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
const notify = useNotify();
const [confirming, setConfirming] = useState(false);
- const {
- data: identities = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.identities],
- queryFn: fetchIdentities,
- });
+ const { data: identities = [], error, isLoading } = useIdentities();
const {
desiredState,
diff --git a/src/pages/permissions/panels/EditGroupPanel.tsx b/src/pages/permissions/panels/EditGroupPanel.tsx
index 0509a493c9..ac771fb8a6 100644
--- a/src/pages/permissions/panels/EditGroupPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupPanel.tsx
@@ -4,7 +4,7 @@ import {
ConfirmationModal,
useNotify,
} from "@canonical/react-components";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useQueryClient } from "@tanstack/react-query";
import SidePanel from "components/SidePanel";
import { FC, useEffect, useState } from "react";
import usePanelParams from "util/usePanelParams";
@@ -32,7 +32,7 @@ import {
getPermissionId,
getResourceLabel,
} from "util/permissions";
-import { fetchIdentities, updateIdentities } from "api/auth-identities";
+import { updateIdentities } from "api/auth-identities";
import LoggedInUserNotification from "pages/permissions/panels/LoggedInUserNotification";
import { useSettings } from "context/useSettings";
import { pluralize } from "util/instanceBulkActions";
@@ -40,6 +40,7 @@ import GroupHeaderTitle from "pages/permissions/panels/GroupHeaderTitle";
import { GroupSubForm } from "pages/permissions/panels/CreateGroupPanel";
import ResourceLink from "components/ResourceLink";
import { useImagesInAllProjects } from "context/useImages";
+import { useIdentities } from "context/useIdentities";
interface Props {
group: LxdGroup;
@@ -70,10 +71,7 @@ const EditGroupPanel: FC = ({ group, onClose }) => {
data: lxdIdentities = [],
isLoading: lxdIdentityLoading,
isError: lxdIdentitiesError,
- } = useQuery({
- queryKey: [queryKeys.identities],
- queryFn: fetchIdentities,
- });
+ } = useIdentities();
useEffect(() => {
if (!lxdIdentityLoading && !lxdIdentitiesError) {
const groupIds = new Set(getIdentityIdsForGroup(group));
diff --git a/src/pages/permissions/panels/EditIdentitiesForm.tsx b/src/pages/permissions/panels/EditIdentitiesForm.tsx
index 31819efb53..79567848ee 100644
--- a/src/pages/permissions/panels/EditIdentitiesForm.tsx
+++ b/src/pages/permissions/panels/EditIdentitiesForm.tsx
@@ -1,13 +1,11 @@
import { Icon, SearchBox, useNotify } from "@canonical/react-components";
-import { useQuery } from "@tanstack/react-query";
import { FC, useState } from "react";
-import { queryKeys } from "util/queryKeys";
import ScrollableTable from "components/ScrollableTable";
import SelectableMainTable from "components/SelectableMainTable";
-import { fetchIdentities } from "api/auth-identities";
import useSortTableData from "util/useSortTableData";
import type { LxdIdentity } from "types/permissions";
import { isUnrestricted } from "util/helpers";
+import { useIdentities } from "context/useIdentities";
export type FormIdentity = LxdIdentity & {
isRemoved?: boolean;
@@ -28,10 +26,7 @@ const EditIdentitiesForm: FC = ({
const notify = useNotify();
const [filter, setFilter] = useState(null);
- const { data: identities = [], error } = useQuery({
- queryKey: [queryKeys.identities],
- queryFn: fetchIdentities,
- });
+ const { data: identities = [], error } = useIdentities();
if (error) {
notify.failure("Loading details failed", error);
diff --git a/src/pages/permissions/panels/PermissionSelector.tsx b/src/pages/permissions/panels/PermissionSelector.tsx
index 681d518950..04b8c793cf 100644
--- a/src/pages/permissions/panels/PermissionSelector.tsx
+++ b/src/pages/permissions/panels/PermissionSelector.tsx
@@ -16,11 +16,11 @@ import {
} from "util/permissions";
import { queryKeys } from "util/queryKeys";
import { FormPermission } from "pages/permissions/panels/EditGroupPermissionsForm";
-import { fetchIdentities } from "api/auth-identities";
import ResourceOptionHeader from "./ResourceOptionHeader";
import type { LxdPermission } from "types/permissions";
import { SelectRef } from "@canonical/react-components/dist/components/CustomSelect/CustomSelect";
import { useImagesInAllProjects } from "context/useImages";
+import { useIdentities } from "context/useIdentities";
interface Props {
onAddPermission: (permission: FormPermission) => void;
@@ -52,10 +52,7 @@ const PermissionSelector: FC = ({ onAddPermission }) => {
const { data: images = [] } = useImagesInAllProjects();
- const { data: identities = [] } = useQuery({
- queryKey: [queryKeys.identities],
- queryFn: fetchIdentities,
- });
+ const { data: identities = [] } = useIdentities();
const imageLookup = getImageLookup(images);
const identityNamesLookup = getIdentityNameLookup(identities);
diff --git a/src/types/permissions.d.ts b/src/types/permissions.d.ts
index e4800d6e01..6913edecc2 100644
--- a/src/types/permissions.d.ts
+++ b/src/types/permissions.d.ts
@@ -12,6 +12,7 @@ export interface LxdIdentity {
effective_groups?: string[];
effective_permissions?: LxdPermission[];
fine_grained: boolean;
+ access_entitlements?: string[];
}
export interface LxdGroup {
diff --git a/src/util/entitlements/identities.tsx b/src/util/entitlements/identities.tsx
new file mode 100644
index 0000000000..edac3cbb42
--- /dev/null
+++ b/src/util/entitlements/identities.tsx
@@ -0,0 +1,14 @@
+import { useAuth } from "context/auth";
+import { hasEntitlement } from "./helpers";
+import { LxdIdentity } from "types/permissions";
+
+export const useIdentityEntitlements = () => {
+ const { isFineGrained } = useAuth();
+
+ const canEditIdentity = (identity?: LxdIdentity) =>
+ hasEntitlement(isFineGrained, "can_edit", identity?.access_entitlements);
+
+ return {
+ canEditIdentity,
+ };
+};
From ef0613dc3abfca379a15b55c811450c9a7815f56 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 09:59:48 +0200
Subject: [PATCH 03/22] feat: disable edit identity groups for restricted
permissions
Signed-off-by: Mason Hu
---
src/pages/permissions/PermissionIdentities.tsx | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/pages/permissions/PermissionIdentities.tsx b/src/pages/permissions/PermissionIdentities.tsx
index 7e9ac7a57c..a5b4d4a67e 100644
--- a/src/pages/permissions/PermissionIdentities.tsx
+++ b/src/pages/permissions/PermissionIdentities.tsx
@@ -34,6 +34,7 @@ import { isUnrestricted } from "util/helpers";
import IdentityResource from "components/IdentityResource";
import CreateTlsIdentityBtn from "./CreateTlsIdentityBtn";
import { useIdentities } from "context/useIdentities";
+import { useIdentityEntitlements } from "util/entitlements/identities";
const PermissionIdentities: FC = () => {
const notify = useNotify();
@@ -44,6 +45,7 @@ const PermissionIdentities: FC = () => {
const [searchParams] = useSearchParams();
const [selectedIdentityIds, setSelectedIdentityIds] = useState([]);
const { hasAccessManagementTLS } = useSupportedFeatures();
+ const { canEditIdentity } = useIdentityEntitlements();
useEffect(() => {
const validIdentityIds = new Set(identities.map((identity) => identity.id));
@@ -147,10 +149,12 @@ const PermissionIdentities: FC = () => {
className: "u-truncate",
},
{
- content: (
+ content: canEditIdentity(identity) ? (
{identity.groups?.length || 0}
+ ) : (
+ identity.groups?.length || 0
),
role: "cell",
className: "u-align--right group-count",
@@ -167,7 +171,12 @@ const PermissionIdentities: FC = () => {
onClick={openGroupPanelForIdentity}
type="button"
aria-label="Manage groups"
- title="Manage groups"
+ title={
+ canEditIdentity()
+ ? "Manage groups"
+ : "You do not have permission to modify this identity"
+ }
+ disabled={!canEditIdentity(identity)}
>
From 5a5fa5290d4344aeb166b8b4dda8c7733b54d3e4 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 10:08:55 +0200
Subject: [PATCH 04/22] feat: disable delete identity button when the identity
is restricted
Signed-off-by: Mason Hu
---
src/pages/permissions/actions/DeleteIdentityBtn.tsx | 9 ++++++++-
src/util/entitlements/identities.tsx | 4 ++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/pages/permissions/actions/DeleteIdentityBtn.tsx b/src/pages/permissions/actions/DeleteIdentityBtn.tsx
index fcc72d8d29..29701728b0 100644
--- a/src/pages/permissions/actions/DeleteIdentityBtn.tsx
+++ b/src/pages/permissions/actions/DeleteIdentityBtn.tsx
@@ -11,6 +11,7 @@ import type { LxdIdentity } from "types/permissions";
import ItemName from "components/ItemName";
import { deleteIdentity } from "api/auth-identities";
import IdentityResource from "components/IdentityResource";
+import { useIdentityEntitlements } from "util/entitlements/identities";
interface Props {
identity: LxdIdentity;
@@ -21,6 +22,7 @@ const DeleteIdentityBtn: FC = ({ identity }) => {
const notify = useNotify();
const toastNotify = useToastNotification();
const [isDeleting, setDeleting] = useState(false);
+ const { canDeleteIdentity } = useIdentityEntitlements();
const handleDelete = () => {
setDeleting(true);
@@ -53,7 +55,11 @@ const DeleteIdentityBtn: FC = ({ identity }) => {
return (
= ({ identity }) => {
shiftClickEnabled
showShiftClickHint
loading={isDeleting}
+ disabled={!canDeleteIdentity(identity)}
>
diff --git a/src/util/entitlements/identities.tsx b/src/util/entitlements/identities.tsx
index edac3cbb42..a7394ab4f3 100644
--- a/src/util/entitlements/identities.tsx
+++ b/src/util/entitlements/identities.tsx
@@ -8,7 +8,11 @@ export const useIdentityEntitlements = () => {
const canEditIdentity = (identity?: LxdIdentity) =>
hasEntitlement(isFineGrained, "can_edit", identity?.access_entitlements);
+ const canDeleteIdentity = (identity?: LxdIdentity) =>
+ hasEntitlement(isFineGrained, "can_delete", identity?.access_entitlements);
+
return {
+ canDeleteIdentity,
canEditIdentity,
};
};
From 773c8a95ba2f9cf92e3fb4aa951b550aadb49694 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 13:05:20 +0200
Subject: [PATCH 05/22] feat: disable bulk edit identity groups button when
user has no permission
Signed-off-by: Mason Hu
---
.../actions/EditIdentityGroupsBtn.tsx | 24 +++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx b/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
index bea67e14fc..10e3da0e62 100644
--- a/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
+++ b/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
@@ -2,6 +2,8 @@ import { FC } from "react";
import { Button, ButtonProps } from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
import usePanelParams from "util/usePanelParams";
+import { useIdentityEntitlements } from "util/entitlements/identities";
+import { pluralize } from "util/instanceBulkActions";
interface Props {
identities: LxdIdentity[];
@@ -13,20 +15,38 @@ const EditIdentityGroupsBtn: FC = ({
className,
...buttonProps
}) => {
+ const { canEditIdentity } = useIdentityEntitlements();
const panelParams = usePanelParams();
const buttonText =
identities.length > 1
? `Modify groups for ${identities.length} identities`
: "Modify groups";
+ const restrictedIdentities = identities.filter(
+ (identity) => !canEditIdentity(identity),
+ );
+
+ const getRestrictedWarning = () => {
+ const test = restrictedIdentities
+ .map((identity) => `\n- ${identity.name}`)
+ .join("");
+ return `You do not have permission to modify ${restrictedIdentities.length > 1 ? "some of the selected" : "the selected"} ${pluralize("identity", restrictedIdentities.length)}:${test}`;
+ };
+
return (
<>
panelParams.openIdentityGroups()}
aria-label="Modify groups"
- title="Modify groups"
+ title={
+ restrictedIdentities.length ? getRestrictedWarning() : "Modify groups"
+ }
className={className}
- disabled={!identities.length || !!panelParams.panel}
+ disabled={
+ !!restrictedIdentities.length ||
+ !identities.length ||
+ !!panelParams.panel
+ }
{...buttonProps}
>
{buttonText}
From 7a230e890714346656ef2b8258583e6d7ddb85ba Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 13:24:00 +0200
Subject: [PATCH 06/22] feat: show restricted identities in bulk delete
confirmation modal
Signed-off-by: Mason Hu
---
src/api/auth-identities.tsx | 2 +-
.../actions/BulkDeleteIdentitiesBtn.tsx | 66 +++++++++++++++----
2 files changed, 53 insertions(+), 15 deletions(-)
diff --git a/src/api/auth-identities.tsx b/src/api/auth-identities.tsx
index bfbe473a3d..4244f0e922 100644
--- a/src/api/auth-identities.tsx
+++ b/src/api/auth-identities.tsx
@@ -3,7 +3,7 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdIdentity, TlsIdentityTokenDetail } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-export const identitiesEntitlements = ["can_edit"];
+export const identitiesEntitlements = ["can_edit", "can_delete"];
export const fetchIdentities = (
isFineGrained: boolean | null,
diff --git a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
index 22fa6c9126..7b59aba125 100644
--- a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
@@ -2,6 +2,7 @@ import { FC, useState } from "react";
import {
ButtonProps,
ConfirmationButton,
+ Notification,
useNotify,
} from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
@@ -10,6 +11,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useToastNotification } from "context/toastNotificationProvider";
import { queryKeys } from "util/queryKeys";
import { pluralize } from "util/instanceBulkActions";
+import { useIdentityEntitlements } from "util/entitlements/identities";
interface Props {
identities: LxdIdentity[];
@@ -25,11 +27,22 @@ const BulkDeleteIdentitiesBtn: FC = ({
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 { canDeleteIdentity } = useIdentityEntitlements();
+
+ const restrictedIdentities: LxdIdentity[] = [];
+ const deletableIdentities: LxdIdentity[] = [];
+ identities.forEach((identity) => {
+ if (canDeleteIdentity(identity)) {
+ deletableIdentities.push(identity);
+ } else {
+ restrictedIdentities.push(identity);
+ }
+ });
const handleDelete = () => {
setLoading(true);
- deleteIdentities(identities)
+ const successMessage = `${deletableIdentities.length} ${pluralize("identity", deletableIdentities.length)} successfully deleted`;
+ deleteIdentities(deletableIdentities)
.then(() => {
void queryClient.invalidateQueries({
predicate: (query) => {
@@ -48,6 +61,40 @@ const BulkDeleteIdentitiesBtn: FC = ({
});
};
+ const getDeleteConfirmationMessage = () => {
+ if (!deletableIdentities.length) {
+ return (
+
+ You do not have permission to delete the selected identities
+
+ );
+ }
+
+ return (
+
+ This will permanently delete the following{" "}
+ {pluralize("identity", deletableIdentities.length)}:
+
+ {deletableIdentities.map((identity) => (
+ {identity.name}
+ ))}
+
+ You do not have permission to delete the following{" "}
+ {pluralize("identity", deletableIdentities.length)}:
+
+ {restrictedIdentities.map((identity) => (
+ {identity.name}
+ ))}
+
+ This action cannot be undone, and can result in data loss.
+
+ );
+ };
+
return (
= ({
loading={isLoading}
confirmationModalProps={{
title: "Confirm delete",
- children: (
-
- This will permanently delete the following identities:
-
- {identities.map((identity) => (
- {identity.name}
- ))}
-
- This action cannot be undone, and can result in data loss.
-
- ),
+ children: getDeleteConfirmationMessage(),
confirmButtonLabel: "Delete",
+ confirmButtonDisabled: !deletableIdentities.length,
onConfirm: handleDelete,
}}
disabled={!identities.length}
shiftClickEnabled
- showShiftClickHint
+ showShiftClickHint={!!deletableIdentities.length}
>
{buttonText}
From 5c1bf877ab4b4d77e59d6f55cab79ef39fcf8683 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 14:22:14 +0200
Subject: [PATCH 07/22] feat: enable group entitlements query
Signed-off-by: Mason Hu
---
src/api/auth-groups.tsx | 10 ++++++++--
src/context/useGroups.tsx | 15 +++++++++++++++
src/pages/permissions/PermissionGroups.tsx | 13 ++-----------
.../permissions/panels/CreateIdpGroupPanel.tsx | 13 +++----------
.../panels/EditIdentityGroupsPanel.tsx | 13 ++-----------
.../permissions/panels/EditIdpGroupPanel.tsx | 13 +++----------
src/types/permissions.d.ts | 1 +
src/util/entitlements/groups.tsx | 18 ++++++++++++++++++
8 files changed, 52 insertions(+), 44 deletions(-)
create mode 100644 src/context/useGroups.tsx
create mode 100644 src/util/entitlements/groups.tsx
diff --git a/src/api/auth-groups.tsx b/src/api/auth-groups.tsx
index f023208b92..d664f4c5cc 100644
--- a/src/api/auth-groups.tsx
+++ b/src/api/auth-groups.tsx
@@ -1,10 +1,16 @@
import { handleResponse, handleSettledResult } from "util/helpers";
import type { LxdApiResponse } from "types/apiResponse";
import type { LxdGroup } from "types/permissions";
+import { withEntitlementsQuery } from "util/entitlements/api";
-export const fetchGroups = (): Promise => {
+export const groupEntitlements = ["can_edit", "can_delete"];
+
+export const fetchGroups = (
+ isFineGrained: boolean | null,
+): Promise => {
+ const entitlements = `&${withEntitlementsQuery(isFineGrained, groupEntitlements)}`;
return new Promise((resolve, reject) => {
- fetch(`/1.0/auth/groups?recursion=1`)
+ fetch(`/1.0/auth/groups?recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse) => resolve(data.metadata))
.catch(reject);
diff --git a/src/context/useGroups.tsx b/src/context/useGroups.tsx
new file mode 100644
index 0000000000..93ad56cf41
--- /dev/null
+++ b/src/context/useGroups.tsx
@@ -0,0 +1,15 @@
+import { useQuery } from "@tanstack/react-query";
+import { queryKeys } from "util/queryKeys";
+import { UseQueryResult } from "@tanstack/react-query";
+import { useAuth } from "./auth";
+import { LxdGroup } from "types/permissions";
+import { fetchGroups } from "api/auth-groups";
+
+export const useGroups = (): UseQueryResult => {
+ const { isFineGrained } = useAuth();
+ return useQuery({
+ queryKey: [queryKeys.authGroups],
+ queryFn: () => fetchGroups(isFineGrained),
+ enabled: isFineGrained !== null,
+ });
+};
diff --git a/src/pages/permissions/PermissionGroups.tsx b/src/pages/permissions/PermissionGroups.tsx
index 7c07a5aa1b..5f51095736 100644
--- a/src/pages/permissions/PermissionGroups.tsx
+++ b/src/pages/permissions/PermissionGroups.tsx
@@ -6,13 +6,11 @@ import {
TablePagination,
useNotify,
} from "@canonical/react-components";
-import { useQuery } from "@tanstack/react-query";
import Loader from "components/Loader";
import ScrollableTable from "components/ScrollableTable";
import SelectableMainTable from "components/SelectableMainTable";
import SelectedTableNotification from "components/SelectedTableNotification";
import { FC, useEffect, useState } from "react";
-import { queryKeys } from "util/queryKeys";
import useSortTableData from "util/useSortTableData";
import { getIdentityIdsForGroup } from "util/permissionIdentities";
import usePanelParams, { panels } from "util/usePanelParams";
@@ -21,7 +19,6 @@ import PageHeader from "components/PageHeader";
import NotificationRow from "components/NotificationRow";
import HelpLink from "components/HelpLink";
import { useDocs } from "context/useDocs";
-import { fetchGroups } from "api/auth-groups";
import GroupActions from "./actions/GroupActions";
import CreateGroupPanel from "./panels/CreateGroupPanel";
import EditGroupPanel from "./panels/EditGroupPanel";
@@ -29,17 +26,11 @@ import PermissionGroupsFilter from "./PermissionGroupsFilter";
import EditGroupIdentitiesBtn from "./actions/EditGroupIdentitiesBtn";
import EditGroupIdentitiesPanel from "./panels/EditGroupIdentitiesPanel";
import BulkDeleteGroupsBtn from "./actions/BulkDeleteGroupsBtn";
+import { useGroups } from "context/useGroups";
const PermissionGroups: FC = () => {
const notify = useNotify();
- const {
- data: groups = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.authGroups],
- queryFn: fetchGroups,
- });
+ const { data: groups = [], error, isLoading } = useGroups();
const docBaseLink = useDocs();
const panelParams = usePanelParams();
const [search, setSearch] = useState("");
diff --git a/src/pages/permissions/panels/CreateIdpGroupPanel.tsx b/src/pages/permissions/panels/CreateIdpGroupPanel.tsx
index ad79f4c841..6c2c40502d 100644
--- a/src/pages/permissions/panels/CreateIdpGroupPanel.tsx
+++ b/src/pages/permissions/panels/CreateIdpGroupPanel.tsx
@@ -1,5 +1,5 @@
import { useNotify } from "@canonical/react-components";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useQueryClient } from "@tanstack/react-query";
import SidePanel from "components/SidePanel";
import { FC, useState } from "react";
import usePanelParams from "util/usePanelParams";
@@ -10,12 +10,12 @@ import { queryKeys } from "util/queryKeys";
import NotificationRow from "components/NotificationRow";
import { createIdpGroup } from "api/auth-idp-groups";
import { testDuplicateIdpGroupName } from "util/permissionIdpGroups";
-import { fetchGroups } from "api/auth-groups";
import IdpGroupForm, { IdpGroupFormValues } from "../forms/IdpGroupForm";
import GroupSelection from "./GroupSelection";
import useEditHistory from "util/useEditHistory";
import GroupSelectionActions from "../actions/GroupSelectionActions";
import ResourceLink from "components/ResourceLink";
+import { useGroups } from "context/useGroups";
type GroupEditHistory = {
groupsAdded: Set;
@@ -28,14 +28,7 @@ const CreateIdpGroupPanel: FC = () => {
const queryClient = useQueryClient();
const controllerState = useState(null);
- const {
- data: groups = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.authGroups],
- queryFn: fetchGroups,
- });
+ const { data: groups = [], error, isLoading } = useGroups();
const {
desiredState,
diff --git a/src/pages/permissions/panels/EditIdentityGroupsPanel.tsx b/src/pages/permissions/panels/EditIdentityGroupsPanel.tsx
index 9ba479ac37..f73c680059 100644
--- a/src/pages/permissions/panels/EditIdentityGroupsPanel.tsx
+++ b/src/pages/permissions/panels/EditIdentityGroupsPanel.tsx
@@ -1,9 +1,6 @@
import { useNotify } from "@canonical/react-components";
-import { useQuery } from "@tanstack/react-query";
-import { fetchGroups } from "api/auth-groups";
import SidePanel from "components/SidePanel";
import { FC, useEffect, useState } from "react";
-import { queryKeys } from "util/queryKeys";
import usePanelParams from "util/usePanelParams";
import { getGroupsForIdentities } from "util/permissionIdentities";
import useEditHistory from "util/useEditHistory";
@@ -12,6 +9,7 @@ import type { LxdIdentity } from "types/permissions";
import NotificationRow from "components/NotificationRow";
import GroupSelection from "./GroupSelection";
import GroupSelectionActions from "../actions/GroupSelectionActions";
+import { useGroups } from "context/useGroups";
type GroupEditHistory = {
groupsAdded: Set;
@@ -28,14 +26,7 @@ const EditIdentityGroupsPanel: FC = ({ identities, onClose }) => {
const notify = useNotify();
const [confirming, setConfirming] = useState(false);
- const {
- data: groups = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.authGroups],
- queryFn: fetchGroups,
- });
+ const { data: groups = [], error, isLoading } = useGroups();
const {
desiredState,
diff --git a/src/pages/permissions/panels/EditIdpGroupPanel.tsx b/src/pages/permissions/panels/EditIdpGroupPanel.tsx
index 51d187d6e8..17ecfdbfff 100644
--- a/src/pages/permissions/panels/EditIdpGroupPanel.tsx
+++ b/src/pages/permissions/panels/EditIdpGroupPanel.tsx
@@ -1,5 +1,5 @@
import { useNotify } from "@canonical/react-components";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useQueryClient } from "@tanstack/react-query";
import SidePanel from "components/SidePanel";
import { FC, useState } from "react";
import usePanelParams from "util/usePanelParams";
@@ -11,12 +11,12 @@ import NotificationRow from "components/NotificationRow";
import type { IdpGroup } from "types/permissions";
import { renameIdpGroup, updateIdpGroup } from "api/auth-idp-groups";
import { testDuplicateIdpGroupName } from "util/permissionIdpGroups";
-import { fetchGroups } from "api/auth-groups";
import useEditHistory from "util/useEditHistory";
import IdpGroupForm, { IdpGroupFormValues } from "../forms/IdpGroupForm";
import GroupSelection from "./GroupSelection";
import GroupSelectionActions from "../actions/GroupSelectionActions";
import ResourceLink from "components/ResourceLink";
+import { useGroups } from "context/useGroups";
type GroupEditHistory = {
groupsAdded: Set;
@@ -35,14 +35,7 @@ const EditIdpGroupPanel: FC = ({ idpGroup, onClose }) => {
const queryClient = useQueryClient();
const controllerState = useState(null);
- const {
- data: groups = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.authGroups],
- queryFn: fetchGroups,
- });
+ const { data: groups = [], error, isLoading } = useGroups();
const {
desiredState,
diff --git a/src/types/permissions.d.ts b/src/types/permissions.d.ts
index 6913edecc2..e4223dca6c 100644
--- a/src/types/permissions.d.ts
+++ b/src/types/permissions.d.ts
@@ -24,6 +24,7 @@ export interface LxdGroup {
tls?: string[];
};
identity_provider_groups?: string[];
+ access_entitlements?: string[];
}
export interface LxdPermission {
diff --git a/src/util/entitlements/groups.tsx b/src/util/entitlements/groups.tsx
new file mode 100644
index 0000000000..1556ddf873
--- /dev/null
+++ b/src/util/entitlements/groups.tsx
@@ -0,0 +1,18 @@
+import { useAuth } from "context/auth";
+import { hasEntitlement } from "./helpers";
+import { LxdGroup } from "types/permissions";
+
+export const useGroupEntitlements = () => {
+ const { isFineGrained } = useAuth();
+
+ const canEditGroup = (group?: LxdGroup) =>
+ hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
+
+ const canDeleteGroup = (group?: LxdGroup) =>
+ hasEntitlement(isFineGrained, "can_delete", group?.access_entitlements);
+
+ return {
+ canDeleteGroup,
+ canEditGroup,
+ };
+};
From d06810a6e251ad642f45971a2a1191bc1c713f86 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 14:27:48 +0200
Subject: [PATCH 08/22] feat: disable group creation for restricted permissions
Signed-off-by: Mason Hu
---
src/pages/permissions/PermissionGroups.tsx | 12 ++++++++++++
src/util/entitlements/server.tsx | 6 ++++++
2 files changed, 18 insertions(+)
diff --git a/src/pages/permissions/PermissionGroups.tsx b/src/pages/permissions/PermissionGroups.tsx
index 5f51095736..be37c60e1e 100644
--- a/src/pages/permissions/PermissionGroups.tsx
+++ b/src/pages/permissions/PermissionGroups.tsx
@@ -27,6 +27,7 @@ import EditGroupIdentitiesBtn from "./actions/EditGroupIdentitiesBtn";
import EditGroupIdentitiesPanel from "./panels/EditGroupIdentitiesPanel";
import BulkDeleteGroupsBtn from "./actions/BulkDeleteGroupsBtn";
import { useGroups } from "context/useGroups";
+import { useServerEntitlements } from "util/entitlements/server";
const PermissionGroups: FC = () => {
const notify = useNotify();
@@ -35,6 +36,7 @@ const PermissionGroups: FC = () => {
const panelParams = usePanelParams();
const [search, setSearch] = useState("");
const [selectedGroupNames, setSelectedGroupNames] = useState([]);
+ const { canCreateGroups } = useServerEntitlements();
if (error) {
notify.failure("Loading groups failed", error);
@@ -237,6 +239,10 @@ const PermissionGroups: FC = () => {
className="empty-state-button"
appearance="positive"
onClick={panelParams.openCreateGroup}
+ disabled={!canCreateGroups()}
+ title={
+ canCreateGroups() ? "" : "You do not have permission to create groups"
+ }
>
Create group
@@ -288,6 +294,12 @@ const PermissionGroups: FC = () => {
appearance="positive"
className="u-no-margin--bottom u-float-right"
onClick={panelParams.openCreateGroup}
+ disabled={!canCreateGroups()}
+ title={
+ canCreateGroups()
+ ? ""
+ : "You do not have permission to create groups"
+ }
>
Create group
diff --git a/src/util/entitlements/server.tsx b/src/util/entitlements/server.tsx
index cef83c28a1..7bc96e7196 100644
--- a/src/util/entitlements/server.tsx
+++ b/src/util/entitlements/server.tsx
@@ -4,6 +4,11 @@ import { hasEntitlement } from "./helpers";
export const useServerEntitlements = () => {
const { isFineGrained, serverEntitlements } = useAuth();
+ const canCreateGroups = () =>
+ hasEntitlement(isFineGrained, "can_create_groups", serverEntitlements) ||
+ hasEntitlement(isFineGrained, "permission_manager", serverEntitlements) ||
+ hasEntitlement(isFineGrained, "admin", serverEntitlements);
+
const canCreateIdentities = () =>
hasEntitlement(
isFineGrained,
@@ -42,6 +47,7 @@ export const useServerEntitlements = () => {
hasEntitlement(isFineGrained, "viewer", serverEntitlements);
return {
+ canCreateGroups,
canCreateIdentities,
canCreateProjects,
canCreateStoragePools,
From 8c6065f0b49abe0cff530e7ed241cd2b2b02502c Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 14:32:32 +0200
Subject: [PATCH 09/22] feat: disable delete group button if user does not have
permission
Signed-off-by: Mason Hu
---
src/pages/permissions/actions/GroupActions.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/pages/permissions/actions/GroupActions.tsx b/src/pages/permissions/actions/GroupActions.tsx
index 47c158d5c7..d611cab418 100644
--- a/src/pages/permissions/actions/GroupActions.tsx
+++ b/src/pages/permissions/actions/GroupActions.tsx
@@ -4,6 +4,7 @@ import type { LxdGroup } from "types/permissions";
import usePanelParams from "util/usePanelParams";
import DeleteGroupModal from "./DeleteGroupModal";
import { usePortal } from "@canonical/react-components";
+import { useGroupEntitlements } from "util/entitlements/groups";
interface Props {
group: LxdGroup;
@@ -12,6 +13,7 @@ interface Props {
const GroupActions: FC = ({ group }) => {
const panelParams = usePanelParams();
const { openPortal, closePortal, isOpen, Portal } = usePortal();
+ const { canDeleteGroup } = useGroupEntitlements();
return (
<>
@@ -37,7 +39,12 @@ const GroupActions: FC = ({ group }) => {
hasIcon
onClick={openPortal}
type="button"
- title="Delete group"
+ title={
+ canDeleteGroup()
+ ? "Delete group"
+ : "You do not have permission to delete this group"
+ }
+ disabled={!canDeleteGroup(group)}
>
,
From eff8daa6f433f8be4e4b0c9bda696f6f4c5ecc94 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 14:52:48 +0200
Subject: [PATCH 10/22] feat: show warning about groups with restricted
permissions when bulk deleting
Signed-off-by: Mason Hu
---
.../permissions/actions/DeleteGroupModal.tsx | 97 ++++++++++++++-----
1 file changed, 75 insertions(+), 22 deletions(-)
diff --git a/src/pages/permissions/actions/DeleteGroupModal.tsx b/src/pages/permissions/actions/DeleteGroupModal.tsx
index cbe908dfe8..8a75d6a3d4 100644
--- a/src/pages/permissions/actions/DeleteGroupModal.tsx
+++ b/src/pages/permissions/actions/DeleteGroupModal.tsx
@@ -2,6 +2,7 @@ import {
ActionButton,
Input,
Modal,
+ Notification,
useNotify,
} from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
@@ -10,6 +11,7 @@ import ResourceLabel from "components/ResourceLabel";
import { useToastNotification } from "context/toastNotificationProvider";
import { ChangeEvent, FC, useState } from "react";
import type { LxdGroup } from "types/permissions";
+import { useGroupEntitlements } from "util/entitlements/groups";
import { pluralize } from "util/instanceBulkActions";
import { queryKeys } from "util/queryKeys";
@@ -27,6 +29,17 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const [submitting, setSubmitting] = useState(false);
const hasOneGroup = groups.length === 1;
const confirmText = "confirm-delete-group";
+ const { canDeleteGroup } = useGroupEntitlements();
+
+ const restrictedGroups: LxdGroup[] = [];
+ const deletableGroups: LxdGroup[] = [];
+ groups.forEach((group) => {
+ if (canDeleteGroup(group)) {
+ deletableGroups.push(group);
+ } else {
+ restrictedGroups.push(group);
+ }
+ });
const handleConfirmInputChange = (e: ChangeEvent) => {
if (e.target.value === confirmText) {
@@ -40,18 +53,19 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const handleDeleteGroups = () => {
setSubmitting(true);
- const hasSingleGroup = groups.length === 1;
+ const hasSingleGroup = deletableGroups.length === 1;
const mutationPromise = hasSingleGroup
- ? deleteGroup(groups[0].name)
- : deleteGroups(groups.map((group) => group.name));
+ ? deleteGroup(deletableGroups[0].name)
+ : deleteGroups(deletableGroups.map((group) => group.name));
const successMessage = hasSingleGroup ? (
<>
- Group {" "}
+ Group{" "}
+ {" "}
deleted.
>
) : (
- `${groups.length} groups deleted.`
+ `${deletableGroups.length} groups deleted.`
);
mutationPromise
@@ -68,7 +82,7 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
})
.catch((e) => {
notify.failure(
- `${pluralize("group", groups.length)} deletion failed`,
+ `${pluralize("group", deletableGroups.length)} deletion failed`,
e,
);
})
@@ -77,6 +91,58 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
});
};
+ const getModalContent = () => {
+ if (!deletableGroups.length) {
+ return (
+
+ You do not have permission to delete the selected groups
+
+ );
+ }
+
+ return (
+ <>
+
+ Are you sure you want to permanently delete{" "}
+
+ {hasOneGroup
+ ? deletableGroups[0].name
+ : `${deletableGroups.length} groups`}
+
+ ?
+
+ {restrictedGroups.length && (
+
+
+ You do not have permission to delete the following groups:
+
+
+ {restrictedGroups.map((group) => (
+ {group.name}
+ ))}
+
+
+ )}
+
+ This action cannot be undone and may result in users losing access to
+ LXD, including the possibility that all users lose admin access.
+
+ To continue, please type the confirmation text below.
+
+ {confirmText}
+
+ >
+ );
+ };
+
return (
= ({ groups, close }) => {
value={confirmInput}
placeholder={confirmText}
className="u-no-margin--bottom"
+ disabled={!deletableGroups.length}
/>
,
= ({ groups, close }) => {
loading={submitting}
disabled={disableConfirm}
>
- {`Permanently delete ${groups.length} ${pluralize("group", groups.length)}`}
+ {`Permanently delete ${deletableGroups.length} ${pluralize("group", deletableGroups.length)}`}
,
]}
>
-
- Are you sure you want to permanently delete{" "}
-
- {hasOneGroup ? groups[0].name : `${groups.length} groups`}
-
- ?
-
-
- This action cannot be undone and may result in users losing access to
- LXD, including the possibility that all users lose admin access.
-
- To continue, please type the confirmation text below.
-
- {confirmText}
-
+ {getModalContent()}
);
};
From 8e2ac2a0e51255b153ab5378fbb4373e71c37852 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 15:18:37 +0200
Subject: [PATCH 11/22] feat: don't allow group identities bulk edits if the
user doesn't have the required permissions
Signed-off-by: Mason Hu
---
.../panels/EditGroupIdentitiesPanel.tsx | 95 ++++++++++++-------
1 file changed, 62 insertions(+), 33 deletions(-)
diff --git a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
index d85db66258..905792e46e 100644
--- a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
@@ -2,6 +2,7 @@ import {
ActionButton,
Button,
Icon,
+ Notification,
useNotify,
} from "@canonical/react-components";
import SidePanel from "components/SidePanel";
@@ -26,6 +27,7 @@ import ScrollableContainer from "components/ScrollableContainer";
import useSortTableData from "util/useSortTableData";
import { isUnrestricted } from "util/helpers";
import { useIdentities } from "context/useIdentities";
+import { useIdentityEntitlements } from "util/entitlements/identities";
type IdentityEditHistory = {
identitiesAdded: Set;
@@ -43,6 +45,10 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
const [confirming, setConfirming] = useState(false);
const { data: identities = [], error, isLoading } = useIdentities();
+ const { canEditIdentity } = useIdentityEntitlements();
+ const editableIdentities = identities.filter((identity) =>
+ canEditIdentity(identity),
+ );
const {
desiredState,
@@ -68,7 +74,7 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
}, [groups]);
const fineGrainedIdentities = identities.filter(
- (identity) => !isUnrestricted(identity),
+ (identity) => !isUnrestricted(identity) && canEditIdentity(identity),
);
const {
@@ -245,54 +251,77 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
defaultSort: "name",
});
- const content = (
-
- identity.id)}
- indeterminateNames={Array.from(indeterminateIdentities)}
- onToggleRow={toggleRow}
- hideContextualMenu
- />
-
- );
+ const confirmButtonText = modifiedIdentities.size
+ ? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
+ : "Modify identities";
+
+ const getPanelTitle = () => {
+ if (!editableIdentities.length) {
+ return;
+ }
- const panelTitle =
- groups.length > 1
+ return groups.length > 1
? `Change identities for ${groups.length} groups`
: `Change identities for ${groups[0]?.name}`;
+ };
- const confirmButtonText = modifiedIdentities.size
- ? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
- : "Modify identities";
+ const getPanelContent = () => {
+ if (!editableIdentities.length) {
+ return (
+
+ You do not have permission to change identities in groups.
+
+ );
+ }
+
+ return (
+
+ identity.id)}
+ indeterminateNames={Array.from(indeterminateIdentities)}
+ onToggleRow={toggleRow}
+ hideContextualMenu
+ />
+
+ );
+ };
return (
<>
- {panelTitle}
+ {getPanelTitle()}
-
+ {editableIdentities.length ? : null}
- {content}
+ {getPanelContent()}
From 836130592720aadddc7a20cbf5f2a8aa284d1e71 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 16:50:31 +0200
Subject: [PATCH 12/22] feat: disable bulk delete identities button when user
has no permission to delete any of the selected identities
Signed-off-by: Mason Hu
---
.../actions/BulkDeleteIdentitiesBtn.tsx | 66 ++++++++-----------
1 file changed, 26 insertions(+), 40 deletions(-)
diff --git a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
index 7b59aba125..eb57c22a18 100644
--- a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
@@ -2,7 +2,6 @@ import { FC, useState } from "react";
import {
ButtonProps,
ConfirmationButton,
- Notification,
useNotify,
} from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
@@ -61,57 +60,44 @@ const BulkDeleteIdentitiesBtn: FC = ({
});
};
- const getDeleteConfirmationMessage = () => {
- if (!deletableIdentities.length) {
- return (
-
- You do not have permission to delete the selected identities
-
- );
- }
-
- return (
-
- This will permanently delete the following{" "}
- {pluralize("identity", deletableIdentities.length)}:
-
- {deletableIdentities.map((identity) => (
- {identity.name}
- ))}
-
- You do not have permission to delete the following{" "}
- {pluralize("identity", deletableIdentities.length)}:
-
- {restrictedIdentities.map((identity) => (
- {identity.name}
- ))}
-
- This action cannot be undone, and can result in data loss.
-
- );
- };
-
return (
+ This will permanently delete the following{" "}
+ {pluralize("identity", deletableIdentities.length)}:
+
+ {deletableIdentities.map((identity) => (
+ {identity.name}
+ ))}
+
+ You do not have permission to delete the following{" "}
+ {pluralize("identity", deletableIdentities.length)}:
+
+ {restrictedIdentities.map((identity) => (
+ {identity.name}
+ ))}
+
+ This action cannot be undone, and can result in data loss.
+
+ ),
confirmButtonLabel: "Delete",
- confirmButtonDisabled: !deletableIdentities.length,
onConfirm: handleDelete,
}}
- disabled={!identities.length}
+ disabled={!deletableIdentities.length}
shiftClickEnabled
- showShiftClickHint={!!deletableIdentities.length}
+ showShiftClickHint
>
{buttonText}
From 79354c6ed80253a7e36e098d318acac584f65c85 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 17:01:12 +0200
Subject: [PATCH 13/22] feat: disable bulk delete group button if user does not
have permission to delete all selected groups
Signed-off-by: Mason Hu
---
.../permissions/actions/BulkDeleteGroupsBtn.tsx | 10 +++++++++-
src/pages/permissions/actions/DeleteGroupModal.tsx | 12 ------------
2 files changed, 9 insertions(+), 13 deletions(-)
diff --git a/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx b/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
index c20013e641..fa7b16f1ce 100644
--- a/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
@@ -3,6 +3,7 @@ import { Button, Icon } from "@canonical/react-components";
import type { LxdGroup } from "types/permissions";
import DeleteGroupModal from "./DeleteGroupModal";
import { pluralize } from "util/instanceBulkActions";
+import { useGroupEntitlements } from "util/entitlements/groups";
interface Props {
groups: LxdGroup[];
@@ -12,6 +13,8 @@ interface Props {
const BulkDeleteGroupsBtn: FC = ({ groups, className, onDelete }) => {
const [confirming, setConfirming] = useState(false);
+ const { canDeleteGroup } = useGroupEntitlements();
+ const deletableGroups = groups.filter(canDeleteGroup);
const handleConfirmDelete = () => {
setConfirming(true);
@@ -27,10 +30,15 @@ const BulkDeleteGroupsBtn: FC = ({ groups, className, onDelete }) => {
{`Delete ${groups.length} ${pluralize("group", groups.length)}`}
diff --git a/src/pages/permissions/actions/DeleteGroupModal.tsx b/src/pages/permissions/actions/DeleteGroupModal.tsx
index 8a75d6a3d4..5559e7d72a 100644
--- a/src/pages/permissions/actions/DeleteGroupModal.tsx
+++ b/src/pages/permissions/actions/DeleteGroupModal.tsx
@@ -92,18 +92,6 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
};
const getModalContent = () => {
- if (!deletableGroups.length) {
- return (
-
- You do not have permission to delete the selected groups
-
- );
- }
-
return (
<>
From 15e2c3ea82504554cdd5e0dc771c2462c996781e Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 17:19:22 +0200
Subject: [PATCH 14/22] feat: disable rows in bulk identity edit panel for
groups with restricted permissions
Signed-off-by: Mason Hu
---
src/components/SelectableMainTable.tsx | 20 ++--
.../panels/EditGroupIdentitiesPanel.tsx | 93 +++++++------------
2 files changed, 48 insertions(+), 65 deletions(-)
diff --git a/src/components/SelectableMainTable.tsx b/src/components/SelectableMainTable.tsx
index 18782b3194..cf08527d8e 100644
--- a/src/components/SelectableMainTable.tsx
+++ b/src/components/SelectableMainTable.tsx
@@ -26,6 +26,7 @@ interface SelectableMainTableProps {
onToggleRow?: (rowName: string) => void;
hideContextualMenu?: boolean;
defaultSortKey?: string;
+ hideHeaderCheckbox?: boolean;
}
type Props = SelectableMainTableProps & MainTableProps;
@@ -44,6 +45,7 @@ const SelectableMainTable: FC = ({
onToggleRow,
hideContextualMenu,
defaultSortKey,
+ hideHeaderCheckbox,
...props
}: Props) => {
const [currentSelectedIndex, setCurrentSelectedIndex] = useState();
@@ -85,14 +87,16 @@ const SelectableMainTable: FC = ({
{
content: (
<>
- Select all}
- labelClassName="multiselect-checkbox"
- checked={isAllSelected}
- indeterminate={isSomeSelected && !isAllSelected}
- onChange={isSomeSelected ? selectNone : selectPage}
- disabled={disableSelect}
- />
+ {!hideHeaderCheckbox && (
+ Select all}
+ labelClassName="multiselect-checkbox"
+ checked={isAllSelected}
+ indeterminate={isSomeSelected && !isAllSelected}
+ onChange={isSomeSelected ? selectNone : selectPage}
+ disabled={disableSelect}
+ />
+ )}
{!hideContextualMenu && (
= ({ groups }) => {
const { data: identities = [], error, isLoading } = useIdentities();
const { canEditIdentity } = useIdentityEntitlements();
- const editableIdentities = identities.filter((identity) =>
- canEditIdentity(identity),
+ const restrictedIdentities = identities.filter(
+ (identity) => !canEditIdentity(identity),
);
const {
@@ -74,7 +73,7 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
}, [groups]);
const fineGrainedIdentities = identities.filter(
- (identity) => !isUnrestricted(identity) && canEditIdentity(identity),
+ (identity) => !isUnrestricted(identity),
);
const {
@@ -228,7 +227,9 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
content: identity.name,
role: "cell",
"aria-label": "Identity",
- title: identity.name,
+ title: canEditIdentity(identity)
+ ? identity.name
+ : "You do not have permission to allocate this identity to groups",
},
{
content: modifiedIdentities.has(identity.id) && (
@@ -255,73 +256,51 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
: "Modify identities";
- const getPanelTitle = () => {
- if (!editableIdentities.length) {
- return;
- }
-
- return groups.length > 1
+ const panelTitle =
+ groups.length > 1
? `Change identities for ${groups.length} groups`
: `Change identities for ${groups[0]?.name}`;
- };
- const getPanelContent = () => {
- if (!editableIdentities.length) {
- return (
-
- You do not have permission to change identities in groups.
-
- );
- }
-
- return (
-
- identity.id)}
- indeterminateNames={Array.from(indeterminateIdentities)}
- onToggleRow={toggleRow}
- hideContextualMenu
- />
-
- );
- };
+ const content = (
+
+ identity.id)}
+ filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
+ indeterminateNames={Array.from(indeterminateIdentities)}
+ onToggleRow={toggleRow}
+ hideContextualMenu
+ hideHeaderCheckbox={!!restrictedIdentities.length}
+ />
+
+ );
return (
<>
- {getPanelTitle()}
+ {panelTitle}
- {editableIdentities.length ? : null}
+
- {getPanelContent()}
+ {content}
From 5f5e8437d1306470e81c8b42973a6c1fe35a6b75 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 18:52:27 +0200
Subject: [PATCH 15/22] feat: disable rows in edit group identities panel for
restricted identities
Signed-off-by: Mason Hu
---
src/components/SelectableMainTable.tsx | 22 +++++++++----------
src/pages/permissions/forms/GroupForm.tsx | 14 ++++++++++++
.../panels/EditGroupIdentitiesPanel.tsx | 2 +-
.../permissions/panels/EditGroupPanel.tsx | 22 ++++++++++++++-----
.../permissions/panels/EditIdentitiesForm.tsx | 14 ++++++++++--
5 files changed, 53 insertions(+), 21 deletions(-)
diff --git a/src/components/SelectableMainTable.tsx b/src/components/SelectableMainTable.tsx
index cf08527d8e..b8fdd2f42e 100644
--- a/src/components/SelectableMainTable.tsx
+++ b/src/components/SelectableMainTable.tsx
@@ -26,7 +26,7 @@ interface SelectableMainTableProps {
onToggleRow?: (rowName: string) => void;
hideContextualMenu?: boolean;
defaultSortKey?: string;
- hideHeaderCheckbox?: boolean;
+ disableHeaderCheckbox?: boolean;
}
type Props = SelectableMainTableProps & MainTableProps;
@@ -45,7 +45,7 @@ const SelectableMainTable: FC = ({
onToggleRow,
hideContextualMenu,
defaultSortKey,
- hideHeaderCheckbox,
+ disableHeaderCheckbox,
...props
}: Props) => {
const [currentSelectedIndex, setCurrentSelectedIndex] = useState();
@@ -87,16 +87,14 @@ const SelectableMainTable: FC = ({
{
content: (
<>
- {!hideHeaderCheckbox && (
- Select all}
- labelClassName="multiselect-checkbox"
- checked={isAllSelected}
- indeterminate={isSomeSelected && !isAllSelected}
- onChange={isSomeSelected ? selectNone : selectPage}
- disabled={disableSelect}
- />
- )}
+ Select all}
+ labelClassName="multiselect-checkbox"
+ checked={isAllSelected}
+ indeterminate={isSomeSelected && !isAllSelected}
+ onChange={isSomeSelected ? selectNone : selectPage}
+ disabled={disableSelect || disableHeaderCheckbox}
+ />
{!hideContextualMenu && (
= ({
@@ -29,7 +32,9 @@ const GroupForm: FC = ({
permissionCount,
permissionModifyCount,
isEditing = true,
+ group,
}) => {
+ const { canEditGroup } = useGroupEntitlements();
const getFormProps = (id: "name" | "description") => {
return {
id: id,
@@ -42,6 +47,11 @@ const GroupForm: FC = ({
};
};
+ const groupEditRestriction =
+ !isEditing || canEditGroup(group)
+ ? ""
+ : "You do not have permission to modify this group";
+
return (
@@ -212,7 +221,10 @@ const EditGroupPermissionsForm: FC = ({
Select the appropriate resource and entitlement below and add it to
the list of permissions for this group.
-
+
{!permissions.length ? (
diff --git a/src/pages/permissions/panels/PermissionSelector.tsx b/src/pages/permissions/panels/PermissionSelector.tsx
index 04b8c793cf..f89e651b31 100644
--- a/src/pages/permissions/panels/PermissionSelector.tsx
+++ b/src/pages/permissions/panels/PermissionSelector.tsx
@@ -24,9 +24,10 @@ import { useIdentities } from "context/useIdentities";
interface Props {
onAddPermission: (permission: FormPermission) => void;
+ disableReason?: string;
}
-const PermissionSelector: FC = ({ onAddPermission }) => {
+const PermissionSelector: FC = ({ onAddPermission, disableReason }) => {
const notify = useNotify();
const [resourceType, setResourceType] = useState("");
const [resource, setResource] = useState("");
@@ -47,7 +48,7 @@ const PermissionSelector: FC = ({ onAddPermission }) => {
} = useQuery({
queryKey: [queryKeys.permissions, resourceType],
queryFn: () => fetchPermissions({ resourceType }),
- enabled: !!resourceType,
+ enabled: !!resourceType && !!disableReason,
});
const { data: images = [] } = useImagesInAllProjects();
@@ -180,6 +181,7 @@ const PermissionSelector: FC = ({ onAddPermission }) => {
className="permission-selector"
tabIndex={0}
ref={permissionSelectorRef}
+ title={disableReason}
>
= ({ onAddPermission }) => {
value={resourceType}
selectRef={resourceTypeRef as SelectRef}
searchable="always"
+ disabled={!!disableReason}
/>
{
hasEntitlement(isFineGrained, "admin", serverEntitlements) ||
hasEntitlement(isFineGrained, "viewer", serverEntitlements);
+ const canViewPermissions = () =>
+ hasEntitlement(isFineGrained, "can_view_permissions", serverEntitlements) ||
+ hasEntitlement(isFineGrained, "permission_manager", serverEntitlements) ||
+ hasEntitlement(isFineGrained, "admin", serverEntitlements);
+
const canViewResources = () =>
hasEntitlement(isFineGrained, "can_view_resources", serverEntitlements) ||
hasEntitlement(isFineGrained, "admin", serverEntitlements) ||
@@ -53,6 +58,7 @@ export const useServerEntitlements = () => {
canCreateStoragePools,
canEditServerConfiguration,
canViewMetrics,
+ canViewPermissions,
canViewResources,
};
};
From d80b4ddb3af61c4e2f9d8a25a768aab26178fe8d Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 20:58:04 +0200
Subject: [PATCH 17/22] feat: enable idp group entitlement query
Signed-off-by: Mason Hu
---
src/api/auth-idp-groups.tsx | 10 ++++++++--
src/context/useIdpGroups.tsx | 15 +++++++++++++++
src/pages/permissions/PermissionIdpGroups.tsx | 13 ++-----------
src/types/permissions.d.ts | 1 +
src/util/entitlements/idp-groups.tsx | 18 ++++++++++++++++++
5 files changed, 44 insertions(+), 13 deletions(-)
create mode 100644 src/context/useIdpGroups.tsx
create mode 100644 src/util/entitlements/idp-groups.tsx
diff --git a/src/api/auth-idp-groups.tsx b/src/api/auth-idp-groups.tsx
index 01c964bf64..a4b5934bca 100644
--- a/src/api/auth-idp-groups.tsx
+++ b/src/api/auth-idp-groups.tsx
@@ -1,10 +1,16 @@
import { handleResponse, handleSettledResult } from "util/helpers";
import type { LxdApiResponse } from "types/apiResponse";
import type { IdpGroup } from "types/permissions";
+import { withEntitlementsQuery } from "util/entitlements/api";
-export const fetchIdpGroups = (): Promise => {
+const idpGroupEntitlements = ["can_edit", "can_delete"];
+
+export const fetchIdpGroups = (
+ isFineGrained: boolean | null,
+): Promise => {
+ const entitlements = `&${withEntitlementsQuery(isFineGrained, idpGroupEntitlements)}`;
return new Promise((resolve, reject) => {
- fetch(`/1.0/auth/identity-provider-groups?recursion=1`)
+ fetch(`/1.0/auth/identity-provider-groups?recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse) => resolve(data.metadata))
.catch(reject);
diff --git a/src/context/useIdpGroups.tsx b/src/context/useIdpGroups.tsx
new file mode 100644
index 0000000000..3b7955f2d9
--- /dev/null
+++ b/src/context/useIdpGroups.tsx
@@ -0,0 +1,15 @@
+import { useQuery } from "@tanstack/react-query";
+import { queryKeys } from "util/queryKeys";
+import { UseQueryResult } from "@tanstack/react-query";
+import { useAuth } from "./auth";
+import { IdpGroup } from "types/permissions";
+import { fetchIdpGroups } from "api/auth-idp-groups";
+
+export const useIdpGroups = (): UseQueryResult => {
+ const { isFineGrained } = useAuth();
+ return useQuery({
+ queryKey: [queryKeys.idpGroups],
+ queryFn: () => fetchIdpGroups(isFineGrained),
+ enabled: isFineGrained !== null,
+ });
+};
diff --git a/src/pages/permissions/PermissionIdpGroups.tsx b/src/pages/permissions/PermissionIdpGroups.tsx
index da7805482a..901b5ffc53 100644
--- a/src/pages/permissions/PermissionIdpGroups.tsx
+++ b/src/pages/permissions/PermissionIdpGroups.tsx
@@ -8,13 +8,11 @@ import {
TablePagination,
useNotify,
} from "@canonical/react-components";
-import { useQuery } from "@tanstack/react-query";
import Loader from "components/Loader";
import ScrollableTable from "components/ScrollableTable";
import SelectableMainTable from "components/SelectableMainTable";
import SelectedTableNotification from "components/SelectedTableNotification";
import { FC, useEffect, useState } from "react";
-import { queryKeys } from "util/queryKeys";
import useSortTableData from "util/useSortTableData";
import usePanelParams, { panels } from "util/usePanelParams";
import CustomLayout from "components/CustomLayout";
@@ -23,24 +21,17 @@ import NotificationRow from "components/NotificationRow";
import HelpLink from "components/HelpLink";
import { useDocs } from "context/useDocs";
import PermissionGroupsFilter from "./PermissionGroupsFilter";
-import { fetchIdpGroups } from "api/auth-idp-groups";
import CreateIdpGroupPanel from "./panels/CreateIdpGroupPanel";
import BulkDeleteIdpGroupsBtn from "./actions/BulkDeleteIdpGroupsBtn";
import EditIdpGroupPanel from "./panels/EditIdpGroupPanel";
import DeleteIdpGroupBtn from "./actions/DeleteIdpGroupBtn";
import { useSettings } from "context/useSettings";
import { Link } from "react-router-dom";
+import { useIdpGroups } from "context/useIdpGroups";
const PermissionIdpGroups: FC = () => {
const notify = useNotify();
- const {
- data: groups = [],
- error,
- isLoading,
- } = useQuery({
- queryKey: [queryKeys.idpGroups],
- queryFn: fetchIdpGroups,
- });
+ const { data: groups = [], error, isLoading } = useIdpGroups();
const docBaseLink = useDocs();
const panelParams = usePanelParams();
const [search, setSearch] = useState("");
diff --git a/src/types/permissions.d.ts b/src/types/permissions.d.ts
index e4223dca6c..525ff24b78 100644
--- a/src/types/permissions.d.ts
+++ b/src/types/permissions.d.ts
@@ -37,6 +37,7 @@ export interface LxdPermission {
export interface IdpGroup {
name: string;
groups: string[]; // these should be names of lxd groups
+ access_entitlements?: string[];
}
export interface TlsIdentityTokenDetail {
diff --git a/src/util/entitlements/idp-groups.tsx b/src/util/entitlements/idp-groups.tsx
new file mode 100644
index 0000000000..0a637ffb2d
--- /dev/null
+++ b/src/util/entitlements/idp-groups.tsx
@@ -0,0 +1,18 @@
+import { useAuth } from "context/auth";
+import { hasEntitlement } from "./helpers";
+import { IdpGroup } from "types/permissions";
+
+export const useIdpGroupEntitlements = () => {
+ const { isFineGrained } = useAuth();
+
+ const canEditGroup = (group?: IdpGroup) =>
+ hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
+
+ const canDeleteGroup = (group?: IdpGroup) =>
+ hasEntitlement(isFineGrained, "can_delete", group?.access_entitlements);
+
+ return {
+ canDeleteGroup,
+ canEditGroup,
+ };
+};
From 0f5afe6e7aaa4bb2130a706877c0dd76f4b3f948 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 21:04:21 +0200
Subject: [PATCH 18/22] feat: disable create idp groups for restricted
permissions
Signed-off-by: Mason Hu
---
src/pages/permissions/PermissionIdpGroups.tsx | 14 ++++++++++++++
src/pages/permissions/forms/GroupForm.tsx | 2 +-
src/util/entitlements/server.tsx | 10 ++++++++++
3 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/src/pages/permissions/PermissionIdpGroups.tsx b/src/pages/permissions/PermissionIdpGroups.tsx
index 901b5ffc53..55f1919fe5 100644
--- a/src/pages/permissions/PermissionIdpGroups.tsx
+++ b/src/pages/permissions/PermissionIdpGroups.tsx
@@ -28,6 +28,7 @@ import DeleteIdpGroupBtn from "./actions/DeleteIdpGroupBtn";
import { useSettings } from "context/useSettings";
import { Link } from "react-router-dom";
import { useIdpGroups } from "context/useIdpGroups";
+import { useServerEntitlements } from "util/entitlements/server";
const PermissionIdpGroups: FC = () => {
const notify = useNotify();
@@ -38,6 +39,7 @@ const PermissionIdpGroups: FC = () => {
const [selectedGroupNames, setSelectedGroupNames] = useState([]);
const { data: settings } = useSettings();
const hasCustomClaim = settings?.config?.["oidc.groups.claim"];
+ const { canCreateIdpGroups } = useServerEntitlements();
if (error) {
notify.failure("Loading provider groups failed", error);
@@ -239,6 +241,12 @@ const PermissionIdpGroups: FC = () => {
className="empty-state-button"
appearance="positive"
onClick={panelParams.openCreateIdpGroup}
+ disabled={!canCreateIdpGroups()}
+ title={
+ canCreateIdpGroups()
+ ? ""
+ : "You do not have permission to create IDP groups"
+ }
>
Create IDP group
@@ -285,6 +293,12 @@ const PermissionIdpGroups: FC = () => {
appearance="positive"
className="u-no-margin--bottom u-float-right"
onClick={panelParams.openCreateIdpGroup}
+ disabled={!canCreateIdpGroups()}
+ title={
+ canCreateIdpGroups()
+ ? ""
+ : "You do not have permission to create IDP groups"
+ }
>
Create IDP group
diff --git a/src/pages/permissions/forms/GroupForm.tsx b/src/pages/permissions/forms/GroupForm.tsx
index 64ce8d4c1f..7548f6cbcb 100644
--- a/src/pages/permissions/forms/GroupForm.tsx
+++ b/src/pages/permissions/forms/GroupForm.tsx
@@ -21,7 +21,7 @@ interface Props {
permissionCount: number;
permissionModifyCount: number;
isEditing?: boolean;
- group: LxdGroup;
+ group?: LxdGroup;
}
const GroupForm: FC = ({
diff --git a/src/util/entitlements/server.tsx b/src/util/entitlements/server.tsx
index 0faa0892e7..bc13a52bbc 100644
--- a/src/util/entitlements/server.tsx
+++ b/src/util/entitlements/server.tsx
@@ -18,6 +18,15 @@ export const useServerEntitlements = () => {
hasEntitlement(isFineGrained, "permission_manager", serverEntitlements) ||
hasEntitlement(isFineGrained, "admin", serverEntitlements);
+ const canCreateIdpGroups = () =>
+ hasEntitlement(
+ isFineGrained,
+ "can_create_identity_provider_groups",
+ serverEntitlements,
+ ) ||
+ hasEntitlement(isFineGrained, "permission_manager", serverEntitlements) ||
+ hasEntitlement(isFineGrained, "admin", serverEntitlements);
+
const canCreateProjects = () =>
hasEntitlement(isFineGrained, "can_create_projects", serverEntitlements) ||
hasEntitlement(isFineGrained, "project_manager", serverEntitlements) ||
@@ -54,6 +63,7 @@ export const useServerEntitlements = () => {
return {
canCreateGroups,
canCreateIdentities,
+ canCreateIdpGroups,
canCreateProjects,
canCreateStoragePools,
canEditServerConfiguration,
From cd4e3c12ca26d1e8592a2dceb1cfd2e033bd0cbb Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 21:12:04 +0200
Subject: [PATCH 19/22] feat: disable bulk delete idp group button with
restricted permissions
Signed-off-by: Mason Hu
---
.../actions/BulkDeleteIdpGroupsBtn.tsx | 10 +++-
.../permissions/actions/DeleteGroupModal.tsx | 3 +-
.../actions/DeleteIdpGroupsModal.tsx | 51 +++++++++++++++----
3 files changed, 53 insertions(+), 11 deletions(-)
diff --git a/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
index a16f8cf6ee..053c511bb1 100644
--- a/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
@@ -3,6 +3,7 @@ import { Button, Icon } from "@canonical/react-components";
import type { IdpGroup } from "types/permissions";
import { pluralize } from "util/instanceBulkActions";
import DeleteIdpGroupsModal from "./DeleteIdpGroupsModal";
+import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
interface Props {
idpGroups: IdpGroup[];
@@ -11,6 +12,8 @@ interface Props {
const BulkDeleteIdpGroupsBtn: FC = ({ idpGroups, className }) => {
const [confirming, setConfirming] = useState(false);
+ const { canDeleteGroup } = useIdpGroupEntitlements();
+ const deletableGroups = idpGroups.filter(canDeleteGroup);
const handleConfirmDelete = () => {
setConfirming(true);
@@ -25,10 +28,15 @@ const BulkDeleteIdpGroupsBtn: FC = ({ idpGroups, className }) => {
{`Delete ${idpGroups.length} ${pluralize("IDP group", idpGroups.length)}`}
diff --git a/src/pages/permissions/actions/DeleteGroupModal.tsx b/src/pages/permissions/actions/DeleteGroupModal.tsx
index 5559e7d72a..470c90c7bd 100644
--- a/src/pages/permissions/actions/DeleteGroupModal.tsx
+++ b/src/pages/permissions/actions/DeleteGroupModal.tsx
@@ -27,7 +27,6 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const [confirmInput, setConfirmInput] = useState("");
const [disableConfirm, setDisableConfirm] = useState(true);
const [submitting, setSubmitting] = useState(false);
- const hasOneGroup = groups.length === 1;
const confirmText = "confirm-delete-group";
const { canDeleteGroup } = useGroupEntitlements();
@@ -41,6 +40,8 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
}
});
+ const hasOneGroup = deletableGroups.length === 1;
+
const handleConfirmInputChange = (e: ChangeEvent) => {
if (e.target.value === confirmText) {
setDisableConfirm(false);
diff --git a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx b/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
index a88ccbb45e..d5dba19756 100644
--- a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
+++ b/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
@@ -1,10 +1,15 @@
-import { ConfirmationModal, useNotify } from "@canonical/react-components";
+import {
+ ConfirmationModal,
+ Notification,
+ useNotify,
+} from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
import { deleteIdpGroup, deleteIdpGroups } from "api/auth-idp-groups";
import ResourceLabel from "components/ResourceLabel";
import { useToastNotification } from "context/toastNotificationProvider";
import { FC, useState } from "react";
import type { IdpGroup } from "types/permissions";
+import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
import { pluralize } from "util/instanceBulkActions";
import { queryKeys } from "util/queryKeys";
@@ -18,22 +23,34 @@ const DeleteIdpGroupsModal: FC = ({ idpGroups, close }) => {
const notify = useNotify();
const toastNotify = useToastNotification();
const [submitting, setSubmitting] = useState(false);
- const hasOneGroup = idpGroups.length === 1;
+ const { canDeleteGroup } = useIdpGroupEntitlements();
+
+ const restrictedGroups: IdpGroup[] = [];
+ const deletableGroups: IdpGroup[] = [];
+ idpGroups.forEach((group) => {
+ if (canDeleteGroup(group)) {
+ deletableGroups.push(group);
+ } else {
+ restrictedGroups.push(group);
+ }
+ });
+
+ const hasOneGroup = deletableGroups.length === 1;
const handleDeleteIdpGroups = () => {
setSubmitting(true);
const mutationPromise = hasOneGroup
- ? deleteIdpGroup(idpGroups[0].name)
- : deleteIdpGroups(idpGroups.map((group) => group.name));
+ ? deleteIdpGroup(deletableGroups[0].name)
+ : deleteIdpGroups(deletableGroups.map((group) => group.name));
const successMessage = hasOneGroup ? (
<>
IDP group{" "}
- {" "}
+ {" "}
deleted.
>
) : (
- `${idpGroups.length} IDP groups deleted.`
+ `${deletableGroups.length} IDP groups deleted.`
);
mutationPromise
@@ -46,7 +63,7 @@ const DeleteIdpGroupsModal: FC = ({ idpGroups, close }) => {
})
.catch((e) => {
notify.failure(
- `${pluralize("IDP group", idpGroups.length)} deletion failed`,
+ `${pluralize("IDP group", deletableGroups.length)} deletion failed`,
e,
);
})
@@ -68,11 +85,27 @@ const DeleteIdpGroupsModal: FC = ({ idpGroups, close }) => {
Are you sure you want to delete{" "}
{hasOneGroup
- ? `"${idpGroups[0].name}"`
- : `${idpGroups.length} IDP groups`}
+ ? `"${deletableGroups[0].name}"`
+ : `${deletableGroups.length} IDP groups`}
? This action is permanent and can not be undone.
+ {restrictedGroups.length && (
+
+
+ You do not have permission to delete the following IDP groups:
+
+
+ {restrictedGroups.map((group) => (
+ {group.name}
+ ))}
+
+
+ )}
);
};
From 2f21a1255bc427d48b8d304228b324fdfdd6bb5e Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 21:14:14 +0200
Subject: [PATCH 20/22] feat: disable edit IDP group button for restricted
permissions
Signed-off-by: Mason Hu
---
src/pages/permissions/PermissionIdpGroups.tsx | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/pages/permissions/PermissionIdpGroups.tsx b/src/pages/permissions/PermissionIdpGroups.tsx
index 55f1919fe5..73cf2cfbbe 100644
--- a/src/pages/permissions/PermissionIdpGroups.tsx
+++ b/src/pages/permissions/PermissionIdpGroups.tsx
@@ -29,6 +29,7 @@ import { useSettings } from "context/useSettings";
import { Link } from "react-router-dom";
import { useIdpGroups } from "context/useIdpGroups";
import { useServerEntitlements } from "util/entitlements/server";
+import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
const PermissionIdpGroups: FC = () => {
const notify = useNotify();
@@ -40,6 +41,7 @@ const PermissionIdpGroups: FC = () => {
const { data: settings } = useSettings();
const hasCustomClaim = settings?.config?.["oidc.groups.claim"];
const { canCreateIdpGroups } = useServerEntitlements();
+ const { canEditGroup } = useIdpGroupEntitlements();
if (error) {
notify.failure("Loading provider groups failed", error);
@@ -120,7 +122,12 @@ const PermissionIdpGroups: FC = () => {
onClick={() => panelParams.openEditIdpGroup(idpGroup.name)}
type="button"
aria-label="Edit IDP group details"
- title="Edit details"
+ title={
+ canEditGroup(idpGroup)
+ ? "Edit details"
+ : "You do not have permission to edit this IDP group"
+ }
+ disabled={!canEditGroup(idpGroup)}
>
,
From 445950c8bb35bea8d9d2b3554d0d039ee50ee67f Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Mon, 17 Feb 2025 21:17:15 +0200
Subject: [PATCH 21/22] feat: disable delete idp group button for restricted
permissions
Signed-off-by: Mason Hu
---
src/pages/permissions/PermissionIdpGroups.tsx | 4 +++-
src/pages/permissions/actions/DeleteIdpGroupBtn.tsx | 9 ++++++++-
src/pages/permissions/panels/PermissionSelector.tsx | 2 +-
3 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/pages/permissions/PermissionIdpGroups.tsx b/src/pages/permissions/PermissionIdpGroups.tsx
index 73cf2cfbbe..6036c36b1e 100644
--- a/src/pages/permissions/PermissionIdpGroups.tsx
+++ b/src/pages/permissions/PermissionIdpGroups.tsx
@@ -94,7 +94,7 @@ const PermissionIdpGroups: FC = () => {
title: idpGroup.name,
},
{
- content: (
+ content: canEditGroup(idpGroup) ? (
{
>
{idpGroup.groups.length}
+ ) : (
+ idpGroup.groups.length
),
role: "cell",
className: "u-align--right",
diff --git a/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx b/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
index 99fbe45ac5..a7066923bd 100644
--- a/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
+++ b/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
@@ -3,6 +3,7 @@ import { Button, Icon } from "@canonical/react-components";
import type { IdpGroup } from "types/permissions";
import DeleteIdpGroupsModal from "./DeleteIdpGroupsModal";
import { usePortal } from "@canonical/react-components";
+import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
interface Props {
idpGroup: IdpGroup;
@@ -10,6 +11,7 @@ interface Props {
const DeleteIdpGroupBtn: FC = ({ idpGroup }) => {
const { openPortal, closePortal, isOpen, Portal } = usePortal();
+ const { canDeleteGroup } = useIdpGroupEntitlements();
return (
<>
@@ -20,7 +22,12 @@ const DeleteIdpGroupBtn: FC = ({ idpGroup }) => {
onClick={openPortal}
type="button"
aria-label="Delete IDP group"
- title="Delete IDP group"
+ title={
+ canDeleteGroup()
+ ? "Delete IDP group"
+ : "You do not have permission to delete this IDP group"
+ }
+ disabled={!canDeleteGroup(idpGroup)}
>
diff --git a/src/pages/permissions/panels/PermissionSelector.tsx b/src/pages/permissions/panels/PermissionSelector.tsx
index f89e651b31..a96848b972 100644
--- a/src/pages/permissions/panels/PermissionSelector.tsx
+++ b/src/pages/permissions/panels/PermissionSelector.tsx
@@ -48,7 +48,7 @@ const PermissionSelector: FC = ({ onAddPermission, disableReason }) => {
} = useQuery({
queryKey: [queryKeys.permissions, resourceType],
queryFn: () => fetchPermissions({ resourceType }),
- enabled: !!resourceType && !!disableReason,
+ enabled: !!resourceType && !disableReason,
});
const { data: images = [] } = useImagesInAllProjects();
From 01995ebc1f755003b61cf796d1552aa5bbcf61b7 Mon Sep 17 00:00:00 2001
From: Mason Hu
Date: Tue, 18 Feb 2025 10:20:31 +0200
Subject: [PATCH 22/22] fix: misc fixes
Signed-off-by: Mason Hu
---
src/api/auth-groups.tsx | 4 +-
src/api/auth-identities.tsx | 12 +-
src/api/auth-idp-groups.tsx | 15 +--
src/components/SelectableMainTable.tsx | 18 +--
src/context/useIdentities.tsx | 2 +-
src/pages/cluster/ClusterGroupForm.tsx | 2 +-
src/pages/images/ImageList.tsx | 2 +-
src/pages/instances/InstanceList.tsx | 4 +-
src/pages/instances/InstanceSnapshots.tsx | 2 +-
src/pages/permissions/PermissionGroups.tsx | 8 +-
.../permissions/PermissionIdentities.tsx | 32 +++--
src/pages/permissions/PermissionIdpGroups.tsx | 55 +++++----
.../actions/BulkDeleteGroupsBtn.tsx | 3 +-
.../actions/BulkDeleteIdentitiesBtn.tsx | 23 ++--
.../actions/BulkDeleteIdpGroupsBtn.tsx | 106 +++++++++-------
.../permissions/actions/DeleteGroupModal.tsx | 60 ++++++----
.../permissions/actions/DeleteIdpGroupBtn.tsx | 63 +++++-----
.../actions/DeleteIdpGroupsModal.tsx | 113 ------------------
.../actions/EditIdentityGroupsBtn.tsx | 10 +-
.../panels/EditGroupIdentitiesPanel.tsx | 24 ++--
.../permissions/panels/EditGroupPanel.tsx | 4 +-
.../panels/EditGroupPermissionsForm.tsx | 21 ++--
.../permissions/panels/EditIdentitiesForm.tsx | 16 +--
.../permissions/panels/GroupSelection.tsx | 2 +-
src/pages/storage/StorageVolumeSnapshots.tsx | 2 +-
src/sass/_selectable_main_table.scss | 2 +-
src/util/entitlements/groups.tsx | 6 +-
src/util/entitlements/identities.tsx | 6 +-
src/util/entitlements/idp-groups.tsx | 12 +-
src/util/permissionIdpGroups.tsx | 68 +++++++++++
30 files changed, 373 insertions(+), 324 deletions(-)
delete mode 100644 src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
diff --git a/src/api/auth-groups.tsx b/src/api/auth-groups.tsx
index d664f4c5cc..de0dacbbb8 100644
--- a/src/api/auth-groups.tsx
+++ b/src/api/auth-groups.tsx
@@ -3,12 +3,12 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdGroup } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-export const groupEntitlements = ["can_edit", "can_delete"];
+export const groupEntitlements = ["can_delete", "can_edit"];
export const fetchGroups = (
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, groupEntitlements)}`;
+ const entitlements = withEntitlementsQuery(isFineGrained, groupEntitlements);
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/groups?recursion=1${entitlements}`)
.then(handleResponse)
diff --git a/src/api/auth-identities.tsx b/src/api/auth-identities.tsx
index 4244f0e922..59dfbd3634 100644
--- a/src/api/auth-identities.tsx
+++ b/src/api/auth-identities.tsx
@@ -3,12 +3,15 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { LxdIdentity, TlsIdentityTokenDetail } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-export const identitiesEntitlements = ["can_edit", "can_delete"];
+export const identitiesEntitlements = ["can_delete", "can_edit"];
export const fetchIdentities = (
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, identitiesEntitlements)}`;
+ const entitlements = withEntitlementsQuery(
+ isFineGrained,
+ identitiesEntitlements,
+ );
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identities?recursion=1${entitlements}`)
.then(handleResponse)
@@ -31,7 +34,10 @@ export const fetchIdentity = (
authMethod: string,
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, identitiesEntitlements)}`;
+ const entitlements = withEntitlementsQuery(
+ isFineGrained,
+ identitiesEntitlements,
+ );
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identities/${authMethod}/${id}?recursion=1${entitlements}`)
.then(handleResponse)
diff --git a/src/api/auth-idp-groups.tsx b/src/api/auth-idp-groups.tsx
index a4b5934bca..7a76cd848e 100644
--- a/src/api/auth-idp-groups.tsx
+++ b/src/api/auth-idp-groups.tsx
@@ -3,12 +3,15 @@ import type { LxdApiResponse } from "types/apiResponse";
import type { IdpGroup } from "types/permissions";
import { withEntitlementsQuery } from "util/entitlements/api";
-const idpGroupEntitlements = ["can_edit", "can_delete"];
+const idpGroupEntitlements = ["can_delete", "can_edit"];
export const fetchIdpGroups = (
isFineGrained: boolean | null,
): Promise => {
- const entitlements = `&${withEntitlementsQuery(isFineGrained, idpGroupEntitlements)}`;
+ const entitlements = withEntitlementsQuery(
+ isFineGrained,
+ idpGroupEntitlements,
+ );
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identity-provider-groups?recursion=1${entitlements}`)
.then(handleResponse)
@@ -21,14 +24,8 @@ export const createIdpGroup = (group: Partial): Promise => {
return new Promise((resolve, reject) => {
fetch(`/1.0/auth/identity-provider-groups`, {
method: "POST",
- body: JSON.stringify({ name: group.name }),
+ body: JSON.stringify({ name: group.name, groups: group.groups }),
})
- .then(() =>
- fetch(`/1.0/auth/identity-provider-groups/${group.name}`, {
- method: "PUT",
- body: JSON.stringify(group),
- }),
- )
.then(handleResponse)
.then(resolve)
.catch(reject);
diff --git a/src/components/SelectableMainTable.tsx b/src/components/SelectableMainTable.tsx
index b8fdd2f42e..215574d428 100644
--- a/src/components/SelectableMainTable.tsx
+++ b/src/components/SelectableMainTable.tsx
@@ -19,14 +19,14 @@ interface SelectableMainTableProps {
parentName: string;
selectedNames: string[];
setSelectedNames: (val: string[], isUnselectAll?: boolean) => void;
- processingNames: string[];
+ disabledNames: string[];
rows: MainTableRow[];
indeterminateNames?: string[];
disableSelect?: boolean;
onToggleRow?: (rowName: string) => void;
hideContextualMenu?: boolean;
defaultSortKey?: string;
- disableHeaderCheckbox?: boolean;
+ disableSelectAll?: boolean;
}
type Props = SelectableMainTableProps & MainTableProps;
@@ -37,7 +37,7 @@ const SelectableMainTable: FC = ({
parentName,
selectedNames,
setSelectedNames,
- processingNames,
+ disabledNames,
rows,
headers,
indeterminateNames = [],
@@ -45,7 +45,7 @@ const SelectableMainTable: FC = ({
onToggleRow,
hideContextualMenu,
defaultSortKey,
- disableHeaderCheckbox,
+ disableSelectAll,
...props
}: Props) => {
const [currentSelectedIndex, setCurrentSelectedIndex] = useState();
@@ -93,7 +93,7 @@ const SelectableMainTable: FC = ({
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onChange={isSomeSelected ? selectNone : selectPage}
- disabled={disableSelect || disableHeaderCheckbox}
+ disabled={disableSelect || disableSelectAll}
/>
{!hideContextualMenu && (
= ({
];
const selectedNamesLookup = new Set(selectedNames);
- const processingNamesLookup = new Set(processingNames);
+ const disabledNamesLookup = new Set(disabledNames);
const indeterminateNamesLookup = new Set(indeterminateNames);
const rowsWithCheckbox = rows.map((row, rowIndex) => {
const isRowSelected = selectedNamesLookup.has(row.name ?? "");
- const isRowProcessing = processingNamesLookup.has(row.name ?? "");
+ const isRowDisabled = disabledNamesLookup.has(row.name ?? "");
const isRowIndeterminate = indeterminateNamesLookup.has(row.name ?? "");
const toggleRow = (event: PointerEvent) => {
@@ -185,7 +185,7 @@ const SelectableMainTable: FC = ({
labelClassName="u-no-margin--bottom"
checked={isRowSelected}
onChange={toggleRow}
- disabled={isRowProcessing || !row.name || disableSelect}
+ disabled={isRowDisabled || !row.name || disableSelect}
indeterminate={isRowIndeterminate && !isRowSelected}
/>
),
@@ -197,7 +197,7 @@ const SelectableMainTable: FC = ({
const className = classnames(row.className, {
"selected-row": isRowSelected,
- "processing-row": isRowProcessing,
+ "disabled-row": isRowDisabled,
});
const key = row.key ?? row.name;
diff --git a/src/context/useIdentities.tsx b/src/context/useIdentities.tsx
index e523226a19..615756013d 100644
--- a/src/context/useIdentities.tsx
+++ b/src/context/useIdentities.tsx
@@ -21,7 +21,7 @@ export const useIdentity = (
): UseQueryResult => {
const { isFineGrained } = useAuth();
return useQuery({
- queryKey: [queryKeys.identities, id],
+ queryKey: [queryKeys.identities, authMethod, id],
queryFn: () => fetchIdentity(id, authMethod, isFineGrained),
enabled: (enabled ?? true) && isFineGrained !== null,
});
diff --git a/src/pages/cluster/ClusterGroupForm.tsx b/src/pages/cluster/ClusterGroupForm.tsx
index 595cda1760..cf52d243ae 100644
--- a/src/pages/cluster/ClusterGroupForm.tsx
+++ b/src/pages/cluster/ClusterGroupForm.tsx
@@ -170,7 +170,7 @@ const ClusterGroupForm: FC = ({ group }) => {
setSelectedNames={(newMembers: string[]) =>
void formik.setFieldValue("members", newMembers)
}
- processingNames={[]}
+ disabledNames={[]}
/>
diff --git a/src/pages/images/ImageList.tsx b/src/pages/images/ImageList.tsx
index dcfc204dd1..73b6e0c4dd 100644
--- a/src/pages/images/ImageList.tsx
+++ b/src/pages/images/ImageList.tsx
@@ -288,7 +288,7 @@ const ImageList: FC = () => {
itemName="image"
parentName="project"
filteredNames={filteredImages.map((item) => item.fingerprint)}
- processingNames={processingNames}
+ disabledNames={processingNames}
rows={[]}
disableSelect={!deletableImages.length}
/>
diff --git a/src/pages/instances/InstanceList.tsx b/src/pages/instances/InstanceList.tsx
index 7fcf01d86c..8f73764bb6 100644
--- a/src/pages/instances/InstanceList.tsx
+++ b/src/pages/instances/InstanceList.tsx
@@ -699,7 +699,7 @@ const InstanceList: FC = () => {
parentName="project"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredInstances.map(
(instance) => instance.name,
)}
@@ -716,7 +716,7 @@ const InstanceList: FC = () => {
parentName="project"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredInstances.map(
(instance) => instance.name,
)}
diff --git a/src/pages/instances/InstanceSnapshots.tsx b/src/pages/instances/InstanceSnapshots.tsx
index 13744285f7..6eba368e5f 100644
--- a/src/pages/instances/InstanceSnapshots.tsx
+++ b/src/pages/instances/InstanceSnapshots.tsx
@@ -278,7 +278,7 @@ const InstanceSnapshots = (props: Props) => {
parentName="instance"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredSnapshots.map(
(snapshot) => snapshot.name,
)}
diff --git a/src/pages/permissions/PermissionGroups.tsx b/src/pages/permissions/PermissionGroups.tsx
index be37c60e1e..fa910b9ae3 100644
--- a/src/pages/permissions/PermissionGroups.tsx
+++ b/src/pages/permissions/PermissionGroups.tsx
@@ -209,7 +209,7 @@ const PermissionGroups: FC = () => {
parentName=""
selectedNames={selectedGroupNames}
setSelectedNames={setSelectedGroupNames}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={filteredGroups.map((item) => item.name)}
disableSelect={!!panelParams.panel}
/>
@@ -275,14 +275,14 @@ const PermissionGroups: FC = () => {
)}
{selectedGroupNames.length > 0 && !panelParams.panel && (
<>
- setSelectedGroupNames([])}
/>
- setSelectedGroupNames([])}
/>
>
)}
diff --git a/src/pages/permissions/PermissionIdentities.tsx b/src/pages/permissions/PermissionIdentities.tsx
index a5b4d4a67e..09613e51c3 100644
--- a/src/pages/permissions/PermissionIdentities.tsx
+++ b/src/pages/permissions/PermissionIdentities.tsx
@@ -35,6 +35,7 @@ import IdentityResource from "components/IdentityResource";
import CreateTlsIdentityBtn from "./CreateTlsIdentityBtn";
import { useIdentities } from "context/useIdentities";
import { useIdentityEntitlements } from "util/entitlements/identities";
+import { pluralize } from "util/instanceBulkActions";
const PermissionIdentities: FC = () => {
const notify = useNotify();
@@ -113,6 +114,25 @@ const PermissionIdentities: FC = () => {
setSelectedIdentityIds([identity.id]);
};
+ const getGroupLink = () => {
+ if (canEditIdentity(identity)) {
+ return (
+
+ {identity.groups?.length || 0}
+
+ );
+ }
+
+ const groupsText = pluralize("group", identity.groups?.length ?? 0);
+ const groupsList = identity.groups?.join("\n- ");
+ const groupsTitle = `Assigned ${groupsText}:\n- ${groupsList}`;
+ return (
+
+ {identity.groups?.length || 0}
+
+ );
+ };
+
return {
key: identity.id,
name: isUnrestricted(identity) ? "" : identity.id,
@@ -149,13 +169,7 @@ const PermissionIdentities: FC = () => {
className: "u-truncate",
},
{
- content: canEditIdentity(identity) ? (
-
- {identity.groups?.length || 0}
-
- ) : (
- identity.groups?.length || 0
- ),
+ content: getGroupLink(),
role: "cell",
className: "u-align--right group-count",
"aria-label": "Groups for this identity",
@@ -265,7 +279,7 @@ const PermissionIdentities: FC = () => {
{!!selectedIdentityIds.length && hasAccessManagementTLS && (
)}
@@ -302,7 +316,7 @@ const PermissionIdentities: FC = () => {
parentName=""
selectedNames={selectedIdentityIds}
setSelectedNames={setSelectedIdentityIds}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={fineGrainedIdentities.map(
(identity) => identity.id,
)}
diff --git a/src/pages/permissions/PermissionIdpGroups.tsx b/src/pages/permissions/PermissionIdpGroups.tsx
index 6036c36b1e..9caf4a156e 100644
--- a/src/pages/permissions/PermissionIdpGroups.tsx
+++ b/src/pages/permissions/PermissionIdpGroups.tsx
@@ -30,6 +30,7 @@ import { Link } from "react-router-dom";
import { useIdpGroups } from "context/useIdpGroups";
import { useServerEntitlements } from "util/entitlements/server";
import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import { pluralize } from "util/instanceBulkActions";
const PermissionIdpGroups: FC = () => {
const notify = useNotify();
@@ -41,7 +42,7 @@ const PermissionIdpGroups: FC = () => {
const { data: settings } = useSettings();
const hasCustomClaim = settings?.config?.["oidc.groups.claim"];
const { canCreateIdpGroups } = useServerEntitlements();
- const { canEditGroup } = useIdpGroupEntitlements();
+ const { canEditIdpGroup } = useIdpGroupEntitlements();
if (error) {
notify.failure("Loading provider groups failed", error);
@@ -81,6 +82,29 @@ const PermissionIdpGroups: FC = () => {
);
const rows = filteredGroups.map((idpGroup) => {
+ const getGroupLink = () => {
+ if (canEditIdpGroup(idpGroup)) {
+ return (
+ panelParams.openEditIdpGroup(idpGroup.name)}
+ >
+ {idpGroup.groups.length}
+
+ );
+ }
+
+ const groupsText = pluralize("group", idpGroup.groups?.length ?? 0);
+ const groupsList = idpGroup.groups?.join("\n- ");
+ const groupsTitle = `Assigned ${groupsText}:\n- ${groupsList}`;
+ return (
+
+ {idpGroup.groups?.length || 0}
+
+ );
+ };
+
return {
key: idpGroup.name,
name: idpGroup.name,
@@ -94,17 +118,7 @@ const PermissionIdpGroups: FC = () => {
title: idpGroup.name,
},
{
- content: canEditGroup(idpGroup) ? (
- panelParams.openEditIdpGroup(idpGroup.name)}
- >
- {idpGroup.groups.length}
-
- ) : (
- idpGroup.groups.length
- ),
+ content: getGroupLink(),
role: "cell",
className: "u-align--right",
"aria-label": "Number of mapped groups",
@@ -125,11 +139,11 @@ const PermissionIdpGroups: FC = () => {
type="button"
aria-label="Edit IDP group details"
title={
- canEditGroup(idpGroup)
+ canEditIdpGroup(idpGroup)
? "Edit details"
- : "You do not have permission to edit this IDP group"
+ : "You do not have permission to modify this IDP group"
}
- disabled={!canEditGroup(idpGroup)}
+ disabled={!canEditIdpGroup(idpGroup)}
>
,
@@ -232,7 +246,7 @@ const PermissionIdpGroups: FC = () => {
parentName=""
selectedNames={selectedGroupNames}
setSelectedNames={setSelectedGroupNames}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={filteredGroups.map((item) => item.name)}
disableSelect={!!panelParams.panel}
/>
@@ -277,7 +291,7 @@ const PermissionIdpGroups: FC = () => {
Identity provider groups
- {!selectedGroupNames.length && hasGroups && (
+ {!selectedGroupNames.length && hasGroups ? (
{
disabled={!!panelParams.idpGroup}
/>
- )}
+ ) : null}
{selectedGroupNames.length > 0 && !panelParams.panel && (
<>
-
+
>
)}
diff --git a/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx b/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
index fa7b16f1ce..7bee3a6863 100644
--- a/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteGroupsBtn.tsx
@@ -36,11 +36,10 @@ const BulkDeleteGroupsBtn: FC = ({ groups, className, onDelete }) => {
: `You do not have permission to delete the selected ${pluralize("group", groups.length)}`
}
className={className}
- appearance="negative"
hasIcon
disabled={!deletableGroups.length}
>
-
+
{`Delete ${groups.length} ${pluralize("group", groups.length)}`}
{confirming && (
diff --git a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
index eb57c22a18..1b41eae16f 100644
--- a/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdentitiesBtn.tsx
@@ -2,6 +2,7 @@ import { FC, useState } from "react";
import {
ButtonProps,
ConfirmationButton,
+ Icon,
useNotify,
} from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
@@ -67,7 +68,6 @@ const BulkDeleteIdentitiesBtn: FC = ({
? buttonText
: `You do not have permission to delete the selected ${pluralize("identity", identities.length)}`
}
- appearance=""
aria-label="Delete identities"
className={className}
loading={isLoading}
@@ -82,13 +82,17 @@ const BulkDeleteIdentitiesBtn: FC = ({
{identity.name}
))}
- You do not have permission to delete the following{" "}
- {pluralize("identity", deletableIdentities.length)}:
-
- {restrictedIdentities.map((identity) => (
- {identity.name}
- ))}
-
+ {restrictedIdentities.length ? (
+ <>
+ You do not have permission to delete the following{" "}
+ {pluralize("identity", restrictedIdentities.length)}:
+
+ {restrictedIdentities.map((identity) => (
+ {identity.name}
+ ))}
+
+ >
+ ) : null}
This action cannot be undone, and can result in data loss.
),
@@ -99,7 +103,8 @@ const BulkDeleteIdentitiesBtn: FC = ({
shiftClickEnabled
showShiftClickHint
>
- {buttonText}
+
+ {buttonText}
);
};
diff --git a/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx b/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
index 053c511bb1..a6cfb7dc76 100644
--- a/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
+++ b/src/pages/permissions/actions/BulkDeleteIdpGroupsBtn.tsx
@@ -1,53 +1,79 @@
-import { FC, useState } from "react";
-import { Button, Icon } from "@canonical/react-components";
+import { FC } from "react";
+import {
+ ConfirmationButton,
+ Icon,
+ Notification,
+} from "@canonical/react-components";
import type { IdpGroup } from "types/permissions";
import { pluralize } from "util/instanceBulkActions";
-import DeleteIdpGroupsModal from "./DeleteIdpGroupsModal";
-import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import { useDeleteIdpGroups } from "util/permissionIdpGroups";
interface Props {
idpGroups: IdpGroup[];
- className?: string;
}
-const BulkDeleteIdpGroupsBtn: FC = ({ idpGroups, className }) => {
- const [confirming, setConfirming] = useState(false);
- const { canDeleteGroup } = useIdpGroupEntitlements();
- const deletableGroups = idpGroups.filter(canDeleteGroup);
+const BulkDeleteIdpGroupsBtn: FC = ({ idpGroups }) => {
+ const {
+ isDeleting,
+ deletableIdpGroups,
+ restrictedIdpGroups,
+ deleteIdpGroups,
+ } = useDeleteIdpGroups(idpGroups);
- const handleConfirmDelete = () => {
- setConfirming(true);
- };
-
- const handleCloseConfirm = () => {
- setConfirming(false);
- };
+ const hasOneGroup = deletableIdpGroups.length === 1;
return (
- <>
-
-
- {`Delete ${idpGroups.length} ${pluralize("IDP group", idpGroups.length)}`}
-
- {confirming && (
-
- )}
- >
+
+
+ Are you sure you want to delete{" "}
+
+ {hasOneGroup
+ ? `"${deletableIdpGroups[0].name}"`
+ : `${deletableIdpGroups.length} IDP groups`}
+
+ ? This action is permanent and can not be undone.
+
+ {restrictedIdpGroups.length ? (
+
+
+ You do not have permission to delete the following IDP groups:
+
+
+ {restrictedIdpGroups.map((group) => (
+ {group.name}
+ ))}
+
+
+ ) : null}
+ >
+ ),
+ }}
+ >
+
+ {`Delete ${idpGroups.length} ${pluralize("IDP group", idpGroups.length)}`}
+
);
};
diff --git a/src/pages/permissions/actions/DeleteGroupModal.tsx b/src/pages/permissions/actions/DeleteGroupModal.tsx
index 470c90c7bd..e03ee31975 100644
--- a/src/pages/permissions/actions/DeleteGroupModal.tsx
+++ b/src/pages/permissions/actions/DeleteGroupModal.tsx
@@ -6,7 +6,7 @@ import {
useNotify,
} from "@canonical/react-components";
import { useQueryClient } from "@tanstack/react-query";
-import { deleteGroup, deleteGroups } from "api/auth-groups";
+import { deleteGroups } from "api/auth-groups";
import ResourceLabel from "components/ResourceLabel";
import { useToastNotification } from "context/toastNotificationProvider";
import { ChangeEvent, FC, useState } from "react";
@@ -14,6 +14,8 @@ import type { LxdGroup } from "types/permissions";
import { useGroupEntitlements } from "util/entitlements/groups";
import { pluralize } from "util/instanceBulkActions";
import { queryKeys } from "util/queryKeys";
+import LoggedInUserNotification from "../panels/LoggedInUserNotification";
+import { useSettings } from "context/useSettings";
interface Props {
groups: LxdGroup[];
@@ -29,15 +31,26 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const [submitting, setSubmitting] = useState(false);
const confirmText = "confirm-delete-group";
const { canDeleteGroup } = useGroupEntitlements();
+ const { data: settings } = useSettings();
+ const loggedInIdentityID = settings?.auth_user_name ?? "";
const restrictedGroups: LxdGroup[] = [];
const deletableGroups: LxdGroup[] = [];
+ let hasGroupsForLoggedInUser = false;
groups.forEach((group) => {
if (canDeleteGroup(group)) {
deletableGroups.push(group);
} else {
restrictedGroups.push(group);
}
+
+ if (group.identities?.oidc?.includes(loggedInIdentityID)) {
+ hasGroupsForLoggedInUser = true;
+ }
+
+ if (group.identities?.tls?.includes(loggedInIdentityID)) {
+ hasGroupsForLoggedInUser = true;
+ }
});
const hasOneGroup = deletableGroups.length === 1;
@@ -55,9 +68,6 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
const handleDeleteGroups = () => {
setSubmitting(true);
const hasSingleGroup = deletableGroups.length === 1;
- const mutationPromise = hasSingleGroup
- ? deleteGroup(deletableGroups[0].name)
- : deleteGroups(deletableGroups.map((group) => group.name));
const successMessage = hasSingleGroup ? (
<>
@@ -69,7 +79,7 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
`${deletableGroups.length} groups deleted.`
);
- mutationPromise
+ deleteGroups(deletableGroups.map((group) => group.name))
.then(() => {
void queryClient.invalidateQueries({
predicate: (query) => {
@@ -83,7 +93,7 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
})
.catch((e) => {
notify.failure(
- `${pluralize("group", deletableGroups.length)} deletion failed`,
+ `Failed deleting ${deletableGroups.length} ${pluralize("group", deletableGroups.length)}.`,
e,
);
})
@@ -104,22 +114,30 @@ const DeleteGroupModal: FC = ({ groups, close }) => {
?
- {restrictedGroups.length && (
-
-
- You do not have permission to delete the following groups:
-
-
- {restrictedGroups.map((group) => (
- {group.name}
- ))}
-
-
+ {hasGroupsForLoggedInUser && (
+
+
+
)}
+ {restrictedGroups.length ? (
+
+
+
+ You do not have permission to delete the following groups:
+
+
+ {restrictedGroups.map((group) => (
+ {group.name}
+ ))}
+
+
+
+ ) : null}
This action cannot be undone and may result in users losing access to
LXD, including the possibility that all users lose admin access.
diff --git a/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx b/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
index a7066923bd..c3af385560 100644
--- a/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
+++ b/src/pages/permissions/actions/DeleteIdpGroupBtn.tsx
@@ -1,42 +1,49 @@
import { FC } from "react";
-import { Button, Icon } from "@canonical/react-components";
+import { ConfirmationButton, Icon } from "@canonical/react-components";
import type { IdpGroup } from "types/permissions";
-import DeleteIdpGroupsModal from "./DeleteIdpGroupsModal";
-import { usePortal } from "@canonical/react-components";
import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import { useDeleteIdpGroups } from "util/permissionIdpGroups";
interface Props {
idpGroup: IdpGroup;
}
const DeleteIdpGroupBtn: FC = ({ idpGroup }) => {
- const { openPortal, closePortal, isOpen, Portal } = usePortal();
- const { canDeleteGroup } = useIdpGroupEntitlements();
+ const { canDeleteIdpGroup } = useIdpGroupEntitlements();
+ const { isDeleting, deletableIdpGroups, deleteIdpGroups } =
+ useDeleteIdpGroups([idpGroup]);
return (
- <>
-
-
-
- {isOpen && (
-
-
-
- )}
- >
+
+ Are you sure you want to delete{" "}
+ {deletableIdpGroups[0]?.name} ? This action is
+ permanent and can not be undone.
+
+ ),
+ }}
+ >
+
+
);
};
diff --git a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx b/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
deleted file mode 100644
index d5dba19756..0000000000
--- a/src/pages/permissions/actions/DeleteIdpGroupsModal.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import {
- ConfirmationModal,
- Notification,
- useNotify,
-} from "@canonical/react-components";
-import { useQueryClient } from "@tanstack/react-query";
-import { deleteIdpGroup, deleteIdpGroups } from "api/auth-idp-groups";
-import ResourceLabel from "components/ResourceLabel";
-import { useToastNotification } from "context/toastNotificationProvider";
-import { FC, useState } from "react";
-import type { IdpGroup } from "types/permissions";
-import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
-import { pluralize } from "util/instanceBulkActions";
-import { queryKeys } from "util/queryKeys";
-
-interface Props {
- idpGroups: IdpGroup[];
- close: () => void;
-}
-
-const DeleteIdpGroupsModal: FC = ({ idpGroups, close }) => {
- const queryClient = useQueryClient();
- const notify = useNotify();
- const toastNotify = useToastNotification();
- const [submitting, setSubmitting] = useState(false);
- const { canDeleteGroup } = useIdpGroupEntitlements();
-
- const restrictedGroups: IdpGroup[] = [];
- const deletableGroups: IdpGroup[] = [];
- idpGroups.forEach((group) => {
- if (canDeleteGroup(group)) {
- deletableGroups.push(group);
- } else {
- restrictedGroups.push(group);
- }
- });
-
- const hasOneGroup = deletableGroups.length === 1;
-
- const handleDeleteIdpGroups = () => {
- setSubmitting(true);
- const mutationPromise = hasOneGroup
- ? deleteIdpGroup(deletableGroups[0].name)
- : deleteIdpGroups(deletableGroups.map((group) => group.name));
-
- const successMessage = hasOneGroup ? (
- <>
- IDP group{" "}
- {" "}
- deleted.
- >
- ) : (
- `${deletableGroups.length} IDP groups deleted.`
- );
-
- mutationPromise
- .then(() => {
- void queryClient.invalidateQueries({
- queryKey: [queryKeys.idpGroups],
- });
- toastNotify.success(successMessage);
- close();
- })
- .catch((e) => {
- notify.failure(
- `${pluralize("IDP group", deletableGroups.length)} deletion failed`,
- e,
- );
- })
- .finally(() => {
- setSubmitting(false);
- });
- };
-
- return (
-
-
- Are you sure you want to delete{" "}
-
- {hasOneGroup
- ? `"${deletableGroups[0].name}"`
- : `${deletableGroups.length} IDP groups`}
-
- ? This action is permanent and can not be undone.
-
- {restrictedGroups.length && (
-
-
- You do not have permission to delete the following IDP groups:
-
-
- {restrictedGroups.map((group) => (
- {group.name}
- ))}
-
-
- )}
-
- );
-};
-
-export default DeleteIdpGroupsModal;
diff --git a/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx b/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
index 10e3da0e62..122861a2ca 100644
--- a/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
+++ b/src/pages/permissions/actions/EditIdentityGroupsBtn.tsx
@@ -1,5 +1,5 @@
import { FC } from "react";
-import { Button, ButtonProps } from "@canonical/react-components";
+import { Button, ButtonProps, Icon } from "@canonical/react-components";
import type { LxdIdentity } from "types/permissions";
import usePanelParams from "util/usePanelParams";
import { useIdentityEntitlements } from "util/entitlements/identities";
@@ -27,10 +27,10 @@ const EditIdentityGroupsBtn: FC = ({
);
const getRestrictedWarning = () => {
- const test = restrictedIdentities
+ const restrictedList = restrictedIdentities
.map((identity) => `\n- ${identity.name}`)
.join("");
- return `You do not have permission to modify ${restrictedIdentities.length > 1 ? "some of the selected" : "the selected"} ${pluralize("identity", restrictedIdentities.length)}:${test}`;
+ return `You do not have permission to modify ${restrictedIdentities.length > 1 ? "some of the selected" : "the selected"} ${pluralize("identity", restrictedIdentities.length)}:${restrictedList}`;
};
return (
@@ -47,9 +47,11 @@ const EditIdentityGroupsBtn: FC = ({
!identities.length ||
!!panelParams.panel
}
+ hasIcon
{...buttonProps}
>
- {buttonText}
+
+ {buttonText}
>
);
diff --git a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
index d5caee4f11..808d73d2fe 100644
--- a/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupIdentitiesPanel.tsx
@@ -229,7 +229,7 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
"aria-label": "Identity",
title: canEditIdentity(identity)
? identity.name
- : "You do not have permission to allocate this identity to groups",
+ : "You do not have permission to manage this identity",
},
{
content: modifiedIdentities.has(identity.id) && (
@@ -252,15 +252,6 @@ const EditGroupIdentitiesPanel: FC = ({ groups }) => {
defaultSort: "name",
});
- const confirmButtonText = modifiedIdentities.size
- ? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
- : "Modify identities";
-
- const panelTitle =
- groups.length > 1
- ? `Change identities for ${groups.length} groups`
- : `Change identities for ${groups[0]?.name}`;
-
const content = (
= ({ groups }) => {
parentName="server"
selectedNames={Array.from(selectedIdentities)}
setSelectedNames={modifyIdentities}
- processingNames={restrictedIdentities.map((identity) => identity.id)}
+ disabledNames={restrictedIdentities.map((identity) => identity.id)}
filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
indeterminateNames={Array.from(indeterminateIdentities)}
onToggleRow={toggleRow}
hideContextualMenu
- disableHeaderCheckbox={!!restrictedIdentities.length}
+ disableSelectAll={!!restrictedIdentities.length}
/>
);
+ const confirmButtonText = modifiedIdentities.size
+ ? `Apply ${modifiedIdentities.size} identity ${pluralize("change", modifiedIdentities.size)}`
+ : "Modify identities";
+
+ const panelTitle =
+ groups.length > 1
+ ? `Change identities for ${groups.length} groups`
+ : `Change identities for ${groups[0]?.name}`;
+
return (
<>
diff --git a/src/pages/permissions/panels/EditGroupPanel.tsx b/src/pages/permissions/panels/EditGroupPanel.tsx
index a05f0ea7ef..f4a92b7faa 100644
--- a/src/pages/permissions/panels/EditGroupPanel.tsx
+++ b/src/pages/permissions/panels/EditGroupPanel.tsx
@@ -167,7 +167,7 @@ const EditGroupPanel: FC = ({ group, onClose }) => {
permissions: permissions.filter((p) => !p.isRemoved),
};
- const getMutationPromise = () => {
+ const mutation = () => {
if (!canEditGroup(group)) {
return saveIdentities();
}
@@ -179,7 +179,7 @@ const EditGroupPanel: FC = ({ group, onClose }) => {
: updateGroup(groupPayload).then(saveIdentities);
};
- getMutationPromise()
+ mutation()
.then(() => {
closePanel();
toastNotify.success(
diff --git a/src/pages/permissions/panels/EditGroupPermissionsForm.tsx b/src/pages/permissions/panels/EditGroupPermissionsForm.tsx
index 1f18f20f2a..1c9201bd80 100644
--- a/src/pages/permissions/panels/EditGroupPermissionsForm.tsx
+++ b/src/pages/permissions/panels/EditGroupPermissionsForm.tsx
@@ -42,10 +42,17 @@ const EditGroupPermissionsForm: FC = ({
const [search, setSearch] = useState("");
const { canViewPermissions } = useServerEntitlements();
const { canEditGroup } = useGroupEntitlements();
- const permissionEditRestriction =
- !canViewPermissions() || !canEditGroup(group)
- ? "You do not have permission to edit entitlements for this group"
- : "";
+ const getEditRestriction = () => {
+ if (!canEditGroup(group)) {
+ return "You do not have permission to edit this group";
+ }
+
+ if (!canViewPermissions()) {
+ return "You are not allowed to view permissions";
+ }
+
+ return "";
+ };
const addPermission = (newPermission: FormPermission) => {
const permissionExists = permissions.find(
@@ -178,9 +185,9 @@ const EditGroupPermissionsForm: FC = ({
onClick={() => deletePermission(permission.id ?? "")}
type="button"
aria-label="Delete permission"
- title={permissionEditRestriction ?? "Delete permission"}
+ title={getEditRestriction() ?? "Delete permission"}
className="u-no-margin--right"
- disabled={!!permissionEditRestriction}
+ disabled={!!getEditRestriction()}
>
@@ -223,7 +230,7 @@ const EditGroupPermissionsForm: FC = ({
diff --git a/src/pages/permissions/panels/EditIdentitiesForm.tsx b/src/pages/permissions/panels/EditIdentitiesForm.tsx
index b59d0ac628..3e1799a488 100644
--- a/src/pages/permissions/panels/EditIdentitiesForm.tsx
+++ b/src/pages/permissions/panels/EditIdentitiesForm.tsx
@@ -29,9 +29,9 @@ const EditIdentitiesForm: FC = ({
const { data: identities = [], error } = useIdentities();
const { canEditIdentity } = useIdentityEntitlements();
- const restrictedIdentities = identities.filter(
- (identity) => !canEditIdentity(identity),
- );
+ const restrictedIdentityNames = identities
+ .filter((identity) => !canEditIdentity(identity))
+ .map((identity) => identity.id);
if (error) {
notify.failure("Loading details failed", error);
@@ -42,6 +42,10 @@ const EditIdentitiesForm: FC = ({
);
const toggleRow = (id: string) => {
+ if (restrictedIdentityNames.includes(id)) {
+ return;
+ }
+
const existing = selected.find((identity) => identity.id === id);
if (existing) {
const filtered = selected.filter((identity) => identity.id !== id);
@@ -171,14 +175,12 @@ const EditIdentitiesForm: FC = ({
.filter((id) => !id.isRemoved)
.map((identity) => identity.id)}
setSelectedNames={bulkSelect}
- processingNames={restrictedIdentities.map(
- (identity) => identity.name,
- )}
+ disabledNames={restrictedIdentityNames}
filteredNames={fineGrainedIdentities.map((identity) => identity.id)}
indeterminateNames={[]}
onToggleRow={toggleRow}
hideContextualMenu
- disableHeaderCheckbox={!!restrictedIdentities.length}
+ disableSelectAll={!!restrictedIdentityNames.length}
/>
>
diff --git a/src/pages/permissions/panels/GroupSelection.tsx b/src/pages/permissions/panels/GroupSelection.tsx
index 822f21c54c..dabb2c48b7 100644
--- a/src/pages/permissions/panels/GroupSelection.tsx
+++ b/src/pages/permissions/panels/GroupSelection.tsx
@@ -145,7 +145,7 @@ const GroupSelection: FC = ({
parentName=""
selectedNames={Array.from(selectedGroups)}
setSelectedNames={setSelectedGroups}
- processingNames={[]}
+ disabledNames={[]}
filteredNames={groups.map((group) => group.name)}
indeterminateNames={Array.from(indeterminateGroups ?? new Set())}
onToggleRow={toggleGroup}
diff --git a/src/pages/storage/StorageVolumeSnapshots.tsx b/src/pages/storage/StorageVolumeSnapshots.tsx
index 5fbd64a09e..a1e769e6f7 100644
--- a/src/pages/storage/StorageVolumeSnapshots.tsx
+++ b/src/pages/storage/StorageVolumeSnapshots.tsx
@@ -276,7 +276,7 @@ const StorageVolumeSnapshots: FC = ({ volume }) => {
parentName="instance"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
- processingNames={processingNames}
+ disabledNames={processingNames}
filteredNames={filteredSnapshots.map(
(snapshot) => snapshot.name,
)}
diff --git a/src/sass/_selectable_main_table.scss b/src/sass/_selectable_main_table.scss
index 8472068236..f80a8eb8ae 100644
--- a/src/sass/_selectable_main_table.scss
+++ b/src/sass/_selectable_main_table.scss
@@ -38,7 +38,7 @@
}
}
-.processing-row {
+.disabled-row {
opacity: 0.5;
.actions > * {
diff --git a/src/util/entitlements/groups.tsx b/src/util/entitlements/groups.tsx
index 1556ddf873..46f7d5e5be 100644
--- a/src/util/entitlements/groups.tsx
+++ b/src/util/entitlements/groups.tsx
@@ -5,12 +5,12 @@ import { LxdGroup } from "types/permissions";
export const useGroupEntitlements = () => {
const { isFineGrained } = useAuth();
- const canEditGroup = (group?: LxdGroup) =>
- hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
-
const canDeleteGroup = (group?: LxdGroup) =>
hasEntitlement(isFineGrained, "can_delete", group?.access_entitlements);
+ const canEditGroup = (group?: LxdGroup) =>
+ hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
+
return {
canDeleteGroup,
canEditGroup,
diff --git a/src/util/entitlements/identities.tsx b/src/util/entitlements/identities.tsx
index a7394ab4f3..56eaa4b1d6 100644
--- a/src/util/entitlements/identities.tsx
+++ b/src/util/entitlements/identities.tsx
@@ -5,12 +5,12 @@ import { LxdIdentity } from "types/permissions";
export const useIdentityEntitlements = () => {
const { isFineGrained } = useAuth();
- const canEditIdentity = (identity?: LxdIdentity) =>
- hasEntitlement(isFineGrained, "can_edit", identity?.access_entitlements);
-
const canDeleteIdentity = (identity?: LxdIdentity) =>
hasEntitlement(isFineGrained, "can_delete", identity?.access_entitlements);
+ const canEditIdentity = (identity?: LxdIdentity) =>
+ hasEntitlement(isFineGrained, "can_edit", identity?.access_entitlements);
+
return {
canDeleteIdentity,
canEditIdentity,
diff --git a/src/util/entitlements/idp-groups.tsx b/src/util/entitlements/idp-groups.tsx
index 0a637ffb2d..a55d5e614a 100644
--- a/src/util/entitlements/idp-groups.tsx
+++ b/src/util/entitlements/idp-groups.tsx
@@ -5,14 +5,14 @@ import { IdpGroup } from "types/permissions";
export const useIdpGroupEntitlements = () => {
const { isFineGrained } = useAuth();
- const canEditGroup = (group?: IdpGroup) =>
- hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
-
- const canDeleteGroup = (group?: IdpGroup) =>
+ const canDeleteIdpGroup = (group?: IdpGroup) =>
hasEntitlement(isFineGrained, "can_delete", group?.access_entitlements);
+ const canEditIdpGroup = (group?: IdpGroup) =>
+ hasEntitlement(isFineGrained, "can_edit", group?.access_entitlements);
+
return {
- canDeleteGroup,
- canEditGroup,
+ canDeleteIdpGroup,
+ canEditIdpGroup,
};
};
diff --git a/src/util/permissionIdpGroups.tsx b/src/util/permissionIdpGroups.tsx
index f6ead0f851..2afc893dfd 100644
--- a/src/util/permissionIdpGroups.tsx
+++ b/src/util/permissionIdpGroups.tsx
@@ -1,5 +1,15 @@
import { AbortControllerState, checkDuplicateName } from "./helpers";
import * as Yup from "yup";
+import { deleteIdpGroups } from "api/auth-idp-groups";
+import { useNotify } from "@canonical/react-components";
+import { useState } from "react";
+import { useIdpGroupEntitlements } from "util/entitlements/idp-groups";
+import ResourceLabel from "components/ResourceLabel";
+import { pluralize } from "util/instanceBulkActions";
+import { useQueryClient } from "@tanstack/react-query";
+import { useToastNotification } from "context/toastNotificationProvider";
+import { IdpGroup } from "types/permissions";
+import { queryKeys } from "./queryKeys";
export const testDuplicateIdpGroupName = (
controllerState: AbortControllerState,
@@ -21,3 +31,61 @@ export const testDuplicateIdpGroupName = (
},
];
};
+
+export const useDeleteIdpGroups = (idpGroups: IdpGroup[]) => {
+ const queryClient = useQueryClient();
+ const notify = useNotify();
+ const toastNotify = useToastNotification();
+ const [isDeleting, setIsDeleting] = useState(false);
+ const { canDeleteIdpGroup } = useIdpGroupEntitlements();
+
+ const restrictedGroups: IdpGroup[] = [];
+ const deletableGroups: IdpGroup[] = [];
+ idpGroups.forEach((group) => {
+ if (canDeleteIdpGroup(group)) {
+ deletableGroups.push(group);
+ } else {
+ restrictedGroups.push(group);
+ }
+ });
+
+ const hasOneGroup = deletableGroups.length === 1;
+
+ const handleDeleteIdpGroups = () => {
+ setIsDeleting(true);
+
+ const successMessage = hasOneGroup ? (
+ <>
+ IDP group{" "}
+ {" "}
+ deleted.
+ >
+ ) : (
+ `${deletableGroups.length} IDP groups deleted.`
+ );
+
+ deleteIdpGroups(deletableGroups.map((group) => group.name))
+ .then(() => {
+ void queryClient.invalidateQueries({
+ queryKey: [queryKeys.idpGroups],
+ });
+ toastNotify.success(successMessage);
+ })
+ .catch((e) => {
+ notify.failure(
+ `${pluralize("IDP group", deletableGroups.length)} deletion failed`,
+ e,
+ );
+ })
+ .finally(() => {
+ setIsDeleting(false);
+ });
+ };
+
+ return {
+ deleteIdpGroups: handleDeleteIdpGroups,
+ isDeleting,
+ restrictedIdpGroups: restrictedGroups,
+ deletableIdpGroups: deletableGroups,
+ };
+};