UNPKG

moleculer-iam

Version:

Centralized IAM module for moleculer. Including a certified OIDC provider and an Identity provider for user profile, credentials, and custom claims management. Custom claims could be defined/updated by declarative schema which contains claims validation a

752 lines 27.8 kB
"use strict"; /* * moleculer-iam * Copyright (c) 2019 QMIT Inc. (https://github.com/qmit-pro/moleculer-iam) * MIT Licensed */ Object.defineProperty(exports, "__esModule", { value: true }); exports.IAMServiceSchema = void 0; const moleculer_1 = require("moleculer"); const idp_1 = require("../idp"); const op_1 = require("../op"); const server_1 = require("../server"); const api_1 = require("./api"); const params_1 = require("./params"); function IAMServiceSchema(opts) { let idp; let op; let server; return { created() { // create identity provider idp = this.idp = new idp_1.IdentityProvider({ logger: this.broker.getLogger("idp"), }, opts.idp); // create oidc provider op = this.op = new op_1.OIDCProvider({ idp, logger: this.broker.getLogger("op"), }, opts.op); // create server server = this.server = new server_1.IAMServer({ op, logger: this.broker.getLogger("server"), }, opts.server); }, async started() { await server.start(); }, async stopped() { await server.stop(); }, name: "iam", settings: {}, mixins: opts.apiGatewayEndpoint ? [api_1.createAPIGatewayMixin(opts.apiGatewayEndpoint)] : [], hooks: { // transform OIDC provider error error: { "*"(ctx, err) { if (err.error) { const e = err; if (e.status === 422) { throw new moleculer_1.Errors.ValidationError(e.error_description, null, e.data); } else if (e.status <= 400 && e.status < 500) { throw new moleculer_1.Errors.MoleculerClientError(e.error_description, e.status, e.error); } else if (e.status >= 500) { throw new moleculer_1.Errors.MoleculerServerError(e.error_description, e.status, e.error); } } this.broker.logger.error(err); throw err; }, }, }, actions: { // receive report from API Gateway and print on console $report: { handler({ params: { table, messages } }) { this.logger.info(table); }, }, /* Client Management */ "client.create": { description: ` Create OIDC Client. All params from below reference will be accepted. ref: https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata `, params: params_1.IAMServiceActionParams["client.create"], async handler(ctx) { const client = await op.createClient(ctx.params); await this.broker.broadcast("iam.client.updated"); return client; }, }, "client.update": { params: params_1.IAMServiceActionParams["client.update"], async handler(ctx) { const client = await op.updateClient(ctx.params); await this.broker.broadcast("iam.client.updated"); return client; }, }, "client.delete": { params: { id: "string", }, async handler(ctx) { await op.deleteClient(ctx.params.id); await this.broker.broadcast("iam.client.deleted", ctx.params); // 'oidc-provider' has a hard coded LRU cache internally... using pub/sub to clear distributed nodes' cache return true; }, }, "client.find": { cache: { ttl: 3600, }, params: { id: "string", }, async handler(ctx) { return (await op.findClient(ctx.params.id)) || null; }, }, "client.get": { cache: { ttl: 3600, }, params: { where: { type: "any", optional: true, }, offset: { type: "number", positive: true, default: 0, }, limit: { type: "number", positive: true, default: 10, }, }, async handler(ctx) { const { offset, limit, where } = ctx.params; const [total, entries] = await Promise.all([ op.countClients(where), op.getClients(ctx.params), ]); return { offset, limit, total, entries }; }, }, "client.count": { cache: { ttl: 3600, }, params: { where: { type: "any", optional: true, }, }, async handler(ctx) { return op.countClients(ctx.params && ctx.params.where); }, }, /* "Session", "AccessToken", "AuthorizationCode", "RefreshToken", "DeviceCode", "InitialAccessToken", "RegistrationAccessToken", "Interaction", "ReplayDetection", "PushedAuthorizationRequest" Management */ "model.get": { cache: { ttl: 30, }, params: { kind: { type: "enum", values: op_1.OIDCProvider.modelNames, }, where: { type: "any", optional: true, }, offset: { type: "number", positive: true, default: 0, }, limit: { type: "number", positive: true, default: 10, }, }, async handler(ctx) { const { offset, limit, kind, where, ...args } = ctx.params; const [total, entries] = await Promise.all([ op.countModels(kind, where), op.getModels(kind, { offset, limit, where, ...args }), ]); return { offset, limit, total, entries }; }, }, "model.count": { cache: { ttl: 30, }, params: { kind: { type: "enum", values: op_1.OIDCProvider.modelNames, }, where: { type: "any", optional: true, }, }, async handler(ctx) { const { kind, where } = ctx.params; return op.countModels(kind, where); }, }, "model.delete": { params: { kind: { type: "enum", values: op_1.OIDCProvider.modelNames, }, where: { type: "any", optional: false, }, offset: { type: "number", positive: true, default: 0, }, limit: { type: "number", positive: true, default: 10, }, }, async handler(ctx) { const { kind, ...args } = ctx.params; return op.deleteModels(kind, args); }, }, /* Identity Claims Schema Management */ "schema.get": { params: { scope: [ { type: "array", items: { type: "string", trim: true, empty: false, }, default: [], optional: true, }, { type: "string", default: "", optional: true, }, ], key: { type: "string", empty: false, trim: true, optional: true, }, version: { type: "string", empty: false, trim: true, optional: true, }, active: { type: "boolean", optional: true, }, }, async handler(ctx) { return idp.claims.getClaimsSchemata(ctx.params); }, }, "schema.find": { params: { key: { type: "string", empty: false, trim: true, }, version: { type: "string", empty: false, trim: true, optional: true, }, active: { type: "boolean", optional: true, }, }, async handler(ctx) { return (await idp.claims.getClaimsSchema(ctx.params)) || null; }, }, "schema.define": { params: params_1.IAMServiceActionParams["schema.define"], async handler(ctx) { const payload = ctx.params; const oldSchema = await idp.claims.getClaimsSchema({ key: payload.key, active: true }); const schema = await idp.claims.defineClaimsSchema(payload); if (!oldSchema || oldSchema.version !== schema.version) { await this.broker.broadcast("iam.schema.updated"); } return schema; }, }, /* Identity Management */ "identity.validate": { params: { id: { type: "string", optional: true, }, scope: [ { type: "array", items: { type: "string", trim: true, empty: false, }, default: [], optional: true, }, { type: "string", default: "", optional: true, }, ], claims: { type: "object", default: {}, }, credentials: { type: "object", default: {}, }, }, async handler(ctx) { await idp.validate(ctx.params); return ctx.params; }, }, "identity.validateCredentials": { params: { password: { type: "string", optional: true, }, }, async handler(ctx) { await idp.validateCredentials(ctx.params); return ctx.params; }, }, "identity.create": { params: { scope: [ { type: "array", items: { type: "string", trim: true, empty: false, }, default: [], optional: true, }, { type: "string", default: "", optional: true, }, ], metadata: { type: "object", default: {}, }, claims: { type: "object", default: {}, }, credentials: { type: "object", default: {}, }, }, async handler(ctx) { const id = await idp.create(ctx.params) .then(i => i.json()); await this.broker.broadcast("iam.identity.updated"); return id; }, }, "identity.update": { params: { id: [ // support batching { type: "string", optional: true, }, { type: "array", items: "string", optional: true, }, ], scope: [ { type: "array", items: { type: "string", trim: true, empty: false, }, default: [], optional: true, }, { type: "string", default: "", optional: true, }, ], claims: { type: "object", default: {}, }, metadata: { type: "object", default: {}, }, credentials: { type: "object", default: {}, }, }, async handler(ctx) { const { id, claims, metadata, credentials, scope } = (ctx.params || {}); let result; // batching if (Array.isArray(id)) { result = await idp.get({ where: { id }, limit: id.length }) .then(ids => Promise.all(ids.map(i => i.update(scope, claims, metadata, credentials) .then(() => i.json(scope), (err) => { err.batchingError = true; return err; })))); } else { result = await idp.findOrFail({ id }) .then(i => i.update(scope, claims, metadata, credentials).then(() => i.json(scope))); } await this.broker.broadcast("iam.identity.updated"); return result; }, }, "identity.delete": { params: { id: [ // support batching { type: "string", optional: true, }, { type: "array", items: "string", optional: true, }, ], permanently: { type: "boolean", default: false, }, }, async handler(ctx) { const { id, permanently } = (ctx.params || {}); const where = { id, metadata: { softDeleted: permanently } }; // batching support if (Array.isArray(id)) { return idp.get({ where, limit: id.length }) .then(ids => Promise.all(ids.map(i => i.delete(permanently) .then(() => i.id, (err) => { err.batchingError = true; return err; })))); } return idp.findOrFail(where).then(i => i.delete(permanently)).then(() => id); }, }, "identity.restore": { params: { id: [ // support batching { type: "string", optional: true, }, { type: "array", items: "string", optional: true, }, ], }, async handler(ctx) { const { id } = (ctx.params || {}); const where = { id, metadata: { softDeleted: true } }; // batching support if (Array.isArray(id)) { return idp.get({ where, limit: id.length }) .then(ids => Promise.all(ids.map(i => i.restoreSoftDeleted() .then(() => i.id, (err) => { err.batchingError = true; return err; })))); } return idp.findOrFail(where).then(i => i.restoreSoftDeleted()).then(() => id); }, }, "identity.find": { cache: { ttl: 3600, }, params: { id: [ // support batching { type: "string", optional: true, }, { type: "array", items: "string", optional: true, }, ], email: { type: "string", optional: true, }, phone_number: { type: "string", optional: true, }, where: { type: "any", optional: true, }, scope: [ { type: "array", items: { type: "string", trim: true, empty: false, }, default: [], optional: true, }, { type: "string", default: "", optional: true, }, ], }, async handler(ctx) { // tslint:disable-next-line:prefer-const let { id, email, phone_number, where, scope } = (ctx.params || {}); if (typeof where !== "object" || where === null) where = {}; // batching support if (Array.isArray(id)) { return idp.get({ where: { id }, limit: id.length }) .then(ids => Promise.all(ids.map(i => i.json(scope) .then(undefined, (err) => { err.batchingError = true; return err; })))); } if (id) where.id = id; if (email) { if (!where.claims) where.claims = {}; where.claims.email = email; } if (phone_number) { if (!where.claims) where.claims = {}; where.claims.phone_number = phone_number; } if (Object.keys(where).length === 0) where.id = null; return idp.find(where).then(async (i) => i ? await i.json(scope) : null); }, }, "identity.get": { cache: { ttl: 3600, }, params: { where: { type: "any", optional: true, }, offset: { type: "number", positive: true, default: 0, }, limit: { type: "number", positive: true, default: 10, }, scope: [ { type: "array", items: { type: "string", trim: true, empty: false, }, default: [], optional: true, }, { type: "string", default: "", optional: true, }, ], }, async handler(ctx) { const { offset, limit, kind, where, scope, ...args } = (ctx.params || {}); const [total, entries] = await Promise.all([ idp.count(where), idp.get({ offset, limit, where, ...args }).then(ids => Promise.all(ids.map(i => i.json(scope)))), ]); return { offset, limit, total, entries }; }, }, "identity.count": { cache: { ttl: 3600, }, params: { where: { type: "any", optional: true, }, }, async handler(ctx) { return idp.count(ctx.params && ctx.params.where); }, }, "identity.refresh": { cache: { ttl: 5, }, params: { id: [ { type: "string", optional: true, }, { type: "array", items: "string", optional: true, }, ], where: { type: "any", optional: true, }, }, async handler(ctx) { const { where, id } = ctx.params; let ids; if (typeof id === "string") ids = [id]; else if (Array.isArray(id)) ids = id; await idp.claims.forceReloadClaims({ where, ids }); await this.broker.broadcast("iam.identity.updated"); }, }, }, events: { "iam.client.deleted": { // @ts-ignore params: { id: "string", }, async handler(ctx) { try { // to clear internal memory cache // await op.deleteClient(ctx.params.id); } catch (err) { // ...NOTHING } finally { await this.clearCache("client.**"); } }, }, "iam.client.updated": { async handler(ctx) { await this.clearCache("client.**"); }, }, "iam.identity.updated": { async handler(ctx) { await this.clearCache("identity.*"); }, }, "iam.schema.updated": { async handler(ctx) { await idp.claims.onClaimsSchemaUpdated(); await op.syncSupportedClaimsAndScopes(); await this.clearCache("schema.*"); await this.clearCache("identity.*"); }, }, }, methods: { async clearCache(...keys) { if (this.broker.cacher) { if (keys.length === 0) { keys = ["**"]; } const fullKeys = keys.map(key => `${this.fullName}.${key}`); await this.broker.cacher.clean(fullKeys); } }, }, }; } exports.IAMServiceSchema = IAMServiceSchema; //# sourceMappingURL=service.js.map