Skip to content

Commit

Permalink
feat: add lake formation (#77)
Browse files Browse the repository at this point in the history
Co-authored-by: Federico Zambelli <[email protected]>
Co-authored-by: rehanvdm <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent cb7d98c commit b19e69f
Show file tree
Hide file tree
Showing 17 changed files with 3,772 additions and 641 deletions.
1 change: 1 addition & 0 deletions .gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,6 @@ project.gitignore.addPatterns('*.js');
project.gitignore.addPatterns('*.js.map');
project.gitignore.addPatterns('*.d.ts');
project.gitignore.addPatterns('*.DS_Store');
project.gitignore.addPatterns('.vscode/');

project.synth();
3,891 changes: 3,262 additions & 629 deletions API.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/.astro/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1732194217563
}
}
24 changes: 24 additions & 0 deletions src/constructs/dlz-lake-formation/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export enum DatabaseAction {
DESCRIBE = 'DESCRIBE',
ALTER = 'ALTER',
DROP = 'DROP',
CREATE_TABLE = 'CREATE_TABLE'
}

export enum TableAction {
DESCRIBE = 'DESCRIBE',
SELECT = 'SELECT',
DELETE = 'DELETE',
INSERT = 'INSERT',
DROP = 'DROP',
ALTER = 'ALTER'
}

export enum TagAction {
DESCRIBE = 'DESCRIBE',
ASSOCIATE = 'ASSOCIATE',
ALTER = 'ALTER',
DROP = 'DROP'
}

export type TagActionExternal = Exclude<TagAction, 'ALTER' | 'DROP'>
176 changes: 176 additions & 0 deletions src/constructs/dlz-lake-formation/dlz-lake-formation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import * as crypto from 'crypto';
import { DefaultStackSynthesizer, Stack } from 'aws-cdk-lib';
import * as lf from 'aws-cdk-lib/aws-lakeformation';
import { Construct } from 'constructs';
import { DatabaseAction, TableAction, TagAction } from './actions';
import { DlzLakeFormationProps, LFTag, LFTagSharable } from './interfaces';

const DEFAULTS: Partial<DlzLakeFormationProps> = {
hybridMode: false,
crossAccountVersion: 4,
};

