UNPKG

@casual-simulation/aux-vm-browser

Version:

A set of utilities required to securely run an AUX in a web browser.

490 lines 19.2 kB
import { BehaviorSubject, NEVER, Subject, Subscription, filter, firstValueFrom, switchMap, } from 'rxjs'; import { asyncResult, hasValue, reportInst, generateV1ConnectionToken, } from '@casual-simulation/aux-common'; /** * Defines a class that is able to coordinate authentication across multiple simulations. */ export class AuthCoordinator { get onMissingPermission() { return this._onMissingPermission; } get onNotAuthorized() { return this._onNotAuthorized; } get onShowAccountInfo() { return this._onShowAccountInfo; } get onRequestAccess() { return this._onRequestAccess; } get onGrantEntitlements() { return this._onGrantEntitlements; } get authEndpoints() { const helper = this.authHelper; if (helper) { return helper.endpoints; } return new Map(); } get onAuthEndpointDiscovered() { return this._onAuthHelper.pipe(switchMap((helper) => helper ? helper.onEndpointDiscovered : NEVER)); } get authHelper() { return this._onAuthHelper.value; } set authHelper(value) { this._onAuthHelper.next(value); } constructor(manager) { this._onMissingPermission = new Subject(); this._onRequestAccess = new Subject(); this._onNotAuthorized = new Subject(); this._onShowAccountInfo = new Subject(); this._onGrantEntitlements = new Subject(); this._onAuthHelper = new BehaviorSubject(null); this._simulationManager = manager; this._sub = new Subscription(); this._sub.add(this._simulationManager.watchSimulations((sim) => { let sub = new Subscription(); sub.add(sim.onAuthMessage.subscribe(async (msg) => { if (msg.type === 'request') { this._handleAuthRequest(sim, msg); } else if (msg.type === 'external_permission_request') { this._onRequestAccess.next({ simulationId: sim.id, origin: msg.origin, reason: msg.reason, user: msg.user, }); } })); sub.add(sim.localEvents.subscribe((event) => { if (event.type === 'show_account_info') { this.showAccountInfo(sim.id); if (hasValue(event.taskId)) { sim.helper.transaction(asyncResult(event.taskId, null)); } } else if (event.type === 'grant_record_entitlements') { this._onGrantEntitlements.next({ simulationId: sim.id, action: event, }); } })); return sub; })); } async openAccountDashboard(simId) { var _a; const sim = this._simulationManager.simulations.get(simId); if (sim) { await sim.auth.primary.openAccountPage(); } else { await ((_a = this.authHelper) === null || _a === void 0 ? void 0 : _a.primary.openAccountPage()); } } async logout(simId) { var _a; const sim = this._simulationManager.simulations.get(simId); if (sim) { await sim.auth.primary.logout(); } else { await ((_a = this.authHelper) === null || _a === void 0 ? void 0 : _a.primary.logout()); } } async showReportInst(simId) { const sim = this._simulationManager.simulations.get(simId); if (sim) { await sim.helper.transaction(reportInst()); } } async showAccountInfo(simId) { console.log(`[AuthCoordinator] [${simId}] Show account info`); const sim = this._simulationManager.simulations.get(simId); if (sim) { const endpoint = sim.auth.primary; const status = endpoint.currentLoginStatus; if (status) { this._onShowAccountInfo.next({ simulationId: sim.id, loginStatus: status, endpoint: endpoint.origin, }); } } else if (this.authHelper) { const endpoint = this.authHelper.primary; const status = endpoint.currentLoginStatus; if (status) { this._onShowAccountInfo.next({ simulationId: null, loginStatus: status, endpoint: endpoint.origin, }); } } } async changeLogin(simId, origin) { console.log(`[AuthCoordinator] [${simId}] Changing login...`); const sim = this._simulationManager.simulations.get(simId); if (sim) { const endpoint = sim.auth.primary; await endpoint.logout(); await endpoint.authenticate(); const key = await endpoint.getConnectionKey(); if (key) { const connectionId = sim.configBotId; const recordName = sim.origin.recordName; const inst = sim.inst; const token = generateV1ConnectionToken(key, connectionId, recordName, inst); console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`); sim.sendAuthMessage({ type: 'response', success: true, origin: origin, indicator: { connectionToken: token, }, }); } } } async requestAccessToMissingPermission(simId, origin, reason) { const sim = this._simulationManager.simulations.get(simId); if (sim) { const promise = firstValueFrom(sim.onAuthMessage.pipe(filter((m) => m.origin === origin && m.type === 'external_permission_result'))); console.log(`[AuthCoordinator] [${sim.id}] Requesting permission`, reason); sim.sendAuthMessage({ type: 'permission_request', origin: origin, reason, }); const response = await Promise.race([ promise, new Promise((resolve) => { setTimeout(() => { resolve({ type: 'external_permission_result', origin, success: false, recordName: reason.recordName, resourceKind: reason.resourceKind, resourceId: reason.resourceKind, subjectType: reason.subjectType, subjectId: reason.subjectId, errorCode: 'not_authorized', errorMessage: 'The request expired.', }); }, 45 * 1000); }), ]); console.log(`[AuthCoordinator] [${sim.id}] Got permission result`, response); if (response.type === 'external_permission_result') { console.log(`[AuthCoordinator] [${sim.id}] Got permission result`, response); return { ...response, type: 'permission_result', }; } } return { type: 'permission_result', origin, success: false, recordName: reason.recordName, resourceKind: reason.resourceKind, resourceId: reason.resourceKind, subjectType: reason.subjectType, subjectId: reason.subjectId, errorCode: 'server_error', errorMessage: 'A server error occurred.', }; } async grantAccessToMissingPermission(simId, origin, reason, expireTimeMs = null, actions = null) { const sim = this._simulationManager.simulations.get(simId); if (sim) { const recordName = reason.recordName; const resourceKind = reason.resourceKind; const resourceId = reason.resourceId; const subjectType = reason.subjectType; const subjectId = reason.subjectId; if (!actions) { const result = await sim.auth.primary.grantPermission(recordName, { resourceKind, resourceId, subjectType, subjectId, action: null, options: {}, expireTimeMs, }); if (result.success === true) { sim.sendAuthMessage({ type: 'permission_result', success: true, origin, recordName, resourceKind, resourceId, subjectType, subjectId, }); } return result; } else { for (let action of actions) { const result = await sim.auth.primary.grantPermission(recordName, { resourceKind, resourceId, subjectType, subjectId, action: action, options: {}, expireTimeMs, }); if (result.success === false) { return result; } } sim.sendAuthMessage({ type: 'permission_result', success: true, origin, recordName, resourceKind, resourceId, subjectType, subjectId, }); return { success: true, }; } } console.error('[AuthCoordinator] Could not find simulation to grant access to.', simId, origin, reason); return { success: false, errorCode: 'server_error', errorMessage: 'A server error occurred.', }; } async respondToPermissionRequest(simId, origin, result) { const sim = this._simulationManager.simulations.get(simId); if (sim) { sim.sendAuthMessage(result); } } async grantEntitlements(entitlementGrantEvent) { const sim = this._simulationManager.simulations.get(entitlementGrantEvent.simulationId); if (sim) { await sim.records.grantEntitlements(entitlementGrantEvent.action); } } async denyEntitlements(entitlementGrantEvent) { const sim = this._simulationManager.simulations.get(entitlementGrantEvent.simulationId); if (sim) { sim.helper.transaction(asyncResult(entitlementGrantEvent.action.taskId, { success: false, errorCode: 'not_authorized', errorMessage: 'The request for access was denied.', })); } } async _handleAuthRequest(sim, request) { if (request.kind === 'need_indicator') { await this._handleNeedIndicator(sim, request); } else if (request.kind === 'invalid_indicator') { await this._handleInvalidIndicator(sim, request); } else if (request.kind === 'not_authorized') { await this._handleNotAuthorized(sim, request); } } async _handleNeedIndicator(sim, request) { console.log(`[AuthCoordinator] [${sim.id}] Needs indicator`); const endpoint = sim.auth.primary; const key = await endpoint.getConnectionKey(); if (!key) { console.log(`[AuthCoordinator] [${sim.id}] Sending connectionId.`); sim.sendAuthMessage({ type: 'response', success: true, origin: request.origin, indicator: { connectionId: sim.configBotId, }, }); } else { const connectionId = sim.configBotId; const recordName = sim.origin.recordName; const inst = sim.inst; const token = generateV1ConnectionToken(key, connectionId, recordName, inst); console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`); sim.sendAuthMessage({ type: 'response', success: true, origin: request.origin, indicator: { connectionToken: token, }, }); } } async _handleInvalidIndicator(sim, request) { console.log(`[AuthCoordinator] [${sim.id}] [${request.errorCode}] Invalid indicator.`); const endpoint = sim.auth.primary; let key; if (request.errorCode === 'invalid_token') { if (await endpoint.isAuthenticated()) { console.log(`[AuthCoordinator] [${sim.id}] Logging out and back in...`); await endpoint.relogin(); } } else { key = await endpoint.getConnectionKey(); if (!key) { console.log(`[AuthCoordinator] [${sim.id}] Logging in...`); await endpoint.authenticate(); } } if (!key) { key = await endpoint.getConnectionKey(); } if (key) { const connectionId = sim.configBotId; const recordName = sim.origin.recordName; const inst = sim.inst; const token = generateV1ConnectionToken(key, connectionId, recordName, inst); console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`); sim.sendAuthMessage({ type: 'response', success: true, origin: request.origin, indicator: { connectionToken: token, }, }); } } async _handleNotAuthorized(sim, request) { var _a, _b; if (request.errorCode === 'not_logged_in') { await this._handleNotLoggedIn(sim, request); } else if (((_a = request.reason) === null || _a === void 0 ? void 0 : _a.type) === 'missing_permission') { await this._handleMissingPermission(sim, request, request.reason); } else if (((_b = request.reason) === null || _b === void 0 ? void 0 : _b.type) === 'invalid_token') { // Trying to watch a branch that the current connection token doesn't support await this._handleInvalidToken(sim, request); } else { await this._handleNotAuthorizedError(sim, request); } } async _handleInvalidToken(sim, request) { var _a, _b, _c; const recordName = (_a = request.resource) === null || _a === void 0 ? void 0 : _a.recordName; const inst = (_b = request.resource) === null || _b === void 0 ? void 0 : _b.inst; const branch = (_c = request.resource) === null || _c === void 0 ? void 0 : _c.branch; if (!recordName || !inst || !branch) { console.log(`[AuthCoordinator] [${sim.id}] Invalid token request missing recordName, inst, or branch`); return; } // Only allow automatically loading branches that start with 'doc/' // This is a temporary solution to prevent loading actual existing inst data and instead only allow loading // shared documents from other records if (!branch.startsWith('doc/')) { console.error(`[AuthCoordinator] [${sim.id}] Invalid token request branch does not start with 'doc/'`); return; } console.log(`[AuthCoordinator] [${sim.id}] Needs new indicator`); const endpoint = sim.auth.primary; const key = await endpoint.getConnectionKey(); if (!key) { console.log(`[AuthCoordinator] [${sim.id}] Sending connectionId.`); sim.sendAuthMessage({ type: 'response', success: true, origin: request.origin, indicator: { connectionId: sim.configBotId, }, }); } else { const connectionId = sim.configBotId; const token = generateV1ConnectionToken(key, connectionId, recordName, inst); console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`); sim.sendAuthMessage({ type: 'response', success: true, origin: request.origin, indicator: { connectionToken: token, }, }); } } async _handleNotLoggedIn(sim, request) { console.log(`[AuthCoordinator] [${sim.id}] Not logged in`); const endpoint = sim.auth.primary; let key = await endpoint.getConnectionKey(); if (!key) { if (!(await endpoint.isAuthenticated())) { console.log(`[AuthCoordinator] [${sim.id}] Logging in...`); await endpoint.authenticate(); key = await endpoint.getConnectionKey(); } } if (key) { const connectionId = sim.configBotId; const recordName = sim.origin.recordName; const inst = sim.inst; const token = generateV1ConnectionToken(key, connectionId, recordName, inst); console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`); sim.sendAuthMessage({ type: 'response', success: true, origin: request.origin, indicator: { connectionToken: token, }, }); } } async _handleMissingPermission(sim, request, reason) { console.log(`[AuthCoordinator] [${sim.id}] Missing permission ${reason.resourceKind}.${reason.action}.`); this._onMissingPermission.next({ simulationId: sim.id, errorCode: request.errorCode, errorMessage: request.errorMessage, origin: request.origin, reason, }); } async _handleNotAuthorizedError(sim, request) { console.log(`[AuthCoordinator] [${sim.id}] Not authorized: ${request.errorMessage}.`); this._onNotAuthorized.next({ simulationId: sim.id, errorCode: request.errorCode, errorMessage: request.errorMessage, origin: request.origin, }); } unsubscribe() { return this._sub.unsubscribe(); } get closed() { return this._sub.closed; } } //# sourceMappingURL=AuthCoordinator.js.map