Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ingest/tableau): support ingestion of access roles #11157

Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6903459
add access aspect to container, permission ingestion wip
Aug 12, 2024
f9e1d7e
fix linting issues
Aug 12, 2024
7edb40d
Merge branch 'master' of https://github.com/haeniya/datahub into feat…
Aug 13, 2024
ee8eb3d
cleanup
Aug 13, 2024
33c9a26
make trust_env property configurable, not hard-coded, minor text change
Aug 20, 2024
a237aa6
Merge branch 'master' of https://github.com/haeniya/datahub into feat…
Sep 10, 2024
a866287
pr feedback implemented: field descriptions and minor refactorings
Sep 11, 2024
9d9c96a
add some documentation with examples to the tableau source docs
Sep 12, 2024
9414cfb
add unit test for new functionality, improve docs
Sep 16, 2024
556a060
fix linting issues
Sep 16, 2024
1219699
fix frontend linting issues
Sep 16, 2024
f0c0e38
pr feedback: change log message to debug
Sep 24, 2024
5048136
minor refactorings (description and log level)
Sep 30, 2024
cdac760
Merge branch 'master' into feature/tableau-ingestion-access-permissions
haeniya Sep 30, 2024
8d279f6
Merge branch 'master' into feature/tableau-ingestion-access-permissions
haeniya Oct 1, 2024
d6fca5e
field description changes, minor refactorings according to pr feedback
Oct 7, 2024
4dca4d5
pr feedback: remove access aspect and role creation and only add grou…
Oct 14, 2024
1ba89e6
pr review: remove displayed capabilities prop and output permissions …
Oct 21, 2024
76b8651
fix linting issues
Oct 21, 2024
3975605
Merge branch 'master' into feature/tableau-ingestion-access-permissions
hsheth2 Oct 21, 2024
af988bd
fix integration tests
Oct 24, 2024
7dcadad
Merge branch 'feature/tableau-ingestion-access-permissions' of https:…
Oct 24, 2024
45949bd
Merge branch 'master' into feature/tableau-ingestion-access-permissions
haeniya Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public class ContainerType
Constants.DEPRECATION_ASPECT_NAME,
Constants.DATA_PRODUCTS_ASPECT_NAME,
Constants.STRUCTURED_PROPERTIES_ASPECT_NAME,
Constants.FORMS_ASPECT_NAME);
Constants.FORMS_ASPECT_NAME,
Constants.ACCESS_ASPECT_NAME);

private static final Set<String> FACET_FIELDS = ImmutableSet.of("origin", "platform");
private static final String ENTITY_NAME = "container";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.linkedin.metadata.Constants.*;

import com.linkedin.common.Access;
import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
import com.linkedin.common.Forms;
Expand Down Expand Up @@ -30,6 +31,7 @@
import com.linkedin.datahub.graphql.types.domain.DomainAssociationMapper;
import com.linkedin.datahub.graphql.types.form.FormsMapper;
import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper;
import com.linkedin.datahub.graphql.types.rolemetadata.mappers.AccessMapper;
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper;
import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper;
import com.linkedin.domain.Domains;
Expand Down Expand Up @@ -105,6 +107,11 @@ public static Container map(
context, new GlossaryTerms(envelopedTerms.getValue().data()), entityUrn));
}

final EnvelopedAspect accessAspect = aspects.get(ACCESS_ASPECT_NAME);
if (accessAspect != null) {
result.setAccess(AccessMapper.map(new Access(accessAspect.getValue().data()), entityUrn));
}

