-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Identity] Support Workload Identity for service connections #28628
Conversation
API change check APIView has identified API level changes in this PR and created following API reviews. |
ae08909
to
2111129
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have a few questions / comments / nits - coming together though!
tenantId?: string; | ||
tokenFilePath?: string; | ||
} | ||
|
||
// @public | ||
export type WorkloadIdentityCredentialOptions = WorkloadIdentityCredentialKubernetesOptions | WorkloadIdentityCredentialServiceConnectionOptions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference between WorkloadIdentityCredentialKubernetesOptions and WorkloadIdentityCredentialServiceConnectionOptions ? They look identical
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One has federatedTokenFilePath required and the other has serviceConnectionId required
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see! I missed the never
on each side.
This makes more sense now, but I wonder if undefined
is a better type than never
here - because the union of <type>
and never
is just <type>
this ends up being undefined
anyway.
What I mean is, try this in a typescript playground:
type Foo = string | never
If you hover over Foo
in the editor you'll notice that the type is just string
- TypeScript is smart enough to know that never
can not be in the set of values here. When a user tries to set both, the error message will say:
Type 'string' is not assignable to type 'undefined'.ts(2322)
Which can be confusing.
So what I am suggesting is to change never
to explicit undefined
as a starting point. It'll make for less confusing error messages. Does that make sense? Happy to discuss more or brainstorm an API shape that provides better devex!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tagging @bterlson to discuss which type should we use here for forbidden fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Karishma - happy to learn one way or the other 👍
if (tenantId) { | ||
checkTenantId(logger, tenantId); | ||
} | ||
if (this.federatedTokenFilePath && this.serviceConnectionId) { | ||
const errorMessage = `${credentialName}: ambiguous. serviceConnectionId and federatedTokenFilePath cannot be provided at the same time. These are used for supporting WorkloadIdentity for different environments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think federatedTokenFilePath
is what we name the variable internally but the param is called tokenFilePath
- if that's the case we should change the error message to specify the correct param name (otherwise it may be confusing to the user).
Also, is it an error to have AZURE_FEDERATED_TOKEN_FILE env var set and pass in serviceConnectionId
via the params? Just curious
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is going to be an error scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks!
sdk/identity/identity/src/credentials/workloadIdentityCredential.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredential.ts
Outdated
Show resolved
Hide resolved
}, | ||
}; | ||
|
||
const response = await fetch(oidcRequestUrl, requestOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should, ideally, use the core-rest-pipeline primitives unless there's a reason to use fetch directly
return true; | ||
} | ||
if (!process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) { | ||
this.systemError = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI,"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that we're building this systemError and then setting it as an instance variable - where do we actually use it / throw it?
I also wonder if it would be cleaner to collect missing env var names in an array and then creating the error if the array is not empty (pseudocode below)
const missingEnvVars = []
if (!process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) missingEnvVars.push("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI")
...
...
if (missingEnvVars.length > 0) this.systemError = "Missing system variable(s) - ${missingEnvVars.join(', ')}"
Last question here is: is this a developer error or an SDK error for these to be missing? They're set by Azure DevOps right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are set by Azure Devops. So definitely not an error by developer. but by Azure Devops
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's a better strategy using the array
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
sdk/identity/identity/src/credentials/workloadIdentityCredential.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredential.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredential.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredentialOptions.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredentialOptions.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredentialOptions.ts
Outdated
Show resolved
Hide resolved
sdk/identity/identity/src/credentials/workloadIdentityCredential.ts
Outdated
Show resolved
Hide resolved
Apologies for the PR noise 😄 but the new code should have test coverage - could you add tests? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a JS perspective this looks much better, thank you for indulging me! I'd like someone from the Identity crew to validate the logic as well 😇
throw new CredentialUnavailableError( | ||
`${credentialName}: is Unavailable. clientId and tenantId are required parameters/ environment varables - AZURE_TENANT_ID, AZURE_CLIENT_ID` | ||
); | ||
} | ||
if (tenantId) { | ||
checkTenantId(logger, tenantId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you could move outside the if
and remove the if
entirely since you are validating tenantId exists 👍
workloadIdentityCredentialOptions.tokenFilePath || process.env.AZURE_FEDERATED_TOKEN_FILE; | ||
workloadIdentityCredentialOptions?.tokenFilePath || process.env.AZURE_FEDERATED_TOKEN_FILE; | ||
this.serviceConnectionId = workloadIdentityCredentialOptions.serviceConnectionId; | ||
if (!tenantId || !clientId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would move this to line 65 to keep it next to the code that sets clientId and tenantId
logger.error(errorMessage); | ||
throw new CredentialUnavailableError(errorMessage); | ||
} | ||
|
||
if (clientId && tenantId && this.federatedTokenFilePath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clientId and tenantId should always be set by now, you can remove check for them
); | ||
} else { | ||
if (clientId && tenantId && this.serviceConnectionId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clientId and tenantId should always be set by now, you can remove check for them
if (clientId && tenantId && this.serviceConnectionId) { | ||
//Ensure all system env vars are there to form the request uri for OIDC token | ||
this.ensurePipelinesSystemVars(); | ||
const oidcRequestUrl = `${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${process.env.SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${process.env.SYSTEM_PLANID}/jobs/${process.env.SYSTEM_JOBID}/oidctoken?api-version=7.1-preview.1&serviceConnectionId=${this.serviceConnectionId}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: should we always hit 7.1-preview.1 API version? If not how do we move forward?
this.federatedTokenFilePath = | ||
workloadIdentityCredentialOptions.tokenFilePath || process.env.AZURE_FEDERATED_TOKEN_FILE; | ||
workloadIdentityCredentialOptions?.tokenFilePath || process.env.AZURE_FEDERATED_TOKEN_FILE; | ||
this.serviceConnectionId = workloadIdentityCredentialOptions.serviceConnectionId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check that this is guid-like?
if (!process.env.SYSTEM_ACCESSTOKEN) missingEnvVars.push("SYSTEM_ACCESSTOKEN"); | ||
if (missingEnvVars.length > 0) | ||
throw new CredentialUnavailableError( | ||
`${credentialName}: is unavailable. Missing system variable(s) - ${missingEnvVars.join( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't a great error message - yes. the variables are missing, but we have a good idea as to why they might be, and what the user should do instead. For the first set, you can generate something like The environment variables SYSTEM_JOBID and SYSTEM_PLANID aren't defined - please ensure that you're running this task in an Azure Pipeline.
We can do more for undefined/non-null check for SYSTEM_ACCESSTOKEN with a message like:
Please ensure that the system access token is available in the SYSTEM_ACCESSTOKEN value; this is often most easily achieved by adding a block to the end of your pipeline yaml for the task with:
env:
- SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||
/** | ||
* Options for the {@link WorkloadIdentityCredential} | ||
*/ | ||
export interface WorkloadIdentityCredentialOptions | ||
export type WorkloadIdentityCredentialOptions = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're doing this, why not do it by inheritance?
export interface WorkloadIdentityCredentialOptions
extends MultiTenantTokenCredentialOptions,
AuthorityValidationOptions {
tenantId?: string,
clientId?: string
}
export interface WorkloadIdentityCredentialKubernetesOptions
extends WorkloadIdentityCredentialOptions {
tokenFilePath?: string;
}
export interface WorkloadIdentityCredentialServiceConnectionOptions
extends WorkloadIdentityCredentialOptions {
serviceConnectionId?: string;
}
Closing in favor of #29392 |
Packages impacted by this PR
Issues associated with this PR
Fixes #27093
Gist
Describe the problem that is addressed by this PR
Work TBD:
Checklists