@xenon-device-management/xenon 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/lib/package.json +5 -5
- package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
- package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
- package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
- package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
- package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
- package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
- package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
- package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
- package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
- package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
- package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
- package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
- package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
- package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
- package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
- package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
- package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
- package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
- package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
- package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
- package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
- package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
- package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
- package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
- package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
- package/lib/public/assets/{copy-CAQydNFe.js → copy-Be7LNMXV.js} +1 -1
- package/lib/public/assets/derive-UnLRp4r3.js +1 -0
- package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
- package/lib/public/assets/{download-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
- package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
- package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
- package/lib/public/assets/index-B9KQ6LsY.js +15 -0
- package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
- package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
- package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
- package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
- package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
- package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
- package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
- package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
- package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
- package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
- package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
- package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
- package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
- package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
- package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
- package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
- package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
- package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
- package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
- package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
- package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
- package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
- package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
- package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
- package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
- package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
- package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
- package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
- package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
- package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
- package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
- package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +5 -5
- package/lib/src/app/dashboardPluginLink.js +117 -0
- package/lib/src/app/index.js +13 -34
- package/lib/src/app/openapi/control.yaml +6 -4
- package/lib/src/app/openapi/grid.yaml +8 -2
- package/lib/src/app/openapi/identity.yaml +103 -31
- package/lib/src/app/openapi/platform.yaml +33 -15
- package/lib/src/app/openapi/sessions.yaml +10 -3
- package/lib/src/app/routers/apikeys.js +14 -2
- package/lib/src/app/routers/auth.js +29 -5
- package/lib/src/app/routers/control.js +13 -5
- package/lib/src/app/routers/dashboard.js +42 -27
- package/lib/src/app/routers/grid.js +5 -3
- package/lib/src/app/routers/profile.js +21 -0
- package/lib/src/app/routers/users.js +10 -5
- package/lib/src/app/swagger.js +2 -0
- package/lib/src/app/ws/h264StreamWs.js +10 -1
- package/lib/src/config.js +1 -0
- package/lib/src/dashboard/commands.js +5 -5
- package/lib/src/dashboard/event-manager.js +49 -61
- package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
- package/lib/src/dashboard/sessionCommandSummary.js +39 -0
- package/lib/src/data-service/device-store.js +14 -1
- package/lib/src/data-service/prisma-store.js +19 -7
- package/lib/src/data-service/web-config-service.js +7 -0
- package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
- package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
- package/lib/src/device-utils.js +20 -2
- package/lib/src/gateway/defaultGateway.js +1 -1
- package/lib/src/gateway/forwardToNode.js +2 -0
- package/lib/src/gateway/healReport.js +87 -0
- package/lib/src/gateway/sessionGateway.js +12 -2
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +32 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +3 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/interceptors/CommandInterceptor.js +243 -181
- package/lib/src/middleware/authMiddleware.js +15 -3
- package/lib/src/middleware/commandCaller.js +3 -3
- package/lib/src/middleware/verifyCredential.js +58 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/services/AICommandService.js +9 -2
- package/lib/src/services/AIService.js +192 -42
- package/lib/src/services/EmailService.js +29 -0
- package/lib/src/services/OrphanSweeper.js +2 -0
- package/lib/src/services/SelectorVerificationJob.js +15 -4
- package/lib/src/services/ServerManager.js +11 -1
- package/lib/src/services/SessionLifecycleService.js +86 -26
- package/lib/src/services/SocketServer.js +19 -3
- package/lib/src/services/bug-report/video-slice.js +4 -1
- package/lib/src/services/device-access/leaseOverride.js +7 -5
- package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
- package/lib/src/services/healing/HealEtalonService.js +3 -2
- package/lib/src/services/healing/HealingOrchestrator.js +55 -72
- package/lib/src/services/healing/OcrHealingProvider.js +41 -64
- package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
- package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
- package/lib/src/services/healing/fingerprintIdentity.js +126 -0
- package/lib/src/services/healing/resilioPath.js +274 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
- package/lib/src/services/logcat/deviceLogBook.js +163 -0
- package/lib/src/services/ocr/ocrData.js +118 -0
- package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
- package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
- package/lib/src/services/passwordResetLink.js +50 -9
- package/lib/src/services/passwordResetMode.js +3 -1
- package/lib/src/services/publicUrl.js +45 -0
- package/lib/src/services/session/sessionIdentity.js +5 -3
- package/lib/src/services/session/shutdownReason.js +26 -0
- package/lib/src/services/settings/aiEngineSettings.js +219 -0
- package/lib/src/services/token/mintedLifetime.js +51 -0
- package/lib/src/sessions/SessionManager.js +8 -2
- package/lib/src/sessions/sessionMemory.js +25 -0
- package/lib/test/helpers/fake-ai-provider.js +188 -0
- package/lib/test/helpers/loki-stores.js +33 -0
- package/lib/test/helpers/page-sources.js +164 -0
- package/lib/test/helpers/scratch-database.js +57 -13
- package/lib/test/integration/audit-ingest.spec.js +10 -10
- package/lib/test/integration/auth-flow.spec.js +9 -14
- package/lib/test/integration/auth-rate-limit.spec.js +6 -3
- package/lib/test/integration/bug-report-route.spec.js +22 -2
- package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
- package/lib/test/integration/forgot-password.spec.js +17 -18
- package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
- package/lib/test/integration/migration.spec.js +6 -1
- package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
- package/lib/test/integration/ports-router.spec.js +4 -4
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
- package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
- package/lib/test/integration/profile-tokens.spec.js +10 -16
- package/lib/test/integration/recordings-router.spec.js +16 -16
- package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
- package/lib/test/integration/role-matrix.spec.js +4 -9
- package/lib/test/integration/sdk-leases-router.spec.js +9 -9
- package/lib/test/integration/sdk-version.spec.js +2 -2
- package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
- package/lib/test/integration/team-membership-migration.spec.js +73 -12
- package/lib/test/integration/team-visibility-apps.spec.js +15 -22
- package/lib/test/integration/team-visibility-control.spec.js +96 -69
- package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
- package/lib/test/integration/team-visibility-grid.spec.js +16 -15
- package/lib/test/integration/team-visibility-leases.spec.js +8 -11
- package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
- package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
- package/lib/test/integration/users-flow.spec.js +7 -12
- package/lib/test/integration/users-lockout.spec.js +5 -3
- package/lib/test/unit/EmailService.test.js +66 -0
- package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
- package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
- package/lib/test/unit/ai-engine-settings.spec.js +338 -0
- package/lib/test/unit/ai-provider-switch.spec.js +130 -0
- package/lib/test/unit/ai-rate-limit.spec.js +126 -0
- package/lib/test/unit/android-h264-viewers.spec.js +272 -0
- package/lib/test/unit/android-preview-hold.spec.js +3 -2
- package/lib/test/unit/apikeys-router.spec.js +84 -0
- package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
- package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
- package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
- package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
- package/lib/test/unit/device-log-book.spec.js +180 -0
- package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
- package/lib/test/unit/event-log-session-data.spec.js +244 -0
- package/lib/test/unit/failure-analysis.spec.js +182 -0
- package/lib/test/unit/failure-categories.spec.js +128 -0
- package/lib/test/unit/fingerprint-identity.spec.js +433 -0
- package/lib/test/unit/h264-multiplexer.spec.js +46 -0
- package/lib/test/unit/heal-report.spec.js +71 -0
- package/lib/test/unit/healing-instrumentation.spec.js +6 -1
- package/lib/test/unit/healing-tier-gate.spec.js +73 -29
- package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
- package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
- package/lib/test/unit/lease/lease-override.spec.js +23 -6
- package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
- package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
- package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
- package/lib/test/unit/ocr-offline.spec.js +213 -0
- package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
- package/lib/test/unit/omni-vision.spec.js +5 -7
- package/lib/test/unit/package-engines.spec.js +27 -0
- package/lib/test/unit/passwordResetMode.test.js +10 -2
- package/lib/test/unit/profile-router.test.js +122 -1
- package/lib/test/unit/queue-requester.spec.js +1 -1
- package/lib/test/unit/reset-link-address.spec.js +266 -0
- package/lib/test/unit/reset-link.test.js +10 -4
- package/lib/test/unit/resilio-path-healing.spec.js +270 -0
- package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
- package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
- package/lib/test/unit/selector-verification-job.spec.js +13 -13
- package/lib/test/unit/self-healing-boot.spec.js +10 -5
- package/lib/test/unit/self-healing-setting.spec.js +7 -6
- package/lib/test/unit/session-app-resolution.spec.js +5 -1
- package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
- package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
- package/lib/test/unit/session-credential-strip.spec.js +1 -1
- package/lib/test/unit/session-device-logs.spec.js +347 -0
- package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
- package/lib/test/unit/session-log-indexes.spec.js +64 -3
- package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
- package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
- package/lib/test/unit/token-mint-limits.spec.js +165 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
- package/lib/test/unit/verify-credential.spec.js +33 -0
- package/lib/test/unit/vision-assertion.spec.js +3 -2
- package/lib/test/unit/web-config-service.spec.js +19 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
- package/prisma/schema.prisma +3 -0
- package/schema.json +5 -5
- package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
- package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
- package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
- package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
- package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
- package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
- /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
|
@@ -20,14 +20,17 @@ const plugin_1 = require("../../src/plugin");
|
|
|
20
20
|
const ServerManager_1 = require("../../src/services/ServerManager");
|
|
21
21
|
const PluginContext_1 = require("../../src/PluginContext");
|
|
22
22
|
const SelfHealingSwitch_1 = require("../../src/services/settings/SelfHealingSwitch");
|
|
23
|
+
const aiEngineSettings_1 = require("../../src/services/settings/aiEngineSettings");
|
|
23
24
|
const logger_1 = __importDefault(require("../../src/logger"));
|
|
24
25
|
/**
|
|
25
|
-
* The self-healing value saved on the Settings page
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* The self-healing value saved on the Settings page, and the AI engine page's
|
|
27
|
+
* provider, model and base URL, are read once at boot, so a restart doesn't put
|
|
28
|
+
* the server back on the options it was started with. They have to be loaded
|
|
29
|
+
* once the database is ready (migrations ran), after the startup options were
|
|
30
|
+
* applied (the AI engine's are what a cleared value goes back to), and before
|
|
31
|
+
* the first route can answer or the first command arrive.
|
|
29
32
|
*/
|
|
30
|
-
describe('loading the saved self-healing
|
|
33
|
+
describe('loading the saved self-healing and AI engine settings, at boot', () => {
|
|
31
34
|
let statics;
|
|
32
35
|
beforeEach(() => {
|
|
33
36
|
statics = {
|
|
@@ -57,6 +60,7 @@ describe('loading the saved self-healing setting, at boot', () => {
|
|
|
57
60
|
// The real one kills every go-ios process on the machine.
|
|
58
61
|
sinon_1.default.stub(boot, 'reapLeftoverGoIos').callsFake(step('reap'));
|
|
59
62
|
sinon_1.default.stub(SelfHealingSwitch_1.SelfHealingSwitch.prototype, 'load').callsFake(step('self-healing setting'));
|
|
63
|
+
sinon_1.default.stub(aiEngineSettings_1.AiEngineSettings.prototype, 'load').callsFake(step('AI engine settings'));
|
|
60
64
|
sinon_1.default.stub(boot, 'registerRoutes').callsFake(() => {
|
|
61
65
|
steps.push('routes');
|
|
62
66
|
throw new Error('boot stopped by the test');
|
|
@@ -69,6 +73,7 @@ describe('loading the saved self-healing setting, at boot', () => {
|
|
|
69
73
|
'database and migrations',
|
|
70
74
|
'reap',
|
|
71
75
|
'self-healing setting',
|
|
76
|
+
'AI engine settings',
|
|
72
77
|
'routes',
|
|
73
78
|
]);
|
|
74
79
|
}));
|
|
@@ -203,10 +203,10 @@ describe('the AI self-healing switch', () => {
|
|
|
203
203
|
(0, chai_1.expect)(attemptHealing.calledOnce).to.equal(true);
|
|
204
204
|
}));
|
|
205
205
|
it("still hands a session's own healing tiers to the healing it runs", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
206
|
+
// Read from the session's driver, which every session has: SESSION_MANAGER
|
|
207
|
+
// holds a local one only with the dashboard on, and holds none here.
|
|
208
|
+
const driver = { sessionId: 'sess-6', caps: { 'xe:options': { healingTiers: [1, 2] } } };
|
|
209
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('sess-6')).to.equal(undefined);
|
|
210
210
|
yield typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor)
|
|
211
211
|
.handle(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
212
|
throw noSuchElement();
|
|
@@ -219,9 +219,10 @@ describe('the AI self-healing switch', () => {
|
|
|
219
219
|
let learn;
|
|
220
220
|
beforeEach(() => {
|
|
221
221
|
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
|
|
222
|
-
learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning');
|
|
222
|
+
learn = sinon_1.default.stub(CommandInterceptor_1.CommandInterceptor.prototype, 'triggerLearning').resolves();
|
|
223
223
|
});
|
|
224
|
-
|
|
224
|
+
/** A findElement the driver answers with its element. */
|
|
225
|
+
const afterFind = () => typedi_1.Container.get(CommandInterceptor_1.CommandInterceptor).handle(() => __awaiter(void 0, void 0, void 0, function* () { return ({ ELEMENT: 'el-1' }); }), { sessionId: 'sess-7' }, 'findElement', ['id', 'there', 'sess-7'], pluginArgs, false);
|
|
225
226
|
it('learns the selector while the switch is on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
227
|
yield afterFind();
|
|
227
228
|
(0, chai_1.expect)(learn.calledOnce).to.equal(true);
|
|
@@ -145,7 +145,11 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
145
145
|
return owned.includes('admin') || req.some((s) => owned.includes(s));
|
|
146
146
|
},
|
|
147
147
|
});
|
|
148
|
-
|
|
148
|
+
// Every key's owner is an ACTIVE member: a key names its owner only while
|
|
149
|
+
// they are. The admin key is unscoped by its admin scope.
|
|
150
|
+
typedi_1.Container.set(UserService_1.UserService, {
|
|
151
|
+
findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
|
|
152
|
+
});
|
|
149
153
|
sinon_1.default.stub(app_service_1.APP_SERVICE, 'getAppById').callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = APPS[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
150
154
|
const device = { udid: 'u1', host: 'h1', platform: 'android', nodeId: 'node-hub' };
|
|
151
155
|
allocate = sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').resolves(device);
|
|
@@ -41,10 +41,12 @@ describe('authorizeSessionRequest — identity', () => {
|
|
|
41
41
|
authDisabledBefore = config_1.config.authDisabled;
|
|
42
42
|
config_1.config.authDisabled = false;
|
|
43
43
|
restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// the
|
|
47
|
-
typedi_1.Container.set(UserService_1.UserService, {
|
|
44
|
+
// Every id is an ACTIVE member in no team: a credential names its owner
|
|
45
|
+
// only while they are (session-create-credential-owner.spec.ts covers
|
|
46
|
+
// the other cases). The real lookups would read the developer's database.
|
|
47
|
+
typedi_1.Container.set(UserService_1.UserService, {
|
|
48
|
+
findById: (id) => __awaiter(void 0, void 0, void 0, function* () { return ({ id, role: 'MEMBER', status: 'ACTIVE' }); }),
|
|
49
|
+
});
|
|
48
50
|
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
49
51
|
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
50
52
|
});
|
|
@@ -105,7 +107,7 @@ describe('authorizeSessionRequest — identity', () => {
|
|
|
105
107
|
hasScope: () => false,
|
|
106
108
|
});
|
|
107
109
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
108
|
-
verify: sinon_1.default.stub().resolves({ sub: 'usr_carol' }),
|
|
110
|
+
verify: sinon_1.default.stub().resolves({ sub: 'usr_carol', scopes: 'sessions' }),
|
|
109
111
|
});
|
|
110
112
|
const res = yield invoke(svc, capsWith({ 'xe:options': { sessionToken: 'tok' } }));
|
|
111
113
|
(0, chai_1.expect)(res.apiKeyId).to.equal(null);
|
|
@@ -156,13 +158,14 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
156
158
|
findUser = sinon_1.default.stub().resolves(owner);
|
|
157
159
|
typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
|
|
158
160
|
};
|
|
159
|
-
|
|
161
|
+
// A session token carries the scopes of the credential that minted it.
|
|
162
|
+
const withSessionToken = (owner, teamId = null, scopes = 'sessions') => {
|
|
160
163
|
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
161
164
|
verifyPair: sinon_1.default.stub().resolves(null),
|
|
162
165
|
hasScope: () => false,
|
|
163
166
|
});
|
|
164
167
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, {
|
|
165
|
-
verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId }),
|
|
168
|
+
verify: sinon_1.default.stub().resolves({ sub: 'usr_alice', teamId, scopes }),
|
|
166
169
|
});
|
|
167
170
|
findUser = sinon_1.default.stub().resolves(owner);
|
|
168
171
|
typedi_1.Container.set(UserService_1.UserService, { findById: findUser });
|
|
@@ -208,9 +211,19 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
208
211
|
withKey(keyRow('admin'), user('SUPER_ADMIN', 'INACTIVE'));
|
|
209
212
|
(0, chai_1.expect)((yield access(pairCaps())).canOverride).to.equal(false);
|
|
210
213
|
}));
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
// By the scopes it was minted with, as a key is judged. By its user's role
|
|
215
|
+
// alone, an ADMIN's key without the admin scope minted itself an override.
|
|
216
|
+
it("follows resolveActor's rule on a session token's user's role now", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
withSessionToken(user('ADMIN'), null, 'admin,sessions');
|
|
218
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
|
|
219
|
+
withSessionToken(user('ADMIN'), null, 'sessions');
|
|
220
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
|
|
221
|
+
withSessionToken(user('SUPER_ADMIN'), null, 'admin,sessions');
|
|
222
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
|
|
223
|
+
withSessionToken(user('SUPER_ADMIN'), null, 'sessions');
|
|
213
224
|
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(true);
|
|
225
|
+
withSessionToken(user('MEMBER'), null, 'admin,sessions');
|
|
226
|
+
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
|
|
214
227
|
withSessionToken(user('MEMBER'));
|
|
215
228
|
(0, chai_1.expect)((yield access(tokenCaps())).canOverride).to.equal(false);
|
|
216
229
|
}));
|
|
@@ -223,9 +236,24 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
223
236
|
(0, chai_1.expect)(res.scoped).to.equal(false);
|
|
224
237
|
(0, chai_1.expect)((yield res.leaseAccess()).canOverride).to.equal(false);
|
|
225
238
|
}));
|
|
226
|
-
|
|
239
|
+
// The owner is looked up when the credential is checked: a key names them
|
|
240
|
+
// only while they are ACTIVE, so a lookup that fails refuses the session
|
|
241
|
+
// rather than run it as someone who may have been switched off.
|
|
242
|
+
it('refuses the session when the owner lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
243
|
withKey(keyRow('admin'), null);
|
|
228
244
|
typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
|
|
245
|
+
let error;
|
|
246
|
+
try {
|
|
247
|
+
yield invoke(svc, pairCaps());
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
error = e;
|
|
251
|
+
}
|
|
252
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal('db down');
|
|
253
|
+
}));
|
|
254
|
+
it('is false, and the teams fall back to the shared pool, when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
|
+
withKey(keyRow('sessions'), user('MEMBER'));
|
|
256
|
+
teamRows.rejects(new Error('db down'));
|
|
229
257
|
(0, chai_1.expect)(yield access(pairCaps())).to.deep.equal({ canOverride: false, teamIds: [] });
|
|
230
258
|
}));
|
|
231
259
|
it('is true when auth is disabled, where every caller is an admin', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -331,9 +359,9 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
331
359
|
}
|
|
332
360
|
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("xe:options.team 'team_c' is not allowed for this API key");
|
|
333
361
|
}));
|
|
334
|
-
it('falls back to the shared pool when the lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
335
|
-
withKey(keyRow('sessions'),
|
|
336
|
-
|
|
362
|
+
it('falls back to the shared pool when the team lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
363
|
+
withKey(keyRow('sessions'), user('MEMBER'));
|
|
364
|
+
teamRows.rejects(new Error('db down'));
|
|
337
365
|
(0, chai_1.expect)(yield scope(pairCaps())).to.deep.equal({ callerTeamIds: [], scoped: true });
|
|
338
366
|
}));
|
|
339
367
|
it('stays unscoped, with no lookup, for a session with no credentials', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
23
|
+
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
24
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
25
|
+
const UserService_1 = require("../../src/services/UserService");
|
|
26
|
+
const config_1 = require("../../src/config");
|
|
27
|
+
const prisma_1 = require("../../src/prisma");
|
|
28
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
29
|
+
const sessionCredentials_1 = require("../../src/services/session/sessionCredentials");
|
|
30
|
+
/**
|
|
31
|
+
* Session create judges the credentials it is given by REST's rule: a key
|
|
32
|
+
* pair or session token says who the caller is only while that user exists
|
|
33
|
+
* and is ACTIVE. Through 2.14 the pair was checked against the key alone
|
|
34
|
+
* (ApiKeyService.verifyPair) and the session token against its signature
|
|
35
|
+
* alone, so a user set to Inactive, or deleted, kept creating sessions as
|
|
36
|
+
* themselves, past XENON_REQUIRE_SESSION_TOKEN too.
|
|
37
|
+
*
|
|
38
|
+
* Such credentials now count as wrong ones: with the gate on the session is
|
|
39
|
+
* refused, with it off it runs with no owner, like any session presenting
|
|
40
|
+
* credentials that don't check out.
|
|
41
|
+
*
|
|
42
|
+
* A session token also has to carry the `sessions` scope, as a key pair
|
|
43
|
+
* must: POST /auth/token gave one to any credential, a read-only one
|
|
44
|
+
* included.
|
|
45
|
+
*/
|
|
46
|
+
// authorizeSessionRequest is private: the single place a create's identity is
|
|
47
|
+
// decided. It is handed the credentials createSession took out of the caps.
|
|
48
|
+
const invoke = (svc, caps) => svc.authorizeSessionRequest(caps, (0, sessionCredentials_1.takeSessionCredentials)(caps));
|
|
49
|
+
const capsWith = (options) => ({
|
|
50
|
+
alwaysMatch: { 'xe:options': options },
|
|
51
|
+
firstMatch: [{}],
|
|
52
|
+
});
|
|
53
|
+
function refusalOf(promise) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _a;
|
|
56
|
+
try {
|
|
57
|
+
yield promise;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return String((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
describe('session create: the credentials of an inactive or deleted user', () => {
|
|
66
|
+
let svc;
|
|
67
|
+
let dir;
|
|
68
|
+
let keys;
|
|
69
|
+
let restore;
|
|
70
|
+
let authDisabledBefore;
|
|
71
|
+
let gateBefore;
|
|
72
|
+
let users;
|
|
73
|
+
let findById;
|
|
74
|
+
const keyRow = { id: 'key_alice', userId: 'usr_alice', scopes: 'sessions', teamId: null };
|
|
75
|
+
const alice = () => users.usr_alice;
|
|
76
|
+
const pair = () => capsWith({ accessKey: 'xen_alice', token: 'tk' });
|
|
77
|
+
const token = (claims = { sub: 'usr_alice', scopes: 'sessions' }) => keys.sign(claims, { audience: 'xenon-session', ttlSeconds: 600 });
|
|
78
|
+
const gate = (on) => {
|
|
79
|
+
if (on)
|
|
80
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = 'true';
|
|
81
|
+
else
|
|
82
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
83
|
+
};
|
|
84
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
86
|
+
config_1.config.authDisabled = false;
|
|
87
|
+
gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
88
|
+
restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
|
|
89
|
+
dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-create-owner-'));
|
|
90
|
+
keys = new JwtKeyService_1.JwtKeyService();
|
|
91
|
+
yield keys.init(dir);
|
|
92
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, keys);
|
|
93
|
+
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
94
|
+
verifyPair: sinon_1.default.stub().resolves(keyRow),
|
|
95
|
+
hasScope: (row, req) => {
|
|
96
|
+
const owned = row.scopes.split(',');
|
|
97
|
+
return owned.includes('admin') || req.some((s) => owned.includes(s));
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
users = { usr_alice: { id: 'usr_alice', role: 'MEMBER', status: 'ACTIVE' } };
|
|
101
|
+
findById = sinon_1.default.stub().callsFake((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = users[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
102
|
+
typedi_1.Container.set(UserService_1.UserService, { findById });
|
|
103
|
+
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
104
|
+
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
105
|
+
}));
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
108
|
+
if (gateBefore === undefined)
|
|
109
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
110
|
+
else
|
|
111
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
|
|
112
|
+
sinon_1.default.restore();
|
|
113
|
+
restore();
|
|
114
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
|
|
117
|
+
beforeEach(() => gate(true));
|
|
118
|
+
it("refuses an Inactive user's access key and token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
alice().status = 'INACTIVE';
|
|
120
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
|
|
121
|
+
}));
|
|
122
|
+
it("refuses a deleted user's key, whose row outlived them", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
123
|
+
users.usr_alice = undefined;
|
|
124
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, pair()))).to.match(/session rejected/);
|
|
125
|
+
}));
|
|
126
|
+
it("refuses an Inactive user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
|
+
alice().status = 'INACTIVE';
|
|
128
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
129
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
|
|
130
|
+
}));
|
|
131
|
+
it("refuses a deleted user's session token, which is still within its lifetime", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
users.usr_alice = undefined;
|
|
133
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
134
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/session rejected/);
|
|
135
|
+
}));
|
|
136
|
+
it("still admits an Active user's key and token, and their session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
const byPair = yield invoke(svc, pair());
|
|
138
|
+
(0, chai_1.expect)(byPair).to.include({ apiKeyId: 'key_alice', userId: 'usr_alice' });
|
|
139
|
+
const byToken = yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
140
|
+
(0, chai_1.expect)(byToken).to.include({ apiKeyId: null, userId: 'usr_alice' });
|
|
141
|
+
}));
|
|
142
|
+
it('looks the owner up once, though the gate and attribution both verify the token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
|
+
yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
144
|
+
(0, chai_1.expect)(findById.callCount).to.equal(1);
|
|
145
|
+
}));
|
|
146
|
+
});
|
|
147
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN off', () => {
|
|
148
|
+
beforeEach(() => gate(false));
|
|
149
|
+
it("gives no owner to a session made with an Inactive user's key", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
alice().status = 'INACTIVE';
|
|
151
|
+
const res = yield invoke(svc, pair());
|
|
152
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
153
|
+
}));
|
|
154
|
+
it("gives no owner to a session made with a deleted user's session token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
users.usr_alice = undefined;
|
|
156
|
+
const res = yield invoke(svc, capsWith({ sessionToken: yield token() }));
|
|
157
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
158
|
+
}));
|
|
159
|
+
it('looks an Active owner up once for a key and token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
const res = yield invoke(svc, pair());
|
|
161
|
+
yield res.leaseAccess();
|
|
162
|
+
(0, chai_1.expect)(res.userId).to.equal('usr_alice');
|
|
163
|
+
(0, chai_1.expect)(findById.callCount).to.equal(1);
|
|
164
|
+
}));
|
|
165
|
+
});
|
|
166
|
+
describe('a session token needs the `sessions` scope', () => {
|
|
167
|
+
for (const on of [false, true]) {
|
|
168
|
+
describe(`(gate ${on ? 'on' : 'off'})`, () => {
|
|
169
|
+
beforeEach(() => gate(on));
|
|
170
|
+
it('refuses one minted for a read-only credential', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
171
|
+
const caps = capsWith({
|
|
172
|
+
sessionToken: yield token({ sub: 'usr_alice', scopes: 'devices' }),
|
|
173
|
+
});
|
|
174
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.match(/sessions` scope/);
|
|
175
|
+
}));
|
|
176
|
+
it('refuses one minted before session tokens carried their scopes, saying so', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
177
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice' }) });
|
|
178
|
+
const refusal = yield refusalOf(invoke(svc, caps));
|
|
179
|
+
(0, chai_1.expect)(refusal).to.match(/carries no `sessions` scope/);
|
|
180
|
+
(0, chai_1.expect)(refusal).to.match(/minted by Xenon 2\.14 or earlier carry none/);
|
|
181
|
+
(0, chai_1.expect)(refusal).to.match(/mint a new one/);
|
|
182
|
+
}));
|
|
183
|
+
it('admits one with `sessions` or `admin`', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
for (const scopes of ['sessions', 'admin', 'devices,sessions']) {
|
|
185
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
|
|
186
|
+
(0, chai_1.expect)((yield invoke(svc, caps)).userId).to.equal('usr_alice');
|
|
187
|
+
}
|
|
188
|
+
}));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Taking over a lease someone else created is an ownership override: a
|
|
193
|
+
// SUPER_ADMIN, or a credential with the `admin` scope. A session token was
|
|
194
|
+
// judged by its user's role alone, so an ADMIN's key without the admin
|
|
195
|
+
// scope, which may not override, minted a session token that could.
|
|
196
|
+
describe('a session token overrides a lease only as the credential that minted it could', () => {
|
|
197
|
+
beforeEach(() => gate(false));
|
|
198
|
+
const overrideWith = (role, scopes) => __awaiter(void 0, void 0, void 0, function* () {
|
|
199
|
+
alice().role = role;
|
|
200
|
+
const caps = capsWith({ sessionToken: yield token({ sub: 'usr_alice', scopes }) });
|
|
201
|
+
return (yield (yield invoke(svc, caps)).leaseAccess()).canOverride;
|
|
202
|
+
});
|
|
203
|
+
it("is refused for an ADMIN's token minted without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
204
|
+
(0, chai_1.expect)(yield overrideWith('ADMIN', 'sessions')).to.equal(false);
|
|
205
|
+
}));
|
|
206
|
+
it("is allowed for an ADMIN's or SUPER_ADMIN's token minted with the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
207
|
+
(0, chai_1.expect)(yield overrideWith('ADMIN', 'admin,sessions')).to.equal(true);
|
|
208
|
+
(0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'admin,sessions')).to.equal(true);
|
|
209
|
+
}));
|
|
210
|
+
// As the SUPER_ADMIN's key pair and Bearer token override.
|
|
211
|
+
it("is allowed for a SUPER_ADMIN's token minted without it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
212
|
+
(0, chai_1.expect)(yield overrideWith('SUPER_ADMIN', 'sessions')).to.equal(true);
|
|
213
|
+
}));
|
|
214
|
+
// The scope is fixed when the token is minted; the role is read now. An
|
|
215
|
+
// admin demoted after minting overrides no more.
|
|
216
|
+
it('is refused once the admin who minted it is a member', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
(0, chai_1.expect)(yield overrideWith('MEMBER', 'admin,sessions')).to.equal(false);
|
|
218
|
+
}));
|
|
219
|
+
it("is refused for a member's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
220
|
+
(0, chai_1.expect)(yield overrideWith('MEMBER', 'sessions')).to.equal(false);
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
// The owner lookup that fails is not a token that doesn't check out: the
|
|
224
|
+
// create is refused, as for a key whose owner can't be looked up. It ran
|
|
225
|
+
// with no owner (gate off) or was refused as "invalid or expired" (gate on).
|
|
226
|
+
describe('a session token whose owner cannot be looked up', () => {
|
|
227
|
+
for (const on of [false, true]) {
|
|
228
|
+
it(`refuses the create (gate ${on ? 'on' : 'off'})`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
229
|
+
gate(on);
|
|
230
|
+
findById.rejects(new Error('db down'));
|
|
231
|
+
const caps = capsWith({ sessionToken: yield token() });
|
|
232
|
+
(0, chai_1.expect)(yield refusalOf(invoke(svc, caps))).to.equal('db down');
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
it('still counts a token that does not check out as none (gate off)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
236
|
+
gate(false);
|
|
237
|
+
const res = yield invoke(svc, capsWith({ sessionToken: 'not-a-jwt' }));
|
|
238
|
+
(0, chai_1.expect)(res).to.include({ apiKeyId: null, userId: null });
|
|
239
|
+
(0, chai_1.expect)(findById.called).to.equal(false);
|
|
240
|
+
}));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -146,7 +146,7 @@ describe('createSession — credentials never reach the driver or storage', () =
|
|
|
146
146
|
verify: (t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
147
|
if (!t.startsWith('jwt:'))
|
|
148
148
|
throw new Error('bad token');
|
|
149
|
-
return { sub: t.slice('jwt:'.length), teamId: null };
|
|
149
|
+
return { sub: t.slice('jwt:'.length), teamId: null, scopes: 'sessions' };
|
|
150
150
|
}),
|
|
151
151
|
});
|
|
152
152
|
// The owner's teams: none, and not read from the developer's database.
|