Allow for domain-level permissions while creating/updating dataProduct

This commit is contained in:
sonika-shah 2025-04-28 13:43:24 +05:30 committed by karanh37
parent 3bec47ef8a
commit bfdf562642
7 changed files with 493 additions and 68 deletions

View File

@ -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.
*
* <p>As multiple threads don't access this, the class is not thread-safe by design.
*/
@Slf4j
public class CreateResourceContext<T extends EntityInterface> implements ResourceContextInterface {
@NonNull @Getter private final String resource;
private final EntityRepository<T> entityRepository;
@ -54,26 +59,47 @@ public class CreateResourceContext<T extends EntityInterface> 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;
}
}
}

View File

@ -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;
*
* <p>As multiple threads don't access this, the class is not thread-safe by design.
*/
@Slf4j
public class ResourceContext<T extends EntityInterface> implements ResourceContextInterface {
@NonNull @Getter private final String resource;
private final EntityRepository<T> entityRepository;
@ -73,7 +81,36 @@ public class ResourceContext<T extends EntityInterface> 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<EntityReference> 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

View File

@ -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<DataProduct> 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<List<TagLabel>>) 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<CompiledRule> 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.");
}
}

View File

@ -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<void>;
};
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();
});
});

View File

@ -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,
};
};

View File

@ -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: (

View File

@ -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<AssetsTabRef>(null);
const dataProductsTabRef = useRef<DataProductsTabRef>(null);
const [domainPermission, setDomainPermission] = useState<OperationPermission>(
@ -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,