Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DynamoDB w/ Serverless, using Fn::GetRef to reference global secondary index

I have an API/service I'm defining with a DynamoDB table. I have a couple indexes (defined as Global Secondary Index) to support a couple queries. I have the table designed, with the GSI definitions, and what looks like correct queries. However, I get this exception when making the query:

{ AccessDeniedException: User: arn:aws:sts::OBSCURED:assumed-role/chatroom-application-dev-us-east-1-lambdaRole/chatroom-application-dev-getRoomMessages is not authorized to perform: dynamodb:Query on resource: arn:aws:dynamodb:us-east-1:OBSCURED:table/messages-table-dev/index/roomIndex
at Request.extractError (/var/task/node_modules/aws-sdk/lib/protocol/json.js:48:27)
at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:105:20)
at Request.emit (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:77:10)
at Request.emit (/var/task/node_modules/aws-sdk/lib/request.js:683:14)
at Request.transition (/var/task/node_modules/aws-sdk/lib/request.js:22:10)
at AcceptorStateMachine.runTo (/var/task/node_modules/aws-sdk/lib/state_machine.js:14:12)
at /var/task/node_modules/aws-sdk/lib/state_machine.js:26:10
at Request.<anonymous> (/var/task/node_modules/aws-sdk/lib/request.js:38:9)
at Request.<anonymous> (/var/task/node_modules/aws-sdk/lib/request.js:685:12)
at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:115:18)
message: 'User: arn:aws:sts::OBSCURED:assumed-role/chatroom-application-dev-us-east-1-lambdaRole/chatroom-application-dev-getRoomMessages is not authorized to perform: dynamodb:Query on resource: arn:aws:dynamodb:us-east-1:OBSCURED:table/messages-table-dev/index/roomIndex',
code: 'AccessDeniedException',
time: 2018-06-02T22:05:46.110Z,
requestId: 'OBSCURED',
statusCode: 400,
retryable: false,
retryDelay: 30.704899664776054 }

At the top of the exception it says the ARN for my getRoomMessages method is not authorized to perform: dynamodb:Query on resource: and shows the ARN for the global secondary index.

It seems clear, I need to define the policy to grant rights to access the global secondary index. But it's not at all clear how. I've seen other StackOverflow questions about DynamoDB complain about the fragmented documentation and how difficult it is to find anything. I have to agree. The word "fragmented" is putting it too mildly.

I am using the Serverless Framework. The provider section shows this policy/role definition:

provider:
  name: aws
  runtime: nodejs8.10
  stage: dev
  region: us-east-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource:
        - { "Fn::GetAtt": ["MessagesDynamoDBTable", "Arn" ] }
        - { "Fn::GetAtt": ["#roomIndex", "Arn" ] }
        - { "Fn::GetAtt": ["#userIndex", "Arn" ] }
  environment:
    MESSAGES_TABLE: ${self:custom.tableName}

In the Resource section, I believe I'm supposed to list the resources for which the permissions are declared. The first references the table as a whole. The last two I just added, and reference the indexes.

EDIT: When I run serverless deploy the following message is printed:

The CloudFormation template is invalid: Template error: instance of Fn::GetAtt references undefined resource #roomIndex

I tried several variations on this only to get the same error. What this boils down to is - how do I, in the serverless.yml, using Cloudfront syntax, get the ARN for the indexes. The ARN does exist because it is shown in the exception.

The DynamoDB table definition:

resources:
  Resources:
    MessagesDynamoDBTable:
      Type: AWS::DynamoDB::Table
      Properties:
        AttributeDefinitions:
          - AttributeName: messageId
            AttributeType: S
          - AttributeName: room
            AttributeType: S
          - AttributeName: userId
            AttributeType: S
        KeySchema:
          - AttributeName: messageId
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: roomIndex
            KeySchema:
              - AttributeName: room
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
          - IndexName: userIndex
            KeySchema:
              - AttributeName: userId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:custom.tableName}

The query being used corresponding to the above exception:

