ultravisor 1.3.16 → 1.3.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultravisor",
3
- "version": "1.3.16",
3
+ "version": "1.3.17",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -947,41 +947,92 @@ class UltravisorAPIServer extends libPictService
947
947
  return { Admitted: false };
948
948
  }
949
949
 
950
- _requireSession(pRequest, pResponse, fNext)
950
+ /**
951
+ * True when this UV is running secured (non-promiscuous) and therefore
952
+ * must require credentials on the protected surface.
953
+ *
954
+ * The authoritative signal is the operator's explicit
955
+ * `UltravisorNonPromiscuous` config flag — the SAME switch that already
956
+ * gates beacon-join admission (_admitBeaconForRegistration). That flag,
957
+ * not the incidental presence of an auth-beacon, is what declares the
958
+ * mesh secured: with it set we fail closed immediately, including during
959
+ * the bootstrap window before the auth-beacon has connected.
960
+ *
961
+ * A connected auth-beacon is also honoured as a safety net so the
962
+ * protected surface is never served unauthenticated merely because the
963
+ * flag was missed — enforcement is thus never weaker than the flag
964
+ * alone. (If you want the flag to be the *sole* signal, drop the bridge
965
+ * check below.)
966
+ */
967
+ _isSecuredMode()
968
+ {
969
+ let tmpConfig = (this.fable && this.fable.ProgramConfiguration) || {};
970
+ if (tmpConfig.UltravisorNonPromiscuous)
971
+ {
972
+ return true;
973
+ }
974
+ let tmpBridge = this._getService('UltravisorAuthBeaconBridge');
975
+ return !!(tmpBridge
976
+ && typeof tmpBridge.isAvailable === 'function'
977
+ && tmpBridge.isAvailable());
978
+ }
979
+
980
+ /**
981
+ * Resolve the principal for a request WITHOUT emitting any response.
982
+ *
983
+ * Returns:
984
+ * - a real session object when a valid cookie/token is presented,
985
+ * - a synthetic anonymous session (Anonymous: true) when this UV is
986
+ * running promiscuously (not secured — see _isSecuredMode()),
987
+ * - null when this UV is secured (non-promiscuous) and no valid session
988
+ * was presented (the caller decides how to reject).
989
+ *
990
+ * This is the single source of truth for "who is this request?" -- both
991
+ * the per-route _requireSession() guard and the global
992
+ * _enforceAuthentication() gate flow through it, so the two can never
993
+ * drift. The resolved value is memoized on the request so a request
994
+ * that passes the gate and then hits a route which also calls
995
+ * _requireSession() resolves the session only once.
996
+ */
997
+ _resolveSession(pRequest)
951
998
  {
952
- // `_OratorAuth` is wired unconditionally during wireEndpoints
953
- // startup, so the legacy `if (!this._OratorAuth)` escape hatch
954
- // is dead code in practice -- but it's preserved for any
955
- // theoretical pre-init request. Treat it the same as the
956
- // "no auth provider connected" path: hand back an anonymous
957
- // session rather than 401.
958
- if (!this._OratorAuth)
999
+ if (pRequest && pRequest._UltravisorSession)
959
1000
  {
960
- return this._anonymousSession();
1001
+ return pRequest._UltravisorSession;
961
1002
  }
962
1003
 
963
- let tmpSession = this._OratorAuth.getSessionForRequest(pRequest);
1004
+ // Resolve the orator-auth session (cookie/token) if one is present.
1005
+ // `_OratorAuth` is wired unconditionally during startup; the guard
1006
+ // is preserved only for a theoretical pre-init request.
1007
+ let tmpResolved = null;
1008
+ let tmpSession = this._OratorAuth ? this._OratorAuth.getSessionForRequest(pRequest) : null;
964
1009
  if (tmpSession)
965
1010
  {
966
- return tmpSession;
1011
+ tmpResolved = tmpSession;
1012
+ }
1013
+ else
1014
+ {
1015
+ // No valid session. In secured (non-promiscuous) mode a missing
1016
+ // session is a hard failure -- return null so the caller 401s,
1017
+ // failing closed even before an auth-beacon has connected. In
1018
+ // promiscuous mode, synthesize an anonymous session so a
1019
+ // beacon-less UV stays genuinely open.
1020
+ tmpResolved = this._isSecuredMode() ? null : this._anonymousSession();
967
1021
  }
968
1022
 
969
- // No valid cookie. If no auth-beacon currently advertises the
970
- // `Authentication` capability, there is no provider that could
971
- // ever issue a session against this UV -- 401-ing would be a
972
- // permanent block on every session-gated route. Synthesize an
973
- // anonymous session instead, so a UV running without an auth-
974
- // beacon is genuinely promiscuous. When an auth-beacon IS
975
- // connected, fall through to 401 so the operator knows login
976
- // is possible. Routes that need real auth should explicitly
977
- // check `session.Anonymous` and reject when appropriate.
978
- let tmpBridge = this._getService('UltravisorAuthBeaconBridge');
979
- let tmpAuthProviderAvailable = tmpBridge
980
- && typeof tmpBridge.isAvailable === 'function'
981
- && tmpBridge.isAvailable();
982
- if (!tmpAuthProviderAvailable)
1023
+ if (pRequest && tmpResolved)
1024
+ {
1025
+ pRequest._UltravisorSession = tmpResolved;
1026
+ }
1027
+ return tmpResolved;
1028
+ }
1029
+
1030
+ _requireSession(pRequest, pResponse, fNext)
1031
+ {
1032
+ let tmpSession = this._resolveSession(pRequest);
1033
+ if (tmpSession)
983
1034
  {
984
- return this._anonymousSession();
1035
+ return tmpSession;
985
1036
  }
986
1037
 
987
1038
  pResponse.send(401, { Error: 'Authentication required.', LoggedIn: false });
@@ -1006,6 +1057,96 @@ class UltravisorAPIServer extends libPictService
1006
1057
  };
