Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Datastore not sync when add ownderField and/or groupsField in schema #1566

Closed
4 of 9 tasks
surisakc opened this issue May 2, 2022 · 34 comments
Closed
4 of 9 tasks

Datastore not sync when add ownderField and/or groupsField in schema #1566

surisakc opened this issue May 2, 2022 · 34 comments
Assignees
Labels
datastore Issues related to the DataStore Category pending-triage This issue is in the backlog of issues to triage

Comments

@surisakc
Copy link

surisakc commented May 2, 2022

Description

Using the schema below that has ownerField and groupsField both point to a list of string. When run the app in iOS, the datastore isn't sync'ed. In Android, the app crashed with error messages.

schema with two fields that cause issue

type Building2 @model @auth(rules: [{allow: owner, ownerField: "users"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

type Building3 @model @auth(rules: [{allow: owner}, {allow: groups, groupsField: "users"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

also tried this schema which has issue like above

type Building2 @model @auth(rules: [{allow: owner, ownerField: "users"}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

type Building3 @model @auth(rules: [{allow: owner}, {allow: groups, groupsField: "users"}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

schema without two fields that has no issue

type Building2 @model @auth(rules: [{allow: owner}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

type Building3 @model @auth(rules: [{allow: owner}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

Categories

  • Analytics
  • API (REST)
  • API (GraphQL)
  • Auth
  • Authenticator
  • DataStore
  • Storage

Steps to Reproduce

  1. Create Flutter App with Amplify API schema with ownerField and groupsField both point to a list of string (userIds)
  2. Deploy the schema
  3. Run the app in iOS. In Terminal, it doesn't show the hub event modelsynced and no error messages.
  4. Run the app in Android. In Terminal, it shows the hub event modelsynced with error messages.
Launching lib/main.dart on sdk gphone64 arm64 in debug mode...
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
Connecting to VM Service at ws://127.0.0.1:63574/oRkmIQmibb4=/ws
[GETX] Instance "GetMaterialController" has been created
[GETX] Instance "GetMaterialController" has been initialized
[GETX] GOING TO ROUTE /login
[GETX] Instance "LoginController" has been created
[GETX] Instance "AuthService" has been created
[GETX] Instance "AmplifyService" has been created
[log] BEGIN INIT AMPLIFY
I/amplify:flutter:auth_cognito( 3631): Added Auth plugin
I/amplify:flutter:analytics_pinpoint( 3631): Added AnalyticsPinpoint plugin
I/amplify:flutter:api( 3631): Added API plugin
[GETX] Instance "AmplifyService" has been initialized
[GETX] Instance "AuthService" has been initialized
[GETX] Instance "LoginController" has been initialized
D/AWSMobileClient( 3631): Using the SignInProviderConfig from `awsconfiguration.json`.
W/AWSMobileClient( 3631): Failed to parse HostedUI settings from store. Defaulting to awsconfiguration.json
W/AWSMobileClient( 3631): java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
W/AWSMobileClient( 3631): 	at org.json.JSONTokener.nextCleanInternal(JSONTokener.java:121)
W/AWSMobileClient( 3631): 	at org.json.JSONTokener.nextValue(JSONTokener.java:98)
W/AWSMobileClient( 3631): 	at org.json.JSONObject.<init>(JSONObject.java:168)
W/AWSMobileClient( 3631): 	at org.json.JSONObject.<init>(JSONObject.java:185)
W/AWSMobileClient( 3631): 	at com.amazonaws.mobile.client.AWSMobileClient.getHostedUIJSON(AWSMobileClient.java:714)
W/AWSMobileClient( 3631): 	at com.amazonaws.mobile.client.AWSMobileClient$2.run(AWSMobileClient.java:615)
W/AWSMobileClient( 3631): 	at com.amazonaws.mobile.client.internal.InternalCallback$1.run(InternalCallback.java:101)
W/AWSMobileClient( 3631): 	at java.lang.Thread.run(Thread.java:920)
D/AWSMobileClient( 3631): initialize: Cognito HostedUI client detected
D/AWSMobileClient( 3631): Inspecting user state details
D/AutoSessionTracker( 3631): Activity paused: MainActivity
D/AutoSessionTracker( 3631): Activity created: com.amazonaws.amplify.amplify_analytics_pinpoint.EmptyActivity
I/amplify:aws-datastore( 3631): Creating table: Building3
I/amplify:aws-datastore( 3631): Creating table: PersistentRecord
I/amplify:aws-datastore( 3631): Creating table: Building2
I/amplify:aws-datastore( 3631): Creating table: PersistentModelVersion
I/amplify:aws-datastore( 3631): Creating table: LastSyncMetadata
I/amplify:aws-datastore( 3631): Creating table: ModelMetadata
I/amplify:aws-datastore( 3631): Creating index for table: PersistentRecord
D/AutoSessionTracker( 3631): Activity resumed: MainActivity
D/AutoSessionTracker( 3631): Application entered the foreground.
W/e.smarterblind( 3631): Accessing hidden method Landroid/app/AppOpsManager;->checkOpNoThrow(IILjava/lang/String;)I (unsupported, reflection, allowed)
W/e.smarterblind( 3631): Accessing hidden field Landroid/app/AppOpsManager;->OP_POST_NOTIFICATION:I (unsupported, reflection, allowed)
D/AutoSessionTracker( 3631): Activity destroyed com.amazonaws.amplify.amplify_analytics_pinpoint.EmptyActivity
[log] BEGIN INIT AMPLIFY HUB SCRIPTION
[log] END INIT AMPLIFY HUB SCRIPTION
[log] END INIT AMPLIFY
D/AWSMobileClient( 3631): Inspecting user state details
D/AWSMobileClient( 3631): waitForSignIn: userState:SIGNED_OUT
I/amplify:flutter:datastore( 3631): Unhandled DataStoreHubEvent: SUCCEEDED
I/amplify:flutter:datastore( 3631): com.amplifyframework.core.category.CategoryInitializationResult@5ee8743
[log] BEGIN AMPLIFY SIGNOUT
D/EGL_emulation( 3631): app_time_stats: avg=1796.79ms min=204.63ms max=4540.98ms count=3
W/AuthClient( 3631): HostedUIRedirectActivity is not declared in AndroidManifest.
D/IdentityManager( 3631): Signing out...
D/AWSMobileClient( 3631): Inspecting user state details
[log] END AMPLIFY SIGNOUT
[log] BEGIN CLEAR DATASTORE
I/amplify:aws-datastore( 3631): Orchestrator lock acquired.
I/amplify:aws-datastore( 3631): Orchestrator lock released.
I/amplify:aws-datastore( 3631): Creating table: Building3
I/amplify:aws-datastore( 3631): Creating table: PersistentRecord
I/amplify:aws-datastore( 3631): Creating table: Building2
I/amplify:aws-datastore( 3631): Creating table: PersistentModelVersion
I/amplify:aws-datastore( 3631): Creating table: LastSyncMetadata
I/amplify:aws-datastore( 3631): Creating table: ModelMetadata
I/amplify:aws-datastore( 3631): Creating index for table: PersistentRecord
I/amplify:flutter:datastore( 3631): Successfully cleared the store
[log] BEGIN CLEAR DATASTORE
[log] BEGIN LOGIN
W/CognitoUserSession( 3631): CognitoUserSession is not valid because idToken is null.
D/AWSMobileClient( 3631): Sending password.
D/AWSMobileClient( 3631): Using USER_SRP_AUTH for flow type.
D/AWSMobileClient( 3631): _federatedSignIn: Putting provider and token in store
D/AWSMobileClient( 3631): Inspecting user state details
D/AWSMobileClient( 3631): hasFederatedToken: false provider: cognito-idp.ca-central-1.amazonaws.com/ca-central-1_bZQUyxwy1
D/AWSMobileClient( 3631): Inspecting user state details
[log] hubEvent.eventName: SIGNED_IN
[log] SIGNED_IN
[log] @_amplifyService.status.listen: AmplifyStatus.signedIn
D/AWSMobileClient( 3631): hasFederatedToken: true provider: cognito-idp.ca-central-1.amazonaws.com/ca-central-1_bZQUyxwy1
[GETX] GOING TO ROUTE /home
[GETX] REMOVING ROUTE /login
D/AWSMobileClient( 3631): Inspecting user state details
D/AWSMobileClient( 3631): hasFederatedToken: true provider: cognito-idp.ca-central-1.amazonaws.com/ca-central-1_bZQUyxwy1
D/AWSMobileClient( 3631): waitForSignIn: userState:SIGNED_IN
D/AWSMobileClient( 3631): getCredentials: Validated user is signed-in
[GETX] Instance "HomeController" has been created
I/amplify:aws-datastore( 3631): Orchestrator lock acquired.
I/amplify:aws-datastore( 3631): Orchestrator transitioning from STOPPED to SYNC_VIA_API
I/amplify:aws-datastore( 3631): Starting to observe local storage changes.
[GETX] Instance "HomeController" has been initialized
I/amplify:aws-datastore( 3631): Now observing local storage. Local changes will be enqueued to mutation outbox.
I/amplify:aws-datastore( 3631): Setting currentState to LOCAL_ONLY
I/amplify:aws-datastore( 3631): Setting currentState to SYNC_VIA_API
I/amplify:flutter:datastore( 3631): Established a new stream form flutter com.amplifyframework.datastore.storage.sqlite.-$$Lambda$coxN3FV0myAqN-gpZfZvj7bzSOI@814523f
I/amplify:aws-datastore( 3631): Starting API synchronization mode.
I/amplify:aws-datastore( 3631): Starting processing subscription events.
I/amplify:aws-datastore( 3631): Orchestrator lock released.
W/amplify:aws-datastore( 3631): An error occurred on the remote ON_UPDATE subscription for model Building3
W/amplify:aws-datastore( 3631): java.lang.NullPointerException: Attempt to invoke interface method 'boolean java.util.List.isEmpty()' on a null object reference
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator.isReadRestrictingStaticGroup(AuthRuleRequestDecorator.java:147)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator.decorate(AuthRuleRequestDecorator.java:103)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.api.aws.AWSApiPlugin.buildSubscriptionOperation(AWSApiPlugin.java:628)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:308)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.api.aws.AWSApiPlugin.subscribe(AWSApiPlugin.java:288)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.api.ApiCategory.subscribe(ApiCategory.java:91)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.datastore.appsync.AppSyncClient.subscription(AppSyncClient.java:332)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.datastore.appsync.AppSyncClient.onUpdate(AppSyncClient.java:272)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.datastore.syncengine.-$$Lambda$r7L8lscweM53-6nW0zECJRGgjT0.subscribe(Unknown Source:7)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.datastore.syncengine.SubscriptionProcessor.lambda$subscriptionObservable$6$SubscriptionProcessor(SubscriptionProcessor.java:187)
W/amplify:aws-datastore( 3631): 	at com.amplifyframework.datastore.syncengine.-$$Lambda$SubscriptionProcessor$w6tohapLGUGmW4mOmsvNOno7GVE.subscribe(Unknown Source:11)
...
E/AndroidRuntime( 3631): 	... 8 more
D/AutoSessionTracker( 3631): Activity paused: MainActivity
I/Process ( 3631): Sending signal. PID: 3631 SIG: 9
Lost connection to device.
Exited (sigterm)

Screenshots

No response

Platforms

  • iOS
  • Android

Android Device/Emulator API Level

No response

Environment

[✓] Flutter (Channel stable, 2.10.5, on macOS 12.3.1 21E258 darwin-arm, locale
    en-CA)
    • Flutter version 2.10.5 at /Users/<user>/fvm/versions/2.10.5
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 5464c5bac7 (2 weeks ago), 2022-04-18 09:55:37 -0700
    • Engine revision 57d3bac3dd
    • Dart version 2.16.2
    • DevTools version 2.9.2

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/<user>/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • Java binary at: /Applications/Android
      Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.3.1)
    • Xcode at /Applications/XCode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Android Studio (version 2020.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.10+0-b96-7249189)

[✓] VS Code (version 1.66.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.38.1

[✓] Connected device (3 available)
    • sdk gphone64 arm64 (mobile) • emulator-5554                        •
      android-arm64 • Android 12 (API 31) (emulator)
    • iPhone 13 Pro (mobile)      • 1CA31312-509F-4F0C-9A9D-DABC5FE54518 • ios
      • com.apple.CoreSimulator.SimRuntime.iOS-15-4 (simulator)
    • iPhone 13 Pro Max (mobile)  • 273BEB6B-5F10-4914-B1EB-15E19F906CD0 • ios
      • com.apple.CoreSimulator.SimRuntime.iOS-15-4 (simulator)

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!

Dependencies

Dart SDK 2.16.2
Flutter SDK 2.10.5
smarterblinds 1.0.0+1

dependencies:
- amplify_analytics_pinpoint 0.4.5 [amplify_analytics_plugin_interface amplify_analytics_pinpoint_android amplify_analytics_pinpoint_ios amplify_core flutter plugin_platform_interface]
- amplify_api 0.4.5 [amplify_api_plugin_interface amplify_core collection flutter meta plugin_platform_interface]
- amplify_auth_cognito 0.4.5 [flutter amplify_auth_plugin_interface amplify_core amplify_auth_cognito_android amplify_auth_cognito_ios collection plugin_platform_interface]
- amplify_datastore 0.4.5 [flutter amplify_datastore_plugin_interface amplify_core plugin_platform_interface meta collection async]
- amplify_flutter 0.4.5 [amplify_analytics_plugin_interface amplify_api_plugin_interface amplify_auth_plugin_interface amplify_core amplify_datastore_plugin_interface amplify_storage_plugin_interface collection flutter json_annotation meta plugin_platform_interface]
- cupertino_icons 1.0.4
- flutter 0.0.0 [characters collection material_color_utilities meta typed_data vector_math sky_engine]
- get 4.6.1 [flutter]

transitive dependencies:
- amplify_analytics_pinpoint_android 0.4.5 [flutter]
- amplify_analytics_pinpoint_ios 0.4.5 [flutter]
- amplify_analytics_plugin_interface 0.4.5 [amplify_core flutter meta]
- amplify_api_plugin_interface 0.4.5 [amplify_core collection flutter json_annotation meta]
- amplify_auth_cognito_android 0.4.5 [flutter]
- amplify_auth_cognito_ios 0.4.5 [amplify_core flutter]
- amplify_auth_plugin_interface 0.4.5 [flutter meta amplify_core]
- amplify_core 0.4.5 [flutter plugin_platform_interface collection date_time_format meta uuid]
- amplify_datastore_plugin_interface 0.4.5 [flutter meta collection amplify_core]
- amplify_storage_plugin_interface 0.4.5 [flutter meta amplify_core]
- async 2.8.2 [collection meta]
- characters 1.2.0
- collection 1.15.0
- crypto 3.0.2 [typed_data]
- date_time_format 2.0.1
- json_annotation 4.5.0 [meta]
- material_color_utilities 0.1.3
- meta 1.7.0
- plugin_platform_interface 2.1.2 [meta]
- sky_engine 0.0.99
- typed_data 1.3.0 [collection]
- uuid 3.0.6 [crypto]
- vector_math 2.1.1

Device

Android Emulator Pixel 3a

OS

Android 12.0 (API 31) arm64

CLI Version

8.1.0

Additional Context

No response

@Jordan-Nelson Jordan-Nelson added datastore Issues related to the DataStore Category pending-triage This issue is in the backlog of issues to triage labels May 2, 2022
@HuiSF
Copy link
Member

HuiSF commented May 5, 2022

Hi @surisakc can you paste the generated Dart class files for Building2 and Building3?

@surisakc
Copy link
Author

surisakc commented May 6, 2022

Hi @surisakc can you paste the generated Dart class files for Building2 and Building3?

@HuiSF here they are

import 'ModelProvider.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';


/** This is an auto generated class representing the Building2 type in your schema. */
@immutable
class Building2 extends Model {
  static const classType = const _Building2ModelType();
  final String id;
  final String? _name;
  final GenericStatus? _status;
  final String? _data;
  final List<String>? _users;
  final TemporalDateTime? _createdAt;
  final TemporalDateTime? _updatedAt;

  @override
  getInstanceType() => classType;
  
  @override
  String getId() {
    return id;
  }
  
  String get name {
    try {
      return _name!;
    } catch(e) {
      throw new AmplifyCodeGenModelException(
          AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
          recoverySuggestion:
            AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
          underlyingException: e.toString()
          );
    }
  }
  
  GenericStatus get status {
    try {
      return _status!;
    } catch(e) {
      throw new AmplifyCodeGenModelException(
          AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
          recoverySuggestion:
            AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
          underlyingException: e.toString()
          );
    }
  }
  
  String? get data {
    return _data;
  }
  
  List<String>? get users {
    return _users;
  }
  
  TemporalDateTime? get createdAt {
    return _createdAt;
  }
  
  TemporalDateTime? get updatedAt {
    return _updatedAt;
  }
  
  const Building2._internal({required this.id, required name, required status, data, users, createdAt, updatedAt}): _name = name, _status = status, _data = data, _users = users, _createdAt = createdAt, _updatedAt = updatedAt;
  
  factory Building2({String? id, required String name, required GenericStatus status, String? data, List<String>? users}) {
    return Building2._internal(
      id: id == null ? UUID.getUUID() : id,
      name: name,
      status: status,
      data: data,
      users: users != null ? List<String>.unmodifiable(users) : users);
  }
  
  bool equals(Object other) {
    return this == other;
  }
  
  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    return other is Building2 &&
      id == other.id &&
      _name == other._name &&
      _status == other._status &&
      _data == other._data &&
      DeepCollectionEquality().equals(_users, other._users);
  }
  
  @override
  int get hashCode => toString().hashCode;
  
  @override
  String toString() {
    var buffer = new StringBuffer();
    
    buffer.write("Building2 {");
    buffer.write("id=" + "$id" + ", ");
    buffer.write("name=" + "$_name" + ", ");
    buffer.write("status=" + (_status != null ? enumToString(_status)! : "null") + ", ");
    buffer.write("data=" + "$_data" + ", ");
    buffer.write("users=" + (_users != null ? _users!.toString() : "null") + ", ");
    buffer.write("createdAt=" + (_createdAt != null ? _createdAt!.format() : "null") + ", ");
    buffer.write("updatedAt=" + (_updatedAt != null ? _updatedAt!.format() : "null"));
    buffer.write("}");
    
    return buffer.toString();
  }
  
  Building2 copyWith({String? id, String? name, GenericStatus? status, String? data, List<String>? users}) {
    return Building2._internal(
      id: id ?? this.id,
      name: name ?? this.name,
      status: status ?? this.status,
      data: data ?? this.data,
      users: users ?? this.users);
  }
  
  Building2.fromJson(Map<String, dynamic> json)  
    : id = json['id'],
      _name = json['name'],
      _status = enumFromString<GenericStatus>(json['status'], GenericStatus.values),
      _data = json['data'],
      _users = json['users']?.cast<String>(),
      _createdAt = json['createdAt'] != null ? TemporalDateTime.fromString(json['createdAt']) : null,
      _updatedAt = json['updatedAt'] != null ? TemporalDateTime.fromString(json['updatedAt']) : null;
  
  Map<String, dynamic> toJson() => {
    'id': id, 'name': _name, 'status': enumToString(_status), 'data': _data, 'users': _users, 'createdAt': _createdAt?.format(), 'updatedAt': _updatedAt?.format()
  };

  static final QueryField ID = QueryField(fieldName: "building2.id");
  static final QueryField NAME = QueryField(fieldName: "name");
  static final QueryField STATUS = QueryField(fieldName: "status");
  static final QueryField DATA = QueryField(fieldName: "data");
  static final QueryField USERS = QueryField(fieldName: "users");
  static var schema = Model.defineSchema(define: (ModelSchemaDefinition modelSchemaDefinition) {
    modelSchemaDefinition.name = "Building2";
    modelSchemaDefinition.pluralName = "Building2s";
    
    modelSchemaDefinition.authRules = [
      AuthRule(
        authStrategy: AuthStrategy.OWNER,
        ownerField: "users",
        identityClaim: "cognito:username",
        provider: AuthRuleProvider.USERPOOLS,
        operations: [
          ModelOperation.CREATE,
          ModelOperation.UPDATE,
          ModelOperation.DELETE,
          ModelOperation.READ
        ])
    ];
    
    modelSchemaDefinition.addField(ModelFieldDefinition.id());
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building2.NAME,
      isRequired: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.string)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building2.STATUS,
      isRequired: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.enumeration)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building2.DATA,
      isRequired: false,
      ofType: ModelFieldType(ModelFieldTypeEnum.string)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building2.USERS,
      isRequired: false,
      isArray: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.collection, ofModelName: describeEnum(ModelFieldTypeEnum.string))
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
      fieldName: 'createdAt',
      isRequired: false,
      isReadOnly: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
      fieldName: 'updatedAt',
      isRequired: false,
      isReadOnly: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
    ));
  });
}

class _Building2ModelType extends ModelType<Building2> {
  const _Building2ModelType();
  
  @override
  Building2 fromJson(Map<String, dynamic> jsonData) {
    return Building2.fromJson(jsonData);
  }
}
import 'ModelProvider.dart';
import 'package:amplify_core/amplify_core.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';


/** This is an auto generated class representing the Building3 type in your schema. */
@immutable
class Building3 extends Model {
  static const classType = const _Building3ModelType();
  final String id;
  final String? _name;
  final GenericStatus? _status;
  final String? _data;
  final List<String>? _users;
  final TemporalDateTime? _createdAt;
  final TemporalDateTime? _updatedAt;

  @override
  getInstanceType() => classType;
  
  @override
  String getId() {
    return id;
  }
  
  String get name {
    try {
      return _name!;
    } catch(e) {
      throw new AmplifyCodeGenModelException(
          AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
          recoverySuggestion:
            AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
          underlyingException: e.toString()
          );
    }
  }
  
  GenericStatus get status {
    try {
      return _status!;
    } catch(e) {
      throw new AmplifyCodeGenModelException(
          AmplifyExceptionMessages.codeGenRequiredFieldForceCastExceptionMessage,
          recoverySuggestion:
            AmplifyExceptionMessages.codeGenRequiredFieldForceCastRecoverySuggestion,
          underlyingException: e.toString()
          );
    }
  }
  
  String? get data {
    return _data;
  }
  
  List<String>? get users {
    return _users;
  }
  
  TemporalDateTime? get createdAt {
    return _createdAt;
  }
  
  TemporalDateTime? get updatedAt {
    return _updatedAt;
  }
  
  const Building3._internal({required this.id, required name, required status, data, users, createdAt, updatedAt}): _name = name, _status = status, _data = data, _users = users, _createdAt = createdAt, _updatedAt = updatedAt;
  
  factory Building3({String? id, required String name, required GenericStatus status, String? data, List<String>? users}) {
    return Building3._internal(
      id: id == null ? UUID.getUUID() : id,
      name: name,
      status: status,
      data: data,
      users: users != null ? List<String>.unmodifiable(users) : users);
  }
  
  bool equals(Object other) {
    return this == other;
  }
  
  @override
  bool operator ==(Object other) {
    if (identical(other, this)) return true;
    return other is Building3 &&
      id == other.id &&
      _name == other._name &&
      _status == other._status &&
      _data == other._data &&
      DeepCollectionEquality().equals(_users, other._users);
  }
  
  @override
  int get hashCode => toString().hashCode;
  
  @override
  String toString() {
    var buffer = new StringBuffer();
    
    buffer.write("Building3 {");
    buffer.write("id=" + "$id" + ", ");
    buffer.write("name=" + "$_name" + ", ");
    buffer.write("status=" + (_status != null ? enumToString(_status)! : "null") + ", ");
    buffer.write("data=" + "$_data" + ", ");
    buffer.write("users=" + (_users != null ? _users!.toString() : "null") + ", ");
    buffer.write("createdAt=" + (_createdAt != null ? _createdAt!.format() : "null") + ", ");
    buffer.write("updatedAt=" + (_updatedAt != null ? _updatedAt!.format() : "null"));
    buffer.write("}");
    
    return buffer.toString();
  }
  
  Building3 copyWith({String? id, String? name, GenericStatus? status, String? data, List<String>? users}) {
    return Building3._internal(
      id: id ?? this.id,
      name: name ?? this.name,
      status: status ?? this.status,
      data: data ?? this.data,
      users: users ?? this.users);
  }
  
  Building3.fromJson(Map<String, dynamic> json)  
    : id = json['id'],
      _name = json['name'],
      _status = enumFromString<GenericStatus>(json['status'], GenericStatus.values),
      _data = json['data'],
      _users = json['users']?.cast<String>(),
      _createdAt = json['createdAt'] != null ? TemporalDateTime.fromString(json['createdAt']) : null,
      _updatedAt = json['updatedAt'] != null ? TemporalDateTime.fromString(json['updatedAt']) : null;
  
  Map<String, dynamic> toJson() => {
    'id': id, 'name': _name, 'status': enumToString(_status), 'data': _data, 'users': _users, 'createdAt': _createdAt?.format(), 'updatedAt': _updatedAt?.format()
  };

  static final QueryField ID = QueryField(fieldName: "building3.id");
  static final QueryField NAME = QueryField(fieldName: "name");
  static final QueryField STATUS = QueryField(fieldName: "status");
  static final QueryField DATA = QueryField(fieldName: "data");
  static final QueryField USERS = QueryField(fieldName: "users");
  static var schema = Model.defineSchema(define: (ModelSchemaDefinition modelSchemaDefinition) {
    modelSchemaDefinition.name = "Building3";
    modelSchemaDefinition.pluralName = "Building3s";
    
    modelSchemaDefinition.authRules = [
      AuthRule(
        authStrategy: AuthStrategy.OWNER,
        ownerField: "owner",
        identityClaim: "cognito:username",
        provider: AuthRuleProvider.USERPOOLS,
        operations: [
          ModelOperation.CREATE,
          ModelOperation.UPDATE,
          ModelOperation.DELETE,
          ModelOperation.READ
        ]),
      AuthRule(
        authStrategy: AuthStrategy.GROUPS,
        groupClaim: "cognito:groups",
        groupsField: "groups",
        provider: AuthRuleProvider.USERPOOLS,
        operations: [
          ModelOperation.CREATE,
          ModelOperation.UPDATE,
          ModelOperation.DELETE,
          ModelOperation.READ
        ])
    ];
    
    modelSchemaDefinition.addField(ModelFieldDefinition.id());
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building3.NAME,
      isRequired: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.string)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building3.STATUS,
      isRequired: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.enumeration)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building3.DATA,
      isRequired: false,
      ofType: ModelFieldType(ModelFieldTypeEnum.string)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.field(
      key: Building3.USERS,
      isRequired: false,
      isArray: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.collection, ofModelName: describeEnum(ModelFieldTypeEnum.string))
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
      fieldName: 'createdAt',
      isRequired: false,
      isReadOnly: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
    ));
    
    modelSchemaDefinition.addField(ModelFieldDefinition.nonQueryField(
      fieldName: 'updatedAt',
      isRequired: false,
      isReadOnly: true,
      ofType: ModelFieldType(ModelFieldTypeEnum.dateTime)
    ));
  });
}

class _Building3ModelType extends ModelType<Building3> {
  const _Building3ModelType();
  
  @override
  Building3 fromJson(Map<String, dynamic> jsonData) {
    return Building3.fromJson(jsonData);
  }
}

@surisakc
Copy link
Author

surisakc commented May 6, 2022

@HuiSF FYI, here is the configuration that works i.e. the datastore is sync'ed up at the app's startup and the datastore has the correct Building2 records when login with different accounts based on the ownerField group. This only works in iPhone. Android still has the same error.

type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub::username", ownerField: "users"}, {allow: private, provider: iam}]) {
id: ID!
name: String!
status: GenericStatus!
data: String
users: [String]
}

@HuiSF HuiSF added the to-be-reproduced Issues that have not been reproduced yet, but have reproduction steps provided label May 6, 2022
@fjnoyp
Copy link
Contributor

fjnoyp commented May 7, 2022

Hi @surisakc based on what you've written, it seems like you saved these models before changing your schema.graphql?

If that is correct, could you try clearing your database, and deleting your app locally (to clear local store), rebuilding your schema.graphql and then saving/syncing your models again?

@surisakc
Copy link
Author

surisakc commented May 10, 2022

Hi @surisakc based on what you've written, it seems like you saved these models before changing your schema.graphql?

If that is correct, could you try clearing your database, and deleting your app locally (to clear local store), rebuilding your schema.graphql and then saving/syncing your models again?

Hi @fjnoyp, from what I did test in Android last Friday with the latest schema I mentioned above, if I deleted the datastore locally and ran the app, it would sync up the datastore with correct data for an account. If I logged-out and logged-in with different account, it didn't sync up the datastore with correct data for this account (still saw the dataset belonged to the previous account). Again, if I deleted the datastore after logged-out every time, then next login it will sync up datastore correctly.

@surisakc
Copy link
Author

surisakc commented May 11, 2022

Could it be the issue with @manyToMany used in model User and Family? Android doesn't delete/clear datastore file when logout and next login with different account it fails to sync? Here is the error got from the debugger log:

W/amplify:aws-datastore( 4279): Unauthorized failure:ON_DELETE FamilyUsers

enum GenericStatus {
  ACTIVE
  INACTIVE
  DELETED
}

enum WindowStatus {
  not_yet_measured
  select_styles
  order_pending
  order_received
  order_in_progress
  order_in_transit
  not_yet_installed
  installed
  pairing_pending
  paired
  deleted
}

enum FamilyRole {
  OWNER
  KID
  USER
  ADMIN
}

enum AddressType {
  billing
  shipping
}

type Address @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  line1: String!
  line2: String
  city: String!
  state: String!
  country: String!
  postalCode: String!
  type: AddressType
  isBilling: Boolean
  isShipping: Boolean
  isSelected: Boolean
  status: GenericStatus!
  data: String
}

type ProductCategory @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  verbose_name: String
  notes: String
  status: GenericStatus!
  data: String
  custom_fields: String
  components: [Component] @hasMany(indexName: "byProductCategory", fields: ["id"])
  productID: ID @index(name: "byProduct")
  product: Product @belongsTo(fields: ["productID"])
  genericproductID: ID @index(name: "byGenericProduct")
  genericproduct: GenericProduct @belongsTo(fields: ["genericproductID"])
}

