upgrade RBAC permissions from read only to read-write

This commit is contained in:
Bikram Choudhury
2026-03-26 12:59:15 +05:30
committed by BChoudhury-ms
parent 8698c6a3e2
commit 3f1819f22a
22 changed files with 157 additions and 132 deletions

View File

@@ -66,7 +66,7 @@ export default {
// Assign Permissions Screen
assignPermissions: {
crossAccountDescription:
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.",
intraAccountOnlineDescription: (accountName: string) =>
`Follow the steps below to enable online copy on your "${accountName}" account.`,
crossAccountConfiguration: {
@@ -119,18 +119,18 @@ export default {
popoverDescription: (accountName: string) =>
`Assign the system-assigned managed identity as the default for "${accountName}". To confirm, click the "Yes" button. `,
},
readPermissionAssigned: {
title: "Read permissions assigned to the default identity.",
readWritePermissionAssigned: {
title: "Read-write permissions assigned to the default identity.",
description:
"To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.",
"To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.",
tooltip: {
content: "Learn more about",
hrefText: "Read permissions.",
hrefText: "Read-write permissions.",
href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
},
popoverTitle: "Read permissions assigned to default identity.",
popoverTitle: "Assign read-write permissions to default identity.",
popoverDescription:
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the Yes button.",
'Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.',
},
pointInTimeRestore: {
title: "Point In Time Restore enabled",

View File

@@ -75,7 +75,7 @@ describe("CopyJobContext", () => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
});
expect(contextValue.flow).toBeNull();
expect(contextValue.contextError).toBeNull();
@@ -620,7 +620,7 @@ describe("CopyJobContext", () => {
expect(contextValue.copyJobState.target.account).toBeNull();
});
it("should initialize sourceReadAccessFromTarget as false", () => {
it("should initialize sourceReadWriteAccessFromTarget as false", () => {
let contextValue: any;
render(
@@ -634,7 +634,7 @@ describe("CopyJobContext", () => {
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false);
expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false);
});
it("should initialize with empty database and container ids", () => {

View File

@@ -34,7 +34,7 @@ const getInitialCopyJobState = (): CopyJobContextState => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
};
};

View File

@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
};
const mockContextValue = {

View File

@@ -4,7 +4,7 @@ import React from "react";
import ContainerCopyMessages from "../../../ContainerCopyMessages";
import { CopyJobContext } from "../../../Context/CopyJobContext";
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity";
import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity";
jest.mock("../../../../../Common/Logger", () => ({
logError: jest.fn(),
@@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
import useToggle from "./hooks/useToggle";
describe("AddReadPermissionToDefaultIdentity Component", () => {
describe("AddReadWritePermissionToDefaultIdentity Component", () => {
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
@@ -119,7 +119,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
},
setCopyJobState: jest.fn(),
setContextError: jest.fn(),
@@ -133,7 +133,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
const renderComponent = (contextValue = mockContextValue) => {
return render(
<CopyJobContext.Provider value={contextValue}>
<AddReadPermissionToDefaultIdentity />
<AddReadWritePermissionToDefaultIdentity />
</CopyJobContext.Provider>,
);
};
@@ -164,12 +164,12 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(container).toMatchSnapshot();
});
it("should render correctly when sourceReadAccessFromTarget is true", () => {
it("should render correctly when sourceReadWriteAccessFromTarget is true", () => {
const contextWithAccess = {
...mockContextValue,
copyJobState: {
...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true,
sourceReadWriteAccessFromTarget: true,
},
};
const { container } = renderComponent(contextWithAccess);
@@ -180,7 +180,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
describe("Component Structure", () => {
it("should display the description text", () => {
renderComponent();
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument();
expect(screen.getByText(ContainerCopyMessages.readWritePermissionAssigned.description)).toBeInTheDocument();
});
it("should display the info tooltip", () => {
@@ -212,10 +212,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(screen.getByTestId("popover-message")).toBeInTheDocument();
expect(screen.getByTestId("popover-title")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverTitle,
ContainerCopyMessages.readWritePermissionAssigned.popoverTitle,
);
expect(screen.getByTestId("popover-content")).toHaveTextContent(
ContainerCopyMessages.readPermissionAssigned.popoverDescription,
ContainerCopyMessages.readWritePermissionAssigned.popoverDescription,
);
});
@@ -243,7 +243,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(mockOnToggle).toHaveBeenCalledWith(null, false);
});
it("should call handleAddReadPermission when primary button is clicked", async () => {
it("should call handleAddReadWritePermission when primary button is clicked", async () => {
mockGetAccountDetailsFromResourceId.mockReturnValue({
subscriptionId: "source-sub-id",
resourceGroup: "source-rg",
@@ -264,7 +264,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
});
});
describe("handleAddReadPermission Function", () => {
describe("handleAddReadWritePermission Function", () => {
beforeEach(() => {
mockUseToggle.mockReturnValue([true, jest.fn()]);
});
@@ -312,7 +312,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(
"Permission denied",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
);
});
@@ -336,14 +336,14 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
await waitFor(() => {
expect(mockLogError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.",
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
"Error assigning read-write permission to default identity. Please try again later.",
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
);
});
await waitFor(() => {
expect(mockContextValue.setContextError).toHaveBeenCalledWith(
"Error assigning read permission to default identity. Please try again later.",
"Error assigning read-write permission to default identity. Please try again later.",
);
});
});
@@ -496,7 +496,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => {
expect(updatedState).toEqual({
...mockContextValue.copyJobState,
sourceReadAccessFromTarget: true,
sourceReadWriteAccessFromTarget: true,
});
});
});

View File

@@ -12,27 +12,29 @@ import useToggle from "./hooks/useToggle";
const TooltipContent = (
<Text>
{ContainerCopyMessages.readPermissionAssigned.tooltip.content} &nbsp;
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.content} &nbsp;
<Link
style={{ color: "var(--colorBrandForeground1)" }}
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
href={ContainerCopyMessages.readWritePermissionAssigned.tooltip.href}
target="_blank"
rel="noopener noreferrer"
>
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.hrefText}
</Link>
</Text>
);
type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => {
type AddReadWritePermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionToDefaultIdentityProps> = () => {
const [loading, setLoading] = React.useState(false);
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
const [readPermissionAssigned, onToggle] = useToggle(false);
const [readWritePermissionAssigned, onToggle] = useToggle(copyJobState.sourceReadWriteAccessFromTarget ?? false);
const handleAddReadPermission = async () => {
const handleAddReadWritePermission = async () => {
const { source, target } = copyJobState;
const selectedSourceAccount = source?.account;
try {
const {
subscriptionId: sourceSubscriptionId,
@@ -47,16 +49,17 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
sourceAccountName,
target?.account?.identity?.principalId ?? "",
);
if (assignedRole) {
setCopyJobState((prevState) => ({
...prevState,
sourceReadAccessFromTarget: true,
sourceReadWriteAccessFromTarget: true,
}));
}
} catch (error) {
const errorMessage =
error.message || "Error assigning read permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission");
error.message || "Error assigning read-write permission to default identity. Please try again later.";
logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission");
setContextError(errorMessage);
} finally {
setLoading(false);
@@ -66,12 +69,12 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
return (
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
<Text className="toggle-label">
{ContainerCopyMessages.readPermissionAssigned.description}&ensp;
{ContainerCopyMessages.readWritePermissionAssigned.description}&ensp;
<InfoTooltip content={TooltipContent} />
</Text>
<Toggle
data-test="btn-toggle"
checked={readPermissionAssigned}
checked={readWritePermissionAssigned}
onText={ContainerCopyMessages.toggleBtn.onText}
offText={ContainerCopyMessages.toggleBtn.offText}
onChange={onToggle}
@@ -83,15 +86,15 @@ const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIde
/>
<PopoverMessage
isLoading={loading}
visible={readPermissionAssigned}
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
visible={readWritePermissionAssigned}
title={ContainerCopyMessages.readWritePermissionAssigned.popoverTitle}
onCancel={() => onToggle(null, false)}
onPrimary={handleAddReadPermission}
onPrimary={handleAddReadWritePermission}
>
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
{ContainerCopyMessages.readWritePermissionAssigned.popoverDescription}
</PopoverMessage>
</Stack>
);
};
export default AddReadPermissionToDefaultIdentity;
export default AddReadWritePermissionToDefaultIdentity;

View File

@@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => {
return MockAddManagedIdentity;
});
jest.mock("./AddReadPermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">Add Read Permission Component</div>;
jest.mock("./AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-write-permission">Add Read-Write Permission Component</div>;
};
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity;
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadWritePermissionToDefaultIdentity;
});
jest.mock("./DefaultManagedIdentity", () => {
@@ -96,7 +96,7 @@ describe("AssignPermissions Component", () => {
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
...overrides,
});
@@ -201,7 +201,7 @@ describe("AssignPermissions Component", () => {
completed: true,
},
{
id: "readPermissionAssigned",
id: "readWritePermissionAssigned",
title: "Read Permission Assigned",
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
disabled: false,

View File

@@ -61,7 +61,7 @@ describe("PointInTimeRestore", () => {
databaseId: "target-db",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
} as CopyJobContextState;
const mockSetCopyJobState = jest.fn();

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -8,7 +8,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
@@ -24,7 +24,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
Read-write permissions.
</a>
</span>
</div>
@@ -63,7 +63,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -71,7 +71,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
@@ -87,7 +87,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
Read-write permissions.
</a>
</span>
</div>
@@ -126,7 +126,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = `
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -134,7 +134,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
@@ -150,7 +150,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
Read-write permissions.
</a>
</span>
</div>
@@ -189,7 +189,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -197,7 +197,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
@@ -213,7 +213,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
Read-write permissions.
</a>
</span>
</div>
@@ -255,12 +255,12 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<div
data-testid="popover-title"
>
Read permissions assigned to default identity.
Assign read-write permissions to default identity.
</div>
<div
data-testid="popover-content"
>
Assign read permissions of the source account to the default identity of the destination account. To confirm click the Yes button.
Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.
</div>
<button
data-testid="popover-cancel"
@@ -277,7 +277,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -285,7 +285,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
@@ -301,7 +301,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
Read-write permissions.
</a>
</span>
</div>
@@ -340,7 +340,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
</div>
`;
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
<div>
<div
class="ms-Stack defaultManagedIdentityContainer css-109"
@@ -348,7 +348,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
<span
class="toggle-label css-110"
>
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
<div
data-testid="info-tooltip"
@@ -364,7 +364,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
rel="noopener noreferrer"
target="_blank"
>
Read permissions.
Read-write permissions.
</a>
</span>
</div>

View File

@@ -9,7 +9,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -212,7 +212,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -618,7 +618,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -1153,7 +1153,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
</span>
<div
class="ms-Stack css-111"
@@ -1307,7 +1307,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
</span>
<div
data-testid="shimmer-tree"
@@ -1329,7 +1329,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
<span
class="css-110"
>
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
</span>
<div
data-testid="shimmer-tree"

View File

@@ -13,7 +13,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
import usePermissionSections, {
checkTargetHasReaderRoleOnSource,
checkTargetHasReadWriteRoleOnSource,
PermissionGroupConfig,
SECTION_IDS,
} from "./usePermissionsSection";
@@ -40,12 +40,12 @@ jest.mock("../AddManagedIdentity", () => {
return MockAddManagedIdentity;
});
jest.mock("../AddReadPermissionToDefaultIdentity", () => {
const MockAddReadPermissionToDefaultIdentity = () => {
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>;
jest.mock("../AddReadWritePermissionToDefaultIdentity", () => {
const MockAddReadWritePermissionToDefaultIdentity = () => {
return <div data-testid="add-read-write-permission">AddReadWritePermissionToDefaultIdentity</div>;
};
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
return MockAddReadPermissionToDefaultIdentity;
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
return MockAddReadWritePermissionToDefaultIdentity;
});
jest.mock("../DefaultManagedIdentity", () => {
@@ -193,7 +193,7 @@ describe("usePermissionsSection", () => {
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
SECTION_IDS.addManagedIdentity,
SECTION_IDS.defaultManagedIdentity,
SECTION_IDS.readPermissionAssigned,
SECTION_IDS.readWritePermissionAssigned,
]);
});
@@ -358,16 +358,17 @@ describe("usePermissionsSection", () => {
expect(defaultManagedIdentitySection?.completed).toBe(true);
});
it("should validate readPermissionAssigned section with reader role", async () => {
it("should validate readWritePermissionAssigned section with contributor role", async () => {
const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Custom Role",
name: "00000000-0000-0000-0000-000000000002",
permissions: [
{
dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
],
},
],
@@ -407,7 +408,9 @@ describe("usePermissionsSection", () => {
render(<TestWrapper state={state} onResult={noop} />);
await waitFor(() => {
expect(screen.getByTestId(`section-${SECTION_IDS.readPermissionAssigned}-completed`)).toHaveTextContent("true");
expect(screen.getByTestId(`section-${SECTION_IDS.readWritePermissionAssigned}-completed`)).toHaveTextContent(
"true",
);
});
expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith(
@@ -568,12 +571,12 @@ describe("usePermissionsSection", () => {
});
});
describe("checkTargetHasReaderRoleOnSource", () => {
it("should return true for built-in Reader role", () => {
describe("checkTargetHasReadWriteRoleOnSource", () => {
it("should return true for built-in Contributor role", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "00000000-0000-0000-0000-000000000001",
name: "00000000-0000-0000-0000-000000000002",
permissions: [],
assignableScopes: [],
resourceGroup: "",
@@ -583,20 +586,21 @@ describe("checkTargetHasReaderRoleOnSource", () => {
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
it("should return true for custom role with required data actions", () => {
it("should return true for custom role with read-write data actions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
name: "Custom Reader Role",
name: "Custom Contributor Role",
permissions: [
{
dataActions: [
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
],
},
],
@@ -608,7 +612,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
@@ -630,12 +634,12 @@ describe("checkTargetHasReaderRoleOnSource", () => {
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(false);
});
it("should return false for empty role definitions", () => {
const result = checkTargetHasReaderRoleOnSource([]);
const result = checkTargetHasReadWriteRoleOnSource([]);
expect(result).toBe(false);
});
@@ -653,11 +657,11 @@ describe("checkTargetHasReaderRoleOnSource", () => {
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(false);
});
it("should handle multiple roles and return true if any has sufficient permissions", () => {
it("should handle multiple roles and return true if any has sufficient read-write permissions", () => {
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
{
id: "role-1",
@@ -675,7 +679,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
},
{
id: "role-2",
name: "00000000-0000-0000-0000-000000000001",
name: "00000000-0000-0000-0000-000000000002",
permissions: [],
assignableScopes: [],
resourceGroup: "",
@@ -685,7 +689,7 @@ describe("checkTargetHasReaderRoleOnSource", () => {
},
];
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
expect(result).toBe(true);
});
});

View File

@@ -12,7 +12,7 @@ import {
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
import AddManagedIdentity from "../AddManagedIdentity";
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
import AddReadWritePermissionToDefaultIdentity from "../AddReadWritePermissionToDefaultIdentity";
import DefaultManagedIdentity from "../DefaultManagedIdentity";
import OnlineCopyEnabled from "../OnlineCopyEnabled";
import PointInTimeRestore from "../PointInTimeRestore";
@@ -36,11 +36,13 @@ export interface PermissionGroupConfig {
export const SECTION_IDS = {
addManagedIdentity: "addManagedIdentity",
defaultManagedIdentity: "defaultManagedIdentity",
readPermissionAssigned: "readPermissionAssigned",
readWritePermissionAssigned: "readWritePermissionAssigned",
pointInTimeRestore: "pointInTimeRestore",
onlineCopyEnabled: "onlineCopyEnabled",
} as const;
const COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID = "00000000-0000-0000-0000-000000000002";
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
{
id: SECTION_IDS.addManagedIdentity,
@@ -66,9 +68,9 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
},
},
{
id: SECTION_IDS.readPermissionAssigned,
title: ContainerCopyMessages.readPermissionAssigned.title,
Component: AddReadPermissionToDefaultIdentity,
id: SECTION_IDS.readWritePermissionAssigned,
title: ContainerCopyMessages.readWritePermissionAssigned.title,
Component: AddReadWritePermissionToDefaultIdentity,
disabled: true,
validate: async (state: CopyJobContextState) => {
const principalId = state?.target?.account?.identity?.principalId;
@@ -87,7 +89,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
);
const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []);
return checkTargetHasReadWriteRoleOnSource(roleDefinitions ?? []);
},
},
];
@@ -119,18 +121,34 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
];
/**
* Checks if the user has the Reader role based on role definitions.
* Checks if the user has contributor-style read-write access on the source account.
*/
export function checkTargetHasReaderRoleOnSource(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"),
),
);
export function checkTargetHasReadWriteRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
return roleDefinitions?.some((role) => {
if (role.name === COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID) {
return true;
}
const dataActions = role.permissions?.flatMap((permission) => permission.dataActions ?? []) ?? [];
const hasAccountWildcard = dataActions.includes("Microsoft.DocumentDB/databaseAccounts/*");
const hasContainerWildcard =
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*");
const hasItemsWildcard =
hasContainerWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*");
const hasAccountReadMetadata =
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata");
const hasItemRead =
hasItemsWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read");
const hasItemWrite =
hasItemsWildcard ||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write");
return hasAccountReadMetadata && hasItemRead && hasItemWrite;
});
}
/**

View File

@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: null,

View File

@@ -98,7 +98,7 @@ describe("PreviewCopyJob", () => {
databaseId: "target-database",
containerId: "target-container",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
...overrides,
};
@@ -290,7 +290,7 @@ describe("PreviewCopyJob", () => {
databaseId: "target-database",
containerId: "target-container",
},
sourceReadAccessFromTarget: true,
sourceReadWriteAccessFromTarget: true,
});
const { container } = render(

View File

@@ -52,7 +52,7 @@ describe("AccountDropdown", () => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
} as CopyJobContextState;
const mockCopyJobContextValue = {

View File

@@ -29,7 +29,7 @@ describe("MigrationType", () => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" },

View File

@@ -41,7 +41,7 @@ describe("SelectAccount", () => {
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
},
setCopyJobState: mockSetCopyJobState,
flow: { currentScreen: "selectAccount" },

View File

@@ -7,7 +7,7 @@ import { dropDownChangeHandler } from "./DropDownChangeHandler";
const createMockInitialState = (): CopyJobContextState => ({
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
source: {
subscriptionId: "source-sub-id",
account: {
@@ -181,7 +181,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
});
});
@@ -227,7 +227,7 @@ describe("dropDownChangeHandler", () => {
expect(capturedState.jobName).toBe(initialState.jobName);
expect(capturedState.migrationType).toBe(initialState.migrationType);
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
});
});

View File

@@ -90,7 +90,7 @@ describe("SelectSourceAndTargetContainers", () => {
databaseId: "db2",
containerId: "container2",
},
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
};
const mockMemoizedData = {

View File

@@ -69,7 +69,7 @@ describe("useSourceAndTargetData", () => {
const mockCopyJobState: CopyJobContextState = {
jobName: "test-job",
migrationType: CopyJobMigrationType.Offline,
sourceReadAccessFromTarget: false,
sourceReadWriteAccessFromTarget: false,
source: {
subscriptionId: "source-subscription-id",
account: mockSourceAccount,

View File

@@ -55,7 +55,7 @@ export interface DatabaseContainerSectionProps {
export interface CopyJobContextState {
jobName: string;
migrationType: CopyJobMigrationType;
sourceReadAccessFromTarget?: boolean;
sourceReadWriteAccessFromTarget?: boolean;
source: {
subscriptionId: string;
account: DatabaseAccount | null;

View File

@@ -93,7 +93,7 @@ export const assignRole = async (
return null;
}
const accountScope = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}`;
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000001`;
const roleDefinitionId = `${accountScope}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002`; // Built-in Contributor role definition ID for Cosmos DB
const roleAssignmentName = crypto.randomUUID();
const path = `${accountScope}/sqlRoleAssignments/${roleAssignmentName}`;