-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Federico Zambelli <[email protected]> Co-authored-by: rehanvdm <[email protected]>
- Loading branch information
1 parent
cb7d98c
commit b19e69f
Showing
17 changed files
with
3,772 additions
and
641 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"_variables": { | ||
"lastUpdateCheck": 1732194217563 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
176
src/constructs/dlz-lake-formation/dlz-lake-formation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.