-
Notifications
You must be signed in to change notification settings - Fork 200
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
feat(datastore): StartSync with Auth #471
Conversation
31f3888
to
e541a79
Compare
@@ -82,7 +88,7 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin { | |||
let filter = HubFilters.forEventName(HubPayload.EventName.Amplify.configured) | |||
var token: UnsubscribeToken? | |||
token = Amplify.Hub.listen(to: .dataStore, isIncluded: filter) { _ in | |||
if self.hasValidAPIPlugin() { | |||
if self.hasRequiredPlugins() { | |||
self.storageEngine.startSync() | |||
} else { | |||
self.log.info("Unable to find suitable plugin for syncEngine. syncEngine will not be started") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could be more informative here. Since the sync engine could not start for different reasons it would be cool if we provided a more detailed message of the actual reason it didn't start (and maybe use log.warn
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i consolidated this logic and the logic from RemoteSyncEngine over to StorageEngine and added more logging. I have some warn
and some error
as well now. the initial check (in the new code) checks for APIPlugin and that is an info
because this is to account for developers using local store first without API. all other logging are warn
afterwards where there is API but something else is unexpected
} | ||
|
||
private func checkIfAuthenticationRequired() { | ||
log.debug(#function) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what will this print? We could have a more informative message here, so it's easier to search in the logs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i believe checkIfAuthenticationRequired()
. added more to all
log.debug("\(#function) <message>")
AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/RemoteSyncEngine.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/RemoteSyncEngine.swift
Outdated
Show resolved
Hide resolved
...toreCategoryPluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift
Outdated
Show resolved
Hide resolved
AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/RemoteSyncEngine.swift
Outdated
Show resolved
Hide resolved
var isAuthEnabled: Bool { | ||
return !authRules.isEmpty | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious as to what isAuthEnabled
refers to. Is it simply to say that the models use an @auth
directive? If so, then using "Enabled" might be misleading. Perhaps a better name would be isAuth
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting feedback, maybe hasAuth
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i was thinking it means these "models are enabled with auth rules for finer grain control", which means that they require auth, or an authenticated signed request when syncing to the cloud. By having auth directive on the model, they can control how the service checks for access, whether that is checking the token or signed request.
from the two isAuth
and hasAuth
, i think isAuth makes me think of "the model is authenticated" which doesn't make a lot of sense, maybe "hasAuth" is "hasAuthentication" or "hasAuthenticationRules" which is closer to the check !authRules.isEmpty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hasAuthenticationRules
lgtm, I in favor of verbosity to communicate clear intent
if !requireAuthPlugin() { | ||
return true | ||
} | ||
|
||
if requireAuthPlugin() && hasValidAuthPlugin() { | ||
return true | ||
} | ||
|
||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like this could be combined:
let requireAuthPlugin = requireAuthPlugin()
return !requireAuthPlugin || (requireAuthPlugin && hasValidAuthPlugin())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, thanks! i ended up adding a bunch of guard statements to expand the checks and add logging in between
case (.clearingStateOutgoingMutations, .clearedStateOutgoingMutations): | ||
return .checkIfAuthenticationRequired | ||
case (.checkIfAuthenticationRequired, .requireAuthenticatedUser): | ||
return .waitForAuthenticatedUser | ||
case (.waitForAuthenticatedUser, .authenticatedUser(let api, | ||
let storageEngineAdapter, | ||
let userId)): | ||
return .initializingSubscriptions(api, storageEngineAdapter, userId) | ||
case (.checkIfAuthenticationRequired, .authenticatedUser(let api, | ||
let storageEngineAdapter, | ||
let userId)): | ||
return .initializingSubscriptions(api, storageEngineAdapter, userId) | ||
case (.checkIfAuthenticationRequired, .authenticationNotRequired(let api, let storageEngineAdapter)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we've adopted a strategy where we handle listening to auth state within the sync engine. Did we consider another approach like listening to auth state in StorageEngine and determine whether or not we should start/stop the sync engine from there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Been chatting with @elorzafe regarding the auth strategy and we agree that DataStore should listen to auth state changes in general
JS current requires the developers to listen to auth state changes themselves:
@elorzafe, please correct me if i'm wrong here:
If sign-in event: no public facing API to start the sync engine, but can call DataStore.clear()
which re-iniitializes the syncEngine as a secondary behavior
if sign out event: call DataStore.clear() to clear local data
JS DataStore
JS customers have to make a call to DataStore APIs like DataStore.query(). this wll try to
- start to start the sync engine (fail if not authenticated, fall back to IAM credentials for sigv4 signing, different architecture from iOS intercategory plugin communication - IOS cannot fall back to IAM, but utimately there is an eventual fail case).
- assuming this is async, the data returned to the customer may not be up to date since sync engine's reconcilation path may not have caught up.
The most efficient time to start the sync engine is by listening to auth sign in event. if all customers which use auth enabled models require the user to be signed in before making API calls (subscriptions and sync query) then DataStore should hold this listening logic. it should be waiting for Auth events like sign-in and sign-out.
where to put auth state listener
The different places this auth state check can live:
- At the very top level, DataStorePlugin will wait for DataStore
configure
event, check for API plugin before starting the sync engine. then check if there are required API and optionally Auth plugin. this can be expanded to add the auth state change listener here - As you mentioned, in the StorageEngine, this looks like a possible candidate and be a good place when we address the auth state listener for sign-out because the
clear()
API is there. (we may then refactor the auth state lisnter code for sign-in event to the same place) - where it is currently in syncEngine: this allows the sync engine to do some of the non-auth related house keeping work that can be done without the API calls (subscription and sync query). leaving the wait for auth state change in the most optimal spot. and SyncEngine already exists a state machine like
schedulingRestart
anderrored
which will allow us to easily add "expoential retry if user is not signed in, then give up and listen for auth state change"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- refactored code to put it in StorageEngine
- also added the signOut listener
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed auth listener code (removed sign in/sign out listener code) for less impactful change
f19d4e4
to
7d8450d
Compare
d920c48
to
d46e2c1
Compare
0302789
to
4cd55d7
Compare
dcf4814
to
7b406e0
Compare
subscriptionType: subscriptionType) | ||
let request: GraphQLRequest<Payload> | ||
if let auth = auth, modelType.schema.hasAuthenticationRules, let user = auth.getCurrentUser() { | ||
// TODO: check model schema for identityClaim to figure out which is the ownerId field coming from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we should open an issue around this (which contains the impact for not doing this work), and then update the comment with a link to the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good idea, adding issue #485, impact, and linked in comment
...CategoryPluginAuthIntegrationTests/AWSDataStoreCategoryPluginAuthIntegrationTests+Auth.swift
Show resolved
Hide resolved
XCTFail("Failed to get auth session \(error)") | ||
} | ||
}) | ||
wait(for: [retrieveUserSubCompleted], timeout: TestCommonConstants.networkTimeout) | ||
guard let result = resultOptional else { | ||
fatalError("Could not get userSub for user") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We seem to use a mix of XCTFail and fatalError throughout this file. Is there any reason for using one over the other? If not, i think it makes sense to just use XCTFail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reason for fatalError is because when using XCTFail, the guard statement still requires a return. although the return will not be used because the test will fail on XCTFail first, it's confusing to have to code a return statement that returns something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i've updated to XCTFail because in these cases, the return type is just String or Bool which makes it easy to return "" and false
AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginAuthIntegrationTests/README.md
Outdated
Show resolved
Hide resolved
AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginAuthIntegrationTests/README.md
Outdated
Show resolved
Hide resolved
? Do you want to generate code for your newly created GraphQL API `No` | ||
``` | ||
|
||
5. Copy `amplifyconfiguration.json` over as `AWSDataStoreCategoryPluginAuthIntegrationTests-amplifyconfiguration.json` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you saying to change the file name from amplifyconfiguration.json to AWSDataStoreCategoryPluginAuthIntegrationTests-amplifyconfiguration.json ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, currently they're added to the HostApp's bundle. If we have multiple integration test targets, we need to prefix the configuration so each target reads their own configuration file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, we can word this to:
Copy amplifyconfiguration.json
to a new file named AWSDataStoreCategoryPluginAuthIntegrationTests-amplifyconfiguration.json
if isSignedIn() { | ||
signOut() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any reason for this... because we are calling tearDown() which has signOut and reset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this was to prevent previous test executions failures so tearDown isn't called, or tearDown() is called but failed for some reason. i think this is applicable for a single test run as well since one test method may fail while the next one continues
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm.. if teardown isnt' being called on a single test run, then should we just add signout to each one of the integration tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
teardown should be called on a single test method run. i was referring to more of a scenario where test is stopped, say i manually terminate it by pressing Stop, then tearDown is never run
Issue #, if available:
Description of changes:
This adds support for auth enabled models.
tryStartSync
that does much more: checks if auth is required, check if signed in, if not then wait for sign in. if signed in, start waiting for signed outremoteSyncEngine.start()
clear()
. Added newclearCompleted
event for tests to listen onMisc updates
ProcessMutationErrorFromCloudOperation
Acceptance tests
testUnauthenticatedSavesToLocalStoreIsReconciledWithCloudStoreAfterAuthentication
testOwnerCreatedDataCanBeReadByOtherUsersForReadableModel
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.