diff --git a/.kokoro/functions/security.cfg b/.kokoro/functions/security.cfg new file mode 100644 index 0000000000..df57560cd0 --- /dev/null +++ b/.kokoro/functions/security.cfg @@ -0,0 +1,13 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Set the folder in which the tests are run +env_vars: { + key: "PROJECT" + value: "functions/security" +} + +# Tell the trampoline which build file to use. +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/nodejs-docs-samples/.kokoro/build.sh" +} diff --git a/functions/security/index.js b/functions/security/index.js new file mode 100644 index 0000000000..228595923c --- /dev/null +++ b/functions/security/index.js @@ -0,0 +1,52 @@ +// Copyright 2020 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'; + +// [START functions_bearer_token] +const {get} = require('axios'); + +// TODO(developer): set these values +const REGION = 'us-central1'; +const PROJECT_ID = 'my-project-id'; +const RECEIVING_FUNCTION = 'myFunction'; + +// Constants for setting up metadata server request +// See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature +const functionURL = `https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${RECEIVING_FUNCTION}`; +const metadataServerURL = + 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience='; +const tokenUrl = metadataServerURL + functionURL; + +exports.callingFunction = async (req, res) => { + // Fetch the token + const tokenResponse = await get(tokenUrl, { + headers: { + 'Metadata-Flavor': 'Google', + }, + }); + const token = tokenResponse.data; + + // Provide the token in the request to the receiving function + try { + const functionResponse = await get(functionURL, { + headers: {Authorization: `bearer ${token}`}, + }); + res.status(200).send(functionResponse.data); + } catch (err) { + console.error(err); + res.status(500).send('An error occurred! See logs for more details.'); + } +}; +// [END functions_bearer_token] diff --git a/functions/security/package.json b/functions/security/package.json new file mode 100644 index 0000000000..7d3ec22a3e --- /dev/null +++ b/functions/security/package.json @@ -0,0 +1,27 @@ +{ + "name": "nodejs-docs-samples-functions-security", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=8.0.0" + }, + "scripts": { + "test": "mocha test/*.test.js" + }, + "dependencies": { + "axios": "^0.19.2", + "eslint-plugin-node": "^11.1.0", + "mocha": "^7.1.1" + }, + "devDependencies": { + "assert": "^2.0.0", + "proxyquire": "^2.1.3", + "sinon": "^9.0.1" + } +} diff --git a/functions/security/test/index.test.js b/functions/security/test/index.test.js new file mode 100644 index 0000000000..0b70787aa3 --- /dev/null +++ b/functions/security/test/index.test.js @@ -0,0 +1,59 @@ +// Copyright 2017 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 proxyquire = require('proxyquire').noCallThru(); +const sinon = require('sinon'); +const assert = require('assert'); + +const getSample = () => { + const getMock = sinon + .stub() + .onFirstCall() + .resolves({data: 'some-token'}) + .onSecondCall() + .resolves({data: 'function-response'}); + + const axiosMock = {get: getMock}; + + const resMock = {}; + resMock.status = sinon.stub().returns(resMock); + resMock.send = sinon.stub().returns(resMock); + + return { + sample: proxyquire('../', { + axios: axiosMock, + }), + mocks: { + res: resMock, + axios: axiosMock, + }, + }; +}; + +describe('functions_bearer_token', () => { + it('should run', async () => { + const {sample, mocks} = getSample(); + + await sample.callingFunction(null, mocks.res); + + assert(mocks.axios.get.calledTwice); + assert.deepEqual(mocks.axios.get.firstCall.args[1], { + headers: {'Metadata-Flavor': 'Google'}, + }); + + assert(mocks.res.send.calledWith('function-response')); + }); +});