diff --git a/common/changes/@microsoft/rush/azure-default-credential_2024-11-07-22-38.json b/common/changes/@microsoft/rush/azure-default-credential_2024-11-07-22-38.json new file mode 100644 index 00000000000..44676b61ec5 --- /dev/null +++ b/common/changes/@microsoft/rush/azure-default-credential_2024-11-07-22-38.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Adds two new properties to the configuration for `rush-azure-storage-build-cache-plugin`: `loginFlow` selects the flow to use for interactive authentication to Entra ID, and `readRequiresAuthentication` specifies that a SAS token is required for read and therefore expired authentication is always fatal.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md b/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md index 5b39e0eee40..5237a79fced 100644 --- a/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md +++ b/common/reviews/api/rush-azure-storage-build-cache-plugin.api.md @@ -47,7 +47,7 @@ export abstract class AzureAuthenticationBase { tryGetCachedCredentialAsync(options: ITryGetCachedCredentialOptionsLogWarning): Promise; // (undocumented) updateCachedCredentialAsync(terminal: ITerminal, credential: string): Promise; - updateCachedCredentialInteractiveAsync(terminal: ITerminal, onlyIfExistingCredentialExpiresAfter?: Date): Promise; + updateCachedCredentialInteractiveAsync(terminal: ITerminal, onlyIfExistingCredentialExpiresBefore?: Date): Promise; } // @public (undocumented) diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts b/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts index 1fe236c91aa..0d0cbf5d87a 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureAuthenticationBase.ts @@ -146,9 +146,13 @@ export abstract class AzureAuthenticationBase { } public constructor(options: IAzureAuthenticationBaseOptions) { - this._azureEnvironment = options.azureEnvironment || 'AzurePublicCloud'; + const { + azureEnvironment = 'AzurePublicCloud', + loginFlow = process.env.CODESPACES === 'true' ? 'AdoCodespacesAuth' : 'InteractiveBrowser' + } = options; + this._azureEnvironment = azureEnvironment; this._credentialUpdateCommandForLogging = options.credentialUpdateCommandForLogging; - this._loginFlow = options.loginFlow || 'DeviceCode'; + this._loginFlow = loginFlow; this._failoverOrder = options.loginFlowFailover || { AdoCodespacesAuth: 'InteractiveBrowser', InteractiveBrowser: 'DeviceCode', @@ -174,25 +178,25 @@ export abstract class AzureAuthenticationBase { * Launches an interactive flow to renew a cached credential. * * @param terminal - The terminal to log output to - * @param onlyIfExistingCredentialExpiresAfter - If specified, and a cached credential exists that is still valid - * after the date specified, no action will be taken. + * @param onlyIfExistingCredentialExpiresBefore - If specified, and a cached credential exists, action will only + * be taken if the cached credential expires before the specified date. */ public async updateCachedCredentialInteractiveAsync( terminal: ITerminal, - onlyIfExistingCredentialExpiresAfter?: Date + onlyIfExistingCredentialExpiresBefore?: Date ): Promise { await CredentialCache.usingAsync( { supportEditing: true }, async (credentialsCache: CredentialCache) => { - if (onlyIfExistingCredentialExpiresAfter) { + if (onlyIfExistingCredentialExpiresBefore) { const existingCredentialExpiration: Date | undefined = credentialsCache.tryGetCacheEntry( this._credentialCacheId )?.expires; if ( existingCredentialExpiration && - existingCredentialExpiration > onlyIfExistingCredentialExpiresAfter + existingCredentialExpiration > onlyIfExistingCredentialExpiresBefore ) { return; } diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureStorageBuildCacheProvider.ts b/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureStorageBuildCacheProvider.ts index ba329be5a72..cfbf7ff1b45 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureStorageBuildCacheProvider.ts +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/AzureStorageBuildCacheProvider.ts @@ -24,6 +24,7 @@ import { export interface IAzureStorageBuildCacheProviderOptions extends IAzureStorageAuthenticationOptions { blobPrefix?: string; + readRequiresAuthentication?: boolean; } interface IBlobError extends Error { @@ -43,6 +44,7 @@ export class AzureStorageBuildCacheProvider { private readonly _blobPrefix: string | undefined; private readonly _environmentCredential: string | undefined; + private readonly _readRequiresAuthentication: boolean; public get isCacheWriteAllowed(): boolean { return EnvironmentConfiguration.buildCacheWriteAllowed ?? this._isCacheWriteAllowedByConfiguration; @@ -58,6 +60,7 @@ export class AzureStorageBuildCacheProvider this._blobPrefix = options.blobPrefix; this._environmentCredential = EnvironmentConfiguration.buildCacheCredential; + this._readRequiresAuthentication = !!options.readRequiresAuthentication; if (!(this._azureEnvironment in AzureAuthorityHosts)) { throw new Error( @@ -208,8 +211,8 @@ export class AzureStorageBuildCacheProvider if (sasString) { const connectionString: string = this._getConnectionString(sasString); blobServiceClient = BlobServiceClient.fromConnectionString(connectionString); - } else if (!this._isCacheWriteAllowedByConfiguration) { - // If cache write isn't allowed and we don't have a credential, assume the blob supports anonymous read + } else if (!this._readRequiresAuthentication && !this._isCacheWriteAllowedByConfiguration) { + // If we don't have a credential and read doesn't require authentication, we can still read from the cache. blobServiceClient = new BlobServiceClient(this._storageAccountUrl); } else { throw new Error( diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureInteractiveAuthPlugin.ts b/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureInteractiveAuthPlugin.ts index 310683fecd1..aef43ad6df2 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureInteractiveAuthPlugin.ts +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureInteractiveAuthPlugin.ts @@ -27,7 +27,7 @@ export interface IAzureInteractiveAuthOptions { /** * Login flow to use for interactive authentication. - * @defaultValue 'deviceCode' + * @defaultValue 'AdoCodespacesAuth' if on GitHub Codespaces, 'InteractiveBrowser' otherwise */ readonly loginFlow?: LoginFlowType; @@ -86,7 +86,7 @@ export default class RushAzureInteractieAuthPlugin implements IRushPlugin { storageContainerName, azureEnvironment = 'AzurePublicCloud', minimumValidityInMinutes, - loginFlow = 'DeviceCode' + loginFlow = process.env.CODESPACES ? 'AdoCodespacesAuth' : 'InteractiveBrowser' } = options; const logger: ILogger = rushSession.getLogger(PLUGIN_NAME); diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureStorageBuildCachePlugin.ts b/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureStorageBuildCachePlugin.ts index 975a95553ed..a0f4883fcad 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureStorageBuildCachePlugin.ts +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/RushAzureStorageBuildCachePlugin.ts @@ -2,7 +2,7 @@ // See LICENSE in the project root for license information. import type { IRushPlugin, RushSession, RushConfiguration } from '@rushstack/rush-sdk'; -import type { AzureEnvironmentName } from './AzureAuthenticationBase'; +import type { AzureEnvironmentName, LoginFlowType } from './AzureAuthenticationBase'; const PLUGIN_NAME: string = 'AzureStorageBuildCachePlugin'; @@ -25,6 +25,12 @@ interface IAzureBlobStorageConfigurationJson { */ azureEnvironment?: AzureEnvironmentName; + /** + * Login flow to use for interactive authentication. + * @defaultValue 'AdoCodespacesAuth' if on GitHub Codespaces, 'InteractiveBrowser' otherwise + */ + readonly loginFlow?: LoginFlowType; + /** * An optional prefix for cache item blob names. */ @@ -34,6 +40,11 @@ interface IAzureBlobStorageConfigurationJson { * If set to true, allow writing to the cache. Defaults to false. */ isCacheWriteAllowed?: boolean; + + /** + * If set to true, reading the cache requires authentication. Defaults to false. + */ + readRequiresAuthentication?: boolean; } /** @@ -55,7 +66,9 @@ export class RushAzureStorageBuildCachePlugin implements IRushPlugin { storageContainerName: azureBlobStorageConfiguration.storageContainerName, azureEnvironment: azureBlobStorageConfiguration.azureEnvironment, blobPrefix: azureBlobStorageConfiguration.blobPrefix, - isCacheWriteAllowed: !!azureBlobStorageConfiguration.isCacheWriteAllowed + loginFlow: azureBlobStorageConfiguration.loginFlow, + isCacheWriteAllowed: !!azureBlobStorageConfiguration.isCacheWriteAllowed, + readRequiresAuthentication: !!azureBlobStorageConfiguration.readRequiresAuthentication }); }); }); diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-blob-storage-config.schema.json b/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-blob-storage-config.schema.json index 835487d5ee1..d471c614172 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-blob-storage-config.schema.json +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-blob-storage-config.schema.json @@ -25,6 +25,12 @@ "enum": ["AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment"] }, + "loginFlow": { + "type": "string", + "description": "The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise.", + "enum": ["AdoCodespacesAuth", "InteractiveBrowser", "DeviceCode"] + }, + "blobPrefix": { "type": "string", "description": "An optional prefix for cache item blob names." @@ -33,6 +39,11 @@ "isCacheWriteAllowed": { "type": "boolean", "description": "If set to true, allow writing to the cache. Defaults to false." + }, + + "readRequiresAuthentication": { + "type": "boolean", + "description": "If set to true, reading the cache requires authentication. Defaults to false." } } } diff --git a/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-interactive-auth.schema.json b/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-interactive-auth.schema.json index a733cd9d6e7..130d6a04d6e 100644 --- a/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-interactive-auth.schema.json +++ b/rush-plugins/rush-azure-storage-build-cache-plugin/src/schemas/azure-interactive-auth.schema.json @@ -25,6 +25,12 @@ "enum": ["AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment"] }, + "loginFlow": { + "type": "string", + "description": "The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise.", + "enum": ["AdoCodespacesAuth", "InteractiveBrowser", "DeviceCode"] + }, + "minimumValidityInMinutes": { "type": "number", "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."