AWS CDK testing references - amazon-web-services

I am trying to unit-test my CDK application. I have a role created and I want to assure that it has all the policies assigned. As roles and policies are different resources, policies are not available from Cloud Formation Role resource. Role only has a reference to the policy:
"MyRole4CBCE4C9": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
...
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Ref": "MyPolicyC18AB378"
}
]
},
In the test I have:
expectCDK(stack).to(haveResource("AWS::IAM::Role", {
AssumeRolePolicyDocument: {
Statement: [
...
],
Version: "2012-10-17",
},
}
));
How can I validate that this exact role has correct policy? Steps I have in my head are as follows:
Get "Ref" from the Role properties
Find Policy by this reference
Assert all the necessary data in Policy
However, it seems that CDK does not provide functions to get the element by its logical id and to get resource from haveResource as an object.
What is CDK way to approach this kind of testing?
UPD: seems like I can approach it with StackInspector, though I still wonder, what is the true way for this.

Scenario 1: you're creating a role and a policy in the same stack, and the policy will not be reused in any other stack.
In this case use iam.Policy and attach it in-line:
export class CdkGetLogicalIdExampleStack extends cdk.Stack {
role: iam.IRole;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const policy = new iam.Policy(this, 'policy', {
policyName: 'policy',
statements: [
new iam.PolicyStatement({
actions: ["s3:*"],
resources: ['*'],
effect: iam.Effect.ALLOW
})
]
});
this.role = new iam.Role(this, 'my-role', {
roleName: 'my-role',
assumedBy: new iam.AccountPrincipal(this.account),
});
// Alternatively: this.role.attachInlinePolicy(policy);
policy.attachToRole(this.role);
}
}
The role is kept as stack's attribute, so it's easy to refer to it in tests:
test('Empty Stack', () => {
const app = new cdk.App();
const stack = new CdkGetLogicalIdExample.CdkGetLogicalIdExampleStack(app, 'MyTestStack');
const roleId = stack.getLogicalId(stack.role.node.findChild('Resource') as cdk.CfnElement);
expectCDK(stack).to(haveResource("AWS::IAM::Policy", {
"PolicyDocument": {
"Statement": [
{
"Action": "s3:*",
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": "policy",
"Roles": [
{
"Ref": roleId
}
]
}));
});
Scenario 2: you're creating a policy that can be used in various stacks, so you want to make it a managed policy.
Testing this scenario is a little bit more complex because iam.ManagedPolicy implements the IManagedPolicy interface, which only provides the managedPolicyArn attribute (I am using CDK 1.124.0).
Nevertheless, the iam.ManagedPolicy extends the cdk.Resource, so we can trick the casting mechanism of TypeScript in the following way:
export class CdkGetLogicalIdExampleStack extends cdk.Stack {
// note the type here
managedPolicy: cdk.Resource | iam.IManagedPolicy;
role: iam.IRole;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
this.managedPolicy = new iam.ManagedPolicy(this, 'managed-policy', {
managedPolicyName: 'managed-policy',
document: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: ["dynamodb:*"],
resources: ["*"],
effect: iam.Effect.ALLOW
})
]
})
});
this.role = new iam.Role(this, 'my-role', {
roleName: 'my-role',
assumedBy: new iam.AccountPrincipal(this.account),
});
// here we need to cast to iam.IManagedPolicy
this.role.addManagedPolicy(this.managedPolicy as iam.IManagedPolicy);
}
}
Now, testing it is possible because we can access managedPolicy attribute as cdk.Resource:
const roleId = stack.getLogicalId(stack.role.node.findChild('Resource') as cdk.CfnElement);
// here we do the casting to cdk.Resource
const managedPolicyResource = stack.managedPolicy as cdk.Resource;
const managedPolicyId = stack.getLogicalId(managedPolicyResource.node.findChild('Resource') as cdk.CfnElement);
expectCDK(stack).to(haveResource("AWS::IAM::ManagedPolicy", {
"PolicyDocument": {
"Statement": [
{
"Action": "dynamodb:*",
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"Description": "",
"ManagedPolicyName": "managed-policy",
"Path": "/"
}));
expectCDK(stack).to(haveResource("AWS::IAM::Role", {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Ref": managedPolicyId
}
],
"RoleName": "my-role"
}));
In this way you can test that your policies have all the necessary data and the relations between roles and policies are correct.
You can access a full working example here.

Related

How to create an IAM policy with multiple statements using CDK?

I am trying to explore if there is a better way. I just define the IAM policy using policy generator and then use the following --
const policyDocument = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FirstStatement",
"Effect": "Allow",
"Action": ["iam:ChangePassword"],
"Resource": "*"
},
{
"Sid": "SecondStatement",
"Effect": "Allow",
"Action": [
"s3:List*",
"s3:Get*"
],
"Resource": [
"arn:aws:s3:::confidential-data",
"arn:aws:s3:::confidential-data/*"
],
"Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}
}
]
};
Then following needs to defined --
const customPolicyDocument = iam.PolicyDocument.fromJson(policyDocument);
const newManagedPolicy = new ManagedPolicy(stack, 'MyNewManagedPolicy', {
document: customPolicyDocument
});
const newPolicy = new Policy(stack, 'MyNewPolicy', {
document: customPolicyDocument
});
Finally, I create a role and attach the policy -
const TestBucketRole = new iam.Role(this, 'TestBucketRole', {
assumedBy: new iam.ArnPrincipal('arn:aws:iam::123456789012:user/user1'),
roleName: "test-role-cdk"
})
TestBucketRole.attachInlinePolicy(newPolicy);
Is there a better way of doing this ?
you can use CDK constructs to iam.PolicyDocument and iam.PolicyStatement to achieve the same thing:
import * as iam from "#aws-cdk/aws-iam";
let policy = new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["iam:ChangePassword"],
resources: ["*"],
}),
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["iam:ChangePassword"],
resources: ["*"],
}),
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["s3:List*", "s3:Get*"],
resources: [
"arn:aws:s3:::confidential-data",
"arn:aws:s3:::confidential-data/*",
],
conditions: {
Bool: { "aws:MultiFactorAuthPresent": "true" },
},
}),
],
});
what I like about using CDK constructs instead of JSON is the TypeScript property/type checking and autocomplete.
but in the end, they are interchangeable!

