Repository: parse-community/Parse-SDK-JS
Pull request #1145 (2020-04-10 22:07:57)

Add Parse.CLP Object to control ClassLevelPermissions
The intent of this pull request is to provide the Parse.CLP object which could be use later to be returned on Parse.Schema.
Currently is possible create a new Parse.CLP(schema.classLevelPermissions) and check permissios of user/role/public.



codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=h1) Report
> Merging [#1145](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=desc) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/2081dc25ed76adf72a062b2e76714407b2febff4&el=desc) will **decrease** coverage by `2.23%`.
> The diff coverage is `49.65%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145/graphs/tree.svg?width=650&height=150&src=pr&token=8nCENM0QHj)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1145      +/-   ##
==========================================
- Coverage   92.29%   90.06%   -2.24%     
==========================================
  Files          54       55       +1     
  Lines        5230     5525     +295     
  Branches     1168     1242      +74     
==========================================
+ Hits         4827     4976     +149     
- Misses        403      549     +146     
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseCLP.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145/diff?src=pr&el=tree#diff-c3JjL1BhcnNlQ0xQLmpz) | `49.48% <49.48%> (ø)` | |
| [src/Parse.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145/diff?src=pr&el=tree#diff-c3JjL1BhcnNlLmpz) | `85.71% <100.00%> (+0.12%)` | :arrow_up: |
| [src/ParseQuery.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145/diff?src=pr&el=tree#diff-c3JjL1BhcnNlUXVlcnkuanM=) | `96.20% <0.00%> (ø)` | |
| [src/RESTController.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145/diff?src=pr&el=tree#diff-c3JjL1JFU1RDb250cm9sbGVyLmpz) | `84.51% <0.00%> (+0.51%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=footer). Last update [2081dc2...8353871](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1145?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

JeromeDeLeon: This is very useful IMO. We had to manually do this ourselves. 
brunocosta91: I found this problem to check permissions on CLP so i decide share my solutions, this is based on ACL.
Just to sumarize my approach (similar to basic vs advanced on parse-dashboard):
write access = update && create && delete && addField access
read access = find && count && get access
davimacedo: @brunocosta91 thanks for the PR. It is a great job! I have some questions:
- Would it be possible to also support requiresAuthentication and pointer permissions? @dplewis @acinader am I missing something else?
- Could you provide an example on how it will integrate with the other functions we have here in the SDK? I mean, how would we use the ParseCLP object and setup the CLP of a certain class?
- Could you please increase the test coverage? Only 50% of the ParseCLP.js file is currently covered.
- It would be greate to also have a PR to the JS Docs explaining how to use it.
acinader: @davimacedo looks good to me.
brunocosta91: This weekend I will add more test and try improve PR with suggestions by @davimacedo  
dplewis: @brunocosta91 Any updates on this? It would be awesome to include this in the SDK. Let us know if you need assistance.
brunocosta91: I will try get some time to improve this PR
peril-parse-community[bot]: 
<!--
  0 failure: 
  0 warning: 
  
  1 markdown notices
  DangerID: danger-id-Peril;
-->



Danger run resulted in 1 warning; to find out more, see the [checks page](https://github.com/parse-community/Parse-SDK-JS/runs/1665022490).
<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a>
</p>

dplewis: @brunocosta91 I can finish this PR up for you. I just need write access to your fork.
dplewis: @davimacedo @mtrezza I believe this is ready for review.

Patch:
diff --git a/src/Parse.js b/src/Parse.js
index a627da8..aeef1f2 100644
--- a/src/Parse.js
+++ b/src/Parse.js
@@ -193,6 +193,7 @@ Parse.ACL = require('./ParseACL').default;
 Parse.Analytics = require('./Analytics');
 Parse.AnonymousUtils = require('./AnonymousUtils').default;
 Parse.Cloud = require('./Cloud');
+Parse.CLP = require('./ParseCLP').default;
 Parse.CoreManager = require('./CoreManager');
 Parse.Config = require('./ParseConfig').default;
 Parse.Error = require('./ParseError').default;
diff --git a/src/ParseCLP.js b/src/ParseCLP.js
new file mode 100644
index 0000000..1791328
--- /dev/null
+++ b/src/ParseCLP.js
@@ -0,0 +1,612 @@
+/**
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @flow
+ */
+
+import ParseRole from './ParseRole';
+import ParseUser from './ParseUser';
+
+type Entity = Entity;
+type UsersMap = { [userId: string]: boolean | any };
+export type PermissionsMap = { [permission: string]: UsersMap };
+
+const PUBLIC_KEY = '*';
+
+const VALID_PERMISSIONS: Map<string, UsersMap> = new Map<string, UsersMap>();
+VALID_PERMISSIONS.set('get', {});
+VALID_PERMISSIONS.set('find', {});
+VALID_PERMISSIONS.set('count', {});
+VALID_PERMISSIONS.set('create', {});
+VALID_PERMISSIONS.set('update', {});
+VALID_PERMISSIONS.set('delete', {});
+VALID_PERMISSIONS.set('addField', {});
+
+const VALID_PERMISSIONS_EXTENDED: Map<string, UsersMap> = new Map<string, UsersMap>();
+VALID_PERMISSIONS_EXTENDED.set('protectedFields', {});
+
+/**
+ * Creates a new CLP.
+ * If no argument is given, the CLP has no permissions for anyone.
+ * If the argument is a Parse.User or Parse.Role, the CLP will have read and write
+ *   permission for only that user or role.
+ * If the argument is any other JSON object, that object will be interpretted
+ *   as a serialized CLP created with toJSON().
+ *
+ * <p>A CLP, or Class Level Permissions can be added to any
+ * <code>Parse.Schema</code> to restrict access to only a subset of users
+ * of your application.</p>
+ *
+ * <p>
+ * For get/count/find/create/update/delete/addField using the following functions:
+ *
+ * Entity is type Parse.User or Parse.Role or string
+ * Role is type Parse.Role or Name of Parse.Role
+ *
+ * getGetRequiresAuthentication()
+ * setGetRequiresAuthentication(allowed: boolean)
+ * getGetPointerFields()
+ * setGetPointerFields(pointerFields: string[])
+ * getGetAccess(entity: Entity)
+ * setGetAccess(entity: Entity, allowed: boolean)
+ * getPublicGetAccess()
+ * setPublicGetAccess(allowed: boolean)
+ * getRoleGetAccess(role: Role)
+ * setRoleGetAccess(role: Role, allowed: boolean)
+ * getFindRequiresAuthentication()
+ * setFindRequiresAuthentication(allowed: boolean)
+ * getFindPointerFields()
+ * setFindPointerFields(pointerFields: string[])
+ * getFindAccess(entity: Entity)
+ * setFindAccess(entity: Entity, allowed: boolean)
+ * getPublicFindAccess()
+ * setPublicFindAccess(allowed: boolean)
+ * getRoleFindAccess(role: Role)
+ * setRoleFindAccess(role: Role, allowed: boolean)
+ * getCountRequiresAuthentication()
+ * setCountRequiresAuthentication(allowed: boolean)
+ * getCountPointerFields()
+ * setCountPointerFields(pointerFields: string[])
+ * getCountAccess(entity: Entity)
+ * setCountAccess(entity: Entity, allowed: boolean)
+ * getPublicCountAccess()
+ * setPublicCountAccess(allowed: boolean)
+ * getRoleCountAccess(role: Role)
+ * setRoleCountAccess(role: Role, allowed: boolean)
+ * getCreateRequiresAuthentication()
+ * setCreateRequiresAuthentication(allowed: boolean)
+ * getCreatePointerFields()
+ * setCreatePointerFields(pointerFields: string[])
+ * getCreateAccess(entity: Entity)
+ * setCreateAccess(entity: Entity, allowed: boolean)
+ * getPublicCreateAccess()
+ * setPublicCreateAccess(allowed: Boolean)
+ * getRoleCreateAccess(role: Role)
+ * setRoleCreateAccess(role: Role, allowed: boolean)
+ * getUpdateRequiresAuthentication()
+ * setUpdateRequiresAuthentication(allowed: boolean)
+ * getUpdatePointerFields()
+ * setUpdatePointerFields(pointerFields: string[])
+ * getUpdateAccess(entity: Entity)
+ * setUpdateAccess(entity: Entity, allowed: boolean)
+ * getPublicUpdateAccess()
+ * setPublicUpdateAccess(allowed: boolean)
+ * getRoleUpdateAccess(role: Role)
+ * setRoleUpdateAccess(role: Role, allowed: boolean)
+ * getDeleteRequiresAuthentication()
+ * setDeleteRequiresAuthentication(allowed: boolean)
+ * getDeletePointerFields()
+ * setDeletePointerFields(pointerFields: string[])
+ * getDeleteAccess(entity: Entity)
+ * setDeleteAccess(entity: Entity, allowed: boolean)
+ * getPublicDeleteAccess()
+ * setPublicDeleteAccess(allowed: boolean)
+ * getRoleDeleteAccess(role: Role)
+ * setRoleDeleteAccess(role: Role, allowed: boolean)
+ * getAddFieldRequiresAuthentication()
+ * setAddFieldRequiresAuthentication(allowed: boolean)
+ * getAddFieldPointerFields()
+ * setAddFieldPointerFields(pointerFields: string[])
+ * getAddFieldAccess(entity: Entity)
+ * setAddFieldAccess(entity: Entity, allowed: boolean)
+ * getPublicAddFieldAccess()
+ * setPublicAddFieldAccess(allowed: boolean)
+ * getRoleAddFieldAccess(role: Role)
+ * setRoleAddFieldAccess(role: Role, allowed: boolean)
+ * </p>
+ *
+ * @alias Parse.CLP
+ */
+class ParseCLP {
+  permissionsMap: PermissionsMap;
+
+  /**
+   * @param {(Parse.User | Parse.Role | object)} userId The user to initialize the CLP for
+   */
+  constructor(userId: ParseUser | ParseRole | PermissionsMap) {
+    this.permissionsMap = {};
+    // Initialize permissions Map with default permissions
+    for (const [operation, group] of VALID_PERMISSIONS.entries()) {
+      this.permissionsMap[operation] = Object.assign({}, group);
+      const action = operation.charAt(0).toUpperCase() + operation.slice(1);
+
+      this[`get${action}RequiresAuthentication`] = function () {
+        return this._getAccess(operation, 'requiresAuthentication');
+      };
+      this[`set${action}RequiresAuthentication`] = function (allowed) {
+        this._setAccess(operation, 'requiresAuthentication', allowed);
+      };
+
+      this[`get${action}PointerFields`] = function () {
+        return this._getAccess(operation, 'pointerFields', false);
+      };
+      this[`set${action}PointerFields`] = function (pointerFields) {
+        this._setArrayAccess(operation, 'pointerFields', pointerFields);
+      };
+
+      this[`get${action}Access`] = function (entity) {
+        return this._getAccess(operation, entity);
+      };
+      this[`set${action}Access`] = function (entity, allowed) {
+        this._setAccess(operation, entity, allowed);
+      };
+
+      this[`getPublic${action}Access`] = function () {
+        return this[`get${action}Access`](PUBLIC_KEY);
+      };
+      this[`setPublic${action}Access`] = function (allowed) {
+        this[`set${action}Access`](PUBLIC_KEY, allowed);
+      };
+
+      this[`getRole${action}Access`] = function (role) {
+        return this[`get${action}Access`](this._getRoleName(role));
+      };
+      this[`setRole${action}Access`] = function (role, allowed) {
+        this[`set${action}Access`](this._getRoleName(role), allowed);
+      };
+    }
+    // Initialize permissions Map with default extended permissions
+    for (const [operation, group] of VALID_PERMISSIONS_EXTENDED.entries()) {
+      this.permissionsMap[operation] = Object.assign({}, group);
+    }
+    if (userId && typeof userId === 'object') {
+      if (userId instanceof ParseUser) {
+        this.setReadAccess(userId, true);
+        this.setWriteAccess(userId, true);
+      } else if (userId instanceof ParseRole) {
+        this.setRoleReadAccess(userId, true);
+        this.setRoleWriteAccess(userId, true);
+      } else {
+        for (const permission in userId) {
+          const users = userId[permission];
+          const isValidPermission = !!VALID_PERMISSIONS.get(permission);
+          const isValidPermissionExtended = !!VALID_PERMISSIONS_EXTENDED.get(permission);
+          const isValidGroupPermission = ['readUserFields', 'writeUserFields'].includes(permission);
+          if (
+            typeof permission !== 'string' ||
+            !(isValidPermission || isValidPermissionExtended || isValidGroupPermission)
+          ) {
+            throw new TypeError('Tried to create an CLP with an invalid permission type.');
+          }
+          if (isValidGroupPermission) {
+            if (users.every(pointer => typeof pointer === 'string')) {
+              this.permissionsMap[permission] = users;
+              continue;
+            } else {
+              throw new TypeError('Tried to create an CLP with an invalid permission value.');
+            }
+          }
+          for (const user in users) {
+            const allowed = users[user];
+            if (
+              typeof allowed !== 'boolean' &&
+              !isValidPermissionExtended &&
+              user !== 'pointerFields'
+            ) {
+              throw new TypeError('Tried to create an CLP with an invalid permission value.');
+            }
+            this.permissionsMap[permission][user] = allowed;
+          }
+        }
+      }
+    } else if (typeof userId === 'function') {
+      throw new TypeError('ParseCLP constructed with a function. Did you forget ()?');
+    }
+  }
+
+  /**
+   * Returns a JSON-encoded version of the CLP.
+   *
+   * @returns {object}
+   */
+  toJSON(): PermissionsMap {
+    return { ...this.permissionsMap };
+  }
+
+  /**
+   * Returns whether this CLP is equal to another object
+   *
+   * @param other The other object to compare to
+   * @returns {boolean}
+   */
+  equals(other: ParseCLP): boolean {
+    if (!(other instanceof ParseCLP)) {
+      return false;
+    }
+    const permissions = Object.keys(this.permissionsMap);
+    const otherPermissions = Object.keys(other.permissionsMap);
+    if (permissions.length !== otherPermissions.length) {
+      return false;
+    }
+    for (const permission in this.permissionsMap) {
+      if (!other.permissionsMap[permission]) {
+        return false;
+      }
+
+      const users = Object.keys(this.permissionsMap[permission]);
+      const otherUsers = Object.keys(other.permissionsMap[permission]);
+      if (users.length !== otherUsers.length) {
+        return false;
+      }
+
+      for (const user in this.permissionsMap[permission]) {
+        if (!other.permissionsMap[permission][user]) {
+          return false;
+        }
+        if (this.permissionsMap[permission][user] !== other.permissionsMap[permission][user]) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  _getRoleName(role: ParseRole | string): string {
+    let name = role;
+    if (role instanceof ParseRole) {
+      // Normalize to the String name
+      name = role.getName();
+    }
+    if (typeof name !== 'string') {
+      throw new TypeError('role must be a Parse.Role or a String');
+    }
+    return `role:${name}`;
+  }
+
+  _parseEntity(entity: Entity) {
+    let userId = entity;
+    if (userId instanceof ParseUser) {
+      userId = userId.id;
+      if (!userId) {
+        throw new Error('Cannot get access for a Parse.User without an id.');
+      }
+    } else if (userId instanceof ParseRole) {
+      userId = this._getRoleName(userId);
+    }
+    if (typeof userId !== 'string') {
+      throw new TypeError('userId must be a string.');
+    }
+    return userId;
+  }
+
+  _setAccess(permission: string, userId: Entity, allowed: boolean) {
+    userId = this._parseEntity(userId);
+
+    if (typeof allowed !== 'boolean') {
+      throw new TypeError('allowed must be either true or false.');
+    }
+    const permissions = this.permissionsMap[permission][userId];
+    if (!permissions) {
+      if (!allowed) {
+        // The user already doesn't have this permission, so no action is needed
+        return;
+      } else {
+        this.permissionsMap[permission][userId] = {};
+      }
+    }
+
+    if (allowed) {
+      this.permissionsMap[permission][userId] = true;
+    } else {
+      delete this.permissionsMap[permission][userId];
+    }
+  }
+
+  _getAccess(permission: string, userId: Entity, returnBoolean = true): boolean | string[] {
+    userId = this._parseEntity(userId);
+
+    const permissions = this.permissionsMap[permission][userId];
+    if (returnBoolean) {
+      if (!permissions) {
+        return false;
+      }
+      return !!this.permissionsMap[permission][userId];
+    }
+    return permissions;
+  }
+
+  _setArrayAccess(permission: string, userId: Entity, fields: string) {
+    userId = this._parseEntity(userId);
+
+    const permissions = this.permissionsMap[permission][userId];
+    if (!permissions) {
+      this.permissionsMap[permission][userId] = [];
+    }
+    if (!fields || (Array.isArray(fields) && fields.length === 0)) {
+      delete this.permissionsMap[permission][userId];
+    } else if (Array.isArray(fields) && fields.every(field => typeof field === 'string')) {
+      this.permissionsMap[permission][userId] = fields;
+    } else {
+      throw new TypeError('fields must be an array of strings or undefined.');
+    }
+  }
+
+  _setGroupPointerPermission(operation: string, pointerFields: string[]) {
+    const fields = this.permissionsMap[operation];
+    if (!fields) {
+      this.permissionsMap[operation] = [];
+    }
+    if (!pointerFields || (Array.isArray(pointerFields) && pointerFields.length === 0)) {
+      delete this.permissionsMap[operation];
+    } else if (
+      Array.isArray(pointerFields) &&
+      pointerFields.every(field => typeof field === 'string')
+    ) {
+      this.permissionsMap[operation] = pointerFields;
+    } else {
+      throw new TypeError(`${operation}.pointerFields must be an array of strings or undefined.`);
+    }
+  }
+
+  _getGroupPointerPermissions(operation: string): string[] {
+    return this.permissionsMap[operation];
+  }
+
+  /**
+   * Sets user pointer fields to allow permission for get/count/find operations.
+   *
+   * @param {string[]} pointerFields User pointer fields
+   */
+  setReadUserFields(pointerFields: string[]) {
+    this._setGroupPointerPermission('readUserFields', pointerFields);
+  }
+
+  /**
+   * @returns {string[]} User pointer fields
+   */
+  getReadUserFields(): string[] {
+    return this._getGroupPointerPermissions('readUserFields');
+  }
+
+  /**
+   * Sets user pointer fields to allow permission for create/delete/update/addField operations
+   *
+   * @param {string[]} pointerFields User pointer fields
+   */
+  setWriteUserFields(pointerFields: string[]) {
+    this._setGroupPointerPermission('writeUserFields', pointerFields);
+  }
+
+  /**
+   * @returns {string[]} User pointer fields
+   */
+  getWriteUserFields(): string[] {
+    return this._getGroupPointerPermissions('writeUserFields');
+  }
+
+  /**
+   * Sets whether the given user is allowed to retrieve fields from this class.
+   *
+   * @param userId An instance of Parse.User or its objectId.
+   * @param {string[]} fields fields to be protected
+   */
+  setProtectedFields(userId: Entity, fields: string[]) {
+    this._setArrayAccess('protectedFields', userId, fields);
+  }
+
+  /**
+   * Returns array of fields are accessable to this user.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role.
+   * @returns {string[]}
+   */
+  getProtectedFields(userId: Entity): string[] {
+    return this._getAccess('protectedFields', userId, false);
+  }
+
+  /**
+   * Sets whether the given user is allowed to read from this class.
+   *
+   * @param userId An instance of Parse.User or its objectId.
+   * @param {boolean} allowed whether that user should have read access.
+   */
+  setReadAccess(userId: Entity, allowed: boolean) {
+    this._setAccess('find', userId, allowed);
+    this._setAccess('get', userId, allowed);
+    this._setAccess('count', userId, allowed);
+  }
+
+  /**
+   * Get whether the given user id is *explicitly* allowed to read from this class.
+   * Even if this returns false, the user may still be able to access it if
+   * getPublicReadAccess returns true or a role that the user belongs to has
+   * write access.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role.
+   * @returns {boolean}
+   */
+  getReadAccess(userId: Entity): boolean {
+    return (
+      this._getAccess('find', userId) &&
+      this._getAccess('get', userId) &&
+      this._getAccess('count', userId)
+    );
+  }
+
+  /**
+   * Sets whether the given user id is allowed to write to this class.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role..
+   * @param {boolean} allowed Whether that user should have write access.
+   */
+  setWriteAccess(userId: Entity, allowed: boolean) {
+    this._setAccess('create', userId, allowed);
+    this._setAccess('update', userId, allowed);
+    this._setAccess('delete', userId, allowed);
+    this._setAccess('addField', userId, allowed);
+  }
+
+  /**
+   * Gets whether the given user id is *explicitly* allowed to write to this class.
+   * Even if this returns false, the user may still be able to write it if
+   * getPublicWriteAccess returns true or a role that the user belongs to has
+   * write access.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role.
+   * @returns {boolean}
+   */
+  getWriteAccess(userId: Entity): boolean {
+    return (
+      this._getAccess('create', userId) &&
+      this._getAccess('update', userId) &&
+      this._getAccess('delete', userId) &&
+      this._getAccess('addField', userId)
+    );
+  }
+
+  /**
+   * Sets whether the public is allowed to read from this class.
+   *
+   * @param {boolean} allowed
+   */
+  setPublicReadAccess(allowed: boolean) {
+    this.setReadAccess(PUBLIC_KEY, allowed);
+  }
+
+  /**
+   * Gets whether the public is allowed to read from this class.
+   *
+   * @returns {boolean}
+   */
+  getPublicReadAccess(): boolean {
+    return this.getReadAccess(PUBLIC_KEY);
+  }
+
+  /**
+   * Sets whether the public is allowed to write to this class.
+   *
+   * @param {boolean} allowed
+   */
+  setPublicWriteAccess(allowed: boolean) {
+    this.setWriteAccess(PUBLIC_KEY, allowed);
+  }
+
+  /**
+   * Gets whether the public is allowed to write to this class.
+   *
+   * @returns {boolean}
+   */
+  getPublicWriteAccess(): boolean {
+    return this.getWriteAccess(PUBLIC_KEY);
+  }
+
+  /**
+   * Sets whether the public is allowed to protect fields in this class.
+   *
+   * @param {string[]} fields
+   */
+  setPublicProtectedFields(fields: string[]) {
+    this.setProtectedFields(PUBLIC_KEY, fields);
+  }
+
+  /**
+   * Gets whether the public is allowed to read fields from this class.
+   *
+   * @returns {string[]}
+   */
+  getPublicProtectedFields(): string[] {
+    return this.getProtectedFields(PUBLIC_KEY);
+  }
+
+  /**
+   * Gets whether users belonging to the given role are allowed
+   * to read from this class. Even if this returns false, the role may
+   * still be able to write it if a parent role has read access.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @returns {boolean} true if the role has read access. false otherwise.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  getRoleReadAccess(role: ParseRole | string): boolean {
+    return this.getReadAccess(this._getRoleName(role));
+  }
+
+  /**
+   * Gets whether users belonging to the given role are allowed
+   * to write to this user. Even if this returns false, the role may
+   * still be able to write it if a parent role has write access.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @returns {boolean} true if the role has write access. false otherwise.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  getRoleWriteAccess(role: ParseRole | string): boolean {
+    return this.getWriteAccess(this._getRoleName(role));
+  }
+
+  /**
+   * Sets whether users belonging to the given role are allowed
+   * to read from this class.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @param {boolean} allowed Whether the given role can read this object.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  setRoleReadAccess(role: ParseRole | string, allowed: boolean) {
+    this.setReadAccess(this._getRoleName(role), allowed);
+  }
+
+  /**
+   * Sets whether users belonging to the given role are allowed
+   * to write to this class.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @param {boolean} allowed Whether the given role can write this object.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  setRoleWriteAccess(role: ParseRole | string, allowed: boolean) {
+    this.setWriteAccess(this._getRoleName(role), allowed);
+  }
+
+  /**
+   * Gets whether users belonging to the given role are allowed
+   * to count to this user. Even if this returns false, the role may
+   * still be able to count it if a parent role has count access.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @returns {string[]}
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  getRoleProtectedFields(role: ParseRole | string): string[] {
+    return this.getProtectedFields(this._getRoleName(role));
+  }
+
+  /**
+   * Sets whether users belonging to the given role are allowed
+   * to set access field in this class.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @param {string[]} fields Fields to be protected by Role.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  setRoleProtectedFields(role: ParseRole | string, fields: string[]) {
+    this.setProtectedFields(this._getRoleName(role), fields);
+  }
+}
+
+export default ParseCLP;
diff --git a/src/ParseSchema.js b/src/ParseSchema.js
index 6d770e9..c7fa844 100644
--- a/src/ParseSchema.js
+++ b/src/ParseSchema.js
@@ -11,6 +11,9 @@
 
 import CoreManager from './CoreManager';
 import ParseObject from './ParseObject';
+import ParseCLP from './ParseCLP';
+
+import type { PermissionsMap } from './ParseCLP';
 
 const FIELD_TYPES = [
   'String',
@@ -190,11 +193,15 @@ class ParseSchema {
    * Sets Class Level Permissions when creating / updating a Schema.
    * EXERCISE CAUTION, running this may override CLP for this schema and cannot be reversed
    *
-   * @param {object} clp Class Level Permissions
+   * @param {object | Parse.CLP} clp Class Level Permissions
    * @returns {Parse.Schema} Returns the schema, so you can chain this call.
    */
-  setCLP(clp: { [key: string]: mixed }) {
-    this._clp = clp;
+  setCLP(clp: PermissionsMap | ParseCLP) {
+    if (clp instanceof ParseCLP) {
+      this._clp = clp.toJSON();
+    } else {
+      this._clp = clp;
+    }
     return this;
   }
 


Test patch:
diff --git a/integration/test/ParseSchemaTest.js b/integration/test/ParseSchemaTest.js
index dfa48a2..9837332 100644
--- a/integration/test/ParseSchemaTest.js
+++ b/integration/test/ParseSchemaTest.js
@@ -264,7 +264,7 @@ describe('Schema', () => {
     assert.deepEqual(json, expected);
   });
 
-  it('save class level permissions', async () => {
+  it('save class level permissions json', async () => {
     const clp = {
       get: { requiresAuthentication: true },
       find: {},
@@ -281,6 +281,25 @@ describe('Schema', () => {
     assert.deepEqual(schema.classLevelPermissions, clp);
   });
 
+  it('save class level permissions object', async () => {
+    const permissionsMap = {
+      get: { '*': true },
+      find: {},
+      count: {},
+      create: { '*': true },
+      update: { '*': true },
+      delete: {},
+      addField: {},
+      protectedFields: {},
+    };
+    const clp = new Parse.CLP(permissionsMap);
+    const testSchema = new Parse.Schema('SchemaTest');
+    testSchema.setCLP(clp);
+    const schema = await testSchema.save();
+    assert.deepEqual(schema.classLevelPermissions, permissionsMap);
+    assert.deepEqual(schema.classLevelPermissions, clp.toJSON());
+  });
+
   it('update class level permissions', async () => {
     const clp = {
       get: { requiresAuthentication: true },
diff --git a/src/__tests__/ParseCLP-test.js b/src/__tests__/ParseCLP-test.js
new file mode 100644
index 0000000..44a28eb
--- /dev/null
+++ b/src/__tests__/ParseCLP-test.js
@@ -0,0 +1,680 @@
+/**
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+jest.dontMock('../ParseCLP');
+
+const mockRole = function (name) {
+  this.name = name;
+};
+
+mockRole.prototype.getName = function () {
+  return this.name;
+};
+
+jest.setMock('../ParseRole', mockRole);
+
+const ParseCLP = require('../ParseCLP').default;
+const ParseUser = require('../ParseUser').default;
+const ParseRole = require('../ParseRole');
+
+function generateReadCLP(key) {
+  return {
+    get: { [key]: true },
+    find: { [key]: true },
+    count: { [key]: true },
+    create: {},
+    update: {},
+    delete: {},
+    addField: {},
+    protectedFields: {},
+  };
+}
+
+function generateWriteCLP(key) {
+  return {
+    get: {},
+    find: {},
+    count: {},
+    create: { [key]: true },
+    update: { [key]: true },
+    delete: { [key]: true },
+    addField: { [key]: true },
+    protectedFields: {},
+  };
+}
+
+const DEFAULT_CLP = {
+  get: {},
+  find: {},
+  count: {},
+  create: {},
+  update: {},
+  delete: {},
+  addField: {},
+  protectedFields: {},
+};
+
+const READ_CLP = generateReadCLP('uid');
+const WRITE_CLP = generateWriteCLP('uid');
+const PUBLIC_READ_CLP = generateReadCLP('*');
+const PUBLIC_WRITE_CLP = generateWriteCLP('*');
+const ROLE_READ_CLP = generateReadCLP('role:admin');
+const ROLE_WRITE_CLP = generateWriteCLP('role:admin');
+
+describe('ParseCLP', () => {
+  it('can be constructed with no arguments', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+  });
+
+  it('can be constructed with a ParseUser', () => {
+    const u = new ParseUser();
+    u.id = 'uid';
+    const a = new ParseCLP(u);
+    expect(a.permissionsMap).toEqual({
+      get: { uid: true },
+      find: { uid: true },
+      count: { uid: true },
+      create: { uid: true },
+      update: { uid: true },
+      delete: { uid: true },
+      addField: { uid: true },
+      protectedFields: {},
+    });
+  });
+
+  it('can be constructed with a map of user IDs', () => {
+    const userCLP = {
+      get: { uid: true },
+      find: { uid: true },
+      count: { uid: true },
+      create: { uid: true },
+      update: { uid: true },
+      delete: { uid: true },
+      addField: { uid: true },
+      protectedFields: {},
+    };
+    const a = new ParseCLP(userCLP);
+    expect(a.permissionsMap).toEqual(userCLP);
+  });
+
+  it('throws when constructed with an invalid permissions map', () => {
+    let err = function () {
+      new ParseCLP({ foobar: { foo: true, bar: false } });
+    };
+    expect(err).toThrow('Tried to create an CLP with an invalid permission type.');
+    err = function () {
+      new ParseCLP({ get: { uid: 12 } });
+    };
+    expect(err).toThrow('Tried to create an CLP with an invalid permission value.');
+  });
+
+  it('throws a helpful error when constructed with a function', () => {
+    expect(function () {
+      new ParseCLP(function () {});
+    }).toThrow('ParseCLP constructed with a function. Did you forget ()?');
+  });
+
+  it('throws when setting an invalid input', () => {
+    const a = new ParseCLP();
+    expect(a.setReadAccess.bind(a, 12, true)).toThrow('userId must be a string.');
+  });
+
+  it('throws when getting an invalid user id', () => {
+    const u = new ParseUser();
+    const a = new ParseCLP();
+    expect(a.getReadAccess.bind(a, u)).toThrow('Cannot get access for a Parse.User without an id.');
+  });
+
+  it('throws when setting an invalid access', () => {
+    const a = new ParseCLP();
+    expect(a.setReadAccess.bind(a, 'uid', 12)).toThrow('allowed must be either true or false.');
+  });
+
+  it('throws when setting an invalid role', () => {
+    const a = new ParseCLP();
+    expect(a.setRoleReadAccess.bind(a, 12, true)).toThrow('role must be a Parse.Role or a String');
+    expect(a.setRoleWriteAccess.bind(a, 12, true)).toThrow('role must be a Parse.Role or a String');
+  });
+
+  it('throws when setting an invalid protectedFields', () => {
+    const a = new ParseCLP();
+    const u = new ParseUser();
+    const r = new ParseRole();
+    expect(a.setProtectedFields.bind(a, 12)).toThrow('userId must be a string.');
+    expect(a.setProtectedFields.bind(a, u)).toThrow(
+      'Cannot get access for a Parse.User without an id.'
+    );
+    expect(a.setProtectedFields.bind(a, r)).toThrow('role must be a Parse.Role or a String');
+    expect(a.setProtectedFields.bind(a, new ParseRole('admin'), 'not_field_array')).toThrow(
+      'fields must be an array of strings or undefined.'
+    );
+  });
+
+  it('throws when setting an invalid group pointer permissions', () => {
+    const a = new ParseCLP();
+    expect(a.setReadUserFields.bind(a, 12)).toThrow(
+      'readUserFields.pointerFields must be an array of strings or undefined.'
+    );
+    expect(a.setWriteUserFields.bind(a, 12)).toThrow(
+      'writeUserFields.pointerFields must be an array of strings or undefined.'
+    );
+
+    const clp = {
+      get: {},
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+      readUserFields: [1234],
+      writeUserFields: ['owner'],
+    };
+    expect(function () {
+      new ParseCLP(clp);
+    }).toThrow('Tried to create an CLP with an invalid permission value.');
+  });
+
+  it('can be rendered to JSON format', () => {
+    const permissionsMap = {
+      get: { uid: true },
+      find: { uid: true },
+      count: { uid: true },
+      create: { admin: true },
+      update: { admin: true },
+      delete: { admin: true },
+      addField: { admin: true },
+      protectedFields: { admin: ['objectId', 'field1'] },
+    };
+    const a = new ParseCLP(permissionsMap);
+    expect(a.toJSON()).toEqual(permissionsMap);
+  });
+
+  it('can set protectedFields', () => {
+    const a = new ParseCLP();
+    const r = new ParseRole('admin');
+
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+
+    a.setProtectedFields('uid', ['admin']);
+    a.setPublicProtectedFields(['foo']);
+    a.setRoleProtectedFields(r, ['bar']);
+
+    expect(a.toJSON()).toEqual({
+      get: {},
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {
+        uid: ['admin'],
+        '*': ['foo'],
+        'role:admin': ['bar'],
+      },
+    });
+    expect(a.getProtectedFields('uid')).toEqual(['admin']);
+    expect(a.getPublicProtectedFields()).toEqual(['foo']);
+    expect(a.getRoleProtectedFields(r)).toEqual(['bar']);
+
+    a.setProtectedFields('uid', undefined);
+    a.setPublicProtectedFields(undefined);
+    a.setRoleProtectedFields(r, undefined);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can get and set requiresAuthentication', () => {
+    const a = new ParseCLP();
+    a.setGetRequiresAuthentication(true);
+    expect(a.toJSON()).toEqual({
+      get: { requiresAuthentication: true },
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+    });
+    expect(a.getGetRequiresAuthentication()).toBe(true);
+    a.setGetRequiresAuthentication(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can get and set pointerFields', () => {
+    const testCLP = {
+      get: { pointerFields: ['owner'] },
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+    };
+    const pointerCLP = new ParseCLP(testCLP);
+    expect(pointerCLP.toJSON()).toEqual(testCLP);
+
+    const a = new ParseCLP();
+    a.setGetPointerFields(['owner']);
+    expect(a.toJSON()).toEqual(testCLP);
+    expect(a.getGetPointerFields()).toEqual(['owner']);
+
+    a.setGetPointerFields(undefined);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getGetPointerFields()).toEqual(undefined);
+  });
+
+  it('can get and set group pointer permissions', () => {
+    const testCLP = {
+      get: {},
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+      readUserFields: ['parent'],
+      writeUserFields: ['owner'],
+    };
+    const groupCLP = new ParseCLP(testCLP);
+    expect(groupCLP.toJSON()).toEqual(testCLP);
+
+    const a = new ParseCLP();
+    a.setWriteUserFields(['owner']);
+    a.setReadUserFields(['parent']);
+    expect(a.toJSON()).toEqual(testCLP);
+    expect(a.getWriteUserFields()).toEqual(['owner']);
+    expect(a.getReadUserFields()).toEqual(['parent']);
+
+    a.setWriteUserFields(undefined);
+    a.setReadUserFields(undefined);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can set read access', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+
+    a.setReadAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess('uid')).toBe(false);
+
+    a.setReadAccess('uid', true);
+    expect(a.permissionsMap).toEqual(READ_CLP);
+    expect(a.getReadAccess('uid')).toBe(true);
+
+    a.setReadAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess('uid')).toBe(false);
+
+    const u = new ParseUser();
+    u.id = 'uid';
+
+    a.setReadAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess(u)).toBe(false);
+
+    a.setReadAccess(u, true);
+    expect(a.permissionsMap).toEqual(READ_CLP);
+    expect(a.getReadAccess(u)).toBe(true);
+
+    a.setReadAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess(u)).toBe(false);
+  });
+
+  it('can get read access', () => {
+    const a = new ParseCLP();
+    a.setGetAccess('uid', true);
+    a.setFindAccess('uid', true);
+    a.setCountAccess('uid', true);
+    expect(a.toJSON()).toEqual(READ_CLP);
+    expect(a.getReadAccess('uid')).toBe(true);
+    expect(a.getGetAccess('uid')).toBe(true);
+    expect(a.getFindAccess('uid')).toBe(true);
+    expect(a.getCountAccess('uid')).toBe(true);
+    a.setGetAccess('uid', false);
+    a.setFindAccess('uid', false);
+    a.setCountAccess('uid', false);
+    expect(a.getReadAccess('uid')).toBe(false);
+    expect(a.getGetAccess('uid')).toBe(false);
+    expect(a.getFindAccess('uid')).toBe(false);
+    expect(a.getCountAccess('uid')).toBe(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can set write access', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+
+    a.setWriteAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess('uid')).toBe(false);
+
+    a.setWriteAccess('uid', true);
+    expect(a.permissionsMap).toEqual(WRITE_CLP);
+    expect(a.getWriteAccess('uid')).toBe(true);
+
+    a.setWriteAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess('uid')).toBe(false);
+
+    const u = new ParseUser();
+    u.id = 'uid';
+
+    a.setWriteAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess(u)).toBe(false);
+
+    a.setWriteAccess(u, true);
+    expect(a.permissionsMap).toEqual(WRITE_CLP);
+    expect(a.getWriteAccess(u)).toBe(true);
+
+    a.setWriteAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess(u)).toBe(false);
+  });
+
+  it('can get write access', () => {
+    const a = new ParseCLP();
+    a.setCreateAccess('uid', true);
+    a.setDeleteAccess('uid', true);
+    a.setUpdateAccess('uid', true);
+    a.setAddFieldAccess('uid', true);
+    expect(a.toJSON()).toEqual(WRITE_CLP);
+    expect(a.getWriteAccess('uid')).toBe(true);
+    expect(a.getCreateAccess('uid')).toBe(true);
+    expect(a.getDeleteAccess('uid')).toBe(true);
+    expect(a.getUpdateAccess('uid')).toBe(true);
+    expect(a.getAddFieldAccess('uid')).toBe(true);
+    a.setCreateAccess('uid', false);
+    a.setDeleteAccess('uid', false);
+    a.setUpdateAccess('uid', false);
+    a.setAddFieldAccess('uid', false);
+    expect(a.getWriteAccess('uid')).toBe(false);
+    expect(a.getCreateAccess('uid')).toBe(false);
+    expect(a.getDeleteAccess('uid')).toBe(false);
+    expect(a.getUpdateAccess('uid')).toBe(false);
+    expect(a.getAddFieldAccess('uid')).toBe(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can handle public access', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getPublicReadAccess()).toBe(false);
+    expect(a.getPublicWriteAccess()).toBe(false);
+
+    a.setPublicGetAccess(true);
+    a.setPublicFindAccess(true);
+    a.setPublicCountAccess(true);
+    expect(a.toJSON()).toEqual(PUBLIC_READ_CLP);
+    expect(a.getPublicReadAccess()).toBe(true);
+    expect(a.getPublicGetAccess()).toBe(true);
+    expect(a.getPublicFindAccess()).toBe(true);
+    expect(a.getPublicCountAccess()).toBe(true);
+
+    a.setPublicReadAccess(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getPublicReadAccess()).toBe(false);
+    expect(a.getPublicGetAccess()).toBe(false);
+    expect(a.getPublicFindAccess()).toBe(false);
+    expect(a.getPublicCountAccess()).toBe(false);
+
+    a.setPublicCreateAccess(true);
+    a.setPublicDeleteAccess(true);
+    a.setPublicUpdateAccess(true);
+    a.setPublicAddFieldAccess(true);
+    expect(a.toJSON()).toEqual(PUBLIC_WRITE_CLP);
+    expect(a.getPublicWriteAccess()).toBe(true);
+    expect(a.getPublicCreateAccess()).toBe(true);
+    expect(a.getPublicDeleteAccess()).toBe(true);
+    expect(a.getPublicUpdateAccess()).toBe(true);
+    expect(a.getPublicAddFieldAccess()).toBe(true);
+
+    a.setPublicWriteAccess(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getPublicWriteAccess()).toBe(false);
+    expect(a.getPublicCreateAccess()).toBe(false);
+    expect(a.getPublicDeleteAccess()).toBe(false);
+    expect(a.getPublicUpdateAccess()).toBe(false);
+    expect(a.getPublicAddFieldAccess()).toBe(false);
+
+    const w = new ParseCLP();
+    w.setPublicWriteAccess(true);
+    expect(w.toJSON()).toEqual(PUBLIC_WRITE_CLP);
+
+    const r = new ParseCLP();
+    r.setPublicReadAccess(true);
+    expect(r.toJSON()).toEqual(PUBLIC_READ_CLP);
+  });
+
+  it('can get clp with role', () => {
+    const role = new ParseRole('admin');
+    const a = new ParseCLP(role);
+
+    expect(a.toJSON()).toEqual({
+      get: { 'role:admin': true },
+      find: { 'role:admin': true },
+      count: { 'role:admin': true },
+      create: { 'role:admin': true },
+      update: { 'role:admin': true },
+      delete: { 'role:admin': true },
+      addField: { 'role:admin': true },
+      protectedFields: {},
+    });
+    expect(a.getRoleReadAccess('admin')).toBe(true);
+    expect(a.getRoleReadAccess(role)).toBe(true);
+    expect(a.getReadAccess(role)).toBe(true);
+  });
+
+  it('can get role read access', () => {
+    const a = new ParseCLP({
+      get: { 'role:admin': true },
+      find: { 'role:admin': true },
+      count: { 'role:admin': true },
+      create: { 'role:admin': true },
+      update: { 'role:admin': true },
+      delete: { 'role:admin': true },
+      addField: { 'role:admin': true },
+      protectedFields: { 'role:admin': [] },
+    });
+
+    expect(a.getRoleReadAccess('admin')).toBe(true);
+    expect(a.getRoleReadAccess(new ParseRole('admin'))).toBe(true);
+    expect(a.getReadAccess(new ParseRole('admin'))).toBe(true);
+  });
+
+  it('can get role write access', () => {
+    const a = new ParseCLP({
+      get: { 'role:admin': true },
+      find: { 'role:admin': true },
+      count: { 'role:admin': true },
+      create: { 'role:admin': true },
+      update: { 'role:admin': true },
+      delete: { 'role:admin': true },
+      addField: { 'role:admin': true },
+      protectedFields: { 'role:admin': [] },
+    });
+
+    expect(a.getRoleWriteAccess('admin')).toBe(true);
+    expect(a.getRoleWriteAccess(new ParseRole('admin'))).toBe(true);
+    expect(a.getWriteAccess(new ParseRole('admin'))).toBe(true);
+  });
+
+  it('can set role read access', () => {
+    const a = new ParseCLP();
+    const adminRole = new ParseRole('admin');
+
+    expect(a.getRoleReadAccess('admin')).toBe(false);
+    expect(a.getRoleReadAccess(adminRole)).toBe(false);
+
+    a.setRoleReadAccess('admin', true);
+    expect(a.getRoleReadAccess('admin')).toBe(true);
+    expect(a.getRoleWriteAccess('admin')).toBe(false);
+
+    a.setRoleReadAccess(adminRole, false);
+    expect(a.getRoleReadAccess(adminRole)).toBe(false);
+
+    a.setReadAccess(adminRole, true);
+    expect(a.getReadAccess(adminRole)).toBe(true);
+  });
+
+  it('can set role write access', () => {
+    const a = new ParseCLP();
+
+    expect(a.getRoleWriteAccess('admin')).toBe(false);
+    expect(a.getRoleWriteAccess(new ParseRole('admin'))).toBe(false);
+
+    a.setRoleWriteAccess('admin', true);
+    expect(a.getRoleWriteAccess('admin')).toBe(true);
+    expect(a.getRoleReadAccess('admin')).toBe(false);
+
+    a.setRoleWriteAccess(new ParseRole('admin'), false);
+    expect(a.getRoleWriteAccess(new ParseRole('admin'))).toBe(false);
+
+    a.setWriteAccess(new ParseRole('admin'), true);
+    expect(a.getWriteAccess(new ParseRole('admin'))).toBe(true);
+  });
+
+  it('can handle role access', () => {
+    const a = new ParseCLP();
+    const r = new ParseRole('admin');
+
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getRoleReadAccess(r)).toBe(false);
+    expect(a.getRoleWriteAccess(r)).toBe(false);
+
+    a.setRoleGetAccess(r, true);
+    a.setRoleFindAccess(r, true);
+    a.setRoleCountAccess(r, true);
+    expect(a.toJSON()).toEqual(ROLE_READ_CLP);
+    expect(a.getRoleReadAccess(r)).toBe(true);
+    expect(a.getRoleGetAccess(r)).toBe(true);
+    expect(a.getRoleFindAccess(r)).toBe(true);
+    expect(a.getRoleCountAccess(r)).toBe(true);
+
+    a.setRoleReadAccess(r, false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getRoleReadAccess(r)).toBe(false);
+    expect(a.getRoleGetAccess(r)).toBe(false);
+    expect(a.getRoleFindAccess(r)).toBe(false);
+    expect(a.getRoleCountAccess(r)).toBe(false);
+
+    a.setRoleCreateAccess(r, true);
+    a.setRoleDeleteAccess(r, true);
+    a.setRoleUpdateAccess(r, true);
+    a.setRoleAddFieldAccess(r, true);
+    expect(a.toJSON()).toEqual(ROLE_WRITE_CLP);
+    expect(a.getRoleWriteAccess(r)).toBe(true);
+    expect(a.getRoleCreateAccess(r)).toBe(true);
+    expect(a.getRoleDeleteAccess(r)).toBe(true);
+    expect(a.getRoleUpdateAccess(r)).toBe(true);
+    expect(a.getRoleAddFieldAccess(r)).toBe(true);
+
+    a.setRoleWriteAccess(r, false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getRoleWriteAccess(r)).toBe(false);
+    expect(a.getRoleCreateAccess(r)).toBe(false);
+    expect(a.getRoleDeleteAccess(r)).toBe(false);
+    expect(a.getRoleUpdateAccess(r)).toBe(false);
+    expect(a.getRoleAddFieldAccess(r)).toBe(false);
+
+    const b = new ParseCLP();
+    b.setRoleWriteAccess(r, true);
+    expect(b.toJSON()).toEqual(ROLE_WRITE_CLP);
+
+    const c = new ParseCLP();
+    c.setRoleReadAccess(r, true);
+    expect(c.toJSON()).toEqual(ROLE_READ_CLP);
+  });
+
+  it('can test equality against another CLP', () => {
+    const a = new ParseCLP();
+    const b = new ParseCLP();
+
+    expect(a.equals(a)).toBe(true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setPublicReadAccess(true);
+    expect(a.equals(a)).toBe(true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setPublicWriteAccess(true);
+    expect(b.equals(b)).toBe(true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    a.setPublicWriteAccess(true);
+    b.setPublicReadAccess(true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setPublicReadAccess(false);
+    a.setPublicWriteAccess(false);
+    b.setPublicReadAccess(false);
+    b.setPublicWriteAccess(false);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+    expect(a.equals(new ParseCLP())).toBe(true);
+
+    a.setReadAccess('uid', true);
+    b.setReadAccess('uid', true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setWriteAccess('uid', true);
+    b.setWriteAccess('uid', true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setWriteAccess('uid2', true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setWriteAccess('uid2', true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setPublicReadAccess(true);
+    a.setReadAccess('uid', true);
+    b.setReadAccess('uid', true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setReadAccess('uid2', true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setReadAccess('uid2', false);
+    b.setPublicReadAccess(true);
+    b.permissionsMap.get['*'] = 'HACKED';
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    const r = new ParseRole('admin');
+    expect(a.equals(r)).toBe(false);
+
+    const hacked = new ParseCLP();
+    delete hacked.permissionsMap.get;
+    expect(a.equals(hacked)).toBe(false);
+    expect(hacked.equals(a)).toBe(false);
+
+    hacked.permissionsMap.newPermission = {};
+    expect(a.equals(hacked)).toBe(false);
+    expect(hacked.equals(a)).toBe(false);
+  });
+});
diff --git a/src/__tests__/ParseSchema-test.js b/src/__tests__/ParseSchema-test.js
index f1ad11f..92386ee 100644
--- a/src/__tests__/ParseSchema-test.js
+++ b/src/__tests__/ParseSchema-test.js
@@ -22,7 +22,16 @@ const mockObject = function (className, id) {
 };
 jest.setMock('../ParseObject', mockObject);
 
+const mockCLP = function (clp) {
+  this.permissionsMap = clp;
+  this.toJSON = function () {
+    return { ...this.permissionsMap };
+  };
+};
+jest.setMock('../ParseCLP', mockCLP);
+
 const ParseObject = require('../ParseObject');
+const ParseCLP = require('../ParseCLP');
 const ParseSchema = require('../ParseSchema').default;
 const CoreManager = require('../CoreManager');
 
@@ -148,6 +157,10 @@ describe('ParseSchema', () => {
     };
     schema.setCLP(clp);
     expect(schema._clp).toEqual(clp);
+
+    const clpObj = new ParseCLP(clp);
+    schema.setCLP(clpObj);
+    expect(schema._clp).toEqual(clpObj.toJSON());
     done();
   });