diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/CreateResourceContext.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/CreateResourceContext.java
index 57d81e9aec4..77d97e544f9 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/CreateResourceContext.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/CreateResourceContext.java
@@ -1,23 +1,28 @@
package org.openmetadata.service.security.policyevaluator;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import javax.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.EntityInterface;
+import org.openmetadata.schema.entity.classification.Tag;
+import org.openmetadata.schema.entity.data.GlossaryTerm;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.service.Entity;
import org.openmetadata.service.exception.EntityNotFoundException;
import org.openmetadata.service.jdbi3.EntityRepository;
-import org.openmetadata.service.util.EntityUtil;
+import org.openmetadata.service.util.EntityUtil.Fields;
/**
* ResourceContext used for CREATE operations where ownership, tags are inherited from the parent term.
*
*
As multiple threads don't access this, the class is not thread-safe by design.
*/
+@Slf4j
public class CreateResourceContext implements ResourceContextInterface {
@NonNull @Getter private final String resource;
private final EntityRepository entityRepository;
@@ -54,26 +59,47 @@ public class CreateResourceContext implements Resourc
}
private void setParent(T entity) {
- String fields = "";
+ Fields fields = new Fields(new HashSet<>());
if (entityRepository.isSupportsOwners()) {
- fields = EntityUtil.addField(fields, Entity.FIELD_OWNERS);
+ fields.getFieldList().add(Entity.FIELD_OWNERS);
}
if (entityRepository.isSupportsTags()) {
- fields = EntityUtil.addField(fields, Entity.FIELD_TAGS);
+ fields.getFieldList().add(Entity.FIELD_TAGS);
}
if (entityRepository.isSupportsDomain()) {
- fields = EntityUtil.addField(fields, Entity.FIELD_DOMAIN);
+ fields.getFieldList().add(Entity.FIELD_DOMAIN);
}
if (entityRepository.isSupportsReviewers()) {
- fields = EntityUtil.addField(fields, Entity.FIELD_REVIEWERS);
- }
- if (entityRepository.isSupportsDomain()) {
- fields = EntityUtil.addField(fields, Entity.FIELD_DOMAIN);
+ fields.getFieldList().add(Entity.FIELD_REVIEWERS);
}
try {
- parentEntity = entityRepository.getParentEntity(entity, fields);
+ // First, check direct parent
+ parentEntity = entityRepository.getParentEntity(entity, fields.toString());
+ // If direct parent is not found, check for root-level parent
+ if (parentEntity == null) {
+ parentEntity = resolveRootParentEntity(entity, fields);
+ }
} catch (EntityNotFoundException e) {
parentEntity = null;
}
}
+
+ private EntityInterface resolveRootParentEntity(T entity, Fields fields) {
+ try {
+ EntityReference rootReference =
+ switch (entityRepository.getEntityType()) {
+ case Entity.GLOSSARY_TERM -> ((GlossaryTerm) entity).getGlossary();
+ case Entity.TAG -> ((Tag) entity).getClassification();
+ case Entity.DATA_PRODUCT -> entity.getDomain();
+ default -> null;
+ };
+
+ if (rootReference == null || rootReference.getId() == null) return null;
+ EntityRepository> rootRepository = Entity.getEntityRepository(rootReference.getType());
+ return rootRepository.get(null, rootReference.getId(), fields);
+ } catch (Exception e) {
+ LOG.error("Failed to resolve root parent entity: {}", e.getMessage(), e);
+ return null;
+ }
+ }
}
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java
index 499f379d014..c0cbb71b377 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/policyevaluator/ResourceContext.java
@@ -1,12 +1,19 @@
package org.openmetadata.service.security.policyevaluator;
+import static org.openmetadata.service.Entity.FIELD_OWNERS;
+
import com.google.common.annotations.VisibleForTesting;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.EntityInterface;
+import org.openmetadata.schema.entity.classification.Tag;
+import org.openmetadata.schema.entity.data.GlossaryTerm;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.TagLabel;
@@ -22,6 +29,7 @@ import org.openmetadata.service.util.EntityUtil.Fields;
*
* As multiple threads don't access this, the class is not thread-safe by design.
*/
+@Slf4j
public class ResourceContext implements ResourceContextInterface {
@NonNull @Getter private final String resource;
private final EntityRepository entityRepository;
@@ -73,7 +81,36 @@ public class ResourceContext implements ResourceConte
} else if (Entity.USER.equals(entityRepository.getEntityType())) {
return List.of(entity.getEntityReference()); // Owner for a user is same as the user
}
- return entity.getOwners();
+
+ // Check for parent owners
+ List owners = entity.getOwners();
+ EntityInterface parentEntity = resolveParentEntity(entity);
+ if (parentEntity != null && parentEntity.getOwners() != null) {
+ if (owners == null) owners = new ArrayList<>();
+ owners.addAll(parentEntity.getOwners());
+ }
+
+ return owners;
+ }
+
+ private EntityInterface resolveParentEntity(T entity) {
+ Fields fields = new Fields(new HashSet<>(Collections.singleton(FIELD_OWNERS)));
+ try {
+ EntityReference parentReference =
+ switch (entityRepository.getEntityType()) {
+ case Entity.GLOSSARY_TERM -> ((GlossaryTerm) entity).getGlossary();
+ case Entity.TAG -> ((Tag) entity).getClassification();
+ case Entity.DATA_PRODUCT -> entity.getDomain();
+ default -> null;
+ };
+
+ if (parentReference == null || parentReference.getId() == null) return null;
+ EntityRepository> rootRepository = Entity.getEntityRepository(parentReference.getType());
+ return rootRepository.get(null, parentReference.getId(), fields);
+ } catch (Exception e) {
+ LOG.error("Failed to resolve parent entity: {}", e.getMessage(), e);
+ return null;
+ }
}
@Override
diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/security/policyevaluator/RuleEvaluatorTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/security/policyevaluator/RuleEvaluatorTest.java
index 7f930c93fa9..94b1d84e30f 100644
--- a/openmetadata-service/src/test/java/org/openmetadata/service/security/policyevaluator/RuleEvaluatorTest.java
+++ b/openmetadata-service/src/test/java/org/openmetadata/service/security/policyevaluator/RuleEvaluatorTest.java
@@ -9,6 +9,9 @@ import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.openmetadata.common.utils.CommonUtil.listOf;
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
+import static org.openmetadata.schema.type.MetadataOperation.CREATE;
+import static org.openmetadata.schema.type.MetadataOperation.EDIT_TAGS;
+import static org.openmetadata.service.resources.EntityResourceTest.DATA_CONSUMER_ROLE_NAME;
import static org.openmetadata.service.security.policyevaluator.CompiledRule.parseExpression;
import static org.openmetadata.service.security.policyevaluator.SubjectContext.TEAM_FIELDS;
@@ -16,12 +19,20 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
+import org.openmetadata.schema.entity.data.Database;
+import org.openmetadata.schema.entity.data.DatabaseSchema;
import org.openmetadata.schema.entity.data.Table;
+import org.openmetadata.schema.entity.domains.DataProduct;
+import org.openmetadata.schema.entity.domains.Domain;
+import org.openmetadata.schema.entity.policies.Policy;
+import org.openmetadata.schema.entity.policies.accessControl.Rule;
import org.openmetadata.schema.entity.teams.Role;
import org.openmetadata.schema.entity.teams.Team;
import org.openmetadata.schema.entity.teams.User;
@@ -30,19 +41,35 @@ import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.TagLabel;
import org.openmetadata.service.Entity;
+import org.openmetadata.service.jdbi3.DataProductRepository;
+import org.openmetadata.service.jdbi3.DatabaseRepository;
+import org.openmetadata.service.jdbi3.DatabaseSchemaRepository;
+import org.openmetadata.service.jdbi3.DomainRepository;
import org.openmetadata.service.jdbi3.EntityRepository;
import org.openmetadata.service.jdbi3.TableRepository;
import org.openmetadata.service.jdbi3.TeamRepository;
import org.openmetadata.service.security.policyevaluator.SubjectContext.PolicyContext;
+import org.openmetadata.service.util.EntityUtil;
import org.springframework.expression.EvaluationContext;
-import org.springframework.expression.spel.support.SimpleEvaluationContext;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+@Slf4j
class RuleEvaluatorTest {
private static final Table table = new Table().withName("table");
private static User user;
private static EvaluationContext evaluationContext;
private static SubjectContext subjectContext;
private static ResourceContext> resourceContext;
+ private static final String DATA_CONSUMER_POLICY_NAME = "DataConsumerPolicy";
+
+ private static CreateResourceContext> createResourceContextSchema;
+ private static CreateResourceContext> createResourceContextDataProduct;
+ private static ResourceContext resourceContextDataProduct;
+
+ private static User ownerUser;
+ private static User nonOwnerUser;
+ private static EntityReference ownerRef;
+ private static EntityReference databaseRef;
@BeforeAll
public static void setup() {
@@ -87,12 +114,77 @@ class RuleEvaluatorTest {
Mockito.when(tableRepository.getAllTags(any()))
.thenAnswer((Answer>) invocationOnMock -> table.getTags());
+ DatabaseRepository databaseRepository = mock(DatabaseRepository.class);
+ Mockito.when(databaseRepository.getEntityType()).thenReturn(Entity.DATABASE);
+ Mockito.when(databaseRepository.isSupportsOwners()).thenReturn(Boolean.TRUE);
+ Entity.registerEntity(Database.class, Entity.DATABASE, databaseRepository);
+
+ DatabaseSchemaRepository databaseSchemaRepository = mock(DatabaseSchemaRepository.class);
+ Mockito.when(databaseSchemaRepository.getEntityType()).thenReturn(Entity.DATABASE_SCHEMA);
+ Mockito.when(databaseSchemaRepository.isSupportsOwners()).thenReturn(Boolean.TRUE);
+ Entity.registerEntity(DatabaseSchema.class, Entity.DATABASE_SCHEMA, databaseSchemaRepository);
+
+ DomainRepository domainRepository = mock(DomainRepository.class);
+ Mockito.when(domainRepository.getEntityType()).thenReturn(Entity.DOMAIN);
+ Mockito.when(domainRepository.isSupportsOwners()).thenReturn(Boolean.TRUE);
+ Entity.registerEntity(Domain.class, Entity.DOMAIN, domainRepository);
+ Mockito.when(domainRepository.get(isNull(), any(UUID.class), any(EntityUtil.Fields.class)))
+ .thenAnswer(
+ i ->
+ EntityRepository.CACHE_WITH_ID.get(
+ new ImmutablePair<>(Entity.DOMAIN, i.getArgument(1))));
+
+ DataProductRepository dataProductRepository = mock(DataProductRepository.class);
+ Mockito.when(dataProductRepository.getEntityType()).thenReturn(Entity.DATA_PRODUCT);
+ Mockito.when(dataProductRepository.isSupportsOwners()).thenReturn(Boolean.TRUE);
+ Entity.registerEntity(DataProduct.class, Entity.DATA_PRODUCT, dataProductRepository);
+
user = new User().withId(UUID.randomUUID()).withName("user");
+ ownerUser = new User().withId(UUID.randomUUID()).withName("owner");
+ nonOwnerUser = new User().withId(UUID.randomUUID()).withName("nonOwner");
+ ownerRef = ownerUser.getEntityReference().withType(Entity.USER);
+
+ Database database = new Database().withId(UUID.randomUUID()).withName("testDB");
+ databaseRef = database.getEntityReference();
+ DatabaseSchema schema = new DatabaseSchema().withId(UUID.randomUUID()).withName("testSchema");
+ schema.setDatabase(databaseRef);
+ database.setOwners(List.of(ownerRef));
+ EntityRepository.CACHE_WITH_ID.put(
+ new ImmutablePair<>(Entity.DATABASE_SCHEMA, schema.getId()), schema);
+ EntityRepository.CACHE_WITH_ID.put(
+ new ImmutablePair<>(Entity.DATABASE, database.getId()), database);
+ Mockito.when(databaseSchemaRepository.getParentEntity(any(DatabaseSchema.class), anyString()))
+ .thenAnswer(
+ i -> {
+ DatabaseSchema cachedSchema = i.getArgument(0);
+ EntityReference dbRef = cachedSchema.getDatabase();
+ if (dbRef == null) return null;
+ Database db =
+ (Database)
+ EntityRepository.CACHE_WITH_ID.get(
+ new ImmutablePair<>(Entity.DATABASE, dbRef.getId()));
+ return db;
+ });
+ createResourceContextSchema =
+ Mockito.spy(new CreateResourceContext<>(Entity.DATABASE_SCHEMA, schema));
+
+ Domain domain = new Domain().withId(UUID.randomUUID()).withName("testDomain");
+ DataProduct dataProduct =
+ new DataProduct().withId(UUID.randomUUID()).withName("testDataProduct");
+ dataProduct.setDomain(domain.getEntityReference());
+ domain.setOwners(List.of(ownerRef));
+ EntityRepository.CACHE_WITH_ID.put(new ImmutablePair<>(Entity.DOMAIN, domain.getId()), domain);
+ EntityRepository.CACHE_WITH_ID.put(
+ new ImmutablePair<>(Entity.DATA_PRODUCT, dataProduct.getId()), dataProduct);
+ resourceContextDataProduct =
+ Mockito.spy(new ResourceContext<>(Entity.DATA_PRODUCT, dataProduct, dataProductRepository));
+ createResourceContextDataProduct =
+ Mockito.spy(new CreateResourceContext<>(Entity.DATA_PRODUCT, dataProduct));
+
resourceContext = new ResourceContext<>("table", table, mock(TableRepository.class));
subjectContext = new SubjectContext(user);
RuleEvaluator ruleEvaluator = new RuleEvaluator(null, subjectContext, resourceContext);
- evaluationContext =
- SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(ruleEvaluator).build();
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
}
@Test
@@ -144,6 +236,73 @@ class RuleEvaluatorTest {
.withName(user.getName())));
assertTrue(evaluateExpression("noOwner() || isOwner()"));
assertFalse(evaluateExpression("!noOwner() && !isOwner()"));
+
+ // Verify that parent owner has the necessary permissions to create child entities -
+ // createResourceContext
+ Role dataConsumerRole = new Role().withId(UUID.randomUUID()).withName(DATA_CONSUMER_ROLE_NAME);
+ ownerUser.setRoles(List.of(dataConsumerRole.getEntityReference()));
+ Rule createRule =
+ new Rule()
+ .withResources(List.of("All"))
+ .withOperations(List.of(CREATE))
+ .withCondition("isOwner()")
+ .withEffect(Rule.Effect.ALLOW);
+ Policy policy = new Policy().withName(DATA_CONSUMER_POLICY_NAME).withRules(List.of(createRule));
+ List compiledRules = List.of(new CompiledRule(createRule));
+ PolicyContext policyContext =
+ new PolicyContext(
+ Entity.USER,
+ ownerUser.getName(),
+ DATA_CONSUMER_ROLE_NAME,
+ policy.getName(),
+ compiledRules);
+
+ subjectContext = new SubjectContext(ownerUser);
+ RuleEvaluator ruleEvaluator =
+ new RuleEvaluator(policyContext, subjectContext, createResourceContextSchema);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ assertTrue(evaluateExpression("isOwner()"));
+
+ subjectContext = new SubjectContext(nonOwnerUser);
+ ruleEvaluator = new RuleEvaluator(policyContext, subjectContext, createResourceContextSchema);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ assertFalse(evaluateExpression("isOwner()"));
+
+ // Verify that domain owner has the necessary permissions to create/edit its dataProduct -
+ // ResourceContext (edit related permissions)
+ Rule editRule =
+ new Rule()
+ .withResources(List.of("All"))
+ .withOperations(List.of(CREATE, EDIT_TAGS))
+ .withCondition("isOwner()")
+ .withEffect(Rule.Effect.ALLOW);
+ policy = new Policy().withName(DATA_CONSUMER_POLICY_NAME).withRules(List.of(editRule));
+ compiledRules = List.of(new CompiledRule(editRule));
+ policyContext =
+ new PolicyContext(
+ Entity.USER,
+ ownerUser.getName(),
+ DATA_CONSUMER_ROLE_NAME,
+ policy.getName(),
+ compiledRules);
+
+ subjectContext = new SubjectContext(ownerUser);
+ ruleEvaluator = new RuleEvaluator(policyContext, subjectContext, resourceContextDataProduct);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ assertTrue(evaluateExpression("isOwner()"));
+ ruleEvaluator =
+ new RuleEvaluator(policyContext, subjectContext, createResourceContextDataProduct);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ assertTrue(evaluateExpression("isOwner()"));
+
+ subjectContext = new SubjectContext(nonOwnerUser);
+ ruleEvaluator = new RuleEvaluator(policyContext, subjectContext, resourceContextDataProduct);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ assertFalse(evaluateExpression("isOwner()"));
+ ruleEvaluator =
+ new RuleEvaluator(policyContext, subjectContext, createResourceContextDataProduct);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ assertFalse(evaluateExpression("isOwner()"));
}
@Test
@@ -363,7 +522,14 @@ class RuleEvaluatorTest {
private void updatePolicyContext(String team) {
PolicyContext policyContext = new PolicyContext(Entity.TEAM, team, null, null, null);
RuleEvaluator ruleEvaluator = new RuleEvaluator(policyContext, subjectContext, resourceContext);
- evaluationContext =
- SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(ruleEvaluator).build();
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ }
+
+ @AfterEach
+ void resetContext() {
+ subjectContext = new SubjectContext(user);
+ RuleEvaluator ruleEvaluator = new RuleEvaluator(null, subjectContext, resourceContext);
+ evaluationContext = new StandardEvaluationContext(ruleEvaluator);
+ LOG.info("Context reset to default state after test completion.");
}
}
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts
index bbebc01dc1b..ce14a841a93 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts
@@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import base, { expect, Page } from '@playwright/test';
+import base, { APIRequestContext, expect, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { get } from 'lodash';
import { SidebarItem } from '../../constant/sidebar';
@@ -24,7 +24,11 @@ import { ClassificationClass } from '../../support/tag/ClassificationClass';
import { TagClass } from '../../support/tag/TagClass';
import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
-import { getApiContext, redirectToHomePage } from '../../utils/common';
+import {
+ clickOutside,
+ getApiContext,
+ redirectToHomePage,
+} from '../../utils/common';
import { CustomPropertyTypeByName } from '../../utils/customProperty';
import {
addAssetsToDataProduct,
@@ -36,15 +40,16 @@ import {
createSubDomain,
removeAssetsFromDataProduct,
selectDataProduct,
+ selectDataProductFromTab,
selectDomain,
selectSubDomain,
setupAssetsForDomain,
+ setupDomainOwnershipTest,
verifyDataProductAssetsAfterDelete,
verifyDomain,
} from '../../utils/domain';
import { sidebarClick } from '../../utils/sidebar';
import { performUserLogin, visitUserProfilePage } from '../../utils/user';
-
const user = new UserClass();
const domain = new Domain();
@@ -711,3 +716,105 @@ test.describe('Domains Rbac', () => {
await afterAction();
});
});
+
+test.describe('Data Consumer Domain Ownership', () => {
+ test.slow(true);
+
+ const classification = new ClassificationClass({
+ provider: 'system',
+ mutuallyExclusive: true,
+ });
+ const tag = new TagClass({
+ classification: classification.data.name,
+ });
+ const glossary = new Glossary();
+ const glossaryTerm = new GlossaryTerm(glossary);
+
+ let testResources: {
+ dataConsumerUser: UserClass;
+ domainForTest: Domain;
+ dataProductForTest: DataProduct;
+ cleanup: (apiContext1: APIRequestContext) => Promise;
+ };
+
+ test.beforeAll('Setup pre-requests', async ({ browser }) => {
+ const { apiContext, afterAction } = await performAdminLogin(browser);
+ await classification.create(apiContext);
+ await tag.create(apiContext);
+ await glossary.create(apiContext);
+ await glossaryTerm.create(apiContext);
+
+ testResources = await setupDomainOwnershipTest(apiContext);
+
+ await afterAction();
+ });
+
+ test.afterAll('Cleanup', async ({ browser }) => {
+ const { apiContext, afterAction } = await performAdminLogin(browser);
+ await tag.delete(apiContext);
+ await glossary.delete(apiContext);
+ await glossaryTerm.delete(apiContext);
+ await classification.delete(apiContext);
+ await testResources.cleanup(apiContext);
+
+ await afterAction();
+ });
+
+ test('Data consumer can manage domain as owner', async ({ browser }) => {
+ const { page: dataConsumerPage, afterAction: consumerAfterAction } =
+ await performUserLogin(browser, testResources.dataConsumerUser);
+
+ await test.step(
+ 'Check domain management permissions for data consumer owner',
+ async () => {
+ await sidebarClick(dataConsumerPage, SidebarItem.DOMAIN);
+ await selectDomain(dataConsumerPage, testResources.domainForTest.data);
+
+ await dataConsumerPage.getByTestId('domain-details-add-button').click();
+
+ // check Data Products menu item is visible
+ await expect(
+ dataConsumerPage.getByRole('menuitem', {
+ name: 'Data Products',
+ exact: true,
+ })
+ ).toBeVisible();
+
+ await clickOutside(dataConsumerPage);
+
+ await selectDataProductFromTab(
+ dataConsumerPage,
+ testResources.dataProductForTest.data
+ );
+
+ // Verify the user can edit owner, tags, glossary and domain experts
+ await expect(dataConsumerPage.getByTestId('edit-owner')).toBeVisible();
+ await expect(
+ dataConsumerPage.getByTestId('tags-container').getByTestId('add-tag')
+ ).toBeVisible();
+
+ await expect(
+ dataConsumerPage
+ .getByTestId('glossary-container')
+ .getByTestId('add-tag')
+ ).toBeVisible();
+
+ await expect(
+ dataConsumerPage.getByTestId('domain-expert-name').getByTestId('Add')
+ ).toBeVisible();
+
+ await expect(
+ dataConsumerPage.getByTestId('manage-button')
+ ).toBeVisible();
+
+ await addTagsAndGlossaryToDomain(dataConsumerPage, {
+ tagFqn: tag.responseData.fullyQualifiedName,
+ glossaryTermFqn: glossaryTerm.responseData.fullyQualifiedName,
+ isDomain: false,
+ });
+ }
+ );
+
+ await consumerAfterAction();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts
index d6d3ef5431a..3d81e654343 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts
@@ -10,9 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import test, { expect, Page } from '@playwright/test';
+import test, { APIRequestContext, expect, Page } from '@playwright/test';
import { get, isEmpty, isUndefined } from 'lodash';
import { SidebarItem } from '../constant/sidebar';
+import { PolicyClass } from '../support/access-control/PoliciesClass';
+import { RolesClass } from '../support/access-control/RolesClass';
import { DataProduct } from '../support/domain/DataProduct';
import { Domain } from '../support/domain/Domain';
import { SubDomain } from '../support/domain/SubDomain';
@@ -21,6 +23,8 @@ import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
import { EntityClass } from '../support/entity/EntityClass';
import { TableClass } from '../support/entity/TableClass';
import { TopicClass } from '../support/entity/TopicClass';
+import { TeamClass } from '../support/team/TeamClass';
+import { UserClass } from '../support/user/UserClass';
import {
closeFirstPopupAlert,
descriptionBox,
@@ -29,6 +33,7 @@ import {
NAME_MAX_LENGTH_VALIDATION_ERROR,
NAME_VALIDATION_ERROR,
redirectToHomePage,
+ uuid,
} from './common';
import { addOwner } from './entity';
import { sidebarClick } from './sidebar';
@@ -144,7 +149,10 @@ export const selectDataProductFromTab = async (
const dpRes = page.waitForResponse(
'/api/v1/search/query?*&from=0&size=50&index=data_product_search_index'
);
- await page.getByText('Data Products').click();
+ await page
+ .locator('.domain-details-page-tabs')
+ .getByText('Data Products')
+ .click();
await dpRes;
@@ -601,3 +609,107 @@ export const addTagsAndGlossaryToDomain = async (
// Add glossary term
await addTagOrTerm('glossary', glossaryTermFqn);
};
+
+/**
+ * Verifies if the active domain is set to All Domains (DEFAULT_DOMAIN_VALUE)
+ */
+export const verifyActiveDomainIsDefault = async (page: Page) => {
+ await expect(page.getByTestId('domain-dropdown')).toContainText(
+ 'All Domains'
+ );
+};
+
+/**
+ * Sets up a complete environment for domain ownership testing
+ * Creates user, policy, role, domain, data product and assigns ownership
+ * Returns all created objects and a cleanup function
+ */
+export const setupDomainOwnershipTest = async (apiContext: any) => {
+ // Create all necessary resources
+ const dataConsumerUser = new UserClass();
+ const id = uuid();
+ const domainForTest = new Domain({
+ name: `PW_Domain_Owner_Rule_Testing-${id}`,
+ displayName: `PW_Domain_Owner_Rule_Testing-${id}`,
+ description: 'playwright domain description',
+ domainType: 'Aggregate',
+ fullyQualifiedName: `PW_Domain_Owner_Rule_Testing-${id}`,
+ });
+ const dataProductForTest = new DataProduct(
+ domainForTest,
+ `PW_DataProduct_Owner_Rule-${id}`
+ );
+
+ await dataConsumerUser.create(apiContext);
+ await domainForTest.create(apiContext);
+
+ // Setup permissions
+ const dataConsumerPolicy = new PolicyClass();
+ const dataConsumerRole = new RolesClass();
+
+ // Create domain access policy
+ const domainRule = [
+ {
+ name: 'DomainRule',
+ description: '',
+ resources: ['dataProduct', 'domain'],
+ operations: ['All'],
+ effect: 'allow',
+ condition: 'isOwner()',
+ },
+ ];
+
+ await dataConsumerPolicy.create(apiContext, domainRule);
+ await dataConsumerRole.create(apiContext, [
+ dataConsumerPolicy.responseData.name,
+ ]);
+
+ await dataProductForTest.create(apiContext);
+
+ // Create team for the user
+ const dataConsumerTeam = new TeamClass({
+ name: `PW_data_consumer_team-${id}`,
+ displayName: `PW Data Consumer Team ${id}`,
+ description: 'playwright data consumer team description',
+ teamType: 'Group',
+ users: [dataConsumerUser.responseData.id ?? ''],
+ defaultRoles: [dataConsumerRole.responseData.id ?? ''],
+ });
+
+ await dataConsumerTeam.create(apiContext);
+
+ // Set domain ownership
+ await domainForTest.patch({
+ apiContext,
+ patchData: [
+ {
+ op: 'add',
+ path: '/owners/0',
+ value: {
+ id: dataConsumerUser.responseData.id,
+ type: 'user',
+ },
+ },
+ ],
+ });
+
+ // Return cleanup function and all created resources
+ const cleanup = async (apiContext1: APIRequestContext) => {
+ await dataProductForTest.delete(apiContext1);
+ await domainForTest.delete(apiContext1);
+ await dataConsumerUser.delete(apiContext1);
+ await dataConsumerTeam.delete(apiContext1);
+ await dataConsumerPolicy.delete(apiContext1);
+ await dataConsumerRole.delete(apiContext1);
+ };
+
+ return {
+ dataConsumerUser,
+ domainForTest,
+ dataProductForTest,
+ dataConsumerTeam,
+ dataConsumerPolicy,
+ dataConsumerRole,
+ cleanup,
+ };
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx
index 6647b671182..0fedec00694 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsDetailsPage/DataProductsDetailsPage.component.tsx
@@ -50,7 +50,6 @@ import {
DataProduct,
} from '../../../generated/entity/domains/dataProduct';
import { Domain } from '../../../generated/entity/domains/domain';
-import { Operation } from '../../../generated/entity/policies/policy';
import { Style } from '../../../generated/type/tagLabel';
import { useFqn } from '../../../hooks/useFqn';
import { QueryFilterInterface } from '../../../pages/ExplorePage/ExplorePage.interface';
@@ -59,10 +58,7 @@ import { getEntityDeleteMessage } from '../../../utils/CommonUtils';
import { getQueryFilterToIncludeDomain } from '../../../utils/DomainUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
-import {
- checkPermission,
- DEFAULT_ENTITY_PERMISSION,
-} from '../../../utils/PermissionsUtils';
+import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
import { getDomainPath } from '../../../utils/RouterUtils';
import {
escapeESReservedCharacters,
@@ -102,7 +98,7 @@ const DataProductsDetailsPage = ({
}: DataProductsDetailsPageProps) => {
const { t } = useTranslation();
const history = useHistory();
- const { getEntityPermission, permissions } = usePermissionProvider();
+ const { getEntityPermission } = usePermissionProvider();
const { tab: activeTab, version } =
useParams<{ tab: string; version: string }>();
const { fqn: dataProductFqn } = useFqn();
@@ -158,7 +154,7 @@ const DataProductsDetailsPage = ({
const {
editDisplayNamePermission,
editAllPermission,
- deleteDataProductPermision,
+ deleteDataProductPermission,
} = useMemo(() => {
if (isVersionsView) {
return {
@@ -168,44 +164,17 @@ const DataProductsDetailsPage = ({
};
}
- const editDescription = checkPermission(
- Operation.EditDescription,
- ResourceEntity.DATA_PRODUCT,
- permissions
- );
-
- const editOwner = checkPermission(
- Operation.EditOwners,
- ResourceEntity.DATA_PRODUCT,
- permissions
- );
-
- const editAll = checkPermission(
- Operation.EditAll,
- ResourceEntity.DATA_PRODUCT,
- permissions
- );
-
- const editDisplayName = checkPermission(
- Operation.EditDisplayName,
- ResourceEntity.DATA_PRODUCT,
- permissions
- );
-
- const deleteDataProduct = checkPermission(
- Operation.Delete,
- ResourceEntity.DATA_PRODUCT,
- permissions
- );
-
return {
- editDescriptionPermission: editDescription || editAll,
- editOwnerPermission: editOwner || editAll,
- editAllPermission: editAll,
- editDisplayNamePermission: editDisplayName || editAll,
- deleteDataProductPermision: deleteDataProduct,
+ editDescriptionPermission:
+ dataProductPermission.EditDescription || dataProductPermission.EditAll,
+ editOwnerPermission:
+ dataProductPermission.EditOwners || dataProductPermission.EditAll,
+ editAllPermission: dataProductPermission.EditAll,
+ editDisplayNamePermission:
+ dataProductPermission.EditDisplayName || dataProductPermission.EditAll,
+ deleteDataProductPermission: dataProductPermission.Delete,
};
- }, [permissions, isVersionsView]);
+ }, [dataProductPermission, isVersionsView]);
const fetchDataProductAssets = async () => {
if (dataProduct) {
@@ -287,7 +256,7 @@ const DataProductsDetailsPage = ({
},
] as ItemType[])
: []),
- ...(deleteDataProductPermision
+ ...(deleteDataProductPermission
? ([
{
label: (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx
index b46180b573c..35f43e79070 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Domain/DomainDetailsPage/DomainDetailsPage.component.tsx
@@ -27,7 +27,7 @@ import { useForm } from 'antd/lib/form/Form';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios';
import classNames from 'classnames';
-import { cloneDeep, isEmpty, toString } from 'lodash';
+import { cloneDeep, isEmpty, isEqual, toString } from 'lodash';
import React, {
useCallback,
useEffect,
@@ -73,6 +73,7 @@ import { DataProduct } from '../../../generated/entity/domains/dataProduct';
import { Domain } from '../../../generated/entity/domains/domain';
import { ChangeDescription } from '../../../generated/entity/type';
import { Style } from '../../../generated/type/tagLabel';
+import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { useFqn } from '../../../hooks/useFqn';
import { addDataProducts } from '../../../rest/dataProductAPI';
import { addDomains } from '../../../rest/domainAPI';
@@ -128,6 +129,8 @@ const DomainDetailsPage = ({
const { tab: activeTab, version } =
useParams<{ tab: string; version: string }>();
const { fqn: domainFqn } = useFqn();
+ const { currentUser } = useApplicationStore();
+
const assetTabRef = useRef(null);
const dataProductsTabRef = useRef(null);
const [domainPermission, setDomainPermission] = useState(
@@ -153,6 +156,11 @@ const DomainDetailsPage = ({
const isSubDomain = useMemo(() => !isEmpty(domain.parent), [domain]);
+ const isOwner = useMemo(
+ () => domain.owners?.some((owner) => isEqual(owner.id, currentUser?.id)),
+ [domain, currentUser]
+ );
+
const breadcrumbs = useMemo(() => {
if (!domainFqn) {
return [];
@@ -217,7 +225,7 @@ const DomainDetailsPage = ({
},
]
: []),
- ...(permissions.dataProduct.Create
+ ...(isOwner || permissions.dataProduct.Create
? [
{
label: t('label.data-product-plural'),
@@ -283,7 +291,7 @@ const DomainDetailsPage = ({
);
const addDataProduct = useCallback(
- async (formData: CreateDataProduct) => {
+ async (formData: CreateDomain | CreateDataProduct) => {
const data = {
...formData,
domain: domain.fullyQualifiedName,