All files / src/common permissions.js

17.86% Statements 15/84
2.38% Branches 1/42
5.56% Functions 1/18
20.27% Lines 15/74
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 1731x 1x     1x 1x 1x 1x   1x           1x             1x     1x 1x                                                                                                                   1x 1x 1x       1x                                                                                                                                                                    
import _ from 'lodash'
import { Ability, AbilityBuilder, toMongoQuery } from 'casl/dist/umd'
 
// Define some alias to simplify ability definitions
Ability.addAlias('update', 'patch')
Ability.addAlias('read', ['get', 'find'])
Ability.addAlias('remove', 'delete')
Ability.addAlias('all', ['read', 'create', 'update', 'remove'])
 
export const Roles = {
  member: 0,
  manager: 1,
  owner: 2
}
 
export const RoleNames = [
  'member',
  'manager',
  'owner'
]
 
// Hooks that can be added to customize abilities computation
let hooks = []
 
// Get the unique global symbol to store resource type / context on a resource object
export const RESOURCE_TYPE = 'type'
export const RESOURCE_TYPE_KEY = Symbol.for(RESOURCE_TYPE)
 
export function defineResourceRules (subject, resource, resourceService, can) {
  const role = Roles[resource.permissions]
 
  if (role >= Roles.member) {
    can('read', resourceService, { _id: resource._id })
  }
  if (role >= Roles.manager) {
    can('update', resourceService, { _id: resource._id })
    can(['create', 'remove'], 'authorisations', { resource: resource._id })
  }
  if (role >= Roles.owner) {
    can('remove', resourceService, { _id: resource._id })
  }
}
 
// Hook computing default abilities for a given user
export function defineUserAbilities (subject, can, cannot) {
  // Register
  can('service', 'users')
  can('create', 'users')
 
  if (subject && subject._id) {
    // Read user profiles for authorizing
    can('read', 'users')
    // Update user profile and destroy it
    can(['update', 'remove'], 'users', { _id: subject._id })
    // Access authorisation service, then rights will be granted on a per-resource basis
    can('service', 'authorisations')
    // Access storage service, then rights will be granted on a per-resource basis
    can('service', 'storage')
    // This is for the user avatar
    // take care that the storage service uses 'id' as input but produces _id as output
    can('create', 'storage', { id: 'avatars/' + subject._id.toString() })
    can('create', 'storage', { id: 'avatars/' + subject._id.toString() + '.thumbnail' })
    can(['read', 'remove'], 'storage', { _id: 'avatars/' + subject._id.toString() })
    can(['read', 'remove'], 'storage', { _id: 'avatars/' + subject._id.toString() + '.thumbnail' })
  }
}
 
// Compute abilities for a given user
export function defineAbilities (subject) {
  const { rules, can, cannot } = AbilityBuilder.extract()
 
  // Run registered hooks
  hooks.forEach(hook => hook(subject, can, cannot))
 
  // CASL cannot infer the object type from the object itself so we need
  // to tell it how he can find the object type, i.e. service name.
  return new Ability(rules, { subjectName: resource => {
    if (!resource || typeof resource === 'string') {
      return resource
    }
    return resource[RESOURCE_TYPE_KEY]
  }})
}
 
defineAbilities.registerHook = function (hook) {
  Eif (!hooks.includes(hook)) {
    hooks.push(hook)
  }
}
 
defineAbilities.unregisterHook = function (hook) {
  hooks = hooks.filter(registeredHook => registeredHook !== hook)
}
 
export function hasServiceAbilities (abilities, service) {
  if (!abilities) return false
  // The unique identifier of a service is its path not its name.
  // Indeed we have for instance a 'groups' service in each organisation
  // Take care that in client we have the service path while on server we have the actual object
  const path = typeof service === 'string' ? service : service.getPath()
  return abilities.can('service', path)
}
 
export function hasResourceAbilities (abilities, operation, resourceType, context, resource) {
  if (!abilities) return false
  // Create a shallow copy adding context and type
  let object = Object.assign({}, resource)
  object[RESOURCE_TYPE_KEY] = resourceType
  // Add a virtual context to take it into account for object having no link to it
  if (context) object.context = (typeof context === 'object' ? context._id.toString() : context)
 
  const result = abilities.can(operation, object)
 
  return result
}
 
// Utility function used to remove the virtual context from query
export function removeContext (query) {
  _.forOwn(query, (value, key) => {
    // Process current attributes or recurse
    // Take care to nested fields like 'field._id'
    if (key === 'context') {
      delete query.context
    } else if (Array.isArray(value)) {
      value.forEach(item => removeContext(item))
      // Remove empty objects from array
      _.remove(value, item => _.isEmpty(item))
      // Remove empty arrays from query
      if (_.isEmpty(value)) delete query[key]
    } else if (typeof value === 'object') {
      removeContext(value)
      // Remove empty objects from query
      if (_.isEmpty(value)) delete query[key]
    }
  })
  return query
}
 
// Get the query used to filter the objects according to given abilities
// A null query indicates that access should not be granted
export function getQueryForAbilities (abilities, operation, resourceType) {
  if (!abilities) return null
 
  const rules = abilities.rulesFor(operation, resourceType)
  let query = toMongoQuery(rules)
  // Remove any context to avoid taking it into account because it is not really stored on objects
  return (query ? removeContext(query) : null)
}
 
function buildSubjectsQueryForResource (resourceScope, resourceId, role) {
  let query = { [resourceScope]: { $elemMatch: { _id: resourceId } } }
  if (role) {
    _.set(query[resourceScope], '$elemMatch.permissions', RoleNames[role])
  }
  return query
}
 
export function findSubjectsForResource (subjectService, resourceScope, resourceId, role) {
  // Build the query
  let query = buildSubjectsQueryForResource(resourceScope, resourceId, role)
  // Execute the query
  return subjectService.find({ query })
}
 
export function countSubjectsForResource (subjectService, resourceScope, resourceId, role) {
  // Build the query
  let query = buildSubjectsQueryForResource(resourceScope, resourceId, role)
  // Indicate we'd only like to count
  query.$limit = 0
  // Execute the query
  return subjectService.find({ query })
}