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.41k stars 2.12k forks source link

Amplify DataStore subscriptionError #9369

Closed bmitioglov closed 2 years ago

bmitioglov commented 2 years ago

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

DataStore

Amplify Categories

api

Environment information

``` # Put output below this line System: OS: macOS 12.0.1 CPU: (16) x64 Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz Memory: 293.63 MB / 32.00 GB Shell: 5.8 - /bin/zsh Binaries: Node: 17.2.0 - /usr/local/bin/node npm: 8.1.4 - /usr/local/bin/npm Browsers: Chrome: 96.0.4664.110 Safari: 15.1 npmPackages: @aws-amplify/datastore-storage-adapter: ^1.2.2 => 1.2.2 @babel/core: ^7.9.0 => 7.15.0 (7.9.0) @react-native-async-storage/async-storage: ^1.15.5 => 1.15.7 @react-native-community/datetimepicker: ^3.5.2 => 3.5.2 @react-native-community/eslint-config: 1.1.0 @react-native-community/eslint-plugin: 1.0.0 @react-native-community/masked-view: ^0.1.11 => 0.1.11 @react-native-community/netinfo: ^7.1.3 => 7.1.3 @react-navigation/bottom-tabs: ^6.0.9 => 6.0.9 @react-navigation/native: ^6.0.6 => 6.0.6 @react-navigation/native-stack: ^6.2.5 => 6.2.5 @react-navigation/stack: ^6.0.11 => 6.0.11 @shoutem/ui: ^4.6.5 => 4.6.5 HelloWorld: 0.0.1 aws-amplify: ^4.3.10 => 4.3.10 aws-amplify-react-native: ^6.0.1 => 6.0.1 axios: ^0.21.1 => 0.21.4 babel-plugin-inline-view-configs: 0.0.5 example: 0.0.1 exampleapp: 0.0.1 expo: ~42.0.1 => 42.0.3 expo-app-loading: ^1.2.1 => 1.2.1 expo-font: ^9.2.1 => 9.2.1 expo-location: ~12.1.2 => 12.1.2 expo-sqlite: ~9.2.1 => 9.2.1 expo-status-bar: ~1.0.4 => 1.0.4 hermes-inspector-msggen: 1.0.0 react: 16.13.1 => 16.13.1 react-animated: 0.1.0 react-dom: 16.13.1 => 16.13.1 react-native: https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz => 0.63.2 react-native-codegen: 0.0.2 react-native-date-picker: ^4.1.1 => 4.1.1 react-native-elements: ^3.4.2 => 3.4.2 react-native-form-select-picker: ^0.0.12 => 0.0.12 react-native-linear-gradient: ~2.5.6 => 2.5.6 react-native-maps: ^0.28.0 => 0.28.0 react-native-number-please: ^1.0.5 => 1.0.5 react-native-photo-view: shoutem/react-native-photo-view#0ffa1481f6b6cb8663cb291b7db1d6644440584d => 1.5.2 react-native-picker-select: ^8.0.4 => 8.0.4 react-native-responsive-linechart: ^5.7.1 => 5.7.1 react-native-sectioned-multi-select: ^0.8.1 => 0.8.1 react-native-svg: ^12.1.1 => 12.1.1 react-native-uuid: ^2.0.1 => 2.0.1 react-native-vector-icons: ~6.6.0 => 6.6.0 react-native-web: ~0.11.0 => 0.11.7 react-native-webview: ~11.0.0 => 11.0.3 react-redux: ^7.2.5 => 7.2.5 redux: ^4.1.1 => 4.1.1 redux-thunk: ^2.4.1 => 2.4.1 npmGlobalPackages: @aws-amplify/cli: 7.6.2 expo-cli: 5.0.1 nodemon: 2.0.15 npm: 8.1.4 ```

Describe the bug

I am creating DataStore Amplify API with several entities having owner authorization rule. After starting application I see lots of errors like this:

