From b5a2cf98229c8fb76a9dd9edb9a1568cdbce93aa Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Mon, 3 Nov 2025 08:40:15 -0800 Subject: [PATCH 01/11] feat(search): Add canViewEntityPage permission flag field to search results --- .../graphql/types/mappers/MapperUtils.java | 36 ++++++++++++++++--- .../src/main/resources/search.graphql | 6 ++++ .../src/app/entity/shared/types.ts | 1 + datahub-web-react/src/graphql/search.graphql | 1 + 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java index f747ba7ee4bd2..e2c053b7364da 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java @@ -3,6 +3,7 @@ import static com.linkedin.datahub.graphql.util.SearchInsightsUtil.*; import static com.linkedin.metadata.utils.SearchUtil.*; +import com.datahub.authorization.EntitySpec; import com.linkedin.common.AuditStamp; import com.linkedin.common.UrnArray; import com.linkedin.common.urn.Urn; @@ -37,11 +38,36 @@ private MapperUtils() {} public static SearchResult mapResult( @Nullable final QueryContext context, SearchEntity searchEntity) { - return new SearchResult( - UrnToEntityMapper.map(context, searchEntity.getEntity()), - getInsightsFromFeatures(searchEntity.getFeatures()), - getMatchedFieldEntry(context, searchEntity.getMatchedFields()), - getExtraProperties(searchEntity.getExtraFields())); + SearchResult result = + new SearchResult( + UrnToEntityMapper.map(context, searchEntity.getEntity()), + getInsightsFromFeatures(searchEntity.getFeatures()), + getMatchedFieldEntry(context, searchEntity.getMatchedFields()), + getExtraProperties(searchEntity.getExtraFields()), + null); // Initialize canViewEntityPage as null + + // Check if the user can view the entity page + if (context != null) { + try { + Urn entityUrn = searchEntity.getEntity(); + EntitySpec entitySpec = new EntitySpec(entityUrn.getEntityType(), entityUrn.toString()); + // Use the authorize method from OperationContext + boolean canView = + context.getOperationContext().authorize("VIEW_ENTITY_PAGE", entitySpec).getType() + == com.datahub.authorization.AuthorizationResult.Type.ALLOW; + result.setCanViewEntityPage(canView); + } catch (Exception e) { + log.warn( + "Failed to check VIEW_ENTITY_PAGE permission for entity {}", + searchEntity.getEntity(), + e); + result.setCanViewEntityPage(true); // Default to true if permission check fails + } + } else { + result.setCanViewEntityPage(true); // Default to true if no context + } + + return result; } private static List getExtraProperties(@Nullable StringMap extraFields) { diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index ba1288da048f1..e5fabf08849c5 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -692,6 +692,12 @@ type SearchResult { Additional properties about the search result. Used for rendering in the UI """ extraProperties: [ExtraProperty!] + + """ + Whether the current user can view the entity page for this result + """ + canViewEntityPage: Boolean + } """ diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index eda056f17efa5..2e13b7fad5191 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -208,4 +208,5 @@ export type RequiredAndNotNull = { export type EntityAndType = { urn: string; type: EntityType; + canViewEntityPage?: boolean | null; }; diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index 8012978ed977f..93f415340db8b 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -1296,6 +1296,7 @@ fragment searchResults on SearchResults { text icon } + canViewEntityPage } facets { ...facetFields From 84796a0f7da014c5b9447b7bee3cc88e179f0f71 Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Mon, 3 Nov 2025 09:18:08 -0800 Subject: [PATCH 02/11] linter happy --- datahub-graphql-core/src/main/resources/search.graphql | 1 - 1 file changed, 1 deletion(-) diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index e5fabf08849c5..cd0e964b42cba 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -697,7 +697,6 @@ type SearchResult { Whether the current user can view the entity page for this result """ canViewEntityPage: Boolean - } """ From ee88f39ee730ae1a50a071f95e6ec47222b1404e Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Mon, 3 Nov 2025 11:02:01 -0800 Subject: [PATCH 03/11] add canViewEntityPage value to mocks --- datahub-web-react/src/Mocks.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 774c6cc79eabf..74e66bf327987 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -2332,6 +2332,7 @@ export const mocks = [ entity: { ...dataset1, }, + canViewEntityPage: true, matchedFields: [ { name: 'fieldName', @@ -2344,12 +2345,14 @@ export const mocks = [ entity: { ...dataset2, }, + canViewEntityPage: true, }, { entity: { __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, }, ], facets: [ @@ -2425,6 +2428,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -2497,6 +2501,7 @@ export const mocks = [ __typename: 'GLOSSARY_TERM', ...glossaryTerm1, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -2605,6 +2610,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -2779,6 +2785,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -2848,6 +2855,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -2856,6 +2864,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -2925,6 +2934,7 @@ export const mocks = [ __typename: 'DataFlow', ...dataFlow1, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3071,6 +3081,7 @@ export const mocks = [ __typename: 'DataJob', ...dataJob1, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3167,6 +3178,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3287,6 +3299,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3337,6 +3350,7 @@ export const mocks = [ __typename: 'DataJob', ...dataJob1, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3414,6 +3428,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3496,6 +3511,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3504,6 +3520,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3593,6 +3610,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3686,6 +3704,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3957,6 +3976,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -3965,6 +3985,7 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, + canViewEntityPage: true, matchedFields: [], insights: [], }, @@ -4360,6 +4381,7 @@ export const mockSearchResult: SearchResult = { __typename: 'Dataset', ...dataset3, }, + canViewEntityPage: true, matchedFields: [], insights: [], extraProperties: [ @@ -4385,7 +4407,9 @@ export const mockFineGrainedLineages1: GenericEntityProperties = { siblingsSearch: { count: 1, total: 1, - searchResults: [{ entity: { type: EntityType.Dataset, urn: 'test_urn' }, matchedFields: [] }], + searchResults: [ + { entity: { type: EntityType.Dataset, urn: 'test_urn' }, matchedFields: [], canViewEntityPage: true }, + ], }, fineGrainedLineages: [ { From 4a3a60cbd315d8c4674999bf613c55ec54b599d2 Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Tue, 4 Nov 2025 07:58:53 -0800 Subject: [PATCH 04/11] boost test coverage for MapperUtils --- .../types/mappers/MapperUtilsTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java index d8eef4ecc2315..12f96bc53faaf 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java @@ -1,5 +1,9 @@ package com.linkedin.datahub.graphql.types.mappers; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; @@ -8,10 +12,14 @@ import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.TestUtils; import com.linkedin.datahub.graphql.generated.MatchedField; +import com.linkedin.datahub.graphql.generated.SearchResult; import com.linkedin.metadata.entity.validation.ValidationApiUtils; import com.linkedin.metadata.models.registry.ConfigEntityRegistry; import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.search.MatchedFieldArray; +import com.linkedin.metadata.search.SearchEntity; import com.linkedin.metadata.snapshot.Snapshot; +import io.datahubproject.metadata.context.OperationContext; import java.net.URISyntaxException; import java.util.List; import org.testng.annotations.BeforeTest; @@ -58,6 +66,48 @@ public void testMatchedFieldValidation() throws URISyntaxException { "With urn should be 1"); } + @Test + public void testMapResultDefaultsCanViewEntityPageWithoutContext() throws URISyntaxException { + SearchEntity searchEntity = buildSearchEntity(); + + SearchResult result = MapperUtils.mapResult(null, searchEntity); + + assertEquals(result.getCanViewEntityPage(), Boolean.TRUE); + } + + @Test + public void testMapResultSetsCanViewEntityPageWhenAuthorized() throws URISyntaxException { + QueryContext context = TestUtils.getMockAllowContext(); + SearchEntity searchEntity = buildSearchEntity(); + + SearchResult result = MapperUtils.mapResult(context, searchEntity); + + assertEquals(result.getCanViewEntityPage(), Boolean.TRUE); + } + + @Test + public void testMapResultSetsCanViewEntityPageWhenUnauthorized() throws URISyntaxException { + QueryContext context = TestUtils.getMockDenyContext(); + SearchEntity searchEntity = buildSearchEntity(); + + SearchResult result = MapperUtils.mapResult(context, searchEntity); + + assertEquals(result.getCanViewEntityPage(), Boolean.FALSE); + } + + @Test + public void testMapResultDefaultsCanViewEntityPageOnFailure() throws URISyntaxException { + QueryContext context = mock(QueryContext.class); + OperationContext operationContext = mock(OperationContext.class); + when(context.getOperationContext()).thenReturn(operationContext); + when(operationContext.authorize(eq("VIEW_ENTITY_PAGE"), any())).thenThrow(new RuntimeException("boom")); + SearchEntity searchEntity = buildSearchEntity(); + + SearchResult result = MapperUtils.mapResult(context, searchEntity); + + assertEquals(result.getCanViewEntityPage(), Boolean.TRUE); + } + private static com.linkedin.metadata.search.MatchedField buildSearchMatchField( String highlightValue) { com.linkedin.metadata.search.MatchedField field = @@ -66,4 +116,12 @@ private static com.linkedin.metadata.search.MatchedField buildSearchMatchField( field.setValue(highlightValue); return field; } + + private static SearchEntity buildSearchEntity() throws URISyntaxException { + SearchEntity searchEntity = new SearchEntity(); + searchEntity.setEntity( + Urn.createFromString("urn:li:dataset:(urn:li:dataPlatform:s3,testDataset,PROD)")); + searchEntity.setMatchedFields(new MatchedFieldArray()); + return searchEntity; + } } From 4271055ecc8bebf59f59be9b045c33c9feab392e Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Tue, 4 Nov 2025 08:27:47 -0800 Subject: [PATCH 05/11] linter green for MapperUtilsTest --- .../datahub/graphql/types/mappers/MapperUtilsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java index 12f96bc53faaf..c26f15b03ba9a 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java @@ -100,7 +100,8 @@ public void testMapResultDefaultsCanViewEntityPageOnFailure() throws URISyntaxEx QueryContext context = mock(QueryContext.class); OperationContext operationContext = mock(OperationContext.class); when(context.getOperationContext()).thenReturn(operationContext); - when(operationContext.authorize(eq("VIEW_ENTITY_PAGE"), any())).thenThrow(new RuntimeException("boom")); + when(operationContext.authorize(eq("VIEW_ENTITY_PAGE"), any())) + .thenThrow(new RuntimeException("boom")); SearchEntity searchEntity = buildSearchEntity(); SearchResult result = MapperUtils.mapResult(context, searchEntity); From 74877affe12ffd72e6d6462ff44466374db01519 Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Mon, 10 Nov 2025 07:21:04 -0800 Subject: [PATCH 06/11] address PR feeback to remove canViewEntityPage into extraProperties --- .../graphql/types/mappers/MapperUtils.java | 36 ++--------- .../src/main/resources/search.graphql | 5 -- .../types/mappers/MapperUtilsTest.java | 49 -------------- datahub-web-react/src/graphql/search.graphql | 5 +- .../search/utils/ESAccessControlUtil.java | 64 +++++++++++++++---- 5 files changed, 61 insertions(+), 98 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java index e2c053b7364da..f747ba7ee4bd2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java @@ -3,7 +3,6 @@ import static com.linkedin.datahub.graphql.util.SearchInsightsUtil.*; import static com.linkedin.metadata.utils.SearchUtil.*; -import com.datahub.authorization.EntitySpec; import com.linkedin.common.AuditStamp; import com.linkedin.common.UrnArray; import com.linkedin.common.urn.Urn; @@ -38,36 +37,11 @@ private MapperUtils() {} public static SearchResult mapResult( @Nullable final QueryContext context, SearchEntity searchEntity) { - SearchResult result = - new SearchResult( - UrnToEntityMapper.map(context, searchEntity.getEntity()), - getInsightsFromFeatures(searchEntity.getFeatures()), - getMatchedFieldEntry(context, searchEntity.getMatchedFields()), - getExtraProperties(searchEntity.getExtraFields()), - null); // Initialize canViewEntityPage as null - - // Check if the user can view the entity page - if (context != null) { - try { - Urn entityUrn = searchEntity.getEntity(); - EntitySpec entitySpec = new EntitySpec(entityUrn.getEntityType(), entityUrn.toString()); - // Use the authorize method from OperationContext - boolean canView = - context.getOperationContext().authorize("VIEW_ENTITY_PAGE", entitySpec).getType() - == com.datahub.authorization.AuthorizationResult.Type.ALLOW; - result.setCanViewEntityPage(canView); - } catch (Exception e) { - log.warn( - "Failed to check VIEW_ENTITY_PAGE permission for entity {}", - searchEntity.getEntity(), - e); - result.setCanViewEntityPage(true); // Default to true if permission check fails - } - } else { - result.setCanViewEntityPage(true); // Default to true if no context - } - - return result; + return new SearchResult( + UrnToEntityMapper.map(context, searchEntity.getEntity()), + getInsightsFromFeatures(searchEntity.getFeatures()), + getMatchedFieldEntry(context, searchEntity.getMatchedFields()), + getExtraProperties(searchEntity.getExtraFields())); } private static List getExtraProperties(@Nullable StringMap extraFields) { diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index cd0e964b42cba..ba1288da048f1 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -692,11 +692,6 @@ type SearchResult { Additional properties about the search result. Used for rendering in the UI """ extraProperties: [ExtraProperty!] - - """ - Whether the current user can view the entity page for this result - """ - canViewEntityPage: Boolean } """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java index c26f15b03ba9a..f1afc2855c052 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java @@ -1,9 +1,5 @@ package com.linkedin.datahub.graphql.types.mappers; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; @@ -12,14 +8,12 @@ import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.TestUtils; import com.linkedin.datahub.graphql.generated.MatchedField; -import com.linkedin.datahub.graphql.generated.SearchResult; import com.linkedin.metadata.entity.validation.ValidationApiUtils; import com.linkedin.metadata.models.registry.ConfigEntityRegistry; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.MatchedFieldArray; import com.linkedin.metadata.search.SearchEntity; import com.linkedin.metadata.snapshot.Snapshot; -import io.datahubproject.metadata.context.OperationContext; import java.net.URISyntaxException; import java.util.List; import org.testng.annotations.BeforeTest; @@ -66,49 +60,6 @@ public void testMatchedFieldValidation() throws URISyntaxException { "With urn should be 1"); } - @Test - public void testMapResultDefaultsCanViewEntityPageWithoutContext() throws URISyntaxException { - SearchEntity searchEntity = buildSearchEntity(); - - SearchResult result = MapperUtils.mapResult(null, searchEntity); - - assertEquals(result.getCanViewEntityPage(), Boolean.TRUE); - } - - @Test - public void testMapResultSetsCanViewEntityPageWhenAuthorized() throws URISyntaxException { - QueryContext context = TestUtils.getMockAllowContext(); - SearchEntity searchEntity = buildSearchEntity(); - - SearchResult result = MapperUtils.mapResult(context, searchEntity); - - assertEquals(result.getCanViewEntityPage(), Boolean.TRUE); - } - - @Test - public void testMapResultSetsCanViewEntityPageWhenUnauthorized() throws URISyntaxException { - QueryContext context = TestUtils.getMockDenyContext(); - SearchEntity searchEntity = buildSearchEntity(); - - SearchResult result = MapperUtils.mapResult(context, searchEntity); - - assertEquals(result.getCanViewEntityPage(), Boolean.FALSE); - } - - @Test - public void testMapResultDefaultsCanViewEntityPageOnFailure() throws URISyntaxException { - QueryContext context = mock(QueryContext.class); - OperationContext operationContext = mock(OperationContext.class); - when(context.getOperationContext()).thenReturn(operationContext); - when(operationContext.authorize(eq("VIEW_ENTITY_PAGE"), any())) - .thenThrow(new RuntimeException("boom")); - SearchEntity searchEntity = buildSearchEntity(); - - SearchResult result = MapperUtils.mapResult(context, searchEntity); - - assertEquals(result.getCanViewEntityPage(), Boolean.TRUE); - } - private static com.linkedin.metadata.search.MatchedField buildSearchMatchField( String highlightValue) { com.linkedin.metadata.search.MatchedField field = diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index f3ac09026b9d4..4057c19437dc1 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -1299,7 +1299,10 @@ fragment searchResults on SearchResults { text icon } - canViewEntityPage + extraProperties { + name + value + } } facets { ...facetFields diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java index d1895de305548..edc7631ddd340 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java @@ -3,8 +3,10 @@ import static com.datahub.authorization.AuthUtil.VIEW_RESTRICTED_ENTITY_TYPES; import com.datahub.authorization.AuthUtil; +import com.datahub.authorization.EntitySpec; import com.linkedin.common.urn.Urn; import com.linkedin.data.template.StringArray; +import com.linkedin.data.template.StringMap; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.SearchEntity; import com.linkedin.metadata.search.SearchResult; @@ -39,31 +41,69 @@ public static Collection restrictSearchResult( final RestrictedService restrictedService = Objects.requireNonNull(opContext.getServicesRegistryContext()).getRestrictedService(); - if (opContext.getSearchContext().isRestrictedSearch()) { - for (SearchEntity searchEntity : searchEntities) { - final String entityType = searchEntity.getEntity().getEntityType(); - final com.linkedin.metadata.models.EntitySpec entitySpec = - entityRegistry.getEntitySpec(entityType); + for (SearchEntity searchEntity : searchEntities) { + final String entityType = searchEntity.getEntity().getEntityType(); + final com.linkedin.metadata.models.EntitySpec entitySpec = + entityRegistry.getEntitySpec(entityType); + // Add canViewEntityPage to extraFields + addCanViewEntityPageField(opContext, searchEntity); + + if (opContext.getSearchContext().isRestrictedSearch()) { if (VIEW_RESTRICTED_ENTITY_TYPES.contains(entityType) && !AuthUtil.canViewEntity(opContext, searchEntity.getEntity())) { // Not authorized && restricted response requested - if (opContext.getSearchContext().isRestrictedSearch()) { - // Restrict entity - searchEntity.setRestrictedAspects( - new StringArray(List.of(entitySpec.getKeyAspectName()))); + // Restrict entity + searchEntity.setRestrictedAspects( + new StringArray(List.of(entitySpec.getKeyAspectName()))); - searchEntity.setEntity( - restrictedService.encryptRestrictedUrn(searchEntity.getEntity())); - } + searchEntity.setEntity( + restrictedService.encryptRestrictedUrn(searchEntity.getEntity())); } } } + } else { + for (SearchEntity searchEntity : searchEntities) { + addDefaultCanViewEntityPageField(searchEntity); + } } return searchEntities; } + private static void addCanViewEntityPageField( + @Nonnull OperationContext opContext, @Nonnull SearchEntity searchEntity) { + try { + Urn entityUrn = searchEntity.getEntity(); + EntitySpec entitySpec = new EntitySpec(entityUrn.getEntityType(), entityUrn.toString()); + boolean canView = + opContext.authorize("VIEW_ENTITY_PAGE", entitySpec).getType() + == com.datahub.authorization.AuthorizationResult.Type.ALLOW; + + StringMap extraFields = searchEntity.getExtraFields(); + if (extraFields == null) { + extraFields = new StringMap(); + } + extraFields.put("canViewEntityPage", String.valueOf(canView)); + searchEntity.setExtraFields(extraFields); + } catch (Exception e) { + log.warn( + "Failed to check VIEW_ENTITY_PAGE permission for entity {}, defaulting to true", + searchEntity.getEntity(), + e); + addDefaultCanViewEntityPageField(searchEntity); + } + } + + private static void addDefaultCanViewEntityPageField(@Nonnull SearchEntity searchEntity) { + StringMap extraFields = searchEntity.getExtraFields(); + if (extraFields == null) { + extraFields = new StringMap(); + } + extraFields.put("canViewEntityPage", "true"); + searchEntity.setExtraFields(extraFields); + } + public static boolean restrictUrn(@Nonnull OperationContext opContext, @Nonnull Urn urn) { if (opContext.getOperationContextConfig().getViewAuthorizationConfiguration().isEnabled() && !opContext.isSystemAuth()) { From cb15a107098dc997d947f8b87d884b22010f5709 Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Mon, 10 Nov 2025 07:35:08 -0800 Subject: [PATCH 07/11] update Mocks.tsx and clean up MapperUtilsText --- .../types/mappers/MapperUtilsTest.java | 10 - datahub-web-react/src/Mocks.tsx | 178 +++++++++++++++--- 2 files changed, 155 insertions(+), 33 deletions(-) diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java index f1afc2855c052..d8eef4ecc2315 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/mappers/MapperUtilsTest.java @@ -11,8 +11,6 @@ import com.linkedin.metadata.entity.validation.ValidationApiUtils; import com.linkedin.metadata.models.registry.ConfigEntityRegistry; import com.linkedin.metadata.models.registry.EntityRegistry; -import com.linkedin.metadata.search.MatchedFieldArray; -import com.linkedin.metadata.search.SearchEntity; import com.linkedin.metadata.snapshot.Snapshot; import java.net.URISyntaxException; import java.util.List; @@ -68,12 +66,4 @@ private static com.linkedin.metadata.search.MatchedField buildSearchMatchField( field.setValue(highlightValue); return field; } - - private static SearchEntity buildSearchEntity() throws URISyntaxException { - SearchEntity searchEntity = new SearchEntity(); - searchEntity.setEntity( - Urn.createFromString("urn:li:dataset:(urn:li:dataPlatform:s3,testDataset,PROD)")); - searchEntity.setMatchedFields(new MatchedFieldArray()); - return searchEntity; - } } diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 74e66bf327987..2a96e4cb09b08 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -2332,7 +2332,13 @@ export const mocks = [ entity: { ...dataset1, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [ { name: 'fieldName', @@ -2345,14 +2351,26 @@ export const mocks = [ entity: { ...dataset2, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], }, { entity: { __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], }, ], facets: [ @@ -2428,7 +2446,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -2501,7 +2525,13 @@ export const mocks = [ __typename: 'GLOSSARY_TERM', ...glossaryTerm1, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -2610,7 +2640,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -2785,7 +2821,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -2855,7 +2897,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -2864,7 +2912,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -2934,7 +2988,13 @@ export const mocks = [ __typename: 'DataFlow', ...dataFlow1, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3081,7 +3141,13 @@ export const mocks = [ __typename: 'DataJob', ...dataJob1, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3178,7 +3244,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3299,7 +3371,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3350,7 +3428,13 @@ export const mocks = [ __typename: 'DataJob', ...dataJob1, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3428,7 +3512,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3511,7 +3601,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3520,7 +3616,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3610,7 +3712,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3704,7 +3812,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3976,7 +4090,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -3985,7 +4105,13 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, - canViewEntityPage: true, + extraProperties: [ + { + name: 'canViewEntityPage', + value: 'true', + __typename: 'ExtraProperty', + }, + ], matchedFields: [], insights: [], }, @@ -4381,10 +4507,10 @@ export const mockSearchResult: SearchResult = { __typename: 'Dataset', ...dataset3, }, - canViewEntityPage: true, matchedFields: [], insights: [], extraProperties: [ + { name: 'canViewEntityPage', value: 'true', __typename: 'ExtraProperty' }, { name: 'isOutputPort', value: 'true' }, { name: 'test2_name', value: 'test2_value' }, ], @@ -4408,7 +4534,13 @@ export const mockFineGrainedLineages1: GenericEntityProperties = { count: 1, total: 1, searchResults: [ - { entity: { type: EntityType.Dataset, urn: 'test_urn' }, matchedFields: [], canViewEntityPage: true }, + { + entity: { type: EntityType.Dataset, urn: 'test_urn' }, + matchedFields: [], + extraProperties: [ + { name: 'canViewEntityPage', value: 'true', __typename: 'ExtraProperty' }, + ], + }, ], }, fineGrainedLineages: [ From 005d1dd12c21f7fb7ae871152fe8a3fed26aadbd Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Mon, 10 Nov 2025 08:26:08 -0800 Subject: [PATCH 08/11] linter happy --- datahub-web-react/src/Mocks.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 2a96e4cb09b08..67c5bc78c97c9 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -4537,9 +4537,7 @@ export const mockFineGrainedLineages1: GenericEntityProperties = { { entity: { type: EntityType.Dataset, urn: 'test_urn' }, matchedFields: [], - extraProperties: [ - { name: 'canViewEntityPage', value: 'true', __typename: 'ExtraProperty' }, - ], + extraProperties: [{ name: 'canViewEntityPage', value: 'true', __typename: 'ExtraProperty' }], }, ], }, From 0b4d99552b25b57044c42e8a85b21edd21ff2b94 Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Wed, 12 Nov 2025 07:49:44 -0800 Subject: [PATCH 09/11] add canViewEntityPage to EntityPrivileges --- .../authorization/AuthorizationUtils.java | 8 + .../entity/EntityPrivilegesResolver.java | 1 + .../src/main/resources/auth.graphql | 5 + datahub-web-react/src/Mocks.tsx | 158 +----------------- .../src/app/entity/shared/types.ts | 1 - datahub-web-react/src/graphql/search.graphql | 4 - .../search/utils/ESAccessControlUtil.java | 64 ++----- .../authorization/PoliciesConfig.java | 2 +- 8 files changed, 29 insertions(+), 214 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java index 6e33046684a8f..f78eaaff2c172 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java @@ -451,6 +451,14 @@ public static boolean isViewDatasetOperationsAuthorized( new EntitySpec(resourceUrn.getEntityType(), resourceUrn.toString())); } + public static boolean isViewEntityPageAuthorized( + final QueryContext context, final Urn resourceUrn) { + return AuthUtil.isAuthorized( + context.getOperationContext(), + PoliciesConfig.VIEW_ENTITY_PAGE_PRIVILEGE, + new EntitySpec(resourceUrn.getEntityType(), resourceUrn.toString())); + } + public static boolean canManageAssetSummary(@Nonnull QueryContext context, @Nonnull Urn urn) { final DisjunctivePrivilegeGroup orPrivilegeGroups = new DisjunctivePrivilegeGroup( diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolver.java index f26e2c9258a57..a3d0894b971f4 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolver.java @@ -182,5 +182,6 @@ private void addCommonPrivileges( result.setCanEditDescription(DescriptionUtils.isAuthorizedToUpdateDescription(context, urn)); result.setCanEditLinks(LinkUtils.isAuthorizedToUpdateLinks(context, urn)); result.setCanManageAssetSummary(AuthorizationUtils.canManageAssetSummary(context, urn)); + result.setCanViewEntityPage(AuthorizationUtils.isViewEntityPageAuthorized(context, urn)); } } diff --git a/datahub-graphql-core/src/main/resources/auth.graphql b/datahub-graphql-core/src/main/resources/auth.graphql index 667a8506f5946..ac13f284693f5 100644 --- a/datahub-graphql-core/src/main/resources/auth.graphql +++ b/datahub-graphql-core/src/main/resources/auth.graphql @@ -368,6 +368,11 @@ type EntityPrivileges { Whether the user can manage asset summary """ canManageAssetSummary: Boolean + + """ + Whether the user can view the entity page + """ + canViewEntityPage: Boolean } """ diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 575ff4b4efcb1..f554155e2c5e0 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -2335,13 +2335,6 @@ export const mocks = [ entity: { ...dataset1, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [ { name: 'fieldName', @@ -2354,26 +2347,12 @@ export const mocks = [ entity: { ...dataset2, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], }, { entity: { __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], }, ], facets: [ @@ -2449,13 +2428,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -2528,13 +2500,6 @@ export const mocks = [ __typename: 'GLOSSARY_TERM', ...glossaryTerm1, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -2643,13 +2608,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -2824,13 +2782,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -2900,13 +2851,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -2915,13 +2859,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -2991,13 +2928,6 @@ export const mocks = [ __typename: 'DataFlow', ...dataFlow1, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3144,13 +3074,6 @@ export const mocks = [ __typename: 'DataJob', ...dataJob1, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3247,13 +3170,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3374,13 +3290,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3431,13 +3340,6 @@ export const mocks = [ __typename: 'DataJob', ...dataJob1, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3515,13 +3417,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3604,13 +3499,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3619,13 +3507,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3715,13 +3596,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -3815,13 +3689,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -4093,13 +3960,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset3, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -4108,13 +3968,6 @@ export const mocks = [ __typename: 'Dataset', ...dataset4, }, - extraProperties: [ - { - name: 'canViewEntityPage', - value: 'true', - __typename: 'ExtraProperty', - }, - ], matchedFields: [], insights: [], }, @@ -4513,7 +4366,6 @@ export const mockSearchResult: SearchResult = { matchedFields: [], insights: [], extraProperties: [ - { name: 'canViewEntityPage', value: 'true', __typename: 'ExtraProperty' }, { name: 'isOutputPort', value: 'true' }, { name: 'test2_name', value: 'test2_value' }, ], @@ -4536,13 +4388,7 @@ export const mockFineGrainedLineages1: GenericEntityProperties = { siblingsSearch: { count: 1, total: 1, - searchResults: [ - { - entity: { type: EntityType.Dataset, urn: 'test_urn' }, - matchedFields: [], - extraProperties: [{ name: 'canViewEntityPage', value: 'true', __typename: 'ExtraProperty' }], - }, - ], + searchResults: [{ entity: { type: EntityType.Dataset, urn: 'test_urn' }, matchedFields: [] }], }, fineGrainedLineages: [ { @@ -4582,4 +4428,4 @@ export const useEntityDataFunc = () => { }, }; return value; -}; +}; \ No newline at end of file diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index 2e13b7fad5191..eda056f17efa5 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -208,5 +208,4 @@ export type RequiredAndNotNull = { export type EntityAndType = { urn: string; type: EntityType; - canViewEntityPage?: boolean | null; }; diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index e60e2415d2973..f9e09ab01bd1b 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -1302,10 +1302,6 @@ fragment searchResults on SearchResults { text icon } - extraProperties { - name - value - } } facets { ...facetFields diff --git a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java index edc7631ddd340..d1895de305548 100644 --- a/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java +++ b/metadata-io/src/main/java/com/linkedin/metadata/search/utils/ESAccessControlUtil.java @@ -3,10 +3,8 @@ import static com.datahub.authorization.AuthUtil.VIEW_RESTRICTED_ENTITY_TYPES; import com.datahub.authorization.AuthUtil; -import com.datahub.authorization.EntitySpec; import com.linkedin.common.urn.Urn; import com.linkedin.data.template.StringArray; -import com.linkedin.data.template.StringMap; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.search.SearchEntity; import com.linkedin.metadata.search.SearchResult; @@ -41,69 +39,31 @@ public static Collection restrictSearchResult( final RestrictedService restrictedService = Objects.requireNonNull(opContext.getServicesRegistryContext()).getRestrictedService(); - for (SearchEntity searchEntity : searchEntities) { - final String entityType = searchEntity.getEntity().getEntityType(); - final com.linkedin.metadata.models.EntitySpec entitySpec = - entityRegistry.getEntitySpec(entityType); + if (opContext.getSearchContext().isRestrictedSearch()) { + for (SearchEntity searchEntity : searchEntities) { + final String entityType = searchEntity.getEntity().getEntityType(); + final com.linkedin.metadata.models.EntitySpec entitySpec = + entityRegistry.getEntitySpec(entityType); - // Add canViewEntityPage to extraFields - addCanViewEntityPageField(opContext, searchEntity); - - if (opContext.getSearchContext().isRestrictedSearch()) { if (VIEW_RESTRICTED_ENTITY_TYPES.contains(entityType) && !AuthUtil.canViewEntity(opContext, searchEntity.getEntity())) { // Not authorized && restricted response requested - // Restrict entity - searchEntity.setRestrictedAspects( - new StringArray(List.of(entitySpec.getKeyAspectName()))); + if (opContext.getSearchContext().isRestrictedSearch()) { + // Restrict entity + searchEntity.setRestrictedAspects( + new StringArray(List.of(entitySpec.getKeyAspectName()))); - searchEntity.setEntity( - restrictedService.encryptRestrictedUrn(searchEntity.getEntity())); + searchEntity.setEntity( + restrictedService.encryptRestrictedUrn(searchEntity.getEntity())); + } } } } - } else { - for (SearchEntity searchEntity : searchEntities) { - addDefaultCanViewEntityPageField(searchEntity); - } } return searchEntities; } - private static void addCanViewEntityPageField( - @Nonnull OperationContext opContext, @Nonnull SearchEntity searchEntity) { - try { - Urn entityUrn = searchEntity.getEntity(); - EntitySpec entitySpec = new EntitySpec(entityUrn.getEntityType(), entityUrn.toString()); - boolean canView = - opContext.authorize("VIEW_ENTITY_PAGE", entitySpec).getType() - == com.datahub.authorization.AuthorizationResult.Type.ALLOW; - - StringMap extraFields = searchEntity.getExtraFields(); - if (extraFields == null) { - extraFields = new StringMap(); - } - extraFields.put("canViewEntityPage", String.valueOf(canView)); - searchEntity.setExtraFields(extraFields); - } catch (Exception e) { - log.warn( - "Failed to check VIEW_ENTITY_PAGE permission for entity {}, defaulting to true", - searchEntity.getEntity(), - e); - addDefaultCanViewEntityPageField(searchEntity); - } - } - - private static void addDefaultCanViewEntityPageField(@Nonnull SearchEntity searchEntity) { - StringMap extraFields = searchEntity.getExtraFields(); - if (extraFields == null) { - extraFields = new StringMap(); - } - extraFields.put("canViewEntityPage", "true"); - searchEntity.setExtraFields(extraFields); - } - public static boolean restrictUrn(@Nonnull OperationContext opContext, @Nonnull Urn urn) { if (opContext.getOperationContextConfig().getViewAuthorizationConfiguration().isEnabled() && !opContext.isSystemAuth()) { diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index 3e7de795aa924..d365326bed0d6 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -267,7 +267,7 @@ public class PoliciesConfig { // Resource Privileges // - static final Privilege VIEW_ENTITY_PAGE_PRIVILEGE = + public static final Privilege VIEW_ENTITY_PAGE_PRIVILEGE = Privilege.of("VIEW_ENTITY_PAGE", "View Entity Page", "The ability to view the entity page."); static final Privilege EXISTS_ENTITY_PRIVILEGE = From 540d5059404d16ef805fef42f19f9461bd977fef Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Wed, 12 Nov 2025 07:51:04 -0800 Subject: [PATCH 10/11] cleanup --- datahub-web-react/src/Mocks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index f554155e2c5e0..b5a0c71611e89 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -4428,4 +4428,4 @@ export const useEntityDataFunc = () => { }, }; return value; -}; \ No newline at end of file +}; From 3f10e0221e49b021b1302332da53054db59e350e Mon Sep 17 00:00:00 2001 From: Seun Animashaun Date: Wed, 12 Nov 2025 08:48:33 -0800 Subject: [PATCH 11/11] test cases for EntityPrivilegesResolver --- .../entity/EntityPrivilegesResolverTest.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolverTest.java index 04b9a1a3dcd00..7441f06b5ce30 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolverTest.java @@ -238,4 +238,60 @@ public void testGetDataJobSuccessWithoutPermissions() throws Exception { assertFalse(result.getCanEditLineage()); } + + @Test + public void testCanViewEntityPageWithPermissions() throws Exception { + final Dataset dataset = new Dataset(); + dataset.setUrn(datasetUrn); + + EntityClient mockClient = Mockito.mock(EntityClient.class); + DataFetchingEnvironment mockEnv = setUpTestWithPermissions(dataset); + + EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient); + EntityPrivileges result = resolver.get(mockEnv).get(); + + assertTrue(result.getCanViewEntityPage()); + } + + @Test + public void testCanViewEntityPageWithoutPermissions() throws Exception { + final Dataset dataset = new Dataset(); + dataset.setUrn(datasetUrn); + + EntityClient mockClient = Mockito.mock(EntityClient.class); + DataFetchingEnvironment mockEnv = setUpTestWithoutPermissions(dataset); + + EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient); + EntityPrivileges result = resolver.get(mockEnv).get(); + + assertFalse(result.getCanViewEntityPage()); + } + + @Test + public void testCanViewEntityPageForChartWithPermissions() throws Exception { + final Chart chart = new Chart(); + chart.setUrn(chartUrn); + + EntityClient mockClient = Mockito.mock(EntityClient.class); + DataFetchingEnvironment mockEnv = setUpTestWithPermissions(chart); + + EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient); + EntityPrivileges result = resolver.get(mockEnv).get(); + + assertTrue(result.getCanViewEntityPage()); + } + + @Test + public void testCanViewEntityPageForChartWithoutPermissions() throws Exception { + final Chart chart = new Chart(); + chart.setUrn(chartUrn); + + EntityClient mockClient = Mockito.mock(EntityClient.class); + DataFetchingEnvironment mockEnv = setUpTestWithoutPermissions(chart); + + EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient); + EntityPrivileges result = resolver.get(mockEnv).get(); + + assertFalse(result.getCanViewEntityPage()); + } }