Skip to content

Commit

Permalink
Merge pull request #64 from Kashoo/issue-61-role-access-assignments
Browse files Browse the repository at this point in the history
Issue 61: Assign role access to users
  • Loading branch information
dkichler authored Dec 14, 2016
2 parents 3ce2e1f + f519631 commit ab2afed
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 79 deletions.
3 changes: 2 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"customActionStub": false,
"requireAccess": false,
"requireRole": false,
"requireUser": false
"requireUser": false,
"role": false
}
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- [#25](https://github.com/Kashoo/synctos/issues/25): Support custom actions to be executed on a document type
- [#61](https://github.com/Kashoo/synctos/issues/61): Support dynamic assignment of roles to users

## [1.4.0] - 2016-11-30
### Added
Expand Down
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,33 @@ Or:
}
```

* `accessAssignments`: (optional) Defines the channels to dynamically assign to users and/or roles when a document of the corresponding type is successfully created, replaced or deleted. It is specified as a list, where each entry is an object that defines `users`, `roles` and/or `channels` properties. The value of each property can be either a list of strings that specify the raw user/role/channel names or a function that returns the corresponding values as a dynamically-constructed list and accepts the following parameters: (1) the new document and (2) the old document that is being replaced/deleted (if any). NOTE: In cases where the document is in the process of being deleted, the first parameter's `_deleted` property will be `true`, so be sure to account for such cases. And, if the old document has been deleted or simply does not exist, the second parameter will be `null`. An example:
* `accessAssignments`: (optional) Defines either the channel access to assign to users/roles or the role access to assign to users when a document of the corresponding type is successfully created, replaced or deleted. It is specified as a list, where each entry is an object that defines `users`, `roles` and/or `channels` properties, depending on the access assignment type. The value of each property can be either a list of strings that specify the raw user/role/channel names or a function that returns the corresponding values as a dynamically-constructed list and accepts the following parameters: (1) the new document and (2) the old document that is being replaced/deleted (if any). NOTE: In cases where the document is in the process of being deleted, the first parameter's `_deleted` property will be `true`, so be sure to account for such cases. And, if the old document has been deleted or simply does not exist, the second parameter will be `null`. The assignment types are specified as follows:
* Channel access assignments:
* `type`: May be either "channel", `null` or `undefined`.
* `channels`: The channels to assign to users and/or roles.
* `roles`: The roles to which to assign the channels.
* `users`: The users to which to assign the channels.
* Role access assignments:
* `type`: Must be "role".
* `roles`: The roles to assign to users.
* `users`: The users to which to assign the roles.

An example of a mix of channel and role access assignments:

```
accessAssignments: [
{
users: [ 'user1', 'user2' ],
channels: [ 'channel1' ]
type: 'role',
users: [ 'user3', 'user4' ],
roles: [ 'role1', 'role2' ]
},
{
roles: [ 'role1', 'role2' ],
channels: [ 'channel2' ]
type: 'channel',
users: [ 'user1', 'user2' ],
channels: [ 'channel1' ]
},
{
type: 'channel',
users: function(doc, oldDoc) {
return doc.users;
},
Expand All @@ -309,7 +323,15 @@ Or:
* `authorization`: An object that indicates which channels, roles and users were used to authorize the current operation, as specified by the `channels`, `roles` and `users` list properties.
3. `onValidationSucceeded`: Executed immediately after the document's contents are validated and before channels are assigned to users/roles and the document. Not executed if the document's contents are invalid. The custom action metadata object parameter includes properties from all previous events but does not include any additional properties.
4. `onAccessAssignmentsSucceeded`: Executed immediately after channel access is assigned to users/roles and before channels are assigned to the document. Not executed if the document definition does not include an `accessAssignments` property. The custom action metadata object parameter includes properties from all previous events in addition to the following properties:
* `accessAssignments`: A list that contains each of the access assignments that were applied, where each element's `usersAndRoles` property is a list of the users and roles and the `channels` property is a list of the channels that were granted to them. Note that, as per the sync function API, each role element's value is prefixed with "role:".
* `accessAssignments`: A list that contains each of the access assignments that were applied. Each element is an object that represents either a channel access assignment or a role access assignment depending on the value of its `type` property. The assignment types are specified as follows:
* Channel access assignments:
* `type`: Value of "channel".
* `channels`: A list of channels that were assigned to the users/roles.
* `usersAndRoles`: A list of the combined users and/or roles to which the channels were assigned. Note that, as per the sync function API, each role element's value is prefixed with "role:".
* Role access assignments:
* `type`: Value of "role".
* `roles`: A list of roles that were assigned to the users.
* `users`: A list of users to which the roles were assigned. Note that, as per the sync function API, each role element's value is prefixed with "role:".
5. `onDocumentChannelAssignmentSucceeded`: Executed immediately after channels are assigned to the document. The last step before the sync function is finished executing and the document revision is written. The custom action metadata object parameter includes properties from all previous events in addition to the following properties:
* `documentChannels`: A list of channels that were assigned to the document.

Expand Down
3 changes: 2 additions & 1 deletion etc/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"customActionStub": true,
"requireAccess": true,
"requireRole": true,
"requireUser": true
"requireUser": true,
"role": true
}
}
72 changes: 50 additions & 22 deletions etc/sync-function-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -744,35 +744,63 @@ function synctos(doc, oldDoc) {
}
}

