From 2055e10ad56c1542c952cbb2e6cbfe78d5281f90 Mon Sep 17 00:00:00 2001 From: doubleface Date: Wed, 23 Oct 2024 09:11:32 +0200 Subject: [PATCH] feat: Check that the sourceAccountIdentifier corresponds to account name Clisk konnectors will now be allowed to have multiple accounts for the same konnector. To make it possible, the flagship app will allways check that the sourceAccountIdentifier returned by the konnector, corresponds to the accountName saved in the account. If there is a difference, it means that the worker webview is authenticated with the wrong account and it will ask to the konnector to authenticate again. If the a wrong sourceAccountIdentifier is return twice, an error is raised. --- src/libs/ReactNativeLauncher.js | 61 ++++++++++++++++++++++++--- src/libs/ReactNativeLauncher.spec.js | 62 +++++++++++++++++++++------- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/src/libs/ReactNativeLauncher.js b/src/libs/ReactNativeLauncher.js index 2b8134067..0e265b1a1 100644 --- a/src/libs/ReactNativeLauncher.js +++ b/src/libs/ReactNativeLauncher.js @@ -318,15 +318,21 @@ class ReactNativeLauncher extends Launcher { await this.pilot.call('ensureNotAuthenticated') } } - await this.pilot.call('ensureAuthenticated', { account: prevAccount }) - const userDataResult = await this.pilot.call('getUserDataFromWebsite') - if (!userDataResult?.sourceAccountIdentifier) { - throw new Error( - 'getUserDataFromWebsite did not return any sourceAccountIdentifier. Cannot continue the execution.' + try { + this.setUserData( + await this.initAndCheckSourceAccountIdentifier(prevAccount) ) + } catch (err) { + if (err.message === 'WRONG_ACCOUNT_IDENTIFIER') { + await this.pilot.call('ensureNotAuthenticated') + this.setUserData( + await this.initAndCheckSourceAccountIdentifier(prevAccount) + ) + } else { + throw err + } } - this.setUserData(userDataResult) const ensureResult = await this.ensureAccountTriggerAndLaunch() await this.createTimeoutTrigger() @@ -369,6 +375,7 @@ class ReactNativeLauncher extends Launcher { await this.stop() } catch (err) { log.error(JSON.stringify(err), 'start error') + log.error(err.message, 'start error message') const automaticDebugTraceFlag = flag('clisk.automatic.html-on-error') if (automaticDebugTraceFlag) { this.emit('SHOW_TRACE_DEBUG_VIEW', err) @@ -380,6 +387,48 @@ class ReactNativeLauncher extends Launcher { this.emit('KONNECTOR_EXECUTION_END') } + /** + * Try to run authentication phase of the konnector and check if the sourceAccountIdentifier + * is the same as the account, if any. + * @param {import('cozy-client/types/types').IOCozyAccount} account + * @returns {Object} + * @throws 'WRONG_ACCOUNT_IDENTIFIER' + */ + async initAndCheckSourceAccountIdentifier(account) { + this.log({ + namespace: 'ReactNativeLauncher', + label: 'initAndCheckSourceAccountIdentifier', + level: 'debug', + msg: 'start' + }) + await this.pilot.call('ensureAuthenticated', { account }) + + const userDataResult = await this.pilot.call('getUserDataFromWebsite') + + if (!userDataResult?.sourceAccountIdentifier) { + throw new Error( + 'getUserDataFromWebsite did not return any sourceAccountIdentifier. Cannot continue the execution.' + ) + } + if ( + account && + userDataResult.sourceAccountIdentifier !== account?.auth?.accountName + ) { + this.log({ + namespace: 'ReactNativeLauncher', + label: '_start', + level: 'error', + msg: + 'Wrong account identifier: ' + + userDataResult.sourceAccountIdentifier + + ' should be ' + + account?.auth?.accountName + }) + throw new Error('WRONG_ACCOUNT_IDENTIFIER') + } + return userDataResult + } + async fetchAndSaveDebugData(force) { const flagvalue = flag('clisk.html-on-error') if (!flagvalue && !force) { diff --git a/src/libs/ReactNativeLauncher.spec.js b/src/libs/ReactNativeLauncher.spec.js index 114a8725d..3d913cb7d 100644 --- a/src/libs/ReactNativeLauncher.spec.js +++ b/src/libs/ReactNativeLauncher.spec.js @@ -36,10 +36,13 @@ import { const fixtures = { job: { _id: 'normal_job_id' }, - account: { _id: 'normal_account_id' }, + account: { + _id: 'normal_account_id', + auth: { accountName: 'testsourceaccountidentifier' } + }, trigger: { _id: 'normal_trigger_id', - message: { folder_to_save: 'normalfolderid' } + message: { folder_to_save: 'testfolderid' } } } @@ -148,10 +151,6 @@ describe('ReactNativeLauncher', () => { state: null }) expect(client.save).toHaveBeenNthCalledWith(2, { - _id: 'normal_account_id', - auth: { accountName: 'testsourceaccountidentifier' } - }) - expect(client.save).toHaveBeenNthCalledWith(3, { _type: 'io.cozy.triggers', type: '@client', worker: 'konnector', @@ -162,7 +161,7 @@ describe('ReactNativeLauncher', () => { } }) expect(client.save).toHaveBeenNthCalledWith( - 4, + 3, expect.objectContaining({ _type: 'io.cozy.triggers', type: '@in', @@ -251,14 +250,8 @@ describe('ReactNativeLauncher', () => { } }) ) - expect(client.save).toHaveBeenNthCalledWith(1, { - _id: 'normal_account_id', - auth: { - accountName: 'testsourceaccountidentifier' - } - }) expect(client.save).toHaveBeenNthCalledWith( - 2, + 1, expect.objectContaining({ _type: 'io.cozy.triggers', type: '@in', @@ -272,7 +265,7 @@ describe('ReactNativeLauncher', () => { } }) ) - expect(client.save).toHaveBeenNthCalledWith(3, { + expect(client.save).toHaveBeenNthCalledWith(2, { _id: 'normal_job_id', attributes: { state: 'done' @@ -411,7 +404,7 @@ describe('ReactNativeLauncher', () => { }) .mockRejectedValue(new Error('test error message')) await launcher.start() - expect(client.save).toHaveBeenNthCalledWith(3, { + expect(client.save).toHaveBeenNthCalledWith(2, { _id: 'normal_job_id', attributes: { state: 'errored', @@ -564,6 +557,43 @@ describe('ReactNativeLauncher', () => { { account: fixtures.account } ) }) + it('should raise WRONG_ACCOUNT_IDENTIFIER when the user authenticates with the wrong identifiers twice', async () => { + const { launcher, client, launch } = setup() + const konnector = { + slug: 'konnectorslug', + clientSide: true, + permissions: { files: { type: 'io.cozy.files' } } + } + launcher.setStartContext({ + client, + konnector, + account: fixtures.account, + trigger: fixtures.trigger, + manifest: konnector, + launcherClient: { + setAppMetadata: () => null + } + }) + launch.mockResolvedValue({ data: fixtures.job }) + client.query.mockResolvedValue({ data: fixtures.account, included: [] }) + client.queryAll.mockResolvedValue([]) + client.save.mockImplementation(async doc => ({ + data: { ...doc, _id: doc._id ? doc._id : 'newid' } + })) + launcher.pilot.call.mockImplementation(method => { + if (method === 'getUserDataFromWebsite') { + return { sourceAccountIdentifier: 'wrongtestsourceaccountidentifier' } + } else { + return true + } + }) + launcher.stop = jest.fn() + await launcher.start() + expect(launcher.pilot.call).toHaveBeenCalledWith('ensureNotAuthenticated') + expect(launcher.stop).toHaveBeenCalledWith({ + message: 'WRONG_ACCOUNT_IDENTIFIER' + }) + }) }) describe('runInWorker', () => { it('should resolve with method result', async () => {