aws-amplify / amplify-swift

A declarative library for application development using cloud services.
Apache License 2.0
442 stars 192 forks source link

Using @auth on graphQL Field leads to DataStore AppSync Failure #1749

Open Etep15 opened 2 years ago

Etep15 commented 2 years ago

Describe the bug

In my graphQL schema I have a field with an auth tag on it that only allows the owner of that model entry to access it..

type Account @model @auth(rules: [{allow: owner}, {allow: private, operations: [read]}]) {
  id: ID!
  email: String @auth(rules: [{allow: owner}])
  givenName: String
  familyName: String
  owner: String
}

If DynamoDB contains data for this model it should normally sync down to the client. But if I delete the app on my device and then reinstall it (effectively clearing out the Datastore local storage) the datastore/appsync will fail. So none of the data will come down. What should happen is the data should sync down with empty or nil values for those it cannot access.

Originally I thought this was an issue because I had not logged in, and indeed it initially was. However after logging in, the app needs to check if the Account exists in DataStore, which in this case it doesn't as it only exists up in DynamoDB. Therefore it tries to create it which then leads to duplicates in DynamoDB which is also what we don't want.

What should happen is that the query for the Account given a predicate for the owner equaling the signed in id should cause Datastore and Appsync to pull down the account and then return it to the query. Instead it's given an empty result. Here's the query :

    let account = Account.keys
    let predicate = account.owner == Amplify.Auth.getCurrentUser()!.username
            _ = Amplify.DataStore.query(Account.self, where: predicate)
        .sink {

        if case let .failure(error) = $0 {
            print("Could not get existing \(modelType) for id \(predicate)")
            self.handleError(error: error, failure: failure);
        }
    }
    receiveValue: { objects in

        if (objects.isEmpty)
        {
            failure (NSError.init(domain: kESErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey:"No objects found for id \(predicate)"]))
        }
        else if (objects.count > 1)
        {
            failure (NSError.init(domain: kESErrorDomain, code: -1, userInfo: [NSLocalizedDescriptionKey:"More than one object (\(objects.count)) found for id \(predicate). THIS SHOULD NEVER HAPPEN!"]))
        }
        else
        {
            let existingObject = objects.first!// as! Account
            success (existingObject)
        }
    }

if (objects.isEmpty) is true in this scenario which it should not be since that Account does indeed exist in DynamoDB. So I feel we have an asynchronous race condition happening. Datastore cannot pull the data from DynamoDB because the user hasn't logged in which is fine. But once the user logs in and then checks if the account exists, Appsync doesn't have enough time to pull the data before the query is executed on the local store and therefore returns an empty result.

This may be how I'm using the system so if there is a way around this I would welcome the insight.

Steps To Reproduce

1. Create a model for Account specific information with one of the fields having an @auth rule that allows owner access only. Account specific is key because this will lead to the race condition
2. Create an entry for that model in Datastore using Amplify.Datastore.save
3. Ensure entry is stored into DynamoDB
4. Clear Datastore or delete app and reinstall
5. Open the app and you should see lots of syncing issues scrolling through on the log output as AppSync / Datastore attempt to sync the data from DynamoDB. This is normal (however a bit concerning that it never stops)
6. Sign in with the webui signin (federated doesn't appear to work right now. See #1728)
7. After successful signin the app should then query to see if this account exists based on the owner field
8. This leads to the race condition.

Expected behavior

There needs to be a way to sync the data and get a completion handler or some sort of notification that syncing has completed after a sign in.

Amplify Framework Version

1.16.1

Amplify Categories

API, Auth, DataStore

Dependency manager

Cocoapods

Swift version

5.0

CLI version

7.6.26

Xcode version

13.3.1

Relevant log output

No response

Is this a regression?

No

Regression additional context

No response

Device

iPhone 12 Pro

iOS Version

iOS 15

Specific to simulators

No response

Additional context

No response

Etep15 commented 2 years ago

The more I dig into this the more it seems that Datastore / Appsync doesn't support field @auth tags.

I've tested a scenario where I've logged in but don't do anything else (no queries etc). I can see in the log output that DataStore/Appsync (sorry, I'm not sure which service is doing what here) is constantly retrying to sync with the cloud and pull down the data but keeps getting an error because of the @auth on the email field. So instead of ignoring that field it just completely errors out and shuts down syncing for a few seconds and then tries again. Constantly. Which could lead to a costly issue. I realize this is a graphQL restriction but I don't have access to the query itself that Datastore/AppSync uses so I can't tell it to omit the email tag for the general sync.

Here's the relevant log output...

