@flink-app/generic-request-plugin 2.0.0-alpha.113 → 2.0.0-alpha.115
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/CHANGELOG.md +14 -0
- package/dist/index.js +41 -3
- package/package.json +3 -3
- package/spec/GenericRequestPlugin.spec.ts +99 -6
- package/src/index.ts +24 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @flink-app/generic-request-plugin
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.115
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @flink-app/flink@2.0.0-alpha.115
|
|
8
|
+
|
|
9
|
+
## 2.0.0-alpha.114
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 383c91b: Fix auth rejections being treated as authenticated. `genericRequestPlugin` routes, `streamingPlugin` routes and the s3-plugin legacy upload route checked the auth result with `if (!authenticated)`. A `FlinkResponse` — the shape an auth plugin returns to replace the default 401, e.g. from jwt-auth-plugin's `onAuthFailure` — is a truthy object, so those routes let the rejected request through to the handler. They now use `isAuthSuccess()` and send the plugin's response verbatim (its `headers` applied to the response and stripped from the JSON body, status defaulting to 401), falling back to each route's existing default response when the plugin simply returned `false`. Headers from a successful authentication are applied too — on streaming routes before the stream headers are flushed.
|
|
14
|
+
- Updated dependencies [5f11719]
|
|
15
|
+
- @flink-app/flink@2.0.0-alpha.114
|
|
16
|
+
|
|
3
17
|
## 2.0.0-alpha.113
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
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
|
+
};
|
|
2
13
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
14
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
15
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -35,11 +46,23 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
46
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
47
|
}
|
|
37
48
|
};
|
|
49
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
50
|
+
var t = {};
|
|
51
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
52
|
+
t[p] = s[p];
|
|
53
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
54
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
55
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
56
|
+
t[p[i]] = s[p[i]];
|
|
57
|
+
}
|
|
58
|
+
return t;
|
|
59
|
+
};
|
|
38
60
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
61
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
62
|
};
|
|
41
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
64
|
exports.genericRequestPlugin = exports.HttpMethod = void 0;
|
|
65
|
+
var flink_1 = require("@flink-app/flink");
|
|
43
66
|
var node_color_log_1 = __importDefault(require("node-color-log"));
|
|
44
67
|
var HttpMethod;
|
|
45
68
|
(function (HttpMethod) {
|
|
@@ -63,7 +86,7 @@ function init(app, options) {
|
|
|
63
86
|
throw new Error("Express app not initialized");
|
|
64
87
|
}
|
|
65
88
|
expressApp[options.method](options.path, function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
66
|
-
var
|
|
89
|
+
var authResult, headers, body, status, authHeaders;
|
|
67
90
|
return __generator(this, function (_a) {
|
|
68
91
|
switch (_a.label) {
|
|
69
92
|
case 0:
|
|
@@ -73,8 +96,19 @@ function init(app, options) {
|
|
|
73
96
|
}
|
|
74
97
|
return [4 /*yield*/, app.auth.authenticateRequest(req, options.permissions, app.ctx)];
|
|
75
98
|
case 1:
|
|
76
|
-
|
|
77
|
-
|
|
99
|
+
authResult = _a.sent();
|
|
100
|
+
// Only `true` and a FlinkAuthSuccess mean authenticated. A FlinkResponse is a
|
|
101
|
+
// rejection the plugin shaped itself and is sent verbatim (its `headers` are
|
|
102
|
+
// applied to the response and stripped from the JSON body, status defaults to 401).
|
|
103
|
+
if (!(0, flink_1.isAuthSuccess)(authResult)) {
|
|
104
|
+
if (authResult && typeof authResult === "object") {
|
|
105
|
+
headers = authResult.headers, body = __rest(authResult, ["headers"]);
|
|
106
|
+
status = authResult.status || 401;
|
|
107
|
+
if (headers) {
|
|
108
|
+
res.set(headers);
|
|
109
|
+
}
|
|
110
|
+
return [2 /*return*/, res.status(status).json(__assign(__assign({}, body), { status: status }))];
|
|
111
|
+
}
|
|
78
112
|
return [2 /*return*/, res.status(401).json({
|
|
79
113
|
status: 401,
|
|
80
114
|
error: {
|
|
@@ -83,6 +117,10 @@ function init(app, options) {
|
|
|
83
117
|
},
|
|
84
118
|
})];
|
|
85
119
|
}
|
|
120
|
+
authHeaders = (0, flink_1.getAuthSuccessHeaders)(authResult);
|
|
121
|
+
if (authHeaders) {
|
|
122
|
+
res.set(authHeaders);
|
|
123
|
+
}
|
|
86
124
|
_a.label = 2;
|
|
87
125
|
case 2:
|
|
88
126
|
// Call the handler
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/generic-request-plugin",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.115",
|
|
4
4
|
"description": "Flink plugin that makes it possible to override default request handlers and handle specific requests manually",
|
|
5
5
|
"author": "johan@frost.se",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"main": "dist/index.js",
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"node-color-log": "^5.3.1",
|
|
14
|
-
"@flink-app/flink": "2.0.0-alpha.
|
|
14
|
+
"@flink-app/flink": "2.0.0-alpha.115"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/jasmine": "^3.7.1",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"gitHead": "4243e3b3cd6d4e1ca001a61baa8436bf2bbe4113",
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@flink-app/flink": ">=2.0.0-alpha.
|
|
22
|
+
"@flink-app/flink": ">=2.0.0-alpha.115"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"test": "jasmine-ts --config=./spec/support/jasmine.json",
|
|
@@ -7,6 +7,9 @@ describe("GenericRequestPlugin", () => {
|
|
|
7
7
|
let mockAuth: FlinkAuthPlugin;
|
|
8
8
|
let registeredRoutes: Map<string, Function>;
|
|
9
9
|
|
|
10
|
+
// The app context, passed to authenticateRequest as third argument
|
|
11
|
+
const mockCtx = { repos: {}, plugins: {} };
|
|
12
|
+
|
|
10
13
|
beforeEach(() => {
|
|
11
14
|
registeredRoutes = new Map();
|
|
12
15
|
|
|
@@ -36,6 +39,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
36
39
|
mockApp = {
|
|
37
40
|
expressApp: mockExpressApp,
|
|
38
41
|
auth: mockAuth,
|
|
42
|
+
ctx: mockCtx,
|
|
39
43
|
} as unknown as FlinkApp<any>;
|
|
40
44
|
});
|
|
41
45
|
|
|
@@ -197,7 +201,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
197
201
|
|
|
198
202
|
await routeHandler(mockReq, mockRes);
|
|
199
203
|
|
|
200
|
-
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read");
|
|
204
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read", mockCtx);
|
|
201
205
|
expect(handlerSpy).toHaveBeenCalled();
|
|
202
206
|
});
|
|
203
207
|
|
|
@@ -223,7 +227,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
223
227
|
|
|
224
228
|
await routeHandler(mockReq, mockRes);
|
|
225
229
|
|
|
226
|
-
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, ["read", "write", "admin"]);
|
|
230
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, ["read", "write", "admin"], mockCtx);
|
|
227
231
|
expect(handlerSpy).toHaveBeenCalled();
|
|
228
232
|
});
|
|
229
233
|
|
|
@@ -254,7 +258,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
254
258
|
|
|
255
259
|
await routeHandler(mockReq, mockRes);
|
|
256
260
|
|
|
257
|
-
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read");
|
|
261
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "read", mockCtx);
|
|
258
262
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
259
263
|
expect(mockJsonSpy).toHaveBeenCalledWith({
|
|
260
264
|
status: 401,
|
|
@@ -266,6 +270,95 @@ describe("GenericRequestPlugin", () => {
|
|
|
266
270
|
expect(handlerSpy).not.toHaveBeenCalled();
|
|
267
271
|
});
|
|
268
272
|
|
|
273
|
+
it("should reject and send the FlinkResponse when the auth plugin shapes the failure", async () => {
|
|
274
|
+
const rejection = {
|
|
275
|
+
status: 403,
|
|
276
|
+
error: { id: "1", code: "PERMISSION_DENIED", title: "Permission denied" },
|
|
277
|
+
headers: { "set-cookie": "jwt=deleted; path=/;" },
|
|
278
|
+
};
|
|
279
|
+
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(rejection));
|
|
280
|
+
|
|
281
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
282
|
+
const plugin = genericRequestPlugin({
|
|
283
|
+
path: "/protected",
|
|
284
|
+
method: HttpMethod.get,
|
|
285
|
+
permissions: "read",
|
|
286
|
+
handler: handlerSpy,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
plugin.init(mockApp);
|
|
290
|
+
|
|
291
|
+
const routeHandler = registeredRoutes.get("GET:/protected")!;
|
|
292
|
+
const mockJsonSpy = jasmine.createSpy("json");
|
|
293
|
+
const mockRes = {
|
|
294
|
+
status: jasmine.createSpy("status").and.returnValue({ json: mockJsonSpy }),
|
|
295
|
+
set: jasmine.createSpy("set"),
|
|
296
|
+
} as any;
|
|
297
|
+
|
|
298
|
+
await routeHandler({} as FlinkRequest, mockRes);
|
|
299
|
+
|
|
300
|
+
expect(handlerSpy).not.toHaveBeenCalled();
|
|
301
|
+
expect(mockRes.status).toHaveBeenCalledWith(403);
|
|
302
|
+
expect(mockRes.set).toHaveBeenCalledWith(rejection.headers);
|
|
303
|
+
// headers are transport level and must not leak into the JSON body
|
|
304
|
+
expect(mockJsonSpy).toHaveBeenCalledWith({ status: 403, error: rejection.error });
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should default a shaped rejection without status to 401", async () => {
|
|
308
|
+
mockAuth.authenticateRequest = jasmine
|
|
309
|
+
.createSpy("authenticateRequest")
|
|
310
|
+
.and.returnValue(Promise.resolve({ error: { id: "1", code: "NOPE", title: "Nope" } }));
|
|
311
|
+
|
|
312
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
313
|
+
const plugin = genericRequestPlugin({
|
|
314
|
+
path: "/protected",
|
|
315
|
+
method: HttpMethod.get,
|
|
316
|
+
permissions: "read",
|
|
317
|
+
handler: handlerSpy,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
plugin.init(mockApp);
|
|
321
|
+
|
|
322
|
+
const routeHandler = registeredRoutes.get("GET:/protected")!;
|
|
323
|
+
const mockRes = {
|
|
324
|
+
status: jasmine.createSpy("status").and.returnValue({ json: () => {} }),
|
|
325
|
+
set: jasmine.createSpy("set"),
|
|
326
|
+
} as any;
|
|
327
|
+
|
|
328
|
+
await routeHandler({} as FlinkRequest, mockRes);
|
|
329
|
+
|
|
330
|
+
expect(handlerSpy).not.toHaveBeenCalled();
|
|
331
|
+
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should call the handler when the auth plugin returns an authenticated result with headers", async () => {
|
|
335
|
+
mockAuth.authenticateRequest = jasmine
|
|
336
|
+
.createSpy("authenticateRequest")
|
|
337
|
+
.and.returnValue(Promise.resolve({ authenticated: true, headers: { "set-cookie": "jwt=renewed; path=/;" } }));
|
|
338
|
+
|
|
339
|
+
const handlerSpy = jasmine.createSpy("handler");
|
|
340
|
+
const plugin = genericRequestPlugin({
|
|
341
|
+
path: "/protected",
|
|
342
|
+
method: HttpMethod.get,
|
|
343
|
+
permissions: "read",
|
|
344
|
+
handler: handlerSpy,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
plugin.init(mockApp);
|
|
348
|
+
|
|
349
|
+
const routeHandler = registeredRoutes.get("GET:/protected")!;
|
|
350
|
+
const mockRes = {
|
|
351
|
+
status: jasmine.createSpy("status").and.returnValue({ json: () => {} }),
|
|
352
|
+
set: jasmine.createSpy("set"),
|
|
353
|
+
} as any;
|
|
354
|
+
|
|
355
|
+
await routeHandler({} as FlinkRequest, mockRes);
|
|
356
|
+
|
|
357
|
+
expect(handlerSpy).toHaveBeenCalled();
|
|
358
|
+
expect(mockRes.status).not.toHaveBeenCalled();
|
|
359
|
+
expect(mockRes.set).toHaveBeenCalledWith({ "set-cookie": "jwt=renewed; path=/;" });
|
|
360
|
+
});
|
|
361
|
+
|
|
269
362
|
it("should not call handler when authentication fails", async () => {
|
|
270
363
|
mockAuth.authenticateRequest = jasmine.createSpy("authenticateRequest").and.returnValue(Promise.resolve(false));
|
|
271
364
|
|
|
@@ -380,7 +473,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
380
473
|
|
|
381
474
|
await routeHandler(mockReq, mockRes);
|
|
382
475
|
|
|
383
|
-
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "*");
|
|
476
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "*", mockCtx);
|
|
384
477
|
expect(handlerSpy).toHaveBeenCalled();
|
|
385
478
|
});
|
|
386
479
|
});
|
|
@@ -415,7 +508,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
415
508
|
|
|
416
509
|
await routeHandler(mockReq, mockRes);
|
|
417
510
|
|
|
418
|
-
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "webhook:stripe");
|
|
511
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "webhook:stripe", mockCtx);
|
|
419
512
|
expect(webhookData).toEqual({ event: "payment.success" });
|
|
420
513
|
expect(mockJsonSpy).toHaveBeenCalledWith({ received: true });
|
|
421
514
|
});
|
|
@@ -451,7 +544,7 @@ describe("GenericRequestPlugin", () => {
|
|
|
451
544
|
|
|
452
545
|
await routeHandler(mockReq, mockRes);
|
|
453
546
|
|
|
454
|
-
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "file:download");
|
|
547
|
+
expect(mockAuth.authenticateRequest).toHaveBeenCalledWith(mockReq, "file:download", mockCtx);
|
|
455
548
|
expect(mockSetHeaderSpy).toHaveBeenCalledWith("Content-Type", "application/pdf");
|
|
456
549
|
expect(mockEndSpy).toHaveBeenCalledWith("file-data");
|
|
457
550
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FlinkApp, FlinkPlugin, FlinkRequest } from "@flink-app/flink";
|
|
1
|
+
import { FlinkApp, FlinkPlugin, FlinkRequest, getAuthSuccessHeaders, isAuthSuccess } from "@flink-app/flink";
|
|
2
2
|
import log from "node-color-log";
|
|
3
3
|
|
|
4
4
|
export enum HttpMethod {
|
|
@@ -56,9 +56,23 @@ function init(app: FlinkApp<any>, options: GenericRequestOptions) {
|
|
|
56
56
|
|
|
57
57
|
// Express Request is structurally compatible with FlinkRequest for auth purposes
|
|
58
58
|
// (both have headers and user properties). This follows the same pattern as FlinkApp.ts:826
|
|
59
|
-
const
|
|
59
|
+
const authResult = await app.auth.authenticateRequest(req as FlinkRequest, options.permissions, app.ctx);
|
|
60
|
+
|
|
61
|
+
// Only `true` and a FlinkAuthSuccess mean authenticated. A FlinkResponse is a
|
|
62
|
+
// rejection the plugin shaped itself and is sent verbatim (its `headers` are
|
|
63
|
+
// applied to the response and stripped from the JSON body, status defaults to 401).
|
|
64
|
+
if (!isAuthSuccess(authResult)) {
|
|
65
|
+
if (authResult && typeof authResult === "object") {
|
|
66
|
+
const { headers, ...body } = authResult;
|
|
67
|
+
const status = authResult.status || 401;
|
|
68
|
+
|
|
69
|
+
if (headers) {
|
|
70
|
+
res.set(headers);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return res.status(status).json({ ...body, status });
|
|
74
|
+
}
|
|
60
75
|
|
|
61
|
-
if (!authenticated) {
|
|
62
76
|
return res.status(401).json({
|
|
63
77
|
status: 401,
|
|
64
78
|
error: {
|
|
@@ -67,6 +81,13 @@ function init(app: FlinkApp<any>, options: GenericRequestOptions) {
|
|
|
67
81
|
},
|
|
68
82
|
});
|
|
69
83
|
}
|
|
84
|
+
|
|
85
|
+
// Headers contributed by a successful authentication, e.g. a renewed session cookie
|
|
86
|
+
const authHeaders = getAuthSuccessHeaders(authResult);
|
|
87
|
+
|
|
88
|
+
if (authHeaders) {
|
|
89
|
+
res.set(authHeaders);
|
|
90
|
+
}
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
// Call the handler
|