Added hooks to evaluate reader role access

This commit is contained in:
Bikram Choudhury
2025-10-15 18:39:32 +05:30
parent 9bfb6aecc9
commit a23a7791d4
10 changed files with 385 additions and 36 deletions

View File

@@ -38,7 +38,9 @@ const getInitialCopyJobState = (): CopyJobContextState => {
const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => {
const config = useConfig();
const { isLoggedIn, armToken } = useAADAuth(config);
const { isLoggedIn, armToken, account } = useAADAuth(config);
const principalId = account?.localAccountId ?? "";
const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState());
const [flow, setFlow] = React.useState<CopyJobFlowType | null>(null);
@@ -52,7 +54,7 @@ const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) =>
}
return (
<CopyJobContext.Provider value={{ armToken, copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}>
<CopyJobContext.Provider value={{ principalId, armToken, copyJobState, setCopyJobState, flow, setFlow, resetCopyJobState }}>
{props.children}
</CopyJobContext.Provider>
);

View File

@@ -1,5 +1,13 @@
import { useRoleAssignments } from "hooks/useRoleAssignments";
import { RoleDefinitionType, useRoleDefinitions } from "hooks/useRoleDefinition";
import { useMemo } from "react";
import ContainerCopyMessages from "../../../../ContainerCopyMessages";
import { CopyJobMigrationType } from "../../../../Enums";
import {
BackupPolicyType,
CopyJobMigrationType,
DefaultIdentityType,
IdentityType
} from "../../../../Enums";
import { CopyJobContextState } from "../../../../Types";
import AddManagedIdentity from "../AddManagedIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
@@ -7,28 +15,36 @@ import DefaultManagedIdentity from "../DefaultManagedIdentity";
import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore";
// Define a typed config for permission sections
export interface PermissionSectionConfig {
id: string;
title: string;
Component: React.FC;
shouldShow?: (state: CopyJobContextState) => boolean; // optional conditional rendering
disabled?: boolean;
completed?: boolean;
}
// Base permission sections with dynamic visibility logic
// Section IDs for maintainability
const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned",
pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled"
} as const;
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{
id: "addManagedIdentity",
id: SECTION_IDS.addManagedIdentity,
title: ContainerCopyMessages.addManagedIdentity.title,
Component: AddManagedIdentity,
},
{
id: "defaultManagedIdentity",
id: SECTION_IDS.defaultManagedIdentity,
title: ContainerCopyMessages.defaultManagedIdentity.title,
Component: DefaultManagedIdentity,
},
{
id: "readPermissionAssigned",
id: SECTION_IDS.readPermissionAssigned,
title: ContainerCopyMessages.readPermissionAssigned.title,
Component: AddReadPermissionToDefaultIdentity,
}
@@ -36,22 +52,139 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
{
id: "pointInTimeRestore",
id: SECTION_IDS.pointInTimeRestore,
title: ContainerCopyMessages.pointInTimeRestore.title,
Component: PointInTimeRestore,
},
{
id: "onlineCopyEnabled",
id: SECTION_IDS.onlineCopyEnabled,
title: ContainerCopyMessages.onlineCopyEnabled.title,
Component: OnlineCopyEnabled,
}
];
const usePermissionSections = (state: CopyJobContextState): PermissionSectionConfig[] => {
return [
...PERMISSION_SECTIONS_CONFIG,
...(state.migrationType !== CopyJobMigrationType.Offline ? PERMISSION_SECTIONS_FOR_ONLINE_JOBS : []),
];
/**
* Checks if the user has the Reader role based on role definitions.
*/
export function checkUserHasReaderRole(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some(
role =>
role.name === "00000000-0000-0000-0000-000000000001" ||
role.permissions.some(
permission =>
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") &&
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read")
)
);
}
/**
* Returns the permission sections configuration for the Assign Permissions screen.
* Memoizes derived values for performance and decouples logic for testability.
*/
const usePermissionSections = (
state: CopyJobContextState,
armToken: string,
principalId: string
): PermissionSectionConfig[] => {
const { source, target } = state;
// Memoize identity types and backup policy
const targetAccountIdentityType = useMemo(
() => (target?.account?.identity?.type ?? "").toLowerCase(),
[target?.account?.identity?.type]
);
const targetAccountDefaultIdentityType = useMemo(
() => (target?.account?.properties?.defaultIdentity ?? "").toLowerCase(),
[target?.account?.properties?.defaultIdentity]
);
const sourceAccountBackupPolicy = useMemo(
() => source?.account?.properties?.backupPolicy?.type ?? "",
[source?.account?.properties?.backupPolicy?.type]
);
// Fetch role assignments and definitions
const roleAssigned = useRoleAssignments(
armToken,
source?.subscription?.subscriptionId,
source?.account?.resourceGroup,
source?.account?.name,
principalId
);
const roleDefinitions = useRoleDefinitions(
armToken,
roleAssigned ?? []
);
const hasReaderRole = useMemo(
() => checkUserHasReaderRole(roleDefinitions ?? []),
[roleDefinitions]
);
// Decouple section state logic for testability
const getBaseSections = useMemo(() => {
return PERMISSION_SECTIONS_CONFIG.map(section => {
if (
section.id === SECTION_IDS.addManagedIdentity &&
(targetAccountIdentityType === IdentityType.SystemAssigned ||
targetAccountIdentityType === IdentityType.UserAssigned)
) {
return {
...section,
disabled: true,
completed: true
};
}
if (
section.id === SECTION_IDS.defaultManagedIdentity &&
targetAccountDefaultIdentityType === DefaultIdentityType.SystemAssignedIdentity
) {
return {
...section,
disabled: true,
completed: true
};
}
if (
section.id === SECTION_IDS.readPermissionAssigned &&
hasReaderRole
) {
return {
...section,
disabled: true,
completed: true
};
}
return section;
});
}, [targetAccountIdentityType, targetAccountDefaultIdentityType, hasReaderRole]);
const getOnlineSections = useMemo(() => {
if (state.migrationType !== CopyJobMigrationType.Online) return [];
return PERMISSION_SECTIONS_FOR_ONLINE_JOBS.map(section => {
if (
section.id === SECTION_IDS.pointInTimeRestore &&
sourceAccountBackupPolicy === BackupPolicyType.Continuous
) {
return {
...section,
disabled: true,
completed: true
};
}
return section;
});
}, [state.migrationType, sourceAccountBackupPolicy]);
// Combine and memoize final sections
const permissionSections = useMemo(
() => [...getBaseSections, ...getOnlineSections],
[getBaseSections, getOnlineSections]
);
return permissionSections;
};
export default usePermissionSections;

View File

@@ -1,26 +1,39 @@
import { Image, Stack, Text } from "@fluentui/react";
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components";
import React from "react";
import CheckmarkIcon from "../../../../../../images/successfulPopup.svg";
import WarningIcon from "../../../../../../images/warning.svg";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { useCopyJobContext } from "../../../Context/CopyJobContext";
import usePermissionSections, { PermissionSectionConfig } from "./hooks/usePermissionsSection";
const PermissionSection: React.FC<PermissionSectionConfig> = ({ id, title, Component }) => (
<AccordionItem key={id} value={id}>
const PermissionSection: React.FC<PermissionSectionConfig> = ({
id,
title,
Component,
completed,
disabled
}) => (
<AccordionItem key={id} value={id} disabled={disabled}>
<AccordionHeader className="accordionHeader">
<Text className="accordionHeaderText" variant="medium">{title}</Text>
<Image className="statusIcon" src={WarningIcon} alt="Warning icon" width={24} height={24} />
<Image
className="statusIcon"
src={completed ? CheckmarkIcon : WarningIcon}
alt={completed ? "Checkmark icon" : "Warning icon"}
width={completed ? 20 : 24}
height={completed ? 20 : 24}
/>
</AccordionHeader>
<AccordionPanel>
<AccordionPanel aria-disabled={disabled} className="accordionPanel" >
<Component />
</AccordionPanel>
</AccordionItem>
);
const AssignPermissions = () => {
const { copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState);
const { armToken, principalId, copyJobState } = useCopyJobContext();
const permissionSections = usePermissionSections(copyJobState, armToken, principalId);
return (
<Stack className="assignPermissionsContainer" tokens={{ childrenGap: 15 }}>
<span>

View File

@@ -3,6 +3,22 @@ export enum CopyJobMigrationType {
Online = "online",
}
// all checks will happen
export enum IdentityType {
SystemAssigned = "systemassigned", // "SystemAssigned"
UserAssigned = "userassigned", // "UserAssigned"
None = "none", // "None"
}
export enum DefaultIdentityType {
SystemAssignedIdentity = "systemassignedidentity", // "SystemAssignedIdentity"
}
export enum BackupPolicyType {
Continuous = "Continuous",
Periodic = "Periodic",
}
export enum CopyJobMigrationStatus {
Pause = "Pause",
Resume = "Resume",

View File

@@ -27,18 +27,6 @@ export type DropdownOptionType = {
data: any
};
export type FetchDatabasesListParams = {
armToken: string;
subscriptionId: string;
resourceGroupName: string;
accountName: string;
apiType?: ApiType;
};
export interface FetchDataContainersListParams extends FetchDatabasesListParams {
databaseName: string;
}
export type DatabaseParams = [
string,
string | undefined,
@@ -91,6 +79,7 @@ export interface CopyJobFlowType {
}
export interface CopyJobContextProviderType {
principalId: string;
armToken: string;
flow: CopyJobFlowType;
setFlow: React.Dispatch<React.SetStateAction<CopyJobFlowType>>;