Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

GraphQL real time subscriptions with shared ownership through auth rules #4794

Closed
abdielou opened this issue Jul 10, 2020 · 9 comments
Closed
Assignees
Labels
@auth Issues tied to @auth directive graphql-transformer-v1 Issue related to GraphQL Transformer v1 question General question

Comments

@abdielou
Copy link

abdielou commented Jul 10, 2020

GraphQL real time subscriptions with shared ownership through auth rules

Which Category is your question related to?
GraphQL Schema @auth configuration

Amplify CLI Version
4.23.0

What AWS Services are you utilizing?
AWS AppSync (api), Amazon Cognito (auth), Amazon DynamoDB (storage)

Provide additional details e.g. code snippets
I am trying to share access to a Model with multiple users. I'm doing so through multiple auth rules where the owner is identified by the field owner and other users are given access through ownerField. Here's my schema:

type Channel
  @model
  @auth(rules: [
    { allow: owner },
    { allow: owner, ownerField: "members", operations: [read] }
  ])
{
  id: ID!
  owner: String
  members: [String]
}

The problem is the following, I have no idea how to listen for onCreateChannel events for member users. As per documentation:

...the user will only get notified of updates to records for which they are the owner.

The only alternative I found was through Static group authorization. As per docs, when using static group auth:

If you don’t pass the user in, but are a member of an allowed group, the subscription will notify you of records added.

So for example I could enable read for everyone in the User group/role. But that's obviously not desired.

What would you recommend? Again and to summarize. I have a simple Channel table where the creator has CRUDL and members can only read.

@jhockett jhockett added @auth Issues tied to @auth directive graphql-transformer-v1 Issue related to GraphQL Transformer v1 question General question labels Jul 10, 2020
@SwaySway
Copy link
Contributor

@abdielou
Is members a list type in the jwt? If so are you looking to allow onCreateChannel for all members?

@SwaySway SwaySway added the pending-response Issue is pending response from the issue author label Jul 14, 2020
@vlac666
Copy link

vlac666 commented Jul 14, 2020

I am having exactly the same problem, using the example from the CLI Directives examples.

type Draft @model
  @auth(rules: [
    # Defaults to use the "owner" field.
    { allow: owner },
    # Authorize the update mutation and both queries.
    { allow: owner, ownerField: "editors", operations: [update, read] }
  ]) {
  id: ID!
  title: String!
  content: String
  owner: String
  editors: [String]
}

Test 1: create mutation with no explicit owner should populate owner and editor field with username
Expected result (according to docs) - should populate editors array and owner.
Failed - Only populates owner.
Solution - The documentation should have "create" in the list of editor operations
{ allow: owner, ownerField: "editors", operations: [create, update, read] }
Retest: Pass

Test 2 owner can read Test 1 record
Result: Pass

Test 3 "otheruser" can't read test 1 record
Result: Pass - does not appear in list result

Test 4 Update Test 1 record so editors contains ["usename", "otheruser"] should mean both users can read it
Result: Pass

Test 5 "Username" user gets subscription notifications when subscribing with input:{user: "username", editors:"username"}
Result: Pass

Test 6 "otheruser" gets notifications input:{user: "otheruser", editors:"otheruser"}
Result: FAIL - no notifications
if you try to set user to any other name you get something similar to an authentication error

