8000 Merge pull request #3313 from dmichon-msft/rush-azure-interactive-aut… · lapolinarweb/rushstack@ec68b61 · GitHub
[go: up one dir, main page]

Skip to content

Commit ec68b61

Browse files
authored
Merge pull request microsoft#3313 from dmichon-msft/rush-azure-interactive-auth-plugin
[rush-azure-storage-build-cache-plugin] Add plugin for Azure authentication prompt
2 parents 8c09635 + 52cdd8c commit ec68b61

File tree

8 files changed

+218
-10
lines changed

8 files changed

+218
-10
lines changed

apps/rush-lib/src/pluginFramework/PluginManager.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ export class PluginManager {
5353
const builtInPluginConfigurations: IBuiltInPluginConfiguration[] = options.builtInPluginConfigurations;
5454

5555
const ownPackageJsonDependencies: Record<string, string> = require('../../package.json').dependencies;
56-
function tryAddBuiltInPlugin(builtInPluginName: string): void {
57-
const pluginPackageName: string = `@rushstack/${builtInPluginName}`;
56+
function tryAddBuiltInPlugin(builtInPluginName: string, pluginPackageName?: string): void {
57+
if (!pluginPackageName) {
58+
pluginPackageName = `@rushstack/${builtInPluginName}`;
59+
}
5860
if (ownPackageJsonDependencies[pluginPackageName]) {
5961
builtInPluginConfigurations.push({
6062
packageName: pluginPackageName,
@@ -69,6 +71,13 @@ export class PluginManager {
6971

7072
tryAddBuiltInPlugin('rush-amazon-s3-build-cache-plugin');
7173
tryAddBuiltInPlugin('rush-azure-storage-build-cache-plugin');
74+
// This is a secondary plugin inside the `@rushstack/rush-azure-storage-build-cache-plugin`
75+
// package. Because that package comes with Rush (for now), it needs to get registered here.
76+
// If the necessary config file doesn't exist, this plugin doesn't do anything.
77+
tryAddBuiltInPlugin(
78+
'rush-azure-interactive-auth-plugin',
79+
'@rushstack/rush-azure-storage-build-cache-plugin'
80+
);
7281

7382
this._builtInPluginLoaders = builtInPluginConfigurations.map((pluginConfiguration) => {
7483
return new BuiltInPluginLoader({

apps/rush/src/start-dev.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { RushCommandSelector } from './RushCommandSelector';
1111

1212
const builtInPluginConfigurations: rushLib._IBuiltInPluginConfiguration[] = [];
1313

14-
function includePlugin(pluginName: string): void {
15-
const pluginPackageName: string = `@rushstack/${pluginName}`;
14+
function includePlugin(pluginName: string, pluginPackageName?: string): void {
15+
if (!pluginPackageName) {< 6D40 /span>
16+
pluginPackageName = `@rushstack/${pluginName}`;
17+
}
1618
builtInPluginConfigurations.push({
1719
packageName: pluginPackageName,
1820
pluginName: pluginName,
@@ -25,6 +27,8 @@ function includePlugin(pluginName: string): void {
2527

2628
includePlugin('rush-amazon-s3-build-cache-plugin');
2729
includePlugin('rush-azure-storage-build-cache-plugin');
30+
// Including this here so that developers can reuse it without installing the plugin a second time
31+
includePlugin('rush-azure-interactive-auth-plugin', '@rushstack/rush-azure-storage-build-cache-plugin');
2832

2933
const currentPackageVersion: string = PackageJsonLookup.loadOwnPackageJson(__dirname).version;
3034
RushCommandSelector.execute(currentPackageVersion, rushLib, {
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": "Add an additional plugin to rush-azure-storage-build-cache-plugin that can be used to prompt for Azure authentication before a command runs.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ export class AzureStorageAuthentication {
4343
tryGetCachedCredentialAsync(): Promise<string | undefined>;
4444
// (undocumented)
4545
updateCachedCredentialAsync(terminal: ITerminal, credential: string): Promise<void>;
46-
// (undocumented)
47-
updateCachedCredentialInteractiveAsync(terminal: ITerminal): Promise<void>;
46+
updateCachedCredentialInteractiveAsync(terminal: ITerminal, onlyIfExistingCredentialExpiresAfter?: Date): Promise<void>;
4847
}
4948

5049
// @public (undocumented)

rush-plugins/rush-azure-storage-build-cache-plugin/rush-plugin-manifest.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
"description": "Rush plugin for Azure storage cloud build cache",
77
"entryPoint": "lib/index.js",
88
"optionsSchema": "lib/schemas/azure-blob-storage-config.schema.json"
9+
},
10+
{
11+
"pluginName": "rush-azure-interactive-auth-plugin",
12+
"description": "Rush plugin for interactive authentication to Azure",
13+
"entryPoint": "lib/RushAzureInteractiveAuthPlugin.js",
14+
"optionsSchema": "lib/schemas/azure-interactive-auth.schema.json"
915
}
1016
]
1117
}

rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureStorageAuthentication.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,31 @@ export class AzureStorageAuthentication {
9696
);
9797
}
9898

99-
public async updateCachedCredentialInteractiveAsync(terminal: ITerminal): Promise<void> {
100-
const sasQueryParameters: SASQueryParameters = await this._getSasQueryParametersAsync(terminal);
101-
const sasString: string = sasQueryParameters.toString();
102-
99+
/**
100+
* Launches an interactive flow to renew a cached credential.
101+
*
102+
* @param terminal - The terminal to log output to
103+
* @param onlyIfExistingCredentialExpiresAfter - If specified, and a cached credential exists that is still valid
104+
* after the date specified, no action will be taken.
105+
*/
106+
public async updateCachedCredentialInteractiveAsync(terminal: ITerminal, onlyIfExistingCredentialExpiresAfter?: Date): Promise<void> {
103107
await CredentialCache.usingAsync(
104108
{
105109
supportEditing: true
106110
},
107111
async (credentialsCache: CredentialCache) => {
112+
if (onlyIfExistingCredentialExpiresAfter) {
113+
const existingCredentialExpiration: Date | undefined = credentialsCache.tryGetCacheEntry(
114+
this._credentialCacheId
115+
)?.expires;
116+
if (existingCredentialExpiration && existingCredentialExpiration > onlyIfExistingCredentialExpiresAfter) {
117+
return;
118+
}
119+
}
120+
121+
const sasQueryParameters: SASQueryParameters = await this._getSasQueryParametersAsync(terminal);
122+
const sasString: string = sasQueryParameters.toString();
123+
108124
credentialsCache.setCacheEntry(this._credentialCacheId, sasString, sasQueryParameters.expiresOn);
109125
await credentialsCache.saveIfModifiedAsync();
110126
}
Lines changed: 115 additions & 0 deletions
26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 type { IRushPlugin, RushSession, RushConfiguration, ILogger } from '@rushstack/rush-sdk';
5+
import type { AzureEnvironmentNames } from './AzureStorageAuthentication';
6+
7+
const PLUGIN_NAME: 'AzureInteractiveAuthPlugin' = 'AzureInteractiveAuthPlugin';
8+
9+
/**
10+
* @public
11+
*/
12+
export interface IAzureInteractiveAuthOptions {
13+
/**
14+
* The name of the the Azure storage account to authenticate to.
15+
*/
16+
readonly storageAccountName: string;
17+
18+
/**
19+
* The name of the container in the Azure storage account to authenticate to.
20+
*/
21+
readonly storageContainerName: string;
22+
23+
/**
24+
* The Azure environment the storage account exists in. Defaults to AzureCloud.
25+
*/
+
readonly azureEnvironment?: AzureEnvironmentNames;
27+
28+
/**
29+
* If specified and a credential exists that will be valid for at least this many minutes from the time
30+
* of execution, no action will be taken.
31+
*/
32+
readonly minimumValidityInMinutes?: number;
33+
34+
/**
35+
* The set of Rush global commands before which credentials should be updated.
36+
*/
37+
readonly globalCommands?: string[];
38+
39+
/**
40+
* The set of Rush phased commands before which credentials should be updated.
41+
*/
42+
readonly phasedCommands?: string[];
43+
}
44+
45+
/**
46+
* This plugin is for performing interactive authentication to an arbitrary Azure blob storage account.
47+
* It is meant to be used for scenarios where custom commands may interact with Azure blob storage beyond
48+
* the scope of the build cache (for build cache, use the RushAzureStorageBuildCachePlugin).
49+
*
50+
* However, since the authentication has the same dependencies, if the repository already uses the build
51+
* cache plugin, the additional functionality for authentication can be provided at minimal cost.
52+
*
53+
* @public
54+
*/
55+
export default class RushAzureInteractieAuthPlugin implements IRushPlugin {
56+
private readonly _options: IAzureInteractiveAuthOptions | undefined;
57+
58+
public readonly pluginName: 'AzureInteractiveAuthPlugin' = PLUGIN_NAME;
59+
60+
public constructor(options: IAzureInteractiveAuthOptions | undefined) {
61+
this._options = options;
62+
}
63+
64+
public apply(rushSession: RushSession, rushConfig: RushConfiguration): void {
65+
const options: IAzureInteractiveAuthOptions | undefined = this._options;
66+
67+
if (!options) {
68+
// Plugin is not enabled.
69+
return;
70+
}
71+
72+
const { globalCommands, phasedCommands } = options;
73+
74+
const { hooks } = rushSession;
75+
76+
const handler: () => Promise<void> = async () => {
77+
const { AzureStorageAuthentication } = await import('./AzureStorageAuthentication');
78+
const {
79+
storageAccountName,
80+
storageContainerName,
81+
azureEnvironment = 'AzurePublicCloud',
82+
minimumValidityInMinutes
83+
} = options;
84+
85+
const logger: ILogger = rushSession.getLogger(PLUGIN_NAME);
86+
logger.terminal.writeLine(
87+
`Authenticating to Azure container "${storageContainerName}" on account "${storageAccountName}" in environment "${azureEnvironment}".`
88+
);
89+
90+
let minimumExpiry: Date | undefined;
91+
if (typeof minimumValidityInMinutes === 'number') {
92+
minimumExpiry = new Date(Date.now() + minimumValidityInMinutes * 60 * 1000);
93+
}
94+
95+
await new AzureStorageAuthentication({
96+
storageAccountName: storageAccountName,
97+
storageContainerName: storageContainerName,
98+
azureEnvironment: options.azureEnvironment,
99+
isCacheWriteAllowed: true
100+
}).updateCachedCredentialInteractiveAsync(logger.terminal, minimumExpiry);
101+
};
102+
103+
if (globalCommands) {
104+
for (const commandName of globalCommands) {
105+
hooks.runGlobalCustomCommand.for(commandName).tapPromise(PLUGIN_NAME, handler);
106+
}
107+
}
108+
109+
if (phasedCommands) {
110+
for (const commandName of phasedCommands) {
111+
hooks.runPhasedCommand.for(commandName).tapPromise(PLUGIN_NAME, handler);
112+
}
113+
}
114+
}
115+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "Configuration for Azure interactive auth prompt",
4+
5+
"type": "object",
6+
7+
"additionalProperties": false,
8+
9+
"required": ["storageAccountName", "storageContainerName"],
10+
11+
"properties": {
12+
"storageAccountName": {
13+
"type": "string",
14+
"description": "(Required) The name of the the Azure storage account to authenticate to."
15+
},
16+
17+
"storageContainerName": {
18+
"type": "string",
19+
"description": "(Required) The name of the container in the Azure storage account to authenticate to."
20+
},
21+
22+
"azureEnvironment": {
23+
"type": "string",
24+
"description": "The Azure environment the storage account exists in. Defaults to AzurePublicCloud.",
25+
"enum": ["AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment"]
26+
},
27+
28+
"minimumValidityInMinutes": {
29+
"type": "number",
30+
"description": "If specified and a credential exists that will be valid for at least this many minutes from the time of execution, no action will be taken."
31+
},
32+
33+
"globalCommands": {
34+
"type": "array",
35+
"description": "The set of global rush commands before which this plugin should update credentials.",
36+
"items": {
37+
"type": "string"
38+
}
39+
},
40+
41+
"phasedCommands": {
42+
"type": "array",
43+
"description": "The set of phased rush commands before which this plugin should update credentials.",
44+
"items": {
45+
"type": "string"
46+
}
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)
0