31:28.602 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onCreateUnit on type Subscription"}]}

I noticed these errors are related to entities with public auth rule. After I sign in with created user I still see entities from another user, that means owner authorization rule is not working by some reason.

Expected behavior

I expect it to load entities which belongs to the owner, without any warnings in the log.

Reproduction steps

amplify init:

Initialize the project with the above configuration? No
? Enter a name for the environment dev
? Choose your default editor: IntelliJ IDEA
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react-native
? Source Directory Path:  src
? Distribution Directory Path: /
? Build Command:  npm build
? Start Command: npm start
Using default provider  awscloudformation
? Select the authentication method you want to use: AWS profile

amplify add auth:

Using service: Cognito, provided by: awscloudformation
The current configured provider is Amazon Cognito. 
Do you want to use the default authentication and security configuration? Default configuration
Warning: you will not be able to edit these selections. 
How do you want users to be able to sign in? Email
Do you want to configure advanced settings? Yes, I want to make some additional changes.
Warning: you will not be able to edit these selections. 
What attributes are required for signing up? Email

amplify add api:

? Select from one of the below mentioned services: GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue Authorization modes: API key (default, expiration time: 7 days from now)
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Configure additional auth types? No
? Here is the GraphQL API that we will create. Select a setting to edit or continue Conflict detection (required for DataStore): Disabled
? Enable conflict detection? Yes
? Select the default resolution strategy Auto Merge
? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue
? Choose a schema template: Blank Schema

Part of my model in schema.graphql:

type Exercise @model @auth(rules: [{allow: owner}]) {
  id: ID!
  name: String
  routines: [Routine] @manyToMany(relationName: "RoutineExercise")
  muscles: [Muscle] @manyToMany(relationName: "ExerciseMuscle")
  Histories: [History] @hasMany(indexName: "byExercise", fields: ["id"])
}

type Routine @model @auth(rules: [{allow: owner}]) {
  id: ID!
  name: String
  planName: String
  Exercises: [Exercise] @manyToMany(relationName: "RoutineExercise")
}

type Unit @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  isActive: Boolean
}

Then I did:

amplify push
amplify codegen models

Here is how I initialize Amplify in App.js:

useEffect(() => {
    Amplify.configure({
      ...config,
      Analytics: {
        disabled: true,
      },
    });
    const result = checkAuthState();
    store.dispatch(fetchRoutines);
  }, []);

My app authentication looks like this:

async function signIn() {
    try {
      await Auth.signIn(username, password);
      updateAuthState('loggedIn');

    } catch (error) {
      console.log('Error signing in...', error);
    }
  }

async function signUp() {
    try {
      await Auth.signUp({ username, password, attributes: { email } });
      console.log('✅ Sign-up Confirmed');
      navigation.navigate('ConfirmSignUp');
    } catch (error) {
      console.log('❌ Error signing up...', error);
    }
  }

  async function checkAuthState() {
    await Auth.currentAuthenticatedUser()
      .then((data) => {
        setUserLoggedIn('loggedIn');
      }).catch((error) => {
        setUserLoggedIn('loggedOut');
      })
  }

  return <Provider store={store} >
      <NavigationContainer>
        {isUserLoggedIn === 'initializing' && <Initializing />}
        {isUserLoggedIn === 'loggedIn' && (
          <AppNavigator updateAuthState={updateAuthState} />
        )}
        {isUserLoggedIn === 'loggedOut' && (
          <AuthenticationNavigator updateAuthState={updateAuthState} />
        )}
      </NavigationContainer>
    </Provider>

Code Snippet

// Put your code below this line.

Log output