I tried simplifying the auth rules to make "owner" an array, in the hope that both owners would get notifications. No luck. They could both list and edit but neither got notifications

  @auth(rules: [    { allow: owner },  ]) {
  owner: [String!]!

At the moment it looks like "owner" has to be a Single string, so only one owner gets notified - unless you can use static groups

@abdielou
Copy link
Author

abdielou commented Jul 14, 2020

Is members a list type in the jwt?

I'm not sure I follow. What do you mean by "in the JWT"? I am not handling the JWT, as this is handled by the autogenerated code. I suppose you are refering to how I configure the @auth directive. I presume this is how one would affect the composition of the JWT. The following is my code:

  @auth(rules: [
    { allow: owner },
    { allow: owner, ownerField: "members", operations: [read] }
  ])

If so are you looking to allow onCreateChannel for all members?

@SwaySway Yes that's what I'm trying to accomplish.

@abdielou
Copy link
Author

abdielou commented Jul 29, 2020

WARNING: This is a hackaround

Finally accomplished what I wanted.

https://github.com/abdielou/nextjs-amplify-members-example

Anyone, please take a look and give me your thoughts?

Workflow:

  • Suppose there are 3 users, Mary, Bob, and Tom (M, B, T).
  • The Schema has 3 tables: Channel, Membership, and MembershipNotification
  • All users, MBT, subscribe to onCreatedMembershipNotification
  • Mary creates a new Membership with members MBT.
  • Mary creates a new Channel with the previously created Membership
  • Mary creates (for each member BT) a new MembershipNotification including the Membership ID and the owner is set as B or T.
  • Everyone gets notified through the MembershipNotification.
  • After the notification, members fetch Channels by Membership ID.
  • Members subscribe to further Channel updates by Channel ID.
  • Everything is protected through @auth

I'll try to explain this in more detail later. 😪

@abdielou
Copy link
Author

abdielou commented Aug 7, 2020

Just an update... I was able to modify the DataStore and enabled syncing with my initial use-case.

This is what I needed, a Channel table where ownership was shared across members, and that members would get notified of channelCreate events.

This is how I roughly did it:

  1. Implemented membership through 1-1 relationship between Channel and Membership tables, where membership stored members array.
  2. Given a membershipID, I was able to implement a custom resolver to validate authorization of members when trying to subscribe to Channel events.
  3. Members are notified of membership through subscription to a third table Notification where membershipID is included.
  4. Member clients subscribe to Channel events using membershipID.
  5. Membership and Notification tables are protected through @auth.

The architecture above works for GQL only. The DataStore builds its own Subscriptions at startup, therefore it's impossible to sync through the custom subscriptions required by the previous architecture.

I modified (still working on cleaning it up) the DataStore to allow custom data sync events, in other words, an Observable is sent through Amplify.DataStore configs, which serves as a bridge between custom Subscriptions on the client and the internal SyncEngine. Events are sent into the SyncEngine for local syncing with the local indexed db.

It works! The DataStore is used normally, and custom subscriptions maintain new objects.

I'm still cleaning up and improving the Channel table security as I am only securing subscriptions. Once I'm happier with the code I'll try to pass it around for thoughts.

NOTE: Again, this is a hackaround, exploring possible longer-term solutions.

@pedramp20
Copy link

pedramp20 commented Aug 10, 2020

This is a limitation of appsync where it expects the exact list with the exact order, which is not possible in this case, as the auto generated subscriptions only accept a string. You can read more about it in this thread and I am not sure, if the appsync/amplify team has any ETA on implementing such a feature

https://stackoverflow.com/questions/56822317/appsync-subscribe-to-element-in-array

Considering that this would not be delivered anytime soon, I tried to use another field as the input of the subscription, lets say in the provided directive example:

type Draft @model
  @auth(rules: [
    # Defaults to use the "owner" field.
    { allow: owner },
    # Authorize the update mutation and both queries.
    { allow: owner, ownerField: "editors", operations: [update, read] }
  ]) {
  id: ID!
  title: String!
  content: String
  owner: String
  editors: [String]
  newField: String!
}

a new subscription is defined as follows:

type Subscription {
  onUpdateDraft(newField: String): Draft @aws_subscribe(mutations: ["updateDraft "])
}

Please note that the newField parameter is optional.

This subscription would be fired on update, however, the received response has an empty data inside value while the provider is populated properly.

Any idea why this is happening?

@pedramp20
Copy link

pedramp20 commented Aug 10, 2020

I realised that this is another bug, where amplify does not work according to the documentation provided here:

https://docs.aws.amazon.com/appsync/latest/devguide/real-time-data.html

Subscriptions dont work with optional arguments, I changed it to a required argument and it is working. Just to explain what I have in mind is another table which is responsible for mapping subscriptions to subscribers and vice versa. It returns a subscription key, which is this newly added field, where subscribers can subscribe to.

@abdielou
Copy link
Author

abdielou commented Aug 20, 2020

I'm going to close this. The original post was regarding how to enable members to listen onCreateChannel events. Not possible. This is a limitation of the Subscription. You need to find a way to get clients notified, SNS, a dedicated Notifications model, or any other PubSub where clients can subscribe. The events could be sent by the clients specifying the members or through post functions.

I have implemented a workaround involving custom resolvers and a Notifications table to get the events through GraphQL. All that works if you are just using GQL. If you want to use the DataStore... the story is a bit different because you will not be able to sync. The DataStore only allows for single ownership, so your subscriptions will fail.

This is what I did:

  1. A Notification table with single owner
  2. Custom resolvers for Notification to allow 2 owners, owner and notifier
  3. The same Channel schema you see on the original issue.
  4. Removed Notification model from the json schema used by the DataStore (the one generated by codegen). I don't want this table synced locally. It will cause other issues.
  5. All DataStore subscriptions will fail because only Channel.owner will be sent, but members is also required.
  6. To get subscription events, I manually subscribe (GQL, not DataStore) to Notification by owner.
  7. Now that I have a proxy for Channel events, I send those events directly into the DataStore SyncEngine through a custom observable.
  8. This custom observable is configured when the DataStore is initialized. (Keep in mind this is all my own custom mods to the DataStore)

So yeah, original issue is not possible. In the future I'm replacing the Notifications table with a PubSub like SNS.

seanim0920 added a commit to tifapp/FitnessProject that referenced this issue Apr 12, 2021
…d of an id array in the conversation schema. That didn't work

Next we tried restricting the private operations, and adding a new owner field to the conversation schema. That still didn't work
Next we tried editing the resolvers for the onupdate and oncreate events, and that still didn't work. Here I realized that the onnewmessage and onnewconversation would need their own resolvers, but none were generated. I checked out how to do this by following this aws-amplify/amplify-cli#4794
Then I realized the onupdate subscription could accept a string, I thought it would be used to check the users array, and tried supplying the user's id when creating the event listener, but that didn't work.
also realized we'd need a second event listener for conversations being created on top of being updated.
playing with giving different permissions in the schema didn't work.
also realized that the subscription for new friend requests has no authorization and probably isn't secure. I'm guessing including different ids into the receiver field would listen for new friend requests with those receivers. instead we'll add auth directives and modify the resolvers directly.
@github-actions
Copy link

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

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
@auth Issues tied to @auth directive graphql-transformer-v1 Issue related to GraphQL Transformer v1 question General question
Projects
None yet
Development

No branches or pull requests

6 participants