Skip to content

Commit

Permalink
feat: add backup level IAM policy controls (#799)
Browse files Browse the repository at this point in the history
The new features allow user to check and tune [IAM policy](https://cloud.google.com/iam/docs/reference/rest/v1/Policy) for the backup level:

- `getIamPolicy` - allows a user obtain the current resource IAM policy.
- `setIamPolicy` - allows a user to set resource level IAM policy.
- `testIamPermissions` - allows a user to pass a list of [`permissions`](https://cloud.google.com/bigtable/docs/access-control#permissions) and get back a sub-list of granted permissions.

- [x] Ensure the tests and linter pass
- [x] Code coverage does not decrease (if any source code was changed)
- [x] Appropriate docs were updated (if necessary)
  • Loading branch information
AVaksman committed Oct 26, 2020
1 parent a14ccba commit 0f3b8b3
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 7 deletions.
121 changes: 120 additions & 1 deletion src/backup.ts
Expand Up @@ -16,7 +16,19 @@ import {PreciseDate} from '@google-cloud/precise-date';
import {promisifyAll} from '@google-cloud/promisify';
import snakeCase = require('lodash.snakecase');
import {google} from '../protos/protos';
import {Bigtable, Cluster, Table} from './';
import {
Bigtable,
Cluster,
GetIamPolicyCallback,
GetIamPolicyOptions,
GetIamPolicyResponse,
Policy,
SetIamPolicyCallback,
SetIamPolicyResponse,
TestIamPermissionsCallback,
TestIamPermissionsResponse,
} from './';
import {Table} from '../src/table';
import {
CreateBackupConfig,
CreateBackupCallback,
Expand Down Expand Up @@ -362,6 +374,38 @@ Please use the format 'my-backup' or '${cluster.name}/backups/my-backup'.`);
);
}

getIamPolicy(options?: GetIamPolicyOptions): Promise<GetIamPolicyResponse>;
getIamPolicy(
options: GetIamPolicyOptions,
callback: GetIamPolicyCallback
): void;
/**
* @param {object} [options] Configuration object.
* @param {object} [options.gaxOptions] Request configuration options, outlined
* here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
* @param {number} [options.requestedPolicyVersion] The policy format version
* to be returned. Valid values are 0, 1, and 3. Requests specifying an
* invalid value will be rejected. Requests for policies with any
* conditional bindings must specify version 3. Policies without any
* conditional bindings may specify any valid value or leave the field unset.
* @param {function} [cb] The callback function.
* @param {?error} callback.error An error returned while making this request.
* @param {Policy} policy The policy.
*
* @example <caption>include:samples/document-snippets/instance.js</caption>
* region_tag:bigtable_get_table_Iam_policy
*/
getIamPolicy(
optionsOrCallback?: GetIamPolicyOptions | GetIamPolicyCallback,
cb?: GetIamPolicyCallback
): void | Promise<GetIamPolicyResponse> {
const options =
typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback =
typeof optionsOrCallback === 'function' ? optionsOrCallback : cb!;
Table.prototype.getIamPolicy.call(this, options, callback);
}

getMetadata(gaxOptions?: CallOptions): Promise<BackupGetMetadataResponse>;
getMetadata(callback: BackupGetMetadataCallback): void;
getMetadata(
Expand Down Expand Up @@ -477,6 +521,38 @@ Please use the format 'my-backup' or '${cluster.name}/backups/my-backup'.`);
);
}

setIamPolicy(
policy: Policy,
gaxOptions?: CallOptions
): Promise<SetIamPolicyResponse>;
setIamPolicy(
policy: Policy,
gaxOptions: CallOptions,
callback: SetIamPolicyCallback
): void;
setIamPolicy(policy: Policy, callback: SetIamPolicyCallback): void;
/**
* @param {object} [gaxOptions] Request configuration options, outlined
* here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
* @param {function} [callback] The callback function.
* @param {?error} callback.error An error returned while making this request.
* @param {Policy} policy The policy.
*
* @example <caption>include:samples/document-snippets/instance.js</caption>
* region_tag:bigtable_set_instance_Iam_policy
*/
setIamPolicy(
policy: Policy,
gaxOptionsOrCallback?: CallOptions | SetIamPolicyCallback,
cb?: SetIamPolicyCallback
): void | Promise<SetIamPolicyResponse> {
const gaxOptions =
typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
const callback =
typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb!;
Table.prototype.setIamPolicy.call(this, policy, gaxOptions, callback);
}

setMetadata(
metadata: ModifiableBackupFields,
gaxOptions?: CallOptions
Expand Down Expand Up @@ -544,6 +620,49 @@ Please use the format 'my-backup' or '${cluster.name}/backups/my-backup'.`);
}
);
}

testIamPermissions(
permissions: string | string[],
gaxOptions?: CallOptions
): Promise<TestIamPermissionsResponse>;
testIamPermissions(
permissions: string | string[],
callback: TestIamPermissionsCallback
): void;
testIamPermissions(
permissions: string | string[],
gaxOptions: CallOptions,
callback: TestIamPermissionsCallback
): void;
/**
*
* @param {string | string[]} permissions The permission(s) to test for.
* @param {object} [gaxOptions] Request configuration options, outlined
* here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
* @param {function} [callback] The callback function.
* @param {?error} callback.error An error returned while making this request.
* @param {string[]} permissions A subset of permissions that the caller is
* allowed.
*
* @example <caption>include:samples/document-snippets/instance.js</caption>
* region_tag:bigtable_test_table_Iam_permissions
*/
testIamPermissions(
permissions: string | string[],
gaxOptionsOrCallback?: CallOptions | TestIamPermissionsCallback,
cb?: TestIamPermissionsCallback
): void | Promise<TestIamPermissionsResponse> {
const gaxOptions =
typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
const callback =
typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb!;
Table.prototype.testIamPermissions.call(
this,
permissions,
gaxOptions,
callback
);
}
}

/*! Developer Documentation
Expand Down
2 changes: 1 addition & 1 deletion src/table.ts
Expand Up @@ -1678,12 +1678,12 @@ Please use the format 'prezzy' or '${instance.name}/tables/prezzy'.`);
policy: Policy,
gaxOptions?: CallOptions
): Promise<SetIamPolicyResponse>;
setIamPolicy(policy: Policy, callback: SetIamPolicyCallback): void;
setIamPolicy(
policy: Policy,
gaxOptions: CallOptions,
callback: SetIamPolicyCallback
): void;
setIamPolicy(policy: Policy, callback: SetIamPolicyCallback): void;
/**
* @param {object} [gaxOptions] Request configuration options, outlined
* here: https://googleapis.github.io/gax-nodejs/CallSettings.html.
Expand Down
35 changes: 31 additions & 4 deletions system-test/bigtable.ts
Expand Up @@ -136,7 +136,7 @@ describe('Bigtable', () => {
const policyProperties = ['version', 'bindings', 'etag'];
const [policy] = await INSTANCE.getIamPolicy();
policyProperties.forEach(property => {
assert.strictEqual(Object.keys(policy).includes(property), true);
assert(property in policy);
});
});

Expand Down Expand Up @@ -170,7 +170,7 @@ describe('Bigtable', () => {

const [policy] = await instance.getIamPolicy();
const [updatedPolicy] = await instance.setIamPolicy(policy);
assert.notStrictEqual(updatedPolicy, null);
Object.keys(policy).forEach(key => assert(key in updatedPolicy));

await instance.delete();
});
Expand Down Expand Up @@ -326,7 +326,7 @@ describe('Bigtable', () => {
const policyProperties = ['version', 'bindings', 'etag'];
const [policy] = await TABLE.getIamPolicy();
policyProperties.forEach(property => {
assert.strictEqual(Object.keys(policy).includes(property), true);
assert(property in policy);
});
});

Expand All @@ -345,7 +345,7 @@ describe('Bigtable', () => {

const [policy] = await table.getIamPolicy();
const [updatedPolicy] = await table.setIamPolicy(policy);
assert.notStrictEqual(updatedPolicy, null);
Object.keys(policy).forEach(key => assert(key in updatedPolicy));

await table.delete();
});
Expand Down Expand Up @@ -1259,6 +1259,33 @@ describe('Bigtable', () => {
assert.strictEqual(metadata.name, backupNameFromCluster);
assert.deepStrictEqual(backup.expireDate, updateExpireTime);
});

it('should get an Iam Policy for the backup', async () => {
const policyProperties = ['version', 'bindings', 'etag'];
const [policy] = await BACKUP.getIamPolicy();

policyProperties.forEach(property => {
assert(property in policy);
});
});

it('should test Iam permissions for the backup', async () => {
const permissions = ['bigtable.backups.get', 'bigtable.backups.delete'];
const [grantedPermissions] = await BACKUP.testIamPermissions(permissions);
assert.strictEqual(grantedPermissions.length, permissions.length);
permissions.forEach(permission => {
assert.strictEqual(grantedPermissions.includes(permission), true);
});
});

it('should set Iam Policy on the backup', async () => {
const backup = CLUSTER.backup(backupIdFromCluster);

const [policy] = await backup.getIamPolicy();
const [updatedPolicy] = await backup.setIamPolicy(policy);

Object.keys(policy).forEach(key => assert(key in updatedPolicy));
});
});
});

Expand Down
115 changes: 114 additions & 1 deletion test/backup.ts
Expand Up @@ -15,15 +15,18 @@
import {PreciseDate} from '@google-cloud/precise-date';
import * as promisify from '@google-cloud/promisify';
import * as assert from 'assert';
import {before, beforeEach, describe, it} from 'mocha';
import {before, beforeEach, describe, it, afterEach} from 'mocha';
import * as proxyquire from 'proxyquire';
import * as pumpify from 'pumpify';
import {ServiceError} from 'google-gax';

import * as clusterTypes from '../src/cluster';
import * as backupTypes from '../src/backup';
import * as instanceTypes from '../src/instance';
import * as sinon from 'sinon';

import {Bigtable} from '../src';
import {Table} from '../src/table';

let promisified = false;
const fakePromisify = Object.assign({}, promisify, {
Expand All @@ -40,6 +43,14 @@ const fakePromisify = Object.assign({}, promisify, {
},
});

class FakeTable extends Table {
calledWith_: Array<{}>;
constructor(...args: [instanceTypes.Instance, string]) {
super(args[0], args[1]);
this.calledWith_ = args;
}
}

describe('Bigtable/Backup', () => {
const BACKUP_ID = 'my-backup';
let CLUSTER: clusterTypes.Cluster;
Expand All @@ -52,6 +63,7 @@ describe('Bigtable/Backup', () => {
before(() => {
Backup = proxyquire('../src/backup.js', {
'@google-cloud/promisify': fakePromisify,
'./table.js': {Table: FakeTable},
pumpify,
}).Backup;
});
Expand Down Expand Up @@ -328,6 +340,30 @@ describe('Bigtable/Backup', () => {
});
});

describe('getIamPolicy', () => {
afterEach(() => {
sinon.restore();
});

it('should correctly call Table#getIamPolicy()', done => {
sinon.stub(Table.prototype, 'getIamPolicy').callsFake((opt, callback) => {
assert.deepStrictEqual(opt, {});
callback(); // done()
});
backup.getIamPolicy(done);
});

it('should accept options', done => {
const options = {gaxOptions: {}, requestedPolicyVersion: 1};

sinon.stub(Table.prototype, 'getIamPolicy').callsFake((opt, callback) => {
assert.strictEqual(opt, options);
callback(); // done()
});
backup.getIamPolicy(options, done);
});
});

describe('getMetadata', () => {
it('should make the correct request', done => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -529,4 +565,81 @@ describe('Bigtable/Backup', () => {
);
});
});

describe('setIamPolicy', () => {
afterEach(() => {
sinon.restore();
});
const policy = {};
it('should correctly call Table#setIamPolicy()', done => {
sinon
.stub(Table.prototype, 'setIamPolicy')
.callsFake((_policy, gaxOpts, callback) => {
assert.strictEqual(_policy, policy);
assert.deepStrictEqual(gaxOpts, {});
callback(); // done()
});
backup.setIamPolicy(policy, done);
});

it('should accept gaxOptions', done => {
const gaxOptions = {};

sinon
.stub(Table.prototype, 'setIamPolicy')
.callsFake((_policy, gaxOpts, callback) => {
assert.strictEqual(_policy, policy);
assert.strictEqual(gaxOpts, gaxOptions);
callback(); // done()
});
backup.setIamPolicy(policy, gaxOptions, done);
});
});

describe('testIamPermissions', () => {
afterEach(() => {
sinon.restore();
});

const permissions = 'bigtable.backups.get';
it('should properly call Table#testIamPermissions', done => {
sinon
.stub(Table.prototype, 'testIamPermissions')
.callsFake((_permissions, gaxOpts, callback) => {
assert.strictEqual(_permissions, permissions);
assert.deepStrictEqual(gaxOpts, {});
callback(); // done()
});
backup.testIamPermissions(permissions, done);
});

it('should accept permissions as array', done => {
const permissions = [
'bigtable.backups.get',
'bigtable.backups.delete',
'bigtable.backups.update',
'bigtable.backups.restore',
];
sinon
.stub(Table.prototype, 'testIamPermissions')
.callsFake((_permissions, gaxOpts, callback) => {
assert.strictEqual(_permissions, permissions);
assert.deepStrictEqual(gaxOpts, {});
callback(); // done()
});
backup.testIamPermissions(permissions, done);
});

it('should accept gaxOptions', done => {
const gaxOptions = {};
sinon
.stub(Table.prototype, 'testIamPermissions')
.callsFake((_permissions, gaxOpts, callback) => {
assert.strictEqual(_permissions, permissions);
assert.strictEqual(gaxOpts, gaxOptions);
callback(); // done()
});
backup.testIamPermissions(permissions, gaxOptions, done);
});
});
});

0 comments on commit 0f3b8b3

Please sign in to comment.