Skip to content
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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/@aws-cdk/aws-organizations/lib/account.ts
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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hoegertn Thank you

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;
}
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-organizations/lib/index.ts
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 packages/@aws-cdk/aws-organizations/lib/organization-root.ts
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) {

Choose a reason for hiding this comment

The 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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hoegertn Here is the important lookup needed for the first OUs

Copy link
Contributor

Choose a reason for hiding this comment

The 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

}
}
62 changes: 62 additions & 0 deletions packages/@aws-cdk/aws-organizations/lib/organizational-unit.ts
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;

Choose a reason for hiding this comment

The 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;
}
}

102 changes: 102 additions & 0 deletions packages/@aws-cdk/aws-organizations/lib/policy.ts
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',
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-organizations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@
},
"dependencies": {
"@aws-cdk/core": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"constructs": "^10.0.0"
},
"peerDependencies": {
"@aws-cdk/core": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"constructs": "^10.0.0"
},
"engines": {
Expand Down
31 changes: 31 additions & 0 deletions packages/@aws-cdk/aws-organizations/test/account.test.ts
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: 'any-email+any-suffix@domain.any',
parent: parent,
});

// Then
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Organizations::Account', {
AccountName: 'AnyAccountName',
Email: 'any-email+any-suffix@domain.any',
RoleName: 'OrganizationAccountAccessRole',
ParentIds: ['any-organizational-unit-id'],
});
});
});
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-organizations/test/organization-root.test.ts
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', {});
});
});
Loading