@_mustachio/openauth 0.11.0 → 0.13.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/dist/esm/adapters/d1/index.js +18 -9
- package/dist/esm/adapters/d1/migrations.js +7 -2
- package/dist/esm/adapters/d1/session-store.js +39 -0
- package/dist/esm/adapters/d1/session.js +2 -2
- package/dist/esm/adapters/durable-object/index.js +4 -0
- package/dist/esm/adapters/durable-object/session-store.js +42 -0
- package/dist/esm/adapters/dynamo/index.js +24 -7
- package/dist/esm/adapters/dynamo/session-store.js +49 -0
- package/dist/esm/adapters/kms/index.js +6 -2
- package/dist/esm/adapters/kv/index.js +6 -2
- package/dist/esm/adapters/memory/index.js +24 -7
- package/dist/esm/adapters/memory/session-store.js +23 -0
- package/dist/esm/adapters/postgres/executor.js +2 -2
- package/dist/esm/adapters/postgres/index.js +33 -10
- package/dist/esm/adapters/postgres/migrations.js +8 -2
- package/dist/esm/adapters/postgres/session-store.js +42 -0
- package/dist/esm/client.js +27 -27
- package/dist/esm/domain/authorize.js +2 -2
- package/dist/esm/domain/callback.js +121 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +2 -2
- package/dist/esm/domain/dpop.js +4 -4
- package/dist/esm/domain/id-token.js +5 -5
- package/dist/esm/domain/jwt.js +4 -4
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +80 -1
- package/dist/esm/domain/par.js +2 -2
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/scim/discovery.js +206 -0
- package/dist/esm/domain/scim/filter.js +177 -0
- package/dist/esm/domain/scim/handle.js +338 -0
- package/dist/esm/domain/scim/patch.js +568 -0
- package/dist/esm/domain/scim/resource.js +256 -0
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token.js +5 -5
- package/dist/esm/error.js +8 -8
- package/dist/esm/http/cookies.js +19 -2
- package/dist/esm/http/errors.js +4 -4
- package/dist/esm/http/handlers/authorize.js +3 -19
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/metadata.js +2 -2
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/handlers/scim.js +59 -0
- package/dist/esm/http/middleware/tenant.js +31 -6
- package/dist/esm/http/router.js +3 -0
- package/dist/esm/http/schemas/authorize.js +2 -2
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +34 -32
- package/dist/esm/methods/oauth2-factory.js +2 -2
- package/dist/esm/methods/password-hash.js +2 -2
- package/dist/esm/methods/providers/index.js +30 -14
- package/dist/esm/methods/saml-sp/acs.js +232 -0
- package/dist/esm/methods/saml-sp/attributes.js +77 -0
- package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
- package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
- package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
- package/dist/esm/methods/saml-sp/factory.js +81 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +71 -0
- package/dist/esm/methods/saml-sp/method.js +29 -0
- package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
- package/dist/esm/methods/saml-sp/saml-instance.js +59 -0
- package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
- package/dist/esm/methods/saml-sp/sls.js +154 -0
- package/dist/esm/methods/saml-sp/types.js +0 -0
- package/dist/esm/ports/scim-directory.js +0 -0
- package/dist/esm/types/error.js +5 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/types/scim.js +0 -0
- package/dist/esm/ui/forms.js +3 -3
- package/dist/esm/ui/index.js +8 -5
- package/dist/esm/ui/picker.js +2 -2
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
- package/dist/types/adapters/d1/session-store.d.ts +3 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
- package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
- package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/session-store.d.ts +3 -0
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
- package/dist/types/adapters/postgres/session-store.d.ts +3 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
- package/dist/types/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +57 -1
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/scim/discovery.d.ts +11 -0
- package/dist/types/domain/scim/discovery.d.ts.map +1 -0
- package/dist/types/domain/scim/filter.d.ts +51 -0
- package/dist/types/domain/scim/filter.d.ts.map +1 -0
- package/dist/types/domain/scim/handle.d.ts +35 -0
- package/dist/types/domain/scim/handle.d.ts.map +1 -0
- package/dist/types/domain/scim/patch.d.ts +73 -0
- package/dist/types/domain/scim/patch.d.ts.map +1 -0
- package/dist/types/domain/scim/resource.d.ts +56 -0
- package/dist/types/domain/scim/resource.d.ts.map +1 -0
- package/dist/types/http/context.d.ts +6 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/cookies.d.ts +9 -1
- package/dist/types/http/cookies.d.ts.map +1 -1
- package/dist/types/http/handlers/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/scim.d.ts +3 -0
- package/dist/types/http/handlers/scim.d.ts.map +1 -0
- package/dist/types/http/middleware/tenant.d.ts.map +1 -1
- package/dist/types/http/router.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/acs.d.ts +4 -0
- package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/attributes.d.ts +45 -0
- package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/factory.d.ts +920 -0
- package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/index.d.ts +24 -0
- package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/metadata.d.ts +50 -0
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/method.d.ts +17 -0
- package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts +93 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/sls.d.ts +4 -0
- package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/types.d.ts +299 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +18 -5
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/scim-directory.d.ts +104 -0
- package/dist/types/ports/scim-directory.d.ts.map +1 -0
- package/dist/types/ports/session-store.d.ts +23 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/types/error.d.ts +5 -0
- package/dist/types/types/error.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +83 -0
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +109 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/dist/types/types/scim.d.ts +269 -0
- package/dist/types/types/scim.d.ts.map +1 -0
- package/dist/types/types/tenant.d.ts +10 -0
- package/dist/types/types/tenant.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/adapters/d1/migrations.ts +5 -0
- package/src/adapters/d1/session-store.ts +68 -0
- package/src/adapters/durable-object/session-store.ts +61 -0
- package/src/adapters/dynamo/session-store.ts +63 -0
- package/src/adapters/memory/session-store.ts +39 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/domain/callback.ts +215 -7
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +190 -1
- package/src/domain/scim/discovery.ts +262 -0
- package/src/domain/scim/filter.ts +295 -0
- package/src/domain/scim/handle.ts +597 -0
- package/src/domain/scim/patch.ts +807 -0
- package/src/domain/scim/resource.ts +358 -0
- package/src/http/context.ts +9 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +2 -22
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/method-route.ts +72 -2
- package/src/http/handlers/scim.ts +107 -0
- package/src/http/middleware/tenant.ts +47 -1
- package/src/http/router.ts +9 -0
- package/src/index.ts +23 -0
- package/src/methods/saml-sp/acs.ts +449 -0
- package/src/methods/saml-sp/attributes.ts +146 -0
- package/src/methods/saml-sp/authnrequest.ts +129 -0
- package/src/methods/saml-sp/cache-provider.ts +76 -0
- package/src/methods/saml-sp/cert-rotation.ts +34 -0
- package/src/methods/saml-sp/factory.ts +139 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +215 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +158 -0
- package/src/methods/saml-sp/saml-instance.ts +210 -0
- package/src/methods/saml-sp/slo-initiate.ts +180 -0
- package/src/methods/saml-sp/sls.ts +307 -0
- package/src/methods/saml-sp/types.ts +298 -0
- package/src/ports/CONSISTENCY.md +52 -24
- package/src/ports/audit-log.ts +18 -5
- package/src/ports/scim-directory.ts +166 -0
- package/src/ports/session-store.ts +24 -0
- package/src/types/error.ts +11 -0
- package/src/types/idp.ts +86 -0
- package/src/types/method.ts +110 -0
- package/src/types/scim.ts +280 -0
- package/src/types/tenant.ts +10 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/domain/scim/filter.ts
|
|
2
|
+
import { err, ok } from "../../types/result";
|
|
3
|
+
var SUPPORTED_FILTER_HELP = 'supported filters are: userName eq "…", externalId eq "…", id eq "…", ' + 'active eq true|false, emails[type eq "…"].value eq "…", and two of ' + 'those joined by "and"';
|
|
4
|
+
var ATTRIBUTES = {
|
|
5
|
+
id: "id",
|
|
6
|
+
username: "userName",
|
|
7
|
+
externalid: "externalId",
|
|
8
|
+
active: "active",
|
|
9
|
+
"emails.value": "emails.value",
|
|
10
|
+
displayname: "displayName"
|
|
11
|
+
};
|
|
12
|
+
var USER_FILTER_ATTRIBUTES = new Set(["id", "userName", "externalId", "active", "emails.value"]);
|
|
13
|
+
var GROUP_FILTER_ATTRIBUTES = new Set(["id", "displayName", "externalId"]);
|
|
14
|
+
function normalizeComplexPath(raw) {
|
|
15
|
+
const m = /^([A-Za-z]+)\[[^\]]*\]\.([A-Za-z]+)$/.exec(raw.trim());
|
|
16
|
+
if (!m)
|
|
17
|
+
return raw.trim();
|
|
18
|
+
return `${m[1]}.${m[2]}`;
|
|
19
|
+
}
|
|
20
|
+
function findOperator(term) {
|
|
21
|
+
let depth = 0;
|
|
22
|
+
let inQuotes = false;
|
|
23
|
+
for (let i = 0;i < term.length; i++) {
|
|
24
|
+
const ch = term[i];
|
|
25
|
+
if (ch === '"' && term[i - 1] !== "\\") {
|
|
26
|
+
inQuotes = !inQuotes;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (inQuotes)
|
|
30
|
+
continue;
|
|
31
|
+
if (ch === "[")
|
|
32
|
+
depth++;
|
|
33
|
+
else if (ch === "]")
|
|
34
|
+
depth--;
|
|
35
|
+
else if (depth === 0 && ch === " ") {
|
|
36
|
+
const m = /^\s+([A-Za-z]{2,3})(?:\s+|$)/.exec(term.slice(i));
|
|
37
|
+
if (m) {
|
|
38
|
+
return {
|
|
39
|
+
attr: term.slice(0, i),
|
|
40
|
+
op: m[1],
|
|
41
|
+
value: term.slice(i + m[0].length)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function parseTerm(raw, allowed) {
|
|
49
|
+
const term = raw.trim();
|
|
50
|
+
const found = findOperator(term);
|
|
51
|
+
if (!found) {
|
|
52
|
+
return err({
|
|
53
|
+
detail: `could not parse filter term "${term}". ${SUPPORTED_FILTER_HELP}`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const { attr: rawAttr, op: rawOp, value: rawValue } = found;
|
|
57
|
+
if (rawOp.toLowerCase() !== "eq") {
|
|
58
|
+
return err({
|
|
59
|
+
detail: `operator "${rawOp}" is not supported; only "eq" is. ` + SUPPORTED_FILTER_HELP
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const attrKey = normalizeComplexPath(rawAttr).toLowerCase();
|
|
63
|
+
const attribute = ATTRIBUTES[attrKey];
|
|
64
|
+
if (!attribute || !allowed.has(attribute)) {
|
|
65
|
+
return err({
|
|
66
|
+
detail: `attribute "${rawAttr.trim()}" is not filterable. ` + SUPPORTED_FILTER_HELP
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const value = rawValue.trim();
|
|
70
|
+
if (attribute === "active") {
|
|
71
|
+
const lowered = value.toLowerCase();
|
|
72
|
+
if (lowered !== "true" && lowered !== "false") {
|
|
73
|
+
return err({
|
|
74
|
+
detail: `"active" must be compared to true or false, got ${value}`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return ok({ op: "eq", attribute, value: lowered === "true" });
|
|
78
|
+
}
|
|
79
|
+
if (value.length < 2 || !value.startsWith('"') || !value.endsWith('"')) {
|
|
80
|
+
return err({
|
|
81
|
+
detail: `value for "${attribute}" must be a quoted string, got ${value}`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const unquoted = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
85
|
+
if (unquoted.length === 0) {
|
|
86
|
+
return err({ detail: `value for "${attribute}" must not be empty` });
|
|
87
|
+
}
|
|
88
|
+
return ok({ op: "eq", attribute, value: unquoted });
|
|
89
|
+
}
|
|
90
|
+
function parseScimFilter(raw, allowed = USER_FILTER_ATTRIBUTES) {
|
|
91
|
+
if (raw === null || raw === undefined || raw.trim().length === 0) {
|
|
92
|
+
return ok(undefined);
|
|
93
|
+
}
|
|
94
|
+
const input = raw.trim();
|
|
95
|
+
const masked = maskQuoted(input);
|
|
96
|
+
if (masked.includes("(") || masked.includes(")")) {
|
|
97
|
+
return err({
|
|
98
|
+
detail: `grouped expressions are not supported. ${SUPPORTED_FILTER_HELP}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const parts = splitTopLevelAnd(input);
|
|
102
|
+
if (parts.length > 2) {
|
|
103
|
+
return err({
|
|
104
|
+
detail: `at most two terms joined by "and" are supported. ` + SUPPORTED_FILTER_HELP
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (/\sor\s/i.test(masked)) {
|
|
108
|
+
return err({
|
|
109
|
+
detail: `"or" is not supported. ${SUPPORTED_FILTER_HELP}`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (/(^|\s)not\s/i.test(masked)) {
|
|
113
|
+
return err({
|
|
114
|
+
detail: `"not" is not supported. ${SUPPORTED_FILTER_HELP}`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const first = parseTerm(parts[0], allowed);
|
|
118
|
+
if (!first.ok)
|
|
119
|
+
return first;
|
|
120
|
+
if (parts.length === 1)
|
|
121
|
+
return ok(first.value);
|
|
122
|
+
const second = parseTerm(parts[1], allowed);
|
|
123
|
+
if (!second.ok)
|
|
124
|
+
return second;
|
|
125
|
+
return ok({ op: "and", left: first.value, right: second.value });
|
|
126
|
+
}
|
|
127
|
+
function maskQuoted(input) {
|
|
128
|
+
let out = "";
|
|
129
|
+
let inQuotes = false;
|
|
130
|
+
for (let i = 0;i < input.length; i++) {
|
|
131
|
+
const ch = input[i];
|
|
132
|
+
if (ch === '"' && input[i - 1] !== "\\") {
|
|
133
|
+
inQuotes = !inQuotes;
|
|
134
|
+
out += ch;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
out += inQuotes ? " " : ch;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function splitTopLevelAnd(input) {
|
|
142
|
+
const parts = [];
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let inQuotes = false;
|
|
145
|
+
let start = 0;
|
|
146
|
+
for (let i = 0;i < input.length; i++) {
|
|
147
|
+
const ch = input[i];
|
|
148
|
+
if (ch === '"' && input[i - 1] !== "\\") {
|
|
149
|
+
inQuotes = !inQuotes;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (inQuotes)
|
|
153
|
+
continue;
|
|
154
|
+
if (ch === "[")
|
|
155
|
+
depth++;
|
|
156
|
+
else if (ch === "]")
|
|
157
|
+
depth--;
|
|
158
|
+
else if (depth === 0 && (ch === "a" || ch === "A")) {
|
|
159
|
+
const window = input.slice(i, i + 3);
|
|
160
|
+
const before = input[i - 1];
|
|
161
|
+
const after = input[i + 3];
|
|
162
|
+
if (window.toLowerCase() === "and" && (before === " " || before === undefined) && (after === " " || after === undefined)) {
|
|
163
|
+
parts.push(input.slice(start, i));
|
|
164
|
+
start = i + 3;
|
|
165
|
+
i += 2;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
parts.push(input.slice(start));
|
|
170
|
+
return parts.map((p) => p.trim()).filter((p) => p.length > 0);
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
GROUP_FILTER_ATTRIBUTES,
|
|
174
|
+
SUPPORTED_FILTER_HELP,
|
|
175
|
+
USER_FILTER_ATTRIBUTES,
|
|
176
|
+
parseScimFilter
|
|
177
|
+
};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// src/domain/scim/handle.ts
|
|
2
|
+
import { timingSafeEqualStr } from "../crypto";
|
|
3
|
+
import { hashClientSecret } from "../token";
|
|
4
|
+
import { resourceTypes, schemas, serviceProviderConfig } from "./discovery";
|
|
5
|
+
import {
|
|
6
|
+
GROUP_FILTER_ATTRIBUTES,
|
|
7
|
+
parseScimFilter,
|
|
8
|
+
SUPPORTED_FILTER_HELP,
|
|
9
|
+
USER_FILTER_ATTRIBUTES
|
|
10
|
+
} from "./filter";
|
|
11
|
+
import { normalizeGroupPatch, normalizePatch } from "./patch";
|
|
12
|
+
import {
|
|
13
|
+
parseGroupWrite,
|
|
14
|
+
parseUserWrite,
|
|
15
|
+
scimErrorBody,
|
|
16
|
+
serializeGroup,
|
|
17
|
+
serializeGroupList,
|
|
18
|
+
serializeList,
|
|
19
|
+
serializeUser
|
|
20
|
+
} from "./resource";
|
|
21
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
22
|
+
var PAGE_SIZE_CEILING = 500;
|
|
23
|
+
function created(body) {
|
|
24
|
+
const location = body["meta"]?.["location"];
|
|
25
|
+
return {
|
|
26
|
+
status: 201,
|
|
27
|
+
body,
|
|
28
|
+
...typeof location === "string" ? { headers: { location } } : {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var fail = (status, detail, scimType) => ({
|
|
32
|
+
status,
|
|
33
|
+
body: scimErrorBody(status, detail, scimType)
|
|
34
|
+
});
|
|
35
|
+
function fromPortError(error) {
|
|
36
|
+
if (error.code === "conflict") {
|
|
37
|
+
return fail(409, error.description, "uniqueness");
|
|
38
|
+
}
|
|
39
|
+
if (error.code === "invalid_request") {
|
|
40
|
+
return fail(400, error.description, "invalidValue");
|
|
41
|
+
}
|
|
42
|
+
return fail(500, "the directory backing this endpoint failed the request");
|
|
43
|
+
}
|
|
44
|
+
function unwrap(r) {
|
|
45
|
+
if (r.ok)
|
|
46
|
+
return { value: r.value };
|
|
47
|
+
return { response: fromPortError(r.error) };
|
|
48
|
+
}
|
|
49
|
+
async function authenticate(scim, authorization) {
|
|
50
|
+
if (!scim || scim.enabled !== true) {
|
|
51
|
+
return fail(403, "SCIM provisioning is not enabled for this tenant");
|
|
52
|
+
}
|
|
53
|
+
const prefix = "bearer ";
|
|
54
|
+
if (authorization === null || !authorization.toLowerCase().startsWith(prefix)) {
|
|
55
|
+
return fail(401, "a Bearer token is required");
|
|
56
|
+
}
|
|
57
|
+
const presented = authorization.slice(prefix.length).trim();
|
|
58
|
+
if (presented.length === 0)
|
|
59
|
+
return fail(401, "a Bearer token is required");
|
|
60
|
+
const hashed = await hashClientSecret(presented);
|
|
61
|
+
if (!timingSafeEqualStr(hashed, scim.tokenHash)) {
|
|
62
|
+
return fail(401, "invalid Bearer token");
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function userIdFrom(path) {
|
|
67
|
+
const m = /^\/Users\/(.+)$/.exec(path);
|
|
68
|
+
if (!m)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
return decodeURIComponent(m[1]);
|
|
72
|
+
} catch {
|
|
73
|
+
return m[1];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function clampPaging(query, maxPageSize) {
|
|
77
|
+
const rawStart = Number(query.get("startIndex") ?? "1");
|
|
78
|
+
const startIndex = Number.isFinite(rawStart) && rawStart >= 1 ? Math.floor(rawStart) : 1;
|
|
79
|
+
const rawCount = query.get("count");
|
|
80
|
+
if (rawCount === null)
|
|
81
|
+
return { startIndex, count: maxPageSize };
|
|
82
|
+
const parsed = Number(rawCount);
|
|
83
|
+
if (!Number.isFinite(parsed))
|
|
84
|
+
return { startIndex, count: maxPageSize };
|
|
85
|
+
if (parsed < 0)
|
|
86
|
+
return { startIndex, count: 0 };
|
|
87
|
+
return { startIndex, count: Math.min(Math.floor(parsed), maxPageSize) };
|
|
88
|
+
}
|
|
89
|
+
async function listUsers(input, maxPageSize) {
|
|
90
|
+
const filter = parseScimFilter(input.query.get("filter"), USER_FILTER_ATTRIBUTES);
|
|
91
|
+
if (!filter.ok) {
|
|
92
|
+
return fail(400, filter.error.detail, "invalidFilter");
|
|
93
|
+
}
|
|
94
|
+
const { startIndex, count } = clampPaging(input.query, maxPageSize);
|
|
95
|
+
const page = await input.directory.findUsers(input.tenant.id, {
|
|
96
|
+
...filter.value !== undefined ? { filter: filter.value } : {},
|
|
97
|
+
startIndex,
|
|
98
|
+
count
|
|
99
|
+
});
|
|
100
|
+
const r = unwrap(page);
|
|
101
|
+
if ("response" in r)
|
|
102
|
+
return r.response;
|
|
103
|
+
return {
|
|
104
|
+
status: 200,
|
|
105
|
+
body: serializeList(r.value.resources, r.value.totalResults, startIndex, input.baseUrl)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function createUser(input) {
|
|
109
|
+
const parsed = parseUserWrite(input.body);
|
|
110
|
+
if (!parsed.ok) {
|
|
111
|
+
return fail(parsed.error.status, parsed.error.detail, parsed.error.scimType);
|
|
112
|
+
}
|
|
113
|
+
const created_ = await input.directory.createUser(input.tenant.id, parsed.value);
|
|
114
|
+
const r = unwrap(created_);
|
|
115
|
+
if ("response" in r)
|
|
116
|
+
return r.response;
|
|
117
|
+
return created(serializeUser(r.value, input.baseUrl));
|
|
118
|
+
}
|
|
119
|
+
async function loadUser(input, id) {
|
|
120
|
+
const found = await input.directory.getUser(input.tenant.id, id);
|
|
121
|
+
const r = unwrap(found);
|
|
122
|
+
if ("response" in r)
|
|
123
|
+
return r;
|
|
124
|
+
if (r.value === null) {
|
|
125
|
+
return { response: fail(404, `no User with id "${id}"`) };
|
|
126
|
+
}
|
|
127
|
+
return { user: r.value };
|
|
128
|
+
}
|
|
129
|
+
async function replaceUser(input, id) {
|
|
130
|
+
const existing = await loadUser(input, id);
|
|
131
|
+
if ("response" in existing)
|
|
132
|
+
return existing.response;
|
|
133
|
+
const parsed = parseUserWrite(input.body);
|
|
134
|
+
if (!parsed.ok) {
|
|
135
|
+
return fail(parsed.error.status, parsed.error.detail, parsed.error.scimType);
|
|
136
|
+
}
|
|
137
|
+
const write = parsed.value;
|
|
138
|
+
const replaced = await input.directory.replaceUser(input.tenant.id, id, write);
|
|
139
|
+
const r = unwrap(replaced);
|
|
140
|
+
if ("response" in r)
|
|
141
|
+
return r.response;
|
|
142
|
+
return { status: 200, body: serializeUser(r.value, input.baseUrl) };
|
|
143
|
+
}
|
|
144
|
+
async function patchUser(input, id) {
|
|
145
|
+
const existing = await loadUser(input, id);
|
|
146
|
+
if ("response" in existing)
|
|
147
|
+
return existing.response;
|
|
148
|
+
const normalized = normalizePatch(input.body, existing.user);
|
|
149
|
+
if (!normalized.ok) {
|
|
150
|
+
return fail(normalized.error.status, normalized.error.detail, normalized.error.scimType);
|
|
151
|
+
}
|
|
152
|
+
const patched = await input.directory.patchUser(input.tenant.id, id, normalized.value);
|
|
153
|
+
const r = unwrap(patched);
|
|
154
|
+
if ("response" in r)
|
|
155
|
+
return r.response;
|
|
156
|
+
return { status: 200, body: serializeUser(r.value, input.baseUrl) };
|
|
157
|
+
}
|
|
158
|
+
async function deleteUser(input, id) {
|
|
159
|
+
const existing = await loadUser(input, id);
|
|
160
|
+
if ("response" in existing)
|
|
161
|
+
return existing.response;
|
|
162
|
+
const deleted = await input.directory.deleteUser(input.tenant.id, id);
|
|
163
|
+
const r = unwrap(deleted);
|
|
164
|
+
if ("response" in r)
|
|
165
|
+
return r.response;
|
|
166
|
+
return { status: 204, body: null };
|
|
167
|
+
}
|
|
168
|
+
function groupsSupported(d) {
|
|
169
|
+
return typeof d.getGroup === "function" && typeof d.findGroups === "function" && typeof d.createGroup === "function" && typeof d.replaceGroup === "function" && typeof d.patchGroup === "function" && typeof d.deleteGroup === "function";
|
|
170
|
+
}
|
|
171
|
+
function excludesMembers(query) {
|
|
172
|
+
const raw = query.get("excludedAttributes");
|
|
173
|
+
if (raw === null)
|
|
174
|
+
return false;
|
|
175
|
+
return raw.split(",").map((s) => s.trim().toLowerCase()).includes("members");
|
|
176
|
+
}
|
|
177
|
+
async function loadGroup(input, groups, id) {
|
|
178
|
+
const found = await groups.getGroup(input.tenant.id, id);
|
|
179
|
+
const r = unwrap(found);
|
|
180
|
+
if ("response" in r)
|
|
181
|
+
return r;
|
|
182
|
+
if (r.value === null) {
|
|
183
|
+
return { response: fail(404, `no Group with id "${id}"`) };
|
|
184
|
+
}
|
|
185
|
+
return { group: r.value };
|
|
186
|
+
}
|
|
187
|
+
async function handleGroups(input, groups, path, method, maxPageSize) {
|
|
188
|
+
if (path === "/Groups" || path === "/Groups/") {
|
|
189
|
+
if (method === "GET") {
|
|
190
|
+
const filter = parseScimFilter(input.query.get("filter"), GROUP_FILTER_ATTRIBUTES);
|
|
191
|
+
if (!filter.ok)
|
|
192
|
+
return fail(400, filter.error.detail, "invalidFilter");
|
|
193
|
+
const { startIndex, count } = clampPaging(input.query, maxPageSize);
|
|
194
|
+
const page = await groups.findGroups(input.tenant.id, {
|
|
195
|
+
...filter.value !== undefined ? { filter: filter.value } : {},
|
|
196
|
+
startIndex,
|
|
197
|
+
count,
|
|
198
|
+
excludeMembers: excludesMembers(input.query)
|
|
199
|
+
});
|
|
200
|
+
const r = unwrap(page);
|
|
201
|
+
if ("response" in r)
|
|
202
|
+
return r.response;
|
|
203
|
+
return {
|
|
204
|
+
status: 200,
|
|
205
|
+
body: serializeGroupList(r.value.resources, r.value.totalResults, startIndex, input.baseUrl)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (method === "POST") {
|
|
209
|
+
const parsed = parseGroupWrite(input.body);
|
|
210
|
+
if (!parsed.ok) {
|
|
211
|
+
return fail(parsed.error.status, parsed.error.detail, parsed.error.scimType);
|
|
212
|
+
}
|
|
213
|
+
const createdGroup = await groups.createGroup(input.tenant.id, parsed.value);
|
|
214
|
+
const r = unwrap(createdGroup);
|
|
215
|
+
if ("response" in r)
|
|
216
|
+
return r.response;
|
|
217
|
+
return created(serializeGroup(r.value, input.baseUrl));
|
|
218
|
+
}
|
|
219
|
+
return fail(405, `${method} is not allowed on /Groups`);
|
|
220
|
+
}
|
|
221
|
+
const m = /^\/Groups\/(.+)$/.exec(path);
|
|
222
|
+
if (!m)
|
|
223
|
+
return fail(404, `unknown SCIM endpoint "${path}"`);
|
|
224
|
+
let id;
|
|
225
|
+
try {
|
|
226
|
+
id = decodeURIComponent(m[1]);
|
|
227
|
+
} catch {
|
|
228
|
+
id = m[1];
|
|
229
|
+
}
|
|
230
|
+
switch (method) {
|
|
231
|
+
case "GET": {
|
|
232
|
+
const existing = await loadGroup(input, groups, id);
|
|
233
|
+
if ("response" in existing)
|
|
234
|
+
return existing.response;
|
|
235
|
+
return {
|
|
236
|
+
status: 200,
|
|
237
|
+
body: serializeGroup(existing.group, input.baseUrl)
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
case "PUT": {
|
|
241
|
+
const existing = await loadGroup(input, groups, id);
|
|
242
|
+
if ("response" in existing)
|
|
243
|
+
return existing.response;
|
|
244
|
+
const parsed = parseGroupWrite(input.body);
|
|
245
|
+
if (!parsed.ok) {
|
|
246
|
+
return fail(parsed.error.status, parsed.error.detail, parsed.error.scimType);
|
|
247
|
+
}
|
|
248
|
+
const replaced = await groups.replaceGroup(input.tenant.id, id, parsed.value);
|
|
249
|
+
const r = unwrap(replaced);
|
|
250
|
+
if ("response" in r)
|
|
251
|
+
return r.response;
|
|
252
|
+
return { status: 200, body: serializeGroup(r.value, input.baseUrl) };
|
|
253
|
+
}
|
|
254
|
+
case "PATCH": {
|
|
255
|
+
const existing = await loadGroup(input, groups, id);
|
|
256
|
+
if ("response" in existing)
|
|
257
|
+
return existing.response;
|
|
258
|
+
const normalized = normalizeGroupPatch(input.body);
|
|
259
|
+
if (!normalized.ok) {
|
|
260
|
+
return fail(normalized.error.status, normalized.error.detail, normalized.error.scimType);
|
|
261
|
+
}
|
|
262
|
+
const patched = await groups.patchGroup(input.tenant.id, id, normalized.value);
|
|
263
|
+
const r = unwrap(patched);
|
|
264
|
+
if ("response" in r)
|
|
265
|
+
return r.response;
|
|
266
|
+
return { status: 200, body: serializeGroup(r.value, input.baseUrl) };
|
|
267
|
+
}
|
|
268
|
+
case "DELETE": {
|
|
269
|
+
const existing = await loadGroup(input, groups, id);
|
|
270
|
+
if ("response" in existing)
|
|
271
|
+
return existing.response;
|
|
272
|
+
const deleted = await groups.deleteGroup(input.tenant.id, id);
|
|
273
|
+
const r = unwrap(deleted);
|
|
274
|
+
if ("response" in r)
|
|
275
|
+
return r.response;
|
|
276
|
+
return { status: 204, body: null };
|
|
277
|
+
}
|
|
278
|
+
default:
|
|
279
|
+
return fail(405, `${method} is not allowed on /Groups/{id}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function handleScimRequest(input) {
|
|
283
|
+
const scim = input.tenant.config.scim;
|
|
284
|
+
const authFailure = await authenticate(scim, input.authorization);
|
|
285
|
+
if (authFailure)
|
|
286
|
+
return authFailure;
|
|
287
|
+
const maxPageSize = Math.min(scim?.maxPageSize ?? DEFAULT_PAGE_SIZE, PAGE_SIZE_CEILING);
|
|
288
|
+
const path = input.path === "" ? "/" : input.path;
|
|
289
|
+
const method = input.method.toUpperCase();
|
|
290
|
+
if (path === "/ServiceProviderConfig" || path === "/ResourceTypes" || path === "/Schemas") {
|
|
291
|
+
if (method !== "GET") {
|
|
292
|
+
return fail(405, `${method} is not allowed on ${path}`);
|
|
293
|
+
}
|
|
294
|
+
const groups = groupsSupported(input.directory);
|
|
295
|
+
const body = path === "/ServiceProviderConfig" ? serviceProviderConfig(input.baseUrl, maxPageSize) : path === "/ResourceTypes" ? resourceTypes(input.baseUrl, groups) : schemas(input.baseUrl, groups);
|
|
296
|
+
return { status: 200, body };
|
|
297
|
+
}
|
|
298
|
+
if (path === "/Users" || path === "/Users/") {
|
|
299
|
+
if (method === "GET")
|
|
300
|
+
return listUsers(input, maxPageSize);
|
|
301
|
+
if (method === "POST")
|
|
302
|
+
return createUser(input);
|
|
303
|
+
return fail(405, `${method} is not allowed on /Users`);
|
|
304
|
+
}
|
|
305
|
+
const id = userIdFrom(path);
|
|
306
|
+
if (id !== null) {
|
|
307
|
+
switch (method) {
|
|
308
|
+
case "GET": {
|
|
309
|
+
const existing = await loadUser(input, id);
|
|
310
|
+
if ("response" in existing)
|
|
311
|
+
return existing.response;
|
|
312
|
+
return {
|
|
313
|
+
status: 200,
|
|
314
|
+
body: serializeUser(existing.user, input.baseUrl)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
case "PUT":
|
|
318
|
+
return replaceUser(input, id);
|
|
319
|
+
case "PATCH":
|
|
320
|
+
return patchUser(input, id);
|
|
321
|
+
case "DELETE":
|
|
322
|
+
return deleteUser(input, id);
|
|
323
|
+
default:
|
|
324
|
+
return fail(405, `${method} is not allowed on /Users/{id}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (path.startsWith("/Groups")) {
|
|
328
|
+
if (!groupsSupported(input.directory)) {
|
|
329
|
+
return fail(501, "Group provisioning is not implemented by this Service Provider");
|
|
330
|
+
}
|
|
331
|
+
return handleGroups(input, input.directory, path, method, maxPageSize);
|
|
332
|
+
}
|
|
333
|
+
return fail(404, `unknown SCIM endpoint "${path}"`);
|
|
334
|
+
}
|
|
335
|
+
export {
|
|
336
|
+
SUPPORTED_FILTER_HELP,
|
|
337
|
+
handleScimRequest
|
|
338
|
+
};
|