diff --git a/.kokoro/pre-system-test.sh b/.kokoro/pre-system-test.sh index a1ffa0cea..abc85f9d4 100755 --- a/.kokoro/pre-system-test.sh +++ b/.kokoro/pre-system-test.sh @@ -15,3 +15,8 @@ # limitations under the License. . .kokoro/setup-vars.sh + +# Lease a second service account for testing listing with multiple service accounts +export HMAC_KEY_TEST_SECOND_SERVICE_ACCOUNT=$(./gimmeproj -project=$HMAC_PROJECT lease 15m) +# Add to the list of leased service account for clean up after tests +export LEASED_SERVICE_ACCOUNTS="$LEASED_SERVICE_ACCOUNTS $HMAC_KEY_TEST_SECOND_SERVICE_ACCOUNT" diff --git a/.kokoro/setup-vars.sh b/.kokoro/setup-vars.sh index 0455845df..881b62611 100644 --- a/.kokoro/setup-vars.sh +++ b/.kokoro/setup-vars.sh @@ -22,3 +22,21 @@ export GCN_STORAGE_2ND_PROJECT_KEY=${KOKORO_GFILE_DIR}/no-whitelist-key.json export GOOGLE_CLOUD_KMS_KEY_ASIA="projects/long-door-651/locations/asia/keyRings/test-key-asia/cryptoKeys/test-key-asia" export GOOGLE_CLOUD_KMS_KEY_US="projects/long-door-651/locations/us/keyRings/test-key-us/cryptoKeys/test-key-us" + +# For testing SA HMAC +export HMAC_PROJECT=gimme-acc +curl https://storage.googleapis.com/gimme-proj/linux_amd64/gimmeproj > gimmeproj +chmod +x gimmeproj +./gimmeproj version + +export HMAC_KEY_TEST_SERVICE_ACCOUNT=$(./gimmeproj -project=$HMAC_PROJECT lease 15m) +echo Leased service account: $HMAC_KEY_TEST_SERVICE_ACCOUNT +export LEASED_SERVICE_ACCOUNTS=$HMAC_KEY_TEST_SERVICE_ACCOUNT + +cleanup_service_accounts () { + for i in $LEASED_SERVICE_ACCOUNTS; do + ./gimmeproj -project=$HMAC_PROJECT "done" $i + done +} + +trap cleanup_service_accounts EXIT diff --git a/README.md b/README.md index adcc18c92..c6464afad 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,12 @@ has instructions for running the samples. | Buckets | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/buckets.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/buckets.js,samples/README.md) | | Encryption | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/encryption.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/encryption.js,samples/README.md) | | Files | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/files.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/files.js,samples/README.md) | +| Activate HMAC SA Key. | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyActivate.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyActivate.js,samples/README.md) | +| Create HMAC SA Key. | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyCreate.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyCreate.js,samples/README.md) | +| Deactivate HMAC SA Key. | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyDeactivate.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyDeactivate.js,samples/README.md) | +| Delete HMAC SA Key. | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyDelete.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyDelete.js,samples/README.md) | +| Get HMAC SA Key Metadata. | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyGet.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyGet.js,samples/README.md) | +| List HMAC SA Keys Metadata. | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeysList.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeysList.js,samples/README.md) | | Iam | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/iam.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/iam.js,samples/README.md) | | Notifications | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/notifications.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/notifications.js,samples/README.md) | | Quickstart | [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/quickstart.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/quickstart.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 6eec5ccd7..78ea557f5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -24,6 +24,12 @@ objects to users via direct download. * [Buckets](#buckets) * [Encryption](#encryption) * [Files](#files) + * [Activate HMAC SA Key.](#activate-hmac-sa-key.) + * [Create HMAC SA Key.](#create-hmac-sa-key.) + * [Deactivate HMAC SA Key.](#deactivate-hmac-sa-key.) + * [Delete HMAC SA Key.](#delete-hmac-sa-key.) + * [Get HMAC SA Key Metadata.](#get-hmac-sa-key-metadata.) + * [List HMAC SA Keys Metadata.](#list-hmac-sa-keys-metadata.) * [Iam](#iam) * [Notifications](#notifications) * [Quickstart](#quickstart) @@ -142,6 +148,120 @@ __Usage:__ +### Activate HMAC SA Key. + +Activate HMAC SA Key. + +View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyActivate.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyActivate.js,samples/README.md) + +__Usage:__ + + +`node hmacKeyActivate.js [projectId]` + + +----- + + + + +### Create HMAC SA Key. + +Create HMAC SA Key. + +View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyCreate.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyCreate.js,samples/README.md) + +__Usage:__ + + +`node hmacKeyCreate.js [projectId]` + + +----- + + + + +### Deactivate HMAC SA Key. + +Deactivate HMAC SA Key. + +View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyDeactivate.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyDeactivate.js,samples/README.md) + +__Usage:__ + + +`node hmacKeyDeactivate.js [projectId]` + + +----- + + + + +### Delete HMAC SA Key. + +Delete HMAC SA Key. + +View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyDelete.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyDelete.js,samples/README.md) + +__Usage:__ + + +`node hmacKeyDelete.js [projectId]` + + +----- + + + + +### Get HMAC SA Key Metadata. + +Get HMAC SA Key Metadata. + +View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeyGet.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeyGet.js,samples/README.md) + +__Usage:__ + + +`node hmacKeyGet.js [projectId]` + + +----- + + + + +### List HMAC SA Keys Metadata. + +List HMAC SA Keys Metadata. + +View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/hmacKeysList.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-storage&page=editor&open_in_editor=samples/hmacKeysList.js,samples/README.md) + +__Usage:__ + + +`node hmacKeyList.js [projectId]` + + +----- + + + + ### Iam View the [source code](https://github.com/googleapis/nodejs-storage/blob/master/samples/iam.js). diff --git a/samples/hmacKeyActivate.js b/samples/hmacKeyActivate.js new file mode 100644 index 000000000..09a2fe4d4 --- /dev/null +++ b/samples/hmacKeyActivate.js @@ -0,0 +1,56 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// sample-metadata: +// title: Activate HMAC SA Key. +// description: Activate HMAC SA Key. +// usage: node hmacKeyActivate.js [projectId] + +function main( + hmacKeyAccessId = 'GOOG0234230X00', + projectId = 'serviceAccountProjectId' +) { + // [START storage_activate_hmac_key] + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // Activate HMAC SA Key + async function activateHmacKey() { + /** + * TODO(developer): Uncomment the following line before running the sample. + */ + // const hmacKeyAccessId = 'HMAC Access Key Id to update, e.g. GOOG0234230X00'; + // const projectId = 'The project Id this service account belongs to, e.g. serviceAccountProjectId'; + + const hmacKey = storage.hmacKey(hmacKeyAccessId, {projectId}); + const [hmacKeyMetadata] = await hmacKey.setMetadata({state: 'ACTIVE'}); + + console.log(`The HMAC key is now active.`); + console.log(`The HMAC key metadata is:`); + for (const [key, value] of Object.entries(hmacKeyMetadata)) { + console.log(`${key}: ${value}`); + } + } + // [END storage_activate_hmac_key] + activateHmacKey(); +} + +main(...process.argv.slice(2)); diff --git a/samples/hmacKeyCreate.js b/samples/hmacKeyCreate.js new file mode 100644 index 000000000..3ae1ac79e --- /dev/null +++ b/samples/hmacKeyCreate.js @@ -0,0 +1,58 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// sample-metadata: +// title: Create HMAC SA Key. +// description: Create HMAC SA Key. +// usage: node hmacKeyCreate.js [projectId] + +function main( + serviceAccountEmail = 'service-account@example.com', + projectId = 'serviceAccountProjectId' +) { + // [START storage_create_hmac_key] + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // Create HMAC SA Key + async function createHmacKey() { + /** + * TODO(developer): Uncomment the following line before running the sample. + */ + // const serviceAccountEmail = 'Service Account Email to associate HMAC Key'; + // const projectId = 'The project Id this service account to be created in, e.g. serviceAccountProjectId'; + + const [hmacKey, secret] = await storage.createHmacKey(serviceAccountEmail, { + projectId, + }); + + console.log(`The base64 encoded secret is: ${secret}`); + console.log(`Do not miss that secret, there is no API to recover it.`); + console.log(`The HMAC key metadata is:`); + for (const [key, value] of Object.entries(hmacKey.metadata)) { + console.log(`${key}: ${value}`); + } + } + // [END storage_create_hmac_key] + createHmacKey(); +} + +main(...process.argv.slice(2)); diff --git a/samples/hmacKeyDeactivate.js b/samples/hmacKeyDeactivate.js new file mode 100644 index 000000000..4c05ff59e --- /dev/null +++ b/samples/hmacKeyDeactivate.js @@ -0,0 +1,56 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// sample-metadata: +// title: Deactivate HMAC SA Key. +// description: Deactivate HMAC SA Key. +// usage: node hmacKeyDeactivate.js [projectId] + +function main( + hmacKeyAccessId = 'GOOG0234230X00', + projectId = 'serviceAccountProjectId' +) { + // [START storage_deactivate_hmac_key] + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // Deactivate HMAC SA Key + async function deactivateHmacKey() { + /** + * TODO(developer): Uncomment the following line before running the sample. + */ + // const hmacKeyAccessId = 'HMAC Access Key Id to update, e.g. GOOG0234230X00'; + // const projectId = 'The project Id this service account belongs to, e.g. serviceAccountProjectId'; + + const hmacKey = storage.hmacKey(hmacKeyAccessId, {projectId}); + const [hmacKeyMetadata] = await hmacKey.setMetadata({state: 'INACTIVE'}); + + console.log(`The HMAC key is now inactive.`); + console.log(`The HMAC key metadata is:`); + for (const [key, value] of Object.entries(hmacKeyMetadata)) { + console.log(`${key}: ${value}`); + } + } + // [END storage_deactivate_hmac_key] + deactivateHmacKey(); +} + +main(...process.argv.slice(2)); diff --git a/samples/hmacKeyDelete.js b/samples/hmacKeyDelete.js new file mode 100644 index 000000000..bb1942825 --- /dev/null +++ b/samples/hmacKeyDelete.js @@ -0,0 +1,54 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// sample-metadata: +// title: Delete HMAC SA Key. +// description: Delete HMAC SA Key. +// usage: node hmacKeyDelete.js [projectId] + +function main( + hmacKeyAccessId = 'GOOG0234230X00', + projectId = 'serviceAccountProjectId' +) { + // [START storage_delete_hmac_key] + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // Delete HMAC SA Key + async function deleteHmacKey() { + /** + * TODO(developer): Uncomment the following line before running the sample. + */ + // const hmacKeyAccessId = 'Inactive HMAC Access Key Id to delete, e.g. GOOG0234230X00'; + // const projectId = 'The project Id this service account belongs to, e.g. serviceAccountProjectId'; + + const hmacKey = storage.hmacKey(hmacKeyAccessId, {projectId}); + await hmacKey.delete(); + + console.log( + `The key is deleted, though it may still appear in getHmacKeys() results.` + ); + } + // [END storage_delete_hmac_key] + deleteHmacKey(); +} + +main(...process.argv.slice(2)); diff --git a/samples/hmacKeyGet.js b/samples/hmacKeyGet.js new file mode 100644 index 000000000..b5954e98a --- /dev/null +++ b/samples/hmacKeyGet.js @@ -0,0 +1,56 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// sample-metadata: +// title: Get HMAC SA Key Metadata. +// description: Get HMAC SA Key Metadata. +// usage: node hmacKeyGet.js [projectId] + +function main( + hmacKeyAccessId = 'GOOG0234230X00', + projectId = 'serviceAccountProjectId' +) { + // [START storage_get_hmac_key] + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // Get HMAC SA Key Metadata + async function getHmacKey() { + /** + * TODO(developer): Uncomment the following line before running the sample. + */ + // const hmacKeyAccessId = 'HMAC Access Key Id to get, e.g. GOOG0234230X00'; + // const projectId = 'The project Id this service account belongs to, e.g. serviceAccountProjectId'; + + const hmacKey = storage.hmacKey(hmacKeyAccessId, {projectId}); + // Populate the hmacKey object with metadata from server. + await hmacKey.getMetadata(); + + console.log(`The HMAC key metadata is:`); + for (const [key, value] of Object.entries(hmacKey.metadata)) { + console.log(`${key}: ${value}`); + } + } + // [END storage_get_hmac_key] + getHmacKey(); +} + +main(...process.argv.slice(2)); diff --git a/samples/hmacKeysList.js b/samples/hmacKeysList.js new file mode 100644 index 000000000..0770eaea8 --- /dev/null +++ b/samples/hmacKeysList.js @@ -0,0 +1,52 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// sample-metadata: +// title: List HMAC SA Keys Metadata. +// description: List HMAC SA Keys Metadata. +// usage: node hmacKeyList.js [projectId] + +function main(projectId = 'serviceAccountProjectId') { + // [START storage_list_hmac_keys] + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + // List HMAC SA Keys' Metadata + async function listHmacKeys() { + /** + * TODO(developer): Uncomment the following line before running the sample. + */ + // const projectId = 'The project Id this service account belongs to, e.g. serviceAccountProjectId'; + const [hmacKeys] = await storage.getHmacKeys({projectId}); + + // hmacKeys is an array of HmacKey objects. + for (const hmacKey of hmacKeys) { + console.log( + `Service Account Email: ${hmacKey.metadata.serviceAccountEmail}` + ); + console.log(`Access Id: ${hmacKey.metadata.accessId}`); + } + } + // [END storage_list_hmac_keys] + listHmacKeys(); +} + +main(...process.argv.slice(2)); diff --git a/samples/system-test/hmacKey.test.js b/samples/system-test/hmacKey.test.js new file mode 100644 index 000000000..4c2070564 --- /dev/null +++ b/samples/system-test/hmacKey.test.js @@ -0,0 +1,103 @@ +/** + * Copyright 2019 Google LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const {Storage} = require(`@google-cloud/storage`); +const {assert} = require('chai'); +const cp = require('child_process'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const storage = new Storage(); +const SERVICE_ACCOUNT_EMAIL = process.env.HMAC_KEY_TEST_SERVICE_ACCOUNT; +const SERVICE_ACCOUNT_PROJECT = process.env.HMAC_PROJECT; + +describe('HMAC SA Key samples', () => { + let hmacKey; + + before(async () => { + await cleanUpHmacKeys(SERVICE_ACCOUNT_EMAIL, SERVICE_ACCOUNT_PROJECT); + [hmacKey] = await storage.createHmacKey(SERVICE_ACCOUNT_EMAIL, { + projectId: SERVICE_ACCOUNT_PROJECT, + }); + }); + + after(async () => { + await cleanUpHmacKeys(SERVICE_ACCOUNT_EMAIL, SERVICE_ACCOUNT_PROJECT); + }); + + it('should create an HMAC Key', async () => { + const output = execSync( + `node hmacKeyCreate.js ${SERVICE_ACCOUNT_EMAIL} ${SERVICE_ACCOUNT_PROJECT}` + ); + assert.include(output, 'The base64 encoded secret is:'); + }); + + it('should list HMAC Keys', async () => { + const output = execSync(`node hmacKeysList.js ${SERVICE_ACCOUNT_PROJECT}`); + assert.include(output, `Service Account Email: ${SERVICE_ACCOUNT_EMAIL}`); + }); + + it('should get HMAC Key', async () => { + const output = execSync( + `node hmacKeyGet.js ${hmacKey.metadata.accessId} ${SERVICE_ACCOUNT_PROJECT}` + ); + assert.include(output, 'The HMAC key metadata is:'); + }); + + it('should deactivate HMAC Key', async () => { + const output = execSync( + `node hmacKeyDeactivate.js ${hmacKey.metadata.accessId} ${SERVICE_ACCOUNT_PROJECT}` + ); + assert.include(output, 'The HMAC key is now inactive.'); + }); + + it('should activate HMAC Key', async () => { + const output = execSync( + `node hmacKeyActivate.js ${hmacKey.metadata.accessId} ${SERVICE_ACCOUNT_PROJECT}` + ); + assert.include(output, 'The HMAC key is now active.'); + }); + + it(`should delete HMAC key`, async () => { + // Deactivate then delete + execSync( + `node hmacKeyDeactivate.js ${hmacKey.metadata.accessId} ${SERVICE_ACCOUNT_PROJECT}` + ); + const output = execSync( + `node hmacKeyDelete.js ${hmacKey.metadata.accessId} ${SERVICE_ACCOUNT_PROJECT}` + ); + assert.include( + output, + `The key is deleted, though it may still appear in getHmacKeys() results.` + ); + }); +}); + +async function cleanUpHmacKeys(serviceAccountEmail, projectId) { + // list all HMAC keys for the given service account. + const [hmacKeys] = await storage.getHmacKeys({ + projectId, + serviceAccountEmail: serviceAccountEmail, + }); + // deactivate and delete the key + for (const hmacKey of hmacKeys) { + if (hmacKey.metadata.state === 'ACTIVE') { + await hmacKey.setMetadata({state: 'INACTIVE'}); + } + await hmacKey.delete(); + } +} diff --git a/src/hmacKey.ts b/src/hmacKey.ts new file mode 100644 index 000000000..3cfa2d1f9 --- /dev/null +++ b/src/hmacKey.ts @@ -0,0 +1,326 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Metadata, ServiceObject, Methods} from '@google-cloud/common'; +import {Storage} from './storage'; + +export interface HmacKeyOptions { + projectId?: string; +} + +export interface HmacKeyMetadata { + accessId: string; + etag?: string; + id?: string; + projectId?: string; + serviceAccountEmail?: string; + state?: string; + timeCreated?: string; + updated?: string; +} + +export interface SetHmacKeyMetadataOptions { + /** + * This parameter is currently ignored. + */ + userProject?: string; +} + +export interface SetHmacKeyMetadata { + state?: 'ACTIVE' | 'INACTIVE'; + etag?: string; +} + +export interface HmacKeyMetadataCallback { + (err: Error | null, metadata?: HmacKeyMetadata, apiResponse?: Metadata): void; +} + +export type HmacKeyMetadataResponse = [HmacKeyMetadata, Metadata]; + +/** + * An HmacKey object contains metadata of an HMAC key created from a + * service account through the {@link Storage} client using + * {@link Storage#createHmacKey}. + * + * @see [HMAC keys documentation]{@link https://cloud.google.com/storage/docs/authentication/hmackeys} + * + * @class + */ +export class HmacKey extends ServiceObject { + metadata: HmacKeyMetadata | undefined; + + /** + * @typedef {object} HmacKeyOptions + * @property {string} [projectId] The project ID of the project that owns + * the service account of the requested HMAC key. If not provided, + * the project ID used to instantiate the Storage client will be used. + */ + /** + * Constructs an HmacKey object. + * + * Note: this only create a local reference to an HMAC key, to create + * an HMAC key, use {@link Storage#createHmacKey}. + * + * @param {Storage} storage The Storage instance this HMAC key is + * attached to. + * @param {string} accessId The unique accessId for this HMAC key. + * @param {HmacKeyOptions} options Constructor configurations. + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * const hmacKey = storage.hmacKey('access-id'); + */ + constructor(storage: Storage, accessId: string, options?: HmacKeyOptions) { + const methods = { + /** + * @typedef {object} DeleteHmacKeyOptions + * @property {string} [userProject] This parameter is currently ignored. + */ + /** + * @typedef {array} DeleteHmacKeyResponse + * @property {object} 0 The full API response. + */ + /** + * @callback DeleteHmacKeyCallback + * @param {?Error} err Request error, if any. + * @param {object} apiResponse The full API response. + */ + /** + * Deletes an HMAC key. + * Key state must be set to `INACTIVE` prior to deletion. + * Caution: HMAC keys cannot be recovered once you delete them. + * + * The authenticated user must have `storage.hmacKeys.delete` permission for the project in which the key exists. + * + * @method HmacKey#delete + * @param {DeleteHmacKeyOptions} [options] Configuration options. + * @param {DeleteHmacKeyCallback} [callback] Callback function. + * @returns {Promise} + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * + * //- + * // Delete HMAC key after making the key inactive. + * //- + * const hmacKey = storage.hmacKey('ACCESS_ID'); + * hmacKey.setMetadata({state: 'INACTIVE'}, (err, hmacKeyMetadata) => { + * if (err) { + * // The request was an error. + * console.error(err); + * return; + * } + * hmacKey.delete((err) => { + * if (err) { + * console.error(err); + * return; + * } + * // The HMAC key is deleted. + * }); + * }); + * + * //- + * // If the callback is omitted, a promise is returned. + * //- + * const hmacKey = storage.hmacKey('ACCESS_ID'); + * hmacKey + * .setMetadata({state: 'INACTIVE'}) + * .then(() => { + * return hmacKey.delete(); + * }); + */ + delete: true, + /** + * @callback GetHmacKeyCallback + * @param {?Error} err Request error, if any. + * @param {HmacKey} hmacKey this {@link HmacKey} instance. + * @param {object} apiResponse The full API response. + */ + /** + * @typedef {array} GetHmacKeyResponse + * @property {HmacKey} 0 This {@link HmacKey} instance. + * @property {object} 1 The full API response. + */ + /** + * @typedef {object} GetHmacKeyOptions + * @property {string} [userProject] This parameter is currently ignored. + */ + /** + * Retrieves and populate an HMAC key's metadata, and return + * this {@link HmacKey} instance. + * + * HmacKey.get() does not give the HMAC key secret, as + * it is only returned on creation. + * + * The authenticated user must have `storage.hmacKeys.get` permission + * for the project in which the key exists. + * + * @method HmacKey#get + * @param {GetHmacKeyOptions} [options] Configuration options. + * @param {GetHmacKeyCallback} [callback] Callback function. + * @returns {Promise} + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * + * //- + * // Get the HmacKey's Metadata. + * //- + * storage.hmacKey('ACCESS_ID') + * .get((err, hmacKey) => { + * if (err) { + * // The request was an error. + * console.error(err); + * return; + * } + * // do something with the returned HmacKey object. + * }); + * + * //- + * // If the callback is omitted, a promise is returned. + * //- + * storage.hmacKey('ACCESS_ID') + * .get() + * .then((data) => { + * const hmacKey = data[0]; + * }); + */ + get: true, + /** + * @typedef {object} GetHmacKeyMetadataOptions + * @property {string} [userProject] This parameter is currently ignored. + */ + /** + * Retrieves and populate an HMAC key's metadata, and return + * the HMAC key's metadata as an object. + * + * HmacKey.getMetadata() does not give the HMAC key secret, as + * it is only returned on creation. + * + * The authenticated user must have `storage.hmacKeys.get` permission + * for the project in which the key exists. + * + * @method HmacKey#getMetadata + * @param {GetHmacKeyMetadataOptions} [options] Configuration options. + * @param {HmacKeyMetadataCallback} [callback] Callback function. + * @returns {Promise} + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * + * //- + * // Get the HmacKey's metadata and populate to the metadata property. + * //- + * storage.hmacKey('ACCESS_ID') + * .getMetadata((err, hmacKeyMetadata) => { + * if (err) { + * // The request was an error. + * console.error(err); + * return; + * } + * console.log(hmacKeyMetadata); + * }); + * + * //- + * // If the callback is omitted, a promise is returned. + * //- + * storage.hmacKey('ACCESS_ID') + * .getMetadata() + * .then((data) => { + * const hmacKeyMetadata = data[0]; + * console.log(hmacKeyMetadata); + * }); + */ + getMetadata: true, + /** + * @typedef {object} SetHmacKeyMetadata Subset of {@link HmacKeyMetadata} to update. + * @property {string} state New state of the HmacKey. Either 'ACTIVE' or 'INACTIVE'. + * @property {string} [etag] Include an etag from a previous get HMAC key request + * to perform safe read-modify-write. + */ + /** + * @typedef {object} SetHmacKeyMetadataOptions + * @property {string} [userProject] This parameter is currently ignored. + */ + /** + * @callback HmacKeyMetadataCallback + * @param {?Error} err Request error, if any. + * @param {HmacKeyMetadata} metadata The updated {@link HmacKeyMetadata} object. + * @param {object} apiResponse The full API response. + */ + /** + * @typedef {array} HmacKeyMetadataResponse + * @property {HmacKeyMetadata} 0 The updated {@link HmacKeyMetadata} object. + * @property {object} 1 The full API response. + */ + /** + * Updates the state of an HMAC key. See {@link SetHmacKeyMetadata} for + * valid states. + * + * @method HmacKey#setMetadata + * @param {SetHmacKeyMetadata} metadata The new metadata. + * @param {SetHmacKeyMetadataOptions} [options] Configuration options. + * @param {HmacKeyMetadataCallback} [callback] Callback function. + * @returns {Promise} + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * + * const metadata = { + * state: 'INACTIVE', + * }; + * + * storage.hmacKey('ACCESS_ID') + * .setMetadata(metadata, (err, hmacKeyMetadata) => { + * if (err) { + * // The request was an error. + * console.error(err); + * return; + * } + * console.log(hmacKeyMetadata); + * }); + * + * //- + * // If the callback is omitted, a promise is returned. + * //- + * storage.hmacKey('ACCESS_ID') + * .setMetadata(metadata) + * .then((data) => { + * const hmacKeyMetadata = data[0]; + * console.log(hmacKeyMetadata); + * }); + */ + setMetadata: { + reqOpts: { + method: 'PUT', + }, + }, + } as Methods; + + const projectId = (options && options.projectId) || storage.projectId; + + super({ + parent: storage, + id: accessId, + baseUrl: `/projects/${projectId}/hmacKeys`, + methods, + }); + } +} diff --git a/src/index.ts b/src/index.ts index 3d003d3b5..0b8a3a36c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2014-2017 Google Inc. All Rights Reserved. + * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -181,6 +181,14 @@ export { SetStorageClassOptions, SetStorageClassResponse, } from './file'; +export { + HmacKey, + HmacKeyMetadata, + HmacKeyMetadataCallback, + HmacKeyMetadataResponse, + SetHmacKeyMetadata, + SetHmacKeyMetadataOptions, +} from './hmacKey'; export { GetPolicyCallback, GetPolicyOptions, @@ -210,12 +218,20 @@ export { BucketOptions, CreateBucketQuery, CreateBucketRequest, + CreateBucketResponse, + CreateHmacKeyCallback, + CreateHmacKeyOptions, + CreateHmacKeyResponse, GetBucketsCallback, GetBucketsRequest, GetBucketsResponse, + GetHmacKeysCallback, + GetHmacKeysOptions, + GetHmacKeysResponse, GetServiceAccountCallback, GetServiceAccountOptions, GetServiceAccountResponse, + HmacKeyResourceResponse, ServiceAccount, Storage, StorageOptions, diff --git a/src/storage.ts b/src/storage.ts index 4f424e6a3..b938d40dd 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,5 +1,5 @@ /** - * Copyright 2014-2017 Google Inc. All Rights Reserved. + * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import {Bucket} from './bucket'; import {Channel} from './channel'; import {File} from './file'; import {normalize} from './util'; +import {HmacKey, HmacKeyMetadata, HmacKeyOptions} from './hmacKey'; export interface GetServiceAccountOptions { userProject?: string; @@ -99,6 +100,49 @@ export interface GetBucketsRequest { userProject?: string; } +export interface HmacKeyResourceResponse { + metadata: HmacKeyMetadata; + secret: string; +} + +export type CreateHmacKeyResponse = [HmacKey, string, HmacKeyResourceResponse]; + +export interface CreateHmacKeyOptions { + projectId?: string; + userProject?: string; +} + +export interface CreateHmacKeyCallback { + ( + err: Error | null, + hmacKey?: HmacKey | null, + secret?: string | null, + apiResponse?: HmacKeyResourceResponse + ): void; +} + +export interface GetHmacKeysOptions { + projectId?: string; + serviceAccountEmail?: string; + showDeletedKeys?: boolean; + autoPaginate?: boolean; + maxApiCalls?: number; + maxResults?: number; + pageToken?: string; + userProject?: string; +} + +export interface GetHmacKeysCallback { + ( + err: Error | null, + hmacKeys: HmacKey[] | null, + nextQuery?: {}, + apiResponse?: Metadata + ): void; +} + +export type GetHmacKeysResponse = [HmacKey[]]; + /*! Developer Documentation * * Invoke this method to create a new Storage object bound with pre-determined @@ -148,6 +192,15 @@ export class Storage extends Service { */ static File: typeof File = File; + /** + * {@link HmacKey} class. + * + * @name Storage.HmacKey + * @see HmacKey + * @type {Constructor} + */ + static HmacKey: typeof HmacKey = HmacKey; + /** * Cloud Storage uses access control lists (ACLs) to manage object and * bucket access. ACLs are the mechanism you use to share objects with other @@ -247,6 +300,35 @@ export class Storage extends Service { */ getBucketsStream: () => Readable; + /** + * Get {@link HmacKey} objects for all of the HMAC keys in the project in + * a readable object stream. + * + * @method Storage#getHmacKeysStream + * @param {GetHmacKeysOptions} [options] Configuration options. + * @returns {ReadableStream} A readable stream that emits {@link HmacKey} instances. + * + * @example + * storage.getHmacKeysStream() + * .on('error', console.error) + * .on('data', function(hmacKey) { + * // hmacKey is an HmacKey object. + * }) + * .on('end', function() { + * // All HmacKey retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * storage.getHmacKeysStream() + * .on('data', function(bucket) { + * this.end(); + * }); + */ + getHmacKeysStream: () => Readable; + /** * @typedef {object} StorageOptions * @property {string} [projectId] The project ID from the Google Developer's @@ -317,6 +399,7 @@ export class Storage extends Service { this.acl = Storage.acl; this.getBucketsStream = paginator.streamify('getBuckets'); + this.getHmacKeysStream = paginator.streamify('getHmacKeys'); } /** @@ -553,6 +636,132 @@ export class Storage extends Service { ); } + createHmacKey( + serviceAccountEmail: string, + options?: CreateHmacKeyOptions + ): Promise; + createHmacKey( + serviceAccountEmail: string, + callback: CreateHmacKeyCallback + ): void; + createHmacKey( + serviceAccountEmail: string, + options: CreateHmacKeyOptions, + callback: CreateHmacKeyCallback + ): void; + /** + * @typedef {object} CreateHmacKeyOptions + * @property {string} [projectId] The project ID of the project that owns + * the service account of the requested HMAC key. If not provided, + * the project ID used to instantiate the Storage client will be used. + * @property {string} [userProject] This parameter is currently ignored. + */ + /** + * @typedef {object} HmacKeyMetadata + * @property {string} accessId The access id identifies which HMAC key was + * used to sign a request when authenticating with HMAC. + * @property {string} etag Used to perform a read-modify-write of the key. + * @property {string} id The resource name of the HMAC key. + * @property {string} projectId The project ID. + * @property {string} serviceAccountEmail The service account's email this + * HMAC key is created for. + * @property {string} state The state of this HMAC key. One of "ACTIVE", + * "INACTIVE" or "DELETED". + * @property {string} timeCreated The creation time of the HMAC key in + * RFC 3339 format. + * @property {string} [updated] The time this HMAC key was last updated in + * RFC 3339 format. + */ + /** + * @typedef {array} CreateHmacKeyResponse + * @property {HmacKey} 0 The HmacKey instance created from API response. + * @property {string} 1 The HMAC key's secret used to access the XML API. + * @property {object} 3 The raw API response. + */ + /** + * @callback CreateHmacKeyCallback Callback function. + * @param {?Error} err Request error, if any. + * @param {HmacKey} hmacKey The HmacKey instance created from API response. + * @param {string} secret The HMAC key's secret used to access the XML API. + * @param {object} apiResponse The raw API response. + */ + /** + * Create an HMAC key associated with an service account to authenticate + * requests to the Cloud Storage XML API. + * + * @see [HMAC keys documentation]{@link https://cloud.google.com/storage/docs/authentication/hmackeys} + * + * @param {string} serviceAccountEmail The service account's email address + * with which the HMAC key is created for. + * @param {CreateHmacKeyCallback} [callback] Callback function. + * @return {Promise} + * + * @example + * const {Storage} = require('google-cloud/storage'); + * const storage = new Storage(); + * + * // Replace with your service account's email address + * const serviceAccountEmail = + * 'my-service-account@appspot.gserviceaccount.com'; + * + * storage.createHmacKey(serviceAccountEmail, function(err, hmacKey, secret) { + * if (!err) { + * // Securely store the secret for use with the XML API. + * } + * }); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * storage.createHmacKey(serviceAccountEmail) + * .then((response) => { + * const hmacKey = response[0]; + * const secret = response[1]; + * // Securely store the secret for use with the XML API. + * }); + */ + createHmacKey( + serviceAccountEmail: string, + optionsOrCb?: CreateHmacKeyOptions | CreateHmacKeyCallback, + cb?: CreateHmacKeyCallback + ): Promise | void { + if (typeof serviceAccountEmail !== 'string') { + throw new Error( + 'The first argument must be a service account email to create an HMAC key.' + ); + } + + const {options, callback} = normalize< + CreateHmacKeyOptions, + CreateHmacKeyCallback + >(optionsOrCb, cb); + const query = Object.assign({}, options, {serviceAccountEmail}); + const projectId = query.projectId || this.projectId; + delete query.projectId; + + this.request( + { + method: 'POST', + uri: `/projects/${projectId}/hmacKeys`, + qs: query, + }, + (err, resp: HmacKeyResourceResponse) => { + if (err) { + callback!(err, null, null, resp); + return; + } + + const metadata = resp.metadata; + const hmacKey = this.hmacKey(metadata.accessId, { + projectId: metadata.projectId, + }); + hmacKey.metadata = resp.metadata; + + callback!(null, hmacKey, resp.secret, resp); + } + ); + } + getBuckets(options?: GetBucketsRequest): Promise; getBuckets(options: GetBucketsRequest, callback: GetBucketsCallback): void; getBuckets(callback: GetBucketsCallback): void; @@ -667,6 +876,122 @@ export class Storage extends Service { ); } + /** + * Query object for listing HMAC keys. + * + * @typedef {object} GetHmacKeysOptions + * @property {string} [projectId] The project ID of the project that owns + * the service account of the requested HMAC key. If not provided, + * the project ID used to instantiate the Storage client will be used. + * @property {string} [serviceAccountEmail] If present, only HMAC keys for the + * given service account are returned. + * @property {boolean} [showDeletedKeys=false] If true, include keys in the DELETE + * state. Default is false. + * @property {boolean} [autoPaginate=true] Have pagination handled + * automatically. + * @property {number} [maxApiCalls] Maximum number of API calls to make. + * @property {number} [maxResults] Maximum number of items plus prefixes to + * return. + * @property {string} [pageToken] A previously-returned page token + * representing part of the larger set of results to view. + * @property {string} [userProject] This parameter is currently ignored. + */ + /** + * @typedef {array} GetHmacKeysResponse + * @property {HmacKey[]} 0 Array of {@link HmacKey} instances. + */ + /** + * @callback GetHmacKeysCallback + * @param {?Error} err Request error, if any. + * @param {HmacKey[]} hmacKeys Array of {@link HmacKey} instances. + */ + /** + * Retrieves a list of HMAC keys matching the criteria. + * + * The authenticated user must have storage.hmacKeys.list permission for the project in which the key exists. + * + * @param {GetHmacKeysOption} options Configuration options. + * @param {GetHmacKeysCallback} callback Callback function. + * @return {Promise} + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * storage.getHmacKeys(function(err, hmacKeys) { + * if (!err) { + * // hmacKeys is an array of HmacKey objects. + * } + * }); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * const callback = function(err, hmacKeys, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * storage.getHmacKeys(nextQuery, callback); + * } + * + * // The `metadata` property is populated for you with the metadata at the + * // time of fetching. + * hmacKeys[0].metadata; + * }; + * + * storage.getHmacKeys({ + * autoPaginate: false + * }, callback); + * + * //- + * // If the callback is omitted, we'll return a Promise. + * //- + * storage.getHmacKeys().then(function(data) { + * const hmacKeys = data[0]; + * }); + */ + getHmacKeys(options?: GetHmacKeysOptions): Promise; + getHmacKeys(callback: GetHmacKeysCallback): void; + getHmacKeys(options: GetHmacKeysOptions, callback: GetHmacKeysCallback): void; + getHmacKeys( + optionsOrCb?: GetHmacKeysOptions | GetHmacKeysCallback, + cb?: GetHmacKeysCallback + ): Promise | void { + const {options, callback} = normalize(optionsOrCb, cb); + const query = Object.assign({}, options); + const projectId = query.projectId || this.projectId; + delete query.projectId; + + this.request( + { + uri: `/projects/${projectId}/hmacKeys`, + qs: query, + }, + (err, resp) => { + if (err) { + callback(err, null, null, resp); + return; + } + + const hmacKeys = arrify(resp.items).map((hmacKey: HmacKeyMetadata) => { + const hmacKeyInstance = this.hmacKey(hmacKey.accessId, { + projectId: hmacKey.projectId, + }); + hmacKeyInstance.metadata = hmacKey; + return hmacKeyInstance; + }); + + const nextQuery = resp.nextPageToken + ? Object.assign({}, options, {pageToken: resp.nextPageToken}) + : null; + + callback(null, hmacKeys, nextQuery, resp); + } + ); + } + + getServiceAccount( + options?: GetServiceAccountOptions + ): Promise; getServiceAccount( options?: GetServiceAccountOptions ): Promise; @@ -757,13 +1082,40 @@ export class Storage extends Service { } ); } + + /** + * Get a reference to an HmacKey object. + * Note: this does not fetch the HMAC key's metadata. Use HmacKey#get() to + * retrieve and populate the metadata. + * + * To get a reference to an HMAC key that's not created for a service + * account in the same project used to instantiate the Storage client, + * supply the project's ID as `projectId` in the `options` argument. + * + * @param {string} accessId The HMAC key's access ID. + * @param {HmacKeyOptions} options HmacKey constructor owptions. + * @returns {HmacKey} + * @see HmacKey + * + * @example + * const {Storage} = require('@google-cloud/storage'); + * const storage = new Storage(); + * const hmacKey = storage.hmacKey('ACCESS_ID'); + */ + hmacKey(accessId: string, options?: HmacKeyOptions) { + if (!accessId) { + throw new Error('An access ID is needed to create an HmacKey object.'); + } + + return new HmacKey(this, accessId, options); + } } /*! Developer Documentation * * These methods can be auto-paginated. */ -paginator.extend(Storage, 'getBuckets'); +paginator.extend(Storage, ['getBuckets', 'getHmacKeys']); /*! Developer Documentation * @@ -771,5 +1123,5 @@ paginator.extend(Storage, 'getBuckets'); * that a callback is omitted. */ promisifyAll(Storage, { - exclude: ['bucket', 'channel'], + exclude: ['bucket', 'channel', 'hmacKey'], }); diff --git a/synth.metadata b/synth.metadata index 3abf3e198..bf13af7bb 100644 --- a/synth.metadata +++ b/synth.metadata @@ -1,5 +1,5 @@ { - "updateTime": "2019-08-02T11:26:24.848730Z", + "updateTime": "2019-08-14T00:53:10.619189Z", "sources": [ { "template": { diff --git a/system-test/storage.ts b/system-test/storage.ts index 4638d8432..ca6d738ad 100644 --- a/system-test/storage.ts +++ b/system-test/storage.ts @@ -1,5 +1,5 @@ /** - * Copyright 2014 Google Inc. All Rights Reserved. + * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -2687,6 +2687,142 @@ describe('storage', () => { }); }); + describe('HMAC keys', () => { + // This is generally a valid service account for a project. + const ALTERNATE_SERVICE_ACCOUNT = `${process.env.PROJECT_ID}@appspot.gserviceaccount.com`; + const SERVICE_ACCOUNT = + process.env.HMAC_KEY_TEST_SERVICE_ACCOUNT || ALTERNATE_SERVICE_ACCOUNT; + const HMAC_PROJECT = process.env.HMAC_KEY_TEST_SERVICE_ACCOUNT + ? process.env.HMAC_PROJECT + : process.env.PROJECT_ID; + // Second service account to test listing HMAC keys from different accounts. + const SECOND_SERVICE_ACCOUNT = + process.env.HMAC_KEY_TEST_SECOND_SERVICE_ACCOUNT; + + let accessId: string; + + before(async () => { + await deleteHmacKeys(SERVICE_ACCOUNT, HMAC_PROJECT!); + }); + + after(async () => { + await deleteHmacKeys(SERVICE_ACCOUNT, HMAC_PROJECT!); + }); + + it('should create an HMAC key for a service account', async () => { + const [hmacKey, secret] = await storage.createHmacKey(SERVICE_ACCOUNT, { + projectId: HMAC_PROJECT, + }); + // We should always get a 40 character secret, which is valid base64. + assert.strictEqual(secret.length, 40); + accessId = hmacKey.id!; + const metadata = hmacKey.metadata!; + assert.strictEqual(metadata.accessId, accessId); + assert.strictEqual(metadata.state, 'ACTIVE'); + assert.strictEqual(metadata.projectId, HMAC_PROJECT); + assert.strictEqual(metadata.serviceAccountEmail, SERVICE_ACCOUNT); + assert(typeof metadata.etag === 'string'); + assert(typeof metadata.timeCreated === 'string'); + assert(typeof metadata.updated === 'string'); + }); + + it('should get metadata for an HMAC key', async () => { + const hmacKey = storage.hmacKey(accessId, {projectId: HMAC_PROJECT}); + const [metadata] = await hmacKey.getMetadata(); + assert.strictEqual(metadata.accessId, accessId); + }); + + it('should show up from getHmacKeys() without serviceAccountEmail param', async () => { + const [hmacKeys] = await storage.getHmacKeys({projectId: HMAC_PROJECT}); + assert(hmacKeys.length > 0); + assert( + hmacKeys.some(hmacKey => hmacKey.id === accessId), + 'created HMAC key not found from getHmacKeys result' + ); + }); + + it('should make the key INACTIVE', async () => { + const hmacKey = storage.hmacKey(accessId, {projectId: HMAC_PROJECT}); + let [metadata] = await hmacKey.setMetadata({state: 'INACTIVE'}); + assert.strictEqual(metadata.state, 'INACTIVE'); + + [metadata] = await hmacKey.getMetadata(); + assert.strictEqual(metadata.state, 'INACTIVE'); + }); + + it('should delete the key', async () => { + const hmacKey = storage.hmacKey(accessId, {projectId: HMAC_PROJECT}); + await hmacKey.delete(); + const [metadata] = await hmacKey.getMetadata(); + assert.strictEqual(metadata.state, 'DELETED'); + assert.strictEqual(hmacKey.metadata!.state, 'DELETED'); + }); + + it('deleted key should not show up from getHmacKeys() by default', async () => { + const [hmacKeys] = await storage.getHmacKeys({ + serviceAccountEmail: SERVICE_ACCOUNT, + projectId: HMAC_PROJECT, + }); + assert(Array.isArray(hmacKeys)); + assert( + !hmacKeys.some(hmacKey => hmacKey.id === accessId), + 'deleted HMAC key is found from getHmacKeys result' + ); + }); + + describe('second service account', () => { + before(async function() { + if (!SECOND_SERVICE_ACCOUNT) { + this.skip(); + return; + } + await deleteHmacKeys(SECOND_SERVICE_ACCOUNT, HMAC_PROJECT!); + }); + + it('should create key for a second service account', async () => { + const _ = await storage.createHmacKey(SECOND_SERVICE_ACCOUNT!, { + projectId: HMAC_PROJECT, + }); + }); + + it('get HMAC keys for both service accounts', async () => { + // Create a key for the first service account + const _ = await storage.createHmacKey(SERVICE_ACCOUNT!, { + projectId: HMAC_PROJECT, + }); + + const [hmacKeys] = await storage.getHmacKeys({projectId: HMAC_PROJECT}); + assert( + hmacKeys.some( + hmacKey => hmacKey.metadata!.serviceAccountEmail === SERVICE_ACCOUNT + ), + `Expected at least 1 key for service account: ${SERVICE_ACCOUNT}` + ); + assert( + hmacKeys.some( + hmacKey => + hmacKey.metadata!.serviceAccountEmail === SECOND_SERVICE_ACCOUNT + ), + `Expected at least 1 key for service account: ${SECOND_SERVICE_ACCOUNT}` + ); + }); + + it('filter by service account email', async () => { + const [hmacKeys] = await storage.getHmacKeys({ + serviceAccountEmail: SECOND_SERVICE_ACCOUNT, + projectId: HMAC_PROJECT, + }); + assert( + hmacKeys.every( + hmacKey => + hmacKey.metadata!.serviceAccountEmail === SECOND_SERVICE_ACCOUNT + ), + 'HMAC key belonging to other service accounts unexpected' + ); + }); + }); + }); + describe('list files', () => { const DIRECTORY_NAME = 'directory-name'; @@ -3235,6 +3371,27 @@ describe('storage', () => { } } + async function deleteHmacKeys( + serviceAccountEmail: string, + projectId: string + ) { + const [hmacKeys] = await storage.getHmacKeys({ + serviceAccountEmail, + projectId, + }); + const limit = pLimit(10); + await Promise.all( + hmacKeys.map(hmacKey => + limit(async () => { + if (hmacKey.metadata!.state === 'ACTIVE') { + await hmacKey.setMetadata({state: 'INACTIVE'}); + } + await hmacKey.delete(); + }) + ) + ); + } + // tslint:disable-next-line no-any function createFileAsync(fileObject: any) { return fileObject.file.save(fileObject.contents); diff --git a/test/hmacKey.ts b/test/hmacKey.ts new file mode 100644 index 000000000..b073a8291 --- /dev/null +++ b/test/hmacKey.ts @@ -0,0 +1,88 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import * as proxyquire from 'proxyquire'; +import * as assert from 'assert'; +import {util, ServiceObject} from '@google-cloud/common'; + +// tslint:disable-next-line: no-any +let sandbox: sinon.SinonSandbox; +// tslint:disable-next-line: no-any +let STORAGE: any; +// tslint:disable-next-line: no-any +let hmacKey: any; + +const ACCESS_ID = 'fake-access-id'; + +describe('HmacKey', () => { + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('initialization', () => { + // tslint:disable-next-line: no-any + let serviceObjectSpy: sinon.SinonSpy; + // tslint:disable-next-line: no-any + let commonModule: any; + // tslint:disable-next-line: no-any variable-name + let HmacKey: any; + + beforeEach(() => { + commonModule = {ServiceObject}; + serviceObjectSpy = sandbox.spy(commonModule, 'ServiceObject'); + + HmacKey = proxyquire('../src/hmacKey', { + '@google-cloud/common': commonModule, + }).HmacKey; + + STORAGE = { + request: util.noop, + projectId: 'my-project', + }; + + hmacKey = new HmacKey(STORAGE, ACCESS_ID); + }); + + it('should inherit from ServiceObject', () => { + assert(hmacKey instanceof ServiceObject); + const ctorArg = serviceObjectSpy.firstCall.args[0]; + assert(ctorArg.parent, STORAGE); + assert(ctorArg.id, ACCESS_ID); + assert(ctorArg.baseUrl, '/projects/my-project/hmacKeys'); + assert.deepStrictEqual(ctorArg.methods, { + delete: true, + get: true, + getMetadata: true, + setMetadata: { + reqOpts: { + method: 'PUT', + }, + }, + }); + }); + + it('should form baseUrl using options.projectId if given', () => { + hmacKey = new HmacKey(STORAGE, ACCESS_ID, {projectId: 'another-project'}); + const ctorArg = serviceObjectSpy.firstCall.args[0]; + assert(ctorArg.baseUrl, '/projects/another-project/hmacKeys'); + }); + }); +}); diff --git a/test/index.ts b/test/index.ts index 076aa803e..1c80d0171 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2014 Google Inc. All Rights Reserved. + * Copyright 2019 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,11 @@ import * as proxyquire from 'proxyquire'; import {Bucket} from '../src'; import {GetFilesOptions} from '../src/bucket'; +import sinon = require('sinon'); +import {HmacKey} from '../src/hmacKey'; +import {HmacKeyResourceResponse} from '../src/storage'; + +const hmacKeyModule = require('../src/hmacKey'); class FakeChannel { calledWith_: Array<{}>; @@ -56,7 +61,7 @@ const fakePaginator = { methods = arrify(methods); assert.strictEqual(Class.name, 'Storage'); - assert.deepStrictEqual(methods, ['getBuckets']); + assert.deepStrictEqual(methods, ['getBuckets', 'getHmacKeys']); extended = true; }, streamify(methodName: string) { @@ -74,7 +79,7 @@ const fakePromisify = { } promisified = true; - assert.deepStrictEqual(options.exclude, ['bucket', 'channel']); + assert.deepStrictEqual(options.exclude, ['bucket', 'channel', 'hmacKey']); }, }; @@ -95,6 +100,7 @@ describe('Storage', () => { Service: FakeService, }, './channel.js': {Channel: FakeChannel}, + './hmacKey': hmacKeyModule, }).Storage; Bucket = Storage.Bucket; }); @@ -110,6 +116,7 @@ describe('Storage', () => { it('should streamify the correct methods', () => { assert.strictEqual(storage.getBucketsStream, 'getBuckets'); + assert.strictEqual(storage.getHmacKeysStream, 'getHmacKeys'); }); it('should promisify all the things', () => { @@ -189,6 +196,186 @@ describe('Storage', () => { }); }); + describe('hmacKey', () => { + let hmacKeyCtor: sinon.SinonSpy; + beforeEach(() => { + hmacKeyCtor = sinon.spy(hmacKeyModule, 'HmacKey'); + }); + + afterEach(() => { + hmacKeyCtor.restore(); + }); + + it('should throw if accessId is not provided', () => { + assert.throws(() => { + storage.hmacKey(); + }, /An access ID is needed to create an HmacKey object./); + }); + + it('should pass options object to HmacKey constructor', () => { + const options = {myOpts: 'a'}; + storage.hmacKey('access-id', options); + assert.deepStrictEqual(hmacKeyCtor.getCall(0).args, [ + storage, + 'access-id', + options, + ]); + }); + }); + + describe('createHmacKey', () => { + const SERVICE_ACCOUNT_EMAIL = 'service-account@gserviceaccount.com'; + const ACCESS_ID = 'some-access-id'; + const metadataResponse = { + accessId: ACCESS_ID, + etag: 'etag', + id: ACCESS_ID, + projectId: 'project-id', + serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, + state: 'ACTIVE', + timeCreated: '20190101T00:00:00Z', + updated: '20190101T00:00:00Z', + }; + const response = { + secret: 'my-secret', + metadata: metadataResponse, + }; + const OPTIONS = { + some: 'value', + }; + + let hmacKeyCtor: sinon.SinonSpy; + beforeEach(() => { + hmacKeyCtor = sinon.spy(hmacKeyModule, 'HmacKey'); + }); + + afterEach(() => { + hmacKeyCtor.restore(); + }); + + it('should make correct API request', done => { + storage.request = ( + reqOpts: DecorateRequestOptions, + callback: Function + ) => { + assert.strictEqual(reqOpts.method, 'POST'); + assert.strictEqual( + reqOpts.uri, + `/projects/${storage.projectId}/hmacKeys` + ); + assert.strictEqual( + reqOpts.qs.serviceAccountEmail, + SERVICE_ACCOUNT_EMAIL + ); + + callback(null, response); + }; + + storage.createHmacKey(SERVICE_ACCOUNT_EMAIL, done); + }); + + it('should throw without a serviceAccountEmail', () => { + assert.throws( + () => storage.createHmacKey(), + /The first argument must be a service account email to create an HMAC key\./ + ); + }); + + it('should throw when first argument is not a string', () => { + assert.throws( + () => + storage.createHmacKey({ + userProject: 'my-project', + }), + /The first argument must be a service account email to create an HMAC key\./ + ); + }); + + it('should make request with method options as query parameter', async () => { + storage.request = sinon + .stub() + .returns((_reqOpts: {}, callback: Function) => callback()); + + await storage.createHmacKey(SERVICE_ACCOUNT_EMAIL, OPTIONS); + const reqArg = storage.request.firstCall.args[0]; + assert.deepStrictEqual(reqArg.qs, { + serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, + ...OPTIONS, + }); + }); + + it('should not modify the options object', done => { + storage.request = (_reqOpts: {}, callback: Function) => { + callback(null, response); + }; + const originalOptions = Object.assign({}, OPTIONS); + + storage.createHmacKey(SERVICE_ACCOUNT_EMAIL, OPTIONS, (err: Error) => { + assert.ifError(err); + assert.deepStrictEqual(OPTIONS, originalOptions); + done(); + }); + }); + + it('should invoke callback with a secret and an HmacKey instance', done => { + storage.request = (_reqOpts: {}, callback: Function) => { + callback(null, response); + }; + + storage.createHmacKey( + SERVICE_ACCOUNT_EMAIL, + (err: Error, hmacKey: HmacKey, secret: string) => { + assert.ifError(err); + assert.strictEqual(secret, response.secret); + assert.deepStrictEqual(hmacKeyCtor.getCall(0).args, [ + storage, + response.metadata.accessId, + {projectId: response.metadata.projectId}, + ]); + assert.strictEqual(hmacKey.metadata, metadataResponse); + done(); + } + ); + }); + + it('should invoke callback with raw apiResponse', done => { + storage.request = (_reqOpts: {}, callback: Function) => { + callback(null, response); + }; + + storage.createHmacKey( + SERVICE_ACCOUNT_EMAIL, + ( + err: Error, + _hmacKey: HmacKey, + _secret: string, + apiResponse: HmacKeyResourceResponse + ) => { + assert.ifError(err); + assert.strictEqual(apiResponse, response); + done(); + } + ); + }); + + it('should execute callback with request error', done => { + const error = new Error('Request error'); + const response = {success: false}; + storage.request = (_reqOpts: {}, callback: Function) => { + callback(error, response); + }; + + storage.createHmacKey( + SERVICE_ACCOUNT_EMAIL, + (err: Error, _hmacKey: HmacKey, _secret: string, apiResponse: {}) => { + assert.strictEqual(err, error); + assert.strictEqual(apiResponse, response); + done(); + } + ); + }); + }); + describe('createBucket', () => { const BUCKET_NAME = 'new-bucket-name'; const METADATA = {a: 'b', c: {d: 'e'}}; @@ -508,6 +695,156 @@ describe('Storage', () => { }); }); + describe('getHmacKeys', () => { + // tslint:disable-next-line: no-any + let storageRequestStub: sinon.SinonStub; + const SERVICE_ACCOUNT_EMAIL = 'service-account@gserviceaccount.com'; + const ACCESS_ID = 'some-access-id'; + const metadataResponse = { + accessId: ACCESS_ID, + etag: 'etag', + id: ACCESS_ID, + projectId: 'project-id', + serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, + state: 'ACTIVE', + timeCreated: '20190101T00:00:00Z', + updated: '20190101T00:00:00Z', + }; + + beforeEach(() => { + storageRequestStub = sinon.stub(storage, 'request'); + storageRequestStub.callsFake((_opts: {}, callback: Function) => { + callback(null, {}); + }); + }); + + let hmacKeyCtor: sinon.SinonSpy; + beforeEach(() => { + hmacKeyCtor = sinon.spy(hmacKeyModule, 'HmacKey'); + }); + + afterEach(() => { + hmacKeyCtor.restore(); + }); + + it('should get HmacKeys without a query', done => { + storage.getHmacKeys(() => { + const firstArg = storage.request.firstCall.args[0]; + assert.strictEqual( + firstArg.uri, + `/projects/${storage.projectId}/hmacKeys` + ); + assert.deepStrictEqual(firstArg.qs, {}); + done(); + }); + }); + + it('should get HmacKeys with a query', done => { + const query = { + maxResults: 5, + pageToken: 'next-page-token', + serviceAccountEmail: SERVICE_ACCOUNT_EMAIL, + showDeletedKeys: false, + }; + + storage.getHmacKeys(query, () => { + const firstArg = storage.request.firstCall.args[0]; + assert.strictEqual( + firstArg.uri, + `/projects/${storage.projectId}/hmacKeys` + ); + assert.deepStrictEqual(firstArg.qs, query); + done(); + }); + }); + + it('should execute callback with error', done => { + const error = new Error('Error.'); + const apiResponse = {}; + + storageRequestStub.callsFake((_opts: {}, callback: Function) => { + callback(error, apiResponse); + }); + + storage.getHmacKeys( + {}, + (err: Error, hmacKeys: HmacKey[], nextQuery: {}, resp: Metadata) => { + assert.strictEqual(err, error); + assert.strictEqual(hmacKeys, null); + assert.strictEqual(nextQuery, null); + assert.strictEqual(resp, apiResponse); + done(); + } + ); + }); + + it('should return nextQuery if more results exist', done => { + const token = 'next-page-token'; + const query = { + param1: 'a', + param2: 'b', + }; + const expectedNextQuery = Object.assign({}, query, {pageToken: token}); + storageRequestStub.callsFake((_opts: {}, callback: Function) => { + callback(null, {nextPageToken: token, items: []}); + }); + + storage.getHmacKeys( + query, + // tslint:disable-next-line: no-any + (err: Error, _hmacKeys: [], nextQuery: any) => { + assert.ifError(err); + assert.deepStrictEqual(nextQuery, expectedNextQuery); + done(); + } + ); + }); + + it('should return null nextQuery if there are no more results', done => { + storageRequestStub.callsFake((_opts: {}, callback: Function) => { + callback(null, {items: []}); + }); + + storage.getHmacKeys({}, (err: Error, _hmacKeys: [], nextQuery: {}) => { + assert.ifError(err); + assert.strictEqual(nextQuery, null); + done(); + }); + }); + + it('should return apiResponse', done => { + const resp = {items: [metadataResponse]}; + storageRequestStub.callsFake((_opts: {}, callback: Function) => { + callback(null, resp); + }); + + storage.getHmacKeys( + (err: Error, _hmacKeys: [], _nextQuery: {}, apiResponse: Metadata) => { + assert.ifError(err); + assert.deepStrictEqual(resp, apiResponse); + done(); + } + ); + }); + + it('should populate returned HmacKey object with accessId and metadata', done => { + storageRequestStub.callsFake((_opts: {}, callback: Function) => { + callback(null, {items: [metadataResponse]}); + }); + + storage.getHmacKeys((err: Error, hmacKeys: HmacKey[]) => { + assert.ifError(err); + assert.deepStrictEqual(hmacKeyCtor.getCall(0).args, [ + storage, + metadataResponse.accessId, + {projectId: metadataResponse.projectId}, + ]); + assert.deepStrictEqual(hmacKeys[0].metadata, metadataResponse); + done(); + }); + }); + }); + describe('getServiceAccount', () => { it('should make the correct request', done => { storage.request = (reqOpts: DecorateRequestOptions) => {