UNPKG

imicros-acl

Version:

Moleculer service for access control

419 lines (375 loc) 15.5 kB
/** * @license MIT, imicros.de (c) 2019 Andreas Leinen */ "use strict"; const dbMixin = require("./db.neo4j"); const jwt = require("jsonwebtoken"); const { Compiler } = require("imicros-rules-compiler"); const jwtIssuer = "imicros.acl"; /** Actions */ // action requestAccess { forGroupId } => { token } // action verify { token } => { acl } // action addGrant { forGroupId, rulesetId } => { result } only for group admins // action removeGrant { forGroupId } => { result } only for group admins // to be done // action addDeny { forGroupId, rulesetId } => { result } only for group admins // action removeDeny { forGroupId } => { result } only for group admins // enhancements ? //?? action useStore { storeId, rulesetId } => { storeId } //?? Set an attributes ruleset to map the grant attributes from the original ressource ?? module.exports = { name: "acl", mixins: [dbMixin], /** * Service settings */ settings: {}, /** * Service metadata */ metadata: {}, /** * Service dependencies */ //dependencies: [], /** * Actions */ actions: { /** * requestAccess * * @actions * @param {String} forGroupId * * @returns {String} token */ requestAccess: { params: { forGroupId: { type: "string" } }, async handler({ params, meta: { user = {} }}) { // Is user member of the group? if (user.id) { let queryParams = { groupId: params.forGroupId, userId: user.id }; let statement = "MATCH (g:Group { uid: $groupId })<-[r:MEMBER_OF]-(u:User { uid: $userId }) "; statement += "RETURN g.uid AS id, r.role AS role, g.core AS core;"; let result = await this.run(statement, queryParams); // TODO: no result, if neo is down.... if (result[0]) { let payload = { ownerId: queryParams.groupId, userId: user.id, role: result[0].role, core: result[0].core, unrestricted: true }; //let options = { issuer: ..., audience: ... }; let options = { issuer: this.jwtIssuer, subject: queryParams.groupId }; return { token: jwt.sign(payload, this.JWT_SECRET,options) }; } } // Is access granted for a group, where user is member? if (user.id) { let queryParams = { groupId: params.forGroupId, userId: user.id }; let statement = "MATCH (g:Group { uid: $groupId })-[gt:GRANT]->(:Group)<-[:MEMBER_OF]-(u:User { uid: $userId }) "; statement += "RETURN g.uid AS id;"; this.logger.debug("request grants", { statement, queryParams }); let result = await this.run(statement, queryParams); if (result[0]) { let payload = { ownerId: queryParams.groupId, userId: user.id, restricted: true, grant: true }; //let options = { issuer: ..., audience: ... }; let options = { issuer: this.jwtIssuer, subject: queryParams.groupId }; return { token: jwt.sign(payload, this.JWT_SECRET,options) }; } } // no access this.logger.debug("no access"); return {}; } }, /** * verify * * @actions * @param {String} token * * @returns {Object} acl */ verify: { visibility: "public", params: { token: { type: "string" } }, async handler({ params: { token }, meta: { user = {}, service = {} }, broker = {} }) { this.logger.debug("verify token", { token }); let acl = jwt.verify(token, this.JWT_SECRET, { }); this.logger.debug("token sucessfully verified", { acl }); // service access if (acl.serviceId) { if (acl.serviceId !== service.serviceId) throw new Error("Token not valid"); return { acl: { nodeID: broker.nodeID, accessToken: token, ownerId: acl.ownerId, role: acl.role, core: acl.core, unrestricted: acl.unrestricted } }; } // user access if ( acl.userId) { if (acl.userId !== user.id) throw new Error("Token not valid"); // unrestricted access if (acl.unrestricted) return { acl: { nodeID: broker.nodeID, accessToken: token, ownerId: acl.ownerId, role: acl.role, core: acl.core, unrestricted: true } }; // restricted access - get acl rules let params = { groupId: acl.ownerId, userId: acl.userId }; let statement = "MATCH (g:Group { uid: $groupId })-[gt:GRANT]->(:Group)<-[:MEMBER_OF]-(u:User { uid: $userId }) "; statement += "RETURN g.uid AS id, gt.function AS function;"; let result = await this.run(statement, params); if (result && result[0]) { return { acl: { nodeID: broker.nodeID, accessToken: token, ownerId: acl.ownerId, grants: result, denies: [], restricted: true } }; } else { this.logger.debug("missing grants", statement, params, result); throw new Error("Restricted access - missing grants"); } } } }, /** * Grant access fo another group * * @actions * @param {String} forGroupId group id access will be granted for * @param {String} ruleset * * @returns {Object} result */ addGrant: { acl: "before", params: { forGroupId: { type: "string" }, ruleset: { type: "string" } }, async handler({ params, meta: { user = {}, acl = {} }}) { // let user = this.isAuthenticated (ctx.meta); if (!acl.ownerId) throw new Error("No group access"); // check, if ruleset can be compiled and the result is a valid function let func = await Compiler.compile(params.ruleset); let f = new Function(func)(); if (f && {}.toString.call(f) !== "[object Function]") throw new Error("unvalid ruleset"); let queryParams = { byGroupId: acl.ownerId || "-", forGroupId: params.forGroupId, adminId: user.id, adminRole: this.roles.admin, ruleset: params.ruleset, function: func }; let statement; statement = "MATCH (g:Group { uid: $byGroupId })<-[:MEMBER_OF { role: $adminRole }]-(:User { uid: $adminId }) "; statement += "MATCH (e:Group { uid: $forGroupId }) "; statement += "MERGE (g)-[a:GRANT]->(e) "; statement += "SET a.ruleset=$ruleset, a.function=$function "; statement += "RETURN g.uid AS byGroupId, e.uid AS forGroupId, a.ruleset AS ruleset;"; return this.run(statement, queryParams); } }, /** * removeGrant * * @actions * @param {String} forGroupId group id access will be removed for * * @returns [] ruleset */ removeGrant: { acl: "before", params: { forGroupId: { type: "string" } }, async handler({ params, meta: { user = {}, acl = {} }}) { // let user = this.isAuthenticated (ctx.meta); if (!acl.ownerId) throw new Error("No group access"); let queryParams = { byGroupId: acl.ownerId || "-", forGroupId: params.forGroupId, adminId: user.id, adminRole: this.roles.admin }; let statement; statement = "MATCH (g:Group { uid: $byGroupId })<-[:MEMBER_OF { role: $adminRole }]-(:User { uid: $adminId }) "; statement += "MATCH (g)-[a:GRANT]->(e:Group { uid: $forGroupId }) "; statement += "DELETE a;"; return this.run(statement, queryParams); } }, /** * grantAccess * * @actions * * @returns {Object} {} || { token } */ grantAccess: { visibility: "public", acl: "before", async handler({ meta: { acl: { ownerId = null }, service: { serviceToken = null }}}) { if (!ownerId || !serviceToken) return {}; const { serviceId } = await this.verifyServiceToken ({ serviceToken }); if (!serviceId) { this.logger.error("grant access - failed to verify service token", { serviceToken, decoded: jwt.decode(serviceToken) }); return {}; } // build grant token let payload = { type: "grant_token", ownerId, serviceId, role: "member", core: false, unrestricted: true }; //let options = { issuer: ..., audience: ... }; let options = { issuer: this.jwtIssuer, subject: serviceId }; return { token: jwt.sign(payload, this.JWT_SECRET ,options) }; } }, /** * exchangeToken * * @actions * @param {String} token * * @returns {Object} {} || { token } */ exchangeToken: { visibility: "public", params: { token: { type: "string" }, expiresIn: { type: "number", optional: true } }, async handler({ params, meta: { service: { serviceToken = null }}}) { if (!serviceToken) return {}; const { serviceId } = await this.verifyServiceToken ({ serviceToken }); if (!serviceId) { this.logger.error("exchange token - failed to verify service token", { serviceToken, decoded: jwt.decode(serviceToken) }); return {}; } try { let grant = jwt.verify(params.token, this.JWT_SECRET, { subject: serviceId }); if (grant.type !== "grant_token") throw new Error("wrong token type", grant.type); // build access token let payload = { type: "access_token", ownerId: grant.ownerId, serviceId, role: grant.role, core: false, unrestricted: grant.unrestricted }; //let options = { issuer: ..., audience: ... }; let options = { issuer: this.jwtIssuer, subject: grant.ownerId, expiresIn: params.expiresIn || 60 }; return { token: jwt.sign(payload, this.JWT_SECRET,options) }; } catch (err) { this.logger.error("Unvalid grant token", err); return {}; } } } }, /** * Events */ events: {}, /** * Methods */ methods: { async verifyServiceToken ({ serviceToken }) { const { service: { serviceId = null } } = await this.broker.call(`${this.services.agents}.verify`, { serviceToken }); return { serviceId }; } /** * Check User * * @param {Object} meta data of call * * @returns {Object} user entity */ /* isAuthenticated (meta) { // Prepared enhancement: individual maps via settings // from : to let map = { "user.id": "id", // from meta.user.id to user.id "user.email": "email" // from meta.user.email to user.email }; let user = objectMapper(meta, map); if (!user || !user.id || !user.email ) { this.logger.debug("user not authenticated", { meta: meta }); // throw new Error("not authenticated" ); return null; } return user; } */ }, /** * Service created lifecycle event handler */ created() { this.JWT_SECRET = process.env.JWT_SECRET; if (!this.JWT_SECRET) throw new Error("Missing jwt secret - service can't be started"); this.jwtIssuer = jwtIssuer; this.services = { agents: this.settings?.services?.agents ?? "agents" }; this.roles = { admin : this.settings.adminRole || "admin", default : this.settings.defaultRole || "member", }; }, /** * Service started lifecycle event handler */ started() {}, /** * Service stopped lifecycle event handler */ stopped() {} };