From b0c41f65537f323c7ad7cca047296b362ad3766a Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Wed, 21 Jan 2015 13:58:53 -0500 Subject: [PATCH] storage: acl: refactor --- lib/storage/acl.js | 252 +++++++++++++++++++++++++++++++++++++++++- lib/storage/bucket.js | 30 +++++ regression/storage.js | 24 ++++ test/storage/acl.js | 114 ++++++++++++++++++- 4 files changed, 416 insertions(+), 4 deletions(-) diff --git a/lib/storage/acl.js b/lib/storage/acl.js index 2b03ba2de571..fd5265f5fe53 100644 --- a/lib/storage/acl.js +++ b/lib/storage/acl.js @@ -26,6 +26,8 @@ */ var util = require('../common/util.js'); +var nodeutil = require('util'); + /** * Google Cloud Storage uses access control lists (ACLs) to manage object and * bucket access. ACLs are the mechanism you use to share objects with other @@ -67,10 +69,131 @@ var util = require('../common/util.js'); * @alias module:storage/acl */ function Acl(options) { + AclRoleAccessorMethods.call(this); + this.makeReq = options.makeReq; this.pathPrefix = options.pathPrefix; } +/** + * A convenience object of methods to add or delete ACL permissions from a + * scope. + * + * The supported methods include: + * + * - `myFile.acl.owners.addAllAuthenticatedUsers` + * - `myFile.acl.owners.deleteAllAuthenticatedUsers` + * - `myFile.acl.owners.addAllUsers` + * - `myFile.acl.owners.deleteAllUsers` + * - `myFile.acl.owners.addDomain` + * - `myFile.acl.owners.deleteDomain` + * - `myFile.acl.owners.addGroup` + * - `myFile.acl.owners.deleteGroup` + * - `myFile.acl.owners.addProject` + * - `myFile.acl.owners.deleteProject` + * - `myFile.acl.owners.addUser` + * - `myFile.acl.owners.deleteUser` + * + * @alias acl.owners + * + * @return {object} + * + * @example + * //- + * // Add a user scope as an owner of a file. + * //- + * myFile.acl.owners.addUser('email@example.com', function(err, aclObject) {}); + * + * //- + * // For reference, the above command is the same as running the following. + * //- + * myFile.acl.add({ + * scope: 'user-email@example.com', + * role: storage.acl.OWNER_ROLE + * }, function(err, aclObject) {}); + */ +Acl.prototype.owners = {}; + +/** + * A convenience object of methods to add or delete ACL permissions from a + * scope. + * + * The supported methods include: + * + * - `myFile.acl.readers.addAllAuthenticatedUsers` + * - `myFile.acl.readers.deleteAllAuthenticatedUsers` + * - `myFile.acl.readers.addAllUsers` + * - `myFile.acl.readers.deleteAllUsers` + * - `myFile.acl.readers.addDomain` + * - `myFile.acl.readers.deleteDomain` + * - `myFile.acl.readers.addGroup` + * - `myFile.acl.readers.deleteGroup` + * - `myFile.acl.readers.addProject` + * - `myFile.acl.readers.deleteProject` + * - `myFile.acl.readers.addUser` + * - `myFile.acl.readers.deleteUser` + * + * @alias acl.readers + * + * @return {object} + * + * @example + * //- + * // Add a user scope as an owner of a file. + * //- + * myFile.acl.readers.addUser('email@example.com', function(err, aclObject) {}); + * + * //- + * // For reference, the above command is the same as running the following. + * //- + * myFile.acl.add({ + * scope: 'user-email@example.com', + * role: storage.acl.READER_ROLE + * }, function(err, aclObject) {}); + */ +Acl.prototype.readers = {}; + +/** + * A convenience object of methods to add or delete ACL permissions from a + * scope. + * + * The supported methods include: + * + * - `myFile.acl.writers.addAllAuthenticatedUsers` + * - `myFile.acl.writers.deleteAllAuthenticatedUsers` + * - `myFile.acl.writers.addAllUsers` + * - `myFile.acl.writers.deleteAllUsers` + * - `myFile.acl.writers.addDomain` + * - `myFile.acl.writers.deleteDomain` + * - `myFile.acl.writers.addGroup` + * - `myFile.acl.writers.deleteGroup` + * - `myFile.acl.writers.addProject` + * - `myFile.acl.writers.deleteProject` + * - `myFile.acl.writers.addUser` + * - `myFile.acl.writers.deleteUser` + * + * @alias acl.writers + * + * @return {object} + * + * @example + * //- + * // Add a user scope as an owner of a file. + * //- + * myFile.acl.writers.addUser('email@example.com', function(err, aclObject) {}); + * + * //- + * // For reference, the above command is the same as running the following. + * //- + * myFile.acl.add({ + * scope: 'user-email@example.com', + * role: storage.acl.WRITER_ROLE + * }, function(err, aclObject) {}); + */ +Acl.prototype.writers = {}; + +nodeutil.inherits(Acl, AclRoleAccessorMethods); + /** * Add access controls on a {module:storage/bucket} or {module:storage/file}. * @@ -294,6 +417,48 @@ Acl.prototype.update = function(options, callback) { }); }; +/** + * Make a {module:storage/bucket} or {module:storage/file} object private. + * + * This is a short-hand method which will remove ACL permissions for the + * "allUsers" scope. + * + * @param {function} callback - The callback function. + * + * @alias acl.makePrivate + * + * @example + * myBucket.acl.makePrivate(function(err) {}); + * myFile.acl.makePrivate(function(err) {}); + */ +Acl.prototype.makePrivate = function(callback) { + this.delete({ + scope: 'allUsers' + }, callback); +}; + +/** + * Make a {module:storage/bucket} or {module:storage/file} object publicly + * readable. + * + * This is a short-hand method which will grant "reader" permissions to the + * "allUsers" scope. + * + * @param {function} callback - The callback function. + * + * @alias acl.makePublic + * + * @example + * myBucket.acl.makePublic(function(err) {}); + * myFile.acl.makePublic(function(err) {}); + */ +Acl.prototype.makePublic = function(callback) { + this.add({ + scope: 'allUsers', + role: 'READER' + }, callback); +}; + /** * Transform API responses to a consistent object format. * @@ -301,7 +466,7 @@ Acl.prototype.update = function(options, callback) { */ Acl.prototype.makeAclObject_ = function(accessControlObject) { var obj = { - scope: accessControlObject.scope, + scope: accessControlObject.entity, role: accessControlObject.role }; @@ -328,3 +493,88 @@ Acl.prototype.makeReq_ = function(method, path, query, body, callback) { }; module.exports = Acl; + +/*! Developer Documentation + * Attach functionality to a {module:storage/acl} instance. This will add an + * object for each role group (owners, readers, and writers), with each object + * containing methods to add or delete a type of entity. + * + * As an example, here are a few methods that are created. + * + * myBucket.acl.readers.deleteGroup('groupId', function(err) {}); + * + * myBucket.acl.owners.addUser('email@example.com', function(err) {}); + * + * myBucket.acl.writers.addDomain('example.com', function(err) {}); + */ +function AclRoleAccessorMethods() { + AclRoleAccessorMethods.roles.forEach(this._assignAccessMethods.bind(this)); +} + +AclRoleAccessorMethods.accessMethods = [ + 'add', + 'delete' +]; + +AclRoleAccessorMethods.entities = [ + // Special entity groups that do not require further specification. + 'allAuthenticatedUsers', + 'allUsers', + + // Entity groups that require specification, e.g. `user-email@example.com`. + 'domain-', + 'group-', + 'project-', + 'user-' +]; + +AclRoleAccessorMethods.roles = [ + 'owner', + 'reader', + 'writer' +]; + +AclRoleAccessorMethods.prototype._assignAccessMethods = function(role) { + var that = this; + + var accessMethods = AclRoleAccessorMethods.accessMethods; + var entities = AclRoleAccessorMethods.entities; + var roleGroup = role + 's'; + + this[roleGroup] = entities.reduce(function(acc, entity) { + var isPrefix = entity.substr(-1) === '-'; + + accessMethods.forEach(function(accessMethod) { + var method = accessMethod + entity[0].toUpperCase() + entity.substr(1); + + if (isPrefix) { + method = method.replace('-', ''); + } + + // Wrap the parent accessor method (e.g. `add` or `delete`) to avoid the + // more complex API of specifying a `scope` and `role`. + acc[method] = function(entityId, callback) { + var scope; + + if (isPrefix) { + scope = entity + entityId; + } else { + // If the scope is not a prefix, it is a special entity group that + // does not require further details. The accessor methods only accept + // a callback. + scope = entity; + callback = entityId; + } + + that[accessMethod]({ + scope: scope, + role: role + }, callback); + }; + }); + + return acc; + }, {}); +}; + +module.exports.AclRoleAccessorMethods = AclRoleAccessorMethods; diff --git a/lib/storage/bucket.js b/lib/storage/bucket.js index 7e5c498fb729..b9c10e21ee6c 100644 --- a/lib/storage/bucket.js +++ b/lib/storage/bucket.js @@ -171,6 +171,36 @@ function Bucket(storage, name) { * @alias acl.default.update */ var aclDefaultUpdate = true; + + /** + * Maps to {module:storage/bucket#acl.makePrivate}. + * @alias acl.default.makePrivate + */ + var aclDefaultMakePrivate = true; + + /** + * Maps to {module:storage/bucket#acl.makePublic}. + * @alias acl.default.makePublic + */ + var aclDefaultMakePublic = true; + + /** + * Maps to {module:storage/bucket#acl.owners}. + * @alias acl.default.owners + */ + var aclDefaultOwners = true; + + /** + * Maps to {module:storage/bucket#acl.readers}. + * @alias acl.default.readers + */ + var aclDefaultReaders = true; + + /** + * Maps to {module:storage/bucket#acl.writers}. + * @alias acl.default.writers + */ + var aclDefaultWriters = true; /* jshint ignore:end */ } diff --git a/regression/storage.js b/regression/storage.js index 322024096c30..9a4a5463faa6 100644 --- a/regression/storage.js +++ b/regression/storage.js @@ -149,6 +149,18 @@ describe('storage', function() { }); }); + it('should make a bucket publicly readable', function(done) { + bucket.acl.makePublic(function(err, accessControl) { + assert.equal(accessControl.role, storage.acl.READER_ROLE); + assert.equal(accessControl.scope, 'allUsers'); + done(); + }); + }); + + it('should make a bucket private', function(done) { + bucket.acl.makePrivate(done); + }); + it('should update an account', function(done) { bucket.acl.add({ scope: USER_ACCOUNT, @@ -234,6 +246,18 @@ describe('storage', function() { }); }); }); + + it('should make a file publicly readable', function(done) { + file.acl.makePublic(function(err, accessControl) { + assert.equal(accessControl.role, storage.acl.READER_ROLE); + assert.equal(accessControl.scope, 'allUsers'); + done(); + }); + }); + + it('should make a file private', function(done) { + file.acl.makePrivate(done); + }); }); }); diff --git a/test/storage/acl.js b/test/storage/acl.js index 425944083489..50ba822186bd 100644 --- a/test/storage/acl.js +++ b/test/storage/acl.js @@ -20,7 +20,8 @@ var Acl = require('../../lib/storage/acl.js'); var assert = require('assert'); -var storage = require('../../lib/storage/index.js'); +var async = require('async'); +var Storage = require('../../lib/storage/index.js'); var util = require('../../lib/common/util.js'); describe('storage/acl', function() { @@ -28,7 +29,7 @@ describe('storage/acl', function() { var ERROR = new Error('Error.'); var MAKE_REQ = util.noop; var PATH_PREFIX = '/acl'; - var ROLE = storage.acl.OWNER_ROLE; + var ROLE = Storage.acl.OWNER_ROLE; var SCOPE = 'user-user@example.com'; beforeEach(function() { @@ -275,6 +276,35 @@ describe('storage/acl', function() { }); }); + describe('makePrivate', function() { + it('should revoke read permission to allUsers entity', function(done) { + acl.delete = function(options, callback) { + assert.deepEqual(options, { + scope: 'allUsers' + }); + + callback(); + }; + + acl.makePrivate(done); + }); + }); + + describe('makePublic', function() { + it('should grant read permission to allUsers entity', function(done) { + acl.add = function(options, callback) { + assert.deepEqual(options, { + scope: 'allUsers', + role: Storage.acl.READER_ROLE + }); + + callback(); + }; + + acl.makePublic(done); + }); + }); + describe('makeAclObject_', function() { it('should return an ACL object from an API response', function() { var projectTeam = { @@ -283,7 +313,7 @@ describe('storage/acl', function() { }; var apiResponse = { - scope: SCOPE, + entity: SCOPE, role: ROLE, projectTeam: projectTeam, extra: 'ignored', @@ -321,3 +351,81 @@ describe('storage/acl', function() { }); }); }); + +describe('storage/AclRoleAccessorMethods', function() { + var aclEntity; + + beforeEach(function() { + aclEntity = new Acl.AclRoleAccessorMethods(); + }); + + describe('initialization', function() { + it('should assign access methods for every role object', function() { + var expectedApi = [ + 'addAllAuthenticatedUsers', + 'deleteAllAuthenticatedUsers', + + 'addAllUsers', + 'deleteAllUsers', + + 'addDomain', + 'deleteDomain', + + 'addGroup', + 'deleteGroup', + + 'addProject', + 'deleteProject', + + 'addUser', + 'deleteUser' + ]; + + var actualOwnersApi = Object.keys(aclEntity.owners); + assert.deepEqual(actualOwnersApi, expectedApi); + + var actualReadersApi = Object.keys(aclEntity.readers); + assert.deepEqual(actualReadersApi, expectedApi); + + var actualWritersApi = Object.keys(aclEntity.writers); + assert.deepEqual(actualWritersApi, expectedApi); + }); + }); + + describe('_assignAccessMethods', function() { + it('should call parent method', function(done) { + var userName = 'email@example.com'; + var role = 'fakerole'; + + aclEntity.add = function (options, callback) { + assert.deepEqual(options, { + scope: 'user-' + userName, + role: role + }); + + callback(); + }; + + aclEntity.delete = function (options, callback) { + assert.deepEqual(options, { + scope: 'user-' + userName, + role: role + }); + + callback(); + }; + + aclEntity._assignAccessMethods(role); + + async.parallel([ + function(next) { + // The method name should be in plural form. (fakeroles vs fakerole) + aclEntity.fakeroles.addUser(userName, next); + }, + function(next) { + aclEntity.fakeroles.deleteUser(userName, next); + } + ], done); + }); + }); +});