packages/@aws-cdk/aws-glue-alpha/lib/s3-table.ts (172 lines of code) (raw):

import { CfnTable } from 'aws-cdk-lib/aws-glue'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as kms from 'aws-cdk-lib/aws-kms'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; import { Column } from './schema'; import { PartitionIndex, TableBase, TableBaseProps } from './table-base'; import { addConstructMetadata, MethodMetadata } from 'aws-cdk-lib/core/lib/metadata-resource'; /** * Encryption options for a Table. * * @see https://docs.aws.amazon.com/athena/latest/ug/encryption.html */ export enum TableEncryption { /** * Server side encryption (SSE) with an Amazon S3-managed key. * * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html */ S3_MANAGED = 'SSE-S3', /** * Server-side encryption (SSE) with an AWS KMS key managed by the account owner. * * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html */ KMS = 'SSE-KMS', /** * Server-side encryption (SSE) with an AWS KMS key managed by the KMS service. */ KMS_MANAGED = 'SSE-KMS-MANAGED', /** * Client-side encryption (CSE) with an AWS KMS key managed by the account owner. * * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingClientSideEncryption.html */ CLIENT_SIDE_KMS = 'CSE-KMS', } export interface S3TableProps extends TableBaseProps { /** * S3 bucket in which to store data. * * @default one is created for you */ readonly bucket?: s3.IBucket; /** * S3 prefix under which table objects are stored. * * @default - No prefix. The data will be stored under the root of the bucket. */ readonly s3Prefix?: string; /** * The kind of encryption to secure the data with. * * You can only provide this option if you are not explicitly passing in a bucket. * * If you choose `SSE-KMS`, you *can* provide an un-managed KMS key with `encryptionKey`. * If you choose `CSE-KMS`, you *must* provide an un-managed KMS key with `encryptionKey`. * * @default BucketEncryption.S3_MANAGED */ readonly encryption?: TableEncryption; /** * External KMS key to use for bucket encryption. * * The `encryption` property must be `SSE-KMS` or `CSE-KMS`. * * @default key is managed by KMS. */ readonly encryptionKey?: kms.IKey; } /** * A Glue table that targets a S3 dataset. * @resource AWS::Glue::Table */ export class S3Table extends TableBase { /** * Name of this table. */ public readonly tableName: string; /** * ARN of this table. */ public readonly tableArn: string; /** * S3 bucket in which the table's data resides. */ public readonly bucket: s3.IBucket; /** * S3 Key Prefix under which this table's files are stored in S3. */ public readonly s3Prefix: string; /** * The type of encryption enabled for the table. */ public readonly encryption: TableEncryption; /** * The KMS key used to secure the data if `encryption` is set to `CSE-KMS` or `SSE-KMS`. Otherwise, `undefined`. */ public readonly encryptionKey?: kms.IKey; /** * This table's partition indexes. */ public readonly partitionIndexes?: PartitionIndex[]; protected readonly tableResource: CfnTable; constructor(scope: Construct, id: string, props: S3TableProps) { super(scope, id, props); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); this.s3Prefix = props.s3Prefix ?? ''; const { bucket, encryption, encryptionKey } = createBucket(this, props); this.bucket = bucket; this.encryption = encryption; this.encryptionKey = encryptionKey; this.tableResource = new CfnTable(this, 'Table', { catalogId: props.database.catalogId, databaseName: props.database.databaseName, tableInput: { name: this.physicalName, description: props.description || `${this.physicalName} generated by CDK`, partitionKeys: renderColumns(props.partitionKeys), parameters: { 'classification': props.dataFormat.classificationString?.value, 'has_encrypted_data': true, 'partition_filtering.enabled': props.enablePartitionFiltering, ...this.parameters, }, storageDescriptor: { location: `s3://${this.bucket.bucketName}/${this.s3Prefix}`, compressed: this.compressed, storedAsSubDirectories: props.storedAsSubDirectories ?? false, columns: renderColumns(props.columns), inputFormat: props.dataFormat.inputFormat.className, outputFormat: props.dataFormat.outputFormat.className, serdeInfo: { serializationLibrary: props.dataFormat.serializationLibrary.className, }, parameters: props.storageParameters ? props.storageParameters.reduce((acc, param) => { if (param.key in acc) { throw new Error(`Duplicate storage parameter key: ${param.key}`); } const key = param.key; acc[key] = param.value; return acc; }, {} as { [key: string]: string }) : undefined, }, tableType: 'EXTERNAL_TABLE', }, }); this.tableName = this.getResourceNameAttribute(this.tableResource.ref); this.tableArn = this.stack.formatArn({ service: 'glue', resource: 'table', resourceName: `${this.database.databaseName}/${this.tableName}`, }); this.node.defaultChild = this.tableResource; // Partition index creation relies on created table. if (props.partitionIndexes) { this.partitionIndexes = props.partitionIndexes; this.partitionIndexes.forEach((index) => this.addPartitionIndex(index)); } } /** * Grant read permissions to the table and the underlying data stored in S3 to an IAM principal. * * @param grantee the principal */ @MethodMetadata() public grantRead(grantee: iam.IGrantable): iam.Grant { const ret = this.grant(grantee, readPermissions); if (this.encryptionKey && this.encryption === TableEncryption.CLIENT_SIDE_KMS) { this.encryptionKey.grantDecrypt(grantee); } this.bucket.grantRead(grantee, this.generateS3PrefixForGrant()); return ret; } /** * Grant write permissions to the table and the underlying data stored in S3 to an IAM principal. * * @param grantee the principal */ @MethodMetadata() public grantWrite(grantee: iam.IGrantable): iam.Grant { const ret = this.grant(grantee, writePermissions); if (this.encryptionKey && this.encryption === TableEncryption.CLIENT_SIDE_KMS) { this.encryptionKey.grantEncrypt(grantee); } this.bucket.grantWrite(grantee, this.generateS3PrefixForGrant()); return ret; } /** * Grant read and write permissions to the table and the underlying data stored in S3 to an IAM principal. * * @param grantee the principal */ @MethodMetadata() public grantReadWrite(grantee: iam.IGrantable): iam.Grant { const ret = this.grant(grantee, [...readPermissions, ...writePermissions]); if (this.encryptionKey && this.encryption === TableEncryption.CLIENT_SIDE_KMS) { this.encryptionKey.grantEncryptDecrypt(grantee); } this.bucket.grantReadWrite(grantee, this.generateS3PrefixForGrant()); return ret; } protected generateS3PrefixForGrant() { return this.s3Prefix + '*'; } } const readPermissions = [ 'glue:BatchGetPartition', 'glue:GetPartition', 'glue:GetPartitions', 'glue:GetTable', 'glue:GetTables', 'glue:GetTableVersion', 'glue:GetTableVersions', ]; const writePermissions = [ 'glue:BatchCreatePartition', 'glue:BatchDeletePartition', 'glue:CreatePartition', 'glue:DeletePartition', 'glue:UpdatePartition', ]; // map TableEncryption to bucket's SSE configuration (s3.BucketEncryption) const encryptionMappings = { [TableEncryption.S3_MANAGED]: s3.BucketEncryption.S3_MANAGED, [TableEncryption.KMS_MANAGED]: s3.BucketEncryption.KMS_MANAGED, [TableEncryption.KMS]: s3.BucketEncryption.KMS, [TableEncryption.CLIENT_SIDE_KMS]: s3.BucketEncryption.S3_MANAGED, }; // create the bucket to store a table's data depending on the `encryption` and `encryptionKey` properties. function createBucket(table: S3Table, props: S3TableProps) { let bucket = props.bucket; if (bucket && (props.encryption !== undefined && props.encryption !== TableEncryption.CLIENT_SIDE_KMS)) { throw new Error('you can not specify encryption settings if you also provide a bucket'); } const encryption = props.encryption || TableEncryption.S3_MANAGED; let encryptionKey: kms.IKey | undefined; if (encryption === TableEncryption.CLIENT_SIDE_KMS && props.encryptionKey === undefined) { // CSE-KMS should behave the same as SSE-KMS - use the provided key or create one automatically // Since Bucket only knows about SSE, we repeat the logic for CSE-KMS at the Table level. encryptionKey = new kms.Key(table, 'Key'); } else { encryptionKey = props.encryptionKey; } // create the bucket if none was provided if (!bucket) { if (encryption === TableEncryption.CLIENT_SIDE_KMS) { bucket = new s3.Bucket(table, 'Bucket'); } else { bucket = new s3.Bucket(table, 'Bucket', { encryption: encryptionMappings[encryption], encryptionKey, }); encryptionKey = bucket.encryptionKey; } } return { bucket, encryption, encryptionKey, }; } function renderColumns(columns?: Array<Column | Column>) { if (columns === undefined) { return undefined; } return columns.map(column => { return { name: column.name, type: column.type.inputString, comment: column.comment, }; }); }