CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/dashboard/data/permission-model.js

Summary

Maintainability
D
2 days
Test Coverage
const _ = require('underscore');
const result = require('builder/helpers/utils').result;
const Backbone = require('backbone');
const UserModel = require('dashboard/data/user-model');
const OrganizationModel = require('dashboard/data/organization-model');
const GroupModel = require('dashboard/data/group-model');
const checkAndBuildOpts = require('builder/helpers/required-opts');

// Nobody else uses this, and it would incur on a circular dependency if moved to a file
const ACLItemModel = Backbone.Model.extend({
  defaults: {
    access: 'r'
  },

  isOwn: function (model) {
    return model.id === this.get('entity').id;
  },

  validate: function (attrs, options) {
    var p = PermissionModel;
    if (attrs.access !== p.READ_ONLY && attrs.access !== p.READ_WRITE) {
      return "access can't take 'r' or 'rw' values";
    }
  },

  toJSON: function () {
    var entity = _.pick(this.get('entity').toJSON(), 'id', 'username', 'avatar_url', 'name');
    // translate name to username
    if (!entity.username) {
      entity.username = entity.name;
      delete entity.name;
    }
    return {
      type: this.get('type') || 'user',
      entity: entity,
      access: this.get('access')
    };
  }
});

const REQUIRED_OPTS = [
  'configModel'
];

/**
 * manages a cartodb permission object, it contains:
 * - owner: an cdb.admin.User instance
 * - acl: a collection which includes the user and their permission.
 *
 *   see https://github.com/Vizzuality/cartodb-management/wiki/multiuser-REST-API#permissions-object
 *
 *   this object is not created to work alone, it should be a member of an object like visualization
 *   table
 */
