amplify-flutter icon indicating copy to clipboard operation
amplify-flutter copied to clipboard

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

Open surisakc opened this issue 3 years ago • 32 comments

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)
  • [x] API (GraphQL)
  • [ ] Auth
  • [ ] Authenticator
  • [x] 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

  • [x] iOS
  • [x] 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

surisakc avatar May 02 '22 19:05 surisakc

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

HuiSF avatar May 05 '22 23:05 HuiSF

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 avatar May 06 '22 10:05 surisakc

@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] }

surisakc avatar May 06 '22 12:05 surisakc

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?

fjnoyp avatar May 07 '22 01:05 fjnoyp

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 avatar May 10 '22 01:05 surisakc

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
}

surisakc avatar May 11 '22 13:05 surisakc

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 avatar May 12 '22 18:05 HuiSF

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!

surisakc avatar May 13 '22 12:05 surisakc

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.

HuiSF avatar May 13 '22 19:05 HuiSF

@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 avatar May 13 '22 20:05 surisakc

@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?

surisakc avatar May 25 '22 13:05 surisakc

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

HuiSF avatar May 25 '22 16:05 HuiSF

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!

surisakc avatar May 26 '22 11:05 surisakc

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.

HuiSF avatar May 26 '22 18:05 HuiSF

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

surisakc avatar May 27 '22 01:05 surisakc

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 avatar Jun 03 '22 16:06 HuiSF

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...

HuiSF avatar Jun 04 '22 04:06 HuiSF

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 avatar Jun 06 '22 11:06 surisakc

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!

surisakc avatar Jun 06 '22 11:06 surisakc

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.

HuiSF avatar Jun 14 '22 18:06 HuiSF

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;
};

surisakc avatar Jun 14 '22 19:06 surisakc

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 avatar Jun 14 '22 20:06 HuiSF

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.

HuiSF avatar Jun 16 '22 18:06 HuiSF

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 avatar Jun 17 '22 11:06 surisakc

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 avatar Jun 17 '22 18:06 surisakc

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).

surisakc avatar Jun 20 '22 16:06 surisakc

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.

HuiSF avatar Jun 20 '22 21:06 HuiSF

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 avatar Jun 20 '22 22:06 surisakc

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)));

surisakc avatar Jun 22 '22 16:06 surisakc

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?

HuiSF avatar Jun 24 '22 15:06 HuiSF