// Assigns channels to users and roles according to the given access assignment definitions
// Transforms a role collection definition into a simple list and prefixes each element with "role:"
function resolveRoleCollectionDefinition(doc, oldDoc, rolesDefinition) {
return resolveCollectionDefinition(doc, oldDoc, rolesDefinition, 'role:');
}

// Assigns channel access to users/roles
function assignChannelsToUsersAndRoles(doc, oldDoc, accessAssignmentDefinition) {
var usersAndRoles = [ ];

var users = resolveCollectionDefinition(doc, oldDoc, accessAssignmentDefinition.users);
for (var userIndex = 0; userIndex < users.length; userIndex++) {
usersAndRoles.push(users[userIndex]);
}

var roles = resolveRoleCollectionDefinition(doc, oldDoc, accessAssignmentDefinition.roles);
for (var roleIndex = 0; roleIndex < roles.length; roleIndex++) {
usersAndRoles.push(roles[roleIndex]);
}

var channels = resolveCollectionDefinition(doc, oldDoc, accessAssignmentDefinition.channels);

access(usersAndRoles, channels);

return {
type: 'channel',
usersAndRoles: usersAndRoles,
channels: channels
};
}

// Assigns role access to users
function assignRolesToUsers(doc, oldDoc, accessAssignmentDefinition) {
var users = resolveCollectionDefinition(doc, oldDoc, accessAssignmentDefinition.users);
var roles = resolveRoleCollectionDefinition(doc, oldDoc, accessAssignmentDefinition.roles);

role(users, roles);

return {
type: 'role',
users: users,
roles: roles
};
}

