From 11d30615629f48f1e8cf79459677c1a985b83beb Mon Sep 17 00:00:00 2001 From: nehaaprasaad Date: Fri, 24 Apr 2026 14:46:46 +0530 Subject: [PATCH] fix(ui): keep policy rule operations in sync with server (#27591) --- .../it/tests/PolicyResourceIT.java | 56 +++++++++++++++++++ .../AddPolicyPage/AddPolicyPage.tsx | 15 ++++- .../PoliciesDetailPage/AddRulePage.tsx | 14 ++++- .../PoliciesDetailPage/EditRulePage.tsx | 25 ++++++++- .../pages/PoliciesPage/RuleForm/RuleForm.tsx | 8 ++- .../ui/src/utils/PolicyRuleUtils.test.ts | 45 +++++++++++++++ .../resources/ui/src/utils/PolicyRuleUtils.ts | 39 +++++++++++++ 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/PolicyRuleUtils.test.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/PolicyRuleUtils.ts diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PolicyResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PolicyResourceIT.java index 41a7306fdd76..7745e10016f2 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PolicyResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/PolicyResourceIT.java @@ -50,6 +50,7 @@ import org.openmetadata.sdk.client.OpenMetadataClient; import org.openmetadata.sdk.models.ListParams; import org.openmetadata.sdk.models.ListResponse; +import org.openmetadata.service.Entity; import org.openmetadata.service.resources.policies.PolicyResource; /** @@ -478,6 +479,61 @@ void test_createPolicyWithMultipleOperations(TestNamespace ns) { assertTrue(policy.getRules().get(0).getOperations().size() >= 3); } + /** + * Reproduces the server behaviour behind “ViewBasic disappears after save” for Classification + * policies (GitHub #27591) when a rule includes both {@code ViewAll} and {@code ViewBasic}: + * {@link org.openmetadata.service.jdbi3.PolicyRepository#filterRedundantOperations} drops {@code + * ViewBasic} as redundant. This test should pass before and after the UI fix; it locks the + * contract the UI was not reflecting immediately. + */ + @Test + void test_createPolicyClassificationViewBasicOmittedWhenRedundantWithViewAll( + TestNamespace ns) { + Rule rule = + new Rule() + .withName("classificationViewAllAndBasic") + .withResources(List.of(Entity.CLASSIFICATION)) + .withOperations( + List.of(MetadataOperation.VIEW_ALL, MetadataOperation.VIEW_BASIC)) + .withEffect(Effect.ALLOW); + CreatePolicy create = + new CreatePolicy() + .withName(ns.prefix("classificationRedundantViewOpsPolicy")) + .withRules(List.of(rule)) + .withDescription("Repro: VIEW_BASIC normalized away when VIEW_ALL is present"); + Policy policy = createEntity(create); + List operations = policy.getRules().get(0).getOperations(); + assertTrue(operations.contains(MetadataOperation.VIEW_ALL)); + assertFalse( + operations.contains(MetadataOperation.VIEW_BASIC), + "VIEW_BASIC is removed as redundant when VIEW_ALL is present (PolicyRepository policy)"); + } + + /** + * Regression: explicit {@code ViewBasic} alone on the classification resource must round-trip + * (GitHub #27591). Fails if the API incorrectly drops a standalone VIEW_BASIC rule. + */ + @Test + void test_createPolicyClassificationViewBasicPersists(TestNamespace ns) { + Rule rule = + new Rule() + .withName("classificationViewBasicOnly") + .withResources(List.of(Entity.CLASSIFICATION)) + .withOperations(List.of(MetadataOperation.VIEW_BASIC)) + .withEffect(Effect.ALLOW); + CreatePolicy create = + new CreatePolicy() + .withName(ns.prefix("classificationViewBasicPolicy")) + .withRules(List.of(rule)) + .withDescription("VIEW_BASIC only on classification resource"); + Policy policy = createEntity(create); + assertNotNull(policy.getRules()); + assertTrue( + policy.getRules().get(0).getOperations().contains(MetadataOperation.VIEW_BASIC), + "VIEW_BASIC must remain when it is the only view operation for classification"); + assertEquals(1, policy.getRules().get(0).getOperations().size()); + } + @Test void test_createPolicyWithOwner(TestNamespace ns) { OpenMetadataClient client = SdkClients.adminClient(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx index de344d089090..185784575d23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx @@ -36,6 +36,7 @@ import brandClassBase from '../../../utils/BrandData/BrandClassBase'; import { getField } from '../../../utils/formUtils'; import { translateWithNestedKeys } from '../../../utils/i18next/LocalUtil'; import { getPath, getPolicyWithFqnPath } from '../../../utils/RouterUtils'; +import { filterRedundantPolicyOperations } from '../../../utils/PolicyRuleUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import RuleForm from '../RuleForm/RuleForm'; @@ -56,6 +57,7 @@ const AddPolicyPage = () => { effect: Effect.Allow, }); const [isSaveLoading, setIsSaveLoading] = useState(false); + const [form] = Form.useForm(); const handleCancel = () => { navigate(policiesPath); @@ -63,10 +65,14 @@ const AddPolicyPage = () => { const handleSubmit = async () => { const { condition, ...rest } = { ...ruleData, name: trim(ruleData.name) }; + const rulePayload = { + ...rest, + operations: filterRedundantPolicyOperations(rest.operations), + }; const data: CreatePolicy = { name: trim(name), description, - rules: [condition ? { ...rest, condition } : rest], + rules: [condition ? { ...rulePayload, condition } : rulePayload], }; setIsSaveLoading(true); @@ -140,6 +146,7 @@ const AddPolicyPage = () => {
{ entity: t('label.rule'), })} - +