@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 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 authenticated;
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
- authenticated = _a.sent();
77
- if (!authenticated) {
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.113",
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.113"
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.113"
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 authenticated = await app.auth.authenticateRequest(req as FlinkRequest, options.permissions, app.ctx);
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