diff --git a/README.md b/README.md index c93351af..040949ce 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ The `moleculer-web` is the official API gateway service for [Moleculer](https:// * support file uploading * alias names (with named parameters & REST shorthand) * whitelist +* blacklist * multiple body parsers (json, urlencoded) * CORS headers * ETags diff --git a/index.d.ts b/index.d.ts index f7815672..f0afa41a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -367,6 +367,7 @@ declare module "moleculer-web" { cors: CorsOptions; etag: boolean | "weak" | "strong" | Function; hasWhitelist: boolean; + hasBlacklist: boolean; logging: boolean; mappingPolicy: string; middlewares: Function[]; @@ -375,6 +376,7 @@ declare module "moleculer-web" { opts: any; path: string; whitelist: string[]; + blacklist: string[]; } type onBeforeCall = ( @@ -514,6 +516,7 @@ declare module "moleculer-web" { * The gateway will dynamically build the full routes from service schema. * Gateway will regenerate the routes every time a service joins or leaves the network.
* Use `whitelist` parameter to specify services that the Gateway should track and build the routes. + * And `blacklist` parameter to specify services that the Gateway should not track and build the routes. * @see https://moleculer.services/docs/0.14/moleculer-web.html#Auto-alias */ autoAliases?: boolean; @@ -597,6 +600,15 @@ declare module "moleculer-web" { * @see https://moleculer.services/docs/0.14/moleculer-web.html#Whitelist */ whitelist?: (string | RegExp)[]; + /** + * If you don’t want to publish all actions, you can filter them with blacklist option.
+ * Use match strings or regexp in list. To enable all actions, use "**" item.
+ * "posts.*": `Access any actions in 'posts' service`
+ * "users.list": `Access call only the 'users.list' action`
+ * /^math\.\w+$/: `Access any actions in 'math' service`
+ * @see https://moleculer.services/docs/0.14/moleculer-web.html#Blacklist + */ + blacklist?: (string | RegExp)[]; } type APISettingServer = diff --git a/src/index.js b/src/index.js index a6bf8037..4b68556b 100644 --- a/src/index.js +++ b/src/index.js @@ -496,6 +496,7 @@ module.exports = { /** * Alias handler. Call action or call custom function * - check whitelist + * - check blacklist * - Rate limiter * - Resolve endpoint * - onBeforeCall @@ -1154,6 +1155,23 @@ module.exports = { }) != null; }, + /** + * Check the action name in blacklist + * + * @param {Object} route + * @param {String} action + * @returns {Boolean} + */ + checkBlacklist(route, action) { + // Rewrite to for iterator (faster) + return ( + route.blacklist.find((mask) => { + if (_.isString(mask)) return match(action, mask); + else if (_.isRegExp(mask)) return mask.test(action); + }) != null + ); + }, + /** * Resolve alias names * @@ -1368,6 +1386,10 @@ module.exports = { route.whitelist = opts.whitelist; route.hasWhitelist = Array.isArray(route.whitelist); + // Handle blacklist + route.blacklist = opts.blacklist; + route.hasBlacklist = Array.isArray(route.blacklist); + // `onBeforeCall` handler if (opts.onBeforeCall) route.onBeforeCall = opts.onBeforeCall; @@ -1522,6 +1544,18 @@ module.exports = { // Check whitelist if (route.hasWhitelist && !this.checkWhitelist(route, action.name)) return; + // Blacklist check + if (route.hasBlacklist) { + if (this.checkBlacklist(route, alias.action)) { + this.logger.debug( + ` The '${alias.action}' action is in the blacklist!` + ); + throw new ServiceNotFoundError({ + action: alias.action, + }); + } + } + let restRoutes = []; if (!_.isArray(action.rest)) { restRoutes = [action.rest]; diff --git a/test/integration/index.spec.js b/test/integration/index.spec.js index f058fee5..f94ab216 100644 --- a/test/integration/index.spec.js +++ b/test/integration/index.spec.js @@ -777,6 +777,125 @@ describe("Test whitelist", () => { }); }); +describe("Test blacklist", () => { + let broker; + let service; + let server; + beforeAll(() => { + [broker, service, server] = setup({ + routes: [ + { + path: "/api", + blacklist: ["test.greeter", "math.sub", /^test\.json/], + }, + ], + }); + broker.loadService("./test/services/math.service"); + return broker.start(); + }); + afterAll(() => broker.stop()); + it("GET /api/test/hello", () => { + return request(server) + .get("/api/test/hello") + .then((res) => { + expect(res.statusCode).toBe(200); + expect(res.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(res.body).toBe("Hello Moleculer"); + }); + }); + it("GET /api/test/json", () => { + return request(server) + .get("/api/test/json") + .then((res) => { + expect(res.statusCode).toBe(404); + expect(res.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(res.body).toEqual({ + code: 404, + message: "Service 'test.json' is not found.", + name: "ServiceNotFoundError", + type: "SERVICE_NOT_FOUND", + data: { + action: "test.json", + }, + }); + }); + }); + it("GET /api/test/jsonArray", () => { + return request(server) + .get("/api/test/jsonArray") + .then((res) => { + expect(res.statusCode).toBe(404); + expect(res.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(res.body).toEqual({ + code: 404, + message: "Service 'test.jsonArray' is not found.", + name: "ServiceNotFoundError", + type: "SERVICE_NOT_FOUND", + data: { + action: "test.jsonArray", + }, + }); + }); + }); + it("GET /api/test/greeter", () => { + return request(server) + .get("/api/test/greeter") + .then((res) => { + expect(res.statusCode).toBe(404); + expect(res.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(res.body).toEqual({ + code: 404, + message: "Service 'test.greeter' is not found.", + name: "ServiceNotFoundError", + type: "SERVICE_NOT_FOUND", + data: { + action: "test.greeter", + }, + }); + }); + }); + it("GET /api/math.add", () => { + return request(server) + .get("/api/math.add") + .query({ a: 5, b: 8 }) + .then((res) => { + expect(res.statusCode).toBe(200); + expect(res.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(res.body).toBe(13); + }); + }); + it("GET /api/math.sub", () => { + return request(server) + .get("/api/math.sub") + .query({ a: 5, b: 8 }) + .then((res) => { + expect(res.statusCode).toBe(404); + expect(res.headers["content-type"]).toBe( + "application/json; charset=utf-8" + ); + expect(res.body).toEqual({ + code: 404, + message: "Service 'math.sub' is not found.", + name: "ServiceNotFoundError", + type: "SERVICE_NOT_FOUND", + data: { + action: "math.sub", + }, + }); + }); + }); +}); + describe("Test aliases", () => { let broker; let service;