Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restricted permissions for permission management [WD-18906] #1113

Merged
merged 22 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c8ef858
feat: disbale create tls identity button when user has no permission
MasWho Feb 17, 2025
845f76e
feat: enable entitlement queries for identities
MasWho Feb 17, 2025
ef0613d
feat: disable edit identity groups for restricted permissions
MasWho Feb 17, 2025
5a5fa52
feat: disable delete identity button when the identity is restricted
MasWho Feb 17, 2025
773c8a9
feat: disable bulk edit identity groups button when user has no permi…
MasWho Feb 17, 2025
7a230e8
feat: show restricted identities in bulk delete confirmation modal
MasWho Feb 17, 2025
5c1bf87
feat: enable group entitlements query
MasWho Feb 17, 2025
d06810a
feat: disable group creation for restricted permissions
MasWho Feb 17, 2025
8c6065f
feat: disable delete group button if user does not have permission
MasWho Feb 17, 2025
eff8daa
feat: show warning about groups with restricted permissions when bulk…
MasWho Feb 17, 2025
8e2ac2a
feat: don't allow group identities bulk edits if the user doesn't hav…
MasWho Feb 17, 2025
8361305
feat: disable bulk delete identities button when user has no permissi…
MasWho Feb 17, 2025
79354c6
feat: disable bulk delete group button if user does not have permissi…
MasWho Feb 17, 2025
15e2c3e
feat: disable rows in bulk identity edit panel for groups with restri…
MasWho Feb 17, 2025
5f5e843
feat: disable rows in edit group identities panel for restricted iden…
MasWho Feb 17, 2025
98b4b1d
feat: disable group permission editing for restricted permissions
MasWho Feb 17, 2025
d80b4dd
feat: enable idp group entitlement query
MasWho Feb 17, 2025
0f5afe6
feat: disable create idp groups for restricted permissions
MasWho Feb 17, 2025
cd4e3c1
feat: disable bulk delete idp group button with restricted permissions
MasWho Feb 17, 2025
2f21a12
feat: disable edit IDP group button for restricted permissions
MasWho Feb 17, 2025
445950c
feat: disable delete idp group button for restricted permissions
MasWho Feb 17, 2025
01995eb
fix: misc fixes
MasWho Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/api/auth-groups.tsx
Original file line number Diff line number Diff line change
@@ -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<LxdGroup[]> => {
export const groupEntitlements = ["can_delete", "can_edit"];

export const fetchGroups = (
isFineGrained: boolean | null,
): Promise<LxdGroup[]> => {
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<LxdGroup[]>) => resolve(data.metadata))
.catch(reject);
Expand Down
20 changes: 17 additions & 3 deletions src/api/auth-identities.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
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<LxdIdentity[]> => {
export const identitiesEntitlements = ["can_delete", "can_edit"];

export const fetchIdentities = (
isFineGrained: boolean | null,
): Promise<LxdIdentity[]> => {
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<LxdIdentity[]>) => resolve(data.metadata))
.catch(reject);
Expand All @@ -23,9 +32,14 @@ export const fetchCurrentIdentity = (): Promise<LxdIdentity> => {
export const fetchIdentity = (
id: string,
authMethod: string,
isFineGrained: boolean | null,
): Promise<LxdIdentity> => {
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<LxdIdentity>) => resolve(data.metadata))
.catch(reject);
Expand Down
21 changes: 12 additions & 9 deletions src/api/auth-idp-groups.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
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<IdpGroup[]> => {
const idpGroupEntitlements = ["can_delete", "can_edit"];

export const fetchIdpGroups = (
isFineGrained: boolean | null,
): Promise<IdpGroup[]> => {
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<IdpGroup[]>) => resolve(data.metadata))
.catch(reject);
Expand All @@ -15,14 +24,8 @@ export const createIdpGroup = (group: Partial<IdpGroup>): Promise<void> => {
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);
Expand Down
16 changes: 9 additions & 7 deletions src/components/SelectableMainTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +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;
disableSelectAll?: boolean;
}

