@milaboratories/pl-client 3.14.7 → 3.16.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/core/PromiseTracker.d.ts +1 -2
- package/dist/core/PromiseTracker.d.ts.map +1 -1
- package/dist/core/abstract_stream.d.ts +2 -3
- package/dist/core/abstract_stream.d.ts.map +1 -1
- package/dist/core/auth.d.ts +2 -4
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/cache.d.ts +1 -3
- package/dist/core/cache.d.ts.map +1 -1
- package/dist/core/capabilities.d.ts +2 -3
- package/dist/core/capabilities.d.ts.map +1 -1
- package/dist/core/client.cjs +7 -0
- package/dist/core/client.cjs.map +1 -1
- package/dist/core/client.d.ts +8 -5
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +7 -0
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +29 -31
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/default_client.d.ts +2 -4
- package/dist/core/default_client.d.ts.map +1 -1
- package/dist/core/driver.d.ts +4 -6
- package/dist/core/driver.d.ts.map +1 -1
- package/dist/core/errors.d.ts +20 -22
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/final.d.ts +4 -5
- package/dist/core/final.d.ts.map +1 -1
- package/dist/core/ll_client.cjs +45 -18
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts +21 -10
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +45 -18
- package/dist/core/ll_client.js.map +1 -1
- package/dist/core/ll_transaction.d.ts +4 -6
- package/dist/core/ll_transaction.d.ts.map +1 -1
- package/dist/core/stat.d.ts +2 -3
- package/dist/core/stat.d.ts.map +1 -1
- package/dist/core/transaction.cjs +2 -1
- package/dist/core/transaction.cjs.map +1 -1
- package/dist/core/transaction.d.ts +30 -32
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +2 -1
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/tree_filter.d.ts +14 -10
- package/dist/core/tree_filter.d.ts.map +1 -1
- package/dist/core/types.d.ts +54 -55
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/unauth_client.cjs +79 -32
- package/dist/core/unauth_client.cjs.map +1 -1
- package/dist/core/unauth_client.d.ts +53 -20
- package/dist/core/unauth_client.d.ts.map +1 -1
- package/dist/core/unauth_client.js +79 -32
- package/dist/core/unauth_client.js.map +1 -1
- package/dist/core/user_resources.cjs +23 -0
- package/dist/core/user_resources.cjs.map +1 -1
- package/dist/core/user_resources.d.ts +33 -7
- package/dist/core/user_resources.d.ts.map +1 -1
- package/dist/core/user_resources.js +23 -0
- package/dist/core/user_resources.js.map +1 -1
- package/dist/core/wire.d.ts +5 -7
- package/dist/core/wire.d.ts.map +1 -1
- package/dist/helpers/pl.d.ts +32 -34
- package/dist/helpers/pl.d.ts.map +1 -1
- package/dist/helpers/poll.d.ts +8 -9
- package/dist/helpers/poll.d.ts.map +1 -1
- package/dist/helpers/tx_helpers.d.ts +2 -4
- package/dist/helpers/tx_helpers.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.d.ts +2 -4
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +310 -15
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +19 -3
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +29 -5
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +19 -3
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +2336 -429
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +309 -16
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts +51 -30
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts +7 -9
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts +6 -8
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/any.d.ts +2 -4
- package/dist/proto-grpc/google/protobuf/any.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/duration.d.ts +2 -4
- package/dist/proto-grpc/google/protobuf/duration.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.d.ts +2 -4
- package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/proto-grpc/google/rpc/code.d.ts +1 -2
- package/dist/proto-grpc/google/rpc/code.d.ts.map +1 -1
- package/dist/proto-rest/index.d.ts +6 -8
- package/dist/proto-rest/index.d.ts.map +1 -1
- package/dist/proto-rest/plapi.d.ts +248 -123
- package/dist/proto-rest/plapi.d.ts.map +1 -1
- package/dist/test/tcp-proxy.d.ts +3 -5
- package/dist/test/tcp-proxy.d.ts.map +1 -1
- package/dist/test/test_config.d.ts +13 -14
- package/dist/test/test_config.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/core/client.ts +9 -0
- package/src/core/ll_client.ts +40 -13
- package/src/core/transaction.ts +2 -0
- package/src/core/unauth_client.test.ts +69 -1
- package/src/core/unauth_client.ts +108 -34
- package/src/core/unauth_client_branch.test.ts +416 -5
- package/src/core/user_resources.ts +39 -0
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +34 -3
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +431 -15
- package/src/proto-rest/plapi.ts +8 -2
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import { test, expect, vi } from "vitest";
|
|
2
2
|
import { UnauthenticatedPlClient } from "./unauth_client";
|
|
3
|
+
import { LLPlClient } from "./ll_client";
|
|
4
|
+
import { PlatformClient as GrpcPlApiClient } from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client";
|
|
3
5
|
import type { BackendCapability } from "./capabilities";
|
|
6
|
+
import {
|
|
7
|
+
AuthAPI_ListMethods_SSOAuthMethod_FlowType,
|
|
8
|
+
type AuthAPI_ListMethods_Response,
|
|
9
|
+
} from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api";
|
|
4
10
|
|
|
5
11
|
type Scheme = "basic" | "token" | "none";
|
|
12
|
+
type MethodInfo = AuthAPI_ListMethods_Response["methods"][number];
|
|
6
13
|
|
|
7
|
-
function makeStub(opts: { hasAuthV2: boolean; scheme?: Scheme }) {
|
|
14
|
+
function makeStub(opts: { hasAuthV2: boolean; scheme?: Scheme; methods?: MethodInfo[] }) {
|
|
8
15
|
const scheme = opts.scheme ?? "basic";
|
|
9
16
|
const methods =
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
opts.methods ??
|
|
18
|
+
(scheme === "basic"
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
id: "basic",
|
|
22
|
+
title: "basic",
|
|
23
|
+
description: "",
|
|
24
|
+
method: { oneofKind: "basic", basic: {} },
|
|
25
|
+
},
|
|
26
|
+
]
|
|
12
27
|
: scheme === "token"
|
|
13
|
-
? [
|
|
14
|
-
|
|
28
|
+
? [
|
|
29
|
+
{
|
|
30
|
+
id: "token",
|
|
31
|
+
title: "token",
|
|
32
|
+
description: "",
|
|
33
|
+
method: { oneofKind: "token", token: {} },
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
: []);
|
|
15
37
|
const ll = {
|
|
16
38
|
hasCapability: vi.fn(
|
|
17
39
|
(capability: BackendCapability) => capability === "auth:v2" && opts.hasAuthV2,
|
|
@@ -19,6 +41,8 @@ function makeStub(opts: { hasAuthV2: boolean; scheme?: Scheme }) {
|
|
|
19
41
|
loginBasic: vi.fn().mockResolvedValue("jwt-from-loginBasic"),
|
|
20
42
|
loginWithToken: vi.fn().mockResolvedValue("jwt-from-loginWithToken"),
|
|
21
43
|
getJwtToken: vi.fn().mockResolvedValue("jwt-from-getJwtToken"),
|
|
44
|
+
beginSSOLogin: vi.fn().mockResolvedValue({ nonce: "nonce", expiresAt: new Date() }),
|
|
45
|
+
loginSSO: vi.fn().mockResolvedValue("jwt-from-loginSSO"),
|
|
22
46
|
authMethodsSync: { methods },
|
|
23
47
|
conf: { authTTLSeconds: 100 },
|
|
24
48
|
};
|
|
@@ -67,3 +91,390 @@ test("hasCapability proxies to underlying LLPlClient", () => {
|
|
|
67
91
|
expect(client.hasCapability("treeFilter:v2")).toBe(false);
|
|
68
92
|
expect(ll.hasCapability).toHaveBeenCalledWith("auth:v2");
|
|
69
93
|
});
|
|
94
|
+
|
|
95
|
+
function ssoMethod(overrides: {
|
|
96
|
+
id: string;
|
|
97
|
+
title?: string;
|
|
98
|
+
description: string;
|
|
99
|
+
issuer: string;
|
|
100
|
+
flowType?: AuthAPI_ListMethods_SSOAuthMethod_FlowType;
|
|
101
|
+
clientId?: string;
|
|
102
|
+
scopes?: string;
|
|
103
|
+
resource?: string;
|
|
104
|
+
prompt?: string;
|
|
105
|
+
redirectPorts?: number[];
|
|
106
|
+
subjectTokenSource?: string;
|
|
107
|
+
userIdClaim?: string;
|
|
108
|
+
groupsClaim?: string;
|
|
109
|
+
accessType?: string;
|
|
110
|
+
}): MethodInfo {
|
|
111
|
+
return {
|
|
112
|
+
id: overrides.id,
|
|
113
|
+
title: overrides.title ?? overrides.id,
|
|
114
|
+
description: overrides.description,
|
|
115
|
+
method: {
|
|
116
|
+
oneofKind: "sso",
|
|
117
|
+
sso: {
|
|
118
|
+
issuer: overrides.issuer,
|
|
119
|
+
clientId: overrides.clientId ?? "client-id",
|
|
120
|
+
scopes: overrides.scopes ?? "openid",
|
|
121
|
+
resource: overrides.resource ?? "",
|
|
122
|
+
prompt: overrides.prompt ?? "",
|
|
123
|
+
redirectPorts: overrides.redirectPorts ?? [12345],
|
|
124
|
+
subjectTokenSource: overrides.subjectTokenSource ?? "id_token",
|
|
125
|
+
userIdClaim: overrides.userIdClaim ?? "sub",
|
|
126
|
+
groupsClaim: overrides.groupsClaim ?? "",
|
|
127
|
+
flowType: overrides.flowType ?? AuthAPI_ListMethods_SSOAuthMethod_FlowType.PUBLIC_PKCE,
|
|
128
|
+
accessType: overrides.accessType,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function basicMethod(id: string, description = "Basic auth", title = id): MethodInfo {
|
|
135
|
+
return { id, title, description, method: { oneofKind: "basic", basic: {} } };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function tokenMethod(id: string, description = "Static token", title = id): MethodInfo {
|
|
139
|
+
return { id, title, description, method: { oneofKind: "token", token: {} } };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- loginMethods() ---
|
|
143
|
+
|
|
144
|
+
test("loginMethods lists every advertised method, of every kind, in advertised order", () => {
|
|
145
|
+
const methods: MethodInfo[] = [
|
|
146
|
+
ssoMethod({ id: "sso-a", description: "First IdP", issuer: "https://first.example" }),
|
|
147
|
+
ssoMethod({ id: "sso-b", description: "Second IdP", issuer: "https://second.example" }),
|
|
148
|
+
basicMethod("htpasswd-a", "htpasswd"),
|
|
149
|
+
basicMethod("htpasswd-b", "htpasswd"),
|
|
150
|
+
tokenMethod("token-a", "static token"),
|
|
151
|
+
];
|
|
152
|
+
const { client } = makeStub({ hasAuthV2: true, methods });
|
|
153
|
+
|
|
154
|
+
const listed = client.loginMethods();
|
|
155
|
+
|
|
156
|
+
expect(listed.map((m) => m.kind)).toEqual(["sso", "sso", "basic", "basic", "token"]);
|
|
157
|
+
expect(listed.map((m) => m.id)).toEqual([
|
|
158
|
+
"sso-a",
|
|
159
|
+
"sso-b",
|
|
160
|
+
"htpasswd-a",
|
|
161
|
+
"htpasswd-b",
|
|
162
|
+
"token-a",
|
|
163
|
+
]);
|
|
164
|
+
expect(listed.map((m) => m.title)).toEqual([
|
|
165
|
+
"sso-a",
|
|
166
|
+
"sso-b",
|
|
167
|
+
"htpasswd-a",
|
|
168
|
+
"htpasswd-b",
|
|
169
|
+
"token-a",
|
|
170
|
+
]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("loginMethods falls back to description, then id, when the backend omits title", () => {
|
|
174
|
+
const methods: MethodInfo[] = [
|
|
175
|
+
{
|
|
176
|
+
id: "no-title",
|
|
177
|
+
title: "",
|
|
178
|
+
description: "Untitled backend",
|
|
179
|
+
method: { oneofKind: "basic", basic: {} },
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "no-title-no-description",
|
|
183
|
+
title: "",
|
|
184
|
+
description: "",
|
|
185
|
+
method: { oneofKind: "token", token: {} },
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
const { client } = makeStub({ hasAuthV2: true, methods });
|
|
189
|
+
|
|
190
|
+
const listed = client.loginMethods();
|
|
191
|
+
|
|
192
|
+
expect(listed.map((m) => m.title)).toEqual(["Untitled backend", "no-title-no-description"]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("loginMethods keeps same-kind methods distinct while supportedAuthSchemes still collapses them", () => {
|
|
196
|
+
const methods: MethodInfo[] = [
|
|
197
|
+
basicMethod("htpasswd-a", "htpasswd"),
|
|
198
|
+
basicMethod("htpasswd-b", "htpasswd"),
|
|
199
|
+
tokenMethod("token-a"),
|
|
200
|
+
];
|
|
201
|
+
const { client } = makeStub({ hasAuthV2: true, methods });
|
|
202
|
+
|
|
203
|
+
const listed = client.loginMethods().filter((m) => m.kind === "basic");
|
|
204
|
+
|
|
205
|
+
expect(listed.map((m) => m.id)).toEqual(["htpasswd-a", "htpasswd-b"]);
|
|
206
|
+
expect(client.supportedAuthSchemes).toEqual({ basic: true, token: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("loginMethods maps every field of an SSO projection, and drops an empty accessType", () => {
|
|
210
|
+
const method = ssoMethod({
|
|
211
|
+
id: "sso-1",
|
|
212
|
+
title: "Corporate SSO",
|
|
213
|
+
description: "Corp SSO",
|
|
214
|
+
issuer: "https://idp.example",
|
|
215
|
+
clientId: "the-client-id",
|
|
216
|
+
scopes: "openid profile",
|
|
217
|
+
resource: "https://api.example",
|
|
218
|
+
prompt: "consent",
|
|
219
|
+
redirectPorts: [11000, 11001],
|
|
220
|
+
subjectTokenSource: "access_token",
|
|
221
|
+
userIdClaim: "email",
|
|
222
|
+
groupsClaim: "groups",
|
|
223
|
+
accessType: "",
|
|
224
|
+
});
|
|
225
|
+
const { client } = makeStub({ hasAuthV2: true, methods: [method] });
|
|
226
|
+
|
|
227
|
+
const [entry] = client.loginMethods();
|
|
228
|
+
|
|
229
|
+
expect(entry).toEqual({
|
|
230
|
+
kind: "sso",
|
|
231
|
+
id: "sso-1",
|
|
232
|
+
title: "Corporate SSO",
|
|
233
|
+
description: "Corp SSO",
|
|
234
|
+
issuer: "https://idp.example",
|
|
235
|
+
clientId: "the-client-id",
|
|
236
|
+
scopes: "openid profile",
|
|
237
|
+
resource: "https://api.example",
|
|
238
|
+
prompt: "consent",
|
|
239
|
+
redirectPorts: [11000, 11001],
|
|
240
|
+
subjectTokenSource: "access_token",
|
|
241
|
+
userIdClaim: "email",
|
|
242
|
+
groupsClaim: "groups",
|
|
243
|
+
flowType: "public_pkce",
|
|
244
|
+
accessType: undefined,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("loginMethods drops an advertised method with no usable arm", () => {
|
|
249
|
+
const armless: MethodInfo = {
|
|
250
|
+
id: "legacy",
|
|
251
|
+
title: "legacy",
|
|
252
|
+
description: "",
|
|
253
|
+
method: { oneofKind: undefined },
|
|
254
|
+
};
|
|
255
|
+
const { client } = makeStub({ hasAuthV2: true, methods: [armless, basicMethod("htpasswd-a")] });
|
|
256
|
+
|
|
257
|
+
expect(client.loginMethods().map((m) => m.id)).toEqual(["htpasswd-a"]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("loginMethods drops an SSO entry whose flow type is unsupported, keeping every other entry", () => {
|
|
261
|
+
const unsupported = ssoMethod({
|
|
262
|
+
id: "sso-legacy",
|
|
263
|
+
description: "Corp SSO",
|
|
264
|
+
issuer: "https://idp.example",
|
|
265
|
+
flowType: 99 as AuthAPI_ListMethods_SSOAuthMethod_FlowType,
|
|
266
|
+
});
|
|
267
|
+
const { client } = makeStub({
|
|
268
|
+
hasAuthV2: true,
|
|
269
|
+
methods: [unsupported, basicMethod("htpasswd-a")],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(client.loginMethods().map((m) => m.id)).toEqual(["htpasswd-a"]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("beginSSOLogin forwards the picked id to the low-level client", async () => {
|
|
276
|
+
const { client, ll } = makeStub({ hasAuthV2: true, scheme: "none" });
|
|
277
|
+
|
|
278
|
+
await client.beginSSOLogin("sso-a");
|
|
279
|
+
|
|
280
|
+
expect(ll.beginSSOLogin).toHaveBeenCalledWith("sso-a");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("beginSSOLogin omits the argument when no id is picked", async () => {
|
|
284
|
+
const { client, ll } = makeStub({ hasAuthV2: true, scheme: "none" });
|
|
285
|
+
|
|
286
|
+
await client.beginSSOLogin();
|
|
287
|
+
|
|
288
|
+
expect(ll.beginSSOLogin).toHaveBeenCalledWith();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("loginSSO carries the picked id to the low-level client alongside the token response", async () => {
|
|
292
|
+
const { client, ll } = makeStub({ hasAuthV2: true, scheme: "none" });
|
|
293
|
+
const tokenResponse = new Uint8Array([1, 2, 3]);
|
|
294
|
+
|
|
295
|
+
await client.loginSSO({ tokenResponse, idP: "sso-a" });
|
|
296
|
+
|
|
297
|
+
expect(ll.loginSSO).toHaveBeenCalledWith(tokenResponse, "sso-a");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("loginSSO sends only the token response when no id is picked", async () => {
|
|
301
|
+
const { client, ll } = makeStub({ hasAuthV2: true, scheme: "none" });
|
|
302
|
+
const tokenResponse = new Uint8Array([1, 2, 3]);
|
|
303
|
+
|
|
304
|
+
await client.loginSSO({ tokenResponse });
|
|
305
|
+
|
|
306
|
+
expect(ll.loginSSO).toHaveBeenCalledWith(tokenResponse);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("login forwards the picked id into loginBasic's options bag", async () => {
|
|
310
|
+
const { client, ll } = makeStub({ hasAuthV2: true, scheme: "basic" });
|
|
311
|
+
|
|
312
|
+
await client.login("alice", "pw", "htpasswd-b");
|
|
313
|
+
|
|
314
|
+
expect(ll.loginBasic).toHaveBeenCalledWith("alice", "pw", { idP: "htpasswd-b" });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("login drops the picked id on the token branch", async () => {
|
|
318
|
+
const { client, ll } = makeStub({ hasAuthV2: true, scheme: "token" });
|
|
319
|
+
|
|
320
|
+
const info = await client.login("alice", "opaque-token", "some-id");
|
|
321
|
+
|
|
322
|
+
expect(ll.loginWithToken).toHaveBeenCalledWith("opaque-token");
|
|
323
|
+
expect(info.jwtToken).toBe("jwt-from-loginWithToken");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("login routes to loginWithToken when the picked id names a token method, even though the backend also advertises basic", async () => {
|
|
327
|
+
const { client, ll } = makeStub({
|
|
328
|
+
hasAuthV2: true,
|
|
329
|
+
methods: [
|
|
330
|
+
{ id: "ldap-a", title: "ldap-a", description: "", method: { oneofKind: "basic", basic: {} } },
|
|
331
|
+
{
|
|
332
|
+
id: "htpasswd",
|
|
333
|
+
title: "htpasswd",
|
|
334
|
+
description: "",
|
|
335
|
+
method: { oneofKind: "token", token: {} },
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const info = await client.login("alice", "opaque-token", "htpasswd");
|
|
341
|
+
|
|
342
|
+
expect(ll.loginWithToken).toHaveBeenCalledWith("opaque-token");
|
|
343
|
+
expect(ll.loginBasic).not.toHaveBeenCalled();
|
|
344
|
+
expect(info.jwtToken).toBe("jwt-from-loginWithToken");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("ssoConfig returns the first sso method and drops the rest", () => {
|
|
348
|
+
const first = ssoMethod({
|
|
349
|
+
id: "sso-first",
|
|
350
|
+
description: "First IdP",
|
|
351
|
+
issuer: "https://first.example",
|
|
352
|
+
});
|
|
353
|
+
const second = ssoMethod({
|
|
354
|
+
id: "sso-second",
|
|
355
|
+
description: "Second IdP",
|
|
356
|
+
issuer: "https://second.example",
|
|
357
|
+
});
|
|
358
|
+
const { client } = makeStub({ hasAuthV2: true, methods: [first, second] });
|
|
359
|
+
|
|
360
|
+
const sso = client.ssoConfig();
|
|
361
|
+
|
|
362
|
+
expect(sso).toMatchObject({
|
|
363
|
+
id: "sso-first",
|
|
364
|
+
description: "First IdP",
|
|
365
|
+
issuer: "https://first.example",
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("ssoConfig carries id, title and description onto the returned SSOAuthMethod", () => {
|
|
370
|
+
const method = ssoMethod({
|
|
371
|
+
id: "sso-1",
|
|
372
|
+
title: "Corporate IdP",
|
|
373
|
+
description: "Corp SSO",
|
|
374
|
+
issuer: "https://idp.example",
|
|
375
|
+
});
|
|
376
|
+
const { client } = makeStub({ hasAuthV2: true, methods: [method] });
|
|
377
|
+
|
|
378
|
+
const sso = client.ssoConfig();
|
|
379
|
+
|
|
380
|
+
expect(sso?.id).toBe("sso-1");
|
|
381
|
+
expect(sso?.title).toBe("Corporate IdP");
|
|
382
|
+
expect(sso?.description).toBe("Corp SSO");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("ssoConfig throws when the sso method's flow type is not PUBLIC_PKCE", () => {
|
|
386
|
+
const method = ssoMethod({
|
|
387
|
+
id: "sso-1",
|
|
388
|
+
description: "Corp SSO",
|
|
389
|
+
issuer: "https://idp.example",
|
|
390
|
+
// 99 is not a defined FlowType value; casts an out-of-range value the
|
|
391
|
+
// proto currently has none for, to pin the guard against a future flow.
|
|
392
|
+
flowType: 99 as AuthAPI_ListMethods_SSOAuthMethod_FlowType,
|
|
393
|
+
});
|
|
394
|
+
const { client } = makeStub({ hasAuthV2: true, methods: [method] });
|
|
395
|
+
|
|
396
|
+
expect(() => client.ssoConfig()).toThrow("ssoConfig: unsupported SSO flow type");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("supportedAuthSchemes collapses a basic+token+sso method list to scheme booleans", () => {
|
|
400
|
+
const methods: MethodInfo[] = [
|
|
401
|
+
{ id: "basic", title: "basic", description: "", method: { oneofKind: "basic", basic: {} } },
|
|
402
|
+
{ id: "token", title: "token", description: "", method: { oneofKind: "token", token: {} } },
|
|
403
|
+
ssoMethod({ id: "sso-1", description: "Corp SSO", issuer: "https://idp.example" }),
|
|
404
|
+
];
|
|
405
|
+
const { client } = makeStub({ hasAuthV2: true, methods });
|
|
406
|
+
|
|
407
|
+
expect(client.supportedAuthSchemes).toEqual({ basic: true, token: true });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// --- LLPlClient.loginBasic carries the id on both transports ---
|
|
411
|
+
|
|
412
|
+
function makeLLStub(cl: { login?: ReturnType<typeof vi.fn>; POST?: ReturnType<typeof vi.fn> }) {
|
|
413
|
+
return Object.assign(Object.create(LLPlClient.prototype) as object, {
|
|
414
|
+
clientProvider: { get: () => cl },
|
|
415
|
+
conf: { authTTLSeconds: 100 },
|
|
416
|
+
}) as LLPlClient;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
test("loginBasic carries idP in the request body on the gRPC transport", async () => {
|
|
420
|
+
const login = vi.fn(() => ({ response: Promise.resolve({ token: "jwt" }) }));
|
|
421
|
+
const cl = Object.assign(Object.create(GrpcPlApiClient.prototype) as object, { login });
|
|
422
|
+
const ll = makeLLStub(cl);
|
|
423
|
+
|
|
424
|
+
await ll.loginBasic("alice", "pw", { idP: "htpasswd-b" });
|
|
425
|
+
|
|
426
|
+
expect(login).toHaveBeenCalledWith(
|
|
427
|
+
expect.objectContaining({
|
|
428
|
+
credentials: {
|
|
429
|
+
oneofKind: "basic",
|
|
430
|
+
basic: { login: "alice", password: "pw", idp: "htpasswd-b" },
|
|
431
|
+
},
|
|
432
|
+
}),
|
|
433
|
+
expect.anything(),
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("loginBasic without idP produces the same gRPC request body as today", async () => {
|
|
438
|
+
const login = vi.fn(() => ({ response: Promise.resolve({ token: "jwt" }) }));
|
|
439
|
+
const cl = Object.assign(Object.create(GrpcPlApiClient.prototype) as object, { login });
|
|
440
|
+
const ll = makeLLStub(cl);
|
|
441
|
+
|
|
442
|
+
await ll.loginBasic("alice", "pw");
|
|
443
|
+
|
|
444
|
+
expect(login).toHaveBeenCalledWith(
|
|
445
|
+
expect.objectContaining({
|
|
446
|
+
credentials: { oneofKind: "basic", basic: { login: "alice", password: "pw" } },
|
|
447
|
+
}),
|
|
448
|
+
expect.anything(),
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("loginBasic carries idP in the request body on the REST transport", async () => {
|
|
453
|
+
const POST = vi.fn(() => Promise.resolve({ data: { token: "jwt" } }));
|
|
454
|
+
const ll = makeLLStub({ POST });
|
|
455
|
+
|
|
456
|
+
await ll.loginBasic("alice", "pw", { idP: "htpasswd-b" });
|
|
457
|
+
|
|
458
|
+
expect(POST).toHaveBeenCalledWith(
|
|
459
|
+
"/v1/auth/login",
|
|
460
|
+
expect.objectContaining({
|
|
461
|
+
body: expect.objectContaining({
|
|
462
|
+
basic: { login: "alice", password: "pw", idp: "htpasswd-b" },
|
|
463
|
+
}),
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("loginBasic without idP produces the same REST request body as today", async () => {
|
|
469
|
+
const POST = vi.fn(() => Promise.resolve({ data: { token: "jwt" } }));
|
|
470
|
+
const ll = makeLLStub({ POST });
|
|
471
|
+
|
|
472
|
+
await ll.loginBasic("alice", "pw");
|
|
473
|
+
|
|
474
|
+
expect(POST).toHaveBeenCalledWith(
|
|
475
|
+
"/v1/auth/login",
|
|
476
|
+
expect.objectContaining({
|
|
477
|
+
body: expect.objectContaining({ basic: { login: "alice", password: "pw" } }),
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
});
|
|
@@ -36,6 +36,20 @@ export interface UserEntry {
|
|
|
36
36
|
readonly login: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** What {@link UserResources.deleteUser} removed, so a caller can report it. */
|
|
40
|
+
export interface UserDeletionReport {
|
|
41
|
+
/** Raw id of the root resource the user owned, or undefined when they had none. Unsigned —
|
|
42
|
+
* the resource is gone, so this is for the audit line, not for further calls. */
|
|
43
|
+
readonly userRootId: bigint | undefined;
|
|
44
|
+
/** Whether that root resource was deleted. */
|
|
45
|
+
readonly userRootDeleted: boolean;
|
|
46
|
+
/** Grants revoked, the user-root self-grant included. */
|
|
47
|
+
readonly revokedGrants: number;
|
|
48
|
+
/** Identity-index entries removed — one per value that resolved to the account: its login,
|
|
49
|
+
* its email, and any alternative of either. */
|
|
50
|
+
readonly removedIdentityIndexEntries: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/** Information about a single data library (LS storage). */
|
|
40
54
|
export interface StorageInfo {
|
|
41
55
|
/** Machine-stable identifier, e.g. "library". Used for filtering and map keys. */
|
|
@@ -257,6 +271,31 @@ export class UserResources {
|
|
|
257
271
|
return users.map((user) => ({ login: user.login }));
|
|
258
272
|
}
|
|
259
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Deletes a user account: the record, every identity-index entry that resolves to it (its login,
|
|
276
|
+
* its email, and any alternative of either), every grant it holds, and its root resource — and
|
|
277
|
+
* with the root, everything still attached under it.
|
|
278
|
+
*
|
|
279
|
+
* Exists for the duplicate account one person can end up with — an identity that could not be
|
|
280
|
+
* matched across a provider cutover, or one minted before the backend started refusing two
|
|
281
|
+
* records for one identity. The spare record shows up wherever users are listed and its projects
|
|
282
|
+
* keep taking part in deduplication.
|
|
283
|
+
*
|
|
284
|
+
* Destructive and irreversible. Re-attach anything worth keeping to another user's root before
|
|
285
|
+
* calling — `pl-cli admin delete-user` does both steps in order. Requires admin/controller
|
|
286
|
+
* credentials; gRPC-only, like {@link listUsers}.
|
|
287
|
+
*/
|
|
288
|
+
async deleteUser(login: string): Promise<UserDeletionReport> {
|
|
289
|
+
const resp = await this.ll.deleteUser(login);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
userRootId: resp.userRootId === 0n ? undefined : resp.userRootId,
|
|
293
|
+
userRootDeleted: resp.userRootDeleted,
|
|
294
|
+
revokedGrants: resp.revokedGrants,
|
|
295
|
+
removedIdentityIndexEntries: resp.removedIdentityIndexEntries,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
260
299
|
private async getUserRootViaRpc(opts: {
|
|
261
300
|
login?: string;
|
|
262
301
|
createIfNotExists: true;
|
|
@@ -10,6 +10,8 @@ import type { MaintenanceAPI_Ping_Response } from "./api";
|
|
|
10
10
|
import type { MaintenanceAPI_Ping_Request } from "./api";
|
|
11
11
|
import type { MiscAPI_ListResourceTypes_Response } from "./api";
|
|
12
12
|
import type { MiscAPI_ListResourceTypes_Request } from "./api";
|
|
13
|
+
import type { AuthAPI_DeleteUser_Response } from "./api";
|
|
14
|
+
import type { AuthAPI_DeleteUser_Request } from "./api";
|
|
13
15
|
import type { AuthAPI_ListUsers_Response } from "./api";
|
|
14
16
|
import type { AuthAPI_ListUsers_Request } from "./api";
|
|
15
17
|
import type { AuthAPI_ListUserResources_Response } from "./api";
|
|
@@ -290,6 +292,19 @@ export interface IPlatformClient {
|
|
|
290
292
|
* @generated from protobuf rpc: ListUsers
|
|
291
293
|
*/
|
|
292
294
|
listUsers(input: AuthAPI_ListUsers_Request, options?: RpcOptions): UnaryCall<AuthAPI_ListUsers_Request, AuthAPI_ListUsers_Response>;
|
|
295
|
+
/**
|
|
296
|
+
* DeleteUser removes a user account from the instance: the user record, every login-index
|
|
297
|
+
* entry that resolves to it, every grant it holds, and its root resource with everything
|
|
298
|
+
* still attached under it. Admin-only and irreversible - re-attach anything worth keeping
|
|
299
|
+
* to another user's root before calling (`pl-cli admin delete-user` does both steps).
|
|
300
|
+
*
|
|
301
|
+
* Exists because multi-provider auth can mint a duplicate account for one person: the waste
|
|
302
|
+
* record shows up in the sharing UI and its projects keep taking part in deduplication, and
|
|
303
|
+
* until now nothing could remove it.
|
|
304
|
+
*
|
|
305
|
+
* @generated from protobuf rpc: DeleteUser
|
|
306
|
+
*/
|
|
307
|
+
deleteUser(input: AuthAPI_DeleteUser_Request, options?: RpcOptions): UnaryCall<AuthAPI_DeleteUser_Request, AuthAPI_DeleteUser_Response>;
|
|
293
308
|
/**
|
|
294
309
|
*
|
|
295
310
|
* Other stuff
|
|
@@ -626,6 +641,22 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
|
|
|
626
641
|
const method = this.methods[34], opt = this._transport.mergeOptions(options);
|
|
627
642
|
return stackIntercept<AuthAPI_ListUsers_Request, AuthAPI_ListUsers_Response>("unary", this._transport, method, opt, input);
|
|
628
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* DeleteUser removes a user account from the instance: the user record, every login-index
|
|
646
|
+
* entry that resolves to it, every grant it holds, and its root resource with everything
|
|
647
|
+
* still attached under it. Admin-only and irreversible - re-attach anything worth keeping
|
|
648
|
+
* to another user's root before calling (`pl-cli admin delete-user` does both steps).
|
|
649
|
+
*
|
|
650
|
+
* Exists because multi-provider auth can mint a duplicate account for one person: the waste
|
|
651
|
+
* record shows up in the sharing UI and its projects keep taking part in deduplication, and
|
|
652
|
+
* until now nothing could remove it.
|
|
653
|
+
*
|
|
654
|
+
* @generated from protobuf rpc: DeleteUser
|
|
655
|
+
*/
|
|
656
|
+
deleteUser(input: AuthAPI_DeleteUser_Request, options?: RpcOptions): UnaryCall<AuthAPI_DeleteUser_Request, AuthAPI_DeleteUser_Response> {
|
|
657
|
+
const method = this.methods[35], opt = this._transport.mergeOptions(options);
|
|
658
|
+
return stackIntercept<AuthAPI_DeleteUser_Request, AuthAPI_DeleteUser_Response>("unary", this._transport, method, opt, input);
|
|
659
|
+
}
|
|
629
660
|
/**
|
|
630
661
|
*
|
|
631
662
|
* Other stuff
|
|
@@ -634,7 +665,7 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
|
|
|
634
665
|
* @generated from protobuf rpc: ListResourceTypes
|
|
635
666
|
*/
|
|
636
667
|
listResourceTypes(input: MiscAPI_ListResourceTypes_Request, options?: RpcOptions): UnaryCall<MiscAPI_ListResourceTypes_Request, MiscAPI_ListResourceTypes_Response> {
|
|
637
|
-
const method = this.methods[
|
|
668
|
+
const method = this.methods[36], opt = this._transport.mergeOptions(options);
|
|
638
669
|
return stackIntercept<MiscAPI_ListResourceTypes_Request, MiscAPI_ListResourceTypes_Response>("unary", this._transport, method, opt, input);
|
|
639
670
|
}
|
|
640
671
|
/**
|
|
@@ -645,14 +676,14 @@ export class PlatformClient implements IPlatformClient, ServiceInfo {
|
|
|
645
676
|
* @generated from protobuf rpc: Ping
|
|
646
677
|
*/
|
|
647
678
|
ping(input: MaintenanceAPI_Ping_Request, options?: RpcOptions): UnaryCall<MaintenanceAPI_Ping_Request, MaintenanceAPI_Ping_Response> {
|
|
648
|
-
const method = this.methods[
|
|
679
|
+
const method = this.methods[37], opt = this._transport.mergeOptions(options);
|
|
649
680
|
return stackIntercept<MaintenanceAPI_Ping_Request, MaintenanceAPI_Ping_Response>("unary", this._transport, method, opt, input);
|
|
650
681
|
}
|
|
651
682
|
/**
|
|
652
683
|
* @generated from protobuf rpc: License
|
|
653
684
|
*/
|
|
654
685
|
license(input: MaintenanceAPI_License_Request, options?: RpcOptions): UnaryCall<MaintenanceAPI_License_Request, MaintenanceAPI_License_Response> {
|
|
655
|
-
const method = this.methods[
|
|
686
|
+
const method = this.methods[38], opt = this._transport.mergeOptions(options);
|
|
656
687
|
return stackIntercept<MaintenanceAPI_License_Request, MaintenanceAPI_License_Response>("unary", this._transport, method, opt, input);
|
|
657
688
|
}
|
|
658
689
|
}
|