Skip to content

Instantly share code, notes, and snippets.

@ThomasBurleson
Last active June 17, 2024 23:17
Show Gist options
  • Save ThomasBurleson/0ae1e9e30d9397da7110e595d21e18e3 to your computer and use it in GitHub Desktop.
Save ThomasBurleson/0ae1e9e30d9397da7110e595d21e18e3 to your computer and use it in GitHub Desktop.
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
    };
  });
}
@ThomasBurleson
Copy link
Author

ThomasBurleson commented Apr 3, 2018

In fact, with this improved approach a UserSession service might look like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { SharedService } from '../shared/shared.service';

import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { of as observableOf } from 'rxjs/observable/of';
import { merge } from 'rxjs/observable/merge';
import { map, distinctUntilChanged } from 'rxjs/operators';
import * as api from '../../api-interfaces';

@Injectable()
export class UserSession {
  public permissions$ : Observable<api.UserPermissions>;

  constructor(private _http: HttpClient) {
    this.permissions$ = merge( 
      observableOf( PermissionsEnum.NONE ), 
      this._newPermissions$ = new Subject()
    );
    this.loadUserPermissions();
  }

  loadUserPermissions(): Observable<api.UserPermissions> {
    const PERMISSIONS_URL = SharedService.getUsersPermissions();

    this._newPermissions$.next(
      this._http.get<number>(PERMISSIONS_URL)
          .pipe(
            map(perms => {
              return {
                project : new ProjectPermissions(perms),
                content : new ContentPermissions(perms),
                qa      : new QAPermissions(perms)
              };
            })
          )
    );

    return this.permissions$
  }

  select<T>(selector: (permissions: api.UserPermissions) => T): Observable<T> {
    return this.permissions$.pipe(map(selector), distinctUntilChanged());
  }

  private _newPermissions$ : Subject<Observable<api.UserPermissions>>;
}

@ThomasBurleson
Copy link
Author

ThomasBurleson commented Apr 4, 2018

We could even have a PermissionUpdater to easily set a permission flag:

class PermissionUpdater {
  constructor(private permissions:number = 0) {}
  readonly permissions : number;

  /**
   * If this is a flag inspection/lookup only, the val === undefined
   * otherwise set the flag value and then return state
   */
  setPermission(flag:PermissionsEnum, val:boolean = true):PermissionUpdater {
    this.permissions = val ? this.permissions | flag : this.permissions ^ flag;
    return this;
  }
}

And we can use a simple test to validate our custom PermissionUpdater

it("should manage changes to permissions",() => {
    const canView = (1<<0), canWrite = (1<<3), canDelete=(1<<5);

    let myPermissions = canView | canWrite | canDelete,  // 101001
        updater = new PermissionUpdater( myPermissions );

   expect(updater.permission).toBe( myPermissions );

   updater.setPermission(canWrite, false);

   expect( updater.permissions ).toBe( canDelete | canView );
   expect( myPermissions ).toBe( canDelete | canWrite | canView );
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment