Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds README, samples and integration tests for downscoping with CAB #1311

Merged
merged 23 commits into from Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 117 additions & 0 deletions README.md
Expand Up @@ -54,6 +54,9 @@ This library provides a variety of ways to authenticate to your Google services.
- [Google Compute](#compute) - Directly use a service account on Google Cloud Platform. Useful for server->server or server->API communication.
- [Workload Identity Federation](#workload-identity-federation) - Use workload identity federation to access Google Cloud resources from Amazon Web Services (AWS), Microsoft Azure or any identity provider that supports OpenID Connect (OIDC).
- [Impersonated Credentials Client](#impersonated-credentials-client) - access protected resources on behalf of another service account.
- [Downscoped Client](#downscoped-client) - Use Downscoped Client with Credential
Access Boundary to generate a short-lived credential with downscoped, restricted
IAM permissions that can use for Cloud Storage.

## Application Default Credentials
This library provides an implementation of [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started)for Node.js. The [Application Default Credentials](https://cloud.google.com/docs/authentication/getting-started) provide a simple way to get authorization credentials for use in calling Google APIs.
Expand Down Expand Up @@ -721,6 +724,119 @@ async function main() {
main();
```

## Downscoped Client

[Downscoping with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials) is used to restrict the Identity and Access Management (IAM) permissions that a short-lived credential can use.

The `DownscopedClient` class can be used to produce a downscoped access token from a
`CredentialAccessBoundary` and a source credential. The Credential Access Boundary specifies which resources the newly created credential can access, as well as an upper bound on the permissions that are available on each resource. Using downscoped credentials ensures tokens in flight always have the least privileges, e.g. Principle of Least Privilege.

Notice:
xil222 marked this conversation as resolved.
Show resolved Hide resolved
Only Cloud Storage supports Credential Access Boundaries for now.
xil222 marked this conversation as resolved.
Show resolved Hide resolved

### Sample Usage
There are two entities needed to generate and use credentials generated from
Downscoped Client with Credential Access Boundaries.

- Token broker: This is the entity with elevated permissions. This entity has the permissions needed to generate downscoped tokens. The common pattern of usage is to have a token broker with elevated access generate these downscoped credentials from higher access source credentials and pass the downscoped short-lived access tokens to a token consumer via some secure authenticated channel for limited access to Google Cloud Storage resources.

``` js
const {GoogleAuth, DownscopedClient} = require('google-auth-library');
// Define CAB rules which will restrict the downscoped token to have readonly
// access to objects starting with "customer-a" in bucket "bucket_name".
const cabRules = {
accessBoundary: {
accessBoundaryRules: [
{
availableResource: `//storage.googleapis.com/projects/_/buckets/bucket_name`,
availablePermissions: ['inRole:roles/storage.objectViewer'],
availabilityCondition: {
expression:
`resource.name.startsWith('projects/_/buckets/` +
`bucket_name/objects/customer-a)`
}
},
],
},
};

// This will use ADC to get the credentials used for the downscoped client.
const googleAuth = new GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform']
});

// Obtain an authenticated client via ADC.
const client = await googleAuth.getClient();

// Use the client to create a DownscopedClient.
const cabClient = new DownscopedClient(client, cab);

// Refresh the tokens.
const refreshedAccessToken = await cabClient.getAccessToken();

// This will need to be passed to the token consumer.
access_token = refreshedAccessToken.token;
expiry_date = refreshedAccessToken.expirationTime;
```

A token broker can be set up on a server in a private network. Various workloads
(token consumers) in the same network will send authenticated requests to that broker for downscoped tokens to access or modify specific google cloud storage buckets.

The broker will instantiate downscoped credentials instances that can be used to generate short lived downscoped access tokens which will be passed to the token consumer.

- Token consumer: This is the consumer of the downscoped tokens. This entity does not have the direct ability to generate access tokens and instead relies on the token broker to provide it with downscoped tokens to run operations on GCS buckets. It is assumed that the downscoped token consumer may have its own mechanism to authenticate itself with the token broker.

``` js
const {OAuth2Client} = require('google-auth-library');
const {Storage} = require('@google-cloud/storage');

// Create the OAuth credentials (the consumer).
const oauth2Client = new OAuth2Client();
// We are defining a refresh handler instead of a one-time access
// token/expiry pair.
// This will allow the consumer to obtain new downscoped tokens on
// demand every time a token is expired, without any additional code
// changes.
oauth2Client.refreshHandler = async () => {
// The common pattern of usage is to have a token broker pass the
// downscoped short-lived access tokens to a token consumer via some
// secure authenticated channel.
const refreshedAccessToken = await cabClient.getAccessToken();
return {
access_token: refreshedAccessToken.token,
expiry_date: refreshedAccessToken.expirationTime,
}
};

// Use the consumer client to define storageOptions and create a GCS object.
const storageOptions = {
projectId: 'my_project_id',
authClient: {
sign: () => Promise.reject('unsupported'),
getCredentials: () => Promise.reject(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach you came up with here ends up not looking quite as ugly as I was fearing.

How does this compare to Python and other languages? Is there work we should be tracking to make this API easier?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Python and Java directly passing authClient as variable to create Storage object without need to redefine a couple of APIs such as sign() or getCredentials().
And, yes, the long-term work is referred in last PR
GoogleCloudPlatform/nodejs-docs-samples#2368 (comment)
and you can track in bug b/196442993

request: (opts, callback) => {
return oauth2Client.request(opts, callback);
},
authorizeRequest: async (opts) => {
opts = opts || {};
const url = opts.url || opts.uri;
const headers = await oauth2Client.getRequestHeaders(url);
opts.headers = Object.assign(opts.headers || {}, headers);
return opts;
},
},
};

const storage = new Storage(storageOptions);

const downloadFile = await storage
.bucket('bucket_name')
.file('customer-a-data.txt')
.download();
console.log(downloadFile.toString('utf8'));

main().catch(console.error);
```

## Samples

Expand All @@ -731,6 +847,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/google-auth-librar
| Adc | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/adc.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/adc.js,samples/README.md) |
| Compute | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/compute.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/compute.js,samples/README.md) |
| Credentials | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/credentials.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/credentials.js,samples/README.md) |
| DownscopedClient | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/downscopedclient.js,samples/README.md) |
| Headers | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/headers.js,samples/README.md) |
| ID Tokens for Identity-Aware Proxy (IAP) | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-iap.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-iap.js,samples/README.md) |
| ID Tokens for Serverless | [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/idtokens-serverless.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/idtokens-serverless.js,samples/README.md) |
Expand Down
18 changes: 18 additions & 0 deletions samples/README.md
Expand Up @@ -15,6 +15,7 @@ This is Google's officially supported [node.js](http://nodejs.org/) client libra
* [Adc](#adc)
* [Compute](#compute)
* [Credentials](#credentials)
* [DownscopedClient](#downscopedclient)
* [Headers](#headers)
* [ID Tokens for Identity-Aware Proxy (IAP)](#id-tokens-for-identity-aware-proxy-iap)
* [ID Tokens for Serverless](#id-tokens-for-serverless)
Expand Down Expand Up @@ -93,6 +94,23 @@ __Usage:__



### DownscopedClient

View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/downscopedclient.js).

[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/google-auth-library-nodejs&page=editor&open_in_editor=samples/downscopedclient.js,samples/README.md)

__Usage:__


`node samples/downscopedclient.js`


-----




### Headers

View the [source code](https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/headers.js).
Expand Down
111 changes: 111 additions & 0 deletions samples/downscopedclient.js
@@ -0,0 +1,111 @@
// Copyright 2021 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';

/**
* Imports the Google Auth and Google Cloud libraries.
*/
const {
OAuth2Client,
GoogleAuth,
DownscopedClient,
} = require('google-auth-library');
const {Storage} = require('@google-cloud/storage');

// TODO(developer): Replace these variables before running the sample.
// Make sure the bucket and object exists in your project.
// The Cloud Storage bucket name.
const bucketName = 'your-gcs-bucket-name';
// The Cloud Storage object name that resides in the specified bucket.
const objectName = 'your-gcs-object-name';

/**
* The following sample demonstrates how to initialize a DownscopedClient using
* a credential access boundary and a client obtained via ADC. The
* DownscopedClient is used to create downscoped tokens which can be consumed
* via the OAuth2Client. A refresh handler is used to obtain new downscoped
* tokens seamlessly when they expire. Then the oauth2Client is used to define
* a cloud storage object and call GCS APIs to access specified object and
* print the contents.
*/
async function main() {
// Defines a credential access boundary that grants full control over objects
// in the specified bucket.
xil222 marked this conversation as resolved.
Show resolved Hide resolved
const cab = {
accessBoundary: {
accessBoundaryRules: [
{
availableResource: `//storage.googleapis.com/projects/_/buckets/${bucketName}`,
availablePermissions: ['inRole:roles/storage.objectViewer'],
availabilityCondition: {
expression:
"resource.name.startsWith('projects/_/buckets/" +
`${bucketName}/objects/${objectName}')`,
},
},
],
},
};

const oauth2Client = new OAuth2Client();
const googleAuth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/cloud-platform',
});
const projectId = await googleAuth.getProjectId();
// Obtain an authenticated client via ADC.
const client = await googleAuth.getClient();
// Use the client to generate a DownscopedClient.
const cabClient = new DownscopedClient(client, cab);
// Define a refreshHandler that will be used to refresh the downscoped token
// when it expires.
oauth2Client.refreshHandler = async () => {
const refreshedAccessToken = await cabClient.getAccessToken();
return {
access_token: refreshedAccessToken.token,
expiry_date: refreshedAccessToken.expirationTime,
};
};

