Skip to content

Commit

Permalink
feat: restricted permissions for profiles [WD-18904] (canonical#1116)
Browse files Browse the repository at this point in the history
  • Loading branch information
mas-who authored Feb 20, 2025
2 parents e751537 + 4a15d27 commit 2b6fe55
Show file tree
Hide file tree
Showing 33 changed files with 223 additions and 152 deletions.
4 changes: 2 additions & 2 deletions src/api/images.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const fetchImagesInProject = (
project: string,
isFineGrained: boolean | null,
): Promise<LxdImage[]> => {
const entitlements = `&${withEntitlementsQuery(isFineGrained, imageEntitlements)}`;
const entitlements = withEntitlementsQuery(isFineGrained, imageEntitlements);
return new Promise((resolve, reject) => {
fetch(`/1.0/images?recursion=1&project=${project}${entitlements}`)
.then(handleResponse)
Expand All @@ -31,7 +31,7 @@ export const fetchImagesInProject = (
export const fetchImagesInAllProjects = (
isFineGrained: boolean | null,
): Promise<LxdImage[]> => {
const entitlements = `&${withEntitlementsQuery(isFineGrained, imageEntitlements)}`;
const entitlements = withEntitlementsQuery(isFineGrained, imageEntitlements);
return new Promise((resolve, reject) => {
fetch(`/1.0/images?recursion=1&all-projects=1${entitlements}`)
.then(handleResponse)
Expand Down
16 changes: 11 additions & 5 deletions src/api/instances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@ import type { UploadState } from "types/storage";
import { withEntitlementsQuery } from "util/entitlements/api";

export const instanceEntitlements = [
"can_update_state",
"can_access_console",
"can_delete",
"can_edit",
"can_exec",
"can_manage_backups",
"can_manage_snapshots",
"can_exec",
"can_access_console",
"can_update_state",
];

export const fetchInstance = (
name: string,
project: string,
isFineGrained: boolean | null,
): Promise<LxdInstance> => {
const entitlements = `&${withEntitlementsQuery(isFineGrained, instanceEntitlements)}`;
const entitlements = withEntitlementsQuery(
isFineGrained,
instanceEntitlements,
);
return new Promise((resolve, reject) => {
fetch(
`/1.0/instances/${name}?project=${project}&recursion=2${entitlements}`,
Expand All @@ -45,7 +48,10 @@ export const fetchInstances = (
project: string,
isFineGrained: boolean | null,
): Promise<LxdInstance[]> => {
const entitlements = `&${withEntitlementsQuery(isFineGrained, instanceEntitlements)}`;
const entitlements = withEntitlementsQuery(
isFineGrained,
instanceEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/instances?project=${project}&recursion=2${entitlements}`)
.then(handleResponse)
Expand Down
21 changes: 18 additions & 3 deletions src/api/profiles.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import { handleEtagResponse, handleResponse } from "util/helpers";
import type { LxdProfile } from "types/profile";
import type { LxdApiResponse } from "types/apiResponse";
import { withEntitlementsQuery } from "util/entitlements/api";

const profileEntitlements = ["can_delete", "can_edit"];

export const fetchProfile = (
name: string,
project: string,
isFineGrained: boolean | null,
): Promise<LxdProfile> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
profileEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/profiles/${name}?project=${project}&recursion=1`)
fetch(`/1.0/profiles/${name}?project=${project}&recursion=1${entitlements}`)
.then(handleEtagResponse)
.then((data) => resolve(data as LxdProfile))
.catch(reject);
});
};

export const fetchProfiles = (project: string): Promise<LxdProfile[]> => {
export const fetchProfiles = (
project: string,
isFineGrained: boolean | null,
): Promise<LxdProfile[]> => {
const entitlements = withEntitlementsQuery(
isFineGrained,
profileEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/profiles?project=${project}&recursion=1`)
fetch(`/1.0/profiles?project=${project}&recursion=1${entitlements}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdProfile[]>) => resolve(data.metadata))
.catch(reject);
Expand Down
16 changes: 12 additions & 4 deletions src/api/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ import type { LxdOperationResponse } from "types/operation";
import { withEntitlementsQuery } from "util/entitlements/api";

const projectEntitlements = [
"can_create_images",
"can_create_image_aliases",
"can_create_images",
"can_create_instances",
"can_create_storage_volumes",
"can_create_networks",
"can_create_profiles",
"can_create_storage_volumes",
];

export const fetchProjects = (
isFineGrained: boolean | null,
): Promise<LxdProject[]> => {
const entitlements = `&${withEntitlementsQuery(isFineGrained, projectEntitlements)}`;
const entitlements = withEntitlementsQuery(
isFineGrained,
projectEntitlements,
);
return new Promise((resolve, reject) => {
fetch(`/1.0/projects?recursion=1${entitlements}`)
.then(handleResponse)
Expand All @@ -28,7 +32,11 @@ export const fetchProject = (
name: string,
isFineGrained: boolean | null,
): Promise<LxdProject> => {
const entitlements = `?${withEntitlementsQuery(isFineGrained, projectEntitlements)}`;
const entitlements = withEntitlementsQuery(
isFineGrained,
projectEntitlements,
"?",
);
return new Promise((resolve, reject) => {
fetch(`/1.0/projects/${name}${entitlements}`)
.then(handleEtagResponse)
Expand Down
11 changes: 5 additions & 6 deletions src/components/forms/CpuLimitInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchResources } from "api/server";
import Loader from "components/Loader";
import { useServerEntitlements } from "util/entitlements/server";

type Props = {
help?: string;
Expand All @@ -14,6 +15,7 @@ type Props = {

const CpuLimitInput: FC<Props> = ({ help, project, ...props }) => {
const notify = useNotify();
const { canViewResources } = useServerEntitlements();

const {
data: resources,
Expand All @@ -22,6 +24,7 @@ const CpuLimitInput: FC<Props> = ({ help, project, ...props }) => {
} = useQuery({
queryKey: [queryKeys.resources],
queryFn: fetchResources,
enabled: canViewResources(),
});

if (isLoading) {
Expand All @@ -43,15 +46,11 @@ const CpuLimitInput: FC<Props> = ({ help, project, ...props }) => {
};

const numberOfCores = getNumberOfCores();
if (!numberOfCores) {
return null;
}

const totalAvailable = (
const totalAvailable = numberOfCores ? (
<>
Total number of CPU cores: <b>{numberOfCores}</b>
</>
);
) : null;

return (
<Input
Expand Down
7 changes: 2 additions & 5 deletions src/components/forms/DiskDeviceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchStoragePools } from "api/storage-pools";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import { fetchProfiles } from "api/profiles";
import Loader from "components/Loader";
import { getInheritedDiskDevices } from "util/configInheritance";
import DiskDeviceFormRoot from "./DiskDeviceFormRoot";
import DiskDeviceFormInherited from "./DiskDeviceFormInherited";
import DiskDeviceFormCustom from "./DiskDeviceFormCustom";
import classnames from "classnames";
import ScrollableForm from "components/ScrollableForm";
import { useProfiles } from "context/useProfiles";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand All @@ -25,10 +25,7 @@ const DiskDeviceForm: FC<Props> = ({ formik, project }) => {
data: profiles = [],
isLoading: isProfileLoading,
error: profileError,
} = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});
} = useProfiles(project);

if (profileError) {
notify.failure("Loading profiles failed", profileError);
Expand Down
3 changes: 2 additions & 1 deletion src/components/forms/DiskDeviceFormRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,10 @@ const DiskDeviceFormRoot: FC<Props> = ({ formik, pools, profiles }) => {
}}
type="button"
appearance="base"
title="Edit"
title={formik.values.editRestriction ?? "Edit"}
className="u-no-margin--bottom"
hasIcon
disabled={!!formik.values.editRestriction}
>
<Icon name="edit" />
</Button>
Expand Down
9 changes: 2 additions & 7 deletions src/components/forms/GPUDeviceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import {
Notification,
useNotify,
} from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import type { LxdGPUDevice } from "types/device";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import { fetchProfiles } from "api/profiles";
import { getInheritedGPUs } from "util/configInheritance";
import Loader from "components/Loader";
import AttachGPUBtn from "components/forms/SelectGPUBtn";
Expand All @@ -32,6 +29,7 @@ import { deviceKeyToLabel, getExistingDeviceNames } from "util/devices";
import { ensureEditMode } from "util/instanceEdit";
import { useDocs } from "context/useDocs";
import GPUDeviceInput from "components/forms/GPUDeviceInput";
import { useProfiles } from "context/useProfiles";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand All @@ -46,10 +44,7 @@ const GPUDevicesForm: FC<Props> = ({ formik, project }) => {
data: profiles = [],
isLoading: isProfileLoading,
error: profileError,
} = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});
} = useProfiles(project);

if (profileError) {
notify.failure("Loading profiles failed", profileError);
Expand Down
3 changes: 3 additions & 0 deletions src/components/forms/MemoryLimitAvailable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { humanFileSize } from "util/helpers";
import Loader from "components/Loader";
import { limitToBytes } from "util/limits";
import { LxdProject } from "types/project";
import { useServerEntitlements } from "util/entitlements/server";

type Props = {
project?: LxdProject;
};

const MemoryLimitAvailable: FC<Props> = ({ project }) => {
const notify = useNotify();
const { canViewResources } = useServerEntitlements();

const {
data: resources,
Expand All @@ -22,6 +24,7 @@ const MemoryLimitAvailable: FC<Props> = ({ project }) => {
} = useQuery({
queryKey: [queryKeys.resources],
queryFn: fetchResources,
enabled: canViewResources(),
});

if (isLoading) {
Expand Down
9 changes: 2 additions & 7 deletions src/components/forms/NetworkDevicesForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import {
Tooltip,
useNotify,
} from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import type { LxdNicDevice } from "types/device";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import ScrollableConfigurationTable from "components/forms/ScrollableConfigurationTable";
import { fetchProfiles } from "api/profiles";
import { EditInstanceFormValues } from "pages/instances/EditInstance";
import { getConfigurationRowBase } from "components/ConfigurationRow";
import Loader from "components/Loader";
Expand All @@ -23,6 +20,7 @@ import { ensureEditMode } from "util/instanceEdit";
import { getExistingDeviceNames } from "util/devices";
import { focusField } from "util/formFields";
import { useNetworks } from "context/useNetworks";
import { useProfiles } from "context/useProfiles";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand All @@ -36,10 +34,7 @@ const NetworkDevicesForm: FC<Props> = ({ formik, project }) => {
data: profiles = [],
isLoading: isProfileLoading,
error: profileError,
} = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});
} = useProfiles(project);

if (profileError) {
notify.failure("Loading profiles failed", profileError);
Expand Down
7 changes: 2 additions & 5 deletions src/components/forms/OtherDeviceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import type { LxdDeviceValue } from "types/device";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import { fetchProfiles } from "api/profiles";
import { fetchConfigOptions } from "api/server";
import { useSupportedFeatures } from "context/useSupportedFeatures";
import { toConfigFields } from "util/config";
Expand All @@ -38,6 +37,7 @@ import {
findNoneDeviceIndex,
removeDevice,
} from "util/formDevices";
import { useProfiles } from "context/useProfiles";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand Down Expand Up @@ -66,10 +66,7 @@ const OtherDeviceForm: FC<Props> = ({ formik, project }) => {
data: profiles = [],
isLoading: isProfileLoading,
error: profileError,
} = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});
} = useProfiles(project);

if (profileError) {
notify.failure("Loading profiles failed", profileError);
Expand Down
9 changes: 2 additions & 7 deletions src/components/forms/ProxyDeviceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import {
Select,
useNotify,
} from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import type { LxdProxyDevice } from "types/device";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import { fetchProfiles } from "api/profiles";
import { getInheritedProxies } from "util/configInheritance";
import Loader from "components/Loader";
import ScrollableForm from "components/ScrollableForm";
Expand All @@ -33,6 +30,7 @@ import NewProxyBtn from "components/forms/NewProxyBtn";
import ConfigFieldDescription from "pages/settings/ConfigFieldDescription";
import { optionEnabledDisabled } from "util/instanceOptions";
import { getProxyAddress } from "util/proxyDevices";
import { useProfiles } from "context/useProfiles";

interface Props {
formik: InstanceAndProfileFormikProps;
Expand All @@ -46,10 +44,7 @@ const ProxyDeviceForm: FC<Props> = ({ formik, project }) => {
data: profiles = [],
isLoading: isProfileLoading,
error: profileError,
} = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});
} = useProfiles(project);

if (profileError) {
notify.failure("Loading profiles failed", profileError);
Expand Down
28 changes: 28 additions & 0 deletions src/context/useProfiles.tsx
Original file line number Diff line number Diff line change
@@ -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 { LxdProfile } from "types/profile";
import { fetchProfile, fetchProfiles } from "api/profiles";

export const useProfiles = (project: string): UseQueryResult<LxdProfile[]> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.profiles, project],
queryFn: () => fetchProfiles(project, isFineGrained),
enabled: !!project && isFineGrained !== null,
});
};

export const useProfile = (
profile: string,
project: string,
enabled?: boolean,
): UseQueryResult<LxdProfile> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.profiles, profile, queryKeys.projects, project],
queryFn: () => fetchProfile(profile, project, isFineGrained),
enabled: (enabled ?? true) && isFineGrained !== null,
});
};
Loading

0 comments on commit 2b6fe55

Please sign in to comment.