UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

545 lines (433 loc) 20.8 kB
'use strict'; var async = require('async'); var ForbiddenError = require('../utils/errors/http/forbidden.js'); var utils = require('../utils/utils.js'); var securityContext = require("../security/securityContext"); var uriTypes = require("../uri/uriType"); /** * The authorization processor. Depending on the existence of scopes, it validates the authorization * for accessing resources on all the corresponding levels: service-root, entity-set(s) and CRUD * method(s). * If authorized, the framework's async callback is called without an error. Otherwise, it is called * with a corresponding authorization error regarding top https://tools.ietf.org/html/rfc7235. * * @param context {Object} The OData context of the framework. * @param asyncDone {function} The async callback of the framework. */ exports.processAuthorization = function (context, asyncDone) { var strategies; var builtStrategies; var hasScopes; var token; var contextUriType = context.uriTree.uriType; var innerContext = { parentContext: context, isAuthorized: false, scopes: [] }; context.logger.info("authorizationProcessor", "processAuthorization()"); try { // check if the model of context has scopes hasScopes = exports.contextHasScopes(context); context.logger.debug("authorizationProcessor", " xsodata config has scopes: " + hasScopes); // If there is no scopes available at all, we allow request by default if (hasScopes === false) { return asyncDone(null, context); } if (contextUriType === undefined || contextUriType === null) { context.uriTree.uriType = uriTypes.determineUriType(context); } // if in batch request then use the parent context, within batch request not authorization header SHOULD be used if (context.batchContext && context.batchContext.parentContext) { token = securityContext.getAuthToken(context.batchContext.parentContext); } else { token = securityContext.getAuthToken(context); } context.logger.debug("authorizationProcessor", " token: " + token); builtStrategies = exports.buildStrategies(token, context); } catch (err) { // Call exit for logging purposes if (context.callRegisteredStep) { return context.callRegisteredStep(10, err, context, () => { return asyncDone(err, context); }); } else { return asyncDone(err, context); } } strategies = [ function (cb) { return cb(null, innerContext); } ].concat(builtStrategies); async.waterfall( strategies, function (err, innerContext) { let retError = err; // set isAuthorized if (err) { context.isAuthorized = false; } if (innerContext.isAuthorized !== true) { retError = new ForbiddenError('Forbidden request', context); context.isAuthorized = false; } // forward error to exit if (retError) { if (context.callRegisteredStep) { return context.callRegisteredStep(10, retError, context, () => { // TESTED return asyncDone(retError, context); }); } } return asyncDone(retError, context); } ); }; /** * Checks whether scopes are given in the corresponding XSOData service definition. This is used by * the authorization processor to validate the need for authorization checks. If no scopes are * defined at all authorization will be skipped. * * @param context {Object} The OData context of the framework. * @returns {boolean}. */ exports.contextHasScopes = function (context) { context.logger.debug("authorizationProcessor", "contextHasScopes()"); context.logger.debug("authorizationProcessor", "context.getScopes(): " + JSON.stringify(context.getScopes())); if (context.getScopes()) { return true; } return false; }; /** * The strategies builder method. It builds the needed strategies for the authorization checks * based on the request requirements. The collection of strategies is returned to the authorization * processor method, where they run within an async waterfall. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Array<Function>} Collection of required authorization-check strategies. */ exports.buildStrategies = function (token, context) { var strategies = [], uType = context.uriTree.uriType; context.logger.debug("authorizationProcessor", "buildStrategies()"); context.logger.debug("authorizationProcessor", " uriType: " + uType); // requests to service root or $metadata should be always allowed if (uType === uriTypes.URI0 || uType === uriTypes.URI8 || uType === uriTypes.URI9) { strategies = strategies.concat(exports.buildAlwaysAllowStrategy(token, context)); return strategies; } // all requests have to check scopes on service-root level strategies = strategies.concat(exports.buildServiceRootStrategy(token, context)); if (uType === uriTypes.URI1 || uType === uriTypes.URI2 || uType === uriTypes.URI3 || uType === uriTypes.URI4 || uType === uriTypes.URI5A || uType === uriTypes.URI5B || uType === uriTypes.URI15 || uType === uriTypes.URI16 || uType === uriTypes.URI17) { strategies = strategies.concat(exports.buildDbSegmentLastStrategy(token, context)); if (context.oData.systemQueryParameters && context.oData.systemQueryParameters.expand) { strategies = strategies.concat(exports.build$expandStrategy(token, context)); } } if (uType === uriTypes.URI6A || uType === uriTypes.URI6B) { strategies = strategies.concat(exports.buildMultipleDbSegmentsStrategy(token, context)); if (context.oData.systemQueryParameters && context.oData.systemQueryParameters.expand) { strategies = strategies.concat(exports.build$expandStrategy(token, context)); } } if (uType === uriTypes.URI7A || uType === uriTypes.URI7B) { strategies = strategies.concat(exports.build$linksStrategy(token, context)); // because $links supports multiple navProps, we have to prove the scopes of the middle Segs if (context.oData.dbSegment.nextDBSegment !== context.oData.dbSegmentLast) { strategies = strategies.concat(exports.buildMultipleDbSegmentsStrategy(token, context)); } } if (context.uriTree.queryParameters && context.uriTree.queryParameters["sap-ds-debug"]) { strategies = strategies.concat(exports.buildDebugStrategy(token, context)); } strategies = strategies.concat(exports.buildSecurityContextStrategy(token, context)); return strategies; }; /** * Builds the strategy that authorizes the request without any checks. This is used by the * strategies builder method to skip authorization checks for certain request types. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.buildAlwaysAllowStrategy = function buildAlwaysAllowStrategy(token, context) { context.logger.debug("authorizationProcessor", "buildAlwaysAllowStrategy()"); return function (innerContext, done) { innerContext.isAuthorized = true; done(null, innerContext); }; }; /** * Builds the strategy that collects scopes from the service-root level. * Scopes are collected in dependency of http request method. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.buildServiceRootStrategy = function buildServiceRootStrategy(token, context) { context.logger.debug("authorizationProcessor", "buildServiceRootStrategy()"); return function (innerContext, done) { var scopes = innerContext.parentContext.getScopes(), method = innerContext.parentContext.request.method, isLinks = innerContext.parentContext.oData.isLinks, collectedScopes = exports.collectScopes(method, scopes, isLinks); innerContext.scopes = innerContext.scopes.concat(collectedScopes); return done(null, innerContext); }; }; /** * Builds the strategy that collects scopes from the entity-set level of a single * concerned DB-segment (dbSegmentLast). Scopes are collected in dependency of http request method. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.buildDbSegmentLastStrategy = function buildDbSegmentLastStrategy(token, context) { context.logger.debug("authorizationProcessor", "buildDbSegmentLastStrategy()"); return function (innerContext, done) { var scopes = innerContext.parentContext.oData.dbSegmentLast.entityType.getScopes(), method = innerContext.parentContext.request.method, collectedScopes = exports.collectScopes(method, scopes); innerContext.scopes = innerContext.scopes.concat(collectedScopes); done(null, innerContext); }; }; /** * Builds the strategy that loops and collects scopes from the entity-set level * of consecutive DB-segments. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.buildMultipleDbSegmentsStrategy = function (token, context) { context.logger.debug("authorizationProcessor", "buildMultipleDbSegmentsStrategy()"); return function (innerContext, done) { var currentSegment, scopes, method = innerContext.parentContext.request.method, collectedScopes; currentSegment = innerContext.parentContext.oData.dbSegment; while (currentSegment) { scopes = currentSegment.entityType.getScopes(); collectedScopes = exports.collectScopes(method, scopes); innerContext.scopes = innerContext.scopes.concat(collectedScopes); currentSegment = currentSegment.nextDBSegment; } done(null, innerContext); }; }; /** * Builds the strategy that collects scopes from the entity-set level of the two * concerned DB-segments of $links: toBeUpdated and keySource. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.build$linksStrategy = function build$linksStrategy(token, context) { context.logger.debug("authorizationProcessor", "build$linksStrategy()"); return function (innerContext, done) { var method = innerContext.parentContext.request.method, toBeUpdated = innerContext.parentContext.oData.links.toBeUpdated, keySource = innerContext.parentContext.oData.links.keySource, toBeUpdatedScopes = toBeUpdated.entityType.getScopes(), keySourceScopes = keySource.entityType.getScopes(), m2n = innerContext.parentContext.oData.links.m2n; // This block checks if the keySource (source) or the toBeUpdated entity has to be checked. // In a $links request it is possible to have one --> many association or many --> one // association or many --> many association. This is handled by the different else if // blocks and the m2n property. if (method === "GET") { if (toBeUpdatedScopes && toBeUpdatedScopes.read) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.read); } if (keySourceScopes && keySourceScopes.read) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.read); } } else if (method === "POST") { if (toBeUpdatedScopes && toBeUpdatedScopes.create) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.create); } if (keySourceScopes && keySourceScopes.read) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.read); } // For MxN relationships, both segments must have the scopes of: read AND create if (m2n === true) { if (toBeUpdatedScopes && toBeUpdatedScopes.read) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.read); } if (keySourceScopes && keySourceScopes.create) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.create); } } } else if (method === "PUT") { if (toBeUpdatedScopes && toBeUpdatedScopes.update) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.update); } if (keySourceScopes && keySourceScopes.read) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.read); } // For MxN relationships, both segments must have the scopes of: read AND update if (m2n === true) { if (toBeUpdatedScopes && toBeUpdatedScopes.read) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.read); } if (keySourceScopes && keySourceScopes.update) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.update); } } } else if (method === "DELETE") { if (toBeUpdatedScopes && toBeUpdatedScopes.delete) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.delete); } if (keySourceScopes && keySourceScopes.read) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.read); } // For MxN relationships, both segments must have the scopes of: read AND delete if (m2n === true) { if (toBeUpdatedScopes && toBeUpdatedScopes.read) { innerContext.scopes = innerContext.scopes.concat(toBeUpdatedScopes.read); } if (keySourceScopes && keySourceScopes.delete) { innerContext.scopes = innerContext.scopes.concat(keySourceScopes.delete); } } } return done(null, innerContext); }; }; /** * Builds the strategy that collects scopes from the entity-set level of all * the concerned DB-segments of $expand. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.build$expandStrategy = function (token, context) { context.logger.debug("authorizationProcessor", "build$expandStrategy()"); return function (innerContext, done) { var dbSeg = innerContext.parentContext.oData.dbSegmentLast; innerContext.scopes = innerContext.scopes.concat( getScopesFromExpandTree(dbSeg._ExpandedNavigationsDBSeg) ); done(null, innerContext); }; /** * Loops over an expand tree and recursively collects scopes from all composing entity sets. * * @param expTree {Object} The expand tree of a DB-segment. e.g. for the following request * -> Employees('3')?$expand=ne_Room,ne_Room/nr_Building * the expand tree will look like: * dbSegmentLast._ExpandedNavigationsDBSeg: { * ne_Room: { * _ExpandedNavigationsDBSeg: { * nr_Building: {} * } * } * } * @returns {Array<String>} Collection of scopes. */ function getScopesFromExpandTree(expTree) { var expandScopes = [], expandTreeHasProperties, elem, elemScopes; Object.keys(expTree).forEach(function (key) { elem = expTree[key]; elemScopes = elem.entityType.getScopes(); if (elemScopes && elemScopes.read) { expandScopes = expandScopes.concat(elemScopes.read); } expandTreeHasProperties = utils.hasProperties(elem._ExpandedNavigationsDBSeg); if (expandTreeHasProperties) { expandScopes = expandScopes.concat(getScopesFromExpandTree(elem._ExpandedNavigationsDBSeg)); } }); return expandScopes; } }; /** * Builds the strategy that collects the debug scope from the service-root level. * If no debug scope is found (when the debug view is called), a 403 forbidden error is directly * returned. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.buildDebugStrategy = function buildDebugStrategy(token, context) { context.logger.debug("authorizationProcessor", "buildDebugStrategy()"); return function (innerContext, done) { var scopes = innerContext.parentContext.getScopes(); if (scopes && scopes.debug) { innerContext.scopes = innerContext.scopes.concat(scopes.debug); return done(null, innerContext); } else { return done(new ForbiddenError('Forbidden request', context), innerContext); } }; }; /** * Builds the security strategy that passes all collected scopes from previous strategies to the * security context in order to be checked against the given token. This strategy should be last * strategy of the strategies processing chain. * * @param token {string} Authorization token extracted from the HTTP header. * @param context {Object} The OData context of the framework. * @returns {Function} Task to run in the async waterfall of the authorization processor. */ exports.buildSecurityContextStrategy = function buildSecurityContextStrategy(token, context) { context.logger.debug("authorizationProcessor", "buildSecurityContextStrategy()"); return function (innerContext, done) { var scopes = innerContext.scopes; securityContext.checkScopes(context, token, scopes, function (err, isAuthorized) { innerContext.isAuthorized = isAuthorized; return done(err, innerContext); }); }; }; /** * Collect scopes regarding to http method. This method collects read scopes for GET requests, * update scopes for PUT request, create scopes for POST request and delete scopes for DELETE * requests. If request is $links request this method also collects read scope * * @param method {String} The http method * @param scopes {Object} The current scope object abbreviated from xsodata scopes * @param isLinks {Boolean} If true read scope will be collected anyway * @returns {Array} Returns the collection of scopes like ["scope1", "scope2"] */ exports.collectScopes = function collectScopes(method, scopes, isLinks) { var collection = []; // The "read" scope is always needed for $links regardless of HTTP method, because when you // want to update/create/delete from an entitySet, you always have to READ the key from the // source entitySet if (method === "GET" || isLinks === true) { if (scopes && scopes.read) { collection = collection.concat(scopes.read); } } if (method === "POST" && scopes && scopes.create) { collection = collection.concat(scopes.create); } else if (method === "PUT" && scopes && scopes.update) { collection = collection.concat(scopes.update); } else if (method === "DELETE" && scopes && scopes.delete) { collection = collection.concat(scopes.delete); } return collection; };