AWS CDK : Why S3 bucket policy occurs error

I try to deploy S3 bucket by CDK in TypeScript. When the code is executed, an error occurs even if I have a privilige of administrator. Anyone knows the reason?
export class S3Stack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const s3Example = new s3.Bucket(this, 'bucket', {
versioned: false,
bucketName: 'bucket',
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY
});
const s3ExamplePolicyDocument = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "FirstStatement",
"Effect": "Allow",
"Action": [
"s3:List*",
"s3:Get*"
],
"Resource": [
"arn:aws:s3:::bucket",
"arn:aws:s3:::bucket/*"
],
"Principal": "*",
}
]
};
const s3ExamplePolicy = iam.PolicyDocument.fromJson(s3ExamplePolicyDocument);
new s3.CfnBucketPolicy(this, 'bucketpolicy', {
bucket: s3Example.bucketName,
policyDocument: s3ExamplePolicy
});
}
}
Error message
API: s3:PutBucketPolicy Access Denied
I found the solution. This is caused because I try to apply public permission to non-public bucket. After I modified s3ExamplePolicyDocument, it works

Pulumi: How to serialize Output<string>[] to JSON

I want to allow Lambda service to create a deployment inside my VPC, thus I have subnet ids array of type Output<string>[] that I want to put into role policy as follows:
export const createNetworkInterfacePolicy = new aws.iam.RolePolicy(
"network-interface-policy-2",
{
policy: pulumi.interpolate `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ec2:CreateNetworkInterfacePermission"],
"Resource": [
"arn:aws:ec2:${region}:${callerIdentity.accountId}:network-interface/*"
],
"Condition": {
"StringEquals": {
"ec2:Subnet": ${JSON.stringify(vpc.vpcPrivateSubnetIds.map(item => item.apply(JSON.stringify)))},
"ec2:AuthorizedService": "lambda.amazonaws.com"
}
}
}
]
}`,
role: deploymentRole
}
);
Unfortunately what I end up with is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterfacePermission"
],
"Resource": [
"arn:aws:ec2:us-east-2:removedAccountId:network-interface/*"
],
"Condition": {
"StringEquals": {
"ec2:Subnet": [
"Calling [toJSON] on an [Output<T>] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://pulumi.io/help/outputs for more details.\nThis function may throw in a future version of #pulumi/pulumi.",
"Calling [toJSON] on an [Output<T>] is not supported.\n\nTo get the value of an Output as a JSON value or JSON string consider either:\n 1: o.apply(v => v.toJSON())\n 2: o.apply(v => JSON.stringify(v))\n\nSee https://pulumi.io/help/outputs for more details.\nThis function may throw in a future version of #pulumi/pulumi."
],
"ec2:AuthorizedService": "lambda.amazonaws.com"
}
}
}
]
}
I tried many combinations but none of them work. How do I generate JSON array from Output<string>[]?
Sometimes it's easiest to wrap an apply around the entire creation of another resource. In this case appTaskPolicy becomes an OutputInstance<aws.iam.Policy> which you can then feed into other parts of your program using it's own Outputs.
You'll need to import * as pulumi from '#pulumi/pulumi'; if you haven't already for this to work
const vpc = awsx.Network.getDefault();
const appTaskPolicyName = named('app-task-policy');
const appTaskPolicy = pulumi.all(vpc.publicSubnetIds).apply(([...subnetIds]) => {
return new aws.iam.Policy(appTaskPolicyName, {
policy: {
Version: '2012-10-17',
Statement: [
{
Action: ['sqs:GetQueueUrl', 'sqs:SendMessage'],
Resource: [
'someresourcearn'
],
Effect: 'Allow',
Condition: {
StringEquals: {
'ec2:Subnet': subnetIds,
'ec2:AuthorizedService': 'lambda.amazonaws.com'
}
}
}
]
}
});
});

AWS CDK IAM Federated and User IamRoleAccess?

i want to know how i can set an assume role policy document to something more complex than a service...
this is what i found till now and maybe this will work:
this.TestRole = new iam.Role(this, "Test", {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
roleName: "TestRole"
})
But i want to add something like this:
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sts:AssumeRole"
],
"Principal": {
"AWS": [
"arn:aws:iam::account1:role/Role1",
"arn:aws:iam::account2:role/Role2"
]
}
},
{
"Effect": "Allow",
"Action": [
"sts:AssumeRoleWithSAML"
],
"Principal": {
"Federated": {
some sub and so on
}
},
"Condition": {
"StringEquals": {
"SAML:aud": some saml stuff
}
}
}
]
},
I have no clue how to achieve this... can you help me?
Ok, its possible to do something like this:
this.TestRole = new iam.Role(this, "Test", {
assumedBy: new iam.FederatedPrincipal(new cdk.FnSub("arn:aws:iam::${AWS::AccountId}:saml-provider/SAMLIDP"), {
"StringEquals": {
"SAML:aud": "https://signin.aws.amazon.com/saml"
}
}, "sts:AssumeRoleWithSAML"),
roleName: parent.getApplicationName().charAt(0).toUpperCase() + parent.getApplicationName().slice(1)
})
that was easy :-/ But now i want to add the two roles with action sts:AssumeRole - i don't know how to add another principal...
Fortunately, https://github.com/aws/aws-cdk/pull/1377 delivered the fix we need. You can now use aws_iam.CompositePrincipal to add multiple Principle including service Principles.
For example, in Python for a Data Pipeline Role:
pipeline_role = aws_iam.Role(
scope=self, id='pipeline-role',
role_name='pipeline',
assumed_by=aws_iam.CompositePrincipal(
aws_iam.ServicePrincipal('datapipeline.amazonaws.com'),
aws_iam.ServicePrincipal('elasticmapreduce.amazonaws.com')
)
)
The documentation for iam.RoleProps#assumedBy mentions that you can access the assume policy using the iam.Role#assumeRolePolicy attribute. You could try something like the following:
this.TestRole = new iam.Role(this, 'Test', {
assumedBy: new iam.FederatedPrincipal(/*...*/)
/* ... */
});
this.TestRole.assumeRolePolicy.addStatement(
new iam.PolicyStatement().allow()
.addAction('sts:AssumeRole')
.addAwsPrincipal('arn:aws:iam::account1:role/Role1')
.addAwsPrincipal('arn:aws:iam::account2:role/Role2')
);

Managed policy for a role in an AWS cloud formation stack

Using AWS, I'm building a cloud formation stack defining:
A Managed Policy called MyPolicy
A Role called MyRole that should attach that policy
The stack will be created by an admin ; and once created, the goal is to allow (from outside the stack) some users to assume MyRole.
My question: How should the role be defined in order to attach that policy ?
The AWS help page of the role properties suggests to use the ManagedPolicyArns, but I get various errors depending on how I refer to MyPolicy:
If I use the GetAtt function to retrieve the policy's arn, I get an error at the template validation:
"ManagedPolicyArns": [ { "Fn::GetAtt" : [ "MyPolicy", "Arn" ] } ]
Template error: resource MyPolicy does not support attribute type Arn in Fn::GetAtt
And If I use the Join function to build the policy's arn, I get an error during the role creation.
"ManagedPolicyArns": [ { "Fn::Join" : [ "", [ "arn:aws:iam::", { "Ref": "AWS::AccountId" }, ":policy/", { "Ref": "MyPolicy" } ] ] } ]
ARN arn:aws:iam::aws:policy/arn:aws:iam::«my-account-id»:policy/MyPolicy is not valid. (Service: AmazonIdentityManagement; Status Code: 400; Error Code: InvalidInput; Request ID: «an-id»)
Below is my stack definition using JSON format:
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Resources" : {
"MyPolicy" : {
"Type": "AWS::IAM::ManagedPolicy",
"Properties": {
"ManagedPolicyName" : "MyPolicy",
"PolicyDocument" : {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [ "s3:*" ],
"Resource": "arn:aws:s3:::the-bucket"
}
]
}
}
},
"MyRole" : {
"Type": "AWS::IAM::Role",
"RoleName": "MyRole",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": {"Fn::Join" : [ "", [ "arn:aws:iam::", { "Ref": "AWS::AccountId" }, ":root" ] ] } },,
"Action": [ "sts:AssumeRole" ]
}
]
},
"ManagedPolicyArns": [
{ "Fn::GetAtt" : [ "MyPolicy", "Arn" ] }
]
}
}
}
{"Ref": "MyPolicy"} will return the ARN of the managed policy created by your stack. Your error message indicates that. Also, check this AWS documentation.