// Assigns role access to users and/or channel access to users/roles according to the given access assignment definitions
function assignUserAccess(doc, oldDoc, accessAssignmentDefinitions) {
var effectiveOldDoc = getEffectiveOldDoc(oldDoc);

var effectiveAssignments = [ ];
for (var assignmentIndex = 0; assignmentIndex < accessAssignmentDefinitions.length; assignmentIndex++) {
var definition = accessAssignmentDefinitions[assignmentIndex];
var usersAndRoles = [ ];

var users = resolveCollectionDefinition(doc, effectiveOldDoc, definition.users);
for (var userIndex = 0; userIndex < users.length; userIndex++) {
usersAndRoles.push(users[userIndex]);
if (definition.type === 'role') {
effectiveAssignments.push(assignRolesToUsers(doc, effectiveOldDoc, definition));
} else if (definition.type === 'channel' || isValueNullOrUndefined(definition.type)) {
effectiveAssignments.push(assignChannelsToUsersAndRoles(doc, effectiveOldDoc, definition));
}

// Role names must begin with the special token "role:" to distinguish them from users
var roles = resolveCollectionDefinition(doc, effectiveOldDoc, definition.roles, 'role:');
for (var roleIndex = 0; roleIndex < roles.length; roleIndex++) {
usersAndRoles.push(roles[roleIndex]);
}

var channels = resolveCollectionDefinition(doc, effectiveOldDoc, definition.channels);

access(usersAndRoles, channels);

effectiveAssignments.push({
type: 'channel',
usersAndRoles: usersAndRoles,
channels: channels
});
}

return effectiveAssignments;
Expand Down Expand Up @@ -840,7 +868,7 @@ function synctos(doc, oldDoc) {
}
}

// Getting here means the document write is authorized and valid, and the appropriate channel(s) should now be assigned
// Getting here means the document revision is authorized and valid, and the appropriate channel(s) should now be assigned
var allDocChannels = getAllDocChannels(doc, oldDoc, theDocDefinition);
channel(allDocChannels);
customActionMetadata.documentChannels = allDocChannels;
Expand Down
140 changes: 97 additions & 43 deletions etc/test-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var requireRole;
var requireUser;
var channel;
var access;
var role;

var syncFunction;

Expand All @@ -31,6 +32,7 @@ function init(syncFunctionPath) {
exports.requireUser = requireUser = simple.stub();
exports.channel = channel = simple.stub();
exports.access = access = simple.stub();
exports.role = role = simple.stub();

exports.customActionStub = customActionStub = simple.stub();
}
Expand Down Expand Up @@ -102,69 +104,121 @@ function areUnorderedListsEqual(list1, list2) {
return true;
}

function accessAssignmentCallExists(expectedUsersAndRoles, expectedChannels) {
// Try to find an actual access assignment call that matches the expected call
for (var accessCallIndex = 0; accessCallIndex < access.callCount; accessCallIndex++) {
var accessCall = access.calls[accessCallIndex];
if (areUnorderedListsEqual(accessCall.args[0], expectedUsersAndRoles) && areUnorderedListsEqual(accessCall.args[1], expectedChannels)) {
function accessAssignmentCallExists(accessFunction, expectedParam1, expectedParam2) {
// Try to find an actual channel/role access assignment call that matches the expected call
for (var accessCallIndex = 0; accessCallIndex < accessFunction.callCount; accessCallIndex++) {
var accessCall = accessFunction.calls[accessCallIndex];
if (areUnorderedListsEqual(accessCall.args[0], expectedParam1) && areUnorderedListsEqual(accessCall.args[1], expectedParam2)) {
return true;
}
}

return false;
}

function verifyAccessAssignments(expectedAccessAssignments) {
var assignmentIndex;
for (assignmentIndex = 0; assignmentIndex < expectedAccessAssignments.length; assignmentIndex++) {
var expectedAssignment = expectedAccessAssignments[assignmentIndex];
function prefixRoleName(role) {
return 'role:' + role;
}

var expectedUsersAndRoles = [ ];
if (expectedAssignment.expectedUsers) {
if (expectedAssignment.expectedUsers instanceof Array) {
for (var userIndex = 0; userIndex < expectedAssignment.expectedUsers.length; userIndex++) {
expectedUsersAndRoles.push(expectedAssignment.expectedUsers[userIndex]);
}
} else {
expectedUsersAndRoles.push(expectedAssignment.expectedUsers);
function verifyChannelAccessAssignment(expectedAssignment) {
var expectedUsersAndRoles = [ ];
if (expectedAssignment.expectedUsers) {
if (expectedAssignment.expectedUsers instanceof Array) {
for (var userIndex = 0; userIndex < expectedAssignment.expectedUsers.length; userIndex++) {
expectedUsersAndRoles.push(expectedAssignment.expectedUsers[userIndex]);
}
} else {
expectedUsersAndRoles.push(expectedAssignment.expectedUsers);
}
}

if (expectedAssignment.expectedRoles) {
// The prefix "role:" must be applied to roles when calling the access function, as specified by
// http://developer.couchbase.com/documentation/mobile/current/develop/guides/sync-gateway/channels/developing/index.html#programmatic-authorization
if (expectedAssignment.expectedRoles instanceof Array) {
for (var roleIndex = 0; roleIndex < expectedAssignment.expectedRoles.length; roleIndex++) {
expectedUsersAndRoles.push('role:' + expectedAssignment.expectedRoles[roleIndex]);
}
} else {
expectedUsersAndRoles.push('role:' + expectedAssignment.expectedRoles);
if (expectedAssignment.expectedRoles) {
// The prefix "role:" must be applied to roles when calling the access function, as specified by
// http://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#access-username-channelname
if (expectedAssignment.expectedRoles instanceof Array) {
for (var roleIndex = 0; roleIndex < expectedAssignment.expectedRoles.length; roleIndex++) {
expectedUsersAndRoles.push(prefixRoleName(expectedAssignment.expectedRoles[roleIndex]));
}
} else {
expectedUsersAndRoles.push(prefixRoleName(expectedAssignment.expectedRoles));
}
}

var expectedChannels = [ ];
if (expectedAssignment.expectedChannels) {
if (expectedAssignment.expectedChannels instanceof Array) {
for (var channelIndex = 0; channelIndex < expectedAssignment.expectedChannels.length; channelIndex++) {
expectedChannels.push(expectedAssignment.expectedChannels[channelIndex]);
}
} else {
expectedChannels.push(expectedAssignment.expectedChannels);
var expectedChannels = [ ];
if (expectedAssignment.expectedChannels) {
if (expectedAssignment.expectedChannels instanceof Array) {
for (var channelIndex = 0; channelIndex < expectedAssignment.expectedChannels.length; channelIndex++) {
expectedChannels.push(expectedAssignment.expectedChannels[channelIndex]);
}
} else {
expectedChannels.push(expectedAssignment.expectedChannels);
}
}

if (!accessAssignmentCallExists(access, expectedUsersAndRoles, expectedChannels)) {
expect().fail(
'Missing expected call to assign channel access (' +
JSON.stringify(expectedChannels) +
') to users and roles (' +
JSON.stringify(expectedUsersAndRoles) +
')');
}
}

if (!accessAssignmentCallExists(expectedUsersAndRoles, expectedChannels)) {
expect().fail(
'Missing expected call to assign channel access (' +
JSON.stringify(expectedChannels) +
') to users and roles (' +
JSON.stringify(expectedUsersAndRoles) +
')');
function verifyRoleAccessAssignment(expectedAssignment) {
var expectedUsers = [ ];
if (expectedAssignment.expectedUsers) {
if (expectedAssignment.expectedUsers instanceof Array) {
expectedUsers = expectedAssignment.expectedUsers;
} else {
expectedUsers.push(expectedAssignment.expectedUsers);
}
}

if (access.callCount !== assignmentIndex) {
expect().fail('Number of calls to assign channel access (' + access.callCount + ') does not match expected (' + assignmentIndex + ')');
var expectedRoles = [ ];
if (expectedAssignment.expectedRoles) {
// The prefix "role:" must be applied to roles when calling the role function, as specified by
// http://developer.couchbase.com/documentation/mobile/current/guides/sync-gateway/sync-function-api-guide/index.html#role-username-rolename
if (expectedAssignment.expectedRoles instanceof Array) {
for (var roleIndex = 0; roleIndex < expectedAssignment.expectedRoles.length; roleIndex++) {
expectedRoles.push(prefixRoleName(expectedAssignment.expectedRoles[roleIndex]));
}
} else {
expectedRoles.push(prefixRoleName(expectedAssignment.expectedRoles));
}
}

if (!accessAssignmentCallExists(role, expectedUsers, expectedRoles)) {
expect().fail(
'Missing expected call to assign role access (' +
JSON.stringify(expectedRoles) +
') to users (' +
JSON.stringify(expectedUsers) +
')');
}
}

function verifyAccessAssignments(expectedAccessAssignments) {
var expectedAccessCalls = 0;
var expectedRoleCalls = 0;
for (var assignmentIndex = 0; assignmentIndex < expectedAccessAssignments.length; assignmentIndex++) {
var expectedAssignment = expectedAccessAssignments[assignmentIndex];

if (expectedAssignment.expectedType === 'role') {
verifyRoleAccessAssignment(expectedAssignment);
expectedRoleCalls++;
} else if (expectedAssignment.expectedType === 'channel' || !(expectedAssignment.expectedType)) {
verifyChannelAccessAssignment(expectedAssignment);
expectedAccessCalls++;
}
}

if (access.callCount !== expectedAccessCalls) {
expect().fail('Number of calls to assign channel access (' + access.callCount + ') does not match expected (' + expectedAccessCalls + ')');
}

if (role.callCount !== expectedRoleCalls) {
expect().fail('Number of calls to assign role access (' + role.callCount + ') does not match expected (' + expectedRoleCalls + ')');
}
}

Expand Down
13 changes: 7 additions & 6 deletions test/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
"extends": "../.jshintrc",
"undef": false,
"globals": {
"access": true,
"channel": true,
"customActionStub": true,
"requireAccess": true,
"requireRole": true,
"requireUser": true
"access": false,
"channel": false,
"customActionStub": false,
"requireAccess": false,
"requireRole": false,
"requireUser": false,
"role": false
}
}
Loading

0 comments on commit ab2afed

Please sign in to comment.