Skip to content

Commit

Permalink
feat: implement support for clusters with CMEK encryption (#855)
Browse files Browse the repository at this point in the history
* feat: implement support for clusters with CMEK encryption

* destroy keyversion
  • Loading branch information
stephenplusplus committed May 5, 2021
1 parent bac490d commit 0d5d8e6
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/cluster.ts
Expand Up @@ -81,6 +81,8 @@ export type SetClusterMetadataCallback = GenericOperationCallback<
Operation | null | undefined
>;
export interface BasicClusterConfig {
encryption?: google.bigtable.admin.v2.Cluster.IEncryptionConfig;
key?: string;
location: string;
nodes: number;
storage?: string;
Expand Down
19 changes: 19 additions & 0 deletions src/index.ts
Expand Up @@ -577,12 +577,31 @@ export class Bigtable {
);
}

if (
typeof cluster.key !== 'undefined' &&
typeof cluster.encryption !== 'undefined'
) {
throw new Error(
'A cluster was provided with both `encryption` and `key` defined.'
);
}

clusters[cluster.id!] = {
location: Cluster.getLocation_(this.projectId, cluster.location!),
serveNodes: cluster.nodes,
defaultStorageType: Cluster.getStorageType_(cluster.storage!),
};

if (cluster.key) {
clusters[cluster.id!].encryptionConfig = {
kmsKeyName: cluster.key,
};
}

if (cluster.encryption) {
clusters[cluster.id!].encryptionConfig = cluster.encryption;
}

return clusters;
}, {} as {[index: string]: google.bigtable.admin.v2.ICluster});

Expand Down
22 changes: 22 additions & 0 deletions src/instance.ts
Expand Up @@ -349,6 +349,9 @@ Please use the format 'my-instance' or '${bigtable.projectName}/instances/my-ins
* @param {object} options Cluster creation options.
* @param {object} [options.gaxOptions] Request configuration options, outlined
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
* @param {object} [options.encryption] CMEK configuration options.
* @param {string} options.encryption.kmsKeyName The KMS key name.
* @param {string} [options.key] Alias for `options.encryption.kmsKeyName`.
* @param {string} options.location The location where this cluster's nodes
* and storage reside. For best performance clients should be located as
* as close as possible to this cluster. Currently only zones are
Expand Down Expand Up @@ -389,6 +392,25 @@ Please use the format 'my-instance' or '${bigtable.projectName}/instances/my-ins
reqOpts.cluster = {};
}

if (
typeof options.key !== 'undefined' &&
typeof options.encryption !== 'undefined'
) {
throw new Error(
'The cluster cannot have both `encryption` and `key` defined.'
);
}

if (options.key) {
reqOpts.cluster!.encryptionConfig = {
kmsKeyName: options.key,
};
}

if (options.encryption) {
reqOpts.cluster!.encryptionConfig = options.encryption;
}

if (options.location) {
reqOpts.cluster!.location = Cluster.getLocation_(
this.bigtable.projectId,
Expand Down
93 changes: 93 additions & 0 deletions system-test/bigtable.ts
Expand Up @@ -169,6 +169,99 @@ describe('Bigtable', () => {
});
});

describe('CMEK', () => {
let kmsKeyName: string;

const CMEK_INSTANCE = bigtable.instance(generateId('instance'));
const CMEK_CLUSTER = CMEK_INSTANCE.cluster(generateId('cluster'));

const cryptoKeyId = generateId('key');
const keyRingId = generateId('key-ring');
let keyRingsBaseUrl: string;
let cryptoKeyVersionName: string;

before(async () => {
const projectId = await bigtable.auth.getProjectId();
kmsKeyName = `projects/${projectId}/locations/us-central1/keyRings/${keyRingId}/cryptoKeys/${cryptoKeyId}`;
keyRingsBaseUrl = `https://cloudkms.googleapis.com/v1/projects/${projectId}/locations/us-central1/keyRings`;

await bigtable.auth.request({
method: 'POST',
url: keyRingsBaseUrl,
params: {keyRingId},
});

const resp = await bigtable.auth.request({
method: 'POST',
url: `${keyRingsBaseUrl}/${keyRingId}/cryptoKeys`,
params: {cryptoKeyId},
data: {purpose: 'ENCRYPT_DECRYPT'},
});
cryptoKeyVersionName = resp.data.primary.name;

const [_, operation] = await CMEK_INSTANCE.create({
clusters: [
{
id: CMEK_CLUSTER.id,
location: 'us-central1-a',
nodes: 3,
key: kmsKeyName,
},
],
labels: {
time_created: Date.now(),
},
});
await operation.promise();
});

after(async () => {
await bigtable.auth.request({
method: 'POST',
url: `${keyRingsBaseUrl}/${keyRingId}/cryptoKeys/${cryptoKeyId}/cryptoKeyVersions/${cryptoKeyVersionName
.split('/')
.pop()}:destroy`,
params: {name: cryptoKeyVersionName},
});
});

it('should have created an instance', async () => {
const [metadata] = await CMEK_CLUSTER.getMetadata();
assert.deepStrictEqual(metadata.encryptionConfig, {kmsKeyName});
});

it('should create a cluster', async () => {
const cluster = CMEK_INSTANCE.cluster(generateId('cluster'));

const [_, operation] = await cluster.create({
location: 'us-central1-b',
nodes: 3,
key: kmsKeyName,
});
await operation.promise();

const [metadata] = await cluster.getMetadata();
assert.deepStrictEqual(metadata.encryptionConfig, {kmsKeyName});
});

it('should fail if key not provided', async () => {
const cluster = CMEK_INSTANCE.cluster(generateId('cluster'));

try {
const [_, operation] = await cluster.create({
location: 'us-central1-b',
nodes: 3,
});
await operation.promise();
throw new Error('Cluster creation should not have succeeded');
} catch (e) {
assert(
e.message.includes('All clusters must specify the same CMEK key')
);
}
});
});

