Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Expose APIs for managing Azure credentials from @rushstack/rush-azure-storage-build-cache-plugin.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
51 changes: 51 additions & 0 deletions common/reviews/api/rush-azure-storage-build-cache-plugin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,60 @@
```ts

import type { IRushPlugin } from '@rushstack/rush-sdk';
import type { ITerminal } from '@rushstack/node-core-library';
import type { RushConfiguration } from '@rushstack/rush-sdk';
import type { RushSession } from '@rushstack/rush-sdk';

// @public (undocumented)
export enum AzureAuthorityHosts {
// (undocumented)
AzureChina = "https://login.chinacloudapi.cn",
// (undocumented)
AzureGermany = "https://login.microsoftonline.de",
// (undocumented)
AzureGovernment = "https://login.microsoftonline.us",
// (undocumented)
AzurePublicCloud = "https://login.microsoftonline.com"
}

// @public (undocumented)
export type AzureEnvironmentNames = keyof typeof AzureAuthorityHosts;

// @public (undocumented)
export class AzureStorageAuthentication {
constructor(options: IAzureStorageAuthenticationOptions);
// (undocumented)
protected readonly _azureEnvironment: AzureEnvironmentNames;
// (undocumented)
deleteCachedCredentialsAsync(terminal: ITerminal): Promise<void>;
// (undocumented)
protected readonly _isCacheWriteAllowedByConfiguration: boolean;
// (undocumented)
protected readonly _storageAccountName: string;
// (undocumented)
protected get _storageAccountUrl(): string;
// (undocumented)
protected readonly _storageContainerName: string;
// (undocumented)
tryGetCachedCredentialAsync(): Promise<string | undefined>;
// (undocumented)
updateCachedCredentialAsync(terminal: ITerminal, credential: string): Promise<void>;
// (undocumented)
updateCachedCredentialInteractiveAsync(terminal: ITerminal): Promise<void>;
}

// @public (undocumented)
export interface IAzureStorageAuthenticationOptions {
// (undocumented)
azureEnvironment?: AzureEnvironmentNames;
// (undocumented)
isCacheWriteAllowed: boolean;
// (undocumented)
storageAccountName: string;
// (undocumented)
storageContainerName: string;
}

// @public (undocumented)
class RushAzureStorageBuildCachePlugin implements IRushPlugin {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { DeviceCodeCredential, DeviceCodeInfo } from '@azure/identity';
import {
BlobServiceClient,
ContainerSASPermissions,
generateBlobSASQueryParameters,
SASQueryParameters,
ServiceGetUserDelegationKeyResponse
} from '@azure/storage-blob';
import type { ITerminal } from '@rushstack/node-core-library';
import { CredentialCache, ICredentialCacheEntry, RushConstants } from '@rushstack/rush-sdk';
import { PrintUtilities } from '@rushstack/terminal';

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// TODO: This is a temporary workaround; it should be reverted when we upgrade to "@azure/identity" version 2.x
// import { AzureAuthorityHosts } from '@azure/identity';
/**
* @public
*/
export enum AzureAuthorityHosts {
AzureChina = 'https://login.chinacloudapi.cn',
AzureGermany = 'https://login.microsoftonline.de',
AzureGovernment = 'https://login.microsoftonline.us',
AzurePublicCloud = 'https://login.microsoftonline.com'
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

/**
* @public
*/
export type AzureEnvironmentNames = keyof typeof AzureAuthorityHosts;

/**
* @public
*/
export interface IAzureStorageAuthenticationOptions {
storageContainerName: string;
storageAccountName: string;
azureEnvironment?: AzureEnvironmentNames;
isCacheWriteAllowed: boolean;
}

const SAS_TTL_MILLISECONDS: number = 7 * 24 * 60 * 60 * 1000; // Seven days

/**
* @public
*/
export class AzureStorageAuthentication {
protected readonly _azureEnvironment: AzureEnvironmentNames;
protected readonly _storageAccountName: string;
protected readonly _storageContainerName: string;
protected readonly _isCacheWriteAllowedByConfiguration: boolean;

private __credentialCacheId: string | undefined;
private get _credentialCacheId(): string {
if (!this.__credentialCacheId) {
const cacheIdParts: string[] = [
'azure-blob-storage',
this._azureEnvironment,
this._storageAccountName,
this._storageContainerName
];

if (this._isCacheWriteAllowedByConfiguration) {
cacheIdParts.push('cacheWriteAllowed');
}

this.__credentialCacheId = cacheIdParts.join('|');
}

return this.__credentialCacheId;
}

protected get _storageAccountUrl(): string {
return `https://${this._storageAccountName}.blob.core.windows.net/`;
}