type Component @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  verbose_name: String
  notes: String
  status: GenericStatus!
  data: String
  custom_fields: String
  productcategoryID: ID @index(name: "byProductCategory", sortKeyFields: ["name"])
  productcategory: ProductCategory @belongsTo(fields: ["productcategoryID"])
}

type Product @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  verbose_name: String
  description: String
  product_line: String
  external_id: String
  price: Float
  notes: String
  status: GenericStatus!
  data: String
  custom_fields: String
  manufacturerID: ID
  manufacturer: Manufacturer @hasOne(fields: ["manufacturerID"])
  componentID: ID
  component: Component @hasOne(fields: ["componentID"])
  categories: [ProductCategory] @hasMany(indexName: "byProduct", fields: ["id"])
  genericproductID: ID @index(name: "byGenericProduct")
  genericproduct: GenericProduct @belongsTo(fields: ["genericproductID"])
}

type Manufacturer @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
  id: ID!
  name: String
  verbose_name: String
  notes: String
  status: GenericStatus!
  data: String
  custom_fields: String
}

type Partner @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
  id: ID!
  name: String
  verbose_name: String
  address: String
  contact: String
  notes: String
  status: GenericStatus!
  data: String
  custom_fields: String
}

type GenericProduct @model @auth(rules: [{allow: private, operations: [read]}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  verbose_name: String
  configuration: String
  notes: String
  status: GenericStatus!
  data: String
  custom_fields: String
  products: [Product] @hasMany(indexName: "byGenericProduct", fields: ["id"])
  categories: [ProductCategory] @hasMany(indexName: "byGenericProduct", fields: ["id"])
}

type Window @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: WindowStatus!
  data: String
  families: [Family] @hasMany(indexName: "byWindowFamily", fields: ["id"])
  users: [User] @hasMany(indexName: "byWindowUser", fields: ["id"])
  roomID: ID
  room: Room @hasOne(fields: ["roomID"])
}

