packages/constructs/L3/governance/lakeformation-access-control-l3-construct/lib/lakeformation-access-control-l3-construct.ts (300 lines of code) (raw):

/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ import { MdaaResolvableRole, MdaaRoleRef } from '@aws-mdaa/iam-role-helper'; import { MdaaL3Construct, MdaaL3ConstructProps } from '@aws-mdaa/l3-construct'; import { Arn, ArnComponents, ArnFormat } from 'aws-cdk-lib'; import { CfnDatabase, CfnDatabaseProps } from 'aws-cdk-lib/aws-glue'; import { CfnPrincipalPermissions } from 'aws-cdk-lib/aws-lakeformation'; import { Construct } from 'constructs'; /** * Permissions to grant. 'read' resolves to SELECT + DESCRIBE. 'write' resolves to SELECT + DESCRIBE + INSERT + DELETE. */ export type PermissionsConfig = 'read' | 'write' | 'super'; export interface NamedResourceLinkProps { /** @jsii ignore */ [name: string]: ResourceLinkProps; } export interface ResourceLinkProps { /** * Name of the target database */ readonly targetDatabase: string; /** * The account where the target database exists */ readonly targetAccount?: string; /** * The account in which the resource link should be created. * If not specified, will default to the local account. */ readonly fromAccount?: string; /** * Named principals to be granted DESCRIBE access to the resource link */ readonly grantPrincipals?: NamedPrincipalProps; } export interface NamedPrincipalProps { /** * Name for the principal. */ /** @jsii ignore */ readonly [name: string]: PrincipalProps; } export interface PrincipalProps { /** * Federated group name for the grant. */ readonly federatedGroup?: string; /** * Federated user name for the grant. */ readonly federatedUser?: string; /** * Arn of the IAM Federation provider that Active Directory uses to federate into the environment. */ readonly federationProviderArn?: string; /** * Arn of an IAM principal for the grant. */ readonly role?: MdaaRoleRef | MdaaResolvableRole; /** * Optionally, the principal account can be specified for cases where the account cannot be * determined from the role arn */ readonly account?: string; } export interface NamedGrantProps { /** * The unique name of the grant */ /** @jsii ignore */ readonly [name: string]: GrantProps; } export interface GrantProps { /** * Name of the existing Glue Database to perform the grant against. */ readonly database: string; /** * Array of strings representing Tables to perform the grant against. Use a '*' for all tables. */ readonly tables?: string[]; /** * LF Permissions to grant on the database */ readonly databasePermissions: string[]; /** * LF Permissions to grant on the tables (if specified) */ readonly tablePermissions?: string[]; /** * LF Grantable Permissions to grant on the database */ readonly databaseGrantablePermissions?: string[]; /** * LF Grantable Permissions to grant on the tables (if specified) */ readonly tableGrantablePermissions?: string[]; /** * Named principals to grant permissions to. */ readonly principals: NamedPrincipalProps; } export interface LakeFormationAccessControlL3ConstructProps extends MdaaL3ConstructProps { /** * List of LakeFormation grant definitions. */ readonly grants: NamedGrantProps; /** * Resource links which will be created in the local account. */ readonly resourceLinks?: NamedResourceLinkProps; /** * External Database reference which may create in parallel (Optional) * This option is useful when a stack is using multiple L2/L3 constructs to create databases and LakeFormation Grants */ readonly externalDatabaseDependency?: CfnDatabase; } export class LakeFormationAccessControlL3Construct extends MdaaL3Construct { protected readonly props: LakeFormationAccessControlL3ConstructProps; public static readonly DATABASE_READ_PERMISSIONS: string[] = ['DESCRIBE']; public static readonly DATABASE_READ_WRITE_PERMISSIONS: string[] = ['DESCRIBE', 'CREATE_TABLE', 'ALTER']; public static readonly DATABASE_SUPER_PERMISSIONS: string[] = ['DESCRIBE', 'CREATE_TABLE', 'ALTER', 'DROP']; public static readonly TABLE_READ_PERMISSIONS: string[] = ['SELECT', 'DESCRIBE']; public static readonly TABLE_READ_WRITE_PERMISSIONS: string[] = ['SELECT', 'DESCRIBE', 'INSERT', 'DELETE']; public static readonly TABLE_SUPER_PERMISSIONS: string[] = [ 'SELECT', 'DESCRIBE', 'INSERT', 'DELETE', 'ALTER', 'DROP', ]; public static readonly TABLE_PERMISSIONS_MAP: { [key: string]: string[] } = { read: LakeFormationAccessControlL3Construct.TABLE_READ_PERMISSIONS, write: LakeFormationAccessControlL3Construct.TABLE_READ_WRITE_PERMISSIONS, super: LakeFormationAccessControlL3Construct.TABLE_SUPER_PERMISSIONS, }; public static readonly DATABASE_PERMISSIONS_MAP: { [key: string]: string[] } = { read: LakeFormationAccessControlL3Construct.DATABASE_READ_PERMISSIONS, write: LakeFormationAccessControlL3Construct.DATABASE_READ_WRITE_PERMISSIONS, super: LakeFormationAccessControlL3Construct.DATABASE_SUPER_PERMISSIONS, }; public static generateIdentifier(grantName: string, principalName: string, prefix?: string) { const id = prefix ? `${prefix}-${grantName}-${principalName}` : `${grantName}-${principalName}`; return id; } private static accountGrants: { [account: string]: CfnPrincipalPermissions } = {}; constructor(scope: Construct, id: string, props: LakeFormationAccessControlL3ConstructProps) { super(scope, id, props); this.props = props; this.createResourceLinks(this.props.resourceLinks || {}, this.props.externalDatabaseDependency); Object.entries(this.props.grants).forEach(grantEntry => { const grantName = grantEntry[0]; const grantProps = grantEntry[1]; Object.entries(grantProps.principals).forEach(principalEntry => { const principalName = principalEntry[0]; const principalProps = principalEntry[1]; const principalIdentity = this.constructPrincipalIdentity(principalName, principalProps); this.createDatabaseGrant( principalIdentity, principalName, grantName, grantProps, principalIdentity.account == this.account ? this.props.externalDatabaseDependency : undefined, ); if (grantProps.tablePermissions) { this.createTableGrant( principalIdentity, principalName, grantName, grantProps, principalIdentity.account == this.account ? this.props.externalDatabaseDependency : undefined, ); } }); }); } private createResourceLinks(resourceLinks: NamedResourceLinkProps, externalDependency?: CfnDatabase) { Object.entries(resourceLinks).forEach(resourceLinkEntry => { const resourceLinkName = resourceLinkEntry[0]; const resourceLinkProps = resourceLinkEntry[1]; const fromAccount = resourceLinkProps.fromAccount || this.account; const createScope = fromAccount != this.account ? this.getCrossAccountStack(fromAccount) : this; const resourceLinkDatabaseProps: CfnDatabaseProps = { catalogId: fromAccount, databaseInput: { name: resourceLinkName, targetDatabase: { catalogId: resourceLinkProps.targetAccount || this.account, databaseName: resourceLinkProps.targetDatabase, }, }, }; console.log(`Creating resource link ${resourceLinkName} in account ${fromAccount}`); const createdResourceLinkDatabase = new CfnDatabase( createScope, `${resourceLinkName}-resource-link`, resourceLinkDatabaseProps, ); Object.entries(resourceLinkProps.grantPrincipals || {}).forEach(grantPrincipalEntry => { const principalName = grantPrincipalEntry[0]; const principalProps = grantPrincipalEntry[1]; const principalIdentity = this.constructPrincipalIdentity(principalName, principalProps); console.log( `Creating resource link grant for ${principalIdentity.identity} to ${resourceLinkName} in account ${fromAccount}`, ); if (principalIdentity.account != fromAccount) { console.warn( `Warning, possibly creating grant to principal in separate account ${principalIdentity.account} from resource link ${resourceLinkName} account ${fromAccount}.`, ); } const createdResourceLinkName = (createdResourceLinkDatabase.databaseInput as CfnDatabase.DatabaseInputProperty) .name; if (createdResourceLinkName) { const databaseGrantIdentifier = LakeFormationAccessControlL3Construct.generateIdentifier( resourceLinkName, principalName, 'RESOURCE-LINK', ); const crossAccountResourceLinkGrant = new CfnPrincipalPermissions( createScope, `grant-${databaseGrantIdentifier}`, { resource: { database: { catalogId: principalIdentity.account || this.account, name: createdResourceLinkName, }, }, principal: { dataLakePrincipalIdentifier: principalIdentity.identity, }, permissions: ['DESCRIBE'], permissionsWithGrantOption: [], }, ); LakeFormationAccessControlL3Construct.addToAccountGrants( fromAccount, crossAccountResourceLinkGrant, fromAccount == this.account ? externalDependency : undefined, ); } }); }); } //We use this static method to ensure that each grant depends on the previous (by account). //This ensures that each grant is deployed in sequence, avoiding LF API rate limits. private static addToAccountGrants(account: string, grant: CfnPrincipalPermissions, externalDependency?: CfnDatabase) { if (this.accountGrants[account]) { grant.addDependency(this.accountGrants[account]); } else if (externalDependency) { grant.addDependency(externalDependency); } this.accountGrants[account] = grant; } private createDatabaseGrant( principalIdentity: PrincipalIdentity, principalName: string, grantName: string, grantProps: GrantProps, externalDependency?: CfnDatabase, ) { const databaseGrantIdentifier = LakeFormationAccessControlL3Construct.generateIdentifier( grantName, principalName, 'DATABASE', ); const databaseGrant = new CfnPrincipalPermissions(this, `grant-${databaseGrantIdentifier}`, { resource: { database: { catalogId: this.account, name: grantProps.database, }, }, principal: { dataLakePrincipalIdentifier: principalIdentity.identity, }, permissions: grantProps.databasePermissions, permissionsWithGrantOption: grantProps.databaseGrantablePermissions || [], }); LakeFormationAccessControlL3Construct.addToAccountGrants(this.account, databaseGrant, externalDependency); } private createTableGrant( principalIdentity: PrincipalIdentity, principalName: string, grantName: string, grantProps: GrantProps, externalDependency?: CfnDatabase, ) { const databaseName = grantProps.database; if (grantProps.tables && grantProps.tables.length > 0) { grantProps.tables.forEach(tableName => { const tableGrantIdentifier = LakeFormationAccessControlL3Construct.generateIdentifier( grantName, principalName, tableName, ); const tableGrant = new CfnPrincipalPermissions(this, `grant-${tableGrantIdentifier}`, { resource: { table: { catalogId: this.account, databaseName: databaseName, name: tableName, }, }, principal: { dataLakePrincipalIdentifier: principalIdentity.identity, }, permissions: grantProps.tablePermissions || [], permissionsWithGrantOption: grantProps.tableGrantablePermissions || [], }); LakeFormationAccessControlL3Construct.addToAccountGrants(this.account, tableGrant, externalDependency); }); } else { const tableGrantIdentifier = LakeFormationAccessControlL3Construct.generateIdentifier( grantName, principalName, 'ALL_TABLES', ); const tableGrant = new CfnPrincipalPermissions(this, `grant-${tableGrantIdentifier}`, { resource: { table: { catalogId: this.account, databaseName: databaseName, tableWildcard: {}, }, }, principal: { dataLakePrincipalIdentifier: principalIdentity.identity, }, permissions: grantProps.tablePermissions || [], permissionsWithGrantOption: grantProps.tableGrantablePermissions || [], }); LakeFormationAccessControlL3Construct.addToAccountGrants(this.account, tableGrant, externalDependency); } } private constructPrincipalIdentity(principalName: string, principalProps: PrincipalProps): PrincipalIdentity { const principalIdentityString = this.constructPrincipalIdentityString(principalName, principalProps); const principalIdentityArn = this.tryParseArn(principalIdentityString); const principalAccount = principalIdentityArn?.account || principalProps.account || this.account; const identity = { identity: principalIdentityString, account: principalAccount, }; return identity; } private constructPrincipalIdentityString(principalName: string, principalProps: PrincipalProps): string { if (principalProps.federationProviderArn) { if (principalProps.federatedGroup) { return `${principalProps.federationProviderArn}:group/${principalProps.federatedGroup}`; } else if (principalProps.federatedUser) { return `${principalProps.federationProviderArn}:user/${principalProps.federatedUser}`; } } else { if (principalProps.role) { if (principalProps.role instanceof MdaaResolvableRole) { return principalProps.role.arn(); } else { return this.props.roleHelper.resolveRoleRefWithRefId(principalProps.role, principalName).arn(); } } } throw new Error(`Unable to construct principal for ${principalName} with provided configuration.`); } private tryParseArn(arnString: string): ArnComponents | undefined { try { return Arn.split(arnString, ArnFormat.NO_RESOURCE_NAME); } catch { return undefined; } } } interface PrincipalIdentity { readonly identity: string; readonly account?: string; }