-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(organizations): add basic organizations higher level constructs #23001
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} | ||
} |
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'; |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't it be private? |
||
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hoegertn Here is the important lookup needed for the first OUs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, but I was thinking about doing a cx-api lookup and store it in cdk.context.json instead of a CR |
||
} | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure we should create the org for the user... |
||
|
||
const resource = new CfnOrganizationalUnit(this, 'Resource', { | ||
name: props.organizationalUnitName, | ||
parentId: parentId, | ||
}); | ||
|
||
this.organizationalUnitName = props.organizationalUnitName; | ||
this.organizationalUnitId = resource.ref; | ||
this.organizationalUnitArn = resource.attrArn; | ||
} | ||
} | ||
|
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', | ||
} |
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'], | ||
}); | ||
}); | ||
}); |
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', {}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hoegertn Thank you