Skip to content

Commit 85a953e

Browse files
authored
Merge pull request #361 from jappyjan/master
feat: added blacklist functionality for auto aliases
2 parents 3205bc0 + 0688e3f commit 85a953e

File tree

4 files changed

+177
-3
lines changed

4 files changed

+177
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The `moleculer-web` is the official API gateway service for [Moleculer](https://
2121
* support file uploading
2222
* alias names (with named parameters & REST shorthand)
2323
* whitelist
24+
* blacklist
2425
* multiple body parsers (json, urlencoded)
2526
* CORS headers
2627
* ETags

index.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ declare module "moleculer-web" {
367367
cors: CorsOptions;
368368
etag: boolean | "weak" | "strong" | Function;
369369
hasWhitelist: boolean;
370+
hasBlacklist: boolean;
370371
logging: boolean;
371372
mappingPolicy: string;
372373
middlewares: Function[];
@@ -375,6 +376,7 @@ declare module "moleculer-web" {
375376
opts: any;
376377
path: string;
377378
whitelist: string[];
379+
blacklist: string[];
378380
}
379381

380382
type onBeforeCall = (
@@ -514,6 +516,7 @@ declare module "moleculer-web" {
514516
* The gateway will dynamically build the full routes from service schema.
515517
* Gateway will regenerate the routes every time a service joins or leaves the network.<br>
516518
* Use `whitelist` parameter to specify services that the Gateway should track and build the routes.
519+
* And `blacklist` parameter to specify services that the Gateway should not track and build the routes.
517520
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Auto-alias
518521
*/
519522
autoAliases?: boolean;
@@ -597,6 +600,15 @@ declare module "moleculer-web" {
597600
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Whitelist
598601
*/
599602
whitelist?: (string | RegExp)[];
603+
/**
604+
* If you don’t want to publish all actions, you can filter them with blacklist option.<br>
605+
* Use match strings or regexp in list. To enable all actions, use "**" item.<br>
606+
* "posts.*": `Access any actions in 'posts' service`<br>
607+
* "users.list": `Access call only the 'users.list' action`<br>
608+
* /^math\.\w+$/: `Access any actions in 'math' service`<br>
609+
* @see https://moleculer.services/docs/0.14/moleculer-web.html#Blacklist
610+
*/
611+
blacklist?: (string | RegExp)[];
600612
}
601613

602614
type APISettingServer =

src/index.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const _ = require("lodash");
1919
const bodyParser = require("body-parser");
2020
const serveStatic = require("serve-static");
2121
const isReadableStream = require("isstream").isReadable;
22-
const { pipeline } = require('stream');
22+
const { pipeline } = require("stream");
2323

2424
const { MoleculerError, MoleculerServerError, ServiceNotFoundError } = require("moleculer").Errors;
2525
const { ServiceUnavailableError, NotFoundError, ForbiddenError, RateLimitExceeded, ERR_ORIGIN_NOT_ALLOWED } = require("./errors");
@@ -496,6 +496,7 @@ module.exports = {
496496
/**
497497
* Alias handler. Call action or call custom function
498498
* - check whitelist
499+
* - check blacklist
499500
* - Rate limiter
500501
* - Resolve endpoint
501502
* - onBeforeCall
@@ -520,6 +521,14 @@ module.exports = {
520521
}
521522
}
522523

524+
// Blacklist check
525+
if (alias.action && route.hasBlacklist) {
526+
if (this.checkBlacklist(route, alias.action)) {
527+
this.logger.debug(` The '${alias.action}' action is in the blacklist!`);
528+
throw new ServiceNotFoundError({ action: alias.action });
529+
}
530+
}
531+
523532
// Rate limiter
524533
if (route.rateLimit) {
525534
const opts = route.rateLimit;
@@ -821,9 +830,9 @@ module.exports = {
821830
if (isReadableStream(data)) { //Stream response
822831
pipeline(data, res, err => {
823832
if (err) {
824-
this.logger.warn("Stream got an error.", { err, url: req.url, actionName: action.name })
833+
this.logger.warn("Stream got an error.", { err, url: req.url, actionName: action.name });
825834
}
826-
})
835+
});
827836
} else {
828837
res.end(chunk);
829838
}
@@ -1154,6 +1163,23 @@ module.exports = {
11541163
}) != null;
11551164
},
11561165

1166+
/**
1167+
* Check the action name in blacklist
1168+
*
1169+
* @param {Object} route
1170+
* @param {String} action
1171+
* @returns {Boolean}
1172+
*/
1173+
checkBlacklist(route, action) {
1174+
// Rewrite to for iterator (faster)
1175+
return (
1176+
route.blacklist.find((mask) => {
1177+
if (_.isString(mask)) return match(action, mask);
1178+
else if (_.isRegExp(mask)) return mask.test(action);
1179+
}) != null
1180+
);
1181+
},
1182+
11571183
/**
11581184
* Resolve alias names
11591185
*
@@ -1368,6 +1394,10 @@ module.exports = {
13681394
route.whitelist = opts.whitelist;
13691395
route.hasWhitelist = Array.isArray(route.whitelist);
13701396

1397+
// Handle blacklist
1398+
route.blacklist = opts.blacklist;
1399+
route.hasBlacklist = Array.isArray(route.blacklist);
1400+
13711401
// `onBeforeCall` handler
13721402
if (opts.onBeforeCall)
13731403
route.onBeforeCall = opts.onBeforeCall;
@@ -1522,6 +1552,18 @@ module.exports = {
15221552
// Check whitelist
15231553
if (route.hasWhitelist && !this.checkWhitelist(route, action.name)) return;
15241554

1555+
// Blacklist check
1556+
if (route.hasBlacklist) {
1557+
if (this.checkBlacklist(route, action.name)) {
1558+
this.logger.debug(
1559+
` The '${action.name}' action is in the blacklist!`
1560+
);
1561+
throw new ServiceNotFoundError({
1562+
action: action.name,
1563+
});
1564+
}
1565+
}
1566+
15251567
let restRoutes = [];
15261568
if (!_.isArray(action.rest)) {
15271569
restRoutes = [action.rest];

test/integration/index.spec.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,125 @@ describe("Test whitelist", () => {
777777
});
778778
});
779779

780+
describe("Test blacklist", () => {
781+
let broker;
782+
let service;
783+
let server;
784+
beforeAll(() => {
785+
[broker, service, server] = setup({
786+
routes: [
787+
{
788+
path: "/api",
789+
blacklist: ["test.greeter", "math.sub", /^test\.json/],
790+
},
791+
],
792+
});
793+
broker.loadService("./test/services/math.service");
794+
return broker.start();
795+
});
796+
afterAll(() => broker.stop());
797+
it("GET /api/test/hello", () => {
798+
return request(server)
799+
.get("/api/test/hello")
800+
.then((res) => {
801+
expect(res.statusCode).toBe(200);
802+
expect(res.headers["content-type"]).toBe(
803+
"application/json; charset=utf-8"
804+
);
805+
expect(res.body).toBe("Hello Moleculer");
806+
});
807+
});
808+
it("GET /api/test/json", () => {
809+
return request(server)
810+
.get("/api/test/json")
811+
.then((res) => {
812+
expect(res.statusCode).toBe(404);
813+
expect(res.headers["content-type"]).toBe(
814+
"application/json; charset=utf-8"
815+
);
816+
expect(res.body).toEqual({
817+
code: 404,
818+
message: "Service 'test.json' is not found.",
819+
name: "ServiceNotFoundError",
820+
type: "SERVICE_NOT_FOUND",
821+
data: {
822+
action: "test.json",
823+
},
824+
});
825+
});
826+
});
827+
it("GET /api/test/jsonArray", () => {
828+
return request(server)
829+
.get("/api/test/jsonArray")
830+
.then((res) => {
831+
expect(res.statusCode).toBe(404);
832+
expect(res.headers["content-type"]).toBe(
833+
"application/json; charset=utf-8"
834+
);
835+
expect(res.body).toEqual({
836+
code: 404,
837+
message: "Service 'test.jsonArray' is not found.",
838+
name: "ServiceNotFoundError",
839+
type: "SERVICE_NOT_FOUND",
840+
data: {
841+
action: "test.jsonArray",
842+
},
843+
});
844+
});
845+
});
846+
it("GET /api/test/greeter", () => {
847+
return request(server)
848+
.get("/api/test/greeter")
849+
.then((res) => {
850+
expect(res.statusCode).toBe(404);
851+
expect(res.headers["content-type"]).toBe(
852+
"application/json; charset=utf-8"
853+
);
854+
expect(res.body).toEqual({
855+
code: 404,
856+
message: "Service 'test.greeter' is not found.",
857+
name: "ServiceNotFoundError",
858+
type: "SERVICE_NOT_FOUND",
859+
data: {
860+
action: "test.greeter",
861+
},
862+
});
863+
});
864+
});
865+
it("GET /api/math.add", () => {
866+
return request(server)
867+
.get("/api/math.add")
868+
.query({ a: 5, b: 8 })
869+
.then((res) => {
870+
expect(res.statusCode).toBe(200);
871+
expect(res.headers["content-type"]).toBe(
872+
"application/json; charset=utf-8"
873+
);
874+
expect(res.body).toBe(13);
875+
});
876+
});
877+
it("GET /api/math.sub", () => {
878+
return request(server)
879+
.get("/api/math.sub")
880+
.query({ a: 5, b: 8 })
881+
.then((res) => {
882+
expect(res.statusCode).toBe(404);
883+
expect(res.headers["content-type"]).toBe(
884+
"application/json; charset=utf-8"
885+
);
886+
expect(res.body).toEqual({
887+
code: 404,
888+
message: "Service 'math.sub' is not found.",
889+
name: "ServiceNotFoundError",
890+
type: "SERVICE_NOT_FOUND",
891+
data: {
892+
action: "math.sub",
893+
},
894+
});
895+
});
896+
});
897+
});
898+
780899
describe("Test aliases", () => {
781900
let broker;
782901
let service;

0 commit comments

Comments
 (0)