1
votes

I'm building an application where some data within DynamoDb can be accessed by users over a Rest API.

What I have in mind is:

  1. User accesses API Gateway, authenticated by a Cognito user pool;
  2. API Gateway invokes a Lambda function (written in Python);
  3. Lambda function accesses DynamoDB and returns data.

I'd like to be able to restrict the level of access to DynamoDb according to the user. Initially I thought that the Lambda function could inherit its permissions from the user, but this doesn't work because it needs an execution role.

What is the best way of achieving this? For example, can I pass user information to the Lambda function, which in turn can assume this role before accessing DynamoDb? If so a code example would be appreciated.

3

3 Answers

0
votes

Take a look at SpaceFinder - Serverless Auth Reference App and Use API Gateway Lambda Authorizers

With Cognito you can use RBAC:

Amazon Cognito identity pools assign your authenticated users a set of temporary, limited privilege credentials to access your AWS resources. The permissions for each user are controlled through IAM roles that you create. You can define rules to choose the role for each user based on claims in the user's ID token. You can define a default role for authenticated users. You can also define a separate IAM role with limited permissions for guest users who are not authenticated.

so you can create specific roles for each user, although it would be better to use groups

With Lambda authorisers you create your own policy. An example is in awslabs.

0
votes

In addition to blueCat's answer, I briefly tried giving my Lambda function sts:AssumeRole permissions, and then allowing it to assume the role of the Cognito user that invoked it via the API. I can then use this to get a new set of credentials and carry out some activity with the Cognito user's permissions. Roughly the code inside the lambda is:

def lambda_handler(event, context):
    sts_client = boto3.client('sts')
    role = event['requestContext']['authorizer']['claims']['cognito:roles']
    cognito_role = sts_client.assume_role(
        RoleArn=role,
        RoleSessionName='lambda-session',
        DurationSeconds=3600
    )
    credentials = cognito_role['Credentials']
    sess = boto3.session.Session(
        aws_access_key_id=credentials['AccessKeyId'], 
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )
    # Do something as the assumed user, e.g. access S3
    s3_client = sess.client('s3')
    # Do stuff here...

Although this works I found that there was roughly 0.5s overhead to assume the role and get the S3 client, and I can't re-use this session between invocations of the function because it is user-specific. As such this method didn't really suit my application.

I've decided instead to give my Lambda full access to the relevant DynamoDb tables, and use the Cognito user groups plus a Lambda authorizer to restrict the parts of the API that individual users are able to call.

0
votes

I dealt with this issue also but I implemented my solution with Node.js and I figured that although your question is for a Python implementation, then maybe someone would stumble upon this question looking for an answer in JS and I figured this could help out the next person who comes along.

It sounds like you're trying to come up with an effective Authorization strategy after the user has Authenticated their credentials against your Cognito User Pool using custom attributes.

I created a library that I use to export a few functions that allow me to capture the UserPoolId and the Username for the authenticated user so that I can capture the custom:<attribute> I need within my lambda so that the conditions I have implemented can then consume the API to the remaining AWS Services I need to provide authorization to for each user that is authenticated by my app.

Here is My library:

import AWS from "aws-sdk";
// ensure correct AWS region is set
AWS.config.update({
    region: "us-east-2"
});

// function will parse the user pool id from a string
export function parseUserPoolId(str) {
    let regex = /[^[/]+(?=,)/g;
    let match = regex.exec(str)[0].toString();

    console.log("Here is the user pool id: ", match);

    return match.toString();
}

// function will parse the username from a string
export function parseUserName(str) {
    let regex = /[a-z,A-Z,0-9,-]+(?![^:]*:)/g;
    let match = regex.exec(str)[0].toString();

    console.log("Here is the username: ", match);

    return match.toString();
}

// function retries UserAttributes array from cognito
export function getCustomUserAttributes(upid, un) {
    // instantiate the cognito IdP
    const cognito = new AWS.CognitoIdentityServiceProvider({
        apiVersion: "2016-04-18"
    });

    const params = {
        UserPoolId: upid,
        Username: un
    };

    console.log("UserPoolId....: ", params.UserPoolId);
    console.log("Username....: ", params.Username);

    try {
        const getUser = cognito.adminGetUser(params).promise();
        console.log("GET USER....: ", getUser);
        // return all of the attributes from cognito
        return getUser;
    } catch (err) {
        console.log("ERROR in getCustomUserAttributes....: ", err.message);
        return err;
    }
}

With this library implemented it can now be used by any lambda you need to create an authorization strategy for.

Inside of your lambda, you need to import the library above (I have left out the import statements below, you will need to add those so you can access the exported functions), and you can implement their use as such::

export async function main(event, context) {
  const upId = parseUserPoolId(
    event.requestContext.identity.cognitoAuthenticationProvider
  );
  // Step 2 --> Get the UserName from the requestContext
  const usrnm = parseUserName(
    event.requestContext.identity.cognitoAuthenticationProvider
  );
  // Request body is passed to a json encoded string in
  // the 'event.body'
  const data = JSON.parse(event.body);

  try {
    // TODO: Make separate lambda for AUTHORIZATION
    let res = await getCustomUserAttributes(upId, usrnm);

    console.log("THIS IS THE custom:primaryAccountId: ", res.UserAttributes[4].Value);
    console.log("THIS IS THE custom:ROLE: ", res.UserAttributes[3].Value);
    console.log("THIS IS THE custom:userName: ", res.UserAttributes[1].Value);

    const primaryAccountId = res.UserAttributes[4].Value;

  } catch (err) {
    // eslint-disable-next-line
    console.log("This call failed to getattributes");
    return failure({
      status: false
    });
  }
}


The response from Cognito will provide an array with the custom attributes you need. Console.log the response from Cognito with console.log("THIS IS THE Cognito response: ", res.UserAttributes); and check the index numbers for the attributes you want in your CloudWatch logs and adjust the index needed with:

res.UserAttributes[n]

Now you have an authorization mechanism that you can use with different conditions within your lambda to permit the user to POST to DynamoDB, or use any other AWS Services from your app with the correct authorization for each authenticated user.