1007
1058
  }
1008
1059
 
1060
+ /**
1061
+ * Global authentication gate. Registered as an Orator/restify `use`
1062
+ * handler so it runs after routing but before every route's own
1063
+ * handlers, across the entire HTTP surface of this server.
1064
+ *
1065
+ * This is the server-side half of "enforce login when not promiscuous".
1066
+ * Historically only the ~19 beacon worker-protocol routes called
1067
+ * _requireSession(); the whole human-facing management/API surface
1068
+ * (GET /Beacon, /Operation, /Schedule, /Manifest, /Timeline, /Fleet and
1069
+ * their writes) was reachable by any unauthenticated client, with the
1070
+ * web UI as the only gate. This gate closes that.
1071
+ *
1072
+ * Mode-aware, mirroring _resolveSession() / _isSecuredMode():
1073
+ * - Promiscuous (not secured): every request resolves to an anonymous
1074
+ * session and passes -- behaviour identical to before.
1075
+ * - Secured (UltravisorNonPromiscuous set, or an auth-beacon connected):
1076
+ * a valid session is required for everything except the exempt routes
1077
+ * (see _isAuthExemptRoute); anything else gets a 401. Beacons
1078
+ * register and authenticate over WebSocket (which bypasses this HTTP
1079
+ * gate), and their HTTP worker routes already required a session in
1080
+ * secured mode -- so the mesh is unaffected.
1081
+ */
1082
+ _enforceAuthentication(pRequest, pResponse, fNext)
1083
+ {
1084
+ if (this._isAuthExemptRoute(pRequest))
1085
+ {
1086
+ return fNext();
1087
+ }
1088
+
1089
+ // Promiscuous -> anonymous session (passes); authenticated with a
1090
+ // valid session -> passes; authenticated without one -> null.
1091
+ let tmpSession = this._resolveSession(pRequest);
1092
+ if (tmpSession)
1093
+ {
1094
+ return fNext();
1095
+ }
1096
+
1097
+ // Authenticated mode, no valid session: reject and stop the chain.
1098
+ // next(false) short-circuits restify's use-chain so the matched
1099
+ // route handler never runs.
1100
+ pResponse.send(401, { Error: 'Authentication required.', LoggedIn: false });
1101
+ return fNext(false);
1102
+ }
1103
+
1104
+ /**
1105
+ * Routes that must stay reachable without a session even in
1106
+ * authenticated mode -- otherwise the UV would be unusable (you could
1107
+ * never load the login page or sign in). Decided from the routed path
1108
+ * pattern (pRequest.route.path) and the raw request URL:
1109
+ *
1110
+ * - the static web UI ('/*') and the root redirect ('/') -- the
1111
+ * browser must fetch these to render the login screen at all;
1112
+ * - GET /status and /package -- the UI reads these before login to
1113
+ * discover the auth mode (and thus whether to show the login screen);
1114
+ * - POST /Beacon/BootstrapAdmin -- the one-time first-admin bootstrap,
1115
+ * the chicken-and-egg path that necessarily precedes any session;
1116
+ * - the orator-auth namespace ('/1.0/...') -- login, session check,
1117
+ * logout, and the admin-gated /Users CRUD, all of which enforce
1118
+ * their own session/IsAdmin checks downstream.
1119
+ */
1120
+ _isAuthExemptRoute(pRequest)
1121
+ {
1122
+ let tmpRoutePath = (pRequest && pRequest.route && pRequest.route.path) ? pRequest.route.path : '';
1123
+
1124
+ // Static web UI catch-all + root redirect.
1125
+ if (tmpRoutePath === '/*' || tmpRoutePath === '/')
1126
+ {
1127
+ return true;
1128
+ }
1129
+
1130
+ // Bootstrap metadata the UI reads before any session exists.
1131
+ if (tmpRoutePath === '/status' || tmpRoutePath === '/package' || tmpRoutePath === '/Beacon/BootstrapAdmin')
1132
+ {
1133
+ return true;
1134
+ }
1135
+
1136
+ // orator-auth namespace (login / session / user management). Match
1137
+ // the raw URL prefix too, since these routes are mounted by
1138
+ // orator-auth and we don't want to depend solely on route.path.
1139
+ let tmpUrlPath = (pRequest && pRequest.url) ? pRequest.url : '';
1140
+ let tmpQueryAt = tmpUrlPath.indexOf('?');
1141
+ if (tmpQueryAt >= 0) { tmpUrlPath = tmpUrlPath.slice(0, tmpQueryAt); }
1142
+ if (tmpRoutePath.indexOf('/1.0/') === 0 || tmpUrlPath.indexOf('/1.0/') === 0)
1143
+ {
1144
+ return true;
1145
+ }
1146
+
1147
+ return false;
1148
+ }
1149
+
1009
1150
  wireEndpoints(fCallback)
