-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
> basic higher level constructs **features:** - adds higher level constructs `Account`, `OrganizationalUnit`, `Policy` building up the org tree - adds utility construct `OrganizationRoot` to retrieve the root for the first organizational units (singleton `AwsCustomResource`) **todo:** - [] decide how to sequentially chain the organization tree - [] add doc blocks, usage example and howtos - [] improve tests (unit coverage and integ tests) > sequentially chain resources is an important feature. The AWS Organizations API can create accounts only sequentially. Also adding policies, delegating administration and enabling trusted services needs to sequentially chained. Here is a solution that uses the construct tree walking `Aspect`: [https://github.com/pepperize/cdk-organizations/blob/main/src/dependency-chain.ts](https://github.com/pepperize/cdk-organizations/blob/main/src/dependency-chain.ts). Another option could be to chain the dependencies in the `Account` and `OrganizationalUnit` **inversion of parentship:** It could be useful to inverse the parent child relation, for example ```typescript organizationalUnit.addAccount(account); ``` instead of ``` new Account(scope, id, { parent: ou, }); ``` also it could be useful to inverse the policy attachment ```typescript export class Account { public function attachPolicy(policy: IPolicy): void { policy.addAccount(this); } } ``` _Delegation of the attachment could also be useful if explicit dependency chaining is used._ **next (later on):** - add `ScpPolicy`, `BackupPolicy`, `TagPolicy`, `AiPolicy` as flavors of `PolicyBase` - add `Organization` construct to enable AWS Organizations - add enabling `PolicyType`, `DelegatedAdministrator`, `TrustedService` Fixes: #2877
- Loading branch information
Showing
10 changed files
with
424 additions
and
0 deletions.
There are no files selected for viewing
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,87 @@ | ||
import { IResource, Resource } from '@aws-cdk/core'; | ||
import { Construct } from 'constructs'; | ||
import { IOrganizationalUnit } from './organizational-unit'; | ||
import { CfnAccount } from './organizations.generated'; | ||
|
||
export interface IAccount extends IResource { | ||
readonly accountId: string; | ||
readonly accountArn: string; | ||
readonly accountName: string; | ||
readonly email: string; | ||
} | ||
|
||
export interface AccountOptions { | ||
readonly accountName: string; | ||
readonly email: string; | ||
readonly roleName?: string; | ||
} | ||
|
||
export interface AccountProps extends AccountOptions { | ||
readonly parent?: IOrganizationalUnit; | ||
} | ||
|
||
abstract class AccountBase extends Resource implements IAccount { | ||
public abstract readonly accountId: string; | ||
public abstract readonly accountArn: string; | ||
public abstract readonly accountName: string; | ||
public abstract readonly email: string; | ||
|
||
public abstract readonly accountJoinedMethod: string; | ||
public abstract readonly accountJoinedTimestamp: string; | ||
public abstract readonly accountStatus: string; | ||
} | ||
|
||
export interface AccountAttributes extends AccountOptions { | ||
readonly accountId: string; | ||
readonly accountArn: string; | ||
|
||
readonly accountJoinedMethod: string; | ||
readonly accountJoinedTimestamp: string; | ||
readonly accountStatus: string; | ||
} | ||
|
||
export class Account extends AccountBase { | ||
public static fromAccountAttributes(scope: Construct, id: string, attrs: AccountAttributes): IAccount { | ||
class Import extends AccountBase { | ||
public readonly accountId: string = attrs.accountId; | ||
public readonly accountArn: string = attrs.accountArn; | ||
public readonly accountName: string = attrs.accountName; | ||
public readonly email: string = attrs.email; | ||
|
||
public readonly accountJoinedMethod: string = attrs.accountJoinedMethod; | ||
public readonly accountJoinedTimestamp: string = attrs.accountJoinedTimestamp; | ||
public readonly accountStatus: string = attrs.accountStatus; | ||
} | ||
|
||
return new Import(scope, id); | ||
}; | ||
|
||
public readonly accountId: string; | ||
public readonly accountArn: string; | ||
public readonly accountName: string; | ||
public readonly email: string; | ||
|
||
public readonly accountJoinedMethod: string; | ||
public readonly accountJoinedTimestamp: string; | ||
public readonly accountStatus: string; | ||
|
||
public constructor(scope: Construct, id: string, props: AccountProps) { | ||
super(scope, id); | ||
|
||
const resource = new CfnAccount(this, 'Resource', { | ||
accountName: props.accountName, | ||
email: props.email, | ||
roleName: props.roleName ?? 'OrganizationAccountAccessRole', | ||
parentIds: props.parent ? [props.parent.organizationalUnitId] : undefined, | ||
}); | ||
|
||
this.accountId = resource.ref; | ||
this.accountArn = resource.attrArn; | ||
this.accountName = props.accountName; | ||
this.email = props.email; | ||
|
||
this.accountJoinedMethod = resource.attrJoinedMethod; | ||
this.accountJoinedTimestamp = resource.attrJoinedTimestamp; | ||
this.accountStatus = resource.attrStatus; | ||
} | ||
} |
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,2 +1,6 @@ | ||
// AWS::Organizations CloudFormation Resources: | ||
export * from './organizations.generated'; | ||
export * from './account'; | ||
export * from './organization-root'; | ||
export * from './organizational-unit'; | ||
export * from './policy'; |
55 changes: 55 additions & 0 deletions
55
packages/@aws-cdk/aws-organizations/lib/organization-root.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,55 @@ | ||
import { Stack } from '@aws-cdk/core'; | ||
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; | ||
|
||
import { Construct, IConstruct } from 'constructs'; | ||
export interface IOrganizationRoot extends IConstruct { | ||
readonly organizationRootId: string; | ||
} | ||
|
||
export interface OrganizationRootProps {} | ||
|
||
export interface OrganizationRootAttributes { | ||
readonly organizationRootId: string; | ||
} | ||
|
||
export class OrganizationRoot extends Construct implements IOrganizationRoot { | ||
public static fromOrganizationRootAttributes(scope: Construct, id: string, attrs: OrganizationRootAttributes): IOrganizationRoot { | ||
class Import extends Construct implements IOrganizationRoot { | ||
readonly organizationRootId: string = attrs.organizationRootId; | ||
} | ||
|
||
return new Import(scope, id); | ||
} | ||
public static getOrCreate(scope: Construct): IOrganizationRoot { | ||
const stack = Stack.of(scope); | ||
const id ='@aws-cdk/aws-organizations.OrganizationRoot'; | ||
return stack.node.tryFindChild(id) as IOrganizationRoot ?? new OrganizationRoot(stack, id, {}); | ||
} | ||
|
||
public readonly organizationRootId: string; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public constructor(scope: Construct, id: string, props?: OrganizationRootProps) { | ||
super(scope, id); | ||
|
||
props; | ||
|
||
const resource = new AwsCustomResource(this, 'Resource', { | ||
resourceType: 'Custom::OrganizationRoot', | ||
onUpdate: { | ||
service: 'Organizations', | ||
action: 'listRoots', // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Organizations.html#listRoots-property | ||
region: 'us-east-1', | ||
physicalResourceId: PhysicalResourceId.fromResponse('Roots.0.Id'), | ||
}, | ||
installLatestAwsSdk: false, | ||
policy: AwsCustomResourcePolicy.fromSdkCalls({ | ||
resources: AwsCustomResourcePolicy.ANY_RESOURCE, | ||
}), | ||
}); | ||
|
||
this.organizationRootId = resource.getResponseField('Roots.0.Id'); | ||
} | ||
} |
62 changes: 62 additions & 0 deletions
62
packages/@aws-cdk/aws-organizations/lib/organizational-unit.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,62 @@ | ||
import { IResource, Resource } from '@aws-cdk/core'; | ||
import { Construct } from 'constructs'; | ||
import { OrganizationRoot } from './organization-root'; | ||
import { CfnOrganizationalUnit } from './organizations.generated'; | ||
|
||
export interface IOrganizationalUnit extends IResource{ | ||
readonly organizationalUnitName: string; | ||
readonly organizationalUnitId: string; | ||
readonly organizationalUnitArn: string; | ||
} | ||
|
||
export interface OrganizationUnitOptions { | ||
readonly organizationalUnitName: string; | ||
} | ||
|
||
export interface OrganizationalUnitProps extends OrganizationUnitOptions { | ||
readonly parent?: IOrganizationalUnit; | ||
} | ||
|
||
abstract class OrganizationalUnitBase extends Resource implements IOrganizationalUnit { | ||
readonly abstract organizationalUnitName: string; | ||
readonly abstract organizationalUnitId: string; | ||
readonly abstract organizationalUnitArn: string; | ||
} | ||
|
||
export interface OrganizationalUnitAttributes extends OrganizationUnitOptions { | ||
readonly organizationalUnitId: string; | ||
readonly organizationalUnitArn: string; | ||
} | ||
|
||
export class OrganizationalUnit extends OrganizationalUnitBase { | ||
public static fromOrganizationalUnitAttributes(scope: Construct, id: string, attrs: OrganizationalUnitAttributes): IOrganizationalUnit { | ||
return new class extends OrganizationalUnitBase { | ||
readonly organizationalUnitArn: string = attrs.organizationalUnitArn; | ||
readonly organizationalUnitId: string = attrs.organizationalUnitId; | ||
readonly organizationalUnitName: string = attrs.organizationalUnitName; | ||
|
||
constructor() { | ||
super(scope, id); | ||
} | ||
}; | ||
} | ||
readonly organizationalUnitName: string; | ||
readonly organizationalUnitId: string; | ||
readonly organizationalUnitArn: string; | ||
|
||
public constructor(scope: Construct, id: string, props: OrganizationalUnitProps) { | ||
super(scope, id); | ||
|
||
const parentId = props.parent?.organizationalUnitId ?? OrganizationRoot.getOrCreate(this).organizationRootId; | ||
|
||
const resource = new CfnOrganizationalUnit(this, 'Resource', { | ||
name: props.organizationalUnitName, | ||
parentId: parentId, | ||
}); | ||
|
||
this.organizationalUnitName = props.organizationalUnitName; | ||
this.organizationalUnitId = resource.ref; | ||
this.organizationalUnitArn = resource.attrArn; | ||
} | ||
} | ||
|
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,102 @@ | ||
import { IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; | ||
import { Construct } from 'constructs'; | ||
import { IAccount } from './account'; | ||
import { IOrganizationRoot } from './organization-root'; | ||
import { IOrganizationalUnit } from './organizational-unit'; | ||
import { CfnPolicy } from './organizations.generated'; | ||
|
||
export interface IPolicy extends IResource{ | ||
readonly policyName: string; | ||
readonly policyId: string; | ||
readonly policyArn: string; | ||
readonly awsManaged: boolean; | ||
} | ||
|
||
export interface PolicyOptions { | ||
readonly policyName: string; | ||
readonly description: string; | ||
} | ||
|
||
abstract class PolicyBase extends Resource implements IPolicy { | ||
readonly abstract policyName: string; | ||
readonly abstract policyId: string; | ||
readonly abstract policyArn: string; | ||
readonly abstract awsManaged: boolean; | ||
} | ||
|
||
export interface PolicyProps extends PolicyOptions { | ||
readonly policyType: PolicyType; | ||
readonly content: { [key: string]: any }; | ||
readonly targets?: PolicyAttachmentTarget[]; | ||
} | ||
|
||
export interface PolicyAttributes { | ||
readonly policyName: string; | ||
readonly policyId: string; | ||
readonly policyArn: string; | ||
readonly awsManaged: boolean; | ||
} | ||
|
||
export class Policy extends PolicyBase { | ||
public static fromPolicyAttributes(scope: Construct, id: string, attrs: PolicyAttributes): IPolicy { | ||
class Import extends PolicyBase { | ||
readonly policyName: string = attrs.policyName; | ||
readonly policyId: string = attrs.policyId; | ||
readonly policyArn: string = attrs.policyArn; | ||
readonly awsManaged: boolean=attrs.awsManaged; | ||
} | ||
|
||
return new Import(scope, id); | ||
} | ||
|
||
public readonly policyName: string; | ||
public readonly policyId: string; | ||
public readonly policyArn: string; | ||
public readonly awsManaged: boolean; | ||
|
||
private targets: PolicyAttachmentTarget[]; | ||
|
||
public constructor(scope: Construct, id: string, props: PolicyProps) { | ||
super(scope, id); | ||
|
||
this.targets = props.targets ?? []; | ||
|
||
const resource = new CfnPolicy(this, 'Resource', { | ||
name: props.policyName, | ||
description: props.description, | ||
content: Lazy.uncachedString({ produce: () => Stack.of(this).toJsonString(props.content) }), | ||
targetIds: Lazy.uncachedList({ produce: () => this.targets.map((target) => target.targetId) }), | ||
type: props.policyType, | ||
}); | ||
|
||
this.policyName = props.policyName; | ||
this.policyId = resource.ref; | ||
this.policyArn = resource.attrArn; | ||
this.awsManaged = resource.attrAwsManaged as unknown as boolean; | ||
} | ||
} | ||
|
||
export class PolicyAttachmentTarget { | ||
public static ofAccount(account: IAccount) : PolicyAttachmentTarget { | ||
return new PolicyAttachmentTarget(account.accountId); | ||
} | ||
public static ofOrganizationalRoot(organizationRoot: IOrganizationRoot) : PolicyAttachmentTarget { | ||
return new PolicyAttachmentTarget(organizationRoot.organizationRootId); | ||
} | ||
public static ofOrganizationalUnit(organizationalUnit: IOrganizationalUnit) : PolicyAttachmentTarget { | ||
return new PolicyAttachmentTarget(organizationalUnit.organizationalUnitId); | ||
} | ||
|
||
public readonly targetId: string; | ||
|
||
private constructor(targetId: string) { | ||
this.targetId = targetId; | ||
} | ||
} | ||
|
||
export enum PolicyType { | ||
SERVICE_CONTROL_POLICY = 'SERVICE_CONTROL_POLICY', | ||
TAG_POLICY = 'TAG_POLICY', | ||
BACKUP_POLICY = 'BACKUP_POLICY', | ||
AISERVICES_OPT_OUT_POLICY = 'AISERVICES_OPT_OUT_POLICY', | ||
} |
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,31 @@ | ||
import { Template } from '@aws-cdk/assertions'; | ||
import * as cdk from '@aws-cdk/core'; | ||
import { Account, OrganizationalUnit } from '../lib'; | ||
|
||
describe('Account', () => { | ||
it('Should create an account', () => { | ||
// Given | ||
const stack = new cdk.Stack(); | ||
const parent = OrganizationalUnit.fromOrganizationalUnitAttributes(stack, 'OrganizationalUnit', { | ||
organizationalUnitName: 'any-organizational-unit-name', | ||
organizationalUnitId: 'any-organizational-unit-id', | ||
organizationalUnitArn: 'any-organizational-unit-arn', | ||
}); | ||
|
||
// When | ||
new Account(stack, 'Account', { | ||
accountName: 'AnyAccountName', | ||
email: '[email protected]', | ||
parent: parent, | ||
}); | ||
|
||
// Then | ||
const template = Template.fromStack(stack); | ||
template.hasResourceProperties('AWS::Organizations::Account', { | ||
AccountName: 'AnyAccountName', | ||
Email: '[email protected]', | ||
RoleName: 'OrganizationAccountAccessRole', | ||
ParentIds: ['any-organizational-unit-id'], | ||
}); | ||
}); | ||
}); |
17 changes: 17 additions & 0 deletions
17
packages/@aws-cdk/aws-organizations/test/organization-root.test.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,17 @@ | ||
import { Template } from '@aws-cdk/assertions'; | ||
import * as cdk from '@aws-cdk/core'; | ||
import { OrganizationRoot } from '../lib'; | ||
|
||
describe('OrganizationRoot', () => { | ||
it('Should create an organization root', () => { | ||
// Given | ||
const stack = new cdk.Stack(); | ||
|
||
// When | ||
OrganizationRoot.getOrCreate(stack); | ||
|
||
// Then | ||
const template = Template.fromStack(stack); | ||
template.hasResourceProperties('Custom::OrganizationRoot', {}); | ||
}); | ||
}); |
Oops, something went wrong.