const storageOptions = {
projectId,
authClient: {
getCredentials: async () => {
Promise.reject();
},
request: opts => {
return oauth2Client.request(opts);
},
sign: () => {
Promise.reject('unsupported');
},
authorizeRequest: async opts => {
opts = opts || {};
const url = opts.url || opts.uri;
const headers = await oauth2Client.getRequestHeaders(url);
opts.headers = Object.assign(opts.headers || {}, headers);
return opts;
},
},
};

const storage = new Storage(storageOptions);
const downloadFile = await storage
.bucket(bucketName)
.file(objectName)
.download();
console.log(downloadFile.toString('utf8'));
}

main().catch(console.error);
3 changes: 2 additions & 1 deletion samples/package.json
Expand Up @@ -5,14 +5,15 @@
"*.js"
],
"scripts": {
"setup": "node scripts/externalclient-setup.js",
"setup": "node scripts/*.js",
"test": "mocha --timeout 60000"
},
"engines": {
"node": ">=10"
},
"license": "Apache-2.0",
"dependencies": {
"@google-cloud/storage": "^5.15.4",
"@googleapis/iam": "^2.0.0",
"google-auth-library": "^7.10.2",
"node-fetch": "^2.3.0",
Expand Down
94 changes: 94 additions & 0 deletions samples/scripts/downscoping-with-cab-setup.js
@@ -0,0 +1,94 @@
// Copyright 2021 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.

// This script is used to generate the project configurations needed to
// end-to-end test Downscoping with Credential Access Boundaries in the Auth
// library.

// In order to run this script, the GOOGLE_APPLICATION_CREDENTIALS environment
// variable needs to be set to point to a service account key file.
//
// GCP project changes:
// --------------------
// The following IAM role need to be set on the service account:
// 1. Storage Admin (needed to create bucket and object).

// This script needs to be run once. It will do the following:
// 1. Generates a random ID for bucketName and objectName.
// 2. Creates a GCS bucket in the specified project defined in GOOGLE_APPLICATION_CREDENTIALS.
// 3. Creates two object in the bucket created in the last step.
// 4. Prints out the identifiers (bucketName, first objectName, second objectName)
// to be used in the accompanying tests.
//
// The same service account used for this setup script should be used for
// the integration tests.
//
// It is safe to run the setup script again. A new bucket is created along with
// new objects. If run multiple times, it is advisable to delete
// unused buckets.

const {Storage} = require('@google-cloud/storage');
const fs = require('fs');
const {promisify} = require('util');

const readFile = promisify(fs.readFile);
const CONTENT1 = 'first';
const CONTENT2 = 'second';

/**
* Generates a random string of the specified length, optionally using the
* specified alphabet.
*
* @param {number} length The length of the string to generate.
* @return {string} A random string of the provided length.
*/
function generateRandomString(length) {
const chars = [];
const allowedChars = 'abcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
chars.push(
allowedChars.charAt(Math.floor(Math.random() * allowedChars.length))
);
}
return chars.join('');
}

async function main() {
const keyFile = process.env.GOOGLE_APPLICATION_CREDENTIALS;
if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
throw new Error('No GOOGLE_APPLICATION_CREDENTIALS env var is available');
}
const keys = JSON.parse(await readFile(keyFile, 'utf8'));

const suffix = generateRandomString(10);
const bucketName = `cab-int-bucket-${suffix}`;
const objectName1 = `cab-first-"${suffix}.txt`;
const objectName2 = `cab-second-"${suffix}.txt`;
const projectId = keys.project_id;
const storage = new Storage(projectId);

try {
await storage.createBucket(bucketName);
await storage.bucket(bucketName).file(objectName1).save(CONTENT1);
await storage.bucket(bucketName).file(objectName2).save(CONTENT2);
} catch (error) {
console.log(error.message);
}

console.log('bucket name: ' + bucketName);
console.log('object1 name: ' + objectName1);
console.log('object2 name: ' + objectName2);
}

main().catch(console.error);