authui-container/server/api/metadata-server.ts (120 lines of code) (raw):
/*
* Copyright 2020 Google Inc. All Rights Reserved.
*
* 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 {HttpServerRequestHandler} from '../../server/utils/http-server-request-handler';
import {TokenManager, AccessTokenManager} from './token-manager';
/** Metadata server project number endpoint. */
const METADATA_SERVER_PROJECT_NUMBER_URL =
'http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id';
/** Metadata server project ID endpoint. */
const METADATA_SERVER_PROJECT_ID_URL =
'http://metadata.google.internal/computeMetadata/v1/project/project-id';
/** Metadata server zone endpoint. */
const METADATA_SERVER_ZONE_URL =
'http://metadata.google.internal/computeMetadata/v1/instance/zone';
/** Network request timeout duration. */
const TIMEOUT_DURATION = 10000;
/** Default zone to use when it is not determined for whatever reason. */
export const DEFAULT_ZONE = 'US-CENTRAL1';
/** Default error message to show when project ID fails to be determined. */
export const DEFAULT_ERROR_MESSAGE_PROJECT_ID = 'Unable to retrieve the project ID.';
/** Default error message to show when project number fails to be determined. */
export const DEFAULT_ERROR_MESSAGE_PROJECT_NUMBER = 'Unable to retrieve the project number.';
/** Default error message to show when GCP zone fails to be determined. */
export const DEFAULT_ERROR_MESSAGE_ZONE = 'Unable to retrieve the GCP zone.';
/** Interface defining all application related data. */
export interface ApplicationData {
getProjectId(): Promise<string>;
getProjectNumber(): Promise<string>;
getZone(): Promise<string>;
log(...args: any[]): void;
}
/**
* Metadata server APIs for retrieving OAuth access tokens, project ID,
* numeric project ID, current GCP zone, etc.
*/
export class MetadataServer implements AccessTokenManager, ApplicationData {
private readonly tokenManager: TokenManager;
private readonly projectIdRetriever: HttpServerRequestHandler;
private readonly projectNumberRetriever: HttpServerRequestHandler;
private readonly zoneRetriever: HttpServerRequestHandler;
private projectId: string;
private projectNumber: string;
private zone: string;
/**
* Instantiates an instance of the metadata server APIs handler.
* @param scopes The OAuth scopes to set on the generated access tokens.
* @param logger The optional logging function used to log request information for debugging purposes.
* This can be accessed via Cloud Run LOGS tab.
*/
constructor(scopes?: string[], private readonly logger?: (...args: any[]) => void) {
this.tokenManager = new TokenManager(scopes);
this.projectIdRetriever = new HttpServerRequestHandler({
method: 'GET',
url: METADATA_SERVER_PROJECT_ID_URL,
headers: {
'Metadata-Flavor': 'Google',
},
timeout: TIMEOUT_DURATION,
}, logger);
this.projectNumberRetriever = new HttpServerRequestHandler({
method: 'GET',
url: METADATA_SERVER_PROJECT_NUMBER_URL,
headers: {
'Metadata-Flavor': 'Google',
},
timeout: TIMEOUT_DURATION,
}, logger);
this.zoneRetriever = new HttpServerRequestHandler({
method: 'GET',
url: METADATA_SERVER_ZONE_URL,
headers: {
'Metadata-Flavor': 'Google',
},
timeout: TIMEOUT_DURATION,
}, logger);
}
/**
* Used to log underlying operations for debugging purposes, if a logger is available.
* @param args The list of arguments to log.
*/
log(...args: any[]) {
if (this.logger) {
this.logger(...args);
}
}
/**
* @return A promise that resolves with a Google OAuth access token.
* A cached token is returned if it is not yet expired.
*/
getAccessToken(forceRefresh: boolean = false): Promise<string> {
return this.tokenManager.getAccessToken(forceRefresh)
.then((result) => {
return result.access_token;
})
.catch((error) => {
// For access token getter, only log errors.
this.log('Error encountered while getting Metadata server access token', error);
throw error;
});
}
/** @return A promise that resolves with the project ID. */
getProjectId(): Promise<string> {
if (this.projectId) {
return Promise.resolve(this.projectId);
} else {
return this.projectIdRetriever.send(null, DEFAULT_ERROR_MESSAGE_PROJECT_ID)
.then((httpResponse) => {
if (httpResponse.statusCode === 200 &&
httpResponse.body) {
this.projectId = httpResponse.body as string;
return this.projectId;
} else {
// No data in body.
throw new Error(DEFAULT_ERROR_MESSAGE_PROJECT_ID);
}
});
}
}
/** @return A promise that resolves with the project number. */
getProjectNumber(): Promise<string> {
if (this.projectNumber) {
return Promise.resolve(this.projectNumber);
} else {
return this.projectNumberRetriever.send(null, DEFAULT_ERROR_MESSAGE_PROJECT_NUMBER)
.then((httpResponse) => {
if (httpResponse.statusCode === 200 &&
httpResponse.body) {
this.projectNumber = httpResponse.body.toString();
return this.projectNumber;
} else {
// No data in body.
throw new Error(DEFAULT_ERROR_MESSAGE_PROJECT_NUMBER);
}
});
}
}
/** @return A promise that resolves with the zone. */
getZone(): Promise<string> {
if (this.zone) {
return Promise.resolve(this.zone);
} else {
return this.zoneRetriever.send(null, DEFAULT_ERROR_MESSAGE_ZONE)
.then((httpResponse) => {
if (httpResponse.statusCode === 200 &&
httpResponse.body) {
const zoneName: string = httpResponse.body;
// Format: projects/327715512941/zones/us-central1-1
const matches = zoneName.match(/\/zones\/(.*)\-[a-zA-Z1-9]$/);
this.zone = matches && matches.length > 1 ? matches[1] : DEFAULT_ZONE;
return this.zone;
} else {
// No data in body.
throw new Error(DEFAULT_ERROR_MESSAGE_ZONE);
}
});
}
}
}