From 136ca7eb8593a4cabc9c593c34aeeda357de4887 Mon Sep 17 00:00:00 2001 From: Maddison Das <272712104+MaddyMicrosoft@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:40:55 +0100 Subject: [PATCH 1/3] Add max-context-population input to override Azure PowerShell MaxContextPopulation Azure PowerShell loads a maximum of 25 subscription contexts by default (MaxContextPopulation = 25). When enable-AzPSSession is true and the identity can access more than 25 subscriptions, only a subset of contexts is loaded, so downstream cmdlets that enumerate or target other subscriptions behave inconsistently. Previously the only workaround was to disable the PowerShell session and call Connect-AzAccount manually. Expose the existing Connect-AzAccount -MaxContextPopulation parameter through a new optional max-context-population input: - action.yml: new optional input (no default). - LoginConfig: reads and trims the value; validate() rejects non-integer or out-of-range values (must be -1 or a positive integer) and warns when it is set without enable-AzPSSession, where it has no effect. - AzPSScriptBuilder: passes -MaxContextPopulation only when the input is set. - AzPSLogin.ps1: adds an [int]$MaxContextPopulation param, forwarded to Connect-AzAccount only when bound so the default behavior is unchanged. - README: input table row and a max-context-population section, including a note that -1 loads all contexts and can slow login for large tenants. - Tests: cover the arg being passed when set and omitted when unset. Fully backward-compatible: when the input is unset, the parameter is never passed and the Azure PowerShell default of 25 applies. Only affects the enable-AzPSSession path. Fixes #606. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 22 ++++++++++ .../PowerShell/AzPSScriptBuilder.test.ts | 43 +++++++++++++++++++ action.yml | 3 ++ src/PowerShell/AzPSLogin.ps1 | 5 ++- src/PowerShell/AzPSScriptBuilder.ts | 3 ++ src/common/LoginConfig.ts | 11 +++++ 6 files changed, 86 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 486be60c0..aba93de8d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [`allow-no-subscriptions`](#allow-no-subscriptions) - [`audience`](#audience) - [`auth-type`](#auth-type) + - [`max-context-population`](#max-context-population) - [Workflow Examples](#workflow-examples) - [Login With OpenID Connect (OIDC) \[Recommended\]](#login-with-openid-connect-oidc-recommended) - [Login With a Service Principal Secret](#login-with-a-service-principal-secret) @@ -144,6 +145,7 @@ Customers using v1 should migrate to v3. End-of-life releases no longer receive |allow-no-subscriptions|false|boolean|false|if login without subscription is allowed| |audience|false|string|api://AzureADTokenExchange|the audience to get the JWT ID token from GitHub OIDC provider| |auth-type|false|string|SERVICE_PRINCIPAL|the auth type| +|max-context-population|false|integer||only used when `enable-AzPSSession` is `true`; overrides the Azure PowerShell `MaxContextPopulation`. Defaults to the Azure PowerShell default of 25 when unset.| ### `client-id` @@ -233,6 +235,26 @@ The input parameter `auth-type` specifies the type of authentication. The defaul Refer to [Login With System-assigned Managed Identity](#login-with-system-assigned-managed-identity) and [Login With User-assigned Managed Identity](#login-with-user-assigned-managed-identity) for its usage. +### `max-context-population` + +The input parameter `max-context-population` is only used when [`enable-AzPSSession`](#enable-azpssession) is `true`. It overrides the Azure PowerShell `MaxContextPopulation` value that `Connect-AzAccount` uses, which controls how many subscription contexts are loaded into the session. + +Azure PowerShell loads a maximum of 25 subscription contexts by default. When the identity has access to more than 25 subscriptions, only a subset is loaded, so commands that enumerate or target subscriptions outside that subset may behave inconsistently. Set `max-context-population` to `-1` to load all subscriptions, or to a positive integer to load a specific number. When it is unset, the Azure PowerShell default of 25 applies and behavior is unchanged. + +```yaml + - name: Azure login + uses: azure/login@v3 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + max-context-population: -1 +``` + +> [!NOTE] +> Loading all subscription contexts with `-1` makes `Connect-AzAccount` slower when the identity can access a large number of subscriptions, because every subscription is enumerated during login. Set it only when your workflow needs contexts beyond the default 25. + ## Workflow Examples ### Login With OpenID Connect (OIDC) [Recommended] diff --git a/__tests__/PowerShell/AzPSScriptBuilder.test.ts b/__tests__/PowerShell/AzPSScriptBuilder.test.ts index a1dba885c..bc01eef97 100644 --- a/__tests__/PowerShell/AzPSScriptBuilder.test.ts +++ b/__tests__/PowerShell/AzPSScriptBuilder.test.ts @@ -160,6 +160,49 @@ describe("Building the Az PS login invocation", () => { }); }); + test('max-context-population set: value passed as -MaxContextPopulation param', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('max-context-population', '-1'); + const creds = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + }; + setEnv('creds', JSON.stringify(creds)); + + const loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args }) => { + expect(args).toEqual(expect.arrayContaining([ + '-MaxContextPopulation', '-1', + ])); + }); + }); + + test('max-context-population unset: -MaxContextPopulation param omitted', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + const creds = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + }; + setEnv('creds', JSON.stringify(creds)); + + const loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args }) => { + expect(args).not.toContain('-MaxContextPopulation'); + }); + }); + test('SECURITY: adversarial ArmEndpoint travels as a discrete argv element', () => { setEnv('environment', 'azurestack'); setEnv('enable-AzPSSession', 'true'); diff --git a/action.yml b/action.yml index bf52a453b..3e2be77b1 100644 --- a/action.yml +++ b/action.yml @@ -34,6 +34,9 @@ inputs: description: 'The type of authentication. Supported values are SERVICE_PRINCIPAL, IDENTITY. Default value is SERVICE_PRINCIPAL' required: false default: 'SERVICE_PRINCIPAL' + max-context-population: + description: 'Only used when enable-AzPSSession is true. Overrides the Azure PowerShell MaxContextPopulation used by Connect-AzAccount (the number of subscription contexts loaded). Set to -1 to load all subscriptions, or a positive integer. When unset, the Azure PowerShell default of 25 applies.' + required: false branding: icon: 'login.svg' color: 'blue' diff --git a/src/PowerShell/AzPSLogin.ps1 b/src/PowerShell/AzPSLogin.ps1 index c084a62e8..52f6dd04f 100644 --- a/src/PowerShell/AzPSLogin.ps1 +++ b/src/PowerShell/AzPSLogin.ps1 @@ -14,7 +14,9 @@ param( [string]$ApplicationId, - [string]$ArmEndpoint + [string]$ArmEndpoint, + + [int]$MaxContextPopulation ) $ErrorActionPreference = 'Stop' @@ -34,6 +36,7 @@ try { } if ($Tenant) { $connectArgs['Tenant'] = $Tenant } if ($Subscription) { $connectArgs['Subscription'] = $Subscription } + if ($PSBoundParameters.ContainsKey('MaxContextPopulation')) { $connectArgs['MaxContextPopulation'] = $MaxContextPopulation } if ($AuthType -eq 'SERVICE_PRINCIPAL') { $connectArgs['ServicePrincipal'] = $true diff --git a/src/PowerShell/AzPSScriptBuilder.ts b/src/PowerShell/AzPSScriptBuilder.ts index d5470ea30..b6f26e12c 100644 --- a/src/PowerShell/AzPSScriptBuilder.ts +++ b/src/PowerShell/AzPSScriptBuilder.ts @@ -53,6 +53,9 @@ export default class AzPSScriptBuilder { if (loginConfig.environment.toLowerCase() === 'azurestack') { args.push('-ArmEndpoint', loginConfig.resourceManagerEndpointUrl); } + if (loginConfig.maxContextPopulation) { + args.push('-MaxContextPopulation', loginConfig.maxContextPopulation); + } if (loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) { args.push('-ApplicationId', loginConfig.servicePrincipalId); diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index b9939c588..d40299354 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -25,6 +25,7 @@ export class LoginConfig { enableAzPSSession: boolean; audience: string; federatedToken: string; + maxContextPopulation: string; async initialize() { this.environment = core.getInput("environment").toLowerCase(); @@ -41,6 +42,7 @@ export class LoginConfig { this.audience = core.getInput('audience', { required: false }); this.federatedToken = null; + this.maxContextPopulation = core.getInput('max-context-population', { required: false }).trim(); this.mask(this.servicePrincipalId); this.mask(this.servicePrincipalSecret); @@ -106,6 +108,15 @@ export class LoginConfig { if (!this.subscriptionId && !this.allowNoSubscriptionsLogin) { throw new Error("Ensure 'subscription-id' is supplied or 'allow-no-subscriptions' is 'true'."); } + if (this.maxContextPopulation) { + const maxContextPopulationNumber = Number(this.maxContextPopulation); + if (!Number.isInteger(maxContextPopulationNumber) || (maxContextPopulationNumber !== -1 && maxContextPopulationNumber < 1)) { + throw new Error(`Invalid value '${this.maxContextPopulation}' for 'max-context-population'. It must be -1 (load all subscription contexts) or a positive integer.`); + } + if (!this.enableAzPSSession) { + core.warning("'max-context-population' is only applied when 'enable-AzPSSession' is 'true'. It has no effect on Azure CLI login and will be ignored."); + } + } } mask(parameterValue: string) { From 31eb85302da47f7ed7be060cbc7a2c64b869a1de Mon Sep 17 00:00:00 2001 From: Maddison Das <272712104+MaddyMicrosoft@users.noreply.github.com> Date: Wed, 9 Sep 2026 03:01:23 +0100 Subject: [PATCH 2/3] Harden max-context-population validation to match Connect-AzAccount range Validate the raw input string rather than Number(), which accepts scientific, hex and float forms (1e3, 0x10, 5.0) and out-of-range values that then fail Connect-AzAccount's [int] binding with a confusing error. Accept only -1 or a plain positive integer within 1..2147483647, matching the cmdlet's ValidateRange(-1, 2147483647). Add unit tests for the accepted and rejected cases, and note the bound in the README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- __tests__/LoginConfig.test.ts | 31 +++++++++++++++++++++++++++++++ src/common/LoginConfig.ts | 10 +++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aba93de8d..f5d41049f 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ Refer to [Login With System-assigned Managed Identity](#login-with-system-assign The input parameter `max-context-population` is only used when [`enable-AzPSSession`](#enable-azpssession) is `true`. It overrides the Azure PowerShell `MaxContextPopulation` value that `Connect-AzAccount` uses, which controls how many subscription contexts are loaded into the session. -Azure PowerShell loads a maximum of 25 subscription contexts by default. When the identity has access to more than 25 subscriptions, only a subset is loaded, so commands that enumerate or target subscriptions outside that subset may behave inconsistently. Set `max-context-population` to `-1` to load all subscriptions, or to a positive integer to load a specific number. When it is unset, the Azure PowerShell default of 25 applies and behavior is unchanged. +Azure PowerShell loads a maximum of 25 subscription contexts by default. When the identity has access to more than 25 subscriptions, only a subset is loaded, so commands that enumerate or target subscriptions outside that subset may behave inconsistently. Set `max-context-population` to `-1` to load all subscriptions, or to a positive integer (1 to 2147483647) to load a specific number. When it is unset, the Azure PowerShell default of 25 applies and behavior is unchanged. ```yaml - name: Azure login diff --git a/__tests__/LoginConfig.test.ts b/__tests__/LoginConfig.test.ts index ee5d67b2c..e3a511a0b 100644 --- a/__tests__/LoginConfig.test.ts +++ b/__tests__/LoginConfig.test.ts @@ -269,4 +269,35 @@ describe("LoginConfig Test", () => { expect(loginConfig.subscriptionId).toBe(""); }); + async function initWithMaxContextPopulation(value: string): Promise { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + setEnv('max-context-population', value); + const loginConfig = new LoginConfig(); + await loginConfig.initialize(); + return loginConfig; + } + + test.each(['-1', '1', '25', '2147483647'])('validate accepts max-context-population=%s', async (value) => { + const loginConfig = await initWithMaxContextPopulation(value); + loginConfig.validate(); + expect(loginConfig.maxContextPopulation).toBe(value); + }); + + test.each(['0', '-2', '1e3', '0x10', '5.0', '2147483648', 'abc'])('validate rejects invalid max-context-population=%s', async (value) => { + const loginConfig = await initWithMaxContextPopulation(value); + testValidateWithErrorMessage(loginConfig, "for 'max-context-population'. It must be -1"); + }); + + test('whitespace-only max-context-population is treated as unset', async () => { + const loginConfig = await initWithMaxContextPopulation(' '); + loginConfig.validate(); + expect(loginConfig.maxContextPopulation).toBe(''); + }); + }); \ No newline at end of file diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index d40299354..e73d4c2ee 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -109,9 +109,13 @@ export class LoginConfig { throw new Error("Ensure 'subscription-id' is supplied or 'allow-no-subscriptions' is 'true'."); } if (this.maxContextPopulation) { - const maxContextPopulationNumber = Number(this.maxContextPopulation); - if (!Number.isInteger(maxContextPopulationNumber) || (maxContextPopulationNumber !== -1 && maxContextPopulationNumber < 1)) { - throw new Error(`Invalid value '${this.maxContextPopulation}' for 'max-context-population'. It must be -1 (load all subscription contexts) or a positive integer.`); + // Validate the raw string (not Number(), which accepts 1e3/0x10/5.0 + // and out-of-range values that PowerShell's [int] then rejects). + const INT32_MAX = 2147483647; + const isValid = this.maxContextPopulation === '-1' + || (/^[1-9][0-9]*$/.test(this.maxContextPopulation) && Number(this.maxContextPopulation) <= INT32_MAX); + if (!isValid) { + throw new Error(`Invalid value '${this.maxContextPopulation}' for 'max-context-population'. It must be -1 (load all subscription contexts) or a positive integer between 1 and ${INT32_MAX}.`); } if (!this.enableAzPSSession) { core.warning("'max-context-population' is only applied when 'enable-AzPSSession' is 'true'. It has no effect on Azure CLI login and will be ignored."); From f50df4e343f9ff0f3298faec4ba7d004aa17ecf4 Mon Sep 17 00:00:00 2001 From: Maddison Das <272712104+MaddyMicrosoft@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:57:51 +0100 Subject: [PATCH 3/3] docs: note mask-client-id and max-context-population are available in azure/login@v3 Add an 'Available in azure/login@v3' label under each input heading. Both are v3-only inputs (not present in v2), so this documents the version and points consumers at v3. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9197cd16a..18890ba58 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,8 @@ Refer to [Login With OpenID Connect (OIDC)](#login-with-openid-connect-oidc-reco ### `mask-client-id` +_Available in `azure/login@v3`._ + The input parameter `mask-client-id` controls whether the login client id is registered as a secret and masked in the workflow logs. It defaults to `true`. Set it to `false` when the client id is not treated as sensitive and masking gets in the way, for example when the same value appears in log output or command results that you need to read. @@ -272,6 +274,8 @@ Refer to [Login With System-assigned Managed Identity](#login-with-system-assign ### `max-context-population` +_Available in `azure/login@v3`._ + The input parameter `max-context-population` is only used when [`enable-AzPSSession`](#enable-azpssession) is `true`. It overrides the Azure PowerShell `MaxContextPopulation` value that `Connect-AzAccount` uses, which controls how many subscription contexts are loaded into the session. Azure PowerShell loads a maximum of 25 subscription contexts by default. When the identity has access to more than 25 subscriptions, only a subset is loaded, so commands that enumerate or target subscriptions outside that subset may behave inconsistently. Set `max-context-population` to `-1` to load all subscriptions, or to a positive integer (1 to 2147483647) to load a specific number. When it is unset, the Azure PowerShell default of 25 applies and behavior is unchanged.