@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
573 lines (497 loc) • 19.8 kB
JavaScript
;
const async = require('async');
const ForbiddenError = require('../utils/errors/http/forbidden.js');
const utils = require('../utils/utils.js');
const securityContext = require('../security/securityContext');
const 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) {
let strategies;
let builtStrategies;
let hasScopes;
let token;
const contextUriType = context.uriTree.uriType;
const 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())
);
return context.getScopes() ? true : 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) {
let 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) {
const 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) {
const 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) {
let currentSegment, scopes, collectedScopes;
const method = innerContext.parentContext.request.method;
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) {
const 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;
innerContext.scopes = innerContext.scopes.concat(
collectLinkScopes(method, toBeUpdatedScopes, keySourceScopes, m2n)
);
return done(null, innerContext);
};
};
function collectLinkScopes(method, toBeUpdatedScopes, keySourceScopes, m2n) {
const req = getLinkRequirements(method, m2n);
let acc = [];
acc = addScopes(acc, toBeUpdatedScopes, req.toBeUpdated);
acc = addScopes(acc, keySourceScopes, req.keySource);
return acc;
}
function getLinkRequirements(method, m2n) {
const base = {
GET: { toBeUpdated: ['read'], keySource: ['read'] },
POST: { toBeUpdated: ['create'], keySource: ['read'] },
PUT: { toBeUpdated: ['update'], keySource: ['read'] },
DELETE: { toBeUpdated: ['delete'], keySource: ['read'] },
};
let req = base[method] || { toBeUpdated: [], keySource: [] };
if (m2n === true) {
const extra = {
POST: { toBeUpdated: ['read'], keySource: ['create'] },
PUT: { toBeUpdated: ['read'], keySource: ['update'] },
DELETE: { toBeUpdated: ['read'], keySource: ['delete'] },
};
const add = extra[method];
if (add) {
req = {
toBeUpdated: req.toBeUpdated.concat(add.toBeUpdated),
keySource: req.keySource.concat(add.keySource),
};
}
}
return req;
}
function addScopes(acc, scopeObj, keys) {
if (!keys || keys.length === 0) {
return acc;
}
for (const key of keys) {
if (scopeObj && scopeObj[key]) {
acc = acc.concat(scopeObj[key]);
}
}
return acc;
}
/**
* 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) {
const 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) {
let 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) {
const 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) {
const 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) {
let 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;
};