Open dgagnon opened 1 year ago
Hi @dgagnon thank you very much for sharing this issue, and apologies you had to spend a lot of time as a result ...
You are right, the documentation is misleading and there is no multi owner support in Datastore at the moment. We'll look to update the docs to clarify the limits of Datastore functionality.
This is a feature we are very much interested in, you can count this a feature request as well!
I have a work around that works pretty well, using VTL resolvers and adding a "sharedWith" field on models. Maintainable easily for queries and subscriptions, but a bit weird for mutations. If there is any interest, I will take the time to write it up here.
I have a work around that works pretty well, using VTL resolvers and adding a "sharedWith" field on models. Maintainable easily for queries and subscriptions, but a bit weird for mutations. If there is any interest, I will take the time to write it up here.
@dgagnon If you could that be great thank you. My scenario is a chat room where chat room hosts can create a chat room and other users can be added read-only to the chat room. Group isn't sufficient as auth rules need to be applied for each chat room. I would end up with an enournmas amount of groups.
@James1R I will do a better write up later, but here is the schema and the required VTL for it. Note that you also need a way to get the uuid of the user you wish to share to. You will need to adjust for your actual auth settings, as this affects the VTL generated. I build these by copying from the current ones I had. This is for a multi-auth config, with IAM, owner and groups.
schema.graphql:
enum Status {
ACTIVE
COMPLETED
DELETED
}
enum UserStatus {
ACTIVE
SUSPENDED
DELETED
}
type User @model @auth(rules: [{ allow: owner, ownerField: "owner"}, {allow: private, provider: iam}]) {
id: ID! @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}])
username: String! @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}])
data: AWSJSON @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}])
attributes: AWSJSON @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}])
# https://docs.amplify.aws/lib/datastore/sync/q/platform/flutter/#advanced-use-case---query-instead-of-scan
status: UserStatus @default(value: "ACTIVE" ) @index(name: "byStatus") @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}])
owner: String @index(name: "byOwner") @auth(rules: [{ allow: owner, ownerField: "owner", operations: [read, create, delete] }, { allow: private, provider: iam }])
tasks: [Task] @hasMany(indexName: "byUser", fields: ["id"])
sharedWith: [String] @auth(rules: [{ allow: owner, ownerField: "owner" }, { allow: private, provider: iam }])
}
type Project @model @auth(rules: [{ allow: owner, ownerField: "owner"}, {allow: private, operations: [read]}, {allow: private, provider: iam}, { allow: groups, groups: ["admin"] }]) {
id: ID! @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
title: String! @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
description: String @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
data: AWSJSON @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
status: Status @default(value: "ACTIVE" ) @index(name: "byStatus") @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
dueAt: AWSDateTime! @auth(rules: [{ allow: owner, ownerField: "owner"}, { allow: private, provider: iam }, {allow: private, operations: [read]}, { allow: groups, groups: ["admin"] }])
owner: String @index(name: "byOwner") @auth(rules: [{ allow: owner, ownerField: "owner", operations: [read, create, delete] }, { allow: private, provider: iam }, {allow: private, operations: [read]},{ allow: groups, groups: ["admin"] }])
tasks: [Task] @hasMany(indexName: "byProject", fields: ["id"]) #https://docs.amplify.aws/cli/graphql/data-modeling/#belongs-to-relationship
}
type Task @model @auth(rules: [{ allow: owner, ownerField: "owner"}, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }]) {
id: ID @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
title: String! @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
description: String @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
data: AWSJSON @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"], operations: [read] }])
status: Status @default(value: "ACTIVE" ) @index(name: "byStatus") @auth(rules: [{ allow: owner, ownerField: "owner" }, {allow: private, provider: iam}, { allow: groups, groups: ["admin"] }])
dueAt: AWSDateTime! @auth(rules: [{ allow: owner, ownerField: "owner" }, { allow: private, provider: iam }, { allow: groups, groups: ["admin"], operations: [read] }])
# https://docs.amplify.aws/cli/graphql/authorization-rules/#multi-user-data-access
# owners: [String]
# BUG: https://github.com/aws-amplify/amplify-flutter/issues/1566
owner: String @index(name: "byOwner") @auth(rules: [{ allow: owner, ownerField: "owner", operations: [read, create, delete] }, { allow: private, provider: iam }, { allow: groups, groups: ["admin"], operations: [read] }])
projectID: ID! @index(name: "byProject") #https://docs.amplify.aws/cli/graphql/data-modeling/#belongs-to-relationship
project: Project! @belongsTo(fields: ["projectID"])
sharedWith: [String] @auth(rules: [{ allow: owner, ownerField: "owner" }, { allow: private, provider: iam }, { allow: groups, groups: ["admin"], operations: [read] }])
userID: ID @index(name: "byUser") #https://docs.amplify.aws/cli/graphql/data-modeling/#belongs-to-relationship
user: User @belongsTo(fields: ["userID"])
}
amplify/backup/api*/resolvers/Query.getTask.postAuth.2.req.vtl:
## [Start] Enable sharing. **
#if( !$util.isNull($ctx.stash.authFilter) )
#set( $authFilter = $ctx.stash.get("authFilter") )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#if( !$util.isNull($ownerClaim0) )
$util.qr($authFilter.or.add({"sharedWith": { "contains": $ownerClaim0 }}))
#end
#end
#set( $role0_0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#if( !$util.isNull($role0_0) )
$util.qr($authFilter.or.add({"sharedWith": { "contains": $role0_0 }}))
#end
#set( $role0_1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($role0_1) )
$util.qr($authFilter.or.add({"sharedWith": { "contains": $role0_1 }}))
#end
$util.qr($ctx.stash.put("authFilter", $authFilter))
#end
$util.toJson({})
## [End] Enable sharing. **
amplify/backup/api*/resolvers/Query.listTasks.postAuth.2.req.vtl:
## [Start] Enable sharing. **
#if( !$util.isNull($ctx.stash.authFilter) )
#set( $authFilter = $ctx.stash.get("authFilter") )
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
#if( !$util.isNull($ownerClaim0) )
$util.qr($authFilter.or.add({"sharedWith": { "contains": $ownerClaim0 }}))
#end
#end
#set( $role0_0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#if( !$util.isNull($role0_0) )
$util.qr($authFilter.or.add({"sharedWith": { "contains": $role0_0 }}))
#end
#set( $role0_1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($role0_1) )
$util.qr($authFilter.or.add({"sharedWith": { "contains": $role0_1 }}))
#end
$util.qr($ctx.stash.put("authFilter", $authFilter))
#end
$util.toJson({})
## [End] Enable sharing. **
amplify/backup/api*/resolvers/Subscription.onDeleteTask.postAuth.2.req.vtl:
## [Start] Enable sharing. **
#if( !$util.isNull($ctx.args.filter) )
#if( !$util.isNull($ctx.args.filter.or) )
#set( $authFilter = $ctx.args.filter.or )
#else
#set( $authFilter = $ctx.args.filter.and[0].or )
#end
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
$util.qr($authFilter.add({ "sharedWith": { "contains": $currentClaim1 } }))
#end
#if( !$util.isNull($ctx.args.filter.or) )
#set( $ctx.args.filter = { "or": $authFilter } )
#else
#set( $ctx.args.filter.and[0] = { "or": $authFilter } )
#end
#end
$util.toJson({})
## [End] Enable sharing. **
amplify/backup/api*/resolvers/Subscription.onUpdateTask.postAuth.2.req.vtl:
## [Start] Enable sharing. **
#if( !$util.isNull($ctx.args.filter) )
#if( !$util.isNull($ctx.args.filter.or) )
#set( $authFilter = $ctx.args.filter.or )
#else
#set( $authFilter = $ctx.args.filter.and[0].or )
#end
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
$util.qr($authFilter.add({ "sharedWith": { "contains": $currentClaim1 } }))
#end
#if( !$util.isNull($ctx.args.filter.or) )
#set( $ctx.args.filter = { "or": $authFilter } )
#else
#set( $ctx.args.filter.and[0] = { "or": $authFilter } )
#end
#end
$util.toJson({})
## [End] Enable sharing. **
amplify/backup/api*/resolvers/Subscription.onCreateTask.postAuth.2.req.vtl:
## [Start] Enable sharing. **
#if( !$util.isNull($ctx.args.filter) )
#if( !$util.isNull($ctx.args.filter.or) )
#set( $authFilter = $ctx.args.filter.or )
#else
#set( $authFilter = $ctx.args.filter.and[0].or )
#end
#set( $ownerClaim0 = $util.defaultIfNull($ctx.identity.claims.get("sub"), null) )
#set( $currentClaim1 = $util.defaultIfNull($ctx.identity.claims.get(
"username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), null)) )
#if( !$util.isNull($ownerClaim0) && !$util.isNull($currentClaim1) )
#set( $ownerClaim0 = "$ownerClaim0::$currentClaim1" )
$util.qr($authFilter.add({ "sharedWith": { "contains": $currentClaim1 } }))
#end
#if( !$util.isNull($ctx.args.filter.or) )
#set( $ctx.args.filter = { "or": $authFilter } )
#else
#set( $ctx.args.filter.and[0] = { "or": $authFilter } )
#end
#end
$util.toJson({})
## [End] Enable sharing. **
Thank you for putting this up. @dgagnon
@fjnoyp is there any progress on this as a feature?
Also interested. 😓
Also interested in this feature. Seems like it only exists in Javascript: https://docs.amplify.aws/gen1/javascript/build-a-backend/graphqlapi/customize-authorization-rules/#multi-user-data-access
Description
By following the Getting started instructions and setting up the datastore Cloud Feature at this page: https://docs.amplify.aws/lib/datastore/sync/q/platform/flutter/#setup-cloud-sync
We can read this line: "DataStore's cloud synchronization uses the API category behind the scenes. Therefore, the first step is to add the API plugin."
This leads people to believing that they can use the documentation at the linked page.
In the same manner, the Setup authorization rules page (at https://docs.amplify.aws/lib/datastore/setup-auth-rules/q/platform/flutter/ ) refers a few times to the AppSync cli page (at https://docs.amplify.aws/cli/graphql/authorization-rules/#per-user--owner-based-data-access ).
On this page, a section is name "Multi-user data access". I can attest that the code in this section will not work with Datastore, I suspect due to the limitation on subscription and multi-owner models.
I tried all possible configuration (transformer v1, removing multi auth, experimental pipeline, other feature flags, as a single model, etc) and all tests were done with an empty datastore. After working 3 days on this, I am convinced there is no way to have this working in a native way.
Having a documentation that is representative of the available feature is of paramount importance to us.
Note: While this could be framed as a bug in the datastore/appsync plugins for this specific example, this is a recurring theme with these two plugins. See https://github.com/aws-amplify/amplify-flutter/issues/1566#issuecomment-1622746682 for technical details.
Categories
Steps to Reproduce
Screenshots
Relevant Logs:
Generated code:
Code to replicate:
Platforms
Flutter Version
3.10.5
Amplify Flutter Version
1.2.0
Deployment Method
Amplify CLI
Schema