aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
90 stars 76 forks source link

Unable to call aggregateItems inside a lambda function #73

Open Tedsterh opened 2 years ago

Tedsterh commented 2 years ago

Before opening, please confirm:

How did you install the Amplify CLI?

npm install -g @aws-amplify/cli

If applicable, what version of Node.js are you using?

v16.14.0

Amplify CLI Version

7.6.26

What operating system are you using?

Mac

Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.

No manual changes made

Amplify Categories

api

Amplify Commands

push

Describe the bug

I am unable to use aggregateItems when searching a model from inside a lambda function, the same lambda function is able to call and return the fields that are being denied by the aggregation.

These are the auth rules on my model

@auth(rules: [
  {allow: private, provider: iam},
  {allow: groups, groups: ["Admin"]},
])

The iam users should have full access permissions, there are no other auth rules on any of the fields.

This is the error returned

message: 'Unauthorized to run aggregation on field: amount'

But there are no other rules on the model that could be denying it.

Expected behavior

I should be able to get a sum from the searching aggregation without being denied.

Reproduction steps

  1. I created a model with the @searchable directive

  2. Added a lambda function with a call to search the model

  3. I gave the lambda function permission to query and mutate from the cli

  4. The request is signed using the access id and secret key as described in the docs

  5. I then ran amplify push to push the changes

  6. Ran the function from the AWS Lambda console

GraphQL schema(s)

```graphql # Put schemas below this line enum TransactionType { income expense } type Transaction @model @searchable @auth(rules: [ {allow: private, provider: iam}, {allow: groups, groups: ["Admin"]}, ]) { id: ID! type: TransactionType! amount: Float! transactionId: String! startedAt: AWSDateTime! completedAt: AWSDateTime! } ```

Log output

``` # Put your logs below this line ```

Additional information

No response

ykethan commented 2 years ago

Hello @Tedsterh, apologies for the delay in a response. Thank you for providing us the reproduction steps. On diving deep, I am curious about the code added to the lambda function as this will enable me to replicate a identical application.(Ref: Added a lambda function with a call to search the model)

Tedsterh commented 2 years ago
module.exports = {
  signRequest,
};

async function signRequest(query, operationName, variable) {
  const req = new AWS.HttpRequest(appsyncUrl, region);

  req.method = "POST";
  req.path = "/graphql";
  req.headers.host = endpoint;
  req.headers["Content-Type"] = "application/json";
  req.body = JSON.stringify({
    query: query,
    operationName: operationName,
    variables: variable,
  });

  const signer = new AWS.Signers.V4(req, "appsync", true);
  signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());

  const data = await new Promise((resolve, reject) => {
    const httpRequest = https.request({ ...req, host: endpoint }, (result) => {
      let data = "";

      result.on("data", (chunk) => {
        data += chunk;
      });

      result.on("end", () => {
        resolve(JSON.parse(data.toString()));
      });
    });

    httpRequest.write(req.body);
    httpRequest.end();
  });

  return data;
}

I have placed this inside a lambda layer that is provided to the function, I then simply call it with signRequest(query, "QueryName", {paramKey: paramValue}) this works for all my calls except aggregation

query Total {
  income: searchTransactions (filter: {type: {eq: "income"}}, aggregates: {field: amount, name: "TotalIncome", type: sum}) {
    aggregateItems {
      result {
        ... on SearchableAggregateScalarResult {
          __typename
          value
        }
      }
      name
    }
  }
  expenses: searchTransactions(filter: {type: {eq: "expense"}}, aggregates: {field: amount, name: "TotalIncome", type: sum}) {
    aggregateItems {
      result {
        ... on SearchableAggregateScalarResult {
          __typename
          value
        }
      }
      name
    }
  }
}

When trying to use the sum is when I get the message: 'Unauthorized to run aggregation on field: amount'

ykethan commented 2 years ago

@Tedsterh Thank you for providing me the requested information. I was able to replicate the error in my Lambda function as well.

marking this as bug.

ykethan commented 2 years ago

Notes: was able to run queries without the aggregates and run filters as well.

example of query that fails:

searchTransactions(aggregates: {field: amount, name: "id", type: sum}) {
    aggregateItems {
      name
      result {
        ... on SearchableAggregateScalarResult {
          __typename
          value
        }
      }
    }
  }

but runs the following.

searchTransactions {
    items {
      amount
    }
    aggregateItems {
      name
      result {
        ... on SearchableAggregateScalarResult {
          __typename
          value
        }
      }
    }
  }
cordosvictor commented 2 years ago

@Tedsterh did you find any temporary solution?

rajeshpalgithub commented 2 years ago

I am also facing the same problem

iprach commented 2 years ago

I found this bug in search auth resolver when adding lambda function with add api permission that return vtl before set allowedAggFields. In file Query.searchTransactions.req.vtl check allowed aggregate field

...
$util.error("Unauthorized to run aggregation on field: ${aggItem.field}", "Unauthorized")
...

but when auth type IAM found so it instant return out of vtl before set stash $allowedAggFields (example lambda function name lambdaEcho-dev in your case is in file Query.searchTransactions.auth.1.req.vtl )

...
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["lambdaEcho-dev"] )
  #foreach( $adminRole in $adminRoles )
    #if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
      #return($util.toJson({})) #### THIS LINE SHOULD PUT TO STASH $allowedAggFields  BEFORE RETURN #####
    #end
  #end
...

should be like this

...
#set( $totalFields = ["id","type","amount","transactionId","startedAt","completedAt","createdAt","updatedAt"] )
#set( $allowedAggFields = ["createdAt","updatedAt","id","type","amount","transactionId","startedAt","completedAt"] )
...
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["lambdaEcho-dev"] )
  #foreach( $adminRole in $adminRoles )
    #if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
      #set( $allowedAggFields = $totalFields )
      $util.qr($ctx.stash.put("allowedAggFields", $allowedAggFields))
      #return($util.toJson({})) 
    #end
  #end
  #if( !$isAuthorized )
    #if( $ctx.identity.userArn == $ctx.stash.authRole )
      #set( $isAuthorized = true )
      #set( $allowedAggFields = $totalFields )
    #end
  #end
#end
...

or workaround solution extend resolver ie. Query.searchTransactions.auth.2.req.vtl like below

#set( $allowedAggFields = ["createdAt","updatedAt","id","type","amount","transactionId","startedAt","completedAt"] )  ## replace your aggregate fields

#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["lambdaEcho-dev"] ) ## replace your lambda function names
  #foreach( $adminRole in $adminRoles )
    #if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
      $util.qr($ctx.stash.put("allowedAggFields", $allowedAggFields))
      #return($util.toJson({}))
    #end
  #end
#end

$util.toJson({"version":"2018-05-29","payload":{}})
juliandm commented 2 years ago

@iprach is correct.

Find the Appsync Resolver Querysearch{YOUR_TABLE_NAME}auth0Function and add $util.qr($ctx.stash.put("allowedAggFields", $allowedAggFields)) right before the return statement as a workaround.

Can someone point to the piece of code where these resolvers are generated so we can write a bugfix?