Skip to content

Commit

Permalink
feat: restricted permissions for storage pool [WD-19339] (canonical#1111
Browse files Browse the repository at this point in the history
)
  • Loading branch information
mas-who authored Feb 21, 2025
2 parents edb5202 + 782f077 commit 88ca2c1
Show file tree
Hide file tree
Showing 38 changed files with 343 additions and 188 deletions.
8 changes: 4 additions & 4 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ export const fetchNetworks = (
target?: string,
): Promise<LxdNetwork[]> => {
const targetParam = target ? `&target=${target}` : "";
const entitlements = `&${withEntitlementsQuery(
const entitlements = withEntitlementsQuery(
isFineGrained,
networkEntitlements,
)}`;
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/networks?project=${project}&recursion=1${targetParam}${entitlements}`,
Expand Down Expand Up @@ -81,10 +81,10 @@ export const fetchNetwork = (
target?: string,
): Promise<LxdNetwork> => {
const targetParam = target ? `&target=${target}` : "";
const entitlements = `&${withEntitlementsQuery(
const entitlements = withEntitlementsQuery(
isFineGrained,
networkEntitlements,
)}`;
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/networks/${name}?project=${project}${targetParam}${entitlements}`,
Expand Down
47 changes: 40 additions & 7 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,38 @@ import type { LxdOperationResponse } from "types/operation";
import axios, { AxiosResponse } from "axios";
import type { LxdClusterMember } from "types/cluster";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";
import { withEntitlementsQuery } from "util/entitlements/api";

export const storagePoolEntitlements = ["can_edit", "can_delete"];
export const storageVolumeEntitlements = ["can_delete"];

export const fetchStoragePool = (
pool: string,
isFineGrained: boolean | null,
target?: string,
): Promise<LxdStoragePool> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storagePoolEntitlements,
);
return new Promise((resolve, reject) => {
const targetParam = target ? `&target=${target}` : "";
fetch(`/1.0/storage-pools/${pool}?recursion=1${targetParam}`)
fetch(`/1.0/storage-pools/${pool}?recursion=1${targetParam}${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStoragePool>) => resolve(data.metadata))
.catch(reject);
});
};

export const fetchStoragePools = (): Promise<LxdStoragePool[]> => {
export const fetchStoragePools = (
isFineGrained: boolean | null,
): Promise<LxdStoragePool[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storagePoolEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools?recursion=1`)
fetch(`/1.0/storage-pools?recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStoragePool[]>) => resolve(data.metadata))
.catch(reject);
Expand Down Expand Up @@ -204,11 +219,12 @@ export const deleteStoragePool = (pool: string): Promise<void> => {
export const fetchPoolFromClusterMembers = (
poolName: string,
clusterMembers: LxdClusterMember[],
isFineGrained: boolean | null,
): Promise<LXDStoragePoolOnClusterMember[]> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchStoragePool(poolName, member.server_name);
return fetchStoragePool(poolName, isFineGrained, member.server_name);
}),
)
.then((results) => {
Expand All @@ -235,9 +251,16 @@ export const fetchPoolFromClusterMembers = (
export const fetchStorageVolumes = (
pool: string,
project: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storageVolumeEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-pools/${pool}/volumes?project=${project}&recursion=1`)
fetch(
`/1.0/storage-pools/${pool}/volumes?project=${project}&recursion=1${entitlements}`,
)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume[]>) =>
resolve(data.metadata.map((volume) => ({ ...volume, pool }))),
Expand All @@ -248,9 +271,14 @@ export const fetchStorageVolumes = (

export const fetchAllStorageVolumes = (
project: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storageVolumeEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/storage-volumes?recursion=1&project=${project}`)
fetch(`/1.0/storage-volumes?recursion=1&project=${project}${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume[]>) =>
resolve(data.metadata),
Expand All @@ -264,10 +292,15 @@ export const fetchStorageVolume = (
project: string,
type: string,
volume: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
storageVolumeEntitlements,
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${pool}/volumes/${type}/${volume}?project=${project}&recursion=1`,
`/1.0/storage-pools/${pool}/volumes/${type}/${volume}?project=${project}&recursion=1${entitlements}`,
)
.then(handleEtagResponse)
.then((data) => resolve({ ...data, pool } as LxdStorageVolume))
Expand Down
10 changes: 8 additions & 2 deletions src/components/forms/ClusterSpecificInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface Props {
helpText?: string | ReactNode;
placeholder?: string;
classname?: string;
disabledReason?: string;
}

const ClusterSpecificInput: FC<Props> = ({
Expand All @@ -36,6 +37,7 @@ const ClusterSpecificInput: FC<Props> = ({
helpText,
placeholder,
classname = "u-sv3",
disabledReason,
}) => {
const [isSpecific, setIsSpecific] = useState<boolean | null>(
isDefaultSpecific,
Expand Down Expand Up @@ -80,6 +82,8 @@ const ClusterSpecificInput: FC<Props> = ({
}
setIsSpecific((val) => !val);
}}
disabled={!!disabledReason}
title={disabledReason}
/>
)}
{isSpecific && (
Expand Down Expand Up @@ -118,8 +122,9 @@ const ClusterSpecificInput: FC<Props> = ({
className="u-no-margin--bottom"
value={activeValue}
onChange={(e) => setValueForMember(e.target.value, item)}
disabled={disabled}
disabled={!!disabledReason || disabled}
placeholder={placeholder}
title={disabledReason}
/>
)}
</div>
Expand Down Expand Up @@ -151,9 +156,10 @@ const ClusterSpecificInput: FC<Props> = ({
type="text"
value={firstValue}
onChange={(e) => setValueForAllMembers(e.target.value)}
disabled={disabled}
disabled={!!disabledReason || disabled}
help={helpText}
placeholder={placeholder}
title={disabledReason}
/>
)}
</div>
Expand Down
8 changes: 6 additions & 2 deletions src/components/forms/ClusteredDiskSizeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ interface Props {
setValue: (value: ClusterSpecificValues) => void;
values?: ClusterSpecificValues;
helpText?: string;
disabledReason?: string;
}

const ClusteredDiskSizeSelector: FC<Props> = ({
id,
setValue,
values,
helpText,
disabledReason,
}) => {
const { data: clusterMembers = [] } = useClusterMembers();
const memberNames = clusterMembers.map((member) => member.server_name);
Expand Down Expand Up @@ -59,6 +61,8 @@ const ClusteredDiskSizeSelector: FC<Props> = ({
setValueForAllMembers(firstValue);
setIsSpecific((val) => !val);
}}
disabled={!!disabledReason}
title={disabledReason}
/>
}
{isSpecific && (
Expand All @@ -81,7 +85,7 @@ const ClusteredDiskSizeSelector: FC<Props> = ({
id={memberNames.indexOf(item) === 0 ? id : `${id}-${item}`}
value={activeValue}
setMemoryLimit={(value) => setValueForMember(value, item)}
disabled={false}
disabled={!!disabledReason}
classname="u-no-margin--bottom"
/>
</div>
Expand All @@ -103,7 +107,7 @@ const ClusteredDiskSizeSelector: FC<Props> = ({
id={id}
value={firstValue}
setMemoryLimit={(value) => setValueForAllMembers(value)}
disabled={false}
disabled={!!disabledReason}
help={helpText}
/>
}
Expand Down
9 changes: 2 additions & 7 deletions src/components/forms/DiskDeviceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { FC } from "react";
import { Input, useNotify } from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchStoragePools } from "api/storage-pools";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import Loader from "components/Loader";
import { getInheritedDiskDevices } from "util/configInheritance";
Expand All @@ -12,6 +9,7 @@ import DiskDeviceFormCustom from "./DiskDeviceFormCustom";
import classnames from "classnames";
import ScrollableForm from "components/ScrollableForm";
import { useProfiles } from "context/useProfiles";
import { useStoragePools } from "context/useStoragePools";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand All @@ -35,10 +33,7 @@ const DiskDeviceForm: FC<Props> = ({ formik, project }) => {
data: pools = [],
isLoading: isStorageLoading,
error: storageError,
} = useQuery({
queryKey: [queryKeys.storage],
queryFn: () => fetchStoragePools(),
});
} = useStoragePools();

if (storageError) {
notify.failure("Loading storage pools failed", storageError);
Expand Down
4 changes: 4 additions & 0 deletions src/components/forms/DiskSizeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Props {
setMemoryLimit: (val: string) => void;
disabled?: boolean;
classname?: string;
disabledReason?: string;
}

const DiskSizeSelector: FC<Props> = ({
Expand All @@ -23,6 +24,7 @@ const DiskSizeSelector: FC<Props> = ({
setMemoryLimit,
disabled,
classname,
disabledReason,
}) => {
const limit = parseMemoryLimit(value) ?? {
value: 1,
Expand Down Expand Up @@ -55,6 +57,7 @@ const DiskSizeSelector: FC<Props> = ({
value={value?.match(/^\d/) ? limit.value : ""}
disabled={disabled}
className={classname}
title={disabledReason}
/>
<Select
id={`memUnitSelect-${id}`}
Expand All @@ -68,6 +71,7 @@ const DiskSizeSelector: FC<Props> = ({
value={limit.unit}
disabled={disabled}
className={classname}
title={disabledReason}
/>
</div>
{(help || helpTotal) && (
Expand Down
7 changes: 6 additions & 1 deletion src/context/loadCustomVolumes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import { loadVolumes } from "context/loadIsoVolumes";
export const loadCustomVolumes = async (
project: string,
hasStorageVolumesAll: boolean,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const result: LxdStorageVolume[] = [];

const volumes = await loadVolumes(project, hasStorageVolumesAll);
const volumes = await loadVolumes(
project,
hasStorageVolumesAll,
isFineGrained,
);
volumes.forEach((volume) => {
const contentTypes = ["filesystem", "block"];
const isFilesystemOrBlock = contentTypes.includes(volume.content_type);
Expand Down
19 changes: 14 additions & 5 deletions src/context/loadIsoVolumes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ import type { LxdStorageVolume } from "types/storage";
export const loadIsoVolumes = async (
project: string,
hasStorageVolumesAll: boolean,
isFineGrained: boolean | null,
): Promise<RemoteImage[]> => {
const remoteImages: RemoteImage[] = [];
const allVolumes = await loadVolumes(project, hasStorageVolumesAll);
const allVolumes = await loadVolumes(
project,
hasStorageVolumesAll,
isFineGrained,
);
allVolumes.forEach((volume) => {
if (volume.content_type === "iso") {
const image = isoToRemoteImage(volume);
Expand All @@ -26,20 +31,24 @@ export const loadIsoVolumes = async (
export const loadVolumes = async (
project: string,
hasStorageVolumesAll: boolean,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
return hasStorageVolumesAll
? fetchAllStorageVolumes(project)
: collectAllStorageVolumes(project);
? fetchAllStorageVolumes(project, isFineGrained)
: collectAllStorageVolumes(project, isFineGrained);
};

export const collectAllStorageVolumes = async (
project: string,
isFineGrained: boolean | null,
): Promise<LxdStorageVolume[]> => {
const allVolumes: LxdStorageVolume[] = [];
const pools = await fetchStoragePools();
const pools = await fetchStoragePools(isFineGrained);

const poolVolumes = await Promise.allSettled(
pools.map(async (pool) => fetchStorageVolumes(pool.name, project)),
pools.map(async (pool) =>
fetchStorageVolumes(pool.name, project, isFineGrained),
),
);

poolVolumes.forEach((result, index) => {
Expand Down
48 changes: 48 additions & 0 deletions src/context/useStoragePools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { UseQueryResult } from "@tanstack/react-query";
import { useAuth } from "./auth";
import { LxdStoragePool, LXDStoragePoolOnClusterMember } from "types/storage";
import {
fetchPoolFromClusterMembers,
fetchStoragePool,
fetchStoragePools,
} from "api/storage-pools";
import { useClusterMembers } from "./useClusterMembers";

export const useStoragePool = (
pool: string,
target?: string,
enabled?: boolean,
): UseQueryResult<LxdStoragePool> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.storage, pool, target],
queryFn: () => fetchStoragePool(pool, isFineGrained, target),
enabled: (enabled ?? true) && isFineGrained !== null,
});
};

export const useStoragePools = (
enabled?: boolean,
): UseQueryResult<LxdStoragePool[]> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.storage],
queryFn: () => fetchStoragePools(isFineGrained),
enabled: (enabled ?? true) && isFineGrained !== null,
});
};

export const usePoolFromClusterMembers = (
pool: string,
): UseQueryResult<LXDStoragePoolOnClusterMember[]> => {
const { isFineGrained } = useAuth();
const { data: clusterMembers = [] } = useClusterMembers();
return useQuery({
queryKey: [queryKeys.storage, pool, queryKeys.cluster],
queryFn: () =>
fetchPoolFromClusterMembers(pool, clusterMembers, isFineGrained),
enabled: isFineGrained !== null && clusterMembers.length > 0,
});
};
Loading

0 comments on commit 88ca2c1

Please sign in to comment.