Add the ability to prevent the masking of clientId - #634
Add the ability to prevent the masking of clientId#634Waiariki Koia (the-coding-cuzzy) wants to merge 5 commits into
Conversation
|
@microsoft-github-policy-service agree company="Global Dairy Trade" |
|
Waiariki Koia (@the-coding-cuzzy) Thank you for opening this PR. This change is under review with our security team. I will keep you updated. |
MaddyMicrosoft
left a comment
There was a problem hiding this comment.
Thanks for this, and for the clear reviewer notes.
Two of your calls are correct and worth confirming:
- Keep the
!== "false"parse, don't switch togetBooleanInput.getBooleanInputthrows on unrecognised values, which fails away from the safe default; your parse keeps a missing or malformed input on the masked path, which is the right behaviour for a masking flag. - Scoping to only the client-id (secret and federated token stay masked) is correct.
Security reviewed this and is happy to move forward. Please add a line to the mask-client-id section of the README along these lines: the client-id is effectively a username, low sensitivity on its own and only useful to an attacker who already holds the client secret or certificate, so disabling masking is reasonable when the value is treated as configuration.
Otherwise just the test fix and the small README rendering issues I've flagged inline.
There was a problem hiding this comment.
🟡 Changes recommended
A newly added unit test sets mask-client-id to true while asserting the false behavior, and the README introduces incorrect “Github” casing that should be corrected.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds an opt-out flag to control whether the client-id is registered as a masked secret in workflow logs, defaulting to the current (masked) behavior to preserve backward compatibility.
Changes:
- Adds a new
mask-client-idinput (defaulttrue) to allow disabling client-id masking. - Gates
core.setSecret(client-id)behind the newmaskClientIdflag inLoginConfig.initialize(). - Updates README documentation and adds test coverage for the new input behavior.
File summaries
| File | Description |
|---|---|
src/common/LoginConfig.ts |
Adds maskClientId and conditionally masks servicePrincipalId based on the new input. |
action.yml |
Declares the new optional mask-client-id input with default true. |
README.md |
Documents the new input and explains when it has an effect. |
__tests__/LoginConfig.test.ts |
Adds tests for mask-client-id set to true, false, and absent. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| setEnv('tenant-id', 'tenant-id'); | ||
| setEnv('subscription-id', 'subscription-id'); | ||
| setEnv('client-id', 'client-id'); | ||
| setEnv('mask-client-id', 'true'); |
There was a problem hiding this comment.
🟡 Changes recommended
The new boolean parsing should be hardened (whitespace) and the added tests should assert the actual masking side effect (core.setSecret) to prevent regressions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
tests/LoginConfig.test.ts:198
- This test should also verify that opting out (
mask-client-id=false) actually prevents masking, i.e.,core.setSecretis not called.
test('initialize with mask-client-id=false', async () => {
setEnv('environment', 'azureusgovernment');
setEnv('enable-AzPSSession', 'false');
setEnv('allow-no-subscriptions', 'true');
setEnv('auth-type', 'SERVICE_PRINCIPAL');
tests/LoginConfig.test.ts:214
- When
mask-client-idis omitted, the key behavior is that masking still happens by default. This test currently only checks the boolean, so it won’t catch regressions where masking is accidentally skipped.
test('initialize without mask-client-id', async () => {
setEnv('environment', 'azureusgovernment');
setEnv('enable-AzPSSession', 'false');
setEnv('allow-no-subscriptions', 'true');
setEnv('auth-type', 'SERVICE_PRINCIPAL');
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
| this.federatedToken = null; | ||
|
|
||
| this.mask(this.servicePrincipalId); | ||
| this.maskClientId = core.getInput('mask-client-id').toLowerCase() !== "false"; |
| test('initialize with mask-client-id=true', async () => { | ||
| setEnv('environment', 'azureusgovernment'); | ||
| setEnv('enable-AzPSSession', 'false'); | ||
| 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('mask-client-id', 'true'); | ||
|
|
||
| let loginConfig = new LoginConfig(); | ||
| await loginConfig.initialize(); | ||
| expect(loginConfig.maskClientId).toBeTruthy(); | ||
| expect(loginConfig.servicePrincipalId).toBe("client-id"); | ||
| }); | ||
|
|
What changed
mask-client-id, a new optional boolean input defaulting to true:
action.yml- declares the input withdefault: truesrc/common/LoginConfig.ts-LoginConfiggains amaskClientIdfield and the existingthis.mask(this.servicePrincipalId)call is now gated on it.README.md- input table row, amask-client-idsection, and the existingclient-idmasking note now points at the opt-out.__tests__/LoginConfig.test.ts- three cases covering the input set totrue, set tofalse, and absent.Scope and default behaviour
Existing workflows are unaffected. The input defaults to
trueand every current caller keeps masking the client ID.Only the client ID/service principal ID is in scope. servicePrincipalSecret and the OIDC federated token are still masked unconditionally.
The gate applies to the client ID from either source - the
client-idinput orclientIdinsidecreds- since both land in the same field.A value passed from
${{ secrets.* }}is still masked by Actions itself regardless of this input. Opting out only has a visible effect for values that are not repository secrets, which is the case this is aimed at.Note for reviewers
The input is parsed as "anything other than an explicit
falsemeans mask":The other boolean inputs in
LoginConfiguse=== "true",which resolves to false when the input is absent. That is fine forenable-AzPSSessionandallow-no-subscriptions, but for a masking flag it fails in the unsafe direction - a missing input would silently stop masking. Inverting the comparison keeps a missing or malformed value on the masked path.Happy to switch to
core.getBooleanInputor to match the surrounding style instead if you would prefer consistency here.