final EnvelopedAspect envelopedInstitutionalMemory =
aspects.get(Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME);
if (envelopedInstitutionalMemory != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class DatasetType
EMBED_ASPECT_NAME,
DATA_PRODUCTS_ASPECT_NAME,
BROWSE_PATHS_V2_ASPECT_NAME,
ACCESS_DATASET_ASPECT_NAME,
ACCESS_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME,
SUB_TYPES_ASPECT_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public Dataset apply(
(dataset, dataMap) ->
dataset.setBrowsePathV2(BrowsePathsV2Mapper.map(context, new BrowsePathsV2(dataMap))));
mappingHelper.mapToResult(
ACCESS_DATASET_ASPECT_NAME,
ACCESS_ASPECT_NAME,
((dataset, dataMap) ->
dataset.setAccess(AccessMapper.map(new Access(dataMap), entityUrn))));
mappingHelper.mapToResult(
Expand Down
5 changes: 5 additions & 0 deletions datahub-graphql-core/src/main/resources/entity.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2827,6 +2827,11 @@ type Container implements Entity {
"""
exists: Boolean

"""
The Roles and the properties to access the container
"""
access: Access

"""
Experimental API.
For fetching extra entities that do not have custom UI code yet
Expand Down
23 changes: 22 additions & 1 deletion datahub-web-react/src/app/entity/container/ContainerEntity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab'
import { SidebarAboutSection } from '../shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarOwnerSection } from '../shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
import { getDataForEntityType } from '../shared/containers/profile/utils';
import { useGetContainerQuery } from '../../../graphql/container.generated';
import { useGetContainerQuery, GetContainerQuery } from '../../../graphql/container.generated';
import { ContainerEntitiesTab } from './ContainerEntitiesTab';
import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection';
import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab';
Expand All @@ -17,6 +17,8 @@ import { capitalizeFirstLetterOnly } from '../../shared/textUtil';
import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { getDataProduct } from '../shared/utils';
import EmbeddedProfile from '../shared/embed/EmbeddedProfile';
import AccessManagement from '../shared/tabs/Dataset/AccessManagement/AccessManagement';
import { useAppConfig } from '../../useAppConfig';

/**
* Definition of the DataHub Container entity.
Expand Down Expand Up @@ -65,6 +67,8 @@ export class ContainerEntity implements Entity<Container> {

useEntityQuery = useGetContainerQuery;

appconfig = useAppConfig;

renderProfile = (urn: string) => (
<EntityProfile
urn={urn}
Expand All @@ -85,6 +89,23 @@ export class ContainerEntity implements Entity<Container> {
name: 'Properties',
component: PropertiesTab,
},
{
name: 'Access Management',
component: AccessManagement,
display: {
visible: (_, container: GetContainerQuery) => {
return (
this.appconfig().config.featureFlags.showAccessManagement &&
!!container?.container?.access
);
},
enabled: (_, container: GetContainerQuery) => {
const accessAspect = container?.container?.access;
const rolesList = accessAspect?.roles;
return !!accessAspect && !!rolesList && rolesList.length > 0;
},
},
},
]}
sidebarSections={this.getSidebarSections()}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import styled from 'styled-components';
import { Button, Table } from 'antd';
import { SpinProps } from 'antd/es/spin';
import { LoadingOutlined } from '@ant-design/icons';
import { useBaseEntity } from '../../../EntityContext';
import { GetDatasetQuery, useGetExternalRolesQuery } from '../../../../../../graphql/dataset.generated';
import { useEntityData } from '../../../EntityContext';
import { useGetExternalRolesQuery } from '../../../../../../graphql/dataset.generated';
import { handleAccessRoles } from './utils';
import AccessManagerDescription from './AccessManagerDescription';

Expand Down Expand Up @@ -60,11 +60,12 @@ const AccessButton = styled(Button)`
`;

export default function AccessManagement() {
const baseEntity = useBaseEntity<GetDatasetQuery>();
const { entityData } = useEntityData();
const entityUrn = (entityData as any)?.urn;

const { data: externalRoles, loading: isLoading } = useGetExternalRolesQuery({
variables: { urn: baseEntity?.dataset?.urn as string },
skip: !baseEntity?.dataset?.urn,
variables: { urn: entityUrn as string },
skip: !entityUrn,
});

const columns = [
Expand Down
7 changes: 7 additions & 0 deletions datahub-web-react/src/graphql/container.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ query getContainer($urn: String!) {
status {
removed
}
access {
roles {
role {
urn
}
}
}
autoRenderAspects: aspects(input: { autoRenderOnly: true }) {
...autoRenderAspectFields
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ public class Constants {

// ExternalRoleMetadata
public static final String ROLE_ENTITY_NAME = "role";
public static final String ACCESS_DATASET_ASPECT_NAME = "access";
public static final String ACCESS_ASPECT_NAME = "access";
public static final String ROLE_KEY = "roleKey";
public static final String ROLE_PROPERTIES_ASPECT_NAME = "roleProperties";
public static final String ROLE_ACTORS_ASPECT_NAME = "actors";
Expand Down
36 changes: 36 additions & 0 deletions metadata-ingestion/docs/sources/tableau/tableau_pre.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ This ingestion source maps the following Source System Concepts to DataHub Conce
| User | [User (a.k.a CorpUser)](../../metamodel/entities/corpuser.md) | Optionally Extracted |
| Workbook | [Container](../../metamodel/entities/container.md) | SubType `"Workbook"` |
| Tag | [Tag](../../metamodel/entities/tag.md) | Optionally Extracted |
| Permissions (Groups) | [Role](../../metamodel/entities/role) | Optionally Extracted |
haeniya marked this conversation as resolved.
Show resolved Hide resolved

#### Lineage

Expand All @@ -69,6 +70,41 @@ Lineage is emitted as received from Tableau's metadata API for
- Custom SQL datasources upstream to Embedded or Published Data Source
- Tables upstream to Custom SQL Data Source

#### Access Roles

The ingestion can be configured to ingest Tableau permissions as access roles. This enables users to request access to Tableau assets from Datahub.
The Tableau group permissions are fetched from the Tableau API, optionally transformed to your needs, and then added as roles to the Tableau asset in Datahub. Currently, this is only available for Workbooks.

Assuming you have groups in Tableau with names such as `AB_XY00-Tableau-Access_A_123_PROJECT_XY_Consumer` or `AB_XY00-Tableau-Access_A_123_PROJECT_XY_Analyst` and corresponding IAM roles like `AR-Tableau-PROJECT_XY_Consumer` or `AR-Tableau-PROJECT_XY_Analyst`.
Using the recipe below, would filter the groups that end with "_Consumer" and transform the group names into the corresponding IAM roles.
With `group_substring_start` and `group_substring_end` you can define a substring of the group name to be used as role name and `role_prefix` can be used to add a prefix to the generated role name.
The role names then act as a substitute of `$ROLE_NAME` in the `request_url` and result in access request URLs like `https://iam.example.com/accessRequest?role=AR-Tableau-PROJECT_XY_Consumer`, for example.
```
source:
type: tableau
config:
connect_uri: https://tableau.example.com

access_role_ingestion:
enable_workbooks: True
role_prefix: "AR-Tableau-"
group_substring_start: 29
role_description: "IAM role required to access this Tableau asset."
displayed_capabilities: ["Read", "Write", "Delete"]
request_url: "https://iam.example.com/accessRequest?role=$ROLE_NAME"
group_name_pattern:
allow: [ "^.*_Consumer$" ]

username: "${TABLEAU_USER}"
password: "${TABLEAU_PASSWORD}"

sink:
type: "datahub-rest"
config:
server: "http://localhost:8080"
```
You can find more information about the specific fields of the `access_role_ingestion` section in the config details below.

#### Caveats

- Tableau metadata API might return incorrect schema name for tables for some databases, leading to incorrect metadata in DataHub. This source attempts to extract correct schema from databaseTable's fully qualified name, wherever possible. Read [Using the databaseTable object in query](https://help.tableau.com/current/api/metadata_api/en-us/docs/meta_api_model.html#schema_attribute) for caveats in using schema attribute.
Expand Down
4 changes: 4 additions & 0 deletions metadata-ingestion/src/datahub/emitter/mce_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ def make_container_urn(guid: Union[str, "DatahubKey"]) -> str:
return f"urn:li:container:{guid}"


def make_role_urn(role_name: str) -> str:
return f"urn:li:role:{role_name}"


def container_urn_to_key(guid: str) -> Optional[ContainerKeyClass]:
pattern = r"urn:li:container:(.*)"
results = re.search(pattern, guid)
Expand Down
Loading
Loading