Skip to content

Commit

Permalink
AccessControl for change replication
Browse files Browse the repository at this point in the history
1) Add integration tests running change replication over REST to verify
that access control at model level is correctly enforced.

2) Implement a new access type "REPLICATE" that allows principals
to create new checkpoints, even though they don't have full WRITE
access to the model. Together with the "READ" permission, these
two types allow principals to replicate (pull) changes from the server.

Note that anybody having "WRITE" access type is automatically
granted "REPLICATE" type too.

3) Add a new model option "enableRemoteReplication" that exposes
replication methods via strong remoting, but does not configure
change rectification. This option should be used the clients
when setting up Remote models attached to the server via the remoting
connector.
  • Loading branch information
Miroslav Bajtoš committed Apr 7, 2015
1 parent 699bc7a commit a2fadae
Show file tree
Hide file tree
Showing 7 changed files with 550 additions and 20 deletions.
42 changes: 30 additions & 12 deletions common/models/acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ module.exports = function(ACL) {
ACL.DENY = AccessContext.DENY; // Deny

ACL.READ = AccessContext.READ; // Read operation
ACL.REPLICATE = AccessContext.REPLICATE; // Replicate (pull) changes
ACL.WRITE = AccessContext.WRITE; // Write operation
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation

Expand All @@ -109,21 +110,31 @@ module.exports = function(ACL) {
for (var i = 0; i < props.length; i++) {
// Shift the score by 4 for each of the properties as the weight
score = score * 4;
var val1 = rule[props[i]] || ACL.ALL;
var val2 = req[props[i]] || ACL.ALL;
var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(val1) !== -1;

// accessType: EXECUTE should match READ or WRITE
var isMatchingAccessType = props[i] === 'accessType' &&
val1 === ACL.EXECUTE;
var ruleValue = rule[props[i]] || ACL.ALL;
var requestedValue = req[props[i]] || ACL.ALL;
var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(ruleValue) !== -1;

var isMatchingAccessType = ruleValue === requestedValue;
if (props[i] === 'accessType' && !isMatchingAccessType) {
switch (ruleValue) {
case ACL.EXECUTE:
// EXECUTE should match READ, REPLICATE and WRITE
isMatchingAccessType = true;
break;
case ACL.WRITE:
// WRITE should match REPLICATE too
isMatchingAccessType = requestedValue === ACL.REPLICATE;
break;
}
}

if (val1 === val2 || isMatchingMethodName || isMatchingAccessType) {
if (isMatchingMethodName || isMatchingAccessType) {
// Exact match
score += 3;
} else if (val1 === ACL.ALL) {
} else if (ruleValue === ACL.ALL) {
// Wildcard match
score += 2;
} else if (val2 === ACL.ALL) {
} else if (requestedValue === ACL.ALL) {
score += 1;
} else {
// Doesn't match at all
Expand Down Expand Up @@ -370,7 +381,8 @@ module.exports = function(ACL) {
* @property {String|Model} model The model name or model class.
* @property {*} id The model instance ID.
* @property {String} property The property/method/relation name.
* @property {String} accessType The access type: READE, WRITE, or EXECUTE.
* @property {String} accessType The access type:
* READ, REPLICATE, WRITE, or EXECUTE.
* @param {Function} callback Callback function
*/

Expand All @@ -388,7 +400,12 @@ module.exports = function(ACL) {

var methodNames = context.methodNames;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};

var accessTypeQuery = (accessType === ACL.ALL) ?
undefined :
(accessType === ACL.REPLICATE) ?
{inq: [ACL.REPLICATE, ACL.WRITE, ACL.ALL]} :
{inq: [accessType, ACL.ALL]};

var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);

Expand Down Expand Up @@ -438,6 +455,7 @@ module.exports = function(ACL) {
if (callback) callback(err, null);
return;
}

var resolved = self.resolvePermission(effectiveACLs, req);
if (resolved && resolved.permission === ACL.DEFAULT) {
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
Expand Down
1 change: 1 addition & 0 deletions lib/access-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ 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

Expand Down
3 changes: 2 additions & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,10 +333,11 @@ module.exports = function(registry) {
// Check the explicit setting of accessType
if (method.accessType) {
assert(method.accessType === ACL.READ ||
method.accessType === ACL.REPLICATE ||
method.accessType === ACL.WRITE ||
method.accessType === ACL.EXECUTE, 'invalid accessType ' +
method.accessType +
'. It must be "READ", "WRITE", or "EXECUTE"');
'. It must be "READ", "REPLICATE", "WRITE", or "EXECUTE"');
return method.accessType;
}

Expand Down
35 changes: 28 additions & 7 deletions lib/persisted-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ module.exports = function(registry) {
PersistedModel.once('dataSourceAttached', function() {
PersistedModel.enableChangeTracking();
});
} else if (this.settings.enableRemoteReplication) {
PersistedModel._defineChangeModel();
}

PersistedModel.setupRemoting();
Expand Down Expand Up @@ -643,7 +645,7 @@ module.exports = function(registry) {
http: {verb: 'put', path: '/'}
});

if (options.trackChanges) {
if (options.trackChanges || options.enableRemoteReplication) {
setRemoting(PersistedModel, 'diff', {
description: 'Get a set of deltas and conflicts since the given checkpoint',
accessType: 'READ',
Expand All @@ -670,7 +672,11 @@ module.exports = function(registry) {

setRemoting(PersistedModel, 'checkpoint', {
description: 'Create a checkpoint.',
accessType: 'WRITE',
// The replication algorithm needs to create a source checkpoint,
// even though it is otherwise not making any source changes.
// We need to allow this method for users that don't have full
// WRITE permissions.
accessType: 'REPLICATE',
returns: {arg: 'checkpoint', type: 'object', root: true},
http: {verb: 'post', path: '/checkpoint'}
});
Expand All @@ -684,7 +690,10 @@ module.exports = function(registry) {

setRemoting(PersistedModel, 'createUpdates', {
description: 'Create an update list from a delta list',
accessType: 'WRITE',
// This operation is read-only, it does not change any local data.
// It is called by the replication algorithm to compile a list
// of changes to apply on the target.
accessType: 'READ',
accepts: {arg: 'deltas', type: 'array', http: {source: 'body'}},
returns: {arg: 'updates', type: 'array', root: true},
http: {verb: 'post', path: '/create-updates'}
Expand All @@ -696,7 +705,11 @@ module.exports = function(registry) {
accepts: {arg: 'updates', type: 'array'},
http: {verb: 'post', path: '/bulk-update'}
});
}

if (options.trackChanges) {
// Deprecated (legacy) exports kept for backwards compatibility
// TODO(bajtos) Hide these two exports in LoopBack 3.0
setRemoting(PersistedModel, 'rectifyAllChanges', {
description: 'Rectify all Model changes.',
accessType: 'WRITE',
Expand Down Expand Up @@ -1280,7 +1293,7 @@ module.exports = function(registry) {
var changeModel = this.Change;
var isSetup = changeModel && changeModel.dataSource;

assert(isSetup, 'Cannot get a setup Change model');
assert(isSetup, 'Cannot get a setup Change model for ' + this.modelName);

return changeModel;
};
Expand Down Expand Up @@ -1327,9 +1340,6 @@ module.exports = function(registry) {
'which requries a string id with GUID/UUID default value.');
}

Change.attachTo(this.dataSource);
Change.getCheckpointModel().attachTo(this.dataSource);

Model.observe('after save', rectifyOnSave);

Model.observe('after delete', rectifyOnDelete);
Expand Down Expand Up @@ -1411,7 +1421,18 @@ module.exports = function(registry) {
}
);

if (this.dataSource) {
attachRelatedModels(this);
} else {
this.once('dataSourceAttached', attachRelatedModels);
}

return this.Change;

function attachRelatedModels(self) {
self.Change.attachTo(self.dataSource);
self.Change.getCheckpointModel().attachTo(self.dataSource);
}
};

PersistedModel.rectifyAllChanges = function(callback) {
Expand Down
Loading

0 comments on commit a2fadae

Please sign in to comment.