Permissions and CRUD

Modules used to facilitate CRUD operations dynamically with permission assignments

The PermissionsService, PermissibleCrudService and PermissionsServiceSync modules provide functionality to execute CRUD operations and dynamically shape views with respect to the users permissions. These modules can be used in place of the existing HTTP module provided by Uranium and provide similar functionality.

PermissionsService Module

The PermissionsService module is a service that can provide permission data for the active user.

Creating an instance of PermissionsService

The PermissionsService constructor requires an instance of the HTTP module.

Signature

Javascript

constructor(private baseApi: axios.AxiosInstance) 

Usage

JavaScript

const { http, httpAPI } = httpLibs

const permissionService = new PermissionsService(http)

Types

interface ISchemaPermissions {
  read: boolean
  create: boolean
  update: boolean
  delete: boolean

  fields: {
    [fieldName: string]: {
      read: boolean
      create: boolean
      update: boolean
    }
  }
}

interface IFieldPermissionResult {
  [schemaName: string]: ISchemaPermissions
}

enum CrudAction {
  CREATE = 'create',
  UPDATE = 'update',
  READ = 'read',
  DELETE ='delete'
}

getPermissionsFor()

This method will return an object of type Promise<IFieldPermissionResult> with the permission data for the given schemas.

Signature

getPermissionsFor(...schemaNames: string[]): Promise<IFieldPermissionResult>

Usage

permissionService.getPermissionsFor('JobProducts', 'Products')
  .then(permissions => {})

canDoFieldAction

This method will return a Promise<boolean> flagging whether or not the user can perform the given action on the given field for the given schema.

Signature

canDoFieldAction(
  action: 'read' | 'create' | 'update', 
  schema: string, 
  field: string
)

Usage

permissionService.canDoFieldAction(CrudAction.READ, 'JobProducts', 'Qty')
  .then(canDoAction => {})

canDoSchemaAction()

This method will return a Promise<boolean> flagging whether or not the user can perform the given action on the given schema.

Signature

canDoSchemaAction(action: CrudAction, schema: string)

Usage

permissionService.canDoSchemaAction(CrudAction.DELETE, 'JobProducts')
  .then(canDoAction => {})

createCrudService()

This method will bind the instance of the PermissionService into a new instance of the PermissibleCrudService with the given schema and return it.

Signature

createCrudService(schema: string, api: axios.AxiosInstance)

Usage

const { http, httpAPI } = httpLibs

const permissionService = new PermissionsService(http)

// Job Products CRUD service
const jobProductsCrud = permissionService.createCrudService(
  'JobProducts', 
  httpAPI
)

// Products CRUD service
const productsCrud = permissionService.createCrudService(
  'Products',
  httpAPI
)

PermissibleCrudService Module

The PermissibleCrudService provides methods for performing CRUD actions on objects that respect the active users permissions. It can be used as a safety harness for users with dynamic permissions or as a tool for exposing inadequate permissions for a user.

Most methods have a toleratePermissions flag that can be used to safe-guard network calls and return data in a normalised format with only the data that user has permission for. For read requests this means data will return successfully with only the fields that the user has permission for. For write requests this means data will be sanitised before it’s saved and fields that cannot be saved will be filtered out. This flag should be be used with discretion as it will not expose missing permissions (which could lead to data-loss), it will however make it much simpler to develop a CRUD stack that can handle dynamic permissions.

For schema level actions (creating or updating an object); if the action is not allowed by the user, an empty set is returned.

With the toleratePermissions flag turned off, any operations that breach the users permissions will result in a Permission Error being thrown. This can be useful for exposing missing permissions however if unintended will likely result in an unhandled error.

fetch()

The fetch method makes use of the Query module to make requests. To add filters or other parameters to the request that the Query module accepts, use the middleware parameter to return a Query instance with the custom parameters bound. Note that the sourceFieldNames are used for permissions checking and eventually get appended to the query using the withFields operator. This means that middleware should not make use of this operator as those fields will not be guaranteed to be safe in the context of the request.

Signature

fetch<T>(
  middleware: (q: Query) => Query, 
  toleratePermissions = false, 
  sourceFieldNames?: string[]
): Promise<{ records: T[] }>

Usage

function fetchJobProducts(jobIds) {
  if(!jobIds.length) return Promise.resolve({})

  return jobProductsCrud.fetch(
    query => query.filter("JobId IN $1", [jobIds]),
    true
  ).then(res => _.groupBy(res.records, "JobId"))
}

create() & update()

Both update and create methods work with the same principle; they will both sanitise datasets with the toleratePermissions flag set true and remove data that cannot be saved. Update calls will always require a UID property for each record being updated.

Signature

create<T>(
  data: _.Dictionary<T>[],
  toleratePermissions = false
): Promise<{ data: T[] }>

update<T>(
  data: (_.Dictionary<any> & { UID: string })[],
  toleratePermissions = false
): Promise<{ data: T[] }>

Usage

function updateJobProducts(diffs) {
  if (diffs.length === 0) return Promise.resolve([])
  return jobProductsCrud
    .update(diffs, true)
    .then(res => res.data)
}

function createJobProducts(objs) {
  if (objs.length === 0) return Promise.resolve([])
  return jobProductsCrud
    .create(objs, true)
    .then(res => res.data)
}

delete()

The delete method is the only method without the permission safe-guard and will throw if the user is unable to delete the object. Delete operations typically should always be checked before execution.

Signature

delete(UID: string): Promise<{ 
  data: [{ success: boolean, UID: string }] 
}>

Usage

function deleteJobProducts(productDeleteIds) {
  if (productDeleteIds.length == 0) return Promise.resolve({})
  return Promise.all(productDeleteIds.map(id => jobProductsCrud.delete(id)))
}

PermissionsServiceSync Module

The PermissionsServiceSync module implements some methods of the PermissionsService synchronously. It requires a PermissionCache on initialisation.

Creating an instance of PermissionsServiceSync

Signature

constructor(permissionCache: IFieldPermissionResult): PermissionsServiceSync

canDoFieldAction(
  action: 'read' | 'create' | 'update', 
  schema: string, 
  field: string
): boolean

canDoSchemaAction(
  action: CrudAction, 
  schema: string
): boolean

Usage

permissionService = new PermissionsServiceSync(permissions)
        
// Permission Checks
canReadField = (field) => permissionService.canDoFieldAction(
  CrudAction.READ,
  'JobProducts',
  field
)

canUpdateField = (field) => permissionService.canDoFieldAction(
  CrudAction.UPDATE,
  'JobProducts',
  field
)

canCreateField = (field) => permissionService.canDoFieldAction(
  CrudAction.CREATE,
  'JobProducts',
  field
)

canDeleteObject = () => permissionService.canDoSchemaAction(
  CrudAction.DELETE,
  'JobProducts'
)

canUpdateObject = () => permissionService.canDoSchemaAction(
  CrudAction.UPDATE,
  'JobProducts'
)

canCreateObject = () => permissionService.canDoSchemaAction(
  CrudAction.CREATE,
  'JobProducts'
)

Last modified August 2, 2019: Updated fonts and finished guide 1:1 (fe87bc2)