1010
1151
  {
1011
1152
  if (!this._Orator)
@@ -1030,28 +1171,26 @@ class UltravisorAPIServer extends libPictService
1030
1171
  function (pRequest, pResponse, fNext)
1031
1172
  {
1032
1173
  let tmpHypervisor = this._getService('UltravisorHypervisor');
1033
- // AuthEnabled tracks whether an auth-beacon is
1034
- // currently advertising the Authentication
1035
- // capability. When false the UV runs in
1036
- // promiscuous mode -- session-gated routes
1037
- // synthesize an anonymous session via
1038
- // _requireSession's anonymous-fallback path.
1039
- // UI components surface this so operators don't
1040
- // have to infer it from "no auth-beacon connected".
1174
+ // AuthEnabled / AuthMode track whether this UV is secured
1175
+ // (non-promiscuous). Driven by _isSecuredMode() — the
1176
+ // UltravisorNonPromiscuous flag, with a connected
1177
+ // auth-beacon as a safety net — so the UI's login screen
1178
+ // and the server's HTTP auth gate always agree on whether
1179
+ // credentials are required.
1041
1180
  let tmpBridge = this._getService('UltravisorAuthBeaconBridge');
1042
- let tmpAuthEnabled = tmpBridge
1181
+ let tmpSecured = this._isSecuredMode();
1182
+ // SupportsUserManagement: derived from the auth beacon's
1183
+ // `UserManagement` tag advertised at registration
1184
+ // ('internal' or 'external'). Missing tag defaults to
1185
+ // 'internal' for back-compat. Independent of the secured
1186
+ // flag — it tracks whether an auth-beacon is actually
1187
+ // connected to manage users against, so the UI hides admin
1188
+ // views when there is no beacon.
1189
+ let tmpAuthBeaconPresent = tmpBridge
1043
1190
  && typeof tmpBridge.isAvailable === 'function'
1044
1191
  && tmpBridge.isAvailable();
1045
- // SupportsUserManagement: derived from the auth
1046
- // beacon's `UserManagement` tag advertised at
1047
- // registration ('internal' or 'external'). Missing
1048
- // tag defaults to 'internal' for back-compat with
1049
- // older auth beacons that didn't stamp it. In
1050
- // promiscuous mode the value is reported as `false`
1051
- // because there's no auth backend to manage users
1052
- // against — the UI hides admin views accordingly.
1053
1192
  let tmpSupportsUM = false;
1054
- if (tmpAuthEnabled && typeof tmpBridge.getAuthBeaconTags === 'function')
1193
+ if (tmpAuthBeaconPresent && typeof tmpBridge.getAuthBeaconTags === 'function')
1055
1194
  {
1056
1195
  let tmpTags = tmpBridge.getAuthBeaconTags();
1057
1196
  let tmpTag = (tmpTags && tmpTags.UserManagement) || 'internal';
@@ -1061,8 +1200,8 @@ class UltravisorAPIServer extends libPictService
1061
1200
  Status: 'Running',
1062
1201
  ScheduleEntries: tmpHypervisor ? tmpHypervisor.getSchedule().length : 0,
1063
1202
  ScheduleRunning: tmpHypervisor ? tmpHypervisor._Running : false,
1064
- AuthEnabled: !!tmpAuthEnabled,
1065
- AuthMode: tmpAuthEnabled ? 'authenticated' : 'promiscuous',
1203
+ AuthEnabled: !!tmpSecured,
1204
+ AuthMode: tmpSecured ? 'authenticated' : 'promiscuous',
1066
1205
  SupportsUserManagement: tmpSupportsUM
1067
1206
  });
1068
1207
  return fNext();
@@ -3977,6 +4116,28 @@ class UltravisorAPIServer extends libPictService
3977
4116
  return fNext();
3978
4117
  }.bind(this));
