@tellescope/sdk 1.255.18 → 1.255.19
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/lib/cjs/sdk.d.ts +7 -2
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js +18 -8
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js +203 -72
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js +102 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js +84 -81
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +773 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +215 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1200 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2330 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js +204 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +211 -178
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +7 -2
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js +18 -8
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.js +205 -74
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js +98 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
- package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js +84 -81
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +766 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +211 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1193 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2323 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js +200 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +211 -178
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +4 -0
- package/src/tests/api_tests/auto_merge_form_submission.test.ts +13 -4
- package/src/tests/api_tests/healthie_multi_integration.test.ts +103 -5
- package/src/tests/api_tests/phone_calls_conference_hold.test.ts +62 -0
- package/src/tests/api_tests/scoped_api_keys.test.ts +5 -0
- package/src/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.ts +38 -4
- package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +506 -0
- package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +125 -0
- package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +753 -0
- package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +1707 -0
- package/src/tests/api_tests/tickets_bulk_assign_care_team.test.ts +132 -0
- package/src/tests/tests.ts +20 -1
- package/test_generated.pdf +0 -0
- package/.env +0 -13
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.allowed_paths_confinement_tests = void 0;
|
|
54
|
+
require('source-map-support').install();
|
|
55
|
+
var axios_1 = __importDefault(require("axios"));
|
|
56
|
+
var sdk_1 = require("../../../sdk");
|
|
57
|
+
var testing_1 = require("@tellescope/testing");
|
|
58
|
+
var setup_1 = require("../../setup");
|
|
59
|
+
var host = process.env.API_URL || 'http://localhost:8080';
|
|
60
|
+
/**
|
|
61
|
+
* Regression test for F-0166 — allowedPaths wildcard disables path confinement entirely.
|
|
62
|
+
*
|
|
63
|
+
* Several public, unauthenticated endpoints mint a JWT confined to a small set of API paths via an
|
|
64
|
+
* `allowedPaths` claim. The enforcement predicate (api/modules/authentication.ts checkAccess)
|
|
65
|
+
* contained:
|
|
66
|
+
*
|
|
67
|
+
* || (p.endsWith('*') && req.path.split('/')[0] === p.split('/')[0])
|
|
68
|
+
*
|
|
69
|
+
* Every request path begins with '/', so req.path.split('/')[0] is ''. Every wildcard entry also
|
|
70
|
+
* begins with '/', so p.split('/')[0] is ''. The clause reduces to '' === '' — always true. Any
|
|
71
|
+
* allowedPaths array containing a single '*' entry therefore matched EVERY request path and the
|
|
72
|
+
* 403 never fired.
|
|
73
|
+
*
|
|
74
|
+
* Two mint sites contain `/meeting/*`, and both are public and unauthenticated:
|
|
75
|
+
* - session_for_join_link — enduser session, MFA bypassed
|
|
76
|
+
* - session_for_start_link — full STAFF session with mfa: false
|
|
77
|
+
*
|
|
78
|
+
* So an unauthenticated caller holding a link token had an effectively unrestricted session.
|
|
79
|
+
*
|
|
80
|
+
* Fixing that exposed a second defect in the same primitive: entries matched by bare string prefix
|
|
81
|
+
* with no notion of HTTP method, so '/users' and '/endusers' were collection-NAMESPACE grants (22
|
|
82
|
+
* and 21 routes each). The worst consequence was a full escape — PATCH /users/consent called
|
|
83
|
+
* login_user on a freshly-read DB record with no options, minting a token with no allowedPaths at
|
|
84
|
+
* all, so two requests turned a start link into an unconfined staff session.
|
|
85
|
+
*
|
|
86
|
+
* Fix, in two parts:
|
|
87
|
+
* - entries are now method-scoped and exact ('get:/endusers'), with ':param' for segments not
|
|
88
|
+
* knowable at mint time ('get:/meeting/:id'). Un-prefixed and '*' entries remain honoured only
|
|
89
|
+
* so tokens minted before this grammar keep working until they expire.
|
|
90
|
+
* - consent / configure_inbox / switch_account forward req.session.allowedPaths into login_user.
|
|
91
|
+
*
|
|
92
|
+
* Scenarios:
|
|
93
|
+
* S1 public-form session — no wildcard; correctly confined throughout (regression anchor)
|
|
94
|
+
* S2 join-link session — RED before the wildcard fix
|
|
95
|
+
* S3 start-link session — RED before the wildcard fix (staff session; the severe case)
|
|
96
|
+
* S4 side-effects of the /calendar-events entry
|
|
97
|
+
* S6 namespace grants and the privilege-escalation chain — RED before method-scoped exact matching
|
|
98
|
+
* S5 ordinary sessions with no allowedPaths claim, incl. headless API-key callers, are unaffected
|
|
99
|
+
*/
|
|
100
|
+
var RAND = function () { return Math.random().toString(36).slice(2, 10); };
|
|
101
|
+
var OBJECT_ID = '68a1b2c3d4e5f60718293a4b';
|
|
102
|
+
var allowed_paths_confinement_tests = function (_a) {
|
|
103
|
+
var sdk = _a.sdk;
|
|
104
|
+
return __awaiter(void 0, void 0, void 0, function () {
|
|
105
|
+
var probes, request, isPathDenial, expectDenied, expectAllowed, formId, productId, joinEnduserId, formEnduserEmail, hostUserId, eventIds, businessId, form, publicFormSession, product, joinEnduser, joinLinkToken, joinEvent, unrelatedEvent, joinSession, hostUser, startLinkToken, startEvent, startSession, baselineEnduserToken, baselineUserToken, S1, S2, refreshedJoin, refreshedJoinToken, S3, refreshedStart, refreshedStartToken, S4, prefixGranted, _i, prefixGranted_1, _b, method, path, r, S6, ABSENT_ID_A, ABSENT_ID_B, INVALID_EMAIL, consentAttempt, refreshedForScopes, rescopedToken, S5, _c, _d, path, _e, _f, path, adminAuthenticated, apiKeySDK, _g, _h, path, r, failures, scenarios, _loop_1, _j, scenarios_1, scenario, _k, probes_1, p, _l, eventIds_1, id, _m, _o, e, _p, _q, _r, _s;
|
|
106
|
+
var _t, _u, _v, _w;
|
|
107
|
+
return __generator(this, function (_x) {
|
|
108
|
+
switch (_x.label) {
|
|
109
|
+
case 0:
|
|
110
|
+
(0, testing_1.log_header)("F-0166: allowedPaths wildcard bypasses session path confinement");
|
|
111
|
+
probes = [];
|
|
112
|
+
request = function (token, method, path, data) { return __awaiter(void 0, void 0, void 0, function () {
|
|
113
|
+
var r;
|
|
114
|
+
return __generator(this, function (_a) {
|
|
115
|
+
switch (_a.label) {
|
|
116
|
+
case 0: return [4 /*yield*/, axios_1.default.request(__assign(__assign({ method: method, url: "".concat(host, "/v1").concat(path) }, method === 'get' ? { params: data } : { data: data }), { headers: { Authorization: "Bearer ".concat(token) }, validateStatus: function () { return true; } }))];
|
|
117
|
+
case 1:
|
|
118
|
+
r = _a.sent();
|
|
119
|
+
return [2 /*return*/, { status: r.status, body: r.data }];
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}); };
|
|
123
|
+
isPathDenial = function (r) {
|
|
124
|
+
var _a;
|
|
125
|
+
return (r.status === 403 && ((_a = r.body) === null || _a === void 0 ? void 0 : _a.message) === 'Inaccessible');
|
|
126
|
+
};
|
|
127
|
+
expectDenied = function (scenario, label, confinedToken, baselineToken, method, path, data) { return __awaiter(void 0, void 0, void 0, function () {
|
|
128
|
+
var baseline, confined;
|
|
129
|
+
return __generator(this, function (_a) {
|
|
130
|
+
switch (_a.label) {
|
|
131
|
+
case 0: return [4 /*yield*/, request(baselineToken, method, path, data)];
|
|
132
|
+
case 1:
|
|
133
|
+
baseline = _a.sent();
|
|
134
|
+
return [4 /*yield*/, request(confinedToken, method, path, data)];
|
|
135
|
+
case 2:
|
|
136
|
+
confined = _a.sent();
|
|
137
|
+
probes.push({
|
|
138
|
+
scenario: scenario,
|
|
139
|
+
label: label,
|
|
140
|
+
path: path,
|
|
141
|
+
expected: 'denied',
|
|
142
|
+
status: confined.status,
|
|
143
|
+
baselineStatus: baseline.status,
|
|
144
|
+
pass: baseline.status !== 403 && isPathDenial(confined),
|
|
145
|
+
note: baseline.status === 403 ? 'BAD PROBE: baseline session also got 403' : undefined,
|
|
146
|
+
});
|
|
147
|
+
return [2 /*return*/];
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}); };
|
|
151
|
+
expectAllowed = function (scenario, label, token, method, path, data, _a) {
|
|
152
|
+
var _b = _a === void 0 ? {} : _a, _c = _b.allow401, allow401 = _c === void 0 ? false : _c;
|
|
153
|
+
return __awaiter(void 0, void 0, void 0, function () {
|
|
154
|
+
var r;
|
|
155
|
+
return __generator(this, function (_d) {
|
|
156
|
+
switch (_d.label) {
|
|
157
|
+
case 0: return [4 /*yield*/, request(token, method, path, data)];
|
|
158
|
+
case 1:
|
|
159
|
+
r = _d.sent();
|
|
160
|
+
probes.push({
|
|
161
|
+
scenario: scenario,
|
|
162
|
+
label: label,
|
|
163
|
+
path: path,
|
|
164
|
+
expected: 'allowed',
|
|
165
|
+
status: r.status,
|
|
166
|
+
pass: r.status !== 403 && (allow401 || r.status !== 401),
|
|
167
|
+
note: allow401 && r.status === 401 ? '401 expected: enduser cannot authenticate this model' : undefined,
|
|
168
|
+
});
|
|
169
|
+
return [2 /*return*/];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
eventIds = [];
|
|
175
|
+
_x.label = 1;
|
|
176
|
+
case 1:
|
|
177
|
+
_x.trys.push([1, , 101, 130]);
|
|
178
|
+
businessId = sdk.userInfo.businessId;
|
|
179
|
+
return [4 /*yield*/, sdk.api.forms.createOne({
|
|
180
|
+
title: "F-0166 path confinement ".concat(RAND()),
|
|
181
|
+
allowPublicURL: true,
|
|
182
|
+
})];
|
|
183
|
+
case 2:
|
|
184
|
+
form = _x.sent();
|
|
185
|
+
formId = form.id;
|
|
186
|
+
return [4 /*yield*/, sdk.api.form_fields.createOne({
|
|
187
|
+
formId: form.id,
|
|
188
|
+
title: 'q',
|
|
189
|
+
type: 'string',
|
|
190
|
+
previousFields: [{ type: 'root', info: {} }],
|
|
191
|
+
})];
|
|
192
|
+
case 3:
|
|
193
|
+
_x.sent();
|
|
194
|
+
formEnduserEmail = "f0166-form-".concat(RAND(), "@tellescope.example");
|
|
195
|
+
return [4 /*yield*/, new sdk_1.EnduserSession({ host: host, businessId: businessId })
|
|
196
|
+
.api.form_responses.session_for_public_form({
|
|
197
|
+
businessId: businessId,
|
|
198
|
+
formId: form.id,
|
|
199
|
+
email: formEnduserEmail,
|
|
200
|
+
fname: 'Path',
|
|
201
|
+
lname: 'Confinement',
|
|
202
|
+
skipMatch: true, // also skips the intake-phone requirement
|
|
203
|
+
})];
|
|
204
|
+
case 4:
|
|
205
|
+
publicFormSession = _x.sent();
|
|
206
|
+
return [4 /*yield*/, sdk.api.products.createOne({
|
|
207
|
+
title: "F-0166 product ".concat(RAND()),
|
|
208
|
+
cost: { amount: 500, currency: 'USD' },
|
|
209
|
+
processor: 'Stripe',
|
|
210
|
+
})];
|
|
211
|
+
case 5:
|
|
212
|
+
product = _x.sent();
|
|
213
|
+
productId = product.id;
|
|
214
|
+
return [4 /*yield*/, sdk.api.endusers.createOne({
|
|
215
|
+
email: "f0166-join-".concat(RAND(), "@tellescope.example"),
|
|
216
|
+
})];
|
|
217
|
+
case 6:
|
|
218
|
+
joinEnduser = _x.sent();
|
|
219
|
+
joinEnduserId = joinEnduser.id;
|
|
220
|
+
joinLinkToken = "f0166-join-".concat(RAND()).concat(RAND());
|
|
221
|
+
return [4 /*yield*/, sdk.api.calendar_events.createOne({
|
|
222
|
+
title: 'F-0166 join', durationInMinutes: 30, startTimeInMS: Date.now() + 3600000,
|
|
223
|
+
attendees: [{ type: 'enduser', id: joinEnduser.id, joinLinkToken: joinLinkToken }],
|
|
224
|
+
})];
|
|
225
|
+
case 7:
|
|
226
|
+
joinEvent = _x.sent();
|
|
227
|
+
eventIds.push(joinEvent.id);
|
|
228
|
+
return [4 /*yield*/, sdk.api.calendar_events.createOne({
|
|
229
|
+
title: 'F-0166 unrelated', durationInMinutes: 30, startTimeInMS: Date.now() + 3600000,
|
|
230
|
+
attendees: [{ type: 'enduser', id: joinEnduser.id }],
|
|
231
|
+
})];
|
|
232
|
+
case 8:
|
|
233
|
+
unrelatedEvent = _x.sent();
|
|
234
|
+
eventIds.push(unrelatedEvent.id);
|
|
235
|
+
return [4 /*yield*/, new sdk_1.EnduserSession({ host: host, businessId: businessId })
|
|
236
|
+
.api.calendar_events.session_for_join_link({ token: joinLinkToken })
|
|
237
|
+
// -- start link (STAFF session, allowedPaths contains /meeting/*)
|
|
238
|
+
// throwaway user; verifiedEmail must be set at create time (it is rejected on update)
|
|
239
|
+
];
|
|
240
|
+
case 9:
|
|
241
|
+
joinSession = _x.sent();
|
|
242
|
+
return [4 /*yield*/, sdk.api.users.createOne({
|
|
243
|
+
email: "f0166-host-".concat(RAND(), "@tellescope.example"),
|
|
244
|
+
fname: 'Path', lname: 'Host',
|
|
245
|
+
verifiedEmail: true,
|
|
246
|
+
notificationEmailsDisabled: true,
|
|
247
|
+
})];
|
|
248
|
+
case 10:
|
|
249
|
+
hostUser = _x.sent();
|
|
250
|
+
hostUserId = hostUser.id;
|
|
251
|
+
startLinkToken = "f0166-start-".concat(RAND()).concat(RAND());
|
|
252
|
+
return [4 /*yield*/, sdk.api.calendar_events.createOne({
|
|
253
|
+
title: 'F-0166 start', durationInMinutes: 30, startTimeInMS: Date.now() + 3600000,
|
|
254
|
+
attendees: [{ type: 'user', id: hostUser.id }, { type: 'enduser', id: joinEnduser.id }],
|
|
255
|
+
startLinkToken: startLinkToken,
|
|
256
|
+
})];
|
|
257
|
+
case 11:
|
|
258
|
+
startEvent = _x.sent();
|
|
259
|
+
eventIds.push(startEvent.id);
|
|
260
|
+
return [4 /*yield*/, sdk.api.calendar_events.session_for_start_link({ token: startLinkToken })
|
|
261
|
+
// -- unconfined baselines of each session type (generate_auth_token, never .authenticate)
|
|
262
|
+
];
|
|
263
|
+
case 12:
|
|
264
|
+
startSession = _x.sent();
|
|
265
|
+
return [4 /*yield*/, sdk.api.endusers.generate_auth_token({ id: joinEnduser.id })];
|
|
266
|
+
case 13:
|
|
267
|
+
baselineEnduserToken = (_x.sent()).authToken;
|
|
268
|
+
return [4 /*yield*/, sdk.api.users.generate_auth_token({ id: hostUser.id })
|
|
269
|
+
/* ============ S1: public-form session — no wildcard, correctly confined already ============ */
|
|
270
|
+
];
|
|
271
|
+
case 14:
|
|
272
|
+
baselineUserToken = (_x.sent()).authToken;
|
|
273
|
+
S1 = 'S1 public-form';
|
|
274
|
+
return [4 /*yield*/, expectAllowed(S1, 'always-allowed path', publicFormSession.authToken, 'get', '/test-authenticated')];
|
|
275
|
+
case 15:
|
|
276
|
+
_x.sent();
|
|
277
|
+
return [4 /*yield*/, expectAllowed(S1, 'listed: form info for access code', publicFormSession.authToken, 'get', '/form-info-for-access-code', { accessCode: publicFormSession.accessCode })];
|
|
278
|
+
case 16:
|
|
279
|
+
_x.sent();
|
|
280
|
+
return [4 /*yield*/, expectAllowed(S1, 'listed: refresh enduser session', publicFormSession.authToken, 'post', '/refresh-enduser-session')];
|
|
281
|
+
case 17:
|
|
282
|
+
_x.sent();
|
|
283
|
+
return [4 /*yield*/, expectAllowed(S1, 'listed: product by id', publicFormSession.authToken, 'get', "/product/".concat(productId))];
|
|
284
|
+
case 18:
|
|
285
|
+
_x.sent();
|
|
286
|
+
return [4 /*yield*/, expectDenied(S1, 'unlisted: calendar events', publicFormSession.authToken, baselineEnduserToken, 'get', '/calendar-events')];
|
|
287
|
+
case 19:
|
|
288
|
+
_x.sent();
|
|
289
|
+
return [4 /*yield*/, expectDenied(S1, 'unlisted: form responses', publicFormSession.authToken, baselineEnduserToken, 'get', '/form-responses')];
|
|
290
|
+
case 20:
|
|
291
|
+
_x.sent();
|
|
292
|
+
return [4 /*yield*/, expectDenied(S1, 'unlisted: files', publicFormSession.authToken, baselineEnduserToken, 'get', '/files')
|
|
293
|
+
// 'get:/product' names one route, so it no longer leaks the whole /products namespace the way a
|
|
294
|
+
// bare '/product' prefix entry did — that used to reach /products/prepare-stripe-checkout too
|
|
295
|
+
];
|
|
296
|
+
case 21:
|
|
297
|
+
_x.sent();
|
|
298
|
+
// 'get:/product' names one route, so it no longer leaks the whole /products namespace the way a
|
|
299
|
+
// bare '/product' prefix entry did — that used to reach /products/prepare-stripe-checkout too
|
|
300
|
+
return [4 /*yield*/, expectDenied(S1, 'exact matching: /products not granted by get:/product', publicFormSession.authToken, baselineEnduserToken, 'get', '/products')
|
|
301
|
+
/* ============ S2: join-link session — RED before the fix ============ */
|
|
302
|
+
];
|
|
303
|
+
case 22:
|
|
304
|
+
// 'get:/product' names one route, so it no longer leaks the whole /products namespace the way a
|
|
305
|
+
// bare '/product' prefix entry did — that used to reach /products/prepare-stripe-checkout too
|
|
306
|
+
_x.sent();
|
|
307
|
+
S2 = 'S2 join-link (enduser)';
|
|
308
|
+
return [4 /*yield*/, expectDenied(S2, 'unlisted: form responses', joinSession.authToken, baselineEnduserToken, 'get', '/form-responses')];
|
|
309
|
+
case 23:
|
|
310
|
+
_x.sent();
|
|
311
|
+
return [4 /*yield*/, expectDenied(S2, 'unlisted: files', joinSession.authToken, baselineEnduserToken, 'get', '/files')];
|
|
312
|
+
case 24:
|
|
313
|
+
_x.sent();
|
|
314
|
+
return [4 /*yield*/, expectDenied(S2, 'unlisted: portal customizations', joinSession.authToken, baselineEnduserToken, 'get', '/portal-customizations')];
|
|
315
|
+
case 25:
|
|
316
|
+
_x.sent();
|
|
317
|
+
return [4 /*yield*/, expectDenied(S2, 'unlisted: UNRELATED calendar event', joinSession.authToken, baselineEnduserToken, 'get', "/calendar-event/".concat(unrelatedEvent.id))];
|
|
318
|
+
case 26:
|
|
319
|
+
_x.sent();
|
|
320
|
+
return [4 /*yield*/, expectAllowed(S2, 'always-allowed path', joinSession.authToken, 'get', '/test-authenticated')];
|
|
321
|
+
case 27:
|
|
322
|
+
_x.sent();
|
|
323
|
+
return [4 /*yield*/, expectAllowed(S2, 'listed: my meetings', joinSession.authToken, 'get', '/my-meetings')
|
|
324
|
+
// enduserActions for organizations is {}, so an enduser 401s here regardless of confinement
|
|
325
|
+
];
|
|
326
|
+
case 28:
|
|
327
|
+
_x.sent();
|
|
328
|
+
// enduserActions for organizations is {}, so an enduser 401s here regardless of confinement
|
|
329
|
+
return [4 /*yield*/, expectAllowed(S2, 'listed: organizations', joinSession.authToken, 'get', '/organizations', undefined, { allow401: true })];
|
|
330
|
+
case 29:
|
|
331
|
+
// enduserActions for organizations is {}, so an enduser 401s here regardless of confinement
|
|
332
|
+
_x.sent();
|
|
333
|
+
return [4 /*yield*/, expectAllowed(S2, 'listed: users', joinSession.authToken, 'get', '/users')];
|
|
334
|
+
case 30:
|
|
335
|
+
_x.sent();
|
|
336
|
+
return [4 /*yield*/, expectAllowed(S2, 'listed: pinned calendar event', joinSession.authToken, 'get', "/calendar-event/".concat(joinEvent.id))];
|
|
337
|
+
case 31:
|
|
338
|
+
_x.sent();
|
|
339
|
+
return [4 /*yield*/, expectAllowed(S2, 'wildcard: meeting by id', joinSession.authToken, 'get', "/meeting/".concat(OBJECT_ID))
|
|
340
|
+
// the two paths widened in this change so the portal video flow keeps working
|
|
341
|
+
];
|
|
342
|
+
case 32:
|
|
343
|
+
_x.sent();
|
|
344
|
+
// the two paths widened in this change so the portal video flow keeps working
|
|
345
|
+
return [4 /*yield*/, expectAllowed(S2, 'widened: calendar events list', joinSession.authToken, 'get', '/calendar-events')];
|
|
346
|
+
case 33:
|
|
347
|
+
// the two paths widened in this change so the portal video flow keeps working
|
|
348
|
+
_x.sent();
|
|
349
|
+
return [4 /*yield*/, expectAllowed(S2, 'widened: join meeting for event', joinSession.authToken, 'post', '/join-meeting-for-event', { calendarEventId: joinEvent.id })
|
|
350
|
+
// confinement must survive a refresh rather than widening
|
|
351
|
+
];
|
|
352
|
+
case 34:
|
|
353
|
+
_x.sent();
|
|
354
|
+
return [4 /*yield*/, request(joinSession.authToken, 'post', '/refresh-enduser-session', {})];
|
|
355
|
+
case 35:
|
|
356
|
+
refreshedJoin = _x.sent();
|
|
357
|
+
refreshedJoinToken = (_t = refreshedJoin.body) === null || _t === void 0 ? void 0 : _t.authToken;
|
|
358
|
+
probes.push({
|
|
359
|
+
scenario: S2, label: 'refresh returned a new token', path: '/refresh-enduser-session',
|
|
360
|
+
expected: 'allowed', status: refreshedJoin.status, pass: !!refreshedJoinToken,
|
|
361
|
+
});
|
|
362
|
+
if (!refreshedJoinToken) return [3 /*break*/, 38];
|
|
363
|
+
return [4 /*yield*/, expectDenied(S2, 'after refresh: still denied form responses', refreshedJoinToken, baselineEnduserToken, 'get', '/form-responses')];
|
|
364
|
+
case 36:
|
|
365
|
+
_x.sent();
|
|
366
|
+
return [4 /*yield*/, expectAllowed(S2, 'after refresh: still allowed my meetings', refreshedJoinToken, 'get', '/my-meetings')];
|
|
367
|
+
case 37:
|
|
368
|
+
_x.sent();
|
|
369
|
+
_x.label = 38;
|
|
370
|
+
case 38:
|
|
371
|
+
S3 = 'S3 start-link (STAFF)';
|
|
372
|
+
return [4 /*yield*/, expectDenied(S3, 'unlisted: form responses', startSession.authToken, baselineUserToken, 'get', '/form-responses')];
|
|
373
|
+
case 39:
|
|
374
|
+
_x.sent();
|
|
375
|
+
return [4 /*yield*/, expectDenied(S3, 'unlisted: journeys', startSession.authToken, baselineUserToken, 'get', '/journeys')];
|
|
376
|
+
case 40:
|
|
377
|
+
_x.sent();
|
|
378
|
+
return [4 /*yield*/, expectDenied(S3, 'unlisted: tickets', startSession.authToken, baselineUserToken, 'get', '/tickets')];
|
|
379
|
+
case 41:
|
|
380
|
+
_x.sent();
|
|
381
|
+
return [4 /*yield*/, expectDenied(S3, 'unlisted: data sync (org-wide change firehose)', startSession.authToken, baselineUserToken, 'get', '/data-sync', { from: new Date(Date.now() - 60000).toISOString() })];
|
|
382
|
+
case 42:
|
|
383
|
+
_x.sent();
|
|
384
|
+
return [4 /*yield*/, expectDenied(S3, 'unlisted: file download URL', startSession.authToken, baselineUserToken, 'get', '/file-download-URL', { secureName: 'f0166-does-not-exist' })];
|
|
385
|
+
case 43:
|
|
386
|
+
_x.sent();
|
|
387
|
+
return [4 /*yield*/, expectDenied(S3, 'unlisted: UNRELATED calendar event', startSession.authToken, baselineUserToken, 'get', "/calendar-event/".concat(unrelatedEvent.id))];
|
|
388
|
+
case 44:
|
|
389
|
+
_x.sent();
|
|
390
|
+
return [4 /*yield*/, expectAllowed(S3, 'always-allowed path', startSession.authToken, 'get', '/test-authenticated')];
|
|
391
|
+
case 45:
|
|
392
|
+
_x.sent();
|
|
393
|
+
return [4 /*yield*/, expectAllowed(S3, 'listed: my meetings', startSession.authToken, 'get', '/my-meetings')];
|
|
394
|
+
case 46:
|
|
395
|
+
_x.sent();
|
|
396
|
+
return [4 /*yield*/, expectAllowed(S3, 'listed: calendar events', startSession.authToken, 'get', '/calendar-events')];
|
|
397
|
+
case 47:
|
|
398
|
+
_x.sent();
|
|
399
|
+
return [4 /*yield*/, expectAllowed(S3, 'listed: organizations', startSession.authToken, 'get', '/organizations')];
|
|
400
|
+
case 48:
|
|
401
|
+
_x.sent();
|
|
402
|
+
return [4 /*yield*/, expectAllowed(S3, 'listed: users', startSession.authToken, 'get', '/users')];
|
|
403
|
+
case 49:
|
|
404
|
+
_x.sent();
|
|
405
|
+
return [4 /*yield*/, expectAllowed(S3, 'listed: endusers', startSession.authToken, 'get', '/endusers')];
|
|
406
|
+
case 50:
|
|
407
|
+
_x.sent();
|
|
408
|
+
return [4 /*yield*/, expectAllowed(S3, 'listed: pinned calendar event', startSession.authToken, 'get', "/calendar-event/".concat(startEvent.id))];
|
|
409
|
+
case 51:
|
|
410
|
+
_x.sent();
|
|
411
|
+
return [4 /*yield*/, expectAllowed(S3, 'wildcard: meeting by id', startSession.authToken, 'get', "/meeting/".concat(OBJECT_ID))];
|
|
412
|
+
case 52:
|
|
413
|
+
_x.sent();
|
|
414
|
+
return [4 /*yield*/, expectAllowed(S3, 'prefix descendant: users bulk-read', startSession.authToken, 'post', '/users/bulk-read', { ids: [hostUser.id] })];
|
|
415
|
+
case 53:
|
|
416
|
+
_x.sent();
|
|
417
|
+
return [4 /*yield*/, expectAllowed(S3, 'prefix descendant: endusers bulk-read', startSession.authToken, 'post', '/endusers/bulk-read', { ids: [joinEnduser.id] })];
|
|
418
|
+
case 54:
|
|
419
|
+
_x.sent();
|
|
420
|
+
return [4 /*yield*/, expectAllowed(S3, 'widened: join meeting for event', startSession.authToken, 'post', '/join-meeting-for-event', { calendarEventId: startEvent.id })];
|
|
421
|
+
case 55:
|
|
422
|
+
_x.sent();
|
|
423
|
+
return [4 /*yield*/, expectAllowed(S3, 'widened: add attendees to meeting', startSession.authToken, 'post', '/add-attendees-to-meeting', { id: OBJECT_ID })
|
|
424
|
+
// login_user propagates allowedPaths implicitly through its ...toSign rest spread — the
|
|
425
|
+
// "(ASSUMPTION)" comments in the mint sites depend on this, so assert it explicitly
|
|
426
|
+
];
|
|
427
|
+
case 56:
|
|
428
|
+
_x.sent();
|
|
429
|
+
return [4 /*yield*/, request(startSession.authToken, 'post', '/refresh-session', {})];
|
|
430
|
+
case 57:
|
|
431
|
+
refreshedStart = _x.sent();
|
|
432
|
+
refreshedStartToken = (_u = refreshedStart.body) === null || _u === void 0 ? void 0 : _u.authToken;
|
|
433
|
+
probes.push({
|
|
434
|
+
scenario: S3, label: 'refresh returned a new token', path: '/refresh-session',
|
|
435
|
+
expected: 'allowed', status: refreshedStart.status, pass: !!refreshedStartToken,
|
|
436
|
+
});
|
|
437
|
+
if (!refreshedStartToken) return [3 /*break*/, 60];
|
|
438
|
+
return [4 /*yield*/, expectDenied(S3, 'after refresh: still denied journeys', refreshedStartToken, baselineUserToken, 'get', '/journeys')];
|
|
439
|
+
case 58:
|
|
440
|
+
_x.sent();
|
|
441
|
+
return [4 /*yield*/, expectAllowed(S3, 'after refresh: still allowed my meetings', refreshedStartToken, 'get', '/my-meetings')];
|
|
442
|
+
case 59:
|
|
443
|
+
_x.sent();
|
|
444
|
+
_x.label = 60;
|
|
445
|
+
case 60:
|
|
446
|
+
S4 = 'S4 widening side-effects';
|
|
447
|
+
prefixGranted = [
|
|
448
|
+
['patch', '/calendar-events/bulk-update'],
|
|
449
|
+
['post', '/calendar-events/report'],
|
|
450
|
+
];
|
|
451
|
+
_i = 0, prefixGranted_1 = prefixGranted;
|
|
452
|
+
_x.label = 61;
|
|
453
|
+
case 61:
|
|
454
|
+
if (!(_i < prefixGranted_1.length)) return [3 /*break*/, 64];
|
|
455
|
+
_b = prefixGranted_1[_i], method = _b[0], path = _b[1];
|
|
456
|
+
return [4 /*yield*/, axios_1.default.request({
|
|
457
|
+
method: method,
|
|
458
|
+
url: "".concat(host, "/v1").concat(path),
|
|
459
|
+
data: {},
|
|
460
|
+
headers: { Authorization: "Bearer ".concat(joinSession.authToken) },
|
|
461
|
+
validateStatus: function () { return true; },
|
|
462
|
+
})];
|
|
463
|
+
case 62:
|
|
464
|
+
r = _x.sent();
|
|
465
|
+
probes.push({
|
|
466
|
+
scenario: S4,
|
|
467
|
+
label: "enduser action gating blocks ".concat(method.toUpperCase(), " ").concat(path),
|
|
468
|
+
path: path,
|
|
469
|
+
expected: 'denied',
|
|
470
|
+
status: r.status,
|
|
471
|
+
pass: r.status === 401 || r.status === 403,
|
|
472
|
+
note: 'prefix-granted by /calendar-events; blocked by enduserActions instead',
|
|
473
|
+
});
|
|
474
|
+
_x.label = 63;
|
|
475
|
+
case 63:
|
|
476
|
+
_i++;
|
|
477
|
+
return [3 /*break*/, 61];
|
|
478
|
+
case 64:
|
|
479
|
+
S6 = 'S6 namespace escapes';
|
|
480
|
+
ABSENT_ID_A = '68a1b2c3d4e5f60718293a4b';
|
|
481
|
+
ABSENT_ID_B = '68a1b2c3d4e5f60718293a4c';
|
|
482
|
+
INVALID_EMAIL = 'f0166-not-an-email';
|
|
483
|
+
return [4 /*yield*/, expectDenied(S6, 'ESCAPE: /users/consent (minted an unconfined token)', startSession.authToken, baselineUserToken, 'patch', '/users/consent', { termsVersion: '1' })];
|
|
484
|
+
case 65:
|
|
485
|
+
_x.sent();
|
|
486
|
+
return [4 /*yield*/, expectDenied(S6, 'ESCAPE: /users/switch-account', startSession.authToken, baselineUserToken, 'post', '/users/switch-account', { targetUserId: ABSENT_ID_A })];
|
|
487
|
+
case 66:
|
|
488
|
+
_x.sent();
|
|
489
|
+
return [4 /*yield*/, expectDenied(S6, 'ESCAPE: /users/configure-inbox', startSession.authToken, baselineUserToken, 'post', '/users/configure-inbox', {})];
|
|
490
|
+
case 67:
|
|
491
|
+
_x.sent();
|
|
492
|
+
return [4 /*yield*/, expectDenied(S6, 'PHI: /endusers/load-inbox-data', startSession.authToken, baselineUserToken, 'get', '/endusers/load-inbox-data')];
|
|
493
|
+
case 68:
|
|
494
|
+
_x.sent();
|
|
495
|
+
return [4 /*yield*/, expectDenied(S6, 'PHI: /endusers/report', startSession.authToken, baselineUserToken, 'get', '/endusers/report')];
|
|
496
|
+
case 69:
|
|
497
|
+
_x.sent();
|
|
498
|
+
return [4 /*yield*/, expectDenied(S6, 'PHI: /endusers/engagement', startSession.authToken, baselineUserToken, 'get', '/endusers/engagement')];
|
|
499
|
+
case 70:
|
|
500
|
+
_x.sent();
|
|
501
|
+
return [4 /*yield*/, expectDenied(S6, 'DESTRUCTIVE: /endusers/merge', startSession.authToken, baselineUserToken, 'post', '/endusers/merge', { sourceEnduserId: ABSENT_ID_A, destinationEnduserId: ABSENT_ID_B })];
|
|
502
|
+
case 71:
|
|
503
|
+
_x.sent();
|
|
504
|
+
return [4 /*yield*/, expectDenied(S6, 'DESTRUCTIVE: /endusers/bulk-update', startSession.authToken, baselineUserToken, 'patch', '/endusers/bulk-update', { ids: [ABSENT_ID_A], fields: {} })
|
|
505
|
+
// method scoping, end to end: the same path is readable but not writable.
|
|
506
|
+
// Baselines use the ADMIN token — the throwaway host user cannot create users or endusers, so it
|
|
507
|
+
// would 403 on permissions and the probe could not distinguish that from a confinement denial.
|
|
508
|
+
];
|
|
509
|
+
case 72:
|
|
510
|
+
_x.sent();
|
|
511
|
+
// method scoping, end to end: the same path is readable but not writable.
|
|
512
|
+
// Baselines use the ADMIN token — the throwaway host user cannot create users or endusers, so it
|
|
513
|
+
// would 403 on permissions and the probe could not distinguish that from a confinement denial.
|
|
514
|
+
return [4 /*yield*/, expectAllowed(S6, 'method scoping: GET /endusers allowed', startSession.authToken, 'get', '/endusers')];
|
|
515
|
+
case 73:
|
|
516
|
+
// method scoping, end to end: the same path is readable but not writable.
|
|
517
|
+
// Baselines use the ADMIN token — the throwaway host user cannot create users or endusers, so it
|
|
518
|
+
// would 403 on permissions and the probe could not distinguish that from a confinement denial.
|
|
519
|
+
_x.sent();
|
|
520
|
+
return [4 /*yield*/, expectDenied(S6, 'method scoping: POST /endusers (create) denied', startSession.authToken, sdk.authToken, 'post', '/endusers', { email: INVALID_EMAIL })];
|
|
521
|
+
case 74:
|
|
522
|
+
_x.sent();
|
|
523
|
+
return [4 /*yield*/, expectAllowed(S6, 'method scoping: GET /users allowed', startSession.authToken, 'get', '/users')];
|
|
524
|
+
case 75:
|
|
525
|
+
_x.sent();
|
|
526
|
+
return [4 /*yield*/, expectDenied(S6, 'method scoping: POST /users (create) denied', startSession.authToken, sdk.authToken, 'post', '/users', { email: INVALID_EMAIL })
|
|
527
|
+
// even if consent were reachable, the token it mints must stay confined (defense in depth)
|
|
528
|
+
];
|
|
529
|
+
case 76:
|
|
530
|
+
_x.sent();
|
|
531
|
+
return [4 /*yield*/, request(startSession.authToken, 'patch', '/users/consent', { termsVersion: '1' })];
|
|
532
|
+
case 77:
|
|
533
|
+
consentAttempt = _x.sent();
|
|
534
|
+
if (!(consentAttempt.status === 200 && ((_v = consentAttempt.body) === null || _v === void 0 ? void 0 : _v.authToken))) return [3 /*break*/, 79];
|
|
535
|
+
return [4 /*yield*/, expectDenied(S6, 'consent-minted token is still confined', consentAttempt.body.authToken, baselineUserToken, 'get', '/journeys')];
|
|
536
|
+
case 78:
|
|
537
|
+
_x.sent();
|
|
538
|
+
_x.label = 79;
|
|
539
|
+
case 79: return [4 /*yield*/, request(startSession.authToken, 'post', '/refresh-session', {})];
|
|
540
|
+
case 80:
|
|
541
|
+
refreshedForScopes = _x.sent();
|
|
542
|
+
rescopedToken = (_w = refreshedForScopes.body) === null || _w === void 0 ? void 0 : _w.authToken;
|
|
543
|
+
probes.push({
|
|
544
|
+
scenario: S6, label: 'refresh returned a new token', path: '/refresh-session',
|
|
545
|
+
expected: 'allowed', status: refreshedForScopes.status, pass: !!rescopedToken,
|
|
546
|
+
});
|
|
547
|
+
if (!rescopedToken) return [3 /*break*/, 86];
|
|
548
|
+
return [4 /*yield*/, expectDenied(S6, 'after refresh: still cannot reach /users/consent', rescopedToken, baselineUserToken, 'patch', '/users/consent', { termsVersion: '1' })];
|
|
549
|
+
case 81:
|
|
550
|
+
_x.sent();
|
|
551
|
+
return [4 /*yield*/, expectDenied(S6, 'after refresh: still cannot reach /endusers/load-inbox-data', rescopedToken, baselineUserToken, 'get', '/endusers/load-inbox-data')];
|
|
552
|
+
case 82:
|
|
553
|
+
_x.sent();
|
|
554
|
+
return [4 /*yield*/, expectDenied(S6, 'after refresh: POST /endusers still denied', rescopedToken, baselineUserToken, 'post', '/endusers', { email: "f0166-should-not-exist-".concat(RAND(), "@tellescope.example") })];
|
|
555
|
+
case 83:
|
|
556
|
+
_x.sent();
|
|
557
|
+
return [4 /*yield*/, expectAllowed(S6, 'after refresh: GET /endusers still allowed', rescopedToken, 'get', '/endusers')];
|
|
558
|
+
case 84:
|
|
559
|
+
_x.sent();
|
|
560
|
+
return [4 /*yield*/, expectAllowed(S6, 'after refresh: my-meetings still allowed', rescopedToken, 'get', '/my-meetings')];
|
|
561
|
+
case 85:
|
|
562
|
+
_x.sent();
|
|
563
|
+
_x.label = 86;
|
|
564
|
+
case 86:
|
|
565
|
+
S5 = 'S5 unconfined sessions';
|
|
566
|
+
_c = 0, _d = ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/tickets'];
|
|
567
|
+
_x.label = 87;
|
|
568
|
+
case 87:
|
|
569
|
+
if (!(_c < _d.length)) return [3 /*break*/, 90];
|
|
570
|
+
path = _d[_c];
|
|
571
|
+
return [4 /*yield*/, expectAllowed(S5, "staff session reaches ".concat(path), baselineUserToken, 'get', path)];
|
|
572
|
+
case 88:
|
|
573
|
+
_x.sent();
|
|
574
|
+
_x.label = 89;
|
|
575
|
+
case 89:
|
|
576
|
+
_c++;
|
|
577
|
+
return [3 /*break*/, 87];
|
|
578
|
+
case 90: return [4 /*yield*/, expectAllowed(S5, 'staff session reaches /data-sync', baselineUserToken, 'get', '/data-sync', { from: new Date(Date.now() - 60000).toISOString() })];
|
|
579
|
+
case 91:
|
|
580
|
+
_x.sent();
|
|
581
|
+
_e = 0, _f = ['/form-responses', '/calendar-events', '/files'];
|
|
582
|
+
_x.label = 92;
|
|
583
|
+
case 92:
|
|
584
|
+
if (!(_e < _f.length)) return [3 /*break*/, 95];
|
|
585
|
+
path = _f[_e];
|
|
586
|
+
return [4 /*yield*/, expectAllowed(S5, "enduser session reaches ".concat(path), baselineEnduserToken, 'get', path)];
|
|
587
|
+
case 93:
|
|
588
|
+
_x.sent();
|
|
589
|
+
_x.label = 94;
|
|
590
|
+
case 94:
|
|
591
|
+
_e++;
|
|
592
|
+
return [3 /*break*/, 92];
|
|
593
|
+
case 95: return [4 /*yield*/, sdk.test_authenticated()];
|
|
594
|
+
case 96:
|
|
595
|
+
adminAuthenticated = _x.sent();
|
|
596
|
+
probes.push({
|
|
597
|
+
scenario: S5, label: 'admin sdk still authenticated', path: '/test-authenticated',
|
|
598
|
+
expected: 'allowed', status: 200, pass: adminAuthenticated === 'Authenticated!',
|
|
599
|
+
});
|
|
600
|
+
apiKeySDK = new sdk_1.Session({ host: host, apiKey: '3n5q0SCBT_iUvZz-b9BJtX7o7HQUVJ9v132PgHJNJsg.' });
|
|
601
|
+
_g = 0, _h = ['/journeys', '/form-responses', '/endusers', '/calendar-events', '/files'];
|
|
602
|
+
_x.label = 97;
|
|
603
|
+
case 97:
|
|
604
|
+
if (!(_g < _h.length)) return [3 /*break*/, 100];
|
|
605
|
+
path = _h[_g];
|
|
606
|
+
return [4 /*yield*/, axios_1.default.request({
|
|
607
|
+
method: 'get',
|
|
608
|
+
url: "".concat(host, "/v1").concat(path),
|
|
609
|
+
headers: { Authorization: "API_KEY ".concat(apiKeySDK.apiKey) },
|
|
610
|
+
validateStatus: function () { return true; },
|
|
611
|
+
})];
|
|
612
|
+
case 98:
|
|
613
|
+
r = _x.sent();
|
|
614
|
+
probes.push({
|
|
615
|
+
scenario: S5,
|
|
616
|
+
label: "API key (headless) reaches ".concat(path),
|
|
617
|
+
path: path,
|
|
618
|
+
expected: 'allowed',
|
|
619
|
+
status: r.status,
|
|
620
|
+
pass: r.status !== 403,
|
|
621
|
+
});
|
|
622
|
+
_x.label = 99;
|
|
623
|
+
case 99:
|
|
624
|
+
_g++;
|
|
625
|
+
return [3 /*break*/, 97];
|
|
626
|
+
case 100:
|
|
627
|
+
failures = probes.filter(function (p) { return !p.pass; });
|
|
628
|
+
console.log('');
|
|
629
|
+
console.log(' F-0166 — full probe table');
|
|
630
|
+
console.log(' ' + '-'.repeat(112));
|
|
631
|
+
scenarios = probes.map(function (p) { return p.scenario; }).filter(function (s, i, all) { return all.indexOf(s) === i; });
|
|
632
|
+
_loop_1 = function (scenario) {
|
|
633
|
+
var inScenario = probes.filter(function (p) { return p.scenario === scenario; });
|
|
634
|
+
var failed = inScenario.filter(function (p) { return !p.pass; }).length;
|
|
635
|
+
console.log(" ".concat(scenario, ": ").concat(inScenario.length - failed, "/").concat(inScenario.length, " passing").concat(failed ? " <-- ".concat(failed, " FAILING") : ''));
|
|
636
|
+
for (var _y = 0, inScenario_1 = inScenario; _y < inScenario_1.length; _y++) {
|
|
637
|
+
var p = inScenario_1[_y];
|
|
638
|
+
var baseline = p.baselineStatus !== undefined ? " baseline=".concat(p.baselineStatus) : '';
|
|
639
|
+
console.log(" ".concat(p.pass ? ' ok ' : 'FAIL', " want=").concat(p.expected.padEnd(7), " got=").concat(p.status).concat(baseline, " ").concat(p.label).concat(p.note ? " (".concat(p.note, ")") : ''));
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
for (_j = 0, scenarios_1 = scenarios; _j < scenarios_1.length; _j++) {
|
|
643
|
+
scenario = scenarios_1[_j];
|
|
644
|
+
_loop_1(scenario);
|
|
645
|
+
}
|
|
646
|
+
console.log(' ' + '-'.repeat(112));
|
|
647
|
+
console.log(" TOTAL: ".concat(probes.length - failures.length, "/").concat(probes.length, " passing, ").concat(failures.length, " failing"));
|
|
648
|
+
console.log('');
|
|
649
|
+
for (_k = 0, probes_1 = probes; _k < probes_1.length; _k++) {
|
|
650
|
+
p = probes_1[_k];
|
|
651
|
+
(0, testing_1.assert)(p.pass, "".concat(p.scenario, " \u2014 ").concat(p.label, " (").concat(p.path, "): wanted ").concat(p.expected, ", got status ").concat(p.status)
|
|
652
|
+
+ (p.baselineStatus !== undefined ? " (unconfined baseline got ".concat(p.baselineStatus, ")") : ''), "".concat(p.scenario, " \u2014 ").concat(p.label));
|
|
653
|
+
}
|
|
654
|
+
return [3 /*break*/, 130];
|
|
655
|
+
case 101:
|
|
656
|
+
_l = 0, eventIds_1 = eventIds;
|
|
657
|
+
_x.label = 102;
|
|
658
|
+
case 102:
|
|
659
|
+
if (!(_l < eventIds_1.length)) return [3 /*break*/, 107];
|
|
660
|
+
id = eventIds_1[_l];
|
|
661
|
+
_x.label = 103;
|
|
662
|
+
case 103:
|
|
663
|
+
_x.trys.push([103, 105, , 106]);
|
|
664
|
+
return [4 /*yield*/, sdk.api.calendar_events.deleteOne(id)];
|
|
665
|
+
case 104:
|
|
666
|
+
_x.sent();
|
|
667
|
+
return [3 /*break*/, 106];
|
|
668
|
+
case 105:
|
|
669
|
+
_m = _x.sent();
|
|
670
|
+
return [3 /*break*/, 106];
|
|
671
|
+
case 106:
|
|
672
|
+
_l++;
|
|
673
|
+
return [3 /*break*/, 102];
|
|
674
|
+
case 107:
|
|
675
|
+
if (!joinEnduserId) return [3 /*break*/, 111];
|
|
676
|
+
_x.label = 108;
|
|
677
|
+
case 108:
|
|
678
|
+
_x.trys.push([108, 110, , 111]);
|
|
679
|
+
return [4 /*yield*/, sdk.api.endusers.deleteOne(joinEnduserId)];
|
|
680
|
+
case 109:
|
|
681
|
+
_x.sent();
|
|
682
|
+
return [3 /*break*/, 111];
|
|
683
|
+
case 110:
|
|
684
|
+
_o = _x.sent();
|
|
685
|
+
return [3 /*break*/, 111];
|
|
686
|
+
case 111:
|
|
687
|
+
if (!formEnduserEmail) return [3 /*break*/, 117];
|
|
688
|
+
_x.label = 112;
|
|
689
|
+
case 112:
|
|
690
|
+
_x.trys.push([112, 116, , 117]);
|
|
691
|
+
return [4 /*yield*/, sdk.api.endusers.getOne({ email: formEnduserEmail })];
|
|
692
|
+
case 113:
|
|
693
|
+
e = _x.sent();
|
|
694
|
+
if (!(e === null || e === void 0 ? void 0 : e.id)) return [3 /*break*/, 115];
|
|
695
|
+
return [4 /*yield*/, sdk.api.endusers.deleteOne(e.id)];
|
|
696
|
+
case 114:
|
|
697
|
+
_x.sent();
|
|
698
|
+
_x.label = 115;
|
|
699
|
+
case 115: return [3 /*break*/, 117];
|
|
700
|
+
case 116:
|
|
701
|
+
_p = _x.sent();
|
|
702
|
+
return [3 /*break*/, 117];
|
|
703
|
+
case 117:
|
|
704
|
+
if (!hostUserId) return [3 /*break*/, 121];
|
|
705
|
+
_x.label = 118;
|
|
706
|
+
case 118:
|
|
707
|
+
_x.trys.push([118, 120, , 121]);
|
|
708
|
+
return [4 /*yield*/, sdk.api.users.deleteOne(hostUserId)];
|
|
709
|
+
case 119:
|
|
710
|
+
_x.sent();
|
|
711
|
+
return [3 /*break*/, 121];
|
|
712
|
+
case 120:
|
|
713
|
+
_q = _x.sent();
|
|
714
|
+
return [3 /*break*/, 121];
|
|
715
|
+
case 121:
|
|
716
|
+
if (!productId) return [3 /*break*/, 125];
|
|
717
|
+
_x.label = 122;
|
|
718
|
+
case 122:
|
|
719
|
+
_x.trys.push([122, 124, , 125]);
|
|
720
|
+
return [4 /*yield*/, sdk.api.products.deleteOne(productId)];
|
|
721
|
+
case 123:
|
|
722
|
+
_x.sent();
|
|
723
|
+
return [3 /*break*/, 125];
|
|
724
|
+
case 124:
|
|
725
|
+
_r = _x.sent();
|
|
726
|
+
return [3 /*break*/, 125];
|
|
727
|
+
case 125:
|
|
728
|
+
if (!formId) return [3 /*break*/, 129];
|
|
729
|
+
_x.label = 126;
|
|
730
|
+
case 126:
|
|
731
|
+
_x.trys.push([126, 128, , 129]);
|
|
732
|
+
return [4 /*yield*/, sdk.api.forms.deleteOne(formId)];
|
|
733
|
+
case 127:
|
|
734
|
+
_x.sent();
|
|
735
|
+
return [3 /*break*/, 129];
|
|
736
|
+
case 128:
|
|
737
|
+
_s = _x.sent();
|
|
738
|
+
return [3 /*break*/, 129];
|
|
739
|
+
case 129: return [7 /*endfinally*/];
|
|
740
|
+
case 130: return [2 /*return*/];
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
};
|
|
745
|
+
exports.allowed_paths_confinement_tests = allowed_paths_confinement_tests;
|
|
746
|
+
if (require.main === module) {
|
|
747
|
+
console.log("\uD83C\uDF10 Using API URL: ".concat(host));
|
|
748
|
+
var sdk_2 = new sdk_1.Session({ host: host });
|
|
749
|
+
var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
|
|
750
|
+
var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
751
|
+
return __generator(this, function (_a) {
|
|
752
|
+
switch (_a.label) {
|
|
753
|
+
case 0: return [4 /*yield*/, (0, setup_1.setup_tests)(sdk_2, sdkNonAdmin_1)];
|
|
754
|
+
case 1:
|
|
755
|
+
_a.sent();
|
|
756
|
+
return [4 /*yield*/, (0, exports.allowed_paths_confinement_tests)({ sdk: sdk_2, sdkNonAdmin: sdkNonAdmin_1 })];
|
|
757
|
+
case 2:
|
|
758
|
+
_a.sent();
|
|
759
|
+
return [2 /*return*/];
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
}); };
|
|
763
|
+
runTests()
|
|
764
|
+
.then(function () {
|
|
765
|
+
console.log("✅ F-0166 allowedPaths confinement test suite completed successfully");
|
|
766
|
+
process.exit(0);
|
|
767
|
+
})
|
|
768
|
+
.catch(function (error) {
|
|
769
|
+
console.error("❌ F-0166 allowedPaths confinement test suite failed:", error);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
//# sourceMappingURL=F-0166-allowed-paths-wildcard-bypass.test.js.map
|