export class DlzLakeFormation {
private props: DlzLakeFormationProps;
private account: string;
private dataLakeSettings: lf.CfnDataLakeSettings;
private lfTags: Record<string, lf.CfnTag> = {};

constructor(private scope: Construct, private id: string, lfProps: DlzLakeFormationProps) {
this.props = { ...DEFAULTS, ...lfProps };
this.account = Stack.of(scope).account;
this.dataLakeSettings = this.setUpLFSettings();

for (const tag of this.props.tags) {
this.createTag(tag);
this.shareTag(tag);

for (const admin of this.props.admins) {
const tagWithWildcard = { tagKey: tag.tagKey, tagValues: ['*'] };
const allTagActions = [TagAction.ALTER, TagAction.ASSOCIATE, TagAction.DESCRIBE, TagAction.DROP];
this.grantPermissionOnTag(admin, tagWithWildcard, allTagActions, allTagActions);
}
}

for (const permission of this.props.permissions) {
const {
principals,
tags,
databaseActions,
databaseActionsWithGrant = [],
tableActions = [],
tableActionsWithGrant = [],
} = permission;
for (const principal of principals) {
this.grantPermissionOnDatabase(principal, tags, databaseActions, databaseActionsWithGrant);
this.grantPermissionOnTable(principal, tags, tableActions, tableActionsWithGrant);
}
}

for (const tag of Object.values(this.lfTags)) {
tag.addDependency(this.dataLakeSettings);
}
}

private setUpLFSettings() {
const { admins, hybridMode, crossAccountVersion } = this.props;
const synthesizer = Stack.of(this.scope).synthesizer as DefaultStackSynthesizer;
const cfnAdmin = synthesizer.cloudFormationExecutionRoleArn.replace('${AWS::Partition}', 'aws');
const lfAdmins = [{ dataLakePrincipalIdentifier: cfnAdmin }];
for (const admin of admins) {
lfAdmins.push({ dataLakePrincipalIdentifier: admin });
}

// WARN: Currently broken due to AWS API!!! https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/2197
const defaultPermissions = hybridMode
? [{ principal: { dataLakePrincipalIdentifier: 'IAM_ALLOWED_PRINCIPALS' }, permissions: ['ALL'] }]
: [];

return new lf.CfnDataLakeSettings(this.scope, `${this.id}-data-lake-settings`, {
admins: lfAdmins,
createDatabaseDefaultPermissions: defaultPermissions,
createTableDefaultPermissions: defaultPermissions,
parameters: { CROSS_ACCOUNT_VERSION: crossAccountVersion },
});
}

private createTag(tag: LFTagSharable) {
const { tagKey, tagValues } = tag;
const lfTag = new lf.CfnTag(
this.scope,
`${this.id}-lftag-${tagKey}`,
{ tagKey, tagValues, catalogId: this.account },
);
this.lfTags[tagKey] = lfTag;
}

private shareTag(tag: LFTagSharable) {
const { tagKey, tagValues, share } = tag;
if (!share) return;

const { withinAccount = [], withExternalAccount = [] } = share;
const shares = [...withinAccount, ...withExternalAccount];

for (const shareOptions of shares) {
const { principals, specificValues, tagActions, tagActionsWithGrant = [] } = shareOptions;
for (const principal of principals) {
const sharedTag = { tagKey, tagValues: specificValues || tagValues };
this.grantPermissionOnTag(principal, sharedTag, tagActions, tagActionsWithGrant);
}
}
}

private grantPermissionOnTag(
principalIdentifier: string,
tag: LFTag,
permissions: TagAction[],
grantablePermissions: TagAction[],
) {
this._grantPermission(principalIdentifier, tag, 'TAG', permissions, grantablePermissions);
}

private grantPermissionOnDatabase(
principalIdentifier: string,
tags: LFTag[],
permissions: DatabaseAction[],
grantablePermissions: DatabaseAction[],
) {
this._grantPermission(principalIdentifier, tags, 'DATABASE', permissions, grantablePermissions);
}

private grantPermissionOnTable(
principalIdentifier: string,
tags: LFTag[],
permissions: TableAction[],
grantablePermissions: TableAction[],
) {
this._grantPermission(principalIdentifier, tags, 'TABLE', permissions, grantablePermissions);
}

private _grantPermission(
principalIdentifier: string,
tagOrTags: LFTag | LFTag[],
resourceType: 'TAG' | 'DATABASE' | 'TABLE',
permissions: (DatabaseAction | TableAction | TagAction)[],
grantablePermissions: (DatabaseAction | TableAction | TagAction)[],
) {
const tags = Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags];
const tagsHash = this._tagsHashValue(tags);
const baseResourceProps = { catalogId: this.account };
const resource = resourceType === 'TAG'
? { lfTag: { ...baseResourceProps, tagKey: tags[0].tagKey, tagValues: tags[0].tagValues } }
: { lfTagPolicy: { ...baseResourceProps, resourceType: resourceType, expression: tags } };

const permission = new lf.CfnPrincipalPermissions(
this.scope,
`${this.id}-${principalIdentifier}-${resourceType.toLowerCase()}-grant-${tagsHash}`,
{
catalog: this.account,
permissions: permissions,
permissionsWithGrantOption: grantablePermissions,
principal: { dataLakePrincipalIdentifier: principalIdentifier },
resource: resource,
},
);

for (const tag of tags) {
const { tagKey } = tag;
permission.addDependency(this.lfTags[tagKey]);
}
}

private _tagsHashValue(tags: LFTag[]) {
const serializedTags = JSON.stringify(
tags
.map((tag) => ({ key: tag.tagKey, values: tag.tagValues.sort() }))
.sort((a, b) => a.key.localeCompare(b.key)),
);
return crypto
.createHash('md5')
.update(serializedTags)
.digest('hex')
.slice(0, 8);
}


}
3 changes: 3 additions & 0 deletions src/constructs/dlz-lake-formation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './actions';
export * from './dlz-lake-formation';
export * from './interfaces';
117 changes: 117 additions & 0 deletions src/constructs/dlz-lake-formation/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { DatabaseAction, TableAction, TagAction, TagActionExternal } from './actions';
import { Region } from '../../data-landing-zone-types';

export interface LFTag {
readonly tagKey: string;
readonly tagValues: string[];
}

export interface BaseSharedTagProps {
/**
* A list of principal identity ARNs (e.g., AWS accounts, IAM roles/users) that the permissions apply to.
*/
readonly principals: string[];
/**
* OPTIONAL - A list of specific values of the tag that can be shared. All possible values if omitted.
*/
readonly specificValues?: string[];
}

export interface SharedInternal extends BaseSharedTagProps {
/**
* A list of actions that can be performed on the tag.
*/
readonly tagActions: TagAction[];
/**
* A list of actions on the tag with grant option, allowing grantees to further grant these permissions.
*/
readonly tagActionsWithGrant?: TagAction[];
}