3979
4118
 
4119
+ // Global authentication gate — see _enforceAuthentication(). Registered
4120
+ // as a `use` handler so it covers every routed request (restify's
4121
+ // use-chain is consulted at request time, independent of route
4122
+ // registration order). The handler reads `_OratorAuth` lazily at
4123
+ // request time, so installing it here — before that provider is
4124
+ // instantiated in the next step — is safe: nothing is served until
4125
+ // listen().
4126
+ tmpAnticipate.anticipate(
4127
+ function (fNext)
4128
+ {
4129
+ try
4130
+ {
4131
+ this._OratorServer.server.use(this._enforceAuthentication.bind(this));
4132
+ this.log.info('Ultravisor: authentication gate installed (enforced when an auth-beacon is connected; promiscuous otherwise).');
4133
+ }
4134
+ catch (pErr)
4135
+ {
4136
+ this.log.error('Ultravisor: failed to install authentication gate: ' + (pErr && pErr.message));
4137
+ }
4138
+ return fNext();
4139
+ }.bind(this));
4140
+
3980
4141
  tmpAnticipate.anticipate(
3981
4142
  function (fNext)
3982
4143
  {
@@ -4128,6 +4289,49 @@ class UltravisorAPIServer extends libPictService
4128
4289
  // WebSocket Server for Execution Events
4129
4290
  // ====================================================================
4130
4291
 
4292
+ /**
4293
+ * Whether a WebSocket connection may subscribe to consumer event streams
4294
+ * (execution + queue). Those streams expose operational data, so in
4295
+ * secured (non-promiscuous) mode they require a valid session — captured
4296
+ * from the cookie/token on the HTTP upgrade (pWebSocket._Authenticated).
4297
+ * In promiscuous mode every connection may subscribe (unchanged).
4298
+ *
4299
+ * This is the WebSocket half of the HTTP auth gate, keyed off the same
4300
+ * _isSecuredMode(). Beacons are unaffected: they never send
4301
+ * Subscribe/QueueSubscribe — they authenticate at the frame level
4302
+ * (BeaconRegister + join-secret admission), which this does not touch.
4303
+ */
4304
+ _wsConsumerAuthorized(pWebSocket)
4305
+ {
4306
+ if (!this._isSecuredMode())
4307
+ {
4308
+ return true;
4309
+ }
4310
+ return !!(pWebSocket && pWebSocket._Authenticated);
4311
+ }
4312
+
4313
+ /**
4314
+ * Reject an unauthenticated consumer subscription: emit an auth-required
4315
+ * control frame on the requested stream ('execution' | 'queue'), then
4316
+ * close the socket with 1008 (policy violation) — the WS analogue of the
4317
+ * HTTP gate's 401.
4318
+ */
4319
+ _rejectWSUnauthenticatedSubscription(pWebSocket, pStream)
4320
+ {
4321
+ try
4322
+ {
4323
+ pWebSocket.send(JSON.stringify(
4324
+ {
4325
+ EventType: (pStream === 'queue') ? 'queue.auth_required' : 'execution.auth_required',
4326
+ Error: 'Authentication required.',
4327
+ LoggedIn: false
4328
+ }));
4329
+ }
4330
+ catch (pErr) { /* socket already dying */ }
4331
+ try { pWebSocket.close(1008, 'Authentication required'); }
4332
+ catch (pErr) { /* ignore */ }
4333
+ }
4334
+
4131
4335
  /**
4132
4336
  * Initialize a WebSocket server on the same HTTP server used by Restify.
4133
4337
  * Clients connect and subscribe to a RunHash to receive real-time
@@ -4187,6 +4391,17 @@ class UltravisorAPIServer extends libPictService
4187
4391
  }
4188
4392
  catch (pErr) { /* best-effort — never block the upgrade on auth lookup */ }
4189
4393
  pWebSocket._UpgradeSessionID = (tmpUpgradeSession && tmpUpgradeSession.SessionID) || null;
4394
+ // Pin whether a *valid* session (real cookie/token, not
4395
+ // anonymous) was presented on the upgrade. The WS
4396
+ // consumer gate (_wsConsumerAuthorized) uses this to
4397
+ // require credentials for event-stream subscriptions when
4398
+ // the UV is secured. We never block the upgrade itself —
4399
+ // beacons authenticate at the frame level (BeaconRegister
4400
+ // + join-secret admission), so gating must be per-frame.
4401
+ pWebSocket._Authenticated = !!(tmpUpgradeSession
4402
+ && tmpUpgradeSession.SessionID
4403
+ && tmpUpgradeSession.SessionID !== 'anonymous'
4404
+ && !tmpUpgradeSession.Anonymous);
4190
4405
  this._WebSocketServer.emit('connection', pWebSocket, pRequest);
4191
4406
  }.bind(this));
