You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
7.4 KiB

"use strict";
// 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.
var _a, _b, _c;
Object.defineProperty(exports, "__esModule", { value: true });
exports.IdentityPoolClient = void 0;
const fs = require("fs");
const util_1 = require("util");
const baseexternalclient_1 = require("./baseexternalclient");
// fs.readfile is undefined in browser karma tests causing
// `npm run browser-test` to fail as test.oauth2.ts imports this file via
// src/index.ts.
// Fallback to void function to avoid promisify throwing a TypeError.
const readFile = (0, util_1.promisify)((_a = fs.readFile) !== null && _a !== void 0 ? _a : (() => { }));
const realpath = (0, util_1.promisify)((_b = fs.realpath) !== null && _b !== void 0 ? _b : (() => { }));
const lstat = (0, util_1.promisify)((_c = fs.lstat) !== null && _c !== void 0 ? _c : (() => { }));
/**
* Defines the Url-sourced and file-sourced external account clients mainly
* used for K8s and Azure workloads.
*/
class IdentityPoolClient extends baseexternalclient_1.BaseExternalAccountClient {
/**
* Instantiate an IdentityPoolClient instance using the provided JSON
* object loaded from an external account credentials file.
* An error is thrown if the credential is not a valid file-sourced or
* url-sourced credential or a workforce pool user project is provided
* with a non workforce audience.
* @param options The external account options object typically loaded
* from the external account JSON credential file.
* @param additionalOptions Optional additional behavior customization
* options. These currently customize expiration threshold time and
* whether to retry on 401/403 API request errors.
*/
constructor(options, additionalOptions) {
var _a, _b;
super(options, additionalOptions);
this.file = options.credential_source.file;
this.url = options.credential_source.url;
this.headers = options.credential_source.headers;
if (!this.file && !this.url) {
throw new Error('No valid Identity Pool "credential_source" provided');
}
// Text is the default format type.
this.formatType = ((_a = options.credential_source.format) === null || _a === void 0 ? void 0 : _a.type) || 'text';
this.formatSubjectTokenFieldName =
(_b = options.credential_source.format) === null || _b === void 0 ? void 0 : _b.subject_token_field_name;
if (this.formatType !== 'json' && this.formatType !== 'text') {
throw new Error(`Invalid credential_source format "${this.formatType}"`);
}
if (this.formatType === 'json' && !this.formatSubjectTokenFieldName) {
throw new Error('Missing subject_token_field_name for JSON credential_source format');
}
}
/**
* Triggered when a external subject token is needed to be exchanged for a GCP
* access token via GCP STS endpoint.
* This uses the `options.credential_source` object to figure out how
* to retrieve the token using the current environment. In this case,
* this either retrieves the local credential from a file location (k8s
* workload) or by sending a GET request to a local metadata server (Azure
* workloads).
* @return A promise that resolves with the external subject token.
*/
async retrieveSubjectToken() {
if (this.file) {
return await this.getTokenFromFile(this.file, this.formatType, this.formatSubjectTokenFieldName);
}
return await this.getTokenFromUrl(this.url, this.formatType, this.formatSubjectTokenFieldName, this.headers);
}
/**
* Looks up the external subject token in the file path provided and
* resolves with that token.
* @param file The file path where the external credential is located.
* @param formatType The token file or URL response type (JSON or text).
* @param formatSubjectTokenFieldName For JSON response types, this is the
* subject_token field name. For Azure, this is access_token. For text
* response types, this is ignored.
* @return A promise that resolves with the external subject token.
*/
async getTokenFromFile(filePath, formatType, formatSubjectTokenFieldName) {
// Make sure there is a file at the path. lstatSync will throw if there is
// nothing there.
try {
// Resolve path to actual file in case of symlink. Expect a thrown error
// if not resolvable.
filePath = await realpath(filePath);
if (!(await lstat(filePath)).isFile()) {
throw new Error();
}
}
catch (err) {
if (err instanceof Error) {
err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`;
}
throw err;
}
let subjectToken;
const rawText = await readFile(filePath, { encoding: 'utf8' });
if (formatType === 'text') {
subjectToken = rawText;
}
else if (formatType === 'json' && formatSubjectTokenFieldName) {
const json = JSON.parse(rawText);
subjectToken = json[formatSubjectTokenFieldName];
}
if (!subjectToken) {
throw new Error('Unable to parse the subject_token from the credential_source file');
}
return subjectToken;
}
/**
* Sends a GET request to the URL provided and resolves with the returned
* external subject token.
* @param url The URL to call to retrieve the subject token. This is typically
* a local metadata server.
* @param formatType The token file or URL response type (JSON or text).
* @param formatSubjectTokenFieldName For JSON response types, this is the
* subject_token field name. For Azure, this is access_token. For text
* response types, this is ignored.
* @param headers The optional additional headers to send with the request to
* the metadata server url.
* @return A promise that resolves with the external subject token.
*/
async getTokenFromUrl(url, formatType, formatSubjectTokenFieldName, headers) {
const opts = {
url,
method: 'GET',
headers,
responseType: formatType,
};
let subjectToken;
if (formatType === 'text') {
const response = await this.transporter.request(opts);
subjectToken = response.data;
}
else if (formatType === 'json' && formatSubjectTokenFieldName) {
const response = await this.transporter.request(opts);
subjectToken = response.data[formatSubjectTokenFieldName];
}
if (!subjectToken) {
throw new Error('Unable to parse the subject_token from the credential_source URL');
}
return subjectToken;
}
}
exports.IdentityPoolClient = IdentityPoolClient;
//# sourceMappingURL=identitypoolclient.js.map