type Room @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  buildingID: ID
  building: Building @hasOne(fields: ["buildingID"])
  families: [Family] @hasMany(indexName: "byRoomFamily", fields: ["id"])
  users: [User] @hasMany(indexName: "byRoomUser", fields: ["id"])
}

type Building @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  families: [Family] @hasMany(indexName: "byBuildingFamily", fields: ["id"])
  users: [User] @hasMany(indexName: "byBuildingUser", fields: ["id"])
}

type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub::username", ownerField: "users"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

#type Building3 @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: groups, groupsField: "users"}, {allow: private, provider: iam}]) {
#  id: ID!
#  name: String!
#  status: GenericStatus!
#  data: String
#  users: [String]
#}

# @manyToMany creates a relationship table FamilyUsers
type Family @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  members: [User] @manyToMany(relationName: "FamilyUsers")
  windowID: ID @index(name: "byWindowFamily", sortKeyFields: ["name"])
  window: Window @belongsTo(fields: ["windowID"])
  roomID: ID @index(name: "byRoomFamily", sortKeyFields: ["name"])
  room: Room @belongsTo(fields: ["roomID"])
  buildingID: ID @index(name: "byBuildingFamily", sortKeyFields: ["name"])
  building: Building @belongsTo(fields: ["buildingID"])
}