4192
4407
  }.bind(this));
@@ -4232,6 +4447,14 @@ class UltravisorAPIServer extends libPictService
4232
4447
 
4233
4448
  if (tmpData.Action === 'Subscribe' && tmpData.RunHash)
4234
4449
  {
4450
+ // Auth gate: in secured mode an execution-event
4451
+ // subscription requires a valid session on the
4452
+ // connection. Beacons never send this frame.
4453
+ if (!this._wsConsumerAuthorized(pWebSocket))
4454
+ {
4455
+ this._rejectWSUnauthenticatedSubscription(pWebSocket, 'execution');
4456
+ return;
4457
+ }
4235
4458
  // --- Execution event subscription (web UI) ---
4236
4459
  //
4237
4460
  // Mirrors the QueueSubscribe resync protocol:
@@ -4326,6 +4549,14 @@ class UltravisorAPIServer extends libPictService
4326
4549
  }
4327
4550
  else if (tmpData.Action === 'QueueSubscribe')
4328
4551
  {
4552
+ // Auth gate: in secured mode a queue-event
4553
+ // subscription requires a valid session on the
4554
+ // connection. Beacons never send this frame.
4555
+ if (!this._wsConsumerAuthorized(pWebSocket))
4556
+ {
4557
+ this._rejectWSUnauthenticatedSubscription(pWebSocket, 'queue');
4558
+ return;
4559
+ }
4329
4560
  // QueueSubscribe doubles as a resync request:
4330
4561
  // callers that pass LastEventGUID get a
4331
4562
  // replay block (replay_begin / events after