diff --git a/apis/stardust/api-spec/listbox-spec.json b/apis/stardust/api-spec/listbox-spec.json index 6b1ffd595..54a8264f5 100644 --- a/apis/stardust/api-spec/listbox-spec.json +++ b/apis/stardust/api-spec/listbox-spec.json @@ -189,4 +189,4 @@ } } } -} \ No newline at end of file +} diff --git a/commands/serve/web/__tests__/connect.test.js b/commands/serve/web/__tests__/connect.test.js index 2c9f593d3..5701a4a59 100644 --- a/commands/serve/web/__tests__/connect.test.js +++ b/commands/serve/web/__tests__/connect.test.js @@ -3,6 +3,9 @@ import * as ENIGMA from 'enigma.js'; import qixSchema from 'enigma.js/schemas/12.2015.0.json'; import * as SenseUtilities from 'enigma.js/sense-utilities'; import { connect, openApp, getConnectionInfo, getParams, parseEngineURL } from '../connect'; +import * as getCsrfToken from '../utils/getCsrfToken'; + +jest.mock('../utils/getCsrfToken', () => jest.fn()); jest.mock('@qlik/sdk'); jest.mock('enigma.js'); @@ -45,6 +48,7 @@ describe('connect.js', () => { generateWebsocketUrl: generateWebsocketUrlMock, })); windowFetchSpy = jest.spyOn(window, 'fetch'); + getCsrfToken.mockResolvedValue('A-CSRF-TOKEN'); }); afterEach(() => { @@ -119,6 +123,9 @@ describe('connect.js', () => { enigma: { secure: false, host: 'localhost:1234/app/engineData', + urlParams: { + 'qlik-csrf-token': 'A-CSRF-TOKEN', + }, }, }; jsonResponseMock.mockImplementation(() => Promise.resolve(connectionResponse)); diff --git a/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx b/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx index bbbd1161a..b113bcab0 100644 --- a/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx +++ b/commands/serve/web/components/Hub/SelectEngine/ConnectionGuid.jsx @@ -59,7 +59,7 @@ const ConnectionGuid = ({ showGuid }) => (
- Qlik Sense Enterprise on Windows + Qlik Sense on Windows WebSocket URL format: wss://<sense-host.com>/<virtual-proxy-prefix> @@ -68,9 +68,8 @@ const ConnectionGuid = ({ showGuid }) => (

Note that for the Qlik Sense Proxy to allow sessions from this webpage, - {window.location.host} needs to be whitelisted in QMC in your Qlik Sense Enterprise on Windows - deployment. In addition, you need to enable Has secure attribute and set SameSite attribute to{' '} - None. + {window.location.host} needs to be whitelisted in QMC in your Qlik Sense on Windows deployment. In + addition, you need to enable Has secure attribute and set SameSite attribute to None.
Make sure you are logged in to Qlik Sense in another browser tab.
diff --git a/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx b/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx index 46f231c78..1010a5207 100644 --- a/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx +++ b/commands/serve/web/components/Hub/SelectEngine/__tests__/ConnectionGuid.test.jsx @@ -11,7 +11,7 @@ describe('', () => { 'Web integration id format:', 'OAuth Client ID URL format:', 'Qlik Cloud Services', - 'Qlik Sense Enterprise on Windows', + 'Qlik Sense on Windows', 'Qlik Sense Desktop', ].map((title) => { expect(screen.queryByText(title)).toBeInTheDocument(); diff --git a/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx b/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx index d42d8c144..0d1e7d05c 100644 --- a/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx +++ b/commands/serve/web/components/Hub/SelectEngine/__tests__/SelectEngine.test.jsx @@ -24,7 +24,7 @@ describe('', () => { 'Web integration id format:', 'OAuth Client ID URL format:', 'Qlik Cloud Services', - 'Qlik Sense Enterprise on Windows', + 'Qlik Sense on Windows', 'Qlik Sense Desktop', ].map((title) => { expect(screen.queryByText(title)).toBeVisible(); @@ -43,7 +43,7 @@ describe('', () => { 'Web integration id format:', 'OAuth Client ID URL format:', 'Qlik Cloud Services', - 'Qlik Sense Enterprise on Windows', + 'Qlik Sense on Windows', 'Qlik Sense Desktop', ].map((title) => { expect(screen.queryByText(title)).not.toBeVisible(); diff --git a/commands/serve/web/connect.js b/commands/serve/web/connect.js index 20e042e97..9a0ea6ea2 100644 --- a/commands/serve/web/connect.js +++ b/commands/serve/web/connect.js @@ -2,6 +2,7 @@ import enigma from 'enigma.js'; import qixSchema from 'enigma.js/schemas/12.2015.0.json'; import SenseUtilities from 'enigma.js/sense-utilities'; import { Auth, AuthType } from '@qlik/sdk'; +import getCsrfToken from './utils/getCsrfToken'; const getParams = () => { const opts = {}; @@ -167,9 +168,11 @@ const connect = async () => { }; } + const csrfToken = await getCsrfToken(`https://${enigmaInfo.host}/${enigmaInfo.prefix}`); const url = SenseUtilities.buildUrl({ secure: false, ...enigmaInfo, + ...{ urlParams: { 'qlik-csrf-token': csrfToken } }, }); return enigma.create({ schema: qixSchema, url }).open(); diff --git a/commands/serve/web/hooks/__tests__/useConnection.test.js b/commands/serve/web/hooks/__tests__/useConnection.test.js index 682785c1d..56f65da52 100644 --- a/commands/serve/web/hooks/__tests__/useConnection.test.js +++ b/commands/serve/web/hooks/__tests__/useConnection.test.js @@ -7,6 +7,9 @@ import { } from '../useConnection'; import * as connectModule from '../../connect'; import { RouterWrapper } from '../../utils'; +import getCsrfToken from '../../utils/getCsrfToken'; + +jest.mock('../../utils/getCsrfToken', () => jest.fn()); describe('useConnection Module', () => { let connectMock; @@ -29,6 +32,7 @@ describe('useConnection Module', () => { connectMock = jest.fn().mockResolvedValue(glob); jest.spyOn(connectModule, 'connect').mockImplementation(connectMock); + getCsrfToken.mockResolvedValue('A-CSRF-TOKEN'); }); afterEach(() => { @@ -213,7 +217,9 @@ describe('useConnection Module', () => { expect(setError).toHaveBeenCalledTimes(1); expect(setError).toHaveBeenCalledWith({ hints: [ - 'If you are connecting to Qlik Cloud Services, make sure to provide a web integration id or client id.', + '- If you are connecting to Qlik Cloud, make sure to provide a web integration id or client id.', + '- For Qlik Sense on Windows, make sure the proxy setup is correct and that you are authenticated.', + '- Press the ? in the top right for more information on how to set up the connection correctly.', ], message: 'Connection failed to wss://some.remote.sde.qlikdev.com', }); diff --git a/commands/serve/web/hooks/__tests__/useOpenApp.test.js b/commands/serve/web/hooks/__tests__/useOpenApp.test.js index 965a5955a..26bca959b 100644 --- a/commands/serve/web/hooks/__tests__/useOpenApp.test.js +++ b/commands/serve/web/hooks/__tests__/useOpenApp.test.js @@ -4,9 +4,11 @@ import * as SenseUtilities from 'enigma.js/sense-utilities'; import qixSchema from 'enigma.js/schemas/12.2015.0.json'; import { useOpenApp } from '../useOpenApp'; import * as getAuthInstanceModule from '../../connect'; +import * as getCsrfToken from '../../utils/getCsrfToken'; jest.mock('enigma.js'); jest.mock('enigma.js/sense-utilities'); +jest.mock('../../utils/getCsrfToken', () => jest.fn()); describe('useOpenApp()', () => { let renderResult; @@ -56,6 +58,8 @@ describe('useOpenApp()', () => { }); jest.spyOn(getAuthInstanceModule, 'getAuthInstance').mockImplementation(getAuthInstanceMock); windowFetchSpy = jest.spyOn(window, 'fetch'); + + getCsrfToken.mockResolvedValue('A-CSRF-TOKEN'); }); afterEach(() => { diff --git a/commands/serve/web/hooks/useConnection.js b/commands/serve/web/hooks/useConnection.js index d79b9d880..f64afc6b1 100644 --- a/commands/serve/web/hooks/useConnection.js +++ b/commands/serve/web/hooks/useConnection.js @@ -49,9 +49,11 @@ export const handleConnectionFailure = ({ error, info, setError }) => { if (error.target instanceof WebSocket) { oops.message = `Connection failed to ${info.engineUrl}`; if (/\.qlik[A-Za-z0-9-]+\.com/.test(info.engineUrl) && !info.webIntegrationId) { + oops.hints.push('- If you are connecting to Qlik Cloud, make sure to provide a web integration id or client id.'); oops.hints.push( - 'If you are connecting to Qlik Cloud Services, make sure to provide a web integration id or client id.' + '- For Qlik Sense on Windows, make sure the proxy setup is correct and that you are authenticated.' ); + oops.hints.push('- Press the ? in the top right for more information on how to set up the connection correctly.'); } setError(oops); return; diff --git a/commands/serve/web/hooks/useOpenApp.js b/commands/serve/web/hooks/useOpenApp.js index 652067207..fff9c62d8 100644 --- a/commands/serve/web/hooks/useOpenApp.js +++ b/commands/serve/web/hooks/useOpenApp.js @@ -3,6 +3,7 @@ import enigma from 'enigma.js'; import qixSchema from 'enigma.js/schemas/12.2015.0.json'; import SenseUtilities from 'enigma.js/sense-utilities'; import { getAuthInstance } from '../connect'; +import getCsrfToken from '../utils/getCsrfToken'; export const useOpenApp = ({ info }) => { const [app, setApp] = useState(null); @@ -29,7 +30,8 @@ export const useOpenApp = ({ info }) => { const { webSocketUrl } = await (await fetch(`/auth/getSocketUrl/${info?.enigma.appId}`)).json(); url = webSocketUrl; } else { - url = SenseUtilities.buildUrl(enigmaInfo); + const csrfToken = await getCsrfToken(`https://${enigmaInfo.host}/${enigmaInfo.prefix}`); + url = SenseUtilities.buildUrl({ ...enigmaInfo, ...{ urlParams: { 'qlik-csrf-token': csrfToken } } }); } const enigmaGlobal = await enigma.create({ schema: qixSchema, url }).open(); diff --git a/commands/serve/web/utils/__tests__/appLinkManager.test.js b/commands/serve/web/utils/__tests__/appLinkManager.test.js index 77e9d923f..507f771c5 100644 --- a/commands/serve/web/utils/__tests__/appLinkManager.test.js +++ b/commands/serve/web/utils/__tests__/appLinkManager.test.js @@ -1,4 +1,7 @@ import { getAppLink } from '../appLinkManager'; +import * as getCsrfToken from '../getCsrfToken'; + +jest.mock('../getCsrfToken', () => jest.fn()); describe('getAppLink()', () => { let navigate; @@ -11,6 +14,7 @@ describe('getAppLink()', () => { navigate = jest.fn(); location = { search: '' }; targetApp = 'targetAppId'; + getCsrfToken.mockResolvedValue('A-CSRF-TOKEN'); }); afterEach(() => { @@ -18,58 +22,66 @@ describe('getAppLink()', () => { jest.resetAllMocks(); }); - test('should call navigate to correct engine url from localhost', () => { + test('should call navigate to correct engine url from localhost', async () => { info = { engineUrl: 'ws://localhost:1234', enigma: { secure: false, host: 'localhost', port: 1234 } }; location.search = `engine_url=${info.engineUrl}`; - getAppLink({ navigate, location, info, targetApp }); + await getAppLink({ navigate, location, info, targetApp }); expect(navigate).toHaveBeenCalledTimes(1); - expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`); + expect(navigate).toHaveBeenCalledWith( + `/dev/engine_url=${info.engineUrl}/app/${targetApp}&qlik-csrf-token=A-CSRF-TOKEN` + ); }); - test('should call navigate to correct engine url from localhost without prefix', () => { + test('should call navigate to correct engine url from localhost without prefix', async () => { info = { engineUrl: 'ws://localhost:1234', enigma: { secure: false, host: 'localhost', port: 1234, prefix: undefined }, }; + + getCsrfToken.mockResolvedValue(null); location.search = `engine_url=${info.engineUrl}`; - getAppLink({ navigate, location, info, targetApp }); + await getAppLink({ navigate, location, info, targetApp }); expect(navigate).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`); }); - test('should call navigate to correct engine url with prefix', () => { + test('should call navigate to correct engine url with prefix', async () => { info = { engineUrl: 'ws://localhost:1234/prefix', enigma: { secure: false, host: 'localhost', port: 1234, prefix: 'prefix' }, }; location.search = `engine_url=${info.engineUrl}`; - getAppLink({ navigate, location, info, targetApp }); + await getAppLink({ navigate, location, info, targetApp }); expect(navigate).toHaveBeenCalledTimes(1); - expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`); + expect(navigate).toHaveBeenCalledWith( + `/dev/engine_url=${info.engineUrl}/app/${targetApp}&qlik-csrf-token=A-CSRF-TOKEN` + ); }); - test('should call navigate to correct engine url from remote SDE', () => { + test('should call navigate to correct engine url from remote SDE', async () => { info = { engineUrl: 'wss://some-remote.sde.in.eu.qlikdev.com', enigma: { secure: true, host: 'some-remote.sde.in.eu.qlikdev.com' }, }; + getCsrfToken.mockResolvedValue(null); location.search = `engine_url=${info.engineUrl}`; - getAppLink({ navigate, location, info, targetApp }); + await getAppLink({ navigate, location, info, targetApp }); expect(navigate).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`); }); - test('should remove `shouldFetchAppList` if it was in search', () => { + test('should remove `shouldFetchAppList` if it was in search', async () => { info = { engineUrl: 'wss://some-remote.sde.in.eu.qlikdev.com', enigma: { secure: true, host: 'some-remote.sde.in.eu.qlikdev.com' }, }; + getCsrfToken.mockResolvedValue(null); location.search = `engine_url=${info.engineUrl}&shouldFetchAppList=true`; - getAppLink({ navigate, location, info, targetApp }); + await getAppLink({ navigate, location, info, targetApp }); expect(navigate).toHaveBeenCalledTimes(1); expect(navigate).toHaveBeenCalledWith(`/dev/engine_url=${info.engineUrl}/app/${targetApp}`); diff --git a/commands/serve/web/utils/appLinkManager.js b/commands/serve/web/utils/appLinkManager.js index fe289016e..eb5360622 100644 --- a/commands/serve/web/utils/appLinkManager.js +++ b/commands/serve/web/utils/appLinkManager.js @@ -1,10 +1,16 @@ -export const getAppLink = ({ navigate, location, targetApp, info }) => { +import getCsrfToken from './getCsrfToken'; + +export const getAppLink = async ({ navigate, location, targetApp, info }) => { const { search } = location; const protocol = info.enigma.secure ? 'wss' : 'ws'; const port = info.enigma.port ? `:${info.enigma.port}` : ''; const prefix = info.enigma.prefix ? `/${info.enigma.prefix}` : ''; const host = (info.enigma.host === 'localhost' ? `${info.enigma.host}${port}` : info.enigma.host) + prefix; - const newEngineUrl = `${protocol}://${host}/app/${encodeURIComponent(targetApp)}`; + + const csrfToken = await getCsrfToken(`https://${info.enigma.host}${prefix}`); + const csrfQuery = csrfToken ? `&qlik-csrf-token=${csrfToken}` : ''; + + const newEngineUrl = `${protocol}://${host}/app/${encodeURIComponent(targetApp)}${csrfQuery}`; const modifiedEngineUrl = search.replace(info.engineUrl, newEngineUrl); navigate(`/dev/${modifiedEngineUrl}`.replace('&shouldFetchAppList=true', '')); diff --git a/commands/serve/web/utils/getCsrfToken.js b/commands/serve/web/utils/getCsrfToken.js new file mode 100644 index 000000000..9288b259e --- /dev/null +++ b/commands/serve/web/utils/getCsrfToken.js @@ -0,0 +1,9 @@ +export default async function getCsrfToken(host) { + try { + const res = await fetch(`${host}/qps/csrftoken`, { credentials: 'include' }); + return res.headers.get('QLIK-CSRF-TOKEN'); + } catch (err) { + console.log('Failed to fetch csrf-token', err); + } + return ''; +}