type User @model @auth(rules: [{allow: owner, identityClaim: "sub::username"}, {allow: private, provider: iam}]) {
  id: ID!
  email: String!
  status: GenericStatus!
  data: String
  families: [Family] @manyToMany(relationName: "FamilyUsers")
  windowID: ID @index(name: "byWindowUser", sortKeyFields: ["email"])
  window: Window @belongsTo(fields: ["windowID"])
  roomID: ID @index(name: "byRoomUser", sortKeyFields: ["email"])
  room: Room @belongsTo(fields: ["roomID"])
  buildingID: ID @index(name: "byBuildingUser", sortKeyFields: ["email"])
  building: Building @belongsTo(fields: ["buildingID"])
}
 
type Client @model @auth(rules: [{ allow: owner, identityClaim: "sub::username" }, { allow: private, provider: iam }]) {
    id: ID!
    firstname: String!
    lastname: String!
    email: String!
    telephone: String!
    claimed: Boolean
    data: String
}

@HuiSF
Copy link
Member

HuiSF commented May 12, 2022

Hello @surisakc sorry for the delayed response.

I did some digging, looking at your schema: ownerField: "users", you are trying to use the dynamic group authorization.

type Building2 @model @auth(rules: [{allow: owner, ownerField: "users"}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

There is a known limitation with AppSync, that realtime subscription is not supported with dynamic group authorization. Hence Amplify DataStore doesn't support this type of authorization as well, but only static group authorization. Please see the documentation.

@HuiSF HuiSF added pending-community-response Pending response from the issue opener or other community members and removed to-be-reproduced Issues that have not been reproduced yet, but have reproduction steps provided pending-triage This issue is in the backlog of issues to triage labels May 12, 2022
@surisakc
Copy link
Author

Hi @HuiSF, thank you for your response.

I saw in the document about the known limitation as well but from my test the iOS worked if I set {allow: owner, identityClaim: "sub::username", ownerField: "users"}. With that I thought Android may also work but it didn't. Android will work if the datastore file gets deleted when sign out but from my test, it failed to delete the datastore file with the error that it couldn't clear the relation table FamilyUser (which is not defined explicitly in the schema as it is created on the fly if not wrong from two models Family and User when using @manyToMany). When I saw such error message pointing to the relation table not being deleted at sign out, I thought that was the issue that prevented the datastore to be synced next sign in (as when I manually deleted the datastore file and next sign in it synced up correct data) and if it could delete the datastore at sign out then next sign in in Android it would sync up the correct datastore for correct account. When I saw this behavior, I wasn't sure if it related to the know limitation (which it should mean it shouldn't sync up datastore at all in any platform; iOS nor Android).

We need this dynamic group authorization in our app. If we use the static group authorization, it means the app has to dynamically create and delete a group and add users (userIds) in the group if I understand correctly and I may have to use the AWS Cognito API library directly if Amplify doesn't provide the API to do so, correct?

Thanks!

@HuiSF
Copy link
Member

HuiSF commented May 13, 2022

Hi @surisakc , for this auth rule {allow: owner, identityClaim: "sub::username", ownerField: "users"} (owner field is a list), as the document states, it also doesn't support AppSync realtime subscription, I'd expect feature gaps in platform libraries, which cause exceptions. :/

Your thought is correct using low level Cognito API to create groups. But I'm not sure if it's a good idea, as Cognito quota has a limit on creating user groups.

Do you mind to described your use case design around the groups? I'll see if we can find any alternatives to resolve this.

@surisakc
Copy link
Author

@HuiSF I agreed with you on using the low level Cognito API to create groups at runtime may not be a good solution. Using the groups as static should be ideal.

From what I was testing, we wanted to share some models in datastore which means;

  • User A creates a model/data/record and saves it in his datastore (he is the owner).
  • User A invites User B to see the model/data later by adding User B ID into a group in this case users: [String] and ownerField: "users". If using low level Cognito API to create a runtime group, say groupA, and add User B ID to the Cognito groupA, later on it could be more users to be added and also we have to delete groups if not used anymore (and also the Cognito quota limits).
  • User B logins and should see the data created by User A in his datastore.
  • User B could modify the data and User A could see the changes in realtime.
  • Note that we want to take advantage of offline feature in Amplify if we could. We knew that if we used Amplify GraphQL API which is online feature, the syncing part may work better but that's not what planned.

@surisakc
Copy link
Author

@HuiSF A quick question about using a lambda function to override authorization rule in schema.

  • Is it possible if I set the owner field to a family ID (to share a model in a family with many users) using the lambda function while the logged-in user can see that model/data in his datastore? Will it break the concept of the datastore update and sync?

@HuiSF
Copy link
Member

HuiSF commented May 25, 2022

Hi @surisakc

Is it possible if I set the owner field to a family ID (to share a model in a family with many users) using the lambda function while the logged-in user can see that model/data in his datastore? Will it break the concept of the datastore update and sync?

I doubt this will work as when you rely on owner based auth, it will check db owner field value and the username attached int he JWT token. For this case I think it breaks.

But have you tried something like this?

  1. Add a custom attribute to userpool, let's say familyId.
  2. Take this schema
    1. set ownerField as familyId
    2. override identityClaim to use the custom attribute
# quick example, I haven't verified this is working
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: "familyId"}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  familyId: String
}
  1. When adding a user to a family, assign familyId (probably need a lambda function or API to do this separately outside of Amplify)
  2. When saving model in datastore, you should be able to get familyId by getting user attribute, and assign its value to the model familyId field, which will be used as "owner"
  3. When you sync, mutate model, the auth will compare against familyId to enforce permission