``` // Put your logs below this line 31:28.602 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onCreateUnit on type Subscription"}]} at node_modules/@aws-amplify/core/lib-esm/Logger/ConsoleLogger.js:138:12 in prototype._log at http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false&minify=false:246139:21 in at node_modules/@aws-amplify/datastore/lib-esm/sync/processors/subscription.js:357:87 in queryObservable.map.subscribe$argument_0.error at node_modules/zen-observable/lib/Observable.js:139:8 in notifySubscription at http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false&minify=false:250258:22 in onNotify at node_modules/zen-observable/lib/Observable.js:239:11 in error at node_modules/zen-observable/lib/Observable.js:329:17 in _this2.subscribe$argument_0.error at node_modules/zen-observable/lib/Observable.js:139:8 in notifySubscription at http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false&minify=false:250258:22 in onNotify at node_modules/zen-observable/lib/Observable.js:239:11 in error at node_modules/@aws-amplify/pubsub/lib-esm/PubSub.js:180:37 in observable.subscribe$argument_0.error at node_modules/zen-observable/lib/Observable.js:139:8 in notifySubscription at http://127.0.0.1:19000/node_modules/expo/AppEntry.bundle?platform=ios&dev=true&hot=false&minify=false:250258:22 in onNotify ```

aws-exports.js

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

const awsmobile = {
    "aws_project_region": "us-west-1",
    "aws_cognito_identity_pool_id": "us-west-1:***",
    "aws_cognito_region": "us-west-1",
    "aws_user_pools_id": "us-west-1_***",
    "aws_user_pools_web_client_id": "***",
    "oauth": {},
    "aws_cognito_username_attributes": [
        "EMAIL"
    ],
    "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_appsync_graphqlEndpoint": "https://***.appsync-api.us-west-1.amazonaws.com/graphql",
    "aws_appsync_region": "us-west-1",
    "aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
    "aws_appsync_apiKey": "da2-***"
};

export default awsmobile;

Manual configuration

No response

Additional configuration

No response

Mobile Device

iPhone13

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

chrisbonifacio commented 2 years ago

Hi @bmitioglov 👋 thanks for raising this issue. First thing I noticed in the code snippet you provided was that Amplify.configure is being called within useEffect. This is probably not a good idea as we don't want to initialize Amplify every time every time App re-renders from any state changes that might come from above the component hierarchy.

So, I would first just move that outside of the useState, as well as the component. Ideally it would go at the top of the App.js file (or index.js if you're using the React Native CLI instead of Expo), right under the imports.

Next, if you're using two types of authorization modes - in your code Cognito User Pools to enable ownership and an API Key for public access, you're going to want to enable Multi Auth for DataStore. You can do this by editing your Amplfiy.configure call to this:

import {Amplify, AuthModeStrategyType } from 'aws-amplify';
import awsconfig from './src/aws-exports'; 

Amplify.configure({
  ...awsconfig,
  DataStore: {
    authModeStrategyType: AuthModeStrategyType.MULTI_AUTH
  }
})

This authorization issue could be that DataStore is making the call to AppSync with Cognito User Pool credentials rather than the API Key because without multi auth enabled, it will only use the default auth type - which, from your aws-exports, seems to be AMAZON_COGNITO_USER_POOLS

Here's some more information on this: https://docs.amplify.aws/lib/datastore/setup-auth-rules/q/platform/js/#configure-multiple-authorization-types

bmitioglov commented 2 years ago

Hi @chrisbonifacio , thank you so much for your answer! I applied your suggestions and now I see less errors, but they are still there. Another thing I noticed that errors now are related to relations tables from one-to-many, many-to-many connections. Example: [WARN] 44:22.891 DataStore - queryError, User is unauthorized to query syncLibraryRoutineLibraryExercises, some items could not be returned. [WARN] 44:23.78 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onUpdateExerciseMuscle on type ExerciseMuscle"}]}

My full model from schema.graphql:

type Unit @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  isActive: Boolean
}

type MeasureHistory @model @auth(rules: [{allow: owner}]) {
  id: ID!
  date: AWSDate
  value: String
  measureID: ID @index(name: "byMeasure")
}

type Measure @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  measureType: String
  MeasureHistories: [MeasureHistory] @hasMany(indexName: "byMeasure", fields: ["id"])
}

type History @model @auth(rules: [{allow: owner}]) {
  id: ID!
  date: String
  weightLb: String
  weightKg: String
  exerciseID: ID @index(name: "byExercise")
}

type Muscle @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  category: String
  image_url: String
  exercises: [Exercise] @manyToMany(relationName: "ExerciseMuscle")
}

type Exercise @model @auth(rules: [{allow: owner}]) {
  id: ID!
  name: String
  routines: [Routine] @manyToMany(relationName: "RoutineExercise")
  muscles: [Muscle] @manyToMany(relationName: "ExerciseMuscle")
  Histories: [History] @hasMany(indexName: "byExercise", fields: ["id"])
}

type Routine @model @auth(rules: [{allow: owner}]) {
  id: ID!
  name: String
  planName: String
  Exercises: [Exercise] @manyToMany(relationName: "RoutineExercise")
}

type LibraryRoutine @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  planName: String
  LibraryExercises: [LibraryExercise] @manyToMany(relationName: "LibraryRoutineLibraryExercise")
}

type LibraryExercise @model @auth(rules: [{allow: public}]) {
  id: ID!
  name: String
  categoryName: String
  libraryroutines: [LibraryRoutine] @manyToMany(relationName: "LibraryRoutineLibraryExercise")
}
chrisbonifacio commented 2 years ago

No problem! If you're logged in as a Cognito User, or logged in at all actually, you won't be able to access the types that are only set to public access. You'll have to add another auth rule like so:

type LibraryRoutine @model @auth(rules: [{allow: public}, {allow: private, operations: [read]}]) {
  id: ID!
  name: String
  planName: String
  LibraryExercises: [LibraryExercise] @manyToMany(relationName: "LibraryRoutineLibraryExercise")
}

You might also want to adjust the public auth rules so that users that are not logged in have restricted access to those records. Without specifying operations, it implies that unauthenticated users can perform all operations (create, read, update, delete) on a record.

The unauthorized error for the onUpdateExerciseMuscle seems a little unusual though unless you're not the owner of the record that was updated. I'll have to mess around with your schema to see what might going on exactly.

In the meantime, can you specify whether you're seeing these errors while logged in? What's the difference in behavior between an unauthenticated user and an authenticated one?

bmitioglov commented 2 years ago

@chrisbonifacio, I did as you said, I have added {allow: private, operations: [read]} to all public entities. So now the behavior is the following: When I am not logged in and open the app (SignIn screen on the mobile phone is being shown), I see these errors in the log:

[WARN] 02:50.173 DataStore - subscriptionError, AppSync Realtime subscription init error: No current user
[WARN] 02:50.231 DataStore - User is unauthorized to query syncMeasureHistories with auth mode AMAZON_COGNITO_USER_POOLS. No data could be returned.
[WARN] 02:50.233 DataStore - User is unauthorized to query syncHistories with auth mode AMAZON_COGNITO_USER_POOLS. No data could be returned.
[WARN] 02:50.234 DataStore - User is unauthorized to query syncExercises with auth mode AMAZON_COGNITO_USER_POOLS. No data could be returned.
[WARN] 02:50.235 DataStore - User is unauthorized to query syncRoutines with auth mode AMAZON_COGNITO_USER_POOLS. No data could be returned.
[WARN] 02:50.243 DataStore - User is unauthorized to query syncRoutineExercises with auth mode undefined. No data could be returned.
[WARN] 02:50.589 DataStore - User is unauthorized to query syncExerciseMuscles with auth mode undefined. No data could be returned.
[WARN] 02:50.596 DataStore - User is unauthorized to query syncLibraryRoutineLibraryExercises with auth mode undefined. No data could be returned.

After I sign in with my user I see only 3 errors in the logs:

[WARN] 05:33.231 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onCreateRoutineExercise on type RoutineExercise"}]}
[WARN] 05:33.242 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onDeleteRoutineExercise on type RoutineExercise"}]}
[WARN] 05:33.249 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onUpdateRoutineExercise on type RoutineExercise"}]}