{
    "TableName": "messages-table-dev",
    "IndexName": "roomIndex",
    "KeyConditionExpression": "#roomIndex = :room",
    "ExpressionAttributeNames": {
        "#roomIndex": "room"
    },
    "ExpressionAttributeValues": {
        ":room": {
            "S": "everyone"
        }
    }
}

And the Lambda function code snippet which generates the query:

app.get('/messages/room/:room', (req, res) => {
    const params = {
        TableName: MESSAGES_TABLE,
        IndexName: "roomIndex",
        KeyConditionExpression: '#roomIndex = :room',
        ExpressionAttributeNames: { '#roomIndex': 'room' },
        ExpressionAttributeValues: {
            ":room": { S: `${req.params.room}` }
        },
    };
    console.log(`QUERY ROOM ${JSON.stringify(params)}`);
    dynamoDb.query(params, (error, result) => {
        if (error) {
            console.log(error);
            res.status(400).json({ error: 'Could not get messages' });
        } else {
            res.json(result.Items);
        }
    });
});
like image 980
David Herron Avatar asked Jun 02 '18 22:06

David Herron


People also ask

What is global secondary index in DynamoDB?

Global secondary index — An index with a partition key and a sort key that can be different from those on the base table. A global secondary index is considered "global" because queries on the index can span all of the data in the base table, across all partitions.

How many global secondary Indexs are there in DynamoDB?

Each table in DynamoDB can have up to 20 global secondary indexes (default quota) and 5 local secondary indexes. For more information about the differences between global secondary indexes and local secondary indexes, see Improving data access with secondary indexes.


3 Answers

I found a better answer than was posted by jake.lang. EDIT: I didn't see his second comment in which he suggested the following.

As he noted, his was incorrect since the ARN can change for valid reasons. However, the solution arose because the ARN for a Global Secondary Index is to append "/INDEXNAME" to the ARN for the table. This means the policy statement can be:

iamRoleStatements:
  - Effect: Allow
    Action:
      - dynamodb:Query
      - dynamodb:Scan
      - dynamodb:GetItem
      - dynamodb:PutItem
      - dynamodb:UpdateItem
      - dynamodb:DeleteItem
    Resource:
      - { "Fn::GetAtt": ["MessagesDynamoDBTable", "Arn" ] }
      - { "Fn::Join": [ "/", [ 
          { "Fn::GetAtt": ["MessagesDynamoDBTable", "Arn" ] }, "index", "roomIndex" 
        ]]}
      - { "Fn::Join": [ "/", [
          { "Fn::GetAtt": ["MessagesDynamoDBTable", "Arn" ] }, "index", "userIndex" 
        ]]}

The "Fn::Join" bit is from CloudFormation, and is a "join" operation. It takes an array of strings, concatenating them using the first argument. Hence it is a rather convoluted and overly complex method to calculate the ARN's required in this policy statement.

For documentation, see: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html

like image 70
David Herron Avatar answered Oct 21 '22 11:10

David Herron


Instead of Fn::Join you can use !Sub "${MessagesDynamoDBTable.Arn}", it's simpler. Moreover if you want to access all indexes (that's usually my case), then /index/* is all you need.

Example:

...
      Policies:
        - Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - dynamodb:Query
              Resource:
                - !Sub "${MessagesDynamoDBTable.Arn}"
                - !Sub "${MessagesDynamoDBTable.Arn}/index/*"
...
like image 42
Stan Prokop Avatar answered Oct 21 '22 11:10

Stan Prokop


Here's how I've declared my working GSI permissions in serverless. I'm not sure but maybe the issue is you need to declare actions for each resource independently?

iamRoleStatements: 
  - Effect: Allow
    Action:
      - dynamodb:Query
      - dynamodb:GetItem
      - dynamodb:PutItem
    Resource: "arn:REDACTED:table/TABLENAME”
  - Effect: Allow
    Action:
      - dynamodb:Query
    Resource: "arn:REDACTED:table/TABLENAME/index/INDEXNAME”

This hardcoding of the arn is probably not the best thing to do but it's worked fine for my development thus far.

like image 21
Jacob Lange Avatar answered Oct 21 '22 10:10

Jacob Lange