Skip to content

Commit

Permalink
feat: [WD-19698] Custom ISO can_delete permission check. (#1129)
Browse files Browse the repository at this point in the history
## Done

- Custom ISO - can delete permission implemented.
- Codebase refactor - Inkeeping with the fact that the there is a
disassociation between Storage pools and volumes within the codebase in
Context.
- Split /api/storage-pools into /api/storage-pools and
/api/storage-volumes and updated all imports.
- Consequently, storageVolumes now have their own entitlements file and
api file.

Fixes [list issues/bugs if needed]

## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
- Attempt to delete a Custom ISO volume with a user that has the
pemissions to do so.
- Attempt to delete a Custom ISO volume with a user that does not have
the pemissions to do so.
    - Observe the expected behaviour.

## Screenshots
Custom ISOs, one of which the user has permissions to delete, the other,
they do not.

![image](https://github.com/user-attachments/assets/84d374fc-6918-46c8-abe8-caf2e73402f7)
  • Loading branch information
Kxiru authored Feb 27, 2025
2 parents 83f1786 + a4bd9fc commit 17321ef
Show file tree
Hide file tree
Showing 16 changed files with 313 additions and 283 deletions.
265 changes: 0 additions & 265 deletions src/api/storage-pools.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import {
constructMemberError,
handleEtagResponse,
handleResponse,
handleSettledResult,
} from "util/helpers";
import type {
LxdStoragePool,
LXDStoragePoolOnClusterMember,
LxdStoragePoolResources,
LxdStorageVolume,
LxdStorageVolumeState,
UploadState,
} from "types/storage";
import type { LxdApiResponse } from "types/apiResponse";
import type { LxdOperationResponse } from "types/operation";
import type { AxiosResponse } from "axios";
import axios from "axios";
import type { LxdClusterMember } from "types/cluster";
import type { 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 = async (
pool: string,
Expand Down Expand Up @@ -252,260 +244,3 @@ export const fetchPoolFromClusterMembers = async (
.catch(reject);
});
};

export const fetchStorageVolumes = async (
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${entitlements}`,
)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume[]>) => {
resolve(data.metadata.map((volume) => ({ ...volume, pool })));
})
.catch(reject);
});
};

export const fetchAllStorageVolumes = async (
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}${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolume[]>) => {
resolve(data.metadata);
})
.catch(reject);
});
};

export const fetchStorageVolume = async (
pool: string,
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${entitlements}`,
)
.then(handleEtagResponse)
.then((data) => {
resolve({ ...data, pool } as LxdStorageVolume);
})
.catch(reject);
});
};

export const fetchStorageVolumeState = async (
pool: string,
project: string,
type: string,
volume: string,
): Promise<LxdStorageVolumeState> => {
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${pool}/volumes/${type}/${volume}/state?project=${project}&recursion=1`,
)
.then(handleResponse)
.then((data: LxdApiResponse<LxdStorageVolumeState>) => {
resolve(data.metadata);
})
.catch(reject);
});
};

export const renameStorageVolume = async (
project: string,
volume: LxdStorageVolume,
newName: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${volume.pool}/volumes/${volume.type}/${volume.name}?project=${project}`,
{
method: "POST",
body: JSON.stringify({
name: newName,
}),
},
)
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

export const createIsoStorageVolume = async (
pool: string,
isoFile: File,
name: string,
project: string,
setUploadState: (value: UploadState) => void,
uploadController: AbortController,
): Promise<LxdOperationResponse> => {
return new Promise((resolve, reject) => {
axios
.post(
`/1.0/storage-pools/${pool}/volumes/custom?project=${project}`,
isoFile,
{
headers: {
"Content-Type": "application/octet-stream",
"X-LXD-name": name,
"X-LXD-type": "iso",
},
onUploadProgress: (event) => {
setUploadState({
percentage: event.progress ? Math.floor(event.progress * 100) : 0,
loaded: event.loaded,
total: event.total,
});
},
signal: uploadController.signal,
},
)
.then((response: AxiosResponse<LxdOperationResponse>) => response.data)
.then(resolve)
.catch(reject);
});
};

export const createStorageVolume = async (
pool: string,
project: string,
volume: Partial<LxdStorageVolume>,
target?: string,
): Promise<void> => {
const targetParam = target ? `&target=${target}` : "";

return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${pool}/volumes?project=${project}${targetParam}`,
{
method: "POST",
body: JSON.stringify(volume),
},
)
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

export const updateStorageVolume = async (
pool: string,
project: string,
volume: Partial<LxdStorageVolume>,
): Promise<void> => {
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${pool}/volumes/${volume.type ?? ""}/${
volume.name ?? ""
}?project=${project}`,
{
method: "PUT",
body: JSON.stringify(volume),
headers: {
"If-Match": volume.etag ?? "invalid-etag",
},
},
)
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

export const deleteStorageVolume = async (
volume: string,
pool: string,
project: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${pool}/volumes/custom/${volume}?project=${project}`,
{
method: "DELETE",
},
)
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

export const migrateStorageVolume = async (
volume: Partial<LxdStorageVolume>,
targetPool: string,
targetProject: string,
): Promise<LxdOperationResponse> => {
return new Promise((resolve, reject) => {
fetch(
`/1.0/storage-pools/${volume.pool}/volumes/custom/${volume.name}?project=${targetProject}`,
{
method: "POST",
body: JSON.stringify({
name: volume.name,
pool: targetPool,
}),
},
)
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};

// Including project and target params if they did not change from source configs breaks the API call.
// Therefore, we only include them if they are different from the source configs, that's why both project and target are optional inputs
export const duplicateStorageVolume = async (
volume: Partial<LxdStorageVolume>,
pool: string,
project?: string,
target?: string,
): Promise<LxdOperationResponse> => {
return new Promise((resolve, reject) => {
const url = new URL(
`/1.0/storage-pools/${pool}/volumes/custom`,
window.location.origin,
);
const params = new URLSearchParams();

if (project) {
params.append("project", project);
}

if (target) {
params.append("target", target);
}

url.search = params.toString();

fetch(url.toString(), {
method: "POST",
body: JSON.stringify(volume),
})
.then(handleResponse)
.then(resolve)
.catch(reject);
});
};
Loading

0 comments on commit 17321ef

Please sign in to comment.