Skip to content

Commit

Permalink
feat: folder contents hierarchical permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
brecke committed Apr 6, 2020
1 parent c24fc91 commit 19dc9da
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 53 deletions.
135 changes: 102 additions & 33 deletions packages/oae-authz/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import util from 'util';
import _ from 'underscore';
import { dropRepeats, isEmpty, contains, not, equals } from 'ramda';

import * as Cassandra from 'oae-util/lib/cassandra';
import * as OaeUtil from 'oae-util/lib/util';
Expand Down Expand Up @@ -160,8 +161,8 @@ const getAllRoles = function(principalId, resourceId, callback) {
};

/**
* Given a principal and a resource, determine all the roles that the principal has on the resource, by virtue of
* indirect group inheritance.
* Given a principal and a resource, determine all the roles that the principal has on the resource,
* by virtue of indirect group inheritance.
*
* @param {String} principalId The principal id. This can be a user or a group
* @param {String} resourceId The resource id. This can be a group as well.
Expand All @@ -171,34 +172,99 @@ const getAllRoles = function(principalId, resourceId, callback) {
* @api private
*/
const _getIndirectRoles = function(principalId, resourceId, callback) {
/**
* If the current membership is 'inherit' that means that the permissions
* the user should hold on the resource indirectly should inherit
* from the permissions the user has as a member of that group.
*
* Example:
* FolderF is created by UserA
* UserA adds UserB to the folder group as manager
* UserA creates Resource R (some content item) inside folderF
* The folder group will hold "inherit" as a role on ResourceR (table AuthzMembers)
* UserB will have indirect permissions on folderF as manager
*
* .─.
* ( )
* creates `─'
* ┌───────────── User A
* │
* │
* ▼
* ┌───────────────┐
* │ Folder F │
* └───────────────┘
* ▲
* │ .─.
* │ ( )
* │ manager `─'
* ├──────────── User B
* │
* │ .─.
* │ ( )
* │ viewer `─'
* └──────────── User C
*
*
*
* .─.
* ( )
* `─' creates
* User A ──────────┐
* │
* │
* ▼
* ┌───────────────┐ inside ┌───────────────┐
* │ Folder F │◀──────────────│ Content item │
* └───────────────┘ └───────────────┘
* ▲ ▲
* │ .─.
* │ ( ) │
* │ manager `─' manager
* ├────────── User B ─ ─ ─ ─ ─ ┤
* │
* │ .─. │
* │ ( )
* │ viewer `─' viewer │
* └────────── User C ─ ─ ─ ─ ─
*
*/
// Get the groups that are directly associated to the resource
_getResourceGroupMembers(resourceId, (err, groups) => {
if (err) {
return callback(err);
}
_getResourceGroupMembers(resourceId, (err, resourceGroupRoles) => {
if (err) return callback(err);

if (_.isEmpty(groups)) {
return callback(null, []);
}
if (isEmpty(resourceGroupRoles)) return callback(null, []);

// Check whether any of these groups are part of the user's direct memberships
const groupIds = _.keys(groups);
const groupIds = _.keys(resourceGroupRoles);

// Make sure that the user's memberships have been exploded and cached
_checkGroupMembershipsForUser(principalId, groupIds, (err, memberships) => {
if (err) {
return callback(err);
}

// Add the roles of the matching groups
const allRoles = [];
for (const element of memberships) {
if (!_.contains(allRoles, groups[element])) {
allRoles.push(groups[element]);
}
}
_checkGroupMembershipsForUser(principalId, groupIds, (err, groupsPrincipalBelongsTo) => {
if (err) return callback(err);

const getIndirectMembershipRole = (memberships, allRoles, callback) => {
const eachMembership = memberships.pop();

if (not(eachMembership)) return callback(null, dropRepeats(allRoles));

const roleOnResource = resourceGroupRoles[eachMembership];
// if (equals(roleOnGroup, 'inherit')) {
// check what the direct role of the principal on the group is, and use that
_getDirectRole(principalId, eachMembership, (err, directRoleOnGroup) => {
if (err) return callback(err);
allRoles.push(directRoleOnGroup);
// debug
console.log('The group role is: ' + roleOnResource);
console.log('The indirect role through the folder is ' + directRoleOnGroup);
return getIndirectMembershipRole(memberships, allRoles, callback);
});
// } else {
// allRoles.push(roleOnGroup);
// return getIndirectMembershipRole(memberships, allRoles, callback);
// }
};

return callback(null, allRoles);
getIndirectMembershipRole(groupsPrincipalBelongsTo, [], callback);
});
});
};
Expand Down Expand Up @@ -347,8 +413,9 @@ const _hasRole = function(principalId, resourceId, role, callback) {
};

/**
* Assign one or multiple principals a role on a resource instance. If the user already has a role, it will simply be updated. When
* false is passed in as a role, the role for that principal will be removed.
* Assign one or multiple principals a role on a resource instance.
* If the user already has a role, it will simply be updated.
* When false is passed in as a role, the role for that principal will be removed.
*
* @param {String} resourceId The resource id
* @param {Object} changes An object keyed by principal id, whose values are the role changes to apply on the resource
Expand Down Expand Up @@ -389,8 +456,9 @@ const updateRoles = function(resourceId, changes, callback) {
};

/**
* Assign multiple principals a role on a resource instance. If the user already has a role, it will simply be updated. When
* false is passed in as a role, the role for that principal will be removed.
* Assign multiple principals a role on a resource instance.
* If the user already has a role, it will simply be updated.
* When false is passed in as a role, the role for that principal will be removed.
*
* @param {String} resourceId The resource id
* @param {Object} changes An object keyed by principal id, whose values are the role changes to apply on the resource
Expand Down Expand Up @@ -643,7 +711,8 @@ const _checkGroupMembershipsForUser = function(userId, groupIds, callback) {
};

/**
* Get all the Authz groups of which a principal is a member. This includes all group ancestors to which the user is indirectly a member.
* Get all the Authz groups of which a principal is a member.
* This includes all group ancestors to which the user is indirectly a member.
* Once these have been retrieved, they will be cached inside of Cassandra for fast permission checks
*
* @param {String} principalId The principal id for whom we want to explode the group memberships
Expand Down Expand Up @@ -888,8 +957,8 @@ const getPrincipalMembershipsGraph = function(principalId, callback) {
};

/**
* Gets all the Authz groups of which a principal (either user or group) is a member. This includes all group ancestors to
* which the user is indirectly a member.
* Gets all the Authz groups of which a principal (either user or group) is a member.
* This includes all group ancestors to which the user is indirectly a member.
*
* @param {String} principalId The principal id for which to retrieve all the group memberships
* @param {String} start Determines the point at which group memberships members are returned for paging purposes. If not provided, the first x elements will be returned
Expand Down Expand Up @@ -1472,9 +1541,9 @@ const computeMemberRolesAfterChanges = function(resourceId, memberRoles, opts, c

/**
* Determine the **effective** role a user has in a resource. Though a user can have multiple roles on a resource
* by virtue of indirect group membership, this determines the highest level of access granted. This check is not only
* implicit, but includes explicit role membership lookup. Therefore, its output alone can be used as an authoritative
* source of access information.
* by virtue of indirect group membership, this determines the highest level of access granted.
* This check is not only implicit, but includes explicit role membership lookup.
* Therefore, its output alone can be used as an authoritative source of access information.
*
* @param {User} [user] The user for which to check access. If unspecified, implies an anonymous user
* @param {Resource} resource The resource against which to check for access
Expand Down
32 changes: 12 additions & 20 deletions packages/oae-folders/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,15 +317,11 @@ const updateFolderContentVisibility = function(ctx, folderId, visibility, callba

// Get the folder from storage to use for permission checks
FoldersDAO.getFolder(folderId, (err, folder) => {
if (err) {
return callback(err);
}
if (err) return callback(err);

// Ensure the current user can manage the folder
AuthzPermissions.canManage(ctx, folder, err => {
if (err) {
return callback(err);
}
if (err) return callback(err);

// Apply the visibility on all the content items in the folder
_updateFolderContentVisibility(ctx, folder, visibility, callback);
Expand Down Expand Up @@ -371,15 +367,15 @@ const _updateFolderContentVisibility = function(ctx, folder, visibility, callbac
}

contentItems = _.chain(contentItems)
// Remove null content items. This can happen if libraries are in an inconsistent
// state. For example, if an item was deleted from the system but hasn't been removed
// from the libraries, a `null` value would be returned by `getMultipleContentItems`
/**
* Remove null content items. This can happen if libraries are in an inconsistent
* state. For example, if an item was deleted from the system but hasn't been removed
* from the libraries, a `null` value would be returned by `getMultipleContentItems`
*/
.compact()

// Grab those content items that don't have the desired visibility
.filter(content => {
return content.visibility !== visibility;
})
.filter(content => content.visibility !== visibility)
.value();

const failedContent = [];
Expand Down Expand Up @@ -1798,8 +1794,8 @@ const getMessages = function(ctx, folderId, start, limit, callback) {
};

/**
* Recursively add the given list of content items to the given folder. This method is
* destructive to the `contentItems` parameter as it iterates
* Recursively add the given list of content items to the given folder.
* This method is destructive to the `contentItems` parameter as it iterates
*
* @param {Folder} folder The folder to which to add the content items
* @param {Content[]} contentItems The content items to add to the folder
Expand All @@ -1808,18 +1804,14 @@ const getMessages = function(ctx, folderId, start, limit, callback) {
* @api private
*/
const _addContentItemsToAuthzFolder = function(folder, contentItems, callback) {
if (_.isEmpty(contentItems)) {
return callback();
}
if (_.isEmpty(contentItems)) return callback();

const roleChange = {};
roleChange[folder.groupId] = AuthzConstants.role.VIEWER;

const contentItem = contentItems.pop();
AuthzAPI.updateRoles(contentItem.id, roleChange, err => {
if (err) {
return callback(err);
}
if (err) return callback(err);

return _addContentItemsToAuthzFolder(folder, contentItems, callback);
});
Expand Down

0 comments on commit 19dc9da

Please sign in to comment.