@surisakc
Copy link
Author

surisakc commented May 26, 2022

Add a custom attribute to userpool, let's say familyId.

Hi @HuiSF Thanks for the reply. For the solution you suggested. I have some questions.

  1. From the schema you provided, I set the ownerField to familyId as your example in some models that they can be shared among users in the same family.
  2. In Cognito Users Pool, I add a new custom field familyId.
  3. In the app, when UserA logged-in and wants to share a model Building2 for instance to UserB and UserC, I need to, say, use AWS Cognito API called AdminUpdateUsername() or something similar to add the custom field familyId and assign the family ID value and update the account of UserA (may be able to update from the app for the logged-in user) and to update to the other accounts of UserB and UserC, I may need to implement a function in the backend (REST) or a lambda function which will call the same Cognito API function to add and set the familyId to those accounts.
  4. When UserA doesn't want to share the model anymore that the Cognito API can set null or empty value to the custom field in those accounts, correct?

Here is how I tested it and if it was correct steps then it didn't work. Let me know if I missed any step here.

  • I did try updating the schema (one at a time) with different identifyClaim as below.
type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: **"familyId"**}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  familyId: String
}

type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: **"custom:familyId"**}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  familyId: String
}

type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", identityClaim: **"custom::familyId"**}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  familyId: String
}
  • I added a custom attribute custom:familyId in Cognito Users Pool and update the attribute to my account as custom:familyId = "familyA" which showed like below when viewed my account in Cognito console.

Screen Shot 2022-05-26 at 10 52 38 AM

  • I then went to Amplify Studio and updated the field familyId with the same value "familyA" I used in the custom attribute above.

Screen Shot 2022-05-26 at 10 52 54 AM

  • I logged-in with my Cognito account (with correct custom:familyId set) but the datastore didn't sync up with correct Building2 data i.e. the table was empty.

Thanks!

@HuiSF
Copy link
Member

HuiSF commented May 26, 2022

The process you described makes sense to me.

When UserA doesn't want to share the model anymore that the Cognito API can set null or empty value to the custom field in those accounts, correct?

Yes I believe so, setting null will remove the auth token to retrieve records for that particular user with owner based auth.

With this use case, I think you need to poll userAttributes of currently signed in user often (or other equivalent), to ensure the user is still assigned with that particular familyID, if the familyID is changed (becomes null or a different) you may need to invoke DataStore.clear() then restart DataStore to re-sync Data.

@surisakc
Copy link
Author

surisakc commented May 27, 2022

With this use case, I think you need to poll userAttributes of currently signed in user often (or other equivalent), to ensure the user is still assigned with that particular familyID, if the familyID is changed (becomes null or a different) you may need to invoke DataStore.clear() then restart DataStore to re-sync Data.

But from testing, this solution doesn't work (the datastore doesn't have the Building2 data).

I saw something similar to your suggestion from here https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims which I did try like schema below but it didn't work either. Does it have to be {allow: owner, identityClaim: "familyId"} or {allow: owner, identityClaim: "custom:familyId"}? Do you have an example that works?

Example I:
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "familyId"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  owner: String
}

Example II:
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "custom:familyId"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  owner: String
}

I set Building2.owner: "abdf51d5-cd60-45c1-8970-55d0972cb7ec" and my Cognito Account custom:familyId: "abdf51d5-cd60-45c1-8970-55d0972cb7ec". I also tried to set both with string familyA. It doesn't work at all.

By setting {allow: owner, identityClaim: "custom:familyId"}, is it by default using the accessToken for access authorization or will it use idtoken which has identity claim in it (I saw my idtoken had the claims with correct custom:family_id with correct value)? I did check in the resolvers and it should get the identity claim for access authorization. Do I have to do anything like update Authorization Header somewhere some how for this to work?

#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.familyId, null) )
    #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("custom:family_id"), "___xamznone____") )
    #set( $ownerAllowedFields0 = ["id","name","status","data","familyId","_version","_deleted","_lastChangedAt"] )
    #set( $ownerNullAllowedFields0 = ["id","name","status","data","familyId"] )
    #set( $isAuthorizedOnAllFields0 = true )
    #if( $ownerEntity0 == $ownerClaim0 )
      #if( $isAuthorizedOnAllFields0 )
        #set( $isAuthorized = true )
      #else
        $util.qr($allowedFields.addAll($ownerAllowedFields0))
        $util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0))
      #end
    #end
  #end
#end

@HuiSF
Copy link
Member

HuiSF commented Jun 3, 2022

Hi @surisakc looking at the resolver, it listed fields in the model schema as

["id","name","status","data","familyId","_version","_deleted","_lastChangedAt"]

Which indicates your model should have familyId field instead of owner... Does this resolver match your model schema?

@HuiSF
Copy link
Member

HuiSF commented Jun 4, 2022

By setting {allow: owner, identityClaim: "custom:familyId"}, is it by default using the accessToken for access authorization or will it use idtoken which has identity claim in it (I saw my idtoken had the claims with correct custom:family_id with correct value)?

I think it uses access token by default, might need to set idToken to the HTTP request Authorization header, though amplify-flutter doesn't support custom header yet. And using id token may not be the best practice for security...

@surisakc
Copy link
Author

surisakc commented Jun 6, 2022

Which indicates your model should have familyId field instead of owner... Does this resolver match your model schema?

Hi @HuiSF, sorry for not posting the latest schema which has familyId.

