Skip to content

Commit

Permalink
[7.x] Allow idleTimeout/lifespan larger than 32-bit signed integer. (e…
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin authored Oct 7, 2020
1 parent fa32d21 commit ab26307
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 3 deletions.
69 changes: 69 additions & 0 deletions x-pack/plugins/security/public/session/session_timeout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ describe('Session Timeout', () => {

afterEach(async () => {
jest.clearAllMocks();
sessionTimeout.stop();
});

afterAll(() => {
Expand Down Expand Up @@ -147,6 +148,27 @@ describe('Session Timeout', () => {
expect(close).toHaveBeenCalled();
expect(cleanup).toHaveBeenCalled();
});

test(`stop works properly for large timeouts`, async () => {
http.fetch.mockResolvedValue({
...defaultSessionInfo,
idleTimeoutExpiration: now + 5_000_000_000,
});
await sessionTimeout.start();

// Advance timers far enough to call intermediate `setTimeout` multiple times, but before any
// of the timers is supposed to be triggered.
jest.advanceTimersByTime(5_000_000_000 - (60 + 5 + 2) * 1000);

sessionTimeout.stop();

// Advance timer even further and make sure that timers were properly cleaned up.
jest.runAllTimers();

expect(http.fetch).toHaveBeenCalledTimes(1);
expect(sessionExpired.logout).not.toHaveBeenCalled();
expectNoWarningToast(notifications);
});
});

describe('API calls', () => {
Expand Down Expand Up @@ -188,6 +210,21 @@ describe('Session Timeout', () => {
expectIdleTimeoutWarningToast(notifications);
});

test(`shows idle timeout warning toast even for large timeouts`, async () => {
http.fetch.mockResolvedValue({
...defaultSessionInfo,
idleTimeoutExpiration: now + 5_000_000_000,
});
await sessionTimeout.start();

// we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires
jest.advanceTimersByTime(5_000_000_000 - 66 * 1000);
expectNoWarningToast(notifications);

jest.advanceTimersByTime(1000);
expectIdleTimeoutWarningToast(notifications);
});

test(`shows lifespan warning toast`, async () => {
const sessionInfo = {
now,
Expand All @@ -203,6 +240,23 @@ describe('Session Timeout', () => {
expectLifespanWarningToast(notifications);
});

test(`shows lifespan warning toast even for large timeouts`, async () => {
const sessionInfo = {
...defaultSessionInfo,
idleTimeoutExpiration: null,
lifespanExpiration: now + 5_000_000_000,
};
http.fetch.mockResolvedValue(sessionInfo);
await sessionTimeout.start();

// we display the warning a minute before we expire the the session, which is 5 seconds before it actually expires
jest.advanceTimersByTime(5_000_000_000 - 66 * 1000);
expectNoWarningToast(notifications);

jest.advanceTimersByTime(1000);
expectLifespanWarningToast(notifications);
});

test(`extend only results in an HTTP call if a warning is shown`, async () => {
await sessionTimeout.start();
expect(http.fetch).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -328,6 +382,21 @@ describe('Session Timeout', () => {
expect(sessionExpired.logout).toHaveBeenCalled();
});

test(`expires the session 5 seconds before it really expires even for large timeouts`, async () => {
http.fetch.mockResolvedValue({
...defaultSessionInfo,
idleTimeoutExpiration: now + 5_000_000_000,
});

await sessionTimeout.start();

jest.advanceTimersByTime(5_000_000_000 - 6000);
expect(sessionExpired.logout).not.toHaveBeenCalled();

jest.advanceTimersByTime(1000);
expect(sessionExpired.logout).toHaveBeenCalled();
});

test(`extend delays the expiration`, async () => {
await sessionTimeout.start();
expect(http.fetch).toHaveBeenCalledTimes(1);
Expand Down
38 changes: 35 additions & 3 deletions x-pack/plugins/security/public/session/session_timeout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,21 @@ export class SessionTimeout implements ISessionTimeout {
const timeoutVal = timeout - WARNING_MS - GRACE_PERIOD_MS - SESSION_CHECK_MS;
if (timeoutVal > 0 && !isLifespanTimeout) {
// we should check for the latest session info before the warning displays
this.fetchTimer = window.setTimeout(this.fetchSessionInfoAndResetTimers, timeoutVal);
this.startTimer(
(timeoutID) => (this.fetchTimer = timeoutID),
this.fetchSessionInfoAndResetTimers,
timeoutVal
);
}
this.warningTimer = window.setTimeout(

this.startTimer(
(timeoutID) => (this.warningTimer = timeoutID),
this.showWarning,
Math.max(timeout - WARNING_MS - GRACE_PERIOD_MS, 0)
);
this.expirationTimer = window.setTimeout(

this.startTimer(
(timeoutID) => (this.expirationTimer = timeoutID),
() => this.sessionExpired.logout(),
Math.max(timeout - GRACE_PERIOD_MS, 0)
);
Expand Down Expand Up @@ -206,4 +214,28 @@ export class SessionTimeout implements ISessionTimeout {
}
this.warningToast = this.notifications.toasts.add(toast);
};

/**
* Starts a timer that uses a native `setTimeout` under the hood. When `timeout` is larger
* than the maximum supported one then method calls itself recursively as many times as needed.
* @param updater Method that is supposed to update a reference to a native timer ID that can be
* used with native `clearTimeout`. It's essential for the larger timeouts when `setTimeout` is
* called multiple times and timer ID changes.
* when timer ID changes
* @param callback A function to be executed after the timer expires.
* @param timeout The time, in milliseconds the timer should wait before the specified function is
* executed.
*/
private startTimer(updater: (timeoutID: number) => void, callback: () => void, timeout: number) {
// Max timeout is the largest possible 32-bit signed integer or 2,147,483,647 or 0x7fffffff.
const maxTimeout = 0x7fffffff;
updater(
timeout > maxTimeout
? window.setTimeout(
() => this.startTimer(updater, callback, timeout - maxTimeout),
maxTimeout
)
: window.setTimeout(callback, timeout)
);
}
}

0 comments on commit ab26307

Please sign in to comment.