From bc3491eb09160a95cde808970ba45b16bf3e0533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Manero?= Date: Thu, 14 May 2026 12:02:18 +0200 Subject: [PATCH 1/6] fix(ingestion-pipeline): inherit owners from service and authorize trigger (#27962) --- .../IngestionPipelineOwnerInheritanceIT.java | 244 ++++++++++++++++++ .../jdbi3/IngestionPipelineRepository.java | 20 ++ .../IngestionPipelineResource.java | 3 +- 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java new file mode 100644 index 000000000000..e5e68c470638 --- /dev/null +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java @@ -0,0 +1,244 @@ +package org.openmetadata.it.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.openmetadata.it.factories.DashboardServiceTestFactory; +import org.openmetadata.it.util.SdkClients; +import org.openmetadata.it.util.TestNamespace; +import org.openmetadata.it.util.TestNamespaceExtension; +import org.openmetadata.schema.api.policies.CreatePolicy; +import org.openmetadata.schema.api.services.ingestionPipelines.CreateIngestionPipeline; +import org.openmetadata.schema.api.teams.CreateRole; +import org.openmetadata.schema.api.teams.CreateUser; +import org.openmetadata.schema.entity.policies.Policy; +import org.openmetadata.schema.entity.policies.accessControl.Rule; +import org.openmetadata.schema.entity.services.DashboardService; +import org.openmetadata.schema.entity.services.ingestionPipelines.AirflowConfig; +import org.openmetadata.schema.entity.services.ingestionPipelines.IngestionPipeline; +import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineType; +import org.openmetadata.schema.entity.teams.Role; +import org.openmetadata.schema.entity.teams.User; +import org.openmetadata.schema.metadataIngestion.DashboardServiceMetadataPipeline; +import org.openmetadata.schema.metadataIngestion.SourceConfig; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.MetadataOperation; +import org.openmetadata.sdk.client.OpenMetadataClient; +import org.openmetadata.sdk.network.HttpMethod; + +/** + * Integration tests for IngestionPipeline owner inheritance and trigger authorization. + * + *

Covers two coordinated changes that fix GH-27962 (Pylon-19838): + * + *

+ */ +@Execution(ExecutionMode.CONCURRENT) +@ExtendWith(TestNamespaceExtension.class) +public class IngestionPipelineOwnerInheritanceIT { + + private static final Date START_DATE = new DateTime("2022-06-10T15:06:47+00:00").toDate(); + + @Test + void test_inheritedOwners_fromService(TestNamespace ns) { + OpenMetadataClient adminClient = SdkClients.adminClient(); + String unique = UUID.randomUUID().toString().substring(0, 8); + String userName = "ipinhowner_" + unique; + User serviceOwner = + adminClient + .users() + .create( + new CreateUser().withName(userName).withEmail(userName + "@test.openmetadata.org")); + + try { + DashboardService service = DashboardServiceTestFactory.createMetabase(ns); + DashboardService fetchedService = + adminClient.dashboardServices().get(service.getId().toString()); + fetchedService.setOwners(List.of(serviceOwner.getEntityReference())); + adminClient.dashboardServices().update(service.getId().toString(), fetchedService); + + try { + IngestionPipeline pipeline = + adminClient + .ingestionPipelines() + .create( + new CreateIngestionPipeline() + .withName(ns.prefix("ipinhPipeline")) + .withPipelineType(PipelineType.METADATA) + .withService(service.getEntityReference()) + .withSourceConfig( + new SourceConfig().withConfig(new DashboardServiceMetadataPipeline())) + .withAirflowConfig(new AirflowConfig().withStartDate(START_DATE))); + + try { + IngestionPipeline withOwners = + adminClient.ingestionPipelines().get(pipeline.getId().toString(), "owners"); + assertNotNull(withOwners.getOwners(), "Inherited owners should be populated"); + assertEquals(1, withOwners.getOwners().size(), "Pipeline should inherit one owner"); + EntityReference inherited = withOwners.getOwners().get(0); + assertEquals( + serviceOwner.getId(), + inherited.getId(), + "Inherited owner should match service owner"); + assertTrue( + Boolean.TRUE.equals(inherited.getInherited()), + "Owner inherited from the parent service must be marked inherited=true"); + } finally { + adminClient.ingestionPipelines().delete(pipeline.getId().toString()); + } + } finally { + adminClient + .dashboardServices() + .delete(service.getId().toString(), Map.of("hardDelete", "true", "recursive", "true")); + } + } finally { + adminClient.users().delete(serviceOwner.getId()); + } + } + + @Test + void test_isOwnerPolicy_appliesToEditAndTrigger(TestNamespace ns) { + OpenMetadataClient adminClient = SdkClients.adminClient(); + String unique = UUID.randomUUID().toString().substring(0, 8); + + Rule ownerRule = + new Rule() + .withName("pipelineOwnerEditAndTrigger") + .withDescription("Allow owners to edit and trigger ingestion pipelines") + .withEffect(Rule.Effect.ALLOW) + .withOperations(List.of(MetadataOperation.EDIT_ALL, MetadataOperation.TRIGGER)) + .withResources(List.of("ingestionPipeline")) + .withCondition("isOwner()"); + Policy ownerPolicy = + adminClient + .policies() + .create( + new CreatePolicy() + .withName("ipauthPolicy_" + unique) + .withDescription("Owner-only policy for ingestion pipelines") + .withRules(List.of(ownerRule))); + + try { + Role ownerRole = + adminClient + .roles() + .create( + new CreateRole() + .withName("ipauthRole_" + unique) + .withPolicies(List.of(ownerPolicy.getFullyQualifiedName()))); + + try { + String ownerName = "ipauthowner_" + unique; + User pipelineOwner = + adminClient + .users() + .create( + new CreateUser() + .withName(ownerName) + .withEmail(ownerName + "@test.openmetadata.org") + .withRoles(List.of(ownerRole.getId()))); + + String otherName = "ipauthother_" + unique; + User otherUser = + adminClient + .users() + .create( + new CreateUser() + .withName(otherName) + .withEmail(otherName + "@test.openmetadata.org")); + + try { + DashboardService service = DashboardServiceTestFactory.createMetabase(ns); + DashboardService fetchedService = + adminClient.dashboardServices().get(service.getId().toString()); + fetchedService.setOwners(List.of(pipelineOwner.getEntityReference())); + adminClient.dashboardServices().update(service.getId().toString(), fetchedService); + + try { + IngestionPipeline pipeline = + adminClient + .ingestionPipelines() + .create( + new CreateIngestionPipeline() + .withName(ns.prefix("ipauthPipeline_" + unique)) + .withPipelineType(PipelineType.METADATA) + .withService(service.getEntityReference()) + .withSourceConfig( + new SourceConfig() + .withConfig(new DashboardServiceMetadataPipeline())) + .withAirflowConfig(new AirflowConfig().withStartDate(START_DATE))); + + try { + OpenMetadataClient ownerClient = + SdkClients.createClient(ownerName, ownerName, new String[] {}); + OpenMetadataClient otherClient = + SdkClients.createClient(otherName, otherName, new String[] {}); + + // Owner can PATCH displayName. + IngestionPipeline ownerEdit = + adminClient.ingestionPipelines().get(pipeline.getId().toString()); + ownerEdit.setDisplayName("owner-updated-display-name"); + ownerClient.ingestionPipelines().update(pipeline.getId().toString(), ownerEdit); + + // Non-owner cannot PATCH displayName. + IngestionPipeline otherEdit = + adminClient.ingestionPipelines().get(pipeline.getId().toString()); + otherEdit.setDisplayName("non-owner-attempt"); + assertThrows( + Exception.class, + () -> + otherClient + .ingestionPipelines() + .update(pipeline.getId().toString(), otherEdit), + "Non-owner PATCH should be forbidden"); + + // Owner can trigger. + String triggerPath = "/v1/services/ingestionPipelines/trigger/" + pipeline.getId(); + ownerClient.getHttpClient().execute(HttpMethod.POST, triggerPath, null, Void.class); + + // Non-owner cannot trigger. + assertThrows( + Exception.class, + () -> + otherClient + .getHttpClient() + .execute(HttpMethod.POST, triggerPath, null, Void.class), + "Non-owner trigger should be forbidden"); + } finally { + adminClient.ingestionPipelines().delete(pipeline.getId().toString()); + } + } finally { + adminClient + .dashboardServices() + .delete( + service.getId().toString(), Map.of("hardDelete", "true", "recursive", "true")); + } + } finally { + adminClient.users().delete(otherUser.getId()); + adminClient.users().delete(pipelineOwner.getId()); + } + } finally { + adminClient.roles().delete(ownerRole.getId()); + } + } finally { + adminClient.policies().delete(ownerPolicy.getId()); + } + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java index 04c121b7c27f..5a022806d14a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/IngestionPipelineRepository.java @@ -15,6 +15,7 @@ import static org.openmetadata.schema.type.EventType.ENTITY_FIELDS_CHANGED; import static org.openmetadata.schema.type.EventType.ENTITY_UPDATED; +import static org.openmetadata.schema.type.Include.ALL; import static org.openmetadata.service.Entity.INGESTION_PIPELINE; import jakarta.ws.rs.core.Response; @@ -67,6 +68,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.events.lifecycle.EntityLifecycleEventDispatcher; +import org.openmetadata.service.exception.EntityNotFoundException; import org.openmetadata.service.logstorage.LogStorageInterface; import org.openmetadata.service.logstorage.S3LogStorage.LogStreamListener; import org.openmetadata.service.monitoring.IngestionProgressTracker; @@ -150,6 +152,24 @@ public void setFields( } } + @Override + public void setInheritedFields(IngestionPipeline ingestionPipeline, Fields fields) { + EntityReference serviceRef = ingestionPipeline.getService(); + if (serviceRef == null) { + return; + } + try { + EntityInterface parent = Entity.getEntity(serviceRef, "owners,domains", ALL); + inheritOwners(ingestionPipeline, fields, parent); + inheritDomains(ingestionPipeline, fields, parent); + } catch (EntityNotFoundException e) { + LOG.debug( + "Parent service {} not found for ingestion pipeline {}; skipping owner/domain inheritance", + serviceRef.getFullyQualifiedName(), + ingestionPipeline.getFullyQualifiedName()); + } + } + @Override public void setFieldsInBulk(Fields fields, List entities) { if (entities == null || entities.isEmpty()) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java index 9216cdd4eb2b..510ff88282af 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java @@ -1337,6 +1337,8 @@ private PipelineServiceClientResponse deployPipelineInternal( public PipelineServiceClientResponse triggerPipelineInternal( UUID id, UriInfo uriInfo, SecurityContext securityContext, String botName) { + OperationContext operationContext = new OperationContext(entityType, MetadataOperation.TRIGGER); + authorizer.authorize(securityContext, operationContext, getResourceContextById(id)); if (pipelineServiceClient == null) { return new PipelineServiceClientResponse() .withCode(200) @@ -1346,7 +1348,6 @@ public PipelineServiceClientResponse triggerPipelineInternal( IngestionPipeline ingestionPipeline = repository.get(uriInfo, id, fields); CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, ingestionPipeline); - OperationContext operationContext = new OperationContext(entityType, MetadataOperation.TRIGGER); limits.enforceLimits(securityContext, createResourceContext, operationContext); if (CommonUtil.nullOrEmpty(botName)) { // Use Default Ingestion Bot From 9b432d4b48fc801db6f5e5e2db5b41c83bbb1345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Manero?= Date: Thu, 14 May 2026 12:02:35 +0200 Subject: [PATCH 2/6] fix(policy): grant Trigger to default bot + steward policies for /trigger authz --- .../migration/mysql/v1129/Migration.java | 20 +++++++++ .../migration/mysql/v1130/Migration.java | 3 ++ .../migration/postgres/v1129/Migration.java | 20 +++++++++ .../migration/postgres/v1130/Migration.java | 3 ++ .../migration/utils/v1129/MigrationUtil.java | 43 +++++++++++++++++++ .../json/data/policy/DataStewardPolicy.json | 2 +- .../json/data/policy/IngestionBotPolicy.json | 2 +- .../json/data/policy/LineageBotPolicy.json | 2 +- .../json/data/policy/ProfilerBotPolicy.json | 2 +- .../json/data/policy/QualityBotPolicy.json | 2 +- .../json/data/policy/UsageBotPolicy.json | 2 +- 11 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java new file mode 100644 index 000000000000..1f719321cef6 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java @@ -0,0 +1,20 @@ +package org.openmetadata.service.migration.mysql.v1129; + +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; + +import lombok.SneakyThrows; +import org.openmetadata.service.migration.api.MigrationProcessImpl; +import org.openmetadata.service.migration.utils.MigrationFile; + +public class Migration extends MigrationProcessImpl { + + public Migration(MigrationFile migrationFile) { + super(migrationFile); + } + + @Override + @SneakyThrows + public void runDataMigration() { + addTriggerOperationToDefaultPolicies(collectionDAO); + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java index 9584febe0a43..d4ffc8e077af 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java @@ -1,5 +1,7 @@ package org.openmetadata.service.migration.mysql.v1130; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; + import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.openmetadata.service.migration.api.MigrationProcessImpl; @@ -31,5 +33,6 @@ public void runDataMigration() { LOG.error("v1130 glossaryTerm version relatedTerms transform failed; re-run to retry.", e); } MigrationUtil.addTableColumnSearchSettings(); + addTriggerOperationToDefaultPolicies(collectionDAO); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java new file mode 100644 index 000000000000..122a013064fb --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java @@ -0,0 +1,20 @@ +package org.openmetadata.service.migration.postgres.v1129; + +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; + +import lombok.SneakyThrows; +import org.openmetadata.service.migration.api.MigrationProcessImpl; +import org.openmetadata.service.migration.utils.MigrationFile; + +public class Migration extends MigrationProcessImpl { + + public Migration(MigrationFile migrationFile) { + super(migrationFile); + } + + @Override + @SneakyThrows + public void runDataMigration() { + addTriggerOperationToDefaultPolicies(collectionDAO); + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java index 64f070b32790..ee4968f40bbc 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java @@ -1,5 +1,7 @@ package org.openmetadata.service.migration.postgres.v1130; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; + import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.openmetadata.service.migration.api.MigrationProcessImpl; @@ -31,5 +33,6 @@ public void runDataMigration() { LOG.error("v1130 glossaryTerm version relatedTerms transform failed; re-run to retry.", e); } MigrationUtil.addTableColumnSearchSettings(); + addTriggerOperationToDefaultPolicies(collectionDAO); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java new file mode 100644 index 000000000000..e81ccd9c16d5 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java @@ -0,0 +1,43 @@ +package org.openmetadata.service.migration.utils.v1129; + +import static org.openmetadata.service.migration.utils.v160.MigrationUtil.addOperationsToPolicyRule; + +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.openmetadata.schema.type.MetadataOperation; +import org.openmetadata.service.jdbi3.CollectionDAO; + +@Slf4j +public class MigrationUtil { + + private MigrationUtil() {} + + /** + * Retrofits default seeded policies that grant broad edit capability with the {@code Trigger} + * operation, keeping pre-existing customer behavior intact after {@code /trigger} starts + * enforcing authz (GH-27962). + * + *

Targets the rules whose resources are {@code "All"} and that already grant {@code EditAll} + * (the broad-bot allow rules) plus {@code DataStewardPolicy} which grants {@code EditOwners} — + * stewards can already reach trigger via the ownership-edit escalation path, so granting it + * explicitly aligns the policy with the effective capability and improves the audit trail. + * + *

Each call is idempotent via {@link + * org.openmetadata.service.migration.utils.v160.MigrationUtil#addOperationsToPolicyRule}. + */ + public static void addTriggerOperationToDefaultPolicies(CollectionDAO collectionDAO) { + record PolicyRule(String policy, String rule) {} + List targets = + List.of( + new PolicyRule("IngestionBotPolicy", "IngestionBotRule-Allow"), + new PolicyRule("LineageBotPolicy", "LineageBotRule-Allow"), + new PolicyRule("ProfilerBotPolicy", "ProfilerBotBotRule-Allow"), + new PolicyRule("QualityBotPolicy", "QualityBotBotRule-Allow"), + new PolicyRule("UsageBotPolicy", "UsageBotRule-Allow-Usage"), + new PolicyRule("DataStewardPolicy", "DataStewardPolicy-EditRule")); + for (PolicyRule t : targets) { + addOperationsToPolicyRule( + t.policy(), t.rule(), List.of(MetadataOperation.TRIGGER), collectionDAO); + } + } +} diff --git a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json index 76a586bfcb12..8d76e7f52047 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json @@ -10,7 +10,7 @@ { "name": "DataStewardPolicy-EditRule", "resources" : ["all"], - "operations": ["ViewAll", "EditDescription", "EditDisplayName", "EditLineage", "EditOwners", "EditTags", "EditTier", "EditGlossaryTerms", "EditCertification"], + "operations": ["ViewAll", "EditDescription", "EditDisplayName", "EditLineage", "EditOwners", "EditTags", "EditTier", "EditGlossaryTerms", "EditCertification", "Trigger"], "effect": "allow" } ] diff --git a/openmetadata-service/src/main/resources/json/data/policy/IngestionBotPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/IngestionBotPolicy.json index 7b75e412f8fd..dc7cb21edaf3 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/IngestionBotPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/IngestionBotPolicy.json @@ -11,7 +11,7 @@ "name": "IngestionBotRule-Allow", "description" : "Allow ingestion bots to create/update/delete data entities", "resources" : ["All"], - "operations": ["Create", "BulkCreate", "BulkUpdate", "EditAll", "ViewAll", "Delete"], + "operations": ["Create", "BulkCreate", "BulkUpdate", "EditAll", "ViewAll", "Delete", "Trigger"], "effect": "allow" }, { diff --git a/openmetadata-service/src/main/resources/json/data/policy/LineageBotPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/LineageBotPolicy.json index 54e26cd92c30..4028df985789 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/LineageBotPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/LineageBotPolicy.json @@ -18,7 +18,7 @@ "name": "LineageBotRule-Allow", "description" : "Allow creating and updating lineage", "resources" : ["All"], - "operations": ["EditAll", "ViewAll"], + "operations": ["EditAll", "ViewAll", "Trigger"], "effect": "allow" }, { diff --git a/openmetadata-service/src/main/resources/json/data/policy/ProfilerBotPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/ProfilerBotPolicy.json index 042b882d47d9..f723c5d7826d 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/ProfilerBotPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/ProfilerBotPolicy.json @@ -11,7 +11,7 @@ "name": "ProfilerBotBotRule-Allow", "description" : "Allow updating sample data, profile data, and tests for all the resources.", "resources" : ["All"], - "operations": ["EditAll", "ViewAll"], + "operations": ["EditAll", "ViewAll", "Trigger"], "effect": "allow" }, { diff --git a/openmetadata-service/src/main/resources/json/data/policy/QualityBotPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/QualityBotPolicy.json index 1ef5c06808c9..dc1d8cc04c90 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/QualityBotPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/QualityBotPolicy.json @@ -11,7 +11,7 @@ "name": "QualityBotBotRule-Allow", "description" : "Allow updating sample data, profile data, and tests for all the resources.", "resources" : ["All"], - "operations": ["EditAll", "ViewAll"], + "operations": ["EditAll", "ViewAll", "Trigger"], "effect": "allow" }, { diff --git a/openmetadata-service/src/main/resources/json/data/policy/UsageBotPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/UsageBotPolicy.json index 993c02e99a9e..f8400ea4d2c5 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/UsageBotPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/UsageBotPolicy.json @@ -18,7 +18,7 @@ "name": "UsageBotRule-Allow-Usage", "description" : "Allow handling usage and lifecycle information.", "resources" : ["All"], - "operations": ["EditAll", "ViewAll"], + "operations": ["EditAll", "ViewAll", "Trigger"], "effect": "allow" }, { From b44a9208a3d455ce3dfde55f7598fdea00a30bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Manero?= Date: Thu, 14 May 2026 15:13:55 +0200 Subject: [PATCH 3/6] fix(test): use java.time.Instant instead of Joda-Time per code review --- .../it/tests/IngestionPipelineOwnerInheritanceIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java index e5e68c470638..964090dc9cfa 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/IngestionPipelineOwnerInheritanceIT.java @@ -5,11 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.time.Instant; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; -import org.joda.time.DateTime; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.parallel.Execution; @@ -54,7 +54,7 @@ @ExtendWith(TestNamespaceExtension.class) public class IngestionPipelineOwnerInheritanceIT { - private static final Date START_DATE = new DateTime("2022-06-10T15:06:47+00:00").toDate(); + private static final Date START_DATE = Date.from(Instant.parse("2022-06-10T15:06:47Z")); @Test void test_inheritedOwners_fromService(TestNamespace ns) { From 5a5e8ee435f7410f7441b3b224894adc7f8ee141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Manero?= Date: Thu, 14 May 2026 16:10:21 +0200 Subject: [PATCH 4/6] fix(policy): use a dedicated TriggerRule on DataStewardPolicy instead of mixing into EditRule --- .../migration/mysql/v1129/Migration.java | 6 +- .../migration/mysql/v1130/Migration.java | 6 +- .../migration/postgres/v1129/Migration.java | 6 +- .../migration/postgres/v1130/Migration.java | 6 +- .../migration/utils/v1129/MigrationUtil.java | 75 ++++++++++++++++--- .../json/data/policy/DataStewardPolicy.json | 9 ++- 6 files changed, 87 insertions(+), 21 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java index 1f719321cef6..4d3924ae4610 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1129/Migration.java @@ -1,6 +1,7 @@ package org.openmetadata.service.migration.mysql.v1129; -import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultBotPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerRuleToDataStewardPolicy; import lombok.SneakyThrows; import org.openmetadata.service.migration.api.MigrationProcessImpl; @@ -15,6 +16,7 @@ public Migration(MigrationFile migrationFile) { @Override @SneakyThrows public void runDataMigration() { - addTriggerOperationToDefaultPolicies(collectionDAO); + addTriggerOperationToDefaultBotPolicies(collectionDAO); + addTriggerRuleToDataStewardPolicy(collectionDAO); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java index d4ffc8e077af..d7592a4c154a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/mysql/v1130/Migration.java @@ -1,6 +1,7 @@ package org.openmetadata.service.migration.mysql.v1130; -import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultBotPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerRuleToDataStewardPolicy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -33,6 +34,7 @@ public void runDataMigration() { LOG.error("v1130 glossaryTerm version relatedTerms transform failed; re-run to retry.", e); } MigrationUtil.addTableColumnSearchSettings(); - addTriggerOperationToDefaultPolicies(collectionDAO); + addTriggerOperationToDefaultBotPolicies(collectionDAO); + addTriggerRuleToDataStewardPolicy(collectionDAO); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java index 122a013064fb..9b008f413532 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1129/Migration.java @@ -1,6 +1,7 @@ package org.openmetadata.service.migration.postgres.v1129; -import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultBotPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerRuleToDataStewardPolicy; import lombok.SneakyThrows; import org.openmetadata.service.migration.api.MigrationProcessImpl; @@ -15,6 +16,7 @@ public Migration(MigrationFile migrationFile) { @Override @SneakyThrows public void runDataMigration() { - addTriggerOperationToDefaultPolicies(collectionDAO); + addTriggerOperationToDefaultBotPolicies(collectionDAO); + addTriggerRuleToDataStewardPolicy(collectionDAO); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java index ee4968f40bbc..d43dee9f5c11 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/postgres/v1130/Migration.java @@ -1,6 +1,7 @@ package org.openmetadata.service.migration.postgres.v1130; -import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerOperationToDefaultBotPolicies; +import static org.openmetadata.service.migration.utils.v1129.MigrationUtil.addTriggerRuleToDataStewardPolicy; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -33,6 +34,7 @@ public void runDataMigration() { LOG.error("v1130 glossaryTerm version relatedTerms transform failed; re-run to retry.", e); } MigrationUtil.addTableColumnSearchSettings(); - addTriggerOperationToDefaultPolicies(collectionDAO); + addTriggerOperationToDefaultBotPolicies(collectionDAO); + addTriggerRuleToDataStewardPolicy(collectionDAO); } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java index e81ccd9c16d5..5631f751d1c9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java @@ -4,8 +4,15 @@ import java.util.List; import lombok.extern.slf4j.Slf4j; +import org.openmetadata.schema.entity.policies.Policy; +import org.openmetadata.schema.entity.policies.accessControl.Rule; +import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.MetadataOperation; +import org.openmetadata.schema.utils.JsonUtils; +import org.openmetadata.service.Entity; +import org.openmetadata.service.exception.EntityNotFoundException; import org.openmetadata.service.jdbi3.CollectionDAO; +import org.openmetadata.service.jdbi3.PolicyRepository; @Slf4j public class MigrationUtil { @@ -13,19 +20,15 @@ public class MigrationUtil { private MigrationUtil() {} /** - * Retrofits default seeded policies that grant broad edit capability with the {@code Trigger} - * operation, keeping pre-existing customer behavior intact after {@code /trigger} starts - * enforcing authz (GH-27962). + * Retrofits seeded bot policies that grant broad {@code EditAll} on {@code ["All"]} resources + * with the {@code Trigger} operation. Pre-fix these identities could trigger pipelines because + * {@code /trigger} skipped authz; the migration preserves that behavior under the new authz + * enforcement (GH-27962). * - *

Targets the rules whose resources are {@code "All"} and that already grant {@code EditAll} - * (the broad-bot allow rules) plus {@code DataStewardPolicy} which grants {@code EditOwners} — - * stewards can already reach trigger via the ownership-edit escalation path, so granting it - * explicitly aligns the policy with the effective capability and improves the audit trail. - * - *

Each call is idempotent via {@link + *

Each entry is idempotent via {@link * org.openmetadata.service.migration.utils.v160.MigrationUtil#addOperationsToPolicyRule}. */ - public static void addTriggerOperationToDefaultPolicies(CollectionDAO collectionDAO) { + public static void addTriggerOperationToDefaultBotPolicies(CollectionDAO collectionDAO) { record PolicyRule(String policy, String rule) {} List targets = List.of( @@ -33,11 +36,59 @@ record PolicyRule(String policy, String rule) {} new PolicyRule("LineageBotPolicy", "LineageBotRule-Allow"), new PolicyRule("ProfilerBotPolicy", "ProfilerBotBotRule-Allow"), new PolicyRule("QualityBotPolicy", "QualityBotBotRule-Allow"), - new PolicyRule("UsageBotPolicy", "UsageBotRule-Allow-Usage"), - new PolicyRule("DataStewardPolicy", "DataStewardPolicy-EditRule")); + new PolicyRule("UsageBotPolicy", "UsageBotRule-Allow-Usage")); for (PolicyRule t : targets) { addOperationsToPolicyRule( t.policy(), t.rule(), List.of(MetadataOperation.TRIGGER), collectionDAO); } } + + /** + * Adds a dedicated {@code DataStewardPolicy-TriggerRule} to the existing {@code + * DataStewardPolicy} if not already present. Data stewards already have {@code EditOwners} on + * all resources, so they could already reach trigger via an ownership rewrite; this rule makes + * the capability explicit for audit clarity rather than burying it inside the existing edit + * rule. + * + *

Mirrors the new-rule shape used by {@code + * v180.MigrationUtil.addDenyDisplayNameRuleToBotPolicies}. Idempotent — skips when the rule + * already exists. + */ + public static void addTriggerRuleToDataStewardPolicy(CollectionDAO collectionDAO) { + PolicyRepository repository = (PolicyRepository) Entity.getEntityRepository(Entity.POLICY); + try { + Policy policy = repository.findByName("DataStewardPolicy", Include.NON_DELETED); + boolean hasTriggerRule = + policy.getRules().stream() + .anyMatch( + r -> + "DataStewardPolicy-TriggerRule".equals(r.getName()) + && r.getEffect() == Rule.Effect.ALLOW + && r.getOperations() != null + && r.getOperations().contains(MetadataOperation.TRIGGER)); + if (!hasTriggerRule) { + Rule triggerRule = + new Rule() + .withName("DataStewardPolicy-TriggerRule") + .withDescription( + "Allow data stewards to trigger ingestion pipelines. Stewards already have " + + "EditOwners on all resources and can reach trigger via an ownership " + + "rewrite; this rule makes the capability explicit for audit clarity.") + .withResources(List.of("all")) + .withOperations(List.of(MetadataOperation.TRIGGER)) + .withEffect(Rule.Effect.ALLOW); + policy.getRules().add(triggerRule); + collectionDAO + .policyDAO() + .update(policy.getId(), policy.getFullyQualifiedName(), JsonUtils.pojoToJson(policy)); + LOG.info("Added DataStewardPolicy-TriggerRule to DataStewardPolicy"); + } else { + LOG.debug("DataStewardPolicy already has TriggerRule, skipping"); + } + } catch (EntityNotFoundException ex) { + LOG.warn("DataStewardPolicy not found, skipping TriggerRule addition"); + } catch (Exception e) { + LOG.error("Failed to add TriggerRule to DataStewardPolicy: {}", e.getMessage(), e); + } + } } diff --git a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json index 8d76e7f52047..e5d69a4adb24 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json @@ -10,7 +10,14 @@ { "name": "DataStewardPolicy-EditRule", "resources" : ["all"], - "operations": ["ViewAll", "EditDescription", "EditDisplayName", "EditLineage", "EditOwners", "EditTags", "EditTier", "EditGlossaryTerms", "EditCertification", "Trigger"], + "operations": ["ViewAll", "EditDescription", "EditDisplayName", "EditLineage", "EditOwners", "EditTags", "EditTier", "EditGlossaryTerms", "EditCertification"], + "effect": "allow" + }, + { + "name": "DataStewardPolicy-TriggerRule", + "description": "Allow data stewards to trigger ingestion pipelines. Stewards already have EditOwners on all resources and can reach trigger via an ownership rewrite; this rule makes the capability explicit for audit clarity.", + "resources": ["all"], + "operations": ["Trigger"], "effect": "allow" } ] From 7e53012baa54213153e435a5dcdee05acf2a2494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Manero?= Date: Thu, 14 May 2026 16:22:32 +0200 Subject: [PATCH 5/6] fix(migration): drop generic exception catch + inline rule description per code review --- .../service/migration/utils/v1129/MigrationUtil.java | 6 ------ .../main/resources/json/data/policy/DataStewardPolicy.json | 1 - 2 files changed, 7 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java index 5631f751d1c9..77ce7f54fe46 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/migration/utils/v1129/MigrationUtil.java @@ -70,10 +70,6 @@ public static void addTriggerRuleToDataStewardPolicy(CollectionDAO collectionDAO Rule triggerRule = new Rule() .withName("DataStewardPolicy-TriggerRule") - .withDescription( - "Allow data stewards to trigger ingestion pipelines. Stewards already have " - + "EditOwners on all resources and can reach trigger via an ownership " - + "rewrite; this rule makes the capability explicit for audit clarity.") .withResources(List.of("all")) .withOperations(List.of(MetadataOperation.TRIGGER)) .withEffect(Rule.Effect.ALLOW); @@ -87,8 +83,6 @@ public static void addTriggerRuleToDataStewardPolicy(CollectionDAO collectionDAO } } catch (EntityNotFoundException ex) { LOG.warn("DataStewardPolicy not found, skipping TriggerRule addition"); - } catch (Exception e) { - LOG.error("Failed to add TriggerRule to DataStewardPolicy: {}", e.getMessage(), e); } } } diff --git a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json index e5d69a4adb24..45c71ef1b4b7 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json @@ -15,7 +15,6 @@ }, { "name": "DataStewardPolicy-TriggerRule", - "description": "Allow data stewards to trigger ingestion pipelines. Stewards already have EditOwners on all resources and can reach trigger via an ownership rewrite; this rule makes the capability explicit for audit clarity.", "resources": ["all"], "operations": ["Trigger"], "effect": "allow" From 8f37761f1aa806833174015755e361a8e0f44ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Manero?= Date: Thu, 14 May 2026 16:30:33 +0200 Subject: [PATCH 6/6] fix(policy): add trailing newline to DataStewardPolicy.json per code review --- .../src/main/resources/json/data/policy/DataStewardPolicy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json index 45c71ef1b4b7..6b894ace543b 100644 --- a/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json +++ b/openmetadata-service/src/main/resources/json/data/policy/DataStewardPolicy.json @@ -20,4 +20,4 @@ "effect": "allow" } ] -} \ No newline at end of file +}