And I still see entities created by another user 😞

Could it be related to the default auth type cognito_user_pool that I chose during amplify add api ?

bmitioglov commented 2 years ago

Hi @chrisbonifacio, is there some workaround maybe? because it's blocking me... What if I just remove all the relations between tables? I need to try

chrisbonifacio commented 2 years ago

Hey, sorry for the late reply. These warnings seem normal because of the default auth type. I'm not at my pc but you can try running amplify update api and setting the default auth type to API_KEY and then add Cognito User Pools as a secondary type. Let me know if that helps with the unauthenticated subscriptions.

One note, you'll notice that some of the warnings are about types that are not in your schema, like RoutineExercise, etc. These are join tables the CLI creates to handle many to many relationships. Here's the doc page for more information and how to link two records and query that relationship.

https://docs.amplify.aws/lib/datastore/relational/q/platform/js/#many-to-many

bmitioglov commented 2 years ago

Hi @chrisbonifacio , thank you for the reply! I updated api to make api_key as a default auth type and user pools as secondary. However I still see the warnings:

[WARN] 06:19.400 DataStore - queryError, User is unauthorized to query syncRoutineExercises, some items could not be returned.
[WARN] 06:19.463 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onCreateRoutineExercise on type Subscription"}]}
[WARN] 06:19.468 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onDeleteRoutineExercise on type Subscription"}]}
[WARN] 06:19.484 DataStore - subscriptionError, Connection failed: {"errors":[{"errorType":"Unauthorized","message":"Not Authorized to access onUpdateRoutineExercise on type Subscription"}]}

But the main problem right now is that I still see Routine entities that another user created in my app... I looked at AppSync graphql api query webpage and I logged in under my user pool user and used this query there:

query MyQuery {
  listRoutines {
    items {
      name
      id
    }
  }
}

and after execution I see the correct result, only routines that belongs to my user. Why do you think it doesn't work in the app? Here is my code to list routines from the app:

const routines = await DataStore.query(Routine);

What do you think might be wrong?

chrisbonifacio commented 2 years ago

I'm not totally sure if the warnings about the join table subscriptions are normal or not, specifically how the auth rules are supposed to be applied to them.

However, I was able to reproduce the behavior where, in the AppSync console the owner auth rule is respected and only my user's Routines are returned, but using DataStore in my client returns both users' Routines so the auth rule is not being respected there.

I will let the team know and get some feedback because that seems like a bug.

bmitioglov commented 2 years ago

@chrisbonifacio thank you Chris. I'll probably stick with my own good old java api for now

chrisbonifacio commented 2 years ago

No problem! So it turns out that if you don't clear DataStore explicitly, records will remain in IndexedDB and can include another user's only if the other user also logged in on the same device. So, to keep from seeing your other test users' records, you can call DataStore.clear before/after signing out.

const signOutAndClearDataStore = async () => {
  await signOut();
  await DataStore.clear();
};

If records continue to persist, you can also try clearing the Site Data altogether by navigating to the Dev Tools > Application > Storage > Clear Site Data.

This should make sure everything's cleared and no credentials are lingering in localStorage.

Afterwards, the behavior seemed consistent between DataStore and AppSync in my app.

bmitioglov commented 2 years ago

Oh ok, that's good to know thanks! BTW was you able to reproduce the warnings I've seen as well? Is there a way to know if they are expected and don't affect functionality?

chrisbonifacio commented 2 years ago

@bmitioglov I did get those warnings about the join tables but so far I don't think they affect functionality. They're just to keep track of the many to many relationships.

Since we resolved the original issue here, going to close this one out but if you experience any weird or unexpected behavior involving them, open a new issue so we can track it as a separate issue.

bmitioglov commented 2 years ago

@chrisbonifacio thank you!

github-actions[bot] commented 1 year ago

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.