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

WIP: Add Optin Regions #588

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"author": "Olaf Conijn",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-account": "^3.637.0",
"@aws-sdk/client-cloudformation": "^3.637.0",
"@aws-sdk/client-ec2": "^3.637.0",
"@aws-sdk/client-eventbridge": "^3.637.0",
Expand Down
34 changes: 32 additions & 2 deletions src/aws-provider/aws-organization-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import * as IAM from '@aws-sdk/client-iam';
import * as Organizations from '@aws-sdk/client-organizations';
import * as Support from '@aws-sdk/client-support';
import * as STS from '@aws-sdk/client-sts';
import * as Account from "@aws-sdk/client-account";
import { AwsUtil } from '../util/aws-util';
import { ConsoleUtil } from '../util/console-util';
import { GetOrganizationAccessRoleInTargetAccount, ICrossAccountConfig } from './aws-account-access';
import { performAndRetryIfNeeded } from './util';

export type AWSObjectType = 'Account' | 'OrganizationalUnit' | 'Policy' | string;
export interface OptInRegion {
RegionName?: string;
}

export type OptInRegions = OptInRegion[];

export type SupportLevel = 'enterprise' | 'business' | 'developer' | 'basic' | string;
interface IAWSTags {
Expand All @@ -31,6 +37,11 @@ interface IAWSAccountWithIAMAttributes {
interface IAWSAccountWithSupportLevel {
SupportLevel?: SupportLevel;
}

interface IAWSAccountWithOptinRegions {
OptInRegions?: OptInRegions;
}

interface IObjectWithParentId {
ParentId: string;
}
Expand All @@ -57,7 +68,7 @@ interface IPolicyTargets {
}

export type AWSPolicy = Organizations.Policy & IPolicyTargets & IAWSObject;
export type AWSAccount = Organizations.Account & IAWSAccountWithTags & IAWSAccountWithSupportLevel & IAWSAccountWithIAMAttributes & IObjectWithParentId & IObjectWithPolicies & IAWSObject & IAWSObjectWithPartition;
export type AWSAccount = Organizations.Account & IAWSAccountWithTags & IAWSAccountWithSupportLevel & IAWSAccountWithOptinRegions & IAWSAccountWithIAMAttributes & IObjectWithParentId & IObjectWithPolicies & IAWSObject & IAWSObjectWithPartition;
export type AWSOrganizationalUnit = Organizations.OrganizationalUnit & IObjectWithParentId & IObjectWithPolicies & IObjectWithAccounts & IAWSObject & IObjectWitOrganizationalUnits & IAWSObjectWithPartition;
export type AWSRoot = Organizations.Root & IObjectWithPolicies & IObjectWitOrganizationalUnits;

Expand Down Expand Up @@ -247,13 +258,15 @@ export class AwsOrganizationReader {
let alias: string;
let passwordPolicy: IAM.PasswordPolicy;
let supportLevel = 'basic';
let optInRegions: OptInRegions;

try {
[tags, alias, passwordPolicy, supportLevel] = await Promise.all([
[tags, alias, passwordPolicy, supportLevel, optInRegions] = await Promise.all([
AwsOrganizationReader.getTagsForAccount(that, acc.Id),
AwsOrganizationReader.getIamAliasForAccount(that, acc.Id),
AwsOrganizationReader.getIamPasswordPolicyForAccount(that, acc.Id),
AwsOrganizationReader.getSupportLevelForAccount(that, acc.Id),
AwsOrganizationReader.getOptInRegionsForAccount(that, acc.Id),
]);

} catch (err) {
Expand All @@ -276,6 +289,7 @@ export class AwsOrganizationReader {
Alias: alias,
PasswordPolicy: passwordPolicy,
SupportLevel: supportLevel,
OptInRegions: optInRegions,
};

const parentOU = organizationalUnits.find(x => x.Id === req.ParentId);
Expand All @@ -297,6 +311,22 @@ export class AwsOrganizationReader {
}
}

private static async getOptInRegionsForAccount(that: AwsOrganizationReader, accountId: string): Promise<OptInRegions> {
try {
await that.organization.getValue();
const targetRoleConfig = await GetOrganizationAccessRoleInTargetAccount(that.crossAccountConfig, accountId);
const accountClient: Account.AccountClient = AwsUtil.GetAccountService(accountId, targetRoleConfig.role, targetRoleConfig.viaRole, that.isPartition);

const command = await accountClient.send( new Account.ListRegionsCommand({RegionOptStatusContains: ["ENABLED"],}));
return command.Regions;
} catch (err) {
if (err instanceof Account.AccountServiceException) {
return undefined;
}
throw err;
}
}

private static async getSupportLevelForAccount(that: AwsOrganizationReader, accountId: string): Promise<SupportLevel> {
try {
await that.organization.getValue();
Expand Down
1 change: 1 addition & 0 deletions src/aws-provider/aws-organization-writer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Organizations from '@aws-sdk/client-organizations';
import * as IAM from '@aws-sdk/client-iam';
import * as Account from "@aws-sdk/client-account";
import { CreateCaseCommand } from '@aws-sdk/client-support';
import { STSServiceException } from '@aws-sdk/client-sts';
import { AwsUtil, passwordPolicyEquals } from '../util/aws-util';
Expand Down
12 changes: 11 additions & 1 deletion src/parser/model/account-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface IAccountProperties {
SupportLevel?: string;
OrganizationAccessRoleName?: string;
BuildAccessRoleName?: string;
OptInRegions?: any;
}

export class AccountResource extends Resource {
Expand All @@ -36,6 +37,7 @@ export class AccountResource extends Resource {
public supportLevel?: string;
public organizationAccessRoleName?: string;
public buildAccessRoleName?: string;
public optInRegions?:any;
private props?: IAccountProperties;

constructor(root: TemplateRoot, id: string, resource: IResource) {
Expand All @@ -60,13 +62,21 @@ export class AccountResource extends Resource {
this.supportLevel = this.props.SupportLevel;
this.organizationAccessRoleName = this.props.OrganizationAccessRoleName;
this.buildAccessRoleName = this.props.BuildAccessRoleName;
this.optInRegions = this.props.OptInRegions;

if (this.supportLevel !== undefined) {
if (!['basic', 'developer', 'business', 'enterprise'].includes(this.supportLevel)) {
throw new OrgFormationError(`Unexpected value for SupportLevel on account ${id}. Found: ${this.supportLevel}, Exported one of 'basic', 'developer', 'business', 'enterprise'.`);
}
}

// add this later so existing regions do not get "enabled"
// if (this.optInRegions !== undefined) {
// if (!['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', 'ap-south-1', ].includes(this.optInRegions)) {
// throw new OrgFormationError(`Unexpected value for SupportLevel on account ${id}. Found: ${this.optInRegions}, Exported one of 'basic', 'developer', 'business', 'enterprise'.`);
// }
// }

if (typeof this.accountId === 'number') {
this.accountId = '' + this.accountId;
}
Expand All @@ -79,7 +89,7 @@ export class AccountResource extends Resource {
this.organizationAccessRoleName = this.props.OrganizationAccessRoleName;

super.throwForUnknownAttributes(resource, id, 'Type', 'Properties');
super.throwForUnknownAttributes(this.props, id, 'RootEmail', 'AccountName', 'AccountId', 'Alias', 'PartitionAlias', 'PartitionAccountId', 'ServiceControlPolicies', 'Tags', 'PasswordPolicy', 'SupportLevel', 'OrganizationAccessRoleName', 'BuildAccessRoleName');
super.throwForUnknownAttributes(this.props, id, 'RootEmail', 'AccountName', 'AccountId', 'Alias', 'PartitionAlias', 'PartitionAccountId', 'ServiceControlPolicies', 'Tags', 'PasswordPolicy', 'SupportLevel', 'OrganizationAccessRoleName', 'BuildAccessRoleName', 'OptInRegions');
}

public calculateHash(): string {
Expand Down
24 changes: 24 additions & 0 deletions src/util/aws-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as ini from 'ini';
import { IAMClient, IAMClientConfig } from '@aws-sdk/client-iam';
import { v4 as uuid } from 'uuid';
import { SupportClient, SupportClientConfig } from '@aws-sdk/client-support';
import { AccountClient, AccountClientConfig } from "@aws-sdk/client-account";
import { AwsCredentialIdentity, AwsCredentialIdentityProvider, Provider } from '@smithy/types';
import { fromEnv, fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import { DescribeOrganizationCommand, DescribeOrganizationCommandOutput, OrganizationsClient, OrganizationsClientConfig } from '@aws-sdk/client-organizations';
Expand Down Expand Up @@ -399,6 +400,28 @@ export class AwsUtil {
return this.IamServiceCache[cacheKey];
}

public static GetAccountService(accountId: string, roleInTargetAccount?: string, viaRoleArn?: string, isPartition?: boolean): AccountClient {
AwsUtil.throwIfNowInitiazized();
const { cacheKey, provider } = AwsUtil.GetCredentialProviderWithRoleAssumptions({
accountId,
roleInTargetAccount,
viaRoleArn,
isPartition,
});
const config: AccountClientConfig = {
region: (isPartition) ? this.partitionRegion : AwsUtil.GetDefaultRegion(),
credentials: provider,
defaultsMode: 'standard',
retryMode: 'standard',
maxAttempts: 6,
};
if (this.AccountServiceCache[cacheKey]) {
return this.AccountServiceCache[cacheKey];
}
this.AccountServiceCache[cacheKey] = new AccountClient(config);
return this.AccountServiceCache[cacheKey];
}

/**
* Returns an authenticated CloudFormationClient in the provided accountId and region assuming the role provided.
*
Expand Down Expand Up @@ -630,6 +653,7 @@ export class AwsUtil {
private static largeTemplateBucketName: string | undefined;
private static partitionCredentials: AwsCredentialIdentity | undefined;
private static buildProcessAccountId: string;
private static AccountServiceCache: Record<string, AccountClient> = {};
private static IamServiceCache: Record<string, IAMClient> = {};
private static SupportServiceCache: Record<string, SupportClient> = {};
private static OrganizationsServiceCache: Record<string, OrganizationsClient> = {};
Expand Down
10 changes: 10 additions & 0 deletions src/writer/default-template-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@ export class DefaultTemplateWriter {
}
}

if (account.OptInRegions) {
const regions = account.OptInRegions;
if (regions.length > 0) {
const regionList = regions
.filter(region => region.RegionName)
.map(region => region.RegionName);
lines.push(new ListLine('OptInRegions', regionList, 6));
}
}

lines.push(new ListLine('ServiceControlPolicies', policiesList, 6));

lines.push(new EmptyLine());
Expand Down
36 changes: 18 additions & 18 deletions test/unit-tests/plugin/impl/cdk-build-task-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,24 @@ describe('when creating cdk plugin', () => {

});

describe('when validating task', () => {
let plugin: CdkBuildTaskPlugin;
let commandArgs: ICdkCommandArgs;

beforeEach(() => {
plugin = new CdkBuildTaskPlugin();
commandArgs = plugin.convertToCommandArgs( {
FilePath: './tasks.yaml',
Type: 'cdk',
MaxConcurrentTasks: 1,
FailedTaskTolerance: 4,
LogicalName: 'test-task',
Path: './',
TaskRoleName: 'TaskRole',
OrganizationBinding: { IncludeMasterAccount: true}},
{ organizationFile: './organization.yml'} as any);
});
});
// describe('when validating task', () => {
// let plugin: CdkBuildTaskPlugin;
// let commandArgs: ICdkCommandArgs;

// beforeEach(() => {
// plugin = new CdkBuildTaskPlugin();
// commandArgs = plugin.convertToCommandArgs( {
// FilePath: './tasks.yaml',
// Type: 'cdk',
// MaxConcurrentTasks: 1,
// FailedTaskTolerance: 4,
// LogicalName: 'test-task',
// Path: './',
// TaskRoleName: 'TaskRole',
// OrganizationBinding: { IncludeMasterAccount: true}},
// { organizationFile: './organization.yml'} as any);
// });
// });

describe('when resolving attribute expressions on update', () => {
let spawnProcessForAccountSpy: jest.SpyInstance;
Expand Down
38 changes: 19 additions & 19 deletions test/unit-tests/plugin/impl/sls-build-task-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,25 @@ describe('when creating sls plugin', () => {
});


describe('when validating task', () => {
let plugin: SlsBuildTaskPlugin;
let commandArgs: ISlsCommandArgs;

beforeEach(() => {
plugin = new SlsBuildTaskPlugin();
commandArgs = plugin.convertToCommandArgs( {
FilePath: './tasks.yaml',
Type: 'update-serverless.com',
MaxConcurrentTasks: 1,
FailedTaskTolerance: 4,
LogicalName: 'test-task',
Path: './',
Config: './README.md',
TaskRoleName: 'TaskRole',
OrganizationBinding: { IncludeMasterAccount: true}},
{ organizationFile: './organization.yml'} as any);
});
});
// describe('when validating task', () => {
// let plugin: SlsBuildTaskPlugin;
// let commandArgs: ISlsCommandArgs;

// beforeEach(() => {
// plugin = new SlsBuildTaskPlugin();
// commandArgs = plugin.convertToCommandArgs( {
// FilePath: './tasks.yaml',
// Type: 'update-serverless.com',
// MaxConcurrentTasks: 1,
// FailedTaskTolerance: 4,
// LogicalName: 'test-task',
// Path: './',
// Config: './README.md',
// TaskRoleName: 'TaskRole',
// OrganizationBinding: { IncludeMasterAccount: true}},
// { organizationFile: './organization.yml'} as any);
// });
// });


describe('when resolving attribute expressions on update', () => {
Expand Down
Loading