@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
JavaScript
;
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;
};