public constructor(options: IAzureStorageAuthenticationOptions) {
this._storageAccountName = options.storageAccountName;
this._storageContainerName = options.storageContainerName;
this._azureEnvironment = options.azureEnvironment || 'AzurePublicCloud';
this._isCacheWriteAllowedByConfiguration = options.isCacheWriteAllowed;
}

public async updateCachedCredentialAsync(terminal: ITerminal, credential: string): Promise<void> {
await CredentialCache.usingAsync(
{
supportEditing: true
},
async (credentialsCache: CredentialCache) => {
credentialsCache.setCacheEntry(this._credentialCacheId, credential);
await credentialsCache.saveIfModifiedAsync();
}
);
}

public async updateCachedCredentialInteractiveAsync(terminal: ITerminal): Promise<void> {
const sasQueryParameters: SASQueryParameters = await this._getSasQueryParametersAsync(terminal);
const sasString: string = sasQueryParameters.toString();

await CredentialCache.usingAsync(
{
supportEditing: true
},
async (credentialsCache: CredentialCache) => {
credentialsCache.setCacheEntry(this._credentialCacheId, sasString, sasQueryParameters.expiresOn);
await credentialsCache.saveIfModifiedAsync();
}
);
}

public async deleteCachedCredentialsAsync(terminal: ITerminal): Promise<void> {
await CredentialCache.usingAsync(
{
supportEditing: true
},
async (credentialsCache: CredentialCache) => {
credentialsCache.deleteCacheEntry(this._credentialCacheId);
await credentialsCache.saveIfModifiedAsync();
}
);
}

public async tryGetCachedCredentialAsync(): Promise<string | undefined> {
let cacheEntry: ICredentialCacheEntry | undefined;
await CredentialCache.usingAsync(
{
supportEditing: false
},
(credentialsCache: CredentialCache) => {
cacheEntry = credentialsCache.tryGetCacheEntry(this._credentialCacheId);
}
);

const expirationTime: number | undefined = cacheEntry?.expires?.getTime();
if (expirationTime && expirationTime < Date.now()) {
throw new Error(
'Cached Azure Storage credentials have expired. ' +
`Update the credentials by running "rush ${RushConstants.updateCloudCredentialsCommandName}".`
);
} else {
return cacheEntry?.credential;
}
}

private async _getSasQueryParametersAsync(terminal: ITerminal): Promise<SASQueryParameters> {
const authorityHost: string | undefined = AzureAuthorityHosts[this._azureEnvironment];
if (!authorityHost) {
throw new Error(`Unexpected Azure environment: ${this._azureEnvironment}`);
}

const DeveloperSignOnClientId: string = '04b07795-8ddb-461a-bbee-02f9e1bf7b46';
const deviceCodeCredential: DeviceCodeCredential = new DeviceCodeCredential(
'organizations',
DeveloperSignOnClientId,
(deviceCodeInfo: DeviceCodeInfo) => {
PrintUtilities.printMessageInBox(deviceCodeInfo.message, terminal);
},
{ authorityHost: authorityHost }
);
const blobServiceClient: BlobServiceClient = new BlobServiceClient(
this._storageAccountUrl,
deviceCodeCredential
);

const startsOn: Date = new Date();
const expires: Date = new Date(Date.now() + SAS_TTL_MILLISECONDS);
const key: ServiceGetUserDelegationKeyResponse = await blobServiceClient.getUserDelegationKey(
startsOn,
expires
);

const containerSasPermissions: ContainerSASPermissions = new ContainerSASPermissions();
containerSasPermissions.read = true;
containerSasPermissions.write = this._isCacheWriteAllowedByConfiguration;

const queryParameters: SASQueryParameters = generateBlobSASQueryParameters(
{
startsOn: startsOn,
expiresOn: expires,
permissions: containerSasPermissions,
containerName: this._storageContainerName
},
key,
this._storageAccountName
);

return queryParameters;
}
}
Loading