Skip to content

Commit

Permalink
feat: organization base
Browse files Browse the repository at this point in the history
> 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
pflorek committed Nov 20, 2022
1 parent 1150ec7 commit fc3fe18
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 0 deletions.
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 {
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) {
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 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;

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: '[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 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

0 comments on commit fc3fe18

Please sign in to comment.