type Props = SelectableMainTableProps & MainTableProps;
Expand All @@ -36,14 +37,15 @@ const SelectableMainTable: FC<Props> = ({
parentName,
selectedNames,
setSelectedNames,
processingNames,
disabledNames,
rows,
headers,
indeterminateNames = [],
disableSelect = false,
onToggleRow,
hideContextualMenu,
defaultSortKey,
disableSelectAll,
...props
}: Props) => {
const [currentSelectedIndex, setCurrentSelectedIndex] = useState<number>();
Expand Down Expand Up @@ -91,7 +93,7 @@ const SelectableMainTable: FC<Props> = ({
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onChange={isSomeSelected ? selectNone : selectPage}
disabled={disableSelect}
disabled={disableSelect || disableSelectAll}
/>
{!hideContextualMenu && (
<ContextualMenu
Expand Down Expand Up @@ -128,11 +130,11 @@ const SelectableMainTable: FC<Props> = ({
];

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<HTMLInputElement>) => {
Expand Down Expand Up @@ -183,7 +185,7 @@ const SelectableMainTable: FC<Props> = ({
labelClassName="u-no-margin--bottom"
checked={isRowSelected}
onChange={toggleRow}
disabled={isRowProcessing || !row.name || disableSelect}
disabled={isRowDisabled || !row.name || disableSelect}
indeterminate={isRowIndeterminate && !isRowSelected}
/>
),
Expand All @@ -195,7 +197,7 @@ const SelectableMainTable: FC<Props> = ({

const className = classnames(row.className, {
"selected-row": isRowSelected,
"processing-row": isRowProcessing,
"disabled-row": isRowDisabled,
});

const key = row.key ?? row.name;
Expand Down
15 changes: 15 additions & 0 deletions src/context/useGroups.tsx
Original file line number Diff line number Diff line change
@@ -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<LxdGroup[]> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.authGroups],
queryFn: () => fetchGroups(isFineGrained),
enabled: isFineGrained !== null,
});
};
28 changes: 28 additions & 0 deletions src/context/useIdentities.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 { LxdIdentity } from "types/permissions";
import { fetchIdentities, fetchIdentity } from "api/auth-identities";

export const useIdentities = (): UseQueryResult<LxdIdentity[]> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.identities],
queryFn: () => fetchIdentities(isFineGrained),
enabled: isFineGrained !== null,
});
};

export const useIdentity = (
id: string,
authMethod: string,
enabled?: boolean,
): UseQueryResult<LxdIdentity> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.identities, authMethod, id],
queryFn: () => fetchIdentity(id, authMethod, isFineGrained),
enabled: (enabled ?? true) && isFineGrained !== null,
});
};
15 changes: 15 additions & 0 deletions src/context/useIdpGroups.tsx
Original file line number Diff line number Diff line change
@@ -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<IdpGroup[]> => {
const { isFineGrained } = useAuth();
return useQuery({
queryKey: [queryKeys.idpGroups],
queryFn: () => fetchIdpGroups(isFineGrained),
enabled: isFineGrained !== null,
});
};
11 changes: 3 additions & 8 deletions src/context/useLoggedInUser.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
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();

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,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/cluster/ClusterGroupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const ClusterGroupForm: FC<Props> = ({ group }) => {
setSelectedNames={(newMembers: string[]) =>
void formik.setFieldValue("members", newMembers)
}
processingNames={[]}
disabledNames={[]}
/>
</Col>
</Row>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/images/ImageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/instances/InstanceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ const InstanceList: FC = () => {
parentName="project"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
processingNames={processingNames}
disabledNames={processingNames}
filteredNames={filteredInstances.map(
(instance) => instance.name,
)}
Expand All @@ -716,7 +716,7 @@ const InstanceList: FC = () => {
parentName="project"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
processingNames={processingNames}
disabledNames={processingNames}
filteredNames={filteredInstances.map(
(instance) => instance.name,
)}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/instances/InstanceSnapshots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const InstanceSnapshots = (props: Props) => {
parentName="instance"
selectedNames={selectedNames}
setSelectedNames={setSelectedNames}
processingNames={processingNames}
disabledNames={processingNames}
filteredNames={filteredSnapshots.map(
(snapshot) => snapshot.name,
)}
Expand Down
8 changes: 8 additions & 0 deletions src/pages/permissions/CreateTlsIdentityBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
Expand All @@ -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 && <Icon name="plus" light />}
<span>Create TLS Identity</span>
Expand Down
Loading