8000 Merge pull request #3329 from iclanton/expose-azure-authentication · lapolinarweb/rushstack@8c09635 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8c09635

Browse files
authored
Merge pull request microsoft#3329 from iclanton/expose-azure-authentication
[rush] Expose APIs for interacting with Azure credentials.
2 parents 43f82fe + 5ff4149 commit 8c09635

File tree

7 files changed

+277
-172
lines changed

7 files changed

+277
-172
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Expose APIs for managing Azure credentials from @rushstack/rush-azure-storage-build-cache-plugin.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/reviews/api/rush-azure-storage-build-cache-plugin.api.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,60 @@
55
```ts
66

77
import type { IRushPlugin } from '@rushstack/rush-sdk';
8+
import type { ITerminal } from '@rushstack/node-core-library';
89
import type { RushConfiguration } from '@rushstack/rush-sdk';
910
import type { RushSession } from '@rushstack/rush-sdk';
1011

12+
// @public (undocumented)
13+
export enum AzureAuthorityHosts {
14+
// (undocumented)
15+
AzureChina = "https://login.chinacloudapi.cn",
16+
// (undocumented)
17+
AzureGermany = "https://login.microsoftonline.de",
18+
// (undocumented)
19+
AzureGovernment = "https://login.microsoftonline.us",
20+
// (undocumented)
21+
AzurePublicCloud = "https://login.microsoftonline.com"
22+
}
23+
24+
// @public (undocumented)
25+
export type AzureEnvironmentNames = keyof typeof AzureAuthorityHosts;
26+
27+
// @public (undocumented)
28+
export class AzureStorageAuthentication {
29+
constructor(options: IAzureStorageAuthenticationOptions);
30+
// (undocumented)
31+
protected readonly _azureEnvironment: AzureEnvironmentNames;
32+
// (undocumented)
33+
deleteCachedCredentialsAsync(terminal: ITerminal): Promise<void>;
34+
// (undocumented)
35+
protected readonly _isCacheWriteAllowedByConfiguration: boolean;
36+
// (undocumented)
37+
protected readonly _storageAccountName: string;
38+
// (undocumented)
39+
protected get _storageAccountUrl(): string;
40+
// (undocumented)
41+
protected readonly _storageContainerName: string;
42+
// (undocumented)
43+
tryGetCachedCredentialAsync(): Promise<string | undefined>;
44+
// (undocumented)
45+
updateCachedCredentialAsync(terminal: ITerminal, credential: string): Promise<void>;
46+
// (undocumented)
47+
updateCachedCredentialInteractiveAsync(terminal: ITerminal): Promise<void>;
48+
}
49+
50+
// @public (undocumented)
51+
export interface IAzureStorageAuthenticationOptions {
52+
// (undocumented)
53+
azureEnvironment?: AzureEnvironmentNames;
54+
// (undocumented)
55+
isCacheWriteAllowed: boolean;
56+
// (undocumented)
57+
storageAccountName: string;
58+
// (undocumented)
59+
storageContainerName: string;
60+
}
61+
1162
// @public (undocumented)
1263
class RushAzureStorageBuildCachePlugin implements IRushPlugin {
1364
// (undocumented)
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { DeviceCodeCredential, DeviceCodeInfo } from '@azure/identity';
5+
import {
6+
BlobServiceClient,
7+
ContainerSASPermissions,
8+
generateBlobSASQueryParameters,
9+
SASQueryParameters,
10+
ServiceGetUserDelegationKeyResponse
11+
} from '@azure/storage-blob';
12+
import type { ITerminal } from '@rushstack/node-core-library';
13+
import { CredentialCache, ICredentialCacheEntry, RushConstants } from '@rushstack/rush-sdk';
14+
import { PrintUtilities } from '@rushstack/terminal';
15+
16+
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
17+
// TODO: This is a temporary workaround; it should be reverted when we upgrade to "@azure/identity" version 2.x
18+
// import { AzureAuthorityHosts } from '@azure/identity';
19+
/**
20+
* @public
21+
*/
22+
export enum AzureAuthorityHosts {
23+
AzureChina = 'https://login.chinacloudapi.cn',
24+
AzureGermany = 'https://login.microsoftonline.de',
25+
AzureGovernment = 'https://login.microsoftonline.us',
26+
AzurePublicCloud = 'https://login.microsoftonline.com'
27+
}
28+
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
29+
30+
/**
31+
* @public
32+
*/
33+
export type AzureEnvironmentNames = keyof typeof AzureAuthorityHosts;
34+
35+
/**
36+
* @public
37+
*/
38+
export interface IAzureStorageAuthenticationOptions {
39+
storageContainerName: string;
40+
storageAccountName: string;
41+
azureEnvironment?: AzureEnvironmentNames;
42+
isCacheWriteAllowed: boolean;
43+
}
44+
45+
const SAS_TTL_MILLISECONDS: number = 7 * 24 * 60 * 60 * 1000; // Seven days
46+
47+
/**
48+
* @public
49+
*/
50+
export class AzureStorageAuthentication {
51+
protected readonly _azureEnvironment: AzureEnvironmentNames;
52+
protected readonly _storageAccountName: string;
53+
protected readonly _storageContainerName: string;
54+
protected readonly _isCacheWriteAllowedByConfiguration: boolean;
55+
56+
private __credentialCacheId: string | undefined;
57+
private get _credentialCacheId(): string {
58+
if (!this.__credentialCacheId) {
59+
const cacheIdParts: string[] = [
60+
'azure-blob-storage',
61+
this._azureEnvironment,
62+
this._storageAccountName,
63+
this._storageContainerName
64+
];
65+
66+
if (this._isCacheWriteAllowedByConfiguration) {
67+
cacheIdParts.push('cacheWriteAllowed');
68+
}
69+
70+
this.__credentialCacheId = cacheIdParts.join('|');
71+
}
72+
73+
return this.__credentialCacheId;
74+
}
75+
76+
protected get _storageAccountUrl(): string {
77+
return `https://${this._storageAccountName}.blob.core.windows.net/`;
78+
}
79+
80+
public constructor(options: IAzureStorageAuthenticationOptions) {
81+
this._storageAccountName = options.storageAccountName;
82+
this._storageContainerName = options.storageContainerName;
83+
this._azureEnvironment = options.azureEnvironment || 'AzurePublicCloud';
84+
this._isCacheWriteAllowedByConfiguration = options.isCacheWriteAllowed;
85+
}
86+
87+
public async updateCachedCredentialAsync(terminal: ITerminal, credential: string): Promise<void> {
88+
await CredentialCache.usingAsync(
89+
{
90+
supportEditing: true
91+
},
92+
async (credentialsCache: CredentialCache) => {
93+
credentialsCache.setCacheEntry(this._credentialCacheId, credential);
94+
await credentialsCache.saveIfModifiedAsync();
95+
}
96+
);
97+
}
98+
99+
public async updateCachedCredentialInteractiveAsync(terminal: ITerminal): Promise<void> {
100+
const sasQueryParameters: SASQueryParameters = await this._getSasQueryParametersAsync(terminal);
101+
const sasString: string = sasQueryParameters.toString();
102+
103+
await CredentialCache.usingAsync(
104+
{
105+
supportEditing: true
106+
},
107+
async (credentialsCache: CredentialCache) => {
108+
credentialsCache.setCacheEntry(this._credentialCacheId, sasString, sasQueryParameters.expiresOn);
109+
await credentialsCache.saveIfModifiedAsync();
110+
}
111+
);
112+
}
113+
114+
public async deleteCachedCredentialsAsync(terminal: ITerminal): Promise<void> {
115+
await CredentialCache.usingAsync(
116+
{
117+
supportEditing: true
118+
},
119+
async (credentialsCache: CredentialCache) => {
120+
credentialsCache.deleteCacheEntry(this._credentialCacheId);
121+
await credentialsCache.saveIfModifiedAsync();
122+
}
123+
);
124+
}
125+
126+
public async tryGetCachedCredentialAsync(): Promise<string | undefined> {
127+
let cacheEntry: ICredentialCacheEntry | undefined;
128+
await CredentialCache.usingAsync(
129+
{
130+
supportEditing: false
131+
},
132+
(credentialsCache: CredentialCache) => {
133+
cacheEntry = credentialsCache.tryGetCacheEntry(this._credentialCacheId);
134+
}
135+
);
136+
137+
const expirationTime: number | undefined = cacheEntry?.expires?.getTime();
138+
if (expirationTime && expirationTime < Date.now()) {
139+
throw new Error(
140+
'Cached Azure Storage credentials have expired. ' +
141+
`Update the credentials by running "rush ${RushConstants.updateCloudCredentialsCommandName}".`
142+
);
143+
} else {
144+
return cacheEntry?.credential;
145+
}
146+
}
147+
148+
private async _getSasQueryParametersAsync(terminal: ITerminal): Promise<SASQueryParameters> {
149+
const authorityHost: string | undefined = AzureAuthorityHosts[this._azureEnvironment];
150+
if (!authorityHost) {
151+
throw new Error(`Unexpected Azure environment: ${this._azureEnvironment}`);
152+
}
153+
154+
const DeveloperSignOnClientId: string = '04b07795-8ddb-461a-bbee-02f9e1bf7b46';
155+
const deviceCodeCredential: DeviceCodeCredential = new DeviceCodeCredential(
156+
'organizations',
157+
DeveloperSignOnClientId,
158+
(deviceCodeInfo: DeviceCodeInfo) => {
159+
PrintUtilities.printMessageInBox(deviceCodeInfo.message, terminal);
160+
},
161+
{ authorityHost: authorityHost }
162+
);
163+
const blobServiceClient: BlobServiceClient = new BlobServiceClient(
164+
this._storageAccountUrl,
165+
deviceCodeCredential
166+
);
167+
168+
const startsOn: Date = new Date();
169+
const expires: Date = new Date(Date.now() + SAS_TTL_MILLISECONDS);
170+
const key: ServiceGetUserDelegationKeyResponse = await blobServiceClient.getUserDelegationKey(
171+
startsOn,
172+
expires
173+
);
174+
175+
const containerSasPermissions: ContainerSASPermissions = new ContainerSASPermissions();
176+
containerSasPermissions.read = true;
177+
containerSasPermissions.write = this._isCacheWriteAllowedByConfiguration;
178+
179+
const queryParameters: SASQueryParameters = generateBlobSASQueryParameters(
180+
{
181+
startsOn: startsOn,
182+
expiresOn: expires,
183+
permissions: containerSasPermissions,
184+
containerName: this._storageContainerName
185+
},
186+
key,
187+
this._storageAccountName
188+
);
189+
190+
return queryParameters;
191+
}
192+
}

0 commit comments

Comments
 (0)
0