const PermissionModel = Backbone.Model.extend({
  urlRoot: '/api/v1/perm',

  initialize: function (attrs, options) {
    checkAndBuildOpts(options, REQUIRED_OPTS, this);
    this.acl = new Backbone.Collection();
    this.owner = null;
    this._generateOwner();
    this._generateAcl();
    this.bind('change:owner', this._generateOwner, this);
    this.bind('change:acl', this._generateAcl, this);
  },

  _generateOwner: function () {
    if (!this.owner) {
      this.owner = new UserModel(undefined, { configModel: this._configModel });
    }
    this.owner.set(this.get('owner'));
  },

  _generateAcl: function () {
    this.acl.reset([], { silent: true });
    _.each(this.get('acl'), function (aclItem) {
      var model;
      switch (aclItem.type) {
        case 'user':
          model = new UserModel(aclItem.entity, { configModel: this._configModel });
          break;
        case 'org':
          model = new OrganizationModel(aclItem.entity, { configModel: this._configModel });
          break;
        case 'group':
          model = new GroupModel(aclItem.entity, { configModel: this._configModel });
          break;
        default:
          throw new Error('Unknown ACL item type: ' + aclItem.type);
      }
      this._grantAccess(model, aclItem.access);
    }, this);
  },

  cleanPermissions: function () {
    this.acl.reset();
  },

  hasAccess: function (model) {
    // Having at least read access is the same as having any access
    return this.hasReadAccess(model);
  },

  hasReadAccess: function (model) {
    // If there is a representable ACL item it must be one of at least READ_ONLY access
    return !!this.findRepresentableAclItem(model);
  },

  hasWriteAccess: function (model) {
    var access = result(this.findRepresentableAclItem(model), 'get', 'access');
    return access === PermissionModel.READ_WRITE;
  },

  canChangeReadAccess: function (model) {
    return this._canChangeAccess(model);
  },

  canChangeWriteAccess: function (model) {
    return (!model.isBuilder || model.isBuilder()) && this._canChangeAccess(model, function (representableAclItem) {
      return result(representableAclItem, 'get', 'access') !== PermissionModel.READ_WRITE;
    });
  },

  _canChangeAccess: function (model) {
    var representableAclItem = this.findRepresentableAclItem(model);
    return this.isOwner(model) || !representableAclItem ||
      representableAclItem === this._ownAclItem(model) || result(arguments, 1, representableAclItem) || false;
  },

  grantWriteAccess: function (model) {
    this._grantAccess(model, this.constructor.READ_WRITE);
  },

  grantReadAccess: function (model) {
    this._grantAccess(model, this.constructor.READ_ONLY);
  },

  revokeWriteAccess: function (model) {
    // Effectively "downgrades" to READ_ONLY
    this.grantReadAccess(model);
  },

  /**
   * Revokes access to a set of items
   * @param {Object} model A single model or an array of models
   */
  revokeAccess: function (model) {
    var aclItem = this._ownAclItem(model);
    if (aclItem) {
      this.acl.remove(aclItem);
    }
  },

  getOwner: function () {
    return this.owner;
  },

  isOwner: function (model) {
    return _.result(this.owner, 'id') === _.result(model, 'id');
  },

  toJSON: function () {
    return {
      entity: this.get('entity'),
      acl: this.acl.toJSON()
    };
  },

  getUsersWithAnyPermission: function () {
    return this.acl.chain()
      .filter(this._hasTypeUser)
      .map(this._getEntity)
      .value();
  },

  isSharedWithOrganization: function () {
    return this.acl.any(this._hasTypeOrg);
  },

  clone: function () {
    var attrs = _.clone(this.attributes);
    delete attrs.id;
    return new PermissionModel(attrs, { configModel: this._configModel });
  },

  /**
   * Overwrite this ACL list from other permission object
   * @param otherPermission {Object} instance of PermissionModel
   */
  overwriteAcl: function (otherPermission) {
    this.acl.reset(otherPermission.acl.models);
  },

  // Note that this may return an inherited ACL item
  // use ._ownAclItem instead if only model's own is wanted (if there is any)
  findRepresentableAclItem: function (model) {
    if (this.isOwner(model)) {
      return this._newAclItem(model, this.constructor.READ_WRITE);
    } else {
      var checkList = ['_ownAclItem', '_organizationAclItem', '_mostPrivilegedGroupAclItem'];
      return this._findMostPrivilegedAclItem(checkList, function (fnName) {
        return this[fnName](model);
      });
    }
  },

  _hasTypeUser: function (m) {
    return m.get('type') === 'user';
  },

  _getEntity: function (m) {
    return m.get('entity');
  },

  _hasTypeOrg: function (m) {
    return m.get('type') === 'org';
  },

  _isOrganization: function (object) {
    return object instanceof OrganizationModel;
  },

  _ownAclItem: function (model) {
    if (!model || !_.isFunction(model.isNew)) {
      console.error('model is required to find an ACL item');
    }
    if (!model.isNew()) {
      return this.acl.find(function (aclItem) {
        return aclItem.get('entity').id === model.id;
      });
    }
  },

  _organizationAclItem: function (m) {
    var org = _.result(m.collection, 'organization') || m.organization;
    if (org) {
      return this._ownAclItem(org);
    }
  },

  _mostPrivilegedGroupAclItem: function (m) {
    var groups = _.result(m.groups, 'models');
    if (groups) {
      return this._findMostPrivilegedAclItem(groups, this._ownAclItem);
    }
  },

  /**
   * Iterates over a items in given list using the iteratee, stops and returns when found the ACL item with best access (i.e. READ_WRITE), or the
   * list is completed.
   * @param {Array} list
   * @param {Function} iteratee that takes an item from list and returns an access
   *   iteratee is called in context of this model.
   * @Return {String} 'r', 'rw', or undefined if there were no access for given item
   */
  _findMostPrivilegedAclItem: function (list, iteratee) {
    var aclItem;
    for (var i = 0, x = list[i]; x && result(aclItem, 'get', 'access') !== PermissionModel.READ_WRITE; x = list[++i]) {
      // Keep last ACL item if iteratee returns nothing
      aclItem = iteratee.call(this, x) || aclItem;
    }
    return aclItem;
  },

  /**
   * Grants access to a set of items
   * @param {Object} model
   * @param {String} access can take the following values:
   * - 'r': read only
   * - 'rw': read and write permission
   */
  _grantAccess: function (model, access) {
    var aclItem = this._ownAclItem(model);
    if (aclItem) {
      aclItem.set('access', access);
    } else {
      aclItem = this._newAclItem(model, access);
      if (aclItem.isValid()) {
        this.acl.add(aclItem);
      } else {
        throw new Error(access + ' is not a valid ACL access');
      }
    }
  },

  _newAclItem: function (model, access) {
    const type = model.get('type') || model.getModelType();

    return new ACLItemModel({
      type: type,
      entity: model,
      access: access
    });
  }

}, {

  READ_ONLY: 'r',
  READ_WRITE: 'rw'

});

module.exports = PermissionModel;