packages/@aws-cdk/aws-s3tables-alpha/lib/table-bucket.ts (214 lines of code) (raw):
import { EOL } from 'os';
import { Construct } from 'constructs';
import * as s3tables from 'aws-cdk-lib/aws-s3tables';
import { TableBucketPolicy } from './table-bucket-policy';
import * as perms from './permissions';
import { validateTableBucketAttributes } from './util';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Resource, IResource, UnscopedValidationError, RemovalPolicy, Token } from 'aws-cdk-lib/core';
import { addConstructMetadata } from 'aws-cdk-lib/core/lib/metadata-resource';
/**
* Interface definition for S3 Table Buckets
*/
export interface ITableBucket extends IResource {
/**
* The ARN of the table bucket.
* @attribute
*/
readonly tableBucketArn: string;
/**
* The name of the table bucket.
* @attribute
*/
readonly tableBucketName: string;
/**
* The accountId containing the table bucket.
* @attribute
*/
readonly account?: string;
/**
* The region containing the table bucket.
* @attribute
*/
readonly region?: string;
/**
* Adds a statement to the resource policy for a principal (i.e.
* account/role/service) to perform actions on this table bucket and/or its
* contents. Use `tableBucketArn` and `arnForObjects(keys)` to obtain ARNs for
* this bucket or objects.
*
* Note that the policy statement may or may not be added to the policy.
* For example, when an `ITableBucket` is created from an existing table bucket,
* it's not possible to tell whether the bucket already has a policy
* attached, let alone to re-use that policy to add more statements to it.
* So it's safest to do nothing in these cases.
*
* @param statement the policy statement to be added to the bucket's
* policy.
* @returns metadata about the execution of this method. If the policy
* was not added, the value of `statementAdded` will be `false`. You
* should always check this value to make sure that the operation was
* actually carried out. Otherwise, synthesis and deploy will terminate
* silently, which may be confusing.
*/
addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult;
/**
* Grant read permissions for this table bucket and its tables
* to an IAM principal (Role/Group/User).
*
* @param identity The principal to allow read permissions to
* @param tableId Allow the permissions to all tables using '*' or to single table by its unique ID.
*/
grantRead(identity: iam.IGrantable, tableId: string): iam.Grant;
/**
* Grant write permissions for this table bucket and its tables
* to an IAM principal (Role/Group/User).
*
* @param identity The principal to allow write permissions to
* @param tableId Allow the permissions to all tables using '*' or to single table by its unique ID.
*/
grantWrite(identity: iam.IGrantable, tableId: string): iam.Grant;
/**
* Grant read and write permissions for this table bucket and its tables
* to an IAM principal (Role/Group/User).
*
* @param identity The principal to allow read and write permissions to
* @param tableId Allow the permissions to all tables using '*' or to single table by its unique ID.
*/
grantReadWrite(identity: iam.IGrantable, tableId: string): iam.Grant;
}
/**
* Unreferenced file removal settings for the this table bucket.
*/
export interface UnreferencedFileRemoval {
/**
* Duration after which noncurrent files should be removed. Should be at least one day.
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-table-buckets-maintenance.html
*
* @default - See S3 Tables User Guide
*/
readonly noncurrentDays?: number;
/**
* Status of unreferenced file removal. Can be Enabled or Disabled.
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-table-buckets-maintenance.html
*
* @default - See S3 Tables User Guide
*/
readonly status?: UnreferencedFileRemovalStatus;
/**
* Duration after which unreferenced files should be removed. Should be at least one day.
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-table-buckets-maintenance.html
*
* @default - See S3 Tables User Guide
*/
readonly unreferencedDays?: number;
}
/**
* Controls whether unreferenced file removal is enabled or disabled.
*/
export enum UnreferencedFileRemovalStatus {
/**
* Enable unreferenced file removal.
*/
ENABLED = 'Enabled',
/**
* Disable unreferenced file removal.
*/
DISABLED = 'Disabled',
}
abstract class TableBucketBase extends Resource implements ITableBucket {
public abstract readonly tableBucketArn: string;
public abstract readonly tableBucketName: string;
/**
* The resource policy associated with this table bucket.
*
* If `autoCreatePolicy` is true, a `TableBucketPolicy` will be created upon the
* first call to addToResourcePolicy(s).
*/
public abstract tableBucketPolicy?: TableBucketPolicy;
/**
* Indicates if a table bucket resource policy should automatically created upon
* the first call to `addToResourcePolicy`.
*/
protected abstract autoCreatePolicy: boolean;
/**
* Adds a statement to the resource policy for a principal (i.e.
* account/role/service) to perform actions on this table bucket and/or its
* contents. Use `tableBucketArn` and `arnForObjects(keys)` to obtain ARNs for
* this bucket or objects.
*
* Note that the policy statement may or may not be added to the policy.
* For example, when an `ITableBucket` is created from an existing table bucket,
* it's not possible to tell whether the bucket already has a policy
* attached, let alone to re-use that policy to add more statements to it.
* So it's safest to do nothing in these cases.
*
* @param statement the policy statement to be added to the bucket's
* policy.
* @returns metadata about the execution of this method. If the policy
* was not added, the value of `statementAdded` will be `false`. You
* should always check this value to make sure that the operation was
* actually carried out. Otherwise, synthesis and deploy will terminate
* silently, which may be confusing.
*/
public addToResourcePolicy(
statement: iam.PolicyStatement,
): iam.AddToResourcePolicyResult {
if (!this.tableBucketPolicy && this.autoCreatePolicy) {
this.tableBucketPolicy = new TableBucketPolicy(this, 'DefaultPolicy', {
tableBucket: this,
});
}
if (this.tableBucketPolicy) {
this.tableBucketPolicy.document.addStatements(statement);
return { statementAdded: true, policyDependable: this.tableBucketPolicy };
}
return { statementAdded: false };
}
public grantRead(identity: iam.IGrantable, tableId: string) {
return this.grant(identity, perms.TABLE_BUCKET_READ_ACCESS, this.tableBucketArn, this.getTableArn(tableId));
}
public grantWrite(identity: iam.IGrantable, tableId: string) {
return this.grant(identity, perms.TABLE_BUCKET_WRITE_ACCESS, this.tableBucketArn, this.getTableArn(tableId));
}
public grantReadWrite(identity: iam.IGrantable, tableId: string) {
return this.grant(identity, perms.TABLE_BUCKET_READ_WRITE_ACCESS, this.tableBucketArn, this.getTableArn(tableId));
}
/**
* Grants the given s3tables permissions to the provided principal
* @returns Grant object
*/
private grant(
grantee: iam.IGrantable,
tableBucketActions: string[],
resourceArn: string,
...otherResourceArns: (string | undefined)[]) {
const resources = [resourceArn, ...otherResourceArns].filter(arn => arn != undefined);
return iam.Grant.addToPrincipalOrResource({
grantee,
actions: tableBucketActions,
resourceArns: resources,
resource: this,
});
}
private getTableArn(tableId: string | undefined) {
return tableId ? `${this.tableBucketArn}/table/${tableId}` : undefined;
}
}
/**
* Parameters for constructing a TableBucket
*/
export interface TableBucketProps {
/**
* Name of the S3 TableBucket.
* @link https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-buckets-naming.html#table-buckets-naming-rules
*/
readonly tableBucketName: string;
/**
* Unreferenced file removal settings for the S3 TableBucket.
* @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3tables-tablebucket-unreferencedfileremoval.html
* @default Enabled with default values
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-table-buckets-maintenance.html
*/
readonly unreferencedFileRemoval?: UnreferencedFileRemoval;
/**
* AWS region that the table bucket exists in.
*
* @default - it's assumed the bucket is in the same region as the scope it's being imported into
*/
readonly region?: string;
/**
* AWS Account ID of the table bucket owner.
*
* @default - it's assumed the bucket belongs to the same account as the scope it's being imported into
*/
readonly account?: string;
/**
* Controls what happens to this table bucket it it stoped being managed by cloudformation.
*
* @default RETAIN
*/
readonly removalPolicy?: RemovalPolicy;
}
/**
* Everything needed to reference a specific table bucket.
* The tableBucketName, region, and account can be provided explicitly
* or will be inferred from the tableBucketArn
*/
export interface TableBucketAttributes {
/**
* AWS region this table bucket exists in
* @default region inferred from scope
*/
readonly region?: string;
/**
* The accountId containing this table bucket
* @default account inferred from scope
*/
readonly account?: string;
/**
* The table bucket name, unique per region
* @default tableBucketName inferred from arn
*/
readonly tableBucketName?: string;
/**
* The table bucket's ARN.
* @default tableBucketArn constructed from region, account and tableBucketName are provided
*/
readonly tableBucketArn?: string;
}
/**
* An S3 table bucket with helpers for associated resource policies
*
* This bucket may not yet have all features that exposed by the underlying CfnTableBucket.
*
* @stateful
* @example
* const sampleTableBucket = new TableBucket(scope, 'ExampleTableBucket', {
* tableBucketName: 'example-bucket',
* // Optional fields:
* unreferencedFileRemoval: {
* noncurrentDays: 123,
* status: UnreferencedFileRemovalStatus.ENABLED,
* unreferencedDays: 123,
* },
* });
*/
export class TableBucket extends TableBucketBase {
/**
* Defines a TableBucket construct from an external table bucket ARN.
*
* @param scope The parent creating construct (usually `this`).
* @param id The construct's name.
* @param tableBucketArn Amazon Resource Name (arn) of the table bucket
*/
public static fromTableBucketArn(scope: Construct, id: string, tableBucketArn: string): ITableBucket {
return TableBucket.fromTableBucketAttributes(scope, id, { tableBucketArn });
}
/**
* Defines a TableBucket construct that represents an external table bucket.
*
* @param scope The parent creating construct (usually `this`).
* @param id The construct's name.
* @param attrs A `TableBucketAttributes` object. Can be manually created.
*/
public static fromTableBucketAttributes(
scope: Construct,
id: string,
attrs: TableBucketAttributes,
): ITableBucket {
const { tableBucketName, region, account, tableBucketArn } = validateTableBucketAttributes(scope, attrs);
TableBucket.validateTableBucketName(tableBucketName);
class Import extends TableBucketBase {
public readonly tableBucketName = tableBucketName!;
public readonly tableBucketArn = tableBucketArn;
public readonly tableBucketPolicy?: TableBucketPolicy;
public readonly region = region;
public readonly account = account;
protected autoCreatePolicy: boolean = false;
/**
* Exports this bucket from the stack.
*/
public export() {
return attrs;
}
}
return new Import(scope, id, {
account,
region,
physicalName: tableBucketName,
});
}
/**
* Throws an exception if the given table bucket name is not valid.
*
* @param bucketName name of the bucket.
*/
public static validateTableBucketName(
bucketName: string | undefined,
) {
if (bucketName == undefined || Token.isUnresolved(bucketName)) {
// the name is a late-bound value, not a defined string, so skip validation
return;
}
const errors: string[] = [];
// Length validation
if (bucketName.length < 3 || bucketName.length > 63) {
errors.push(
'Bucket name must be at least 3 and no more than 63 characters',
);
}
// Character set validation
const illegalCharsetRegEx = /[^a-z0-9-]/;
const allowedEdgeCharsetRegEx = /[a-z0-9]/;
const illegalCharMatch = bucketName.match(illegalCharsetRegEx);
if (illegalCharMatch) {
errors.push(
'Bucket name must only contain lowercase characters, numbers, and hyphens (-)' +
` (offset: ${illegalCharMatch.index})`,
);
}
// Edge character validation
if (!allowedEdgeCharsetRegEx.test(bucketName.charAt(0))) {
errors.push(
'Bucket name must start with a lowercase letter or number (offset: 0)',
);
}
if (
!allowedEdgeCharsetRegEx.test(bucketName.charAt(bucketName.length - 1))
) {
errors.push(
`Bucket name must end with a lowercase letter or number (offset: ${
bucketName.length - 1
})`,
);
}
if (errors.length > 0) {
throw new UnscopedValidationError(
`Invalid S3 table bucket name (value: ${bucketName})${EOL}${errors.join(EOL)}`,
);
}
}
/**
* Throws an exception if the given unreferencedFileRemovalProperty is not valid.
* @param unreferencedFileRemoval configuration for the table bucket
*/
public static validateUnreferencedFileRemoval(
unreferencedFileRemoval?: UnreferencedFileRemoval,
): void {
// Skip validation if property is not defined
if (!unreferencedFileRemoval) {
return;
}
const { noncurrentDays, status, unreferencedDays } = unreferencedFileRemoval;
const errors: string[] = [];
if (noncurrentDays != undefined) {
if (noncurrentDays < 1) {
errors.push('noncurrentDays must be at least 1 day');
}
if (!Number.isInteger(noncurrentDays)) {
errors.push('noncurrentDays must be a whole number');
}
}
if (unreferencedDays != undefined) {
if (unreferencedDays < 1) {
errors.push('unreferencedDays must be at least 1 day');
}
if (!Number.isInteger(noncurrentDays)) {
errors.push('unreferencedDays must be a whole number');
}
}
const allowedStatus = ['Enabled', 'Disabled'];
if (status != undefined && !allowedStatus.includes(status)) {
errors.push('status must be one of \'Enabled\' or \'Disabled\'');
}
if (errors.length > 0) {
throw new UnscopedValidationError(
`Invalid UnreferencedFileRemovalProperty})${EOL}${errors.join(EOL)}`,
);
}
}
/**
* The underlying CfnTableBucket L1 resource
* @internal
*/
private readonly _resource: s3tables.CfnTableBucket;
/**
* The resource policy for this tableBucket.
*/
public readonly tableBucketPolicy?: TableBucketPolicy;
/**
* The unique Amazon Resource Name (arn) of this table bucket
*/
public readonly tableBucketArn: string;
/**
* The name of this table bucket
*/
public readonly tableBucketName: string;
protected autoCreatePolicy: boolean = true;
constructor(scope: Construct, id: string, props: TableBucketProps) {
super(scope, id, {
physicalName: props.tableBucketName,
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
TableBucket.validateTableBucketName(props.tableBucketName);
TableBucket.validateUnreferencedFileRemoval(props.unreferencedFileRemoval);
this._resource = new s3tables.CfnTableBucket(this, id, {
tableBucketName: props.tableBucketName,
unreferencedFileRemoval: {
...props.unreferencedFileRemoval,
noncurrentDays: props.unreferencedFileRemoval?.noncurrentDays,
unreferencedDays: props.unreferencedFileRemoval?.unreferencedDays,
},
});
this.tableBucketName = this.getResourceNameAttribute(this._resource.ref);
this.tableBucketArn = this._resource.attrTableBucketArn;
this._resource.applyRemovalPolicy(props.removalPolicy);
}
}