mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Fix: Fixes user logout for datasourceRequests with 401 from datasource (#21800)
This commit is contained in:
parent
0fa20cb231
commit
08d330b406
@ -170,11 +170,6 @@ export class BackendSrv implements BackendService {
|
|||||||
return merge(successStream, failureStream)
|
return merge(successStream, failureStream)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err: ErrorResponse) => {
|
catchError((err: ErrorResponse) => {
|
||||||
if (err.status === 401) {
|
|
||||||
this.dependencies.logout();
|
|
||||||
return throwError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this setTimeout hack enables any caller catching this err to set isHandled to true
|
// this setTimeout hack enables any caller catching this err to set isHandled to true
|
||||||
setTimeout(() => this.requestErrorHandler(err), 50);
|
setTimeout(() => this.requestErrorHandler(err), 50);
|
||||||
return throwError(err);
|
return throwError(err);
|
||||||
@ -202,7 +197,7 @@ export class BackendSrv implements BackendService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fromFetchStream = this.getFromFetchStream(options);
|
const fromFetchStream = this.getFromFetchStream(options);
|
||||||
const failureStream = fromFetchStream.pipe(this.toFailureStream(options));
|
const failureStream = fromFetchStream.pipe(this.toDataSourceRequestFailureStream(options));
|
||||||
const successStream = fromFetchStream.pipe(
|
const successStream = fromFetchStream.pipe(
|
||||||
filter(response => response.ok === true),
|
filter(response => response.ok === true),
|
||||||
map(response => {
|
map(response => {
|
||||||
@ -226,11 +221,6 @@ export class BackendSrv implements BackendService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.status === 401) {
|
|
||||||
this.dependencies.logout();
|
|
||||||
return throwError(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate error obj on Internal Error
|
// populate error obj on Internal Error
|
||||||
if (typeof err.data === 'string' && err.status === 500) {
|
if (typeof err.data === 'string' && err.status === 500) {
|
||||||
err.data = {
|
err.data = {
|
||||||
@ -498,7 +488,50 @@ export class BackendSrv implements BackendService {
|
|||||||
const firstAttempt = i === 0 && options.retry === 0;
|
const firstAttempt = i === 0 && options.retry === 0;
|
||||||
|
|
||||||
if (error.status === 401 && this.dependencies.contextSrv.user.isSignedIn && firstAttempt) {
|
if (error.status === 401 && this.dependencies.contextSrv.user.isSignedIn && firstAttempt) {
|
||||||
return from(this.loginPing());
|
return from(this.loginPing()).pipe(
|
||||||
|
catchError(err => {
|
||||||
|
if (err.status === 401) {
|
||||||
|
this.dependencies.logout();
|
||||||
|
return throwError(err);
|
||||||
|
}
|
||||||
|
return throwError(err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(error);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private toDataSourceRequestFailureStream = (
|
||||||
|
options: BackendSrvRequest
|
||||||
|
): MonoTypeOperatorFunction<FetchResponse> => inputStream =>
|
||||||
|
inputStream.pipe(
|
||||||
|
filter(response => response.ok === false),
|
||||||
|
mergeMap(response => {
|
||||||
|
const { status, statusText, data } = response;
|
||||||
|
const fetchErrorResponse: ErrorResponse = { status, statusText, data };
|
||||||
|
return throwError(fetchErrorResponse);
|
||||||
|
}),
|
||||||
|
retryWhen((attempts: Observable<any>) =>
|
||||||
|
attempts.pipe(
|
||||||
|
mergeMap((error, i) => {
|
||||||
|
const requestIsLocal = !options.url.match(/^http/);
|
||||||
|
const firstAttempt = i === 0 && options.retry === 0;
|
||||||
|
|
||||||
|
// First retry, if loginPing returns 401 this retry sequence will abort with throwError and user is logged out
|
||||||
|
if (requestIsLocal && firstAttempt && error.status === 401) {
|
||||||
|
return from(this.loginPing()).pipe(
|
||||||
|
catchError(err => {
|
||||||
|
if (err.status === 401) {
|
||||||
|
this.dependencies.logout();
|
||||||
|
return throwError(err);
|
||||||
|
}
|
||||||
|
return throwError(err);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return throwError(error);
|
return throwError(error);
|
||||||
|
@ -164,30 +164,34 @@ describe('backendSrv', () => {
|
|||||||
describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => {
|
describe('when making an unsuccessful call and conditions for retry are favorable and loginPing does not throw', () => {
|
||||||
it('then it should retry', async () => {
|
it('then it should retry', async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
const url = '/api/dashboard/';
|
||||||
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
const { backendSrv, appEventsMock, logoutMock, expectRequestCallChain } = getTestContext({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 401,
|
status: 401,
|
||||||
statusText: 'UnAuthorized',
|
statusText: 'UnAuthorized',
|
||||||
data: { message: 'UnAuthorized' },
|
data: { message: 'UnAuthorized' },
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
backendSrv.loginPing = jest
|
backendSrv.loginPing = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
|
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
|
||||||
const url = '/api/dashboard/';
|
await backendSrv
|
||||||
// it would be better if we could simulate that after the call to loginPing everything is successful but as
|
.request({ url, method: 'GET', retry: 0 })
|
||||||
// our fromFetchMock returns ok:false the second time this retries it will still be ok:false going into the
|
.catch(error => {
|
||||||
// mergeMap in toFailureStream
|
expect(error.status).toBe(401);
|
||||||
await backendSrv.request({ url, method: 'GET', retry: 0 }).catch(error => {
|
expect(error.statusText).toBe('UnAuthorized');
|
||||||
expect(error.status).toBe(401);
|
expect(error.data).toEqual({ message: 'UnAuthorized' });
|
||||||
expect(error.statusText).toBe('UnAuthorized');
|
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
||||||
expect(error.data).toEqual({ message: 'UnAuthorized' });
|
expect(logoutMock).not.toHaveBeenCalled();
|
||||||
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
|
||||||
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
|
expectRequestCallChain({ url, method: 'GET', retry: 0 });
|
||||||
expect(logoutMock).toHaveBeenCalledTimes(1);
|
jest.advanceTimersByTime(50);
|
||||||
expectRequestCallChain({ url, method: 'GET', retry: 0 });
|
})
|
||||||
jest.advanceTimersByTime(50);
|
.catch(error => {
|
||||||
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
expect(error).toEqual({ message: 'UnAuthorized' });
|
||||||
});
|
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(appEventsMock.emit).toHaveBeenCalledWith(AppEvents.alertWarning, ['UnAuthorized', '']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -430,16 +434,18 @@ describe('backendSrv', () => {
|
|||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
|
.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', data: { message: 'Ok' } });
|
||||||
const url = '/api/dashboard/';
|
const url = '/api/dashboard/';
|
||||||
// it would be better if we could simulate that after the call to loginPing everything is successful but as
|
|
||||||
// our fromFetchMock returns ok:false the second time this retries it will still be ok:false going into the
|
|
||||||
// mergeMap in toFailureStream
|
|
||||||
await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => {
|
await backendSrv.datasourceRequest({ url, method: 'GET', retry: 0 }).catch(error => {
|
||||||
expect(error.status).toBe(401);
|
expect(error.status).toBe(401);
|
||||||
expect(error.statusText).toBe('UnAuthorized');
|
expect(error.statusText).toBe('UnAuthorized');
|
||||||
expect(error.data).toEqual({ message: 'UnAuthorized' });
|
expect(error.data).toEqual({ message: 'UnAuthorized' });
|
||||||
expect(appEventsMock.emit).not.toHaveBeenCalled();
|
expect(appEventsMock.emit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(appEventsMock.emit).toHaveBeenCalledWith(CoreEvents.dsRequestError, {
|
||||||
|
data: { message: 'UnAuthorized' },
|
||||||
|
status: 401,
|
||||||
|
statusText: 'UnAuthorized',
|
||||||
|
});
|
||||||
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
|
expect(backendSrv.loginPing).toHaveBeenCalledTimes(1);
|
||||||
expect(logoutMock).toHaveBeenCalledTimes(1);
|
expect(logoutMock).not.toHaveBeenCalled();
|
||||||
expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 });
|
expectDataSourceRequestCallChain({ url, method: 'GET', retry: 0 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user