Access Denied when accessing s3 via Lambda

My application flow starts off with SES depositing an email in S3. My Lambda reads the object, parses out the relevant data, and writes to Dynamo. After the DB write, I’m attempting to copy the object to a different directory in the same bucket. Everything works up until the copy stage.

When attempting to copy I get the following error:
ERROR Invoke Error
{
“errorType”: “AccessDenied”,
“errorMessage”: “Access Denied”,
“code”: “AccessDenied”,
“message”: “Access Denied”,
“region”: null,
“time”: “2021-01-03T04:12:02.789Z”,
“requestId”: “9749B812CDC3E663”,
“extendedRequestId”: “dQPDfERl2+MN6Etf3xEhTq9dfRl90HPUHxBSXKNcV0Vi0pFY1AFcN/6lEPtepwj+IIxeVJcLLXU=”,
“statusCode”: 403,
“retryable”: false,
“retryDelay”: 40.07320833958672,
“stack”: [
“AccessDenied: Access Denied”,
" at Request.extractError (/var/task/node_modules/aws-sdk/lib/services/s3.js:700:35)",
" at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:106:20)",
" at Request.emit (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:78:10)",
" at Request.emit (/var/task/node_modules/aws-sdk/lib/request.js:688: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. (/var/task/node_modules/aws-sdk/lib/request.js:38:9)",
" at Request. (/var/task/node_modules/aws-sdk/lib/request.js:690:12)",
" at Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:116:18)"
]
}

The relevant parts of my serverless.yml:
service: my-email-filter

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-west-2'}
  s3:
    emailRepositoryBucket:
      bucketName: my-email-repository-${self:provider.stage}
      accessControl: BucketOwnerFullControl
      bucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      corsConfiguration:
        CorsRules:
          -
            AllowedOrigins:
              - '*'
            AllowedHeaders:
              - '*'
            AllowedMethods:
              - GET
              - PUT
              - POST
              - DELETE
              - HEAD

  environment:
    STAGE:  ${self:provider.stage}
    REGION: ${self:provider.region}
    EMAIL_BUCKET: busquo-email-repository-${self:provider.stage}

  iamRoleStatements:
    -
      Effect: Allow
      Action:
        - s3:*  
      Resource: arn:aws:s3:::my-email-repository-${self:provider.stage}/*
    -
      Effect: Allow
      Action:
        - s3:*  
      Resource: arn:aws:s3:::my-email-repository-${self:provider.stage}

functions:
  ProcessEmailEvent:
    handler: handlers/handler.process
    name: process-email-event-${self:provider.stage}
    events:
      - s3:
          bucket: my-email-repository-${self:provider.stage}
          event: s3:ObjectCreated:*
          rules:
            - prefix: inbound/

resources:
  Resources:
    EmailRepositoryBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: 'my-email-repository-${self:provider.stage}'
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: AllowSESPuts
              Effect: Allow
              Principal:
                Service: ses.amazonaws.com
              Action: 
                - s3:PutObject
              Resource: arn:aws:s3:::my-email-repository-${self:provider.stage}/*
              Condition:
                StringEquals:
                  aws:Referer: !Ref AWS::AccountId
            - Sid: AllowLambdaAccess
              Effect: Allow
              Principal:
                AWS: <MY LAMBDA EXECUTION ROLE ARN>
              Action: 
                - s3:*
              Resource: 
                - 'arn:aws:s3:::my-email-repository-${self:provider.stage}/*'
                - 'arn:aws:s3:::my-email-repository-${self:provider.stage}'

*NOTE: I’ve tried using the lambda service instead of my role ARN with the same error.

I’m invoking my function in my handler like this:
var AWS = require(‘aws-sdk’);
AWS.config.update({ region: process.env.REGION });
var s3 = new AWS.S3({ apiVersion: ‘2006-03-01’, region: process.env.REGION });

let copyParams = {
            Bucket: EmailRepositoryBucket,  // These values look like they're being set correctly
            CopySource: copySource,
            Key: objectKey
        };
await s3.copyItem(copyParams).promise().then((data) => {
    // Copy succeeds.  Do more work...
}).catch((error) => {
    console.log(error);
    throw error;
});

My lambda role looks like it has the right permissions. I’m not sure how to debug this issue. Any help is appreciated.

You could try adding execution role attached to your lambda to an IAM user that has programmatic access and test your copy operation from either the aws cli or executing a nodejs script locally. In either case, you may want verify read and write operations separately.

Also if you’re referencing process.env.EMAIL_BUCKET in code check that expected values match the intended bucket name (I see you refer to both ‘busquo-email…’ as well as ‘my-email…’ in your sls yml).

Yeah, I tried to remove references to my application name to avoid confusion. I ended up finding a solution that calls getting the object, writing a copy of it, and deleting the old one instead of copy. This was possible without changing permissions. This leads me to think that the s3 copy operation does something else behind the scenes that was being blocked by permissions.