export interface SharedExternal extends BaseSharedTagProps {
/**
* A list of actions that can be performed on the tag. Only `TagAction.DESCRIBE` and `TagAction.ASSOCIATE` are allowed.
*/
readonly tagActions: TagActionExternal[];
/**
* A list of actions on the tag with grant option, allowing grantees to further grant these permissions.
*/
readonly tagActionsWithGrant?: TagActionExternal[];
}

export interface ShareProps {
/**
* Configurations for sharing LF-Tags with principals within the same AWS account.
*/
readonly withinAccount?: SharedInternal[];
/**
* Configurations for sharing LF-Tags with external AWS accounts.
*/
readonly withExternalAccount?: SharedExternal[];
}

export interface LFTagSharable extends LFTag {
/**
* OPTIONAL - Configuration detailing how the tag can be shared with specified principals.
*/
readonly share?: ShareProps;
}

export interface LakePermission {
/**
* A list of principal identity ARNs (e.g., AWS accounts, IAM roles/users) that the permissions apply to.
*/
readonly principals: string[];
/**
* LF tags associated with the permissions, used to specify fine-grained access controls.
*/
readonly tags: LFTag[];
/**
* Actions that can be performed on databases, using Lake Formation Tag Based Access Control.
*/
readonly databaseActions: DatabaseAction[];
/**
* OPTIONAL - Actions on databases with grant option, allowing grantees to further grant these permissions.
*/
readonly databaseActionsWithGrant?: DatabaseAction[];
/**
* OPTIONAL - Actions that can be performed on tables, using Lake Formation Lake Formation Tag Based Access Control.
*/
readonly tableActions?: TableAction[];
/**
* OPTIONAL - Actions on tables with grant option, allowing grantees to further grant these permissions.
*/
readonly tableActionsWithGrant?: TableAction[];
}

export interface DlzLakeFormationProps {
/**
* The region where LakeFormation will be created in
*/
readonly region: Region;
/**
* A list of strings representing the IAM role ARNs.
*/
readonly admins: string[];
/**
* OPTIONAL - Select `true` to use both IAM and Lake Formation for data access, or `false` to use Lake Formation only. Defaults to `false`.
* @note Hybrid mode is only recommended for accounts that already have a data lake managed via IAM permissions.
* For new accounts or accounts that don't have a data lake yet, it is strongly recommended to use Lake Formation only.
* @note `false` is currently not working due to issue with AWS API.
* You will have do disable hybrid mode manually via the AWS console.
* See {@link https://github.com/pulumi/pulumi-aws/issues/4366}
*/
readonly hybridMode?: boolean;
/**
* OPTIONAL - Version for cross-account data sharing. Defaults to `4`. Read more {@link https://docs.aws.amazon.com/lake-formation/latest/dg/cross-account.html | here}.
*/
readonly crossAccountVersion?: 1 | 2 | 3 | 4;
/**
* A list of Lake Formation tags that can be shared across accounts and principals.
*/
readonly tags: LFTagSharable[];
/**
* A list of permission settings, specifying which Lake Formation permissions apply to which principals.
*/
readonly permissions: LakePermission[];
}
11 changes: 6 additions & 5 deletions src/constructs/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from './account-chatbots';
export * from './dlz-budget';
export * from './dlz-control-tower-control';
export * from './dlz-lake-formation';
export * from './dlz-ssm-reader';
export * from './dlz-stack';
export * from './dlz-vpc';
export * from './dlz-control-tower-control';
export * from './iam-identity-center';
export * from './organization-policies';
export * from './dlz-budget';
export * from './account-chatbots';
export * from './dlz-ssm-reader';
export * from './iam-identity-center';
8 changes: 7 additions & 1 deletion src/data-landing-zone-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { InstanceType } from 'aws-cdk-lib/aws-ec2/lib/instance-types';
import * as iam from 'aws-cdk-lib/aws-iam';
import {
DlzBudgetProps,
DlzAccountNetworks,
DlzBudgetProps,
DlzControlTowerStandardControls,
DlzLakeFormationProps,
DlzSsmReaderStackCache,
DlzStackProps,
DlzTag,
Expand Down Expand Up @@ -207,6 +208,11 @@ export interface DLzAccount {
* This will override the organization level defaultNotification.
*/
readonly defaultNotification?: NotificationDetailsProps;

/**
* LakeFormation settings and tags
*/
readonly lakeFormation?: DlzLakeFormationProps[];
}

export enum Ou {
Expand Down
Loading

0 comments on commit b19e69f

Please sign in to comment.