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
88 stars 76 forks source link

DataStore Save Not Working after adding function to Appsync Resolver Pipeline #1484

Closed defish1962 closed 11 months ago

defish1962 commented 1 year ago

Amplify CLI Version

12.0.0

Question

I added a function to the MutationUpdateToDo resolver pipeline and now when I call the DataStore.save function in my app, the changes aren't written to DynamoDB.

I created a Lambda function to validate the user in a multi-tenant environment and am calling it in a request mapping template in an AppSync function I created and added to the pipeline resolver. This is to prevent someone from modifying data that doesn't belong to their associated group from outside the app (groupID is a custom attribute in Cognito and pulled from the JWT Token to compare with the groupID on the row they are trying to access).

Now, when I change the ToDo item in the app, the change does not get written to the DynamoDB table. I understand that modified pipelines aren't really supported with the DataStore, but I think there should be a way to add a function that doesn't modify data, and only authorizes the user to continue, or prevents them from continuing the action, without messing up the DataStore's behavior.

Here is my request mapping template:

`#* The value of 'payload' after the template has been evaluated will be passed as the event to AWS Lambda. # $util.log.info("In the Request mapping template!")

{ "operation": "Invoke", "payload": { "version": "2018-05-29", "operation": "Invoke", "payload": { "tableName": "$ctx.stash.get('tableName')", "primaryKey": { "id": "$ctx.args.input.id" }, "groupID": "$ctx.identity.claims.get('custom:currentGroup')" } } } `

and here is the response mapping template:

`## Raise a GraphQL field error in case of a datasource invocation error

if($ctx.error)

$util.error($ctx.error.message, $ctx.error.type)

end

$util.log.info("In the response mapping template")

if($context.result)

$util.log.info($context.result)

else

$util.log.info("Not a match")
$util.unauthorized()

end

$util.toJson($context.result)`

I placed the new function at the beginning of the pipeline. It is working exactly as expected when the updateToDo mutation is called from the AppSync console.

Any suggestions or pointers would be greatly appreciated.

defish1962 commented 1 year ago

Update: It appears that the DataStore.save isn't working because the currentGroup value is not being retrieved from the claims identity. If I run the mutation in the AppSync console, the claims.identity.custom:currentGroup value is populated. When I edit the ToDo and do a DataStore.save in the app, the resolver is showing the value of the claims.identity.custom:currentGroup = null.

User pool authentication is being used in both cases, so I don't understand why the currentGroup custom attribute isn't available to the resolver.

defish1962 commented 1 year ago

OK, the AppSync console passes the id token to the AppSync API and therefore the resolvers can access any custom attributes added to the Cognito user pool. Apparently the DataStore passes the access token, which does not contain the custom attributes.

Here is another example where AWS Amplify turns out not to be a good choice when building multi-tenant applications. You can't use the OwnersField with the DataStore and you have to write custom resolvers to implement fine grained security. Now you can't even get the custom attributes for a user without having to query the Coginto user pool EVERY TIME a GraphQL query or mutation request is made to check if the user is allowed to access that data. Given that these issues have existed for a few years now without being rectified, one can only conclude that the architecture of Amplify does not provide a way to solve the multi-tenant app security problem
chrisbonifacio commented 1 year ago

Hi @defish1962 thanks for raising this issue and apologies for the delay 🙏

You are correct, Amplify does not explicitly support multi-tenant use cases out of the box. It typically requires extra configuration, advanced workflows, and workarounds.

However, the issues you mentioned should be addressable:

the AppSync console passes the id token to the AppSync API and therefore the resolvers can access any custom attributes added to the Cognito user pool. Apparently the DataStore passes the access token, which does not contain the custom attributes.

This is true, but you can configure the graphql_headers of the API category to send the ID token instead. I just tested this to confirm and it does apply to DataStore graphql requests as well.

Amplify.configure({
  ...awsConfig,
  API: {
    ...awsConfig,
    graphql_headers: async () => {
      const session = await Auth.currentSession();
      return {
        Authorization: session.getIdToken().getJwtToken(),
      };
    },
  },
});

You can't use the OwnersField with the DataStore and you have to write custom resolvers to implement fine grained security

DataStore does support custom ownerFields. I just tried with this schema and front end code:

type Todo @model @auth(rules: [{ allow: owner, ownerField: "author" }]) {
  id: ID!
  name: String!
  description: String
  author: String!
}

you can use either the format of sub::username or just the username. Both seem to work and sync just fine for separate users. The records locally will only show the username.

const createTodo = async () => {
    const sub = user.getSignInUserSession()?.getAccessToken().payload.sub;
    console.log(sub);
    try {
      const todo = await DataStore.save(
        new Todo({
          name: "test",
          author: user.username,
        })
      );
      console.log(todo);
    } catch (error) {
      console.error(error);
    }
  };

createTodo request

CleanShot 2023-05-25 at 14 14 46

createTodo response CleanShot 2023-05-25 at 14 15 13

What issues were you having with using a custom ownerField in a DataStore-enabled API?

AnilMaktala commented 11 months ago

Hey 👋 , This issue is being closed due to inactivity. If you are still experiencing the same problem and need further assistance, please feel free to leave a comment. This will enable us to reopen the issue and provide you with the necessary support.