type Building2 @model @auth(rules: [{allow: owner, ownerField: "familyId", provider: userPools, identityClaim: "custom:family_id"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  familyId: String
  #owner: String
}

@surisakc
Copy link
Author

surisakc commented Jun 6, 2022

I think it uses access token by default, might need to set idToken to the HTTP request Authorization header, though amplify-flutter doesn't support custom header yet. And using id token may not be the best practice for security...

Hi @HuiSF Thank you for your response. So, for the example that using identityClaim: "user_id" in this https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims, does it mean I have to add a custom attribute in Cognito called user_id correct? And Cognito will create it to be custom:user_id, correct? If so, from the example in the link above, how is it going to work for the sync part if Amplify uses the access token but it doesn't contain the custom attribute? My point is if I didn't understand how to setup identityClaim from the link or if there is a working way that I missed?

Another question is when the app starts up first time, how does Amplify sync datastore? i.e. which AppSync API function that Amplify will use to sync datastore at this stage and which resolver.
i.e.
AppSync API: syncBuilding2s()
Resolver: Query.syncBuilding2s.auth.1.req.vlt

Screen Shot 2022-06-06 at 8 53 55 AM

Screen Shot 2022-06-06 at 8 54 30 AM

Thanks!

@Jordan-Nelson Jordan-Nelson added the pending-triage This issue is in the backlog of issues to triage label Jun 6, 2022
@HuiSF
Copy link
Member

HuiSF commented Jun 14, 2022

Hello @surisakc

Sorry for the delay -

So, for the example that using identityClaim: "user_id" in this https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims, does it mean I have to add a custom attribute in Cognito called user_id correct? And Cognito will create it to be custom:user_id, correct?

This example is for using 3rd-party auth provider (OIDC in the doc) with which you override the identity claim to use the fields listed in the JWT token generated by this 3rd-party, not an attribute managed by Cognito.

Based on the findings and your testing, I think using custom user attributes along with idToken unfortunately is not doable now due to

  1. amplify-flutter API plugin doesn't support setting custom headers at this moment
  2. DataStore plugin doesn't set custom headers when making mutation/query requests to AppSync via the API plugin

Another question is when the app starts up first time, how does Amplify sync datastore?

It making requests using all the sync queries (one sync query per model). Were you looking at the generated lambda function files? You should be able to get a better view in AppSync console ("functions " in left navigation menu), where you can view the functions and their implementations associated with sync queries. e.g.

image


Additional thoughts:

In the doc you linked in the latest comment, it mentioned about "Pre Token Generation Lambda Trigger", it has potentials to modify the token before the service return the token to client. This example shows a way to override user group.

@surisakc
Copy link
Author

surisakc commented Jun 14, 2022

Were you looking at the generated lambda function files?

Hi @HuiSF I was guessing and checking the generated lambda functions in AppSync console which generated from the resolver templates generated by the amplify CLI on my machine under the folder amplify/backend, correct?

  • Has anyone requested the same business logic as I did i.e. to be able to share models/data among authorized users?
  • Is there a complete example on how to create a custom lambda authorizer in NodeJS? i.e. advanced technique how to get the value from a field in a model Building2.familyId for instance to compare to the Cognito custom attribute custom:familyId and set the isAuthorized to false or true and if it's true, what data to return in the lambda, see the example below.
  • Will the custom lambda authorizer like below work with DataStore syncing when the app starts and with DataStore functions like Amplify.Datastore.save/update/delete or it only works with Amplify.API.save/update/delete?
  • Does the custom lambda authorizer work with schema @auth(rules: [{allow: custom}]) for Datastore or it's for GraphQL API only?
/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
exports.handler = async (event) => {
  console.log(`EVENT: ${JSON.stringify(event)}`);
  const {
    authorizationToken,
    requestContext: { apiId, accountId },
  } = event;
  const response = {
    isAuthorized: authorizationToken === 'custom-authorized',
    resolverContext: {
      userid: 'user-id',
      info: 'contextual information A',
      more_info: 'contextual information B',
    },
    deniedFields: [
      `arn:aws:appsync:${process.env.AWS_REGION}:${accountId}:apis/${apiId}/types/Event/fields/comments`,
      `Mutation.createEvent`,
    ],
    ttlOverride: 300,
  };
  console.log(`response >`, JSON.stringify(response, null, 2));
  return response;
};

@HuiSF
Copy link
Member

HuiSF commented Jun 14, 2022

under the folder amplify/backend, correct?

That's correct.

Has anyone requested the same business logic as I did i.e. to be able to share models/data among authorized users?

Not I'm aware of, but this is like a multi-tenants use case there may be similar requests in AppSync but I don't have a list in handy.

Is there a complete example on how to create a custom lambda authorizer in NodeJS?

Need to search around.

Will the custom lambda authorizer like below work with DataStore syncing when the app starts and with DataStore functions like Amplify.Datastore.save/update/delete or it only works with Amplify.API.save/update/delete?

If API plugin APIs work with this custom authorizer, including subscription, then DataStore should work with it too, as DataStore is fully depends on API plugin.

Does the custom lambda authorizer work with schema @auth(rules: [{allow: custom}]) for Datastore or it's for GraphQL API only?

It should work for DataStore as well as the answer to the previous question.

@HuiSF
Copy link
Member

HuiSF commented Jun 16, 2022

By setting {allow: owner, identityClaim: "custom:familyId"}, is it by default using the accessToken for access authorization or will it use idtoken which has identity claim in it (I saw my idtoken had the claims with correct custom:family_id with correct value)?

Regarding this point, I'm syncing up with amplify-ios, amplify-android and amplify-cli maintainers to determine the correct behavior - whether the amplify libraries should resolve the custom claim from idToken when nothing can't be resolved from access token.

@surisakc
Copy link
Author

surisakc commented Jun 17, 2022

I'm syncing up with amplify-ios, amplify-android and amplify-cli maintainers to determine the correct behavior - whether the amplify libraries should resolve the custom claim from idToken when nothing can't be resolved from access token.

Hi @HuiSF Thank you for checking! I did more testing and it seems if amplify-android library for flutter could somehow makes the library to work like amplify-ios (for flutter) then it may fix the issue i.e. if amplify-android can support Test 3 like amplify-ios does.

Test 2 and Test 4 partially work in Android because the identityClaim is in the accessToken. While Test 3 the identityClaim isn't in the accessToken and it doesn't work in Android but somehow it works in iOS.

Here are my tests

Flutter: v2.10.5
Amplify Flutter: v0.5.1
Amplify CLI: 8.3.1

Schema using a list of owners which has known limitation (not support realtime subscription)

  • Note although the document said using the schema with this auth rule won't have a realtime subscription but from testing it works in iOS but not Android. This is the closest schema that I thought by fixing something (may be in Android?) and it may work (not sure if there will be any other issues).
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub::username", ownerField: "users"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}
  • Test 1: {allow: owner, identityClaim: "cognito:username", ownerField: "users"} (the claim isn’t in accessToken but in idToken)

    • doesn’t work in iOS.
    • doesn’t work in Android.
  • Test 2: {allow: owner, identityClaim: "username", ownerField: "users"} (the claim is in accessToken)

    • doesn’t work in iOS. only see networkstatus amplify event, not modelsynced
    • works in Android with error below and the datastore sync works sometimes (with or without the error), not every time. (same as ``{allow: owner, identityClaim: "sub", ownerField: "users"}` )
GraphQLResponseException{message=Subscription error for Building2: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}
  • Test 3: {allow: owner, identityClaim: "sub::username", ownerField: "users"} (the claim isn’t in accessToken)
    • works in iOS.
    • doesn’t work in Android, got error
    I/amplify:aws-datastore( 3827): Setting currentState to STOPPED
    I/amplify:flutter:datastore( 3827): Successfully stopped datastore remote synchronization
    I/amplify:aws-datastore( 3827): Orchestrator lock released.
    E/amplify:aws-datastore( 3827): Failure encountered while attempting to start API sync.
    E/amplify:aws-datastore( 3827): DataStoreException{message=Error during subscription., cause=ApiAuthException{message=Attempted to subscribe to a model with owner-based authorization without sub::username which was specified (or defaulted to) as the identity claim., cause=null, recoverySuggestion=If you did not specify a custom identityClaim in your schema, make sure you are logged in. If you did, check that the value you specified in your schema is present in the access key.}, recoverySuggestion=Evaluate details.}
    E/amplify:aws-datastore( 3827): 	at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$3(AppSyncClient.java:331)
    E/amplify:aws-datastore( 3827): 	at com.amplifyframework.datastore.appsync.-$$Lambda$AppSyncClient$797ziDK0io-qXODzROLOA77stS8.accept(Unknown Source:4)
    
    W/amplify:aws-datastore( 3827): DataStoreException{message=Error during subscription., cause=ApiAuthException{message=Attempted to subscribe to a model with owner-based authorization without sub::username which was specified (or defaulted to) as the identity claim., cause=null, recoverySuggestion=If you did not specify a custom identityClaim in your schema, make sure you are logged in. If you did, check that the value you specified in your schema is present in the access key.}, recoverySuggestion=Evaluate details.}
    W/amplify:aws-datastore( 3827): 	at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$3(AppSyncClient.java:331)
    W/amplify:aws-datastore( 3827): 	at com.amplifyframework.datastore.appsync.-$$Lambda$AppSyncClient$797ziDK0io-qXODzROLOA77stS8.accept(Unknown Source:4)
  • Test 4: {allow: owner, identityClaim: "sub", ownerField: "users"} (the claim is in accessToken)
    • doesn’t work in iOS.
    • work in Android with error below and the datastore sync works sometimes (with or without the error), not every time. Also the generated resolver Subscription.onDeleteBuilding2.auth.1.req.vtl (see the Original Resolver below) doesn't have the part #if( $util.authType() == "User Pool Authorization" ) (wondering if this part means it is the known limitation that doesn't support the realtime subscription?). I did create a custom resolver to override the missing part (see the Custom Resolver below) thinking it may fix the issue but it doesn't.

Original Resolver

## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
  #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({}))
    #end
  #end
  #if( !$isAuthorized )
    #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
      #set( $isAuthorized = true )
    #end
  #end
#end
#if( $util.authType() == "User Pool Authorization" )
  
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **

Custom Resolver

## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
  #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({}))
    #end
  #end
  #if( !$isAuthorized )
    #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
      #set( $isAuthorized = true )
    #end
  #end
#end
#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, null) )
    #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
    #set( $ownerClaimsList0 = [] )
    #foreach( $allowedOwner in $ownerEntity0 )
      #if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
        #set( $isAuthorized = true )
      #end
    #end
  #end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **

Test 4 Error

      GraphQLResponseException{message=Subscription error for Building2: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onDeleteBuilding2'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}
      z
  • Test 5: {allow: owner, identityClaim: "custom:familyId", ownerField: "familyId"} (the claim is in idToken which not support in Datastore, accessToken is defaulted)
    • doesn’t work in iOS.
    • doesn’t work in Android.

@surisakc
Copy link
Author

surisakc commented Jun 17, 2022

Hi @HuiSF

I think this solution works in both iOS and Android to share models among users. I didn't test by removing the three custom Subscription resolvers as I thought they may be needed but may be not.

Schema

  • Use the attribute existing in accessToken (in this case use sub)
type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub", ownerField: "users"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

3 Custom Subscription Resolvers project/amplify/backend/api/<api name>/resolvers

FILE: Subscription.onCreateBuilding2.auth.1.req
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
  #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({}))
    #end
  #end
  #if( !$isAuthorized )
    #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
      #set( $isAuthorized = true )
    #end
  #end
#end
#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $ownerEntity0 = $util.defaultIfNull($ctx.args.input.users, null) )
    #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
    #set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
    #set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
    #set( $ownerClaimsList0 = [] )
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
    #set( $ownerAllowedFields0 = ["id","name","status","data","users","_version","_deleted","_lastChangedAt"] )
    #set( $isAuthorizedOnAllFields0 = true )
    #foreach( $allowedOwner in $ownerEntity0 )
      #if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
        #if( $isAuthorizedOnAllFields0 )
          #set( $isAuthorized = true )
          #break
        #else
          $util.qr($allowedFields.addAll($ownerAllowedFields0))
        #end
      #end
    #end
    #if( $util.isNull($ownerEntity0) && !$ctx.args.input.containsKey("users") )
      $util.qr($ctx.args.input.put("users", [$ownerClaim0]))
      #if( $isAuthorizedOnAllFields0 )
        #set( $isAuthorized = true )
      #else
        $util.qr($allowedFields.addAll($ownerAllowedFields0))
      #end
    #end
  #end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
=====================================================================

FILE: Subscription.onDeleteBuilding2.auth.1.req
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
  #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({}))
    #end
  #end
  #if( !$isAuthorized )
    #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
      #set( $isAuthorized = true )
    #end
  #end
#end
#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, null) )
    #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
    #set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
    #set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
    #set( $ownerClaimsList0 = [] )
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
    #foreach( $allowedOwner in $ownerEntity0 )
      #if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
        #set( $isAuthorized = true )
      #end
    #end
  #end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **
=====================================================================
FILE: Subscription.onUpdateBuilding2.auth.1.req
## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
  #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({}))
    #end
  #end
  #if( !$isAuthorized )
    #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
      #set( $isAuthorized = true )
    #end
  #end
#end
#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, []) )
    #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
    #set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
    #set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
    #set( $ownerClaimsList0 = [] )
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
    #set( $ownerAllowedFields0 = ["id","name","status","data","users","_version","_deleted","_lastChangedAt"] )
    #set( $ownerNullAllowedFields0 = ["id","name","status","data","users"] )
    #set( $isAuthorizedOnAllFields0 = true )
    #foreach( $allowedOwner in $ownerEntity0 )
      #if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
        #if( $isAuthorizedOnAllFields0 )
          #set( $isAuthorized = true )
          #break
        #else
          $util.qr($allowedFields.addAll($ownerAllowedFields0))
          $util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0))
        #end
      #end
    #end
  #end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **

Update the Generated Models

  • identityClaim: "sub" only works in Android when sync but not in iOS.
  • identityClaim: "sub::username" only works in iOS when sync but not in
// identityClaim: "sub",
identityClaim: GetPlatform.isAndroid ? "sub" : "sub::username",

Error in Android

  • From testing in Android, the datastore was sync'ed correctly when login with different accounts. Not sure if this will cause any issues later on or not.
W/amplify:aws-datastore(17573): API sync failed - transitioning to LOCAL_ONLY.
W/amplify:aws-datastore(17573): GraphQLResponseException{message=Subscription error for Building2: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onUpdateBuilding2'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument users @ 'onUpdateBuilding2'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}
W/amplify:aws-datastore(17573): 	at com.amplifyframework.datastore.appsync.AppSyncClient.lambda$subscription$2(AppSyncClient.java:324)

@surisakc
Copy link
Author

I think this solution works in both iOS and Android to share models among users. I didn't test by removing the three custom Subscription resolvers as I thought they may be needed but may be not.

For Android, it doesn't work every time when start the app and login i.e. the datastore isn't sync'ed every time (the Amplify event modelSynced isn't fired sometimes).

@HuiSF
Copy link
Member

HuiSF commented Jun 20, 2022

Validation error of type UnknownArgument: Unknown field argument users @ 'onUpdateBuilding2'

This error causes subscription to fail, which stops sync engine so you would see the unsuccessful sync. This error is due to the subscription selection set contains field is not defined in GraphQL schema (located in <projectRoot>/amplify/backend/<your-api-name>/build/schema.graphql), it should not be related to auth in particular.

@surisakc
Copy link
Author

This error causes subscription to fail, which stops sync engine so you would see the unsuccessful sync. This error is due to the subscription selection set contains field is not defined in GraphQL schema (located in <projectRoot>/amplify/backend/<your-api-name>/build/schema.graphql), it should not be related to auth in particular.

Here is my schema. The error points to the unknown field argument users but the field is in the schema. I also made sure the 3 subscription resolvers had the field users in them. The error seems to be from this module AppSyncClient.java. Is there anything else in schema I should set?

type Building2 @model @auth(rules: [{allow: owner, identityClaim: "sub", ownerField: "users"}, {allow: private, provider: iam}]) {
  id: ID!
  name: String!
  status: GenericStatus!
  data: String
  users: [String]
}

Here is one of the three subscription resolvers.

## [Start] Authorization Steps. **
$util.qr($ctx.stash.put("hasAuth", true))
#set( $isAuthorized = false )
#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["ca-central-1_WptfSDLRl_Full-access/CognitoIdentityCredentials","ca-central-1_WptfSDLRl_Manage-only/CognitoIdentityCredentials"] )
  #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({}))
    #end
  #end
  #if( !$isAuthorized )
    #if( ($ctx.identity.userArn == $ctx.stash.authRole) || ($ctx.identity.cognitoIdentityPoolId == "ca-central-1:02a4a59d-f6b2-4c86-b5dc-a42570300f0b" && $ctx.identity.cognitoIdentityAuthType == "authenticated") )
      #set( $isAuthorized = true )
    #end
  #end
#end
#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $ownerEntity0 = $util.defaultIfNull($ctx.result.users, []) )
    #set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____") )
    #set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
    #set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
    #set( $ownerClaimsList0 = [] )
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("sub"), "___xamznone____")))
    $util.qr($ownerClaimsList0.add($util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____"))))
    #set( $ownerAllowedFields0 = ["id","name","status","data","users","_version","_deleted","_lastChangedAt"] )
    #set( $ownerNullAllowedFields0 = ["id","name","status","data","users"] )
    #set( $isAuthorizedOnAllFields0 = true )
    #foreach( $allowedOwner in $ownerEntity0 )
      #if( $allowedOwner == $ownerClaim0 || $ownerClaimsList0.contains($ownerEntity0) )
        #if( $isAuthorizedOnAllFields0 )
          #set( $isAuthorized = true )
          #break
        #else
          $util.qr($allowedFields.addAll($ownerAllowedFields0))
          $util.qr($nullAllowedFields.addAll($ownerNullAllowedFields0))
        #end
      #end
    #end
  #end
#end
#if( !$isAuthorized )
$util.unauthorized()
#end
$util.toJson({"version":"2018-05-29","payload":{}})
## [End] Authorization Steps. **

@surisakc
Copy link
Author

surisakc commented Jun 22, 2022

This error causes subscription to fail, which stops sync engine so you would see the unsuccessful sync. This error is due to the subscription selection set contains field is not defined in GraphQL schema (located in <projectRoot>/amplify/backend/<your-api-name>/build/schema.graphql), it should not be related to auth in particular.

Hi @HuiSF I have fixed this issue but not sure if this is the right way and if it will have any other issues later on. Basically I have to go to AppSync Console and search in Schema for those Subscription functions that set in the schema to use ownerField="users" and users: [String] and the generated Subscription functions didn't have an input parameter (users: String) as expected in the error. I updated them to be like below. Now both iOS and Android can sync datastore.

Original

type Subscription {
        onCreateBuilding2: Building2
		@aws_subscribe(mutations: ["createBuilding2"])
@aws_iam
@aws_cognito_user_pools
	onUpdateBuilding2: Building2
		@aws_subscribe(mutations: ["updateBuilding2"])
@aws_iam
@aws_cognito_user_pools
	onDeleteBuilding2: Building2
		@aws_subscribe(mutations: ["deleteBuilding2"])
@aws_iam
@aws_cognito_user_pools
}

The fix that works

type Subscription {
        onCreateBuilding2(users: String): Building2
		@aws_subscribe(mutations: ["createBuilding2"])
@aws_iam
@aws_cognito_user_pools
	onUpdateBuilding2(users: String): Building2
		@aws_subscribe(mutations: ["updateBuilding2"])
@aws_iam
@aws_cognito_user_pools
	onDeleteBuilding2(users: String): Building2
		@aws_subscribe(mutations: ["deleteBuilding2"])
@aws_iam
@aws_cognito_user_pools
}

There is other issue in the Datastore library (Amplify_Flutter v0.5.1) of iOS and Android that doesn't work with the complex query anymore (let me know if you want me to open an issue for this)

Original Datastore query works in both iOS and Android in Amplify_Flutter v0.2.10

var _components =
          await this.retrieveData<Component>(modelName: 'Component', queryParams: Component.NAME.eq('width').and(Component.STATUS.eq(GenericStatus.ACTIVE)));

To make the query to work in iOS, I have to separate the where clause into two steps:

var _list = await this.retrieveData<Component>(modelName: 'Component', queryParams: Component.NAME.eq('width'));
      if (_list != null) {
        var _components = _list.where((element) => element.status == GenericStatus.ACTIVE).toList();
      }

To make the query to work in Android, I have to update the two query fields (see My fields) to be explicit naming like below, otherwise it will have an error about the ambiguous field. If I update the generated fields directly to be static final QueryField NAME = QueryField(fieldName: "component.name"), this will cause the error when syncing datastore:

class Component extends Model {
...
 // My fields
  static final QueryField NAME$ = QueryField(fieldName: "component.name");
  static final QueryField STATUS$ = QueryField(fieldName: "component.status");

  // Generated fields
  static final QueryField ID = QueryField(fieldName: "component.id");
  static final QueryField NAME = QueryField(fieldName: "name");
  static final QueryField VERBOSE_NAME = QueryField(fieldName: "verbose_name");
  static final QueryField NOTES = QueryField(fieldName: "notes");
  static final QueryField STATUS = QueryField(fieldName: "status");
...
}

var _components =
          await this.retrieveData<Component>(modelName: 'Component', queryParams: Component.NAME$.eq('width').and(Component.STATUS$.eq(GenericStatus.ACTIVE)));

@HuiSF HuiSF removed the pending-community-response Pending response from the issue opener or other community members label Jun 23, 2022
@HuiSF
Copy link
Member

HuiSF commented Jun 24, 2022

Hi @surisakc Thanks for providing the details about the workaround.

I have fixed this issue but not sure if this is the right way and if it will have any other issues later on

So far, I'm unsure if Amplify CLI and GraphQL transformer do have the support of your use case... But looking at your schema and resolver changes it makes sense to me. As long as your custom resolvers are able to ensure correct auth scope for your data, I think it's fine to use it.

I will find some time to do some testing with your workaround as well.

There is other issue in the Datastore library (Amplify_Flutter v0.5.1) of iOS and Android that doesn't work with the complex query anymore (let me know if you want me to open an issue for this)

We do have integration tests for ensure the compound query to be working. And I just ran the tests based on version 0.5.1 and I didn't see any issues.

Could you sharing your implementation of retrieveData?

@surisakc
Copy link
Author

surisakc commented Jun 24, 2022

We do have integration tests for ensure the compound query to be working. And I just ran the tests based on version 0.5.1 and I didn't see any issues.

Hi @HuiSF Thank you for your response. The issue I had in Amplify Android that was when it called Amplify.Datastore.query with where clause and join tables that have the same field name like name and in where clause (after generated to SQL query), it shows something like this.

table1(id, name)
table2(id, name)

SELECT *
FROM table1
JOIN table2 ON table2.id = table1.table2_id
WHERE name=?   <== this is where the error happens in Android library. It should be "WHERE table1.name/table2.name=?"

Here is the retreiveData:

Future<List<T>?> retrieveData<T extends Model>({String? modelName, QueryPredicate? queryParams}) async {
    if (modelName == null) {
      modelName = this.primaryModelName;
    }
    List<T>? results = [];
    try {
      if (this.isReady()) {
        if (queryParams != null) {
          results = await Amplify.DataStore.query(this.getClassTypeByModelName(modelName!) as ModelType<T>, where: queryParams);
        } else {
          results = await Amplify.DataStore.query(this.getClassTypeByModelName(modelName!) as ModelType<T>);
        }
      }
    } catch (e) {
      throw UnknownErrorAmplifyException(e.toString());
    }
    return results;
  }

Also, I use this auth setting ownerField: "users" where users: [String] which from the document, it has a known limitation that it doesn't support realtime subscription which I understood that if I used AppSync API mutate Update

it wouldn't trigger the Amplify.Datastore.observe() in the app, correct? I did test and it didn't, just want to confirm. Or it has to do with the auth rules in some Subscription resolvers that I need to customize them for this to work?

Will it also doesn't trigger if I use Amplify.API.subscribe() in the app?

@HuiSF
Copy link
Member

HuiSF commented Jun 27, 2022

Hi @surisakc for the nested predicate, Amplify libraries don't have a perfect support at this moment.

amplify-ios and amplify-android are working differently on this as well, hence, amplify-flutter can only take a lowest common support as for Flutter consumers both iOS and Android should behave the same. Our current recommendation is to make separate queries to retrieve models when necessary. Better support of nested query predicates are on the roadmap (tracked in this issue: #1449)

it wouldn't trigger the Amplify.Datastore.observe() in the app, correct?

That's correct, DataStore's sync engine uses API plugin, all the limitation on the API plugin are applicable to DataStore API sync as well.

@HuiSF
Copy link
Member

HuiSF commented Apr 3, 2023

From testing a fix implemented by amplify-android for these two issues (aws-amplify/amplify-android#2035, #2632) I can see that when multiple table join is needed by the query predicate, it can generate correct SQL command with proper table namespace in the where clause. I'm optimistically closing this issue, please feel free to reach out if anything.

@HuiSF HuiSF closed this as completed Apr 3, 2023
@dgagnon
Copy link

dgagnon commented Jul 6, 2023

Hi,

I can reproduce the original issue by using the exemple from the documentation at this link: https://docs.amplify.aws/cli/graphql/authorization-rules/#multi-user-data-access

Versions:

  • Amplify cli: 12.1.1
  • node / npm: 16.20 / 9.7.2
  • Flutter / dart: 3.10.5 / 3.0.5
  • amplify_api: 1.2.0
  • amplify_datastore: 1.2.0-supports-only-mobile
  • transformer: 2 with multiAuth
  • create from CLI
    graphql.schema:
type Task @model @auth(rules: [{ allow: owner, ownerField: "authors" }]) {
    content: String
    authors: [String]
}

Relevant Logs:

[  +45 ms] E/amplify:aws-datastore( 6140): Failure encountered while attempting to start API sync.
[        ] E/amplify:aws-datastore( 6140): DataStoreException{message=DataStore subscriptionProcessor failed to start., cause=GraphQLResponseException{message=Subscription error for Task: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument authors @ 'onCreateTask'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument authors @ 'onCreateTask'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}, recoverySuggestion=Check your internet.}
[        ] E/amplify:aws-datastore( 6140): 	at com.amplifyframework.datastore.syncengine.Orchestrator.lambda$startApiSync$3$com-amplifyframework-datastore-syncengine-Orchestrator(Orchestrator.java:306)
[        ] E/amplify:aws-datastore( 6140): 	at com.amplifyframework.datastore.syncengine.Orchestrator$$ExternalSyntheticLambda6.subscribe(Unknown Source:2)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.operators.completable.CompletableCreate.subscribeActual(CompletableCreate.java:40)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.core.Completable.subscribe(Completable.java:2850)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.operators.completable.CompletablePeek.subscribeActual(CompletablePeek.java:51)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.core.Completable.subscribe(Completable.java:2850)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.operators.completable.CompletablePeek.subscribeActual(CompletablePeek.java:51)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.core.Completable.subscribe(Completable.java:2850)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.operators.completable.CompletablePeek.subscribeActual(CompletablePeek.java:51)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.core.Completable.subscribe(Completable.java:2850)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.operators.completable.CompletableSubscribeOn$SubscribeOnObserver.run(CompletableSubscribeOn.java:64)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.core.Scheduler$DisposeTask.run(Scheduler.java:614)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:65)
[        ] E/amplify:aws-datastore( 6140): 	at io.reactivex.rxjava3.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:56)
[        ] E/amplify:aws-datastore( 6140): 	at java.util.concurrent.FutureTask.run(FutureTask.java:264)
[        ] E/amplify:aws-datastore( 6140): 	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307)
[        ] E/amplify:aws-datastore( 6140): 	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
[        ] E/amplify:aws-datastore( 6140): 	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
[        ] E/amplify:aws-datastore( 6140): 	at java.lang.Thread.run(Thread.java:1012)
[        ] E/amplify:aws-datastore( 6140): Caused by: GraphQLResponseException{message=Subscription error for Task: [GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument authors @ 'onCreateTask'', locations='null', path='null', extensions='null'}], errors=[GraphQLResponse.Error{message='Validation error of type UnknownArgument: Unknown field argument authors @ 'onCreateTask'', locations='null', path='null', extensions='null'}], recoverySuggestion=See attached list of GraphQLResponse.Error objects.}

Generated code:

input ModelSubscriptionTaskFilterInput {
  content: ModelSubscriptionStringInput
  and: [ModelSubscriptionTaskFilterInput]
  or: [ModelSubscriptionTaskFilterInput]
  _deleted: ModelBooleanInput
}

type Subscription {
  onCreateTask(filter: ModelSubscriptionTaskFilterInput): Task @aws_subscribe(mutations: ["createTask"]) @aws_iam @aws_cognito_user_pools
  onUpdateTask(filter: ModelSubscriptionTaskFilterInput): Task @aws_subscribe(mutations: ["updateTask"]) @aws_iam @aws_cognito_user_pools
  onDeleteTask(filter: ModelSubscriptionTaskFilterInput): Task @aws_subscribe(mutations: ["deleteTask"]) @aws_iam @aws_cognito_user_pools
}

Code to replicate:

  Future<void> configureAmplify() async {
    // Add any Amplify plugins you want to use
    final authPlugin = AmplifyAuthCognito();
    // await Amplify.addPlugin(authPlugin);

    // Add the following lines to your app initialization to add the DataStore plugin
    final datastorePlugin = AmplifyDataStore(
      modelProvider: ModelProvider.instance,
      // Be sure to add the authModeStrategy
      authModeStrategy: AuthModeStrategy.multiAuth,
    );
    // await Amplify.addPlugin(datastorePlugin);

    final api = AmplifyAPI();

    // You can use addPlugins if you are going to be adding multiple plugins
    await Amplify.addPlugins([authPlugin, api, datastorePlugin]);
    // await Amplify.addPlugins([authPlugin]);

    // Once Plugins are added, configure Amplify
    // Note: Amplify can only be configured once.
    try {
      await Amplify.configure(amplifyconfig);
    } on AmplifyAlreadyConfiguredException {
      safePrint("Tried to reconfigure Amplify; this can occur when your app restarts on Android.");
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
datastore Issues related to the DataStore Category pending-triage This issue is in the backlog of issues to triage
Projects
None yet
Development

No branches or pull requests

5 participants