describe('appProfiles', () => {
it('should retrieve a list of app profiles', async () => {
const [appProfiles] = await INSTANCE.getAppProfiles();
Expand Down
89 changes: 89 additions & 0 deletions test/index.ts
Expand Up @@ -460,6 +460,95 @@ describe('Bigtable', () => {
bigtable.createInstance(INSTANCE_ID, OPTIONS, assert.ifError);
});

it('should handle clusters with a CMEK key', done => {
const key = 'kms-key-name';

FakeCluster.getLocation_ = () => {};
FakeCluster.getStorageType_ = () => {};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
bigtable.request = (config: any) => {
assert.deepStrictEqual(
config.reqOpts.clusters['my-cluster'].encryptionConfig,
{
kmsKeyName: key,
}
);
done();
};

bigtable.createInstance(
INSTANCE_ID,
{
clusters: [
{
id: 'my-cluster',
key,
},
],
},
assert.ifError
);
});

it('should handle clusters with an encryption object', done => {
const key = 'kms-key-name';

FakeCluster.getLocation_ = () => {};
FakeCluster.getStorageType_ = () => {};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
bigtable.request = (config: any) => {
assert.deepStrictEqual(
config.reqOpts.clusters['my-cluster'].encryptionConfig,
{
kmsKeyName: key,
}
);
done();
};

bigtable.createInstance(
INSTANCE_ID,
{
clusters: [
{
id: 'my-cluster',
encryption: {
kmsKeyName: key,
},
},
],
},
assert.ifError
);
});

it('should throw if both an encryption object and a key are provided', () => {
const key = 'kms-key-name';

FakeCluster.getLocation_ = () => {};
FakeCluster.getStorageType_ = () => {};

assert.throws(() => {
bigtable.createInstance(
INSTANCE_ID,
{
clusters: [
{
id: 'my-cluster',
encryption: {
kmsKeyName: key,
},
key,
},
],
},
assert.ifError
);
}, /A cluster was provided with both `encryption` and `key` defined\./);
});

it('should return an error to the callback', done => {
const error = new Error('err');
bigtable.request = (config: {}, callback: Function) => {
Expand Down
48 changes: 48 additions & 0 deletions test/instance.ts
Expand Up @@ -414,6 +414,54 @@ describe('Bigtable/Instance', () => {
instance.createCluster(CLUSTER_ID, options, assert.ifError);
});

it('should respect the key option', done => {
const key = 'kms-key-name';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(instance.bigtable.request as Function) = (config: any) => {
assert.deepStrictEqual(config.reqOpts.cluster.encryptionConfig, {
kmsKeyName: key,
});
done();
};

instance.createCluster(
CLUSTER_ID,
{key} as CreateClusterOptions,
assert.ifError
);
});

it('should handle clusters with an encryption object', done => {
const key = 'kms-key-name';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(instance.bigtable.request as Function) = (config: any) => {
assert.deepStrictEqual(config.reqOpts.cluster.encryptionConfig, {
kmsKeyName: key,
});
done();
};

instance.createCluster(
CLUSTER_ID,
{encryption: {kmsKeyName: key}} as CreateClusterOptions,
assert.ifError
);
});

it('should throw if both an encryption object and a key are provided', () => {
const key = 'kms-key-name';

assert.throws(() => {
instance.createCluster(
CLUSTER_ID,
{encryption: {kmsKeyName: key}, key} as CreateClusterOptions,
assert.ifError
);
}, /The cluster cannot have both `encryption` and `key` defined\./);
});

it('should execute callback with arguments from GAPIC', done => {
const response = {};
sandbox
Expand Down

0 comments on commit 0d5d8e6

Please sign in to comment.