diff --git a/.github/workflows/check-datahub-jars.yml b/.github/workflows/check-datahub-jars.yml index 735ea1e56b169..841a9ed5f9bc7 100644 --- a/.github/workflows/check-datahub-jars.yml +++ b/.github/workflows/check-datahub-jars.yml @@ -46,7 +46,7 @@ jobs: java-version: 11 - uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.10" - name: check ${{ matrix.command }} jar run: | ./gradlew :metadata-integration:java:${{ matrix.command }}:build --info diff --git a/.github/workflows/docker-unified.yml b/.github/workflows/docker-unified.yml index 963155ef09b1c..31fead8a7ade6 100644 --- a/.github/workflows/docker-unified.yml +++ b/.github/workflows/docker-unified.yml @@ -743,7 +743,7 @@ jobs: java-version: 11 - uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.10" cache: "pip" - name: Install dependencies run: ./metadata-ingestion/scripts/install_deps.sh diff --git a/.github/workflows/metadata-io.yml b/.github/workflows/metadata-io.yml index 473a73adba9a2..e37ddd0ce4e86 100644 --- a/.github/workflows/metadata-io.yml +++ b/.github/workflows/metadata-io.yml @@ -36,7 +36,7 @@ jobs: java-version: 11 - uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.10" - name: Gradle build (and test) # there is some race condition in gradle build, which makes gradle never terminate in ~30% of the runs # running build first without datahub-web-react:yarnBuild and then with it is 100% stable diff --git a/.github/workflows/publish-datahub-jars.yml b/.github/workflows/publish-datahub-jars.yml index 1a1523e8db1a1..7cd07b130dd80 100644 --- a/.github/workflows/publish-datahub-jars.yml +++ b/.github/workflows/publish-datahub-jars.yml @@ -58,7 +58,7 @@ jobs: java-version: 11 - uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.10" - name: checkout upstream repo run: | git remote add upstream https://github.com/datahub-project/datahub.git diff --git a/.github/workflows/spark-smoke-test.yml b/.github/workflows/spark-smoke-test.yml index 96b85826cac4f..b2482602e7548 100644 --- a/.github/workflows/spark-smoke-test.yml +++ b/.github/workflows/spark-smoke-test.yml @@ -40,7 +40,7 @@ jobs: java-version: 11 - uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.10" - name: Install dependencies run: ./metadata-ingestion/scripts/install_deps.sh - name: Remove images diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java index f813562945378..de3c217db01ec 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java @@ -15,4 +15,5 @@ public class FeatureFlags { private boolean showBrowseV2 = false; private PreProcessHooks preProcessHooks; private boolean showAcrylInfo = false; + private boolean showAccessManagement = false; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java index 90017f7b87997..09df985b19cf5 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java @@ -171,6 +171,7 @@ public CompletableFuture get(final DataFetchingEnvironment environmen .setReadOnlyModeEnabled(_featureFlags.isReadOnlyModeEnabled()) .setShowBrowseV2(_featureFlags.isShowBrowseV2()) .setShowAcrylInfo(_featureFlags.isShowAcrylInfo()) + .setShowAccessManagement(_featureFlags.isShowAccessManagement()) .build(); appConfig.setFeatureFlags(featureFlagsConfig); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java index fe5b79ba2ea3d..fb146ef72877d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java @@ -68,6 +68,7 @@ private SearchUtils() { EntityType.GLOSSARY_TERM, EntityType.GLOSSARY_NODE, EntityType.TAG, + EntityType.ROLE, EntityType.CORP_USER, EntityType.CORP_GROUP, EntityType.CONTAINER, @@ -94,6 +95,7 @@ private SearchUtils() { EntityType.TAG, EntityType.CORP_USER, EntityType.CORP_GROUP, + EntityType.ROLE, EntityType.NOTEBOOK, EntityType.DATA_PRODUCT); @@ -386,4 +388,4 @@ public static List getEntityNames(List inputTypes) { (inputTypes == null || inputTypes.isEmpty()) ? SEARCHABLE_ENTITY_TYPES : inputTypes; return entityTypes.stream().map(EntityTypeMapper::getName).collect(Collectors.toList()); } -} +} \ No newline at end of file diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index dbee24b4bf6f7..a5057bcf644da 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -441,6 +441,10 @@ type FeatureFlagsConfig { Whether we should show CTAs in the UI related to moving to Managed DataHub by Acryl. """ showAcrylInfo: Boolean! + """ + Whether we should show AccessManagement tab in the datahub UI. + """ + showAccessManagement: Boolean! } """ diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index b1f9d57300177..044c405942a3c 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -68,6 +68,10 @@ type Query { Fetch a Tag by primary key (urn) """ tag(urn: String!): Tag + """ + Fetch a Role by primary key (urn) + """ + role(urn: String!): Role """ Fetch a Glossary Term by primary key (urn) @@ -1451,12 +1455,12 @@ type Role implements Entity { """ Role properties to include Request Access Url """ - properties: RoleProperties! + properties: RoleProperties """ A standard Entity Type """ - actors: Actor! + actors: Actor } @@ -11164,4 +11168,4 @@ input UpdateOwnershipTypeInput { The description of the Custom Ownership Type """ description: String -} +} \ No newline at end of file diff --git a/datahub-web-react/src/App.tsx b/datahub-web-react/src/App.tsx index 68a4b93d71481..b6bc608dccbbb 100644 --- a/datahub-web-react/src/App.tsx +++ b/datahub-web-react/src/App.tsx @@ -35,6 +35,7 @@ import GlossaryNodeEntity from './app/entity/glossaryNode/GlossaryNodeEntity'; import { DataPlatformEntity } from './app/entity/dataPlatform/DataPlatformEntity'; import { DataProductEntity } from './app/entity/dataProduct/DataProductEntity'; import { DataPlatformInstanceEntity } from './app/entity/dataPlatformInstance/DataPlatformInstanceEntity'; +import { RoleEntity } from './app/entity/Access/RoleEntity'; /* Construct Apollo Client @@ -116,6 +117,7 @@ const App: React.VFC = () => { register.register(new DomainEntity()); register.register(new ContainerEntity()); register.register(new GlossaryNodeEntity()); + register.register(new RoleEntity()); register.register(new DataPlatformEntity()); register.register(new DataProductEntity()); register.register(new DataPlatformInstanceEntity()); diff --git a/datahub-web-react/src/app/entity/Access/RoleEntity.tsx b/datahub-web-react/src/app/entity/Access/RoleEntity.tsx new file mode 100644 index 0000000000000..e63db9d0bbb2a --- /dev/null +++ b/datahub-web-react/src/app/entity/Access/RoleEntity.tsx @@ -0,0 +1,88 @@ +import { TagOutlined, TagFilled } from '@ant-design/icons'; +import * as React from 'react'; +import styled from 'styled-components'; +import { Role, EntityType, SearchResult } from '../../../types.generated'; +import DefaultPreviewCard from '../../preview/DefaultPreviewCard'; +import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '../Entity'; +import { getDataForEntityType } from '../shared/containers/profile/utils'; +import { urlEncodeUrn } from '../shared/utils'; +import RoleEntityProfile from './RoleEntityProfile'; + +const PreviewTagIcon = styled(TagOutlined)` + font-size: 20px; +`; +// /** +// * Definition of the DataHub Access Role entity. +// */ +export class RoleEntity implements Entity { + type: EntityType = EntityType.Role; + + icon = (fontSize: number, styleType: IconStyleType, color?: string) => { + if (styleType === IconStyleType.TAB_VIEW) { + return ; + } + + if (styleType === IconStyleType.HIGHLIGHT) { + return ; + } + + return ( + + ); + }; + + isSearchEnabled = () => true; + + isBrowseEnabled = () => false; + + isLineageEnabled = () => false; + + getAutoCompleteFieldName = () => 'name'; + + getPathName: () => string = () => 'role'; + + getCollectionName: () => string = () => 'Roles'; + + getEntityName: () => string = () => 'Role'; + + renderProfile: (urn: string) => JSX.Element = (_) => ; + + renderPreview = (_: PreviewType, data: Role) => ( + } + type="Role" + typeIcon={this.icon(14, IconStyleType.ACCENT)} + /> + ); + + renderSearch = (result: SearchResult) => { + return this.renderPreview(PreviewType.SEARCH, result.entity as Role); + }; + + displayName = (data: Role) => { + return data.properties?.name || data.urn; + }; + + getOverridePropertiesFromEntity = (data: Role) => { + return { + name: data.properties?.name, + }; + }; + + getGenericEntityProperties = (role: Role) => { + return getDataForEntityType({ data: role, entityType: this.type, getOverrideProperties: (data) => data }); + }; + + supportedCapabilities = () => { + return new Set([EntityCapabilityType.OWNERS]); + }; +} diff --git a/datahub-web-react/src/app/entity/Access/RoleEntityProfile.tsx b/datahub-web-react/src/app/entity/Access/RoleEntityProfile.tsx new file mode 100644 index 0000000000000..d8a31700fb918 --- /dev/null +++ b/datahub-web-react/src/app/entity/Access/RoleEntityProfile.tsx @@ -0,0 +1,75 @@ +import React from 'react'; + +import { useParams } from 'react-router'; +import { Divider, Typography } from 'antd'; +import { grey } from '@ant-design/colors'; +import styled from 'styled-components'; + +import { Message } from '../../shared/Message'; +import { decodeUrn } from '../shared/utils'; +import { useGetExternalRoleQuery } from '../../../graphql/accessrole.generated'; + +const PageContainer = styled.div` + padding: 32px 100px; +`; + +const LoadingMessage = styled(Message)` + margin-top: 10%; +`; + +type RolePageParams = { + urn: string; +}; + +const TitleLabel = styled(Typography.Text)` + &&& { + color: ${grey[2]}; + font-size: 12px; + display: block; + line-height: 20px; + font-weight: 700; + } +`; + +const DescriptionLabel = styled(Typography.Text)` + &&& { + text-align: left; + font-weight: bold; + font-size: 14px; + line-height: 28px; + color: rgb(38, 38, 38); + } +`; + +const TitleText = styled(Typography.Text)` + &&& { + color: ${grey[10]}; + font-weight: 700; + font-size: 20px; + line-height: 28px; + display: inline-block; + margin: 0px 7px; + } +`; + +const { Paragraph } = Typography; + +export default function RoleEntityProfile() { + const { urn: encodedUrn } = useParams(); + const urn = decodeUrn(encodedUrn); + const { data, loading } = useGetExternalRoleQuery({ variables: { urn } }); + + return ( + + {loading && } + Role + {data?.role?.properties?.name} + + {/* Role Description */} + About + + {data?.role?.properties?.description} + + + ); +} diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx index 535a3f569964c..7d40b97a66b3b 100644 --- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { DatabaseFilled, DatabaseOutlined } from '@ant-design/icons'; import { Dataset, DatasetProperties, EntityType, OwnershipType, SearchResult } from '../../../types.generated'; import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '../Entity'; +import { useAppConfig } from '../../useAppConfig'; import { Preview } from './preview/Preview'; import { EntityProfile } from '../shared/containers/profile/EntityProfile'; import { GetDatasetQuery, useGetDatasetQuery, useUpdateDatasetMutation } from '../../../graphql/dataset.generated'; @@ -30,6 +31,7 @@ import { EmbedTab } from '../shared/tabs/Embed/EmbedTab'; import EmbeddedProfile from '../shared/embed/EmbeddedProfile'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; +import AccessManagement from '../shared/tabs/Dataset/AccessManagement/AccessManagement'; import { matchedFieldPathsRenderer } from '../../search/matches/matchedFieldPathsRenderer'; const SUBTYPES = { @@ -69,6 +71,8 @@ export class DatasetEntity implements Entity { isSearchEnabled = () => true; + appconfig = useAppConfig; + isBrowseEnabled = () => true; isLineageEnabled = () => true; @@ -176,6 +180,14 @@ export class DatasetEntity implements Entity { }, }, }, + { + name: 'Access Management', + component: AccessManagement, + display: { + visible: (_, _1) => this.appconfig().config.featureFlags.showAccessManagement, + enabled: (_, _2) => true, + }, + }, ]} sidebarSections={[ { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/AccessManagement.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/AccessManagement.tsx new file mode 100644 index 0000000000000..c812569367419 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/AccessManagement.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Button, Table } from 'antd'; +import { useBaseEntity } from '../../../EntityContext'; +import { GetDatasetQuery, useGetExternalRolesQuery } from '../../../../../../graphql/dataset.generated'; +import { useGetMeQuery } from '../../../../../../graphql/me.generated'; +import { handleAccessRoles } from './utils'; +import AccessManagerDescription from './AccessManagerDescription'; + +const StyledTable = styled(Table)` + overflow: inherit; + height: inherit; + + &&& .ant-table-cell { + background-color: #fff; + } + &&& .ant-table-thead .ant-table-cell { + font-weight: 600; + font-size: 12px; + color: '#898989'; + } + && + .ant-table-thead + > tr + > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { + border: 1px solid #f0f0f0; + } +` as typeof Table; + +const StyledSection = styled.section` + background-color: #fff; + color: black; + width: 83px; + text-align: center; + border-radius: 3px; + border: none; + font-weight: bold; +`; + +const AccessButton = styled(Button)` + background-color: #1890ff; + color: white; + width: 80px; + height: 30px; + border-radius: 3.5px; + border: none; + font-weight: bold; + &:hover { + background-color: #18baff; + color: white; + width: 80px; + height: 30px; + border-radius: 3.5px; + border: none; + font-weight: bold; + } +`; + +export default function AccessManagement() { + const { data: loggedInUser } = useGetMeQuery({ fetchPolicy: 'cache-first' }); + const baseEntity = useBaseEntity(); + const { data: externalRoles } = useGetExternalRolesQuery({ + variables: { urn: baseEntity?.dataset?.urn as string }, + skip: !baseEntity?.dataset?.urn, + }); + + const columns = [ + { + title: 'Role Name', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Description', + dataIndex: 'description', + key: 'description', + render: (roleDescription) => { + return ; + }, + }, + { + title: 'Access Type', + dataIndex: 'accessType', + key: 'accessType', + }, + { + title: 'Access', + dataIndex: 'hasAccess', + key: 'hasAccess', + render: (hasAccess, record) => { + if (hasAccess) { + return Provisioned; + } + if (record?.url) { + return ( + { + e.preventDefault(); + window.open(record.url); + }} + > + Request + + ); + } + return ; + }, + hidden: true, + }, + ]; + + return ( + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/AccessManagerDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/AccessManagerDescription.tsx new file mode 100644 index 0000000000000..c87a499e34ac0 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/AccessManagerDescription.tsx @@ -0,0 +1,38 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Typography } from 'antd'; + +export type Props = { + description: any; +}; + +const DescriptionContainer = styled.div` + position: relative; + display: flex; + flex-direction: column; + width: 500px; + height: 100%; + min-height: 22px; +`; + +export default function AccessManagerDescription({ description }: Props) { + const shouldTruncateDescription = description.length > 150; + const [expanded, setIsExpanded] = useState(!shouldTruncateDescription); + const finalDescription = expanded ? description : description.slice(0, 150); + const toggleExpanded = () => { + setIsExpanded(!expanded); + }; + + return ( + + {finalDescription} + { + toggleExpanded(); + }} + > + {(shouldTruncateDescription && (expanded ? ' Read Less' : '...Read More')) || undefined} + + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts new file mode 100644 index 0000000000000..53c7b483d9428 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/__tests__/AccessManagement.test.ts @@ -0,0 +1,267 @@ +import { handleAccessRoles } from '../utils'; +import { GetExternalRolesQuery } from '../../../../../../../graphql/dataset.generated'; +import { GetMeQuery } from '../../../../../../../graphql/me.generated'; + +describe('handleAccessRoles', () => { + it('should properly map the externalroles and loggedin user', () => { + const externalRolesQuery: GetExternalRolesQuery = { + dataset: { + access: { + roles: [ + { + role: { + id: 'accessRole', + properties: { + name: 'accessRole', + description: + 'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ', + type: 'READ', + requestUrl: 'https://www.google.com/', + }, + urn: 'urn:li:role:accessRole', + actors: { + users: null, + }, + }, + }, + ], + }, + __typename: 'Dataset', + }, + }; + + const GetMeQueryUser: GetMeQuery = { + me: { + corpUser: { + urn: 'urn:li:corpuser:datahub', + username: 'datahub', + info: { + active: true, + displayName: 'DataHub', + title: 'DataHub Root User', + firstName: null, + lastName: null, + fullName: null, + email: null, + __typename: 'CorpUserInfo', + }, + editableProperties: { + displayName: null, + title: null, + pictureLink: + 'https://raw.githubusercontent.com/datahub-project/datahub/master/datahub-web-react/src/images/default_avatar.png', + teams: [], + skills: [], + __typename: 'CorpUserEditableProperties', + }, + settings: { + appearance: { + showSimplifiedHomepage: false, + __typename: 'CorpUserAppearanceSettings', + }, + views: null, + __typename: 'CorpUserSettings', + }, + __typename: 'CorpUser', + }, + platformPrivileges: { + viewAnalytics: true, + managePolicies: true, + manageIdentities: true, + generatePersonalAccessTokens: true, + manageIngestion: true, + manageSecrets: true, + manageDomains: true, + manageTests: true, + manageGlossaries: true, + manageUserCredentials: true, + manageTags: true, + createDomains: true, + createTags: true, + manageGlobalViews: true, + manageOwnershipTypes: true, + __typename: 'PlatformPrivileges', + }, + __typename: 'AuthenticatedUser', + }, + }; + const externalRole = handleAccessRoles(externalRolesQuery, GetMeQueryUser); + expect(externalRole).toMatchObject([ + { + name: 'accessRole', + description: + 'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ', + accessType: 'READ', + hasAccess: false, + url: 'https://www.google.com/', + }, + ]); + }); + it('should return empty array', () => { + const externalRolesQuery: GetExternalRolesQuery = { + dataset: { + access: null, + __typename: 'Dataset', + }, + }; + + const GetMeQueryUser: GetMeQuery = { + me: { + corpUser: { + urn: 'urn:li:corpuser:datahub', + username: 'datahub', + info: { + active: true, + displayName: 'DataHub', + title: 'DataHub Root User', + firstName: null, + lastName: null, + fullName: null, + email: null, + __typename: 'CorpUserInfo', + }, + editableProperties: { + displayName: null, + title: null, + pictureLink: + 'https://raw.githubusercontent.com/datahub-project/datahub/master/datahub-web-react/src/images/default_avatar.png', + teams: [], + skills: [], + __typename: 'CorpUserEditableProperties', + }, + settings: { + appearance: { + showSimplifiedHomepage: false, + __typename: 'CorpUserAppearanceSettings', + }, + views: null, + __typename: 'CorpUserSettings', + }, + __typename: 'CorpUser', + }, + platformPrivileges: { + viewAnalytics: true, + managePolicies: true, + manageIdentities: true, + generatePersonalAccessTokens: true, + manageIngestion: true, + manageSecrets: true, + manageDomains: true, + manageTests: true, + manageGlossaries: true, + manageUserCredentials: true, + manageTags: true, + createDomains: true, + createTags: true, + manageGlobalViews: true, + manageOwnershipTypes: true, + __typename: 'PlatformPrivileges', + }, + __typename: 'AuthenticatedUser', + }, + }; + const externalRole = handleAccessRoles(externalRolesQuery, GetMeQueryUser); + expect(externalRole).toMatchObject([]); + }); + it('should properly map the externalroles and loggedin user and access true', () => { + const externalRolesQuery: GetExternalRolesQuery = { + dataset: { + access: { + roles: [ + { + role: { + id: 'accessRole', + properties: { + name: 'accessRole', + description: + 'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ', + type: 'READ', + requestUrl: 'https://www.google.com/', + }, + urn: 'urn:li:role:accessRole', + actors: { + users: [ + { + user: { + urn: 'urn:li:corpuser:datahub', + }, + }, + ], + }, + }, + }, + ], + }, + __typename: 'Dataset', + }, + }; + + const GetMeQueryUser: GetMeQuery = { + me: { + corpUser: { + urn: 'urn:li:corpuser:datahub', + username: 'datahub', + info: { + active: true, + displayName: 'DataHub', + title: 'DataHub Root User', + firstName: null, + lastName: null, + fullName: null, + email: null, + __typename: 'CorpUserInfo', + }, + editableProperties: { + displayName: null, + title: null, + pictureLink: + 'https://raw.githubusercontent.com/datahub-project/datahub/master/datahub-web-react/src/images/default_avatar.png', + teams: [], + skills: [], + __typename: 'CorpUserEditableProperties', + }, + settings: { + appearance: { + showSimplifiedHomepage: false, + __typename: 'CorpUserAppearanceSettings', + }, + views: null, + __typename: 'CorpUserSettings', + }, + __typename: 'CorpUser', + }, + platformPrivileges: { + viewAnalytics: true, + managePolicies: true, + manageIdentities: true, + generatePersonalAccessTokens: true, + manageIngestion: true, + manageSecrets: true, + manageDomains: true, + manageTests: true, + manageGlossaries: true, + manageUserCredentials: true, + manageTags: true, + createDomains: true, + createTags: true, + manageGlobalViews: true, + manageOwnershipTypes: true, + __typename: 'PlatformPrivileges', + }, + __typename: 'AuthenticatedUser', + }, + }; + const externalRole = handleAccessRoles(externalRolesQuery, GetMeQueryUser); + + expect(externalRole).toMatchObject([ + { + name: 'accessRole', + description: + 'This role access is required by the developers to test and deploy the code also adding few more details to check the description length for the given data and hence check the condition of read more and read less ', + accessType: 'READ', + hasAccess: true, + url: 'https://www.google.com/', + }, + ]); + }); +}); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/utils.tsx new file mode 100644 index 0000000000000..71e81e8d7de93 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/AccessManagement/utils.tsx @@ -0,0 +1,27 @@ +export function handleAccessRoles(externalRoles, loggedInUser) { + const accessRoles = new Array(); + if ( + externalRoles?.dataset?.access && + externalRoles?.dataset?.access.roles && + externalRoles?.dataset?.access.roles.length > 0 + ) { + externalRoles?.dataset?.access?.roles?.forEach((userRoles) => { + const role = { + name: userRoles?.role?.properties?.name || ' ', + description: userRoles?.role?.properties?.description || ' ', + accessType: userRoles?.role?.properties?.type || ' ', + hasAccess: + (userRoles?.role?.actors?.users && + userRoles?.role?.actors?.users.length > 0 && + userRoles?.role?.actors?.users?.some( + (user) => user.user.urn === loggedInUser?.me?.corpUser.urn, + )) || + false, + url: userRoles?.role?.properties?.requestUrl || undefined, + }; + accessRoles.push(role); + }); + } + + return accessRoles; +} diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx index 807a17c4fd6a4..096c2fd6ef0e5 100644 --- a/datahub-web-react/src/appConfigContext.tsx +++ b/datahub-web-react/src/appConfigContext.tsx @@ -48,6 +48,7 @@ export const DEFAULT_APP_CONFIG = { showSearchFiltersV2: true, showBrowseV2: true, showAcrylInfo: false, + showAccessManagement: false, }, }; diff --git a/datahub-web-react/src/graphql/accessrole.graphql b/datahub-web-react/src/graphql/accessrole.graphql new file mode 100644 index 0000000000000..ccc7d3496ad6d --- /dev/null +++ b/datahub-web-react/src/graphql/accessrole.graphql @@ -0,0 +1,8 @@ +query getExternalRole($urn: String!) { + role(urn: $urn) { + properties { + name + description + } + } +} \ No newline at end of file diff --git a/datahub-web-react/src/graphql/app.graphql b/datahub-web-react/src/graphql/app.graphql index bf15e5f757f8f..228fa1c9430d0 100644 --- a/datahub-web-react/src/graphql/app.graphql +++ b/datahub-web-react/src/graphql/app.graphql @@ -63,6 +63,7 @@ query appConfig { showSearchFiltersV2 showBrowseV2 showAcrylInfo + showAccessManagement } } } diff --git a/datahub-web-react/src/graphql/dataset.graphql b/datahub-web-react/src/graphql/dataset.graphql index c79c1a4d9d551..658ce2b47c567 100644 --- a/datahub-web-react/src/graphql/dataset.graphql +++ b/datahub-web-react/src/graphql/dataset.graphql @@ -311,3 +311,34 @@ query getDatasetSchema($urn: String!) { } } } + +query getExternalRoles($urn: String!) { + dataset(urn: $urn) { + access { + ...getRoles + } + __typename + } +} + +fragment getRoles on Access { + roles { + role { + id + properties { + name + description + type + requestUrl + } + urn + actors { + users { + user { + urn + } + } + } + } + } +} diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql index 7cd868d7cd2b2..94ff263c02039 100644 --- a/datahub-web-react/src/graphql/search.graphql +++ b/datahub-web-react/src/graphql/search.graphql @@ -44,6 +44,16 @@ fragment autoCompleteFields on Entity { } } ...datasetStatsFields + access { + ...getAccess + } + } + ... on Role { + id + properties { + name + description + } } ... on CorpUser { username @@ -242,6 +252,25 @@ query getAutoCompleteMultipleResults($input: AutoCompleteMultipleInput!) { } } +fragment getAccess on Access { + roles { + role { + ...getRolesName + } + } +} + +fragment getRolesName on Role { + urn + type + id + properties { + name + description + type + } +} + fragment datasetStatsFields on Dataset { lastProfile: datasetProfiles(limit: 1) { rowCount @@ -288,6 +317,9 @@ fragment nonSiblingsDatasetSearchFields on Dataset { editableProperties { description } + access { + ...getAccess + } platformNativeType properties { name @@ -346,6 +378,13 @@ fragment searchResultFields on Entity { } } } + ... on Role { + id + properties { + name + description + } + } ... on CorpUser { username properties { diff --git a/metadata-service/configuration/src/main/resources/application.yml b/metadata-service/configuration/src/main/resources/application.yml index f49498bfa2325..6fd7b9e6a295c 100644 --- a/metadata-service/configuration/src/main/resources/application.yml +++ b/metadata-service/configuration/src/main/resources/application.yml @@ -294,6 +294,7 @@ featureFlags: alwaysEmitChangeLog: ${ALWAYS_EMIT_CHANGE_LOG:false} # Enables always emitting a MCL even when no changes are detected. Used for Time Based Lineage when no changes occur. searchServiceDiffModeEnabled: ${SEARCH_SERVICE_DIFF_MODE_ENABLED:true} # Enables diff mode for search document writes, reduces amount of writes to ElasticSearch documents for no-ops readOnlyModeEnabled: ${READ_ONLY_MODE_ENABLED:false} # Enables read only mode for an instance. Right now this only affects ability to edit user profile image URL but can be extended + showAccessManagement: ${SHOW_ACCESS_MANAGEMENT:false} #Whether we should show AccessManagement tab in the datahub UI. showSearchFiltersV2: ${SHOW_SEARCH_FILTERS_V2:true} # Enables showing the search filters V2 experience. showBrowseV2: ${SHOW_BROWSE_V2:true} # Enables showing the browse v2 sidebar experience. preProcessHooks: diff --git a/smoke-test/tests/cypress/cypress/e2e/glossary/glossary_navigation.js b/smoke-test/tests/cypress/cypress/e2e/glossary/glossary_navigation.js new file mode 100644 index 0000000000000..cd5622d0cd903 --- /dev/null +++ b/smoke-test/tests/cypress/cypress/e2e/glossary/glossary_navigation.js @@ -0,0 +1,61 @@ +const glossaryTerm = "CypressGlosssaryNavigationTerm"; +const glossaryTermGroup = "CypressGlosssaryNavigationGroup"; +const glossaryParentGroup = "Cypress"; + +describe("glossary sidebar navigation test", () => { + it("create term and term parent group, move and delete term group", () => { + //create a new term group and term, move term to the group + cy.loginWithCredentials(); + cy.goToGlossaryList(); + cy.clickOptionWithText("Add Term Group"); + cy.waitTextVisible("Create Term Group"); + cy.get(".ant-input-affix-wrapper > input[type='text']").first().type(glossaryTermGroup); + cy.get(".ant-modal-footer > button:last-child").click(); + cy.get('*[class^="GlossaryBrowser"]').contains(glossaryTermGroup).should("be.visible"); + cy.clickOptionWithText("Add Term"); + cy.waitTextVisible("Create Glossary Term"); + cy.get(".ant-input-affix-wrapper > input[type='text']").first().type(glossaryTerm); + cy.get(".ant-modal-footer > button:last-child").click(); + cy.get('*[class^="GlossaryBrowser"]').contains(glossaryTerm).click(); + cy.waitTextVisible("No documentation yet"); + cy.openThreeDotDropdown(); + cy.clickOptionWithText("Move"); + cy.get('[role="dialog"] [data-icon="close-circle"]').click({force: true}); + cy.get('[role="dialog"]').contains(glossaryTermGroup).click(); + cy.get('[role="dialog"]').contains(glossaryTermGroup).should("be.visible"); + cy.get("button").contains("Move").click(); + cy.waitTextVisible("Moved Glossary Term!"); + //ensure the new term is under the parent term group in the navigation sidebar + cy.get('*[class^="GlossaryBrowser"]').contains(glossaryTermGroup).click(); + cy.get('*[class^="GlossaryEntitiesList"]').contains(glossaryTerm).should("be.visible"); + cy.get('*[class^="GlossaryBrowser"] [aria-label="down"]').click().wait(1000); + cy.get('*[class^="GlossaryBrowser"]').contains(glossaryTerm).should("not.exist"); + //move a term group from the root level to be under a parent term group + cy.goToGlossaryList(); + cy.clickOptionWithText(glossaryTermGroup); + cy.openThreeDotDropdown(); + cy.clickOptionWithText("Move"); + cy.get('[role="dialog"] [data-icon="close-circle"]').click({force: true}); + cy.get('[role="dialog"]').contains(glossaryParentGroup).click(); + cy.get('[role="dialog"]').contains(glossaryParentGroup).should("be.visible"); + cy.get("button").contains("Move").click(); + cy.waitTextVisible("Moved Term Group!"); + //ensure it is no longer on the sidebar navigator at the top level but shows up under the new parent + cy.get('*[class^="GlossaryBrowser"] [aria-label="down"]').click().wait(1000); + cy.get('*[class^="GlossaryBrowser"]').contains(glossaryTermGroup).should("not.exist"); + //delete a term group + cy.goToGlossaryList(); + cy.clickOptionWithText(glossaryParentGroup); + cy.clickOptionWithText(glossaryTermGroup); + cy.clickOptionWithText(glossaryTerm).wait(3000); + cy.deleteFromDropdown(); + cy.waitTextVisible("Deleted Glossary Term!"); + cy.clickOptionWithText(glossaryParentGroup); + cy.clickOptionWithText(glossaryTermGroup).wait(3000); + cy.deleteFromDropdown(); + cy.waitTextVisible("Deleted Term Group!"); + //ensure it is no longer in the sidebar navigator + cy.ensureTextNotPresent(glossaryTerm); + cy.ensureTextNotPresent(glossaryTermGroup); + }); +}); \ No newline at end of file