loopback/lib/access-context.js

328 lines
9.5 KiB
JavaScript

// Copyright IBM Corp. 2014,2016. All Rights Reserved.
// Node module: loopback
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var assert = require('assert');
var loopback = require('./loopback');
var debug = require('debug')('loopback:security:access-context');
/**
* Access context represents the context for a request to access protected
* resources
*
* @class
* @options {Object} context The context object
* @property {Principal[]} principals An array of principals
* @property {Function} model The model class
* @property {String} modelName The model name
* @property {String} modelId The model id
* @property {String} property The model property/method/relation name
* @property {String} method The model method to be invoked
* @property {String} accessType The access type
* @property {AccessToken} accessToken The access token
*
* @returns {AccessContext}
* @constructor
*/
function AccessContext(context) {
if (!(this instanceof AccessContext)) {
return new AccessContext(context);
}
context = context || {};
assert(context.registry,
'Application registry is mandatory in AccessContext but missing in provided context');
this.registry = context.registry;
this.principals = context.principals || [];
var model = context.model;
model = ('string' === typeof model) ? this.registry.getModel(model) : model;
this.model = model;
this.modelName = model && model.modelName;
this.modelId = context.id || context.modelId;
this.property = context.property || AccessContext.ALL;
this.method = context.method;
this.sharedMethod = context.sharedMethod;
this.sharedClass = this.sharedMethod && this.sharedMethod.sharedClass;
if (this.sharedMethod) {
this.methodNames = this.sharedMethod.aliases.concat([this.sharedMethod.name]);
} else {
this.methodNames = [];
}
if (this.sharedMethod) {
this.accessType = this.model._getAccessTypeForMethod(this.sharedMethod);
}
this.accessType = context.accessType || AccessContext.ALL;
assert(loopback.AccessToken,
'AccessToken model must be defined before AccessContext model');
this.accessToken = context.accessToken || loopback.AccessToken.ANONYMOUS;
var principalType = context.principalType || Principal.USER;
var principalId = context.principalId || undefined;
var principalName = context.principalName || undefined;
if (principalId) {
this.addPrincipal(principalType, principalId, principalName);
}
var token = this.accessToken || {};
if (token.userId) {
this.addPrincipal(Principal.USER, token.userId);
}
if (token.appId) {
this.addPrincipal(Principal.APPLICATION, token.appId);
}
this.remotingContext = context.remotingContext;
}
// Define constant for the wildcard
AccessContext.ALL = '*';
// Define constants for access types
AccessContext.READ = 'READ'; // Read operation
AccessContext.REPLICATE = 'REPLICATE'; // Replicate (pull) changes
AccessContext.WRITE = 'WRITE'; // Write operation
AccessContext.EXECUTE = 'EXECUTE'; // Execute operation
AccessContext.DEFAULT = 'DEFAULT'; // Not specified
AccessContext.ALLOW = 'ALLOW'; // Allow
AccessContext.ALARM = 'ALARM'; // Warn - send an alarm
AccessContext.AUDIT = 'AUDIT'; // Audit - record the access
AccessContext.DENY = 'DENY'; // Deny
AccessContext.permissionOrder = {
DEFAULT: 0,
ALLOW: 1,
ALARM: 2,
AUDIT: 3,
DENY: 4,
};
/**
* Add a principal to the context
* @param {String} principalType The principal type
* @param {*} principalId The principal id
* @param {String} [principalName] The principal name
* @returns {boolean}
*/
AccessContext.prototype.addPrincipal = function(principalType, principalId, principalName) {
var principal = new Principal(principalType, principalId, principalName);
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
if (p.equals(principal)) {
return false;
}
}
this.principals.push(principal);
return true;
};
/**
* Get the user id
* @returns {*}
*/
AccessContext.prototype.getUserId = function() {
var BaseUser = this.registry.getModel('User');
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
var isBuiltinPrincipal = p.type === Principal.APP ||
p.type === Principal.ROLE ||
p.type == Principal.SCOPE;
if (isBuiltinPrincipal) continue;
// the principalType must either be 'USER'
if (p.type === Principal.USER) {
return p.id;
}
// or permit to resolve a valid user model
var userModel = this.registry.findModel(p.type);
if (!userModel) continue;
if (userModel.prototype instanceof BaseUser) {
return p.id;
}
}
return null;
};
/**
* Get the application id
* @returns {*}
*/
AccessContext.prototype.getAppId = function() {
for (var i = 0; i < this.principals.length; i++) {
var p = this.principals[i];
if (p.type === Principal.APPLICATION) {
return p.id;
}
}
return null;
};
/**
* Check if the access context has authenticated principals
* @returns {boolean}
*/
AccessContext.prototype.isAuthenticated = function() {
return !!(this.getUserId() || this.getAppId());
};
/*!
* Print debug info for access context.
*/
AccessContext.prototype.debug = function() {
if (debug.enabled) {
debug('---AccessContext---');
if (this.principals && this.principals.length) {
debug('principals:');
this.principals.forEach(function(principal) {
debug('principal: %j', principal);
});
} else {
debug('principals: %j', this.principals);
}
debug('modelName %s', this.modelName);
debug('modelId %s', this.modelId);
debug('property %s', this.property);
debug('method %s', this.method);
debug('accessType %s', this.accessType);
if (this.accessToken) {
debug('accessToken:');
debug(' id %j', this.accessToken.id);
debug(' ttl %j', this.accessToken.ttl);
}
debug('getUserId() %s', this.getUserId());
debug('isAuthenticated() %s', this.isAuthenticated());
}
};
/**
* This class represents the abstract notion of a principal, which can be used
* to represent any entity, such as an individual, a corporation, and a login id
* @param {String} type The principal type
* @param {*} id The principal id
* @param {String} [name] The principal name
* @param {String} modelName The principal model name
* @returns {Principal}
* @class
*/
function Principal(type, id, name) {
if (!(this instanceof Principal)) {
return new Principal(type, id, name);
}
this.type = type;
this.id = id;
this.name = name;
}
// Define constants for principal types
Principal.USER = 'USER';
Principal.APP = Principal.APPLICATION = 'APP';
Principal.ROLE = 'ROLE';
Principal.SCOPE = 'SCOPE';
/**
* Compare if two principals are equal
* Returns true if argument principal is equal to this principal.
* @param {Object} p The other principal
*/
Principal.prototype.equals = function(p) {
if (p instanceof Principal) {
return this.type === p.type && String(this.id) === String(p.id);
}
return false;
};
/**
* A request to access protected resources.
* @param {String} model The model name
* @param {String} property
* @param {String} accessType The access type
* @param {String} permission The requested permission
* @returns {AccessRequest}
* @class
*/
function AccessRequest(model, property, accessType, permission, methodNames) {
if (!(this instanceof AccessRequest)) {
return new AccessRequest(model, property, accessType);
}
if (arguments.length === 1 && typeof model === 'object') {
// The argument is an object that contains all required properties
var obj = model || {};
this.model = obj.model || AccessContext.ALL;
this.property = obj.property || AccessContext.ALL;
this.accessType = obj.accessType || AccessContext.ALL;
this.permission = obj.permission || AccessContext.DEFAULT;
this.methodNames = methodNames || [];
} else {
this.model = model || AccessContext.ALL;
this.property = property || AccessContext.ALL;
this.accessType = accessType || AccessContext.ALL;
this.permission = permission || AccessContext.DEFAULT;
this.methodNames = methodNames || [];
}
}
/**
* Does the request contain any wildcards?
*
* @returns {Boolean}
*/
AccessRequest.prototype.isWildcard = function() {
return this.model === AccessContext.ALL ||
this.property === AccessContext.ALL ||
this.accessType === AccessContext.ALL;
};
/**
* Does the given `ACL` apply to this `AccessRequest`.
*
* @param {ACL} acl
*/
AccessRequest.prototype.exactlyMatches = function(acl) {
var matchesModel = acl.model === this.model;
var matchesProperty = acl.property === this.property;
var matchesMethodName = this.methodNames.indexOf(acl.property) !== -1;
var matchesAccessType = acl.accessType === this.accessType;
if (matchesModel && matchesAccessType) {
return matchesProperty || matchesMethodName;
}
return false;
};
/**
* Is the request for access allowed?
*
* @returns {Boolean}
*/
AccessRequest.prototype.isAllowed = function() {
return this.permission !== loopback.ACL.DENY;
};
AccessRequest.prototype.debug = function() {
if (debug.enabled) {
debug('---AccessRequest---');
debug(' model %s', this.model);
debug(' property %s', this.property);
debug(' accessType %s', this.accessType);
debug(' permission %s', this.permission);
debug(' isWildcard() %s', this.isWildcard());
debug(' isAllowed() %s', this.isAllowed());
}
};
module.exports.AccessContext = AccessContext;
module.exports.Principal = Principal;
module.exports.AccessRequest = AccessRequest;