[SupersetClient] allow csrf token to be passed as configuration (#9)

* [SupersetClient] allow csrf token to be passed as configuration

* [SupersetClient] update readme

* [build-config] ^0.0.20

* [SupersetClient] fix test.

* [build-config] ^0.0.21

* [build-config] ^0.0.22

* [build-config] ^0.0.23
This commit is contained in:
Chris Williams 2018-10-18 17:53:05 -07:00 committed by Yongjie Zhao
parent 405dda57ac
commit f0b02cdf21
5 changed files with 73 additions and 26 deletions

View File

@ -12,6 +12,7 @@
*.map
*.min.js
babel.config.js
build/
coverage/
esm/

View File

@ -14,9 +14,11 @@ for use within the Superset application, or used to issue `CORS` requests in oth
a high-level it supports:
- `CSRF` token authentication
- queues requests in the case that another request is made before the token is received
- it checks for a token before every request, an external app that uses this can detect this by
catching errors, or explicitly checking `SupersetClient.isAuthorized()`
- a token may be passed at configuration time, else the client will handle fetching and passing
the token in all subsequent requests.
- queues requests in the case that another request is made before the token is received.
- it checks for a token before every request, and will fail if no token was received or if it has
expired. In either case the user should be directed to re-authenticate.
- supports `GET` and `POST` requests (no `PUT` or `DELETE`)
- timeouts
- query aborts through the `AbortController` API
@ -46,12 +48,14 @@ SupersetClient.post(...requestConfig)
The following flags can be passed in the client config call
`SupersetClient.configure(...clientConfig);`
- `protocol = 'http'`
- `protocol = 'http:'`
- `host`
- `headers`
- `credentials = 'same-origin'` (set to `include` for non-Superset apps)
- `mode = 'same-origin'` (set to `cors` for non-Superset apps)
- `timeout`
- `csrfToken` you can configure the client with a CSRF token at configuration time, else the client
will attempt to fetch this before any other requests are issued
##### Per-request Configuration

View File

@ -20,7 +20,6 @@
"lint:fix": "yarn run prettier --write && yarn run eslint --fix",
"test": "yarn run jest",
"prettier": "beemo prettier \"./{src,test}/**/*.{js,jsx,json,md}\"",
"sync:gitignore": "beemo sync-dotfiles --filter=gitignore",
"prepublish": "yarn run build"
},
"repository": {
@ -40,12 +39,12 @@
},
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"devDependencies": {
"@data-ui/build-config": "^0.0.14",
"@data-ui/build-config": "^0.0.23",
"fetch-mock": "^6.5.2",
"node-fetch": "^2.2.0"
},
"dependencies": {
"babel-runtime": "^6.26.0",
"@babel/runtime": "^7.1.2",
"whatwg-fetch": "^2.0.4"
},
"beemo": {
@ -65,7 +64,6 @@
"rules": {
"prefer-promise-reject-errors": "off"
}
},
"jest": {}
}
}
}

View File

