ThomasBurleson
4/3/2018 - 10:22 PM

Best Practices: Using Permissions as BitFlags

Best Practices: Using Permissions as BitFlags

💚 Best Practices: Use Permissions as BitFlags

Developers often miss the opportunity to express permissions as a collection of enumerated bitflags; where complex permissions can be easily grouped by context.

Consider the scenario where a user may have 30 or more permission flags.


Improved Version 😄:

See StackBlitz Demo

Managing permissions as bit flags allows each set of permissions to be expressed as a single whole number. The collection of bits can be manipulated as Sets (union, intersect) and flags can be easily toggled.

export enum PermissionsEnum {
  NONE                        = 0,
  ALL                         = 1 << 0,

  DISCLAIMER_ACCESS           = 1 << 1,
  VIEW_DOCUMENTS              = 1 << 2,
  VIEW_PERMISSIONS            = 1 << 3,
  VIEW_REPORTS                = 1 << 4,
  VIEW_USERS                  = 1 << 5,

  NEWFILES_VIEW               = 1 << 6,
  FAVORITE_VIEW               = 1 << 7,
  TRASH_VIEW                  = 1 << 8,
  QUEUE_VIEW                  = 1 << 9,
  TASK_VIEW                   = 1 << 10,

  PROJECT_SEND_EMAIL          = 1 << 11,
  PROJECT_FOLDER_ADD          = 1 << 12,
  PROJECT_FILEROOM_ADD        = 1 << 13,
  PROJECT_FILEROOM_ACTIVATE   = 1 << 14,
  PROJECT_FILEROOM_DEACTIVATE = 1 << 15,

  PROJECT_USER_INVITE         = 1 << 16,
  PROJECT_USER_DEACTIVATE     = 1 << 17,
  PROJECT_USER_ACTIVATE       = 1 << 18,
  PROJECT_USER_CAG_CHANGE     = 1 << 19,
  PROJECT_USER_FAG_CHANGE     = 1 << 20,

  TEAM_QA_QUESTION            = 1 << 21,
  TEAM_QA_ANSWER              = 1 << 22,
  TEAM_QA_ANSWER_VIEW         = 1 << 23,
  TEAM_QA_APPROVER            = 1 << 24,

  CONTENT_UPLOAD              = 1 << 25,
  CONTENT_PERMISSIONS         = 1 << 26,
  CONTENT_RENAME              = 1 << 27,
  CONTENT_DELETE              = 1 << 28,
  CONTENT_MOVE                = 1 << 28,
  CONTENT_COPY                = 1 << 30,
  CONTENT_REPLACE             = 1 << 31,
  CONTENT_SEARCH              = 1 << 32,
  CONTENT_DOWNLOAD            = 1 << 33,
  CONTENT_UPDATE              = 1 << 34,
  CONTENT_DOWNLOAD_BULK       = 1 << 35,
  CONTENT_PERMANENT_DELETE    = 1 << 36,
}

  
/**
 * Abstract base class for common methods
 * 
 * Thx to @Nitin for suggestion!
 */
abstract class AbstractPermissions {
  // Enforce 'permissions' as readOnly
  get permissions() { return this._permissions };
  constructor(private _permissions:number = 0) { }
  protected hasPerms = (perms:number) => !!(this._permissions & (PermissionsEnum.ALL | perms)); 
}

// Approach #1: extending base class

class ProjectPermissions extends AbstractPermissions {
  get canViewDocuments()   { return this.hasPerms(PermissionsEnum.VIEW_DOCUMENTS     );}
  get canViewPermissions() { return this.hasPerms(PermissionsEnum.VIEW_PERMISSIONS   );}
  get canViewReports()     { return this.hasPerms(PermissionsEnum.VIEW_REPORTS       );}
  get canViewUsers()       { return this.hasPerms(PermissionsEnum.VIEW_USERS         );}
  get canViewNewFiles()    { return this.hasPerms(PermissionsEnum.NEWFILES_VIEW      );}
  get canViewFavorites()   { return this.hasPerms(PermissionsEnum.FAVORITE_VIEW      );}
  get canViewTrash()       { return this.hasPerms(PermissionsEnum.TRASH_VIEW         );}
  get canViewQueue()       { return this.hasPerms(PermissionsEnum.QUEUE_VIEW         );}
  get canViewTasks()       { return this.hasPerms(PermissionsEnum.TASK_VIEW          );}  
}

// Approach #2: no inheritance

