diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts new file mode 100644 index 0000000000..919948f531 --- /dev/null +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { OptionalRefreshSessionManagerMux } from './OptionalRefreshSessionManagerMux'; +import { MutableSessionManager, SessionManager } from './types'; + +class MockManager implements MutableSessionManager { + constructor(public session?: string) {} + + setSession(session: string | undefined): void { + this.session = session; + } + async getSession(): Promise { + return this.session; + } + async removeSession(): Promise { + delete this.session; + } + sessionState$(): Observable { + throw new Error('Method not implemented.'); + } +} + +function trackState(manager: SessionManager) { + const states = new Array(); + manager + .sessionState$() + .subscribe(state => states.push(state === SessionState.SignedIn)); + return states; +} + +describe('OptionalRefreshSessionManagerMux', () => { + it('finds no session', async () => { + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager: new MockManager(), + refreshingSessionManager: new MockManager(), + sessionCanRefresh: () => false, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe(undefined); + expect(states).toEqual([false]); + }); + + it('prioritizes a static session', async () => { + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager: new MockManager('static'), + refreshingSessionManager: new MockManager('refreshing'), + sessionCanRefresh: () => false, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('static'); + expect(states).toEqual([false, true]); + }); + + it('transfers a refreshing session to the static manager', async () => { + const staticSessionManager = new MockManager(); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => false, + }); + + const states = trackState(mux); + expect(staticSessionManager.session).toBeUndefined(); + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBe('refreshing'); + expect(states).toEqual([false, true]); + }); + + it('relies on the refreshing manager if refresh is available', async () => { + const staticSessionManager = new MockManager(); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => true, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBeUndefined(); + expect(states).toEqual([false, true]); + }); + + it('can switch between refreshing and static sessions', async () => { + let canRefresh = true; + const staticSessionManager = new MockManager(); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => canRefresh, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBeUndefined(); + canRefresh = false; + await expect(mux.getSession({})).resolves.toBe('refreshing'); + expect(staticSessionManager.session).toBe('refreshing'); + + expect(states).toEqual([false, true]); + }); + + it('removes sessions from both managers', async () => { + const staticSessionManager = new MockManager('static'); + const refreshingSessionManager = new MockManager('refreshing'); + const mux = new OptionalRefreshSessionManagerMux({ + staticSessionManager, + refreshingSessionManager, + sessionCanRefresh: () => true, + }); + + const states = trackState(mux); + await expect(mux.getSession({})).resolves.toBe('static'); + await mux.removeSession(); + expect(staticSessionManager.session).toBeUndefined(); + expect(refreshingSessionManager.session).toBeUndefined(); + expect(states).toEqual([false, true, false]); + }); +}); diff --git a/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts new file mode 100644 index 0000000000..221a7635ea --- /dev/null +++ b/packages/core-app-api/src/lib/AuthSessionManager/OptionalRefreshSessionManagerMux.ts @@ -0,0 +1,105 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Observable, SessionState } from '@backstage/core-plugin-api'; +import { + SessionManager, + MutableSessionManager, + GetSessionOptions, +} from './types'; +import { SessionStateTracker } from './SessionStateTracker'; + +type Options = { + /** + * A callback that is called to determine whether a given session supports refresh + */ + sessionCanRefresh: (session: T) => boolean; + + /** + * The session manager that is used if the a session does not support refresh. + */ + staticSessionManager: MutableSessionManager; + + /** + * The session manager that is used if the a session supports refresh. + */ + refreshingSessionManager: SessionManager; +}; + +/** + * OptionalRefreshSessionManagerMux wraps two different session managers, one for + * static session storage and another one that supports refresh. For each session + * that is retrieved is checked for whether it supports refresh. If it does, the + * refreshing session manager is used, otherwise the static session manager is used. + */ +export class OptionalRefreshSessionManagerMux implements SessionManager { + private readonly stateTracker = new SessionStateTracker(); + + private readonly sessionCanRefresh: (session: T) => boolean; + private readonly staticSessionManager: MutableSessionManager; + private readonly refreshingSessionManager: SessionManager; + + constructor(options: Options) { + this.sessionCanRefresh = options.sessionCanRefresh; + this.staticSessionManager = options.staticSessionManager; + this.refreshingSessionManager = options.refreshingSessionManager; + } + + async getSession(options: GetSessionOptions): Promise { + // First we check if there is an existing static session, using an optional request + const staticSession = await this.staticSessionManager.getSession({ + ...options, + optional: true, + }); + if (staticSession) { + this.stateTracker.setIsSignedIn(true); + return staticSession; + } + + // If there is no static session available, we ask the refresh manager to get a session + const session = await this.refreshingSessionManager.getSession(options); + + // Handling the case where the session request is optional + if (!session) { + this.stateTracker.setIsSignedIn(false); + return undefined; + } + + // Next we check if the session we received from the refreshing manager can actually + // be refreshed. If it can, we use this session without storing it in the static manager. + if (this.sessionCanRefresh(session)) { + this.stateTracker.setIsSignedIn(true); + return session; + } + + // If the session can't be refreshed, we store it in the static manager + this.staticSessionManager.setSession(session); + this.stateTracker.setIsSignedIn(true); + return session; + } + + async removeSession(): Promise { + await Promise.all([ + this.refreshingSessionManager.removeSession(), + this.staticSessionManager.removeSession(), + ]); + this.stateTracker.setIsSignedIn(false); + } + + sessionState$(): Observable { + return this.stateTracker.sessionState$(); + } +}