@ -9,6 +9,7 @@ class SupersetClient {
mode = 'same-origin',
timeout,
credentials,
csrfToken = null,
} = config;
this.headers = headers;
@ -17,16 +18,20 @@ class SupersetClient {
this.timeout = timeout;
this.protocol = `${protocol}${protocol.slice(-1) === ':' ? '' : ':'}`;
this.credentials = credentials;
this.csrfToken = null;
this.didAuthSuccessfully = false;
this.csrfPromise = null;
this.csrfToken = csrfToken;
this.csrfPromise = this.isAuthenticated() ? Promise.resolve(this.csrfToken) : null;
}
isAuthenticated() {
return this.didAuthSuccessfully;
// if CSRF protection is disabled in the Superset app, the token may be an empty string
return this.csrfToken !== null && this.csrfToken !== undefined;
}
init() {
init(force = false) {
if (this.isAuthenticated() && !force) {
return this.csrfPromise;
}
return this.getCSRFToken();
}
@ -48,14 +53,13 @@ class SupersetClient {
if (response.json) {
this.csrfToken = response.json.csrf_token;
this.headers = { ...this.headers, 'X-CSRFToken': this.csrfToken };
this.didAuthSuccessfully = this.csrfToken !== null && this.csrfPromise !== undefined;
}
if (!this.didAuthSuccessfully) {
if (!this.isAuthenticated()) {
return Promise.reject({ error: 'Failed to fetch CSRF token' });
}
return response;
return this.csrfToken;
});
return this.csrfPromise;
@ -140,10 +144,10 @@ const PublicAPI = {
return singletonClient;
},
get: (...args) => hasInstance() && singletonClient.get(...args),
init: () => hasInstance() && singletonClient.init(),
init: force => hasInstance() && singletonClient.init(force),
isAuthenticated: () => hasInstance() && singletonClient.isAuthenticated(),
post: (...args) => hasInstance() && singletonClient.post(...args),
reAuthenticate: () => hasInstance() && singletonClient.getCSRFToken(),
reAuthenticate: () => hasInstance() && singletonClient.init(/* force = */ true),
reset: () => {
singletonClient = null;
},

View File

@ -51,7 +51,10 @@ describe('SupersetClient', () => {
const csrfSpy = jest.spyOn(SupersetClient.prototype, 'getCSRFToken');
PublicAPI.configure({});
expect(authenticatedSpy).toHaveBeenCalledTimes(1);
PublicAPI.init();
expect(initSpy).toHaveBeenCalledTimes(1);
expect(csrfSpy).toHaveBeenCalledTimes(1);
PublicAPI.get({ url: mockGetUrl });
@ -59,10 +62,9 @@ describe('SupersetClient', () => {
PublicAPI.isAuthenticated();
PublicAPI.reAuthenticate({});
expect(initSpy).toHaveBeenCalledTimes(1);
expect(initSpy).toHaveBeenCalledTimes(2);
expect(getSpy).toHaveBeenCalledTimes(1);
expect(postSpy).toHaveBeenCalledTimes(1);
expect(authenticatedSpy).toHaveBeenCalledTimes(1);
expect(csrfSpy).toHaveBeenCalledTimes(2); // from init() + reAuthenticate()
initSpy.mockRestore();
@ -79,7 +81,7 @@ describe('SupersetClient', () => {
describe('CSRF', () => {
afterEach(fetchMock.reset);
it('calls superset/csrf_token/ upon initialization', () => {
it('calls superset/csrf_token/ when init() is called if no CSRF token is passed', () => {
expect.assertions(1);
const client = new SupersetClient({});
@ -90,6 +92,35 @@ describe('SupersetClient', () => {
});
});
it('does NOT call superset/csrf_token/ when init() is called if a CSRF token is passed', () => {
expect.assertions(1);
const client = new SupersetClient({ csrfToken: 'abc' });
return client.init().then(() => {
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
return Promise.resolve();
});
});
it('calls superset/csrf_token/ when init(force=true) is called even if a CSRF token is passed', () => {
expect.assertions(4);
const initialToken = 'inital_token';
const client = new SupersetClient({ csrfToken: initialToken });
return client.init().then(() => {
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
expect(client.csrfToken).toBe(initialToken);
return client.init(true).then(() => {
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
expect(client.csrfToken).not.toBe(initialToken);
return Promise.resolve();
});
});
});
it('isAuthenticated() returns true if there is a token and false if not', () => {
expect.assertions(2);
const client = new SupersetClient({});
@ -102,6 +133,15 @@ describe('SupersetClient', () => {
});
});
it('isAuthenticated() returns true if a token is passed at configuration', () => {
expect.assertions(2);
const clientWithoutToken = new SupersetClient({ csrfToken: null });
const clientWithToken = new SupersetClient({ csrfToken: 'token' });
expect(clientWithoutToken.isAuthenticated()).toBe(false);
expect(clientWithToken.isAuthenticated()).toBe(true);
});
it('init() throws if superset/csrf_token/ returns an error', () => {
expect.assertions(1);
@ -120,7 +160,7 @@ describe('SupersetClient', () => {
// reset
fetchMock.get(
LOGIN_GLOB,
{ csrf_token: 1234 },
{ csrf_token: '1234' },
{
overwriteRoutes: true,
},
@ -167,7 +207,7 @@ describe('SupersetClient', () => {
.then(throwIfCalled)
.catch(error => {
expect(error).toEqual(expect.objectContaining({ error: expect.any(String) }));
expect(client.didAuthSuccessfully).toBe(false);
expect(client.isAuthenticated()).toBe(false);
return Promise.resolve();
});
@ -183,7 +223,7 @@ describe('SupersetClient', () => {
.ensureAuth()
.then(throwIfCalled)
.catch(() => {
expect(client.didAuthSuccessfully).toBe(true);
expect(client.isAuthenticated()).toBe(true);
return Promise.resolve();
}),
@ -211,7 +251,7 @@ describe('SupersetClient', () => {
.then(throwIfCalled)
.catch(error2 => {
expect(error2).toEqual(expect.objectContaining(rejectValue));
expect(client.didAuthSuccessfully).toBe(false);
expect(client.isAuthenticated()).toBe(false);
// reset
fetchMock.get(