class ContentPermissions {
  get canUpload()       { return !!(this.permissions & PermissionsEnum.CONTENT_UPLOAD); }
  get canRename()       { return !!(this.permissions & PermissionsEnum.CONTENT_RENAME); }
  get canExpunge()      { return !!(this.permissions & PermissionsEnum.CONTENT_DELETE); }
  get canMove()         { return !!(this.permissions & PermissionsEnum.CONTENT_MOVE); }
  get canCopy()         { return !!(this.permissions & PermissionsEnum.CONTENT_COPY); }
  get canReplace()      { return !!(this.permissions & PermissionsEnum.CONTENT_REPLACE); }
  get canSearch()       { return !!(this.permissions & PermissionsEnum.CONTENT_SEARCH); }
  get canUpdate()       { return !!(this.permissions & PermissionsEnum.CONTENT_UPDATE); }
  get canDownload()     { return !!(this.permissions & PermissionsEnum.CONTENT_DOWNLOAD); }
  get canDownloadBulk() { return !!(this.permissions & PermissionsEnum.CONTENT_DOWNLOAD_BULK); }
  get canDelete()       { return !!(this.permissions & PermissionsEnum.CONTENT_PERMANENT_DELETE); }

  constructor(private permissions:number = 0) {}
}

class QAPermissions {
  get canQuestion()     { return !!(this.permissions & PermissionsEnum.TEAM_QA_QUESTION   ); }
  get canAnswer()       { return !!(this.permissions & PermissionsEnum.TEAM_QA_ANSWER     ); }
  get canAnswerView()   { return !!(this.permissions & PermissionsEnum.TEAM_QA_ANSWER_VIEW); }
  get canApprove()      { return !!(this.permissions & PermissionsEnum.TEAM_QA_APPROVER   ); }

  constructor(private permissions:number = 0) {}
}
// ***************************************************************
// Using a UserSession service 
// ***************************************************************

interface UserPermissions = {
   project ?: number,   
   content ?: number,
   qa      ?: number
}

/**
 * Load current user permissions for 'project' settings only
 */
function loadPermissions(userSession:UserSession): Observable<UserPermissions> {
  returns userSession.select((allUserPerms:UserPermissions) => allUserPerms.project);
}



Original Version 🧐:

Here is the original version which manifests many issues:

  • Flags are maintained as boolean flags; which are hard to manage as a collection


export class UserPermissionModel {
  PROJECT_DOCUMENT_VIEW = false;
  PROJECT_PERMISSION_VIEW = false;
  PROJECT_USER_VIEW = false;
  PROJECT_REPORTS_VIEW = false;
  PROJECT_MANG_VIEW = false;
  PROJECT_NEWFILES_VIEW = false;
  PROJECT_FAVORITE_VIEW = false;
  PROJECT_TRASH_VIEW = false;
  PROJECT_PROCESSINGQUEUE_VIEW = false;
  PROJECT_FOLDER_ADD = false;
  PROJECT_FILEROOM_ADD = false;
  PROJECT_FILEROOM_ACTIVATE = false;
  PROJECT_FILEROOM_DEACTIVATE = false;
  PROJECT_CONTENT_UPLOAD = false;
  PROJECT_CONTENT_PERMISSIONS = false;
  PROJECT_CONTENT_RENAME = false;
  PROJECT_CONTENT_DELETE = false;
  PROJECT_CONTENT_MOVE = false;
  PROJECT_CONTENT_COPY = false;
  PROJECT_CONTENT_REPLACE = false;
  PROJECT_CONTENT_SEARCH = false;
  PROJECT_CONTENT_DOWNLOAD_BULK = false;
  PROJECT_CONTENT_PERMANENT_DELETE = false;
  PROJECT_CONTENT_DOWNLOAD = false;
  PROJECT_CONTENT_UPDATE = false;
  PROJECT_TASK_VIEW = false;
  PROJECT_SEND_EMAIL = false;
  TEAM_QA_QUESTION = false;
  TEAM_QA_ANSWER = false;
  TEAM_QA_ANSWER_VIEW = false;
  TEAM_QA_APPROVER = false;
  PROJECT_DISCLAIMER_ACCESS_GRANTED = false;
}

// *************************************************************************
// Using a UserSession service and manually transforming to desired model.
// *************************************************************************


function loadPermissions(userSession:UserSession): void {
  returns userSession.select((perms:UserPermissionModel) => {
    return {
      canAddFileroom      : perms.PROJECT_FILEROOM_ADD,
      cannotViewAddFolder : perms.PROJECT_FOLDER_ADD,
      cannotRename        : perms.PROJECT_CONTENT_RENAME,
      cannotMove          : perms.PROJECT_CONTENT_MOVE,
      cannotCopy          : perms.PROJECT_CONTENT_COPY,
      cannotReplace       : perms.PROJECT_CONTENT_REPLACE,
      cannotTrash         : perms.PROJECT_TRASH_VIEW,
      cannotAddCategory   : perms.USER_CATEGORY_MANAGE,
      cannotRenameCategory: perms.USER_CATEGORY_MANAGE,
      cannotPermDelete    : perms.PROJECT_CONTENT_PERMANENT_DELETE,
      cannotDownload      : perms.PROJECT_CONTENT_DOWNLOAD,
      cannotViewAddFile   : perms.PROJECT_FOLDER_ADD
    };
  });
}