2022-04-15 14:15:30.297010-0600[23617:4924260] [RemoteSyncEngine] respond(to:): schedulingRestart(DataStoreError: One or more errors occurred syncing models. See below for detailed error description.
Recovery suggestion: DataStoreError: An error occurred syncing Account
Caused by:
DataStoreError: GraphQL service returned a partially-successful response containing errors: [Amplify.GraphQLError(message: "Not Authorized to access email on type String", locations: Optional([Amplify.GraphQLError.Location(line: 6, column: 7)]), path: Optional([Amplify.JSONValue.string("syncAccounts"), Amplify.JSONValue.string("items"), Amplify.JSONValue.number(0.0), Amplify.JSONValue.string("email")]), extensions: Optional(["data": Amplify.JSONValue.null, "errorInfo": Amplify.JSONValue.null, "errorType": Amplify.JSONValue.string("Unauthorized")]))]
Recovery suggestion: The list of `GraphQLError` contains service-specific messages.
Caused by:
GraphQLResponseError<PaginatedList<AnyModel>>: GraphQL service returned a partially-successful response containing errors: [Amplify.GraphQLError(message: "Not Authorized to access email on type String", locations: Optional([Amplify.GraphQLError.Location(line: 6, column: 7)]), path: Optional([Amplify.JSONValue.string("syncAccounts"), Amplify.JSONValue.string("items"), Amplify.JSONValue.number(0.0), Amplify.JSONValue.string("email")]), extensions: Optional(["data": Amplify.JSONValue.null, "errorInfo": Amplify.JSONValue.null, "errorType": Amplify.JSONValue.string("Unauthorized")]))]
Recovery suggestion: The list of `GraphQLError` contains service-specific messages.)
2022-04-15 14:15:30.297264-0600[23617:4924260] [RemoteSyncEngine] scheduleRestart(advice:) scheduling retry for restarting remote sync engine
2022-04-15 14:15:30.297345-0600[23617:4924260] [MutationRetryNotifier] receive(subscription:)
2022-04-15 14:15:30.299869-0600[23617:4924260] [StarscreamAdapter] socket.write - {"id":"9307F1BD-0452-4134-AFC4-CB5E70004C2C","type":"stop"}
2022-04-15 14:15:30.308671-0600[23617:4924255] [StarscreamAdapter] socket.write - {"id":"68BE8EDE-8B49-44F7-863A-C256086801C4","type":"stop"}
2022-04-15 14:15:30.315222-0600[23617:4924260] [StarscreamAdapter] socket.write - {"id":"104359BE-C8D9-4EC8-8802-9373BA9BEC9A","type":"stop"}
2022-04-15 14:15:30.321626-0600[23617:4924260] [StarscreamAdapter] socket.write - {"id":"17728271-E401-4243-9F02-9E42302C12C5","type":"stop"}
2022-04-15 14:15:30.326975-0600[23617:4924271] [StarscreamAdapter] socket.write - {"id":"E0494F0D-2379-4B2C-91C3-676A882B94CB","type":"stop"}
2022-04-15 14:15:30.332052-0600[23617:4924260] [StarscreamAdapter] socket.write - {"id":"6C9C4138-1F58-4B3F-9467-5602C8177901","type":"stop"}
2022-04-15 14:15:30.336915-0600[23617:4924262] [StarscreamAdapter] socket.write - {"id":"6DEEECB2-6626-4ECB-869E-B1FBCC9B3D61","type":"stop"}
2022-04-15 14:15:30.341424-0600[23617:4924260] [StarscreamAdapter] socket.write - {"id":"C87BA3E7-90F1-42BE-BF53-FEC5FD29BE63","type":"stop"}
2022-04-15 14:15:30.343261-0600[23617:4924271] [StarscreamAdapter] websocketDidReceiveMessage: - {"id":"9307F1BD-0452-4134-AFC4-CB5E70004C2C","type":"complete"}
2022-04-15 14:15:30.343312-0600[23617:4924271] [RealtimeConnectionProvider] Resetting stale connection timer
2022-04-15 14:15:30.345894-0600[23617:4924262] [StarscreamAdapter] socket.write - {"id":"C4812A2B-ECFA-4F1F-87F6-3E57D50F15C6","type":"stop"}
2022-04-15 14:15:30.361122-0600[23617:4924255] [StarscreamAdapter] socket.write - {"id":"81143739-13FC-4EFA-9895-43CFAC406150","type":"stop"}
2022-04-15 14:15:30.367389-0600[23617:4924255] [StarscreamAdapter] socket.write - {"id":"51F13250-5978-4994-9369-C63AC8AF93B9","type":"stop"}
2022-04-15 14:15:30.371457-0600[23617:4924260] [StarscreamAdapter] socket.write - {"id":"233C8252-C1B8-4BF1-83BA-B134CE8FCB75","type":"stop"}
2022-04-15 14:15:30.375222-0600[23617:4924255] [StarscreamAdapter] socket.write - {"id":"21F02E6C-1F63-41C6-B212-39395F0DF1C7","type":"stop"}
2022-04-15 14:15:30.378637-0600[23617:4924262] [StarscreamAdapter] socket.write - {"id":"3A867F66-DB03-4145-9A9B-9C3C24D8FA08","type":"stop"}
2022-04-15 14:15:30.379861-0600[23617:4924255] [StarscreamAdapter] websocketDidReceiveMessage: - {"id":"E0494F0D-2379-4B2C-91C3-676A882B94CB","type":"complete"}
2022-04-15 14:15:30.379892-0600[23617:4924255] [RealtimeConnectionProvider] Resetting stale connection timer
2022-04-15 14:15:30.382056-0600[23617:4924261] [RealtimeConnectionProvider] all subscriptions removed, disconnecting websocket connection.
2022-04-15 14:15:30.382076-0600[23617:4924271] [StarscreamAdapter] socket.write - {"id":"2678DB42-8ABC-4DC9-92A9-23CC8D010EE6","type":"stop"}
2022-04-15 14:15:30.382095-0600[23617:4924271] [StarscreamAdapter] socket.disconnect
diegocstn commented 2 years ago

hi @Etep15, unfortunately per-field @auth rules are not currently supported in DataStore. I'll mark this as a feature request and we'll update this ticket.

peaugust commented 1 year ago

There's any workaround for this issue? I'm facing the same situation on my project, and I cannot change my schema

lawmicha commented 1 year ago

I believe currently per field level auth rules are currently not supported in DataStore. @peaugust would you be able to turn off conflict resolution on your AppSync backend and use Amplify.API instead?

peaugust commented 1 year ago

@lawmicha after talking to my team, we've decided to remove the @auth per field, and the issue is gone.