0

I have two AWS stacks :

one has a dynamoDB table and "exports" (to appConfig) the tableArn, tableName and tableRoleArn (which ideally should allow access to the table).

import { App, Stack, StackProps } from '@aws-cdk/core';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as cdk from '@aws-cdk/core';
import * as appconfig from '@aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam';

export class ExportingStack extends Stack {
    constructor(scope: App, id: string, props: StackProps) {
        super(scope, id, props);

        const table = new dynamodb.Table(this, id, {
            billingMode: dynamodb.BillingMode.PROVISIONED,
            readCapacity: 1,
            writeCapacity: 1,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
            pointInTimeRecovery: true
        });

        const tablePolicy = new PolicyStatement({
            effect: Effect.ALLOW,
            resources: [table.tableArn],
            actions: ['*']
        });
        const role = new Role(this, 'tableRoleArn', {
            assumedBy: new ServicePrincipal('lambda.amazonaws.com')
        });
        role.addToPolicy(
            tablePolicy
        );

        const app = '***';
        const environment = '***';
        const profile = '***';
        const strategy = 'v';

        const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'ConfigurationName', {
            applicationId: app,
            configurationProfileId: profile,
            contentType: 'application/json',
            content: JSON.stringify({
                tableArn: table.tableArn,
                tableName: table.tableName,
                tableRoleArn: role.roleArn
            }),
            description: 'table config'
        });

        const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
            applicationId: app,
            configurationProfileId: profile,
            environmentId: environment,
            configurationVersion: newConfig.ref,
            deploymentStrategyId: strategy
        });
    }
}

The second has a function which I would like to be able to use the appConfig configuration to dynamically access the table.

import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core';
import { LayerVersion, Runtime } from '@aws-cdk/aws-lambda';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import { Effect, PolicyStatement } from '@aws-cdk/aws-iam';

export class ConsumingStack extends Stack {
    constructor(scope: App, id: string, props: StackProps) {
        super(scope, id, props);

        const fn = new NodejsFunction(this, 'foo', {
            runtime: Runtime.NODEJS_12_X,
            handler: 'foo',
            entry: `stack/foo.ts`
        });

        fn.addToRolePolicy(
            new PolicyStatement({
                effect: Effect.ALLOW,
                resources: ['*'],
                actions: [
                    'ssm:*', 
                    'appconfig:*',
                    'sts:*',
                ]
            })
        );

        new CfnOutput(this, 'functionArn', { value: fn.functionArn});

        // https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html
        // https://github.com/aws-samples/aws-appconfig-codepipeline-cdk/blob/main/infrastructure/src/main/kotlin/com/app/config/ServerlessAppStack.kt
        const appConfigLayer = LayerVersion.fromLayerVersionArn(
            this,
            'appconfigLayer',
            'arn:aws:lambda:eu-west-2:282860088358:layer:AWS-AppConfig-Extension:47'
        );

        fn.addLayers(appConfigLayer);
    }
}

and handler

import type { Context } from 'aws-lambda';
import fetch from 'node-fetch';
import { DynamoDB, STS } from 'aws-sdk';
import { Agent } from 'https';

export const foo = async (event: any, lambdaContext: Context): Promise<void> => {
    const application = '*****';
    const environment = '*****';
    const configuration = '*****';

    const response = await fetch(
        `http://localhost:2772/applications/${application}/environments/${environment}/configurations/${configuration}`
    );

    const configurationData = await response.json();

    console.log(configurationData);
    
    const credentials = await assumeRole(configurationData.tableRoleArn);

    const db = new DynamoDB({
        credentials: {
            sessionToken: credentials.sessionToken,
            secretAccessKey: credentials.secretAccessKey,
            accessKeyId: credentials.accessKeyId
        },
        apiVersion: '2012-08-10',
        region: '*****',
        httpOptions: {
            agent: new Agent({ keepAlive: true }),
            connectTimeout: 1000,
            timeout: 5000
        },
        signatureVersion: 'v4',
        maxRetries: 3
    });

    const item = await db
        .getItem({ TableName: configurationData.tableName, Key: { id: { S: 'coolPeople' }, createdAt: { N: '0' } } }, (e) => {
            console.log('e', e);
        })
        .promise();

    console.log('item:', item?.Item?.value?.L);

   
};

/**
 * Assume Role for cross account operations
 */
export const assumeRole = async (tableRoleArn: string): Promise<any> => {

    let params = {
        RoleArn: tableRoleArn,
        RoleSessionName: 'RoleSessionName12345'
    };

    console.info('Assuming Role with params:', params);

    let sts = new STS();

    return new Promise((resolve, reject) => {
        sts.assumeRole(params, (error, data) => {
            if (error) {
                console.log(`Could not assume role, error : ${JSON.stringify(error)}`);
                reject({
                    statusCode: 400,
                    message: error['message']
                });
            } else {
                console.log(`Successfully Assumed Role details data=${JSON.stringify(data)}`);
                resolve({
                    statusCode: 200,
                    body: data
                });
            }
        });
    });
};


The issue is that I get this error when trying to assumeRole within the lambda.

Could not assume role, error : {"message":"User: arn:aws:sts::****:assumed-role/ConsumingStack-fooServiceRole****-***/ConsumingStack-foo****-*** is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::****:role/ExportingStack-tableRoleArn****-***","code":"AccessDenied","time":"2022-02-21T16:06:44.474Z","requestId":"****-***-****-****","statusCode":403,"retryable":false,"retryDelay":26.827985116659757}

So is it possible for a Lambda to dynamically assume a role to access a table from a different stack?

Luke_P
  • 659
  • 2
  • 7
  • 23
  • Yes, Lambda can assume role dynamically during runtime. Looking at the code, I assume that the DynamodB is in a different account, make sure that the policy that you are trying to assume has been added to the "trust relationship" – Rajesh Feb 22 '22 at 05:24
  • Nope, both stacks are in the same account and region. Is there anything in the code which I have overlooked? – Luke_P Feb 22 '22 at 08:55
  • I got it working if I added the ARN of the role of the running Lambda to the trust policy of the tableRole. But the whole idea of this was to completely decouple the two stacks, in this solution the export in stack needs to know the ARN of the consuming stacks lambda role. So the question still stands, is it possible for a Lambda to dynamically (at runtime) assume a role to access a table from a different stack, without the role of the table knowing about the ARN of the role of the Lambda beforehand (deploy time)? – Luke_P Feb 22 '22 at 10:55
  • Maybe, in the trust relationship of the table role can I add something so any Lambda with tags "xyz" can assumeRole? – Luke_P Feb 22 '22 at 10:59

1 Answers1

0

I've got it working by changing the trust relationship of the table role to be arn:aws:iam::${Stack.of(this).account}:root

import { App, Stack, StackProps } from '@aws-cdk/core';
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as cdk from '@aws-cdk/core';
import * as appconfig from '@aws-cdk/aws-appconfig';
import { Effect, PolicyStatement, Role, ArnPrincipal } from '@aws-cdk/aws-iam';

export class ExportingStack extends Stack {
    constructor(scope: App, id: string, props: StackProps) {
        super(scope, id, props);

        const table = new dynamodb.Table(this, id, {
            billingMode: dynamodb.BillingMode.PROVISIONED,
            readCapacity: 1,
            writeCapacity: 1,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
            partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
            sortKey: { name: 'createdAt', type: dynamodb.AttributeType.NUMBER },
            pointInTimeRecovery: true
        });

        const tablePolicy = new PolicyStatement({
            effect: Effect.ALLOW,
            resources: [table.tableArn],
            actions: ['*']
        });

        const role = new Role(this, 'tableRoleArn', {
            assumedBy: new ArnPrincipal(`arn:aws:iam::${Stack.of(this).account}:root`)
        });
        role.addToPolicy(tablePolicy);

        const app = '***';
        const environment = '***';
        const profile = '****';
        const strategy = '****';

        const newConfig = new appconfig.CfnHostedConfigurationVersion(this, 'myConfiguration', {
            applicationId: app,
            configurationProfileId: profile,
            contentType: 'application/json',
            content: JSON.stringify({
                tableArn: table.tableArn,
                tableName: table.tableName,
                tableRoleArn: role.roleArn
            }),
            description: 'table config'
        });

        const cfnDeployment = new appconfig.CfnDeployment(this, 'MyCfnDeployment', {
            applicationId: app,
            configurationProfileId: profile,
            environmentId: environment,
            configurationVersion: newConfig.ref,
            deploymentStrategyId: strategy
        });
    }
}

Luke_P
  • 659
  • 2
  • 7
  • 23