aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.44k stars 2.13k forks source link

Getting error "Not Authorized to access deleteUserData on type Mutation" for cleaning up user data #13796

Closed lafeer closed 1 month ago

lafeer commented 2 months ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

Authentication, GraphQL API, DataStore

Amplify Version

v6

Amplify Categories

auth, storage, function, api

Backend

Amplify CLI

Environment information

``` # Put output below this line System: OS: Windows 11 10.0.22631 CPU: (8) x64 Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz Memory: 1.56 GB / 15.79 GB Binaries: Node: 19.4.0 - C:\Program Files\nodejs\node.EXE Yarn: 1.19.1 - C:\Program Files (x86)\Yarn\bin\yarn.CMD npm: 9.2.0 - C:\Program Files\nodejs\npm.CMD Browsers: Chrome: 128.0.6613.120 Edge: Chromium (127.0.2651.74) Internet Explorer: 11.0.22621.3527 npmPackages: @aws-amplify/datastore-storage-adapter: ^2.1.16 => 2.1.16 @aws-amplify/react-native: ^1.0.16 => 1.0.16 @azure/core-asynciterator-polyfill: ^1.0.2 => 1.0.2 @babel/core: ^7.24.0 => 7.25.2 @expo/react-native-action-sheet: ^4.0.1 => 4.0.1 @gorhom/bottom-sheet: ^4.6.1 => 4.6.1 @react-native-async-storage/async-storage: 1.23.1 => 1.23.1 @react-native-community/netinfo: 11.3.1 => 11.3.1 @react-native-masked-view/masked-view: 0.3.1 => 0.3.1 @react-navigation/bottom-tabs: ^6.5.12 => 6.5.12 @react-navigation/elements: ^1.3.22 => 1.3.22 @react-navigation/native: ^6.1.10 => 6.1.10 @react-navigation/native-stack: ^6.9.18 => 6.9.18 @shopify/flash-list: 1.6.4 => 1.6.4 HelloWorld: 0.0.1 aws-amplify: ^6.0.16 => 6.0.16 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () axios: ^1.6.8 => 1.6.8 expo: ^51.0.31 => 51.0.31 expo-blur: ~13.0.2 => 13.0.2 expo-dev-client: ~4.0.25 => 4.0.25 expo-font: ~12.0.9 => 12.0.9 expo-haptics: ~13.0.1 => 13.0.1 expo-image-picker: ~15.0.7 => 15.0.7 expo-linear-gradient: ~13.0.2 => 13.0.2 expo-linking: ~6.3.1 => 6.3.1 expo-sharing: ~12.0.1 => 12.0.1 expo-splash-screen: ~0.27.5 => 0.27.5 expo-status-bar: ~1.12.1 => 1.12.1 nanoid: ^5.0.5 => 5.0.5 (3.3.7) node-html-parser: ^6.1.12 => 6.1.12 react: 18.2.0 => 18.2.0 react-native: 0.74.5 => 0.74.5 react-native-gesture-handler: ~2.16.1 => 2.16.2 react-native-get-random-values: ~1.11.0 => 1.11.0 react-native-reanimated: ~3.10.1 => 3.10.1 react-native-safe-area-context: 4.10.5 => 4.10.5 react-native-screens: 3.31.1 => 3.31.1 react-native-svg: 15.2.0 => 15.2.0 typescript: ^5.3.0 => 5.3.3 npmGlobalPackages: @ant-design/pro-cli: 3.0.1 @aws-amplify/cli: 12.10.1 corepack: 0.15.2 create-expo-app: 2.1.1 eas-cli: 7.6.2 gatsby-cli: 5.4.0 nodemon: 2.0.20 npm: 9.2.0 ```

Describe the bug

Getting error "Not Authorized to access deleteUserData on type Mutation" for cleaning up user data when following user data cleanup instructions as described here: Steps for cleaning user data for owner based auth schema

schema.graphql

type User
  @model
  @auth(
    rules: [
      { allow: owner }
      { allow: private, operations: [read] }
      { allow: public, operations: [read] }
    ]
  ) {
  userID: ID! @primaryKey
  givenName: String
  familyName: String
  avatar: AWSURL
  avatarColor: String
  followedWishlists: [Wishlist] @manyToMany(relationName: "WishlistUser")
}

type Wishlist
  @model
  @auth(
    rules: [
      { allow: owner, ownerField: "userID" }
      { allow: private, operations: [read] }
      { allow: public, operations: [read] }
    ]
  ) {
  nanoid: ID! @primaryKey
  userID: String!
  name: String!
  description: String
  isPrivate: Boolean!
  items: [Item] @hasMany(indexName: "byWishlist", fields: ["nanoid"])
  followers: [User] @manyToMany(relationName: "WishlistUser")
}

type Item
  @model
  @auth(
    rules: [
      { allow: owner, ownerField: "userID" }
      { allow: private, operations: [read] }
      { allow: public, operations: [read] }
    ]
  ) {
  userID: String!
  id: ID!
  name: String!
  description: String
  price: Float
  imageUrl: AWSURL
  itemUrl: AWSURL
  platform: String
  reservedBy: String
    @auth(
      rules: [
        { allow: owner, ownerField: "userID" }
        { allow: private, operations: [read, update] }
        { allow: public, operations: [read] }
      ]
    )
  purchasedBy: String
    @auth(
      rules: [
        { allow: owner, ownerField: "userID" }
        { allow: private, operations: [read, update] }
        { allow: public, operations: [read] }
      ]
    )
  wishlistID: ID! @index(name: "byWishlist")
}

type Mutation {
  deleteUserData: Boolean!
    @auth(rules: [{ allow: owner }])
    @function(name: "deleteUserData-${env}")
}

deleteUserData lambda function

/* Amplify Params - DO NOT EDIT
    API_WISHWELL_GRAPHQLAPIIDOUTPUT
    API_WISHWELL_ITEMTABLE_ARN
    API_WISHWELL_ITEMTABLE_NAME
    API_WISHWELL_USERTABLE_ARN
    API_WISHWELL_USERTABLE_NAME
    API_WISHWELL_WISHLISTTABLE_ARN
    API_WISHWELL_WISHLISTTABLE_NAME
    ENV
    REGION
    STORAGE_S3WISHWELL_BUCKETNAME
Amplify Params - DO NOT EDIT */

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */

const AWS = require("aws-sdk");
AWS.config.update({ region: process.env.REGION });

const dynamodb = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.API_WISHWELL_ITEMTABLE_NAME;

const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60;

exports.handler = async (event) => {
  console.log(`EVENT: ${JSON.stringify(event)}`);
  const ownerField = "owner"; // owner is default value but if you specified ownerField on auth rule, that must be specified here
  const identityClaim = "username"; // username is default value but if you specified identityField on auth rule, that must be specified here

  var condition = {
    [ownerField]: {
      ComparisonOperator: "EQ",
      AttributeValueList: [event.identity.claims[identityClaim]],
    },
    _deleted: {
      ComparisonOperator: "NE",
      AttributeValueList: [true],
    },
  };

  await new Promise(async (res) => {
    let LastEvaluatedKey;

    do {
      let queryParams = {
        TableName: tableName,
        ScanFilter: condition,
        ExclusiveStartKey: LastEvaluatedKey,
      };

      const items = await new Promise((resolve) => {
        dynamodb.scan(queryParams, (err, data) => {
          if (err) {
            console.log({ error: "Could not load items: " + err });
            resolve([]);
          } else {
            LastEvaluatedKey = data.LastEvaluatedKey;
            resolve(data.Items);
          }
        });
      });

      const dateNow = new Date();

      if (items.length > 0) {
        let deleteParams = {
          RequestItems: {
            [tableName]: items.map((item) => {
              return {
                PutRequest: {
                  Item: {
                    ...item,
                    _deleted: true,
                    _ttl: dateNow.getTime() / 1000 + THIRTY_DAYS_IN_SECONDS,
                    _version: item._version + 1,
                    _lastChangedAt: dateNow.getTime(),
                    updatedAt: dateNow.toISOString(),
                  },
                },
              };
            }),
          },
        };

        await new Promise((resolve) => {
          dynamodb.batchWrite(deleteParams, (err, data) => {
            resolve();
          });
        });
      }
    } while (LastEvaluatedKey);

    res();
  });

  return true;
};

calling deleteUserData

  const client = generateClient();

  const deletedUser = await client.graphql({
      query: deleteUserData,
      variables: {},
    });

Expected behavior

Expecting the deleteUserData function to be successfully called

Reproduction steps

  1. Follow instructions as outlined in: https://gist.github.com/aws-amplify-ops/27954c421bd72930874d48c15c284807
  2. Call deleteUserData as shown in Describe the bug section above.

Code Snippet

// Put your code below this line.

Log output

``` // Put your logs below this line ```

aws-exports.js

No response

Manual configuration


/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "ap-southeast-1",
    "aws_cloud_logic_custom": [
        {
            "name": "getProductDetailsApi",
            "endpoint": "https://xxxxxxx.execute-api.ap-southeast-1.amazonaws.com/dev",
            "region": "ap-southeast-1"
        }
    ],
    "aws_appsync_graphqlEndpoint": "https://xxxxxxxxxxxx.appsync-api.ap-southeast-1.amazonaws.com/graphql",
    "aws_appsync_region": "ap-southeast-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "xxx-xxxxxxxxxxxxx",
    "aws_cognito_identity_pool_id": "ap-southeast-1:xxxxxxxxxxxxxxxxxxx",
    "aws_cognito_region": "ap-southeast-1",
    "aws_user_pools_id": "ap-southeast-1_xxxxxxx",
    "aws_user_pools_web_client_id": "xxxxxxxxxxxxxxxxxxx",
    "oauth": {},
    "aws_cognito_username_attributes": [],
    "aws_cognito_social_providers": [],
    "aws_cognito_signup_attributes": [
        "EMAIL"
    ],
    "aws_cognito_mfa_configuration": "OFF",
    "aws_cognito_mfa_types": [
        "SMS"
    ],
    "aws_cognito_password_protection_settings": {
        "passwordPolicyMinLength": 8,
        "passwordPolicyCharacters": []
    },
    "aws_cognito_verification_mechanisms": [
        "EMAIL"
    ],
    "aws_user_files_s3_bucket": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-dev",
    "aws_user_files_s3_bucket_region": "ap-southeast-1"
};

export default awsmobile;

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

chrisbonifacio commented 2 months ago

Hi @lafeer I think the issue might be using allow: owner as an auth rule. I'm not sure that works on a mutation the same way it does on a model. I'll have to confirm with the team. However, in the meantime, you could try simply allowing authenticated users to perform the mutation (allow: private) and in the lambda logic you are already checking and making use of the event.identity to make sure only to delete the records of the user making the request.

Let me know if that helps!

Also, in case the owner auth rule is expected to work with a custom mutation, what exactly do you expect the auth resolver to do with the owner field before the custom lambda is invoked?

lafeer commented 2 months ago

Hi @chrisbonifacio. Thank you for getting back to me on this.

I've tried using allow: private, and I run into the same "Not Authorized to access deleteUserData on type Mutation" as with allow: owner

When I remove allow: owner or allow: private, I end up with the following event object in my lambda, where the identity is null.

{
    "typeName": "Mutation",
    "fieldName": "deleteUserData",
    "arguments": {},
    "identity": null,
    "source": null,
    "request": {
        "headers": {
            "x-forwarded-for": "xxxxxxxxxxxxxxxx",
            "cloudfront-viewer-country": "ID",
            "cloudfront-is-tablet-viewer": "false",
            "x-amzn-requestid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "via": "2.0 xxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)",
            "cloudfront-forwarded-proto": "https",
            "content-length": "75",
            "accept-language": "en-US,en;q=0.9",
            "host": "xxxxxxxxxxxxxxxxx.appsync-api.ap-southeast-1.amazonaws.com",
            "x-forwarded-proto": "https",
            "user-agent": "wishwell/1 CFNetwork/1498.700.2 Darwin/23.6.0",
            "cloudfront-is-mobile-viewer": "false",
            "accept": "*/*",
            "cloudfront-viewer-asn": "xxxxx",
            "cloudfront-is-smarttv-viewer": "false",
            "x-amzn-appsync-is-vpce-request": "false",
            "accept-encoding": "gzip, deflate, br",
            "x-amzn-remote-ip": "103.175.213.136",
            "content-type": "application/json; charset=UTF-8",
            "x-api-key": "xxx-ccccccccccccccccccccccc",
            "x-amz-cf-id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "x-amzn-trace-id": "Root=1-66e66baf-xxxxxxxxxxxxxxxxxxxx",
            "x-amz-user-agent": "aws-amplify/6.0.16 api/1 framework/202",
            "cloudfront-is-desktop-viewer": "true",
            "x-forwarded-port": "443"
        },
        "domainName": null
    },
    "prev": {
        "result": {}
    }
}
chrisbonifacio commented 2 months ago

Hi @lafeer the identity will be null because without the allow: private auth rule, you're using an auth mode where an identity doesn't get passed along. Does your schema have a global auth rule at the top?

lafeer commented 2 months ago

So even with allow: private added like below, I get the "Not Authorized to access deleteUserData on type Mutation".

type Mutation { deleteUserData: Boolean! @auth(rules: [{ allow: private }]) @function(name: "deleteUserData-${env}") }

No, my schema is exactly like it is in the description above.

chrisbonifacio commented 2 months ago

Hi @lafeer it seems like your default auth mode is API_KEY according to the config shared:

 "aws_appsync_authenticationType": "API_KEY",

You should try setting the auth mode on the request like so:

 const deletedUser = await client.graphql({
      query: deleteUserData,
      variables: {},
    }, 
    {authMode: 'userPool'}
 );

This should set the Authorization header to the current authenticated user's Cognito access token rather than the x-api-key header that is currently being applied as shown in the network logs you shared.

chrisbonifacio commented 1 month ago

Hi 👋 Closing this as we have not heard back from you. If you are still experiencing this issue and in need of assistance, please feel free to comment and provide us with any information previously requested by our team members so we can re-open this issue and be better able to assist you.

Thank you!

Bptmn commented 2 weeks ago

Hi,

I got the same issue "Not Authorized to access deleteUserData on type Mutation". I well followed the doc from aws: Steps to clean up user data

When I test the function from the lambda console, it works.

When I test from my app or from the AppSync query editor, I got the error. If I set the "allow: private", it works as well, but not with "allow: owner"

My schema:

type Item @model @auth(rules: [{allow: owner}]) {
  ownerId: ID!
  lastUseDateTime: AWSDateTime!
  contentEncrypted: String!
  itemKeyEncrypted: String!
  itemType: String!
  createdAt: AWSDateTime!
}

type Device @model @auth(rules: [{allow: owner}]) {
  id: ID! @primaryKey
  ownerId: ID!
  name: String!
  brand: String!
  model: String!
  systemName: String!
  deviceType: String!
  createdAt: AWSDateTime!
}

type Mutation {
  deleteUserData: Boolean
    @function(name: "deleteUserDataAfterAccountDelete-${env}")
    @auth(rules: [{allow: owner}])
}

My function:

const AWS = require('aws-sdk');
AWS.config.update({ region: process.env.REGION });

const dynamodb = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.API_SIMPLIPASSAMPLIFYG1_ITEMTABLE_NAME;

const THIRTY_DAYS_IN_SECONDS = 30 * 24 * 60 * 60;

exports.handler = async (event) => {
    const ownerField = 'owner'; // Default owner field when using allow: owner
    const identityClaim = 'sub'; // Cognito uses 'sub' for user identification by default

    // Define the query condition to find items owned by the user
    var condition = {
        [ownerField]: {
            ComparisonOperator: 'EQ',
            AttributeValueList: [event.identity.claims[identityClaim]]
        },
        '_deleted': {
            ComparisonOperator: 'NE',
            AttributeValueList: [true]
        }
    };

    await new Promise(async (res) => {
        let LastEvaluatedKey;

        do {
            let queryParams = {
                TableName: tableName,
                ScanFilter: condition,
                ExclusiveStartKey: LastEvaluatedKey
            };

            const items = await new Promise(resolve => {
                dynamodb.scan(queryParams, (err, data) => {
                    if (err) {
                        console.log({ error: 'Could not load items: ' + err });
                        resolve([]);
                    } else {
                        LastEvaluatedKey = data.LastEvaluatedKey;
                        resolve(data.Items);
                    }
                });
            });

            const dateNow = new Date();

            if (items.length > 0) {
                let deleteParams = {
                    RequestItems: {
                        [tableName]: items.map(item => {
                            return {
                                PutRequest: {
                                    Item: {
                                        ...item,
                                        _deleted: true,
                                        _ttl: Math.floor(dateNow.getTime() / 1000) + THIRTY_DAYS_IN_SECONDS,
                                        _version: item._version + 1,
                                        _lastChangedAt: dateNow.getTime(),
                                        updatedAt: dateNow.toISOString(),
                                    }
                                }
                            };
                        })
                    }
                };

                await new Promise(resolve => {
                    dynamodb.batchWrite(deleteParams, (err, data) => {
                        if (err) {
                            console.log({ error: 'Could not delete items: ' + err });
                        }
                        resolve();
                    });
                });
            }
        } while (LastEvaluatedKey);

        res();
    });

    return true;
};

Any idea?