@microsoft/vscode-azext-azureauth 5.1.0 → 6.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +9 -79
- package/dist/cjs/src/contracts/AzureAccount.js +7 -0
- package/dist/cjs/src/contracts/AzureSubscriptionProviderRequestOptions.js +48 -0
- package/dist/cjs/src/index.js +13 -10
- package/dist/cjs/src/providers/AzureDevOpsSubscriptionProvider.js +178 -0
- package/dist/cjs/src/providers/AzureSubscriptionProviderBase.js +393 -0
- package/dist/cjs/src/providers/VSCodeAzureSubscriptionProvider.js +269 -0
- package/dist/cjs/src/utils/Limiter.js +41 -0
- package/dist/cjs/src/{NotSignedInError.js → utils/NotSignedInError.js} +3 -2
- package/dist/cjs/src/utils/configuredAzureEnv.js +11 -16
- package/dist/cjs/src/utils/dedupeSubscriptions.js +27 -0
- package/dist/cjs/src/utils/getMetricsForTelemetry.js +47 -0
- package/dist/cjs/src/{getSessionFromVSCode.js → utils/getSessionFromVSCode.js} +5 -2
- package/dist/cjs/src/utils/getSignalForToken.js +29 -0
- package/dist/cjs/src/utils/map/CaselessMap.js +71 -0
- package/dist/cjs/src/utils/map/TwoKeyCaselessMap.js +194 -0
- package/dist/cjs/src/utils/screen.js +62 -0
- package/dist/cjs/src/{signInToTenant.js → utils/signInToTenant.js} +15 -13
- package/dist/cjs/src/utils/tryGetTokenExpiration.js +25 -0
- package/dist/esm/src/contracts/AzureAccount.d.ts +5 -0
- package/dist/esm/src/contracts/AzureAccount.js +6 -0
- package/dist/esm/src/{AzureAuthentication.d.ts → contracts/AzureAuthentication.d.ts} +1 -1
- package/dist/esm/src/{AzureSubscription.d.ts → contracts/AzureSubscription.d.ts} +4 -4
- package/dist/esm/src/contracts/AzureSubscriptionProvider.d.ts +112 -0
- package/dist/esm/src/contracts/AzureSubscriptionProviderRequestOptions.d.ts +103 -0
- package/dist/esm/src/contracts/AzureSubscriptionProviderRequestOptions.js +44 -0
- package/dist/esm/src/contracts/AzureTenant.d.ts +15 -0
- package/dist/esm/src/index.d.ts +13 -10
- package/dist/esm/src/index.js +13 -10
- package/dist/esm/src/providers/AzureDevOpsSubscriptionProvider.d.ts +68 -0
- package/dist/esm/src/providers/AzureDevOpsSubscriptionProvider.js +140 -0
- package/dist/esm/src/providers/AzureSubscriptionProviderBase.d.ts +74 -0
- package/dist/esm/src/providers/AzureSubscriptionProviderBase.js +356 -0
- package/dist/esm/src/providers/VSCodeAzureSubscriptionProvider.d.ts +70 -0
- package/dist/esm/src/providers/VSCodeAzureSubscriptionProvider.js +232 -0
- package/dist/esm/src/utils/Limiter.d.ts +9 -0
- package/dist/esm/src/utils/Limiter.js +37 -0
- package/dist/esm/src/{NotSignedInError.d.ts → utils/NotSignedInError.d.ts} +2 -2
- package/dist/esm/src/{NotSignedInError.js → utils/NotSignedInError.js} +3 -2
- package/dist/esm/src/utils/configuredAzureEnv.d.ts +7 -4
- package/dist/esm/src/utils/configuredAzureEnv.js +11 -16
- package/dist/esm/src/utils/dedupeSubscriptions.d.ts +14 -0
- package/dist/esm/src/utils/dedupeSubscriptions.js +24 -0
- package/dist/esm/src/utils/getMetricsForTelemetry.d.ts +32 -0
- package/dist/esm/src/utils/getMetricsForTelemetry.js +44 -0
- package/dist/esm/src/{getSessionFromVSCode.js → utils/getSessionFromVSCode.js} +5 -2
- package/dist/esm/src/utils/getSignalForToken.d.ts +7 -0
- package/dist/esm/src/utils/getSignalForToken.js +26 -0
- package/dist/esm/src/utils/map/CaselessMap.d.ts +28 -0
- package/dist/esm/src/utils/map/CaselessMap.js +67 -0
- package/dist/esm/src/utils/map/TwoKeyCaselessMap.d.ts +49 -0
- package/dist/esm/src/utils/map/TwoKeyCaselessMap.js +190 -0
- package/dist/esm/src/utils/screen.d.ts +9 -0
- package/dist/esm/src/utils/screen.js +59 -0
- package/dist/esm/src/utils/signInToTenant.d.ts +7 -0
- package/dist/esm/src/{signInToTenant.js → utils/signInToTenant.js} +16 -14
- package/dist/esm/src/utils/tryGetTokenExpiration.d.ts +2 -0
- package/dist/esm/src/utils/tryGetTokenExpiration.js +22 -0
- package/package.json +33 -23
- package/AzureFederatedCredentialsGuide.md +0 -174
- package/dist/cjs/src/AzureDevOpsSubscriptionProvider.js +0 -215
- package/dist/cjs/src/VSCodeAzureSubscriptionProvider.js +0 -385
- package/dist/cjs/src/utils/getUnauthenticatedTenants.js +0 -23
- package/dist/cjs/src/utils/isGetSubscriptionsFilter.js +0 -27
- package/dist/esm/src/AzureDevOpsSubscriptionProvider.d.ts +0 -68
- package/dist/esm/src/AzureDevOpsSubscriptionProvider.js +0 -210
- package/dist/esm/src/AzureSubscriptionProvider.d.ts +0 -82
- package/dist/esm/src/AzureTenant.d.ts +0 -5
- package/dist/esm/src/VSCodeAzureSubscriptionProvider.d.ts +0 -112
- package/dist/esm/src/VSCodeAzureSubscriptionProvider.js +0 -348
- package/dist/esm/src/signInToTenant.d.ts +0 -6
- package/dist/esm/src/utils/getUnauthenticatedTenants.d.ts +0 -9
- package/dist/esm/src/utils/getUnauthenticatedTenants.js +0 -20
- package/dist/esm/src/utils/isGetSubscriptionsFilter.d.ts +0 -14
- package/dist/esm/src/utils/isGetSubscriptionsFilter.js +0 -23
- /package/dist/cjs/src/{AzureAuthentication.js → contracts/AzureAuthentication.js} +0 -0
- /package/dist/cjs/src/{AzureSubscription.js → contracts/AzureSubscription.js} +0 -0
- /package/dist/cjs/src/{AzureSubscriptionProvider.js → contracts/AzureSubscriptionProvider.js} +0 -0
- /package/dist/cjs/src/{AzureTenant.js → contracts/AzureTenant.js} +0 -0
- /package/dist/esm/src/{AzureAuthentication.js → contracts/AzureAuthentication.js} +0 -0
- /package/dist/esm/src/{AzureSubscription.js → contracts/AzureSubscription.js} +0 -0
- /package/dist/esm/src/{AzureSubscriptionProvider.js → contracts/AzureSubscriptionProvider.js} +0 -0
- /package/dist/esm/src/{AzureTenant.js → contracts/AzureTenant.js} +0 -0
- /package/dist/esm/src/{getSessionFromVSCode.d.ts → utils/getSessionFromVSCode.d.ts} +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
// These regexes are not perfect, but should work for common cases
|
|
6
|
+
// If they don't work, we'll just return the account ID, which does not need to be screened
|
|
7
|
+
const accountLabelRegex = /^(?<email>[^@]+)@(?<domain>[\w]+(\.[\w]+)+)$/i;
|
|
8
|
+
const domainRegex = /^(?<domain>[^.]+)(?<safeTld>\.com(\..*)?|\.net|\.org|\.co\..*|\.gov)$/i;
|
|
9
|
+
/**
|
|
10
|
+
* Screens the label or display name of an Azure account or tenant so that it can be logged without exposing PII.
|
|
11
|
+
* This should *NOT* be considered fool-proof nor safe for telemetry, but is acceptable for local logging.
|
|
12
|
+
* @param accountOrTenant The account or tenant to screen the label / display name of
|
|
13
|
+
* @returns The screened label / display name
|
|
14
|
+
*/
|
|
15
|
+
export function screen(accountOrTenant) {
|
|
16
|
+
if ('label' in accountOrTenant && !!accountOrTenant.label) {
|
|
17
|
+
const match = accountLabelRegex.exec(accountOrTenant.label);
|
|
18
|
+
if (match?.groups?.email && match.groups.domain) {
|
|
19
|
+
let screenedEmail = match.groups.email;
|
|
20
|
+
if (screenedEmail.length <= 2) {
|
|
21
|
+
screenedEmail = '***';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
screenedEmail = `${screenedEmail.at(0)}***${screenedEmail.at(-1)}`;
|
|
25
|
+
}
|
|
26
|
+
let screenedDomain = match.groups.domain;
|
|
27
|
+
const domainMatch = domainRegex.exec(match.groups.domain);
|
|
28
|
+
if (domainMatch?.groups?.domain && domainMatch.groups.safeTld) {
|
|
29
|
+
if (domainMatch.groups.domain.length <= 2) {
|
|
30
|
+
screenedDomain = `***${domainMatch.groups.safeTld}`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
screenedDomain = `${domainMatch.groups.domain.at(0)}***${domainMatch.groups.safeTld}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (screenedDomain.length <= 2) {
|
|
37
|
+
screenedDomain = '***';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
screenedDomain = `${screenedDomain.at(0)}***${screenedDomain.at(-1)}`;
|
|
41
|
+
}
|
|
42
|
+
return `${screenedEmail}@${screenedDomain}`;
|
|
43
|
+
}
|
|
44
|
+
// If we can't match it with our simple regex, just return the account ID instead
|
|
45
|
+
return accountOrTenant.id;
|
|
46
|
+
}
|
|
47
|
+
else if ('displayName' in accountOrTenant && !!accountOrTenant.displayName) {
|
|
48
|
+
if (accountOrTenant.displayName.length <= 2) {
|
|
49
|
+
// For too-short names, just return the ID
|
|
50
|
+
return accountOrTenant.tenantId;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Return the first character, three stars, and the last character
|
|
54
|
+
return `${accountOrTenant.displayName.at(0)}***${accountOrTenant.displayName.at(-1)}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 'unknown';
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=screen.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AzureAccount } from '../contracts/AzureAccount';
|
|
2
|
+
import type { AzureSubscriptionProvider } from '../contracts/AzureSubscriptionProvider';
|
|
3
|
+
/**
|
|
4
|
+
* Prompts user to select from a list of unauthenticated tenants.
|
|
5
|
+
* Once selected, requests a new session from VS Code specifically for this tenant.
|
|
6
|
+
*/
|
|
7
|
+
export declare function signInToTenant(subscriptionProvider: AzureSubscriptionProvider, account?: AzureAccount): Promise<void>;
|
|
@@ -2,28 +2,31 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
4
4
|
*--------------------------------------------------------------------------------------------*/
|
|
5
|
-
import * as vscode from
|
|
6
|
-
import { getUnauthenticatedTenants } from "./utils/getUnauthenticatedTenants";
|
|
5
|
+
import * as vscode from 'vscode';
|
|
7
6
|
/**
|
|
8
7
|
* Prompts user to select from a list of unauthenticated tenants.
|
|
9
|
-
* Once selected, requests a new session from VS Code
|
|
8
|
+
* Once selected, requests a new session from VS Code specifically for this tenant.
|
|
10
9
|
*/
|
|
11
|
-
export async function signInToTenant(subscriptionProvider) {
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
await subscriptionProvider.signIn(
|
|
10
|
+
export async function signInToTenant(subscriptionProvider, account) {
|
|
11
|
+
const tenant = await pickTenant(subscriptionProvider, account);
|
|
12
|
+
if (tenant) {
|
|
13
|
+
await subscriptionProvider.signIn(tenant);
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
|
-
async function pickTenant(subscriptionProvider) {
|
|
18
|
-
const pick = await vscode.window.showQuickPick(getPicks(subscriptionProvider), {
|
|
19
|
-
placeHolder: 'Select a Tenant (Directory) to Sign In To',
|
|
16
|
+
async function pickTenant(subscriptionProvider, account) {
|
|
17
|
+
const pick = await vscode.window.showQuickPick(getPicks(subscriptionProvider, account), {
|
|
18
|
+
placeHolder: vscode.l10n.t('Select a Tenant (Directory) to Sign In To'),
|
|
20
19
|
matchOnDescription: true, // allow searching by tenantId
|
|
21
20
|
ignoreFocusOut: true,
|
|
22
21
|
});
|
|
23
|
-
return pick?.tenant
|
|
22
|
+
return pick?.tenant;
|
|
24
23
|
}
|
|
25
|
-
async function getPicks(subscriptionProvider) {
|
|
26
|
-
const unauthenticatedTenants =
|
|
24
|
+
async function getPicks(subscriptionProvider, account) {
|
|
25
|
+
const unauthenticatedTenants = [];
|
|
26
|
+
const accounts = account ? [account] : await subscriptionProvider.getAccounts();
|
|
27
|
+
for (const account of accounts) {
|
|
28
|
+
unauthenticatedTenants.push(...await subscriptionProvider.getUnauthenticatedTenantsForAccount(account));
|
|
29
|
+
}
|
|
27
30
|
const duplicateTenants = new Set(unauthenticatedTenants
|
|
28
31
|
.filter((tenant, index, self) => index !== self.findIndex(t => t.tenantId === tenant.tenantId))
|
|
29
32
|
.map(tenant => tenant.tenantId));
|
|
@@ -33,7 +36,6 @@ async function getPicks(subscriptionProvider) {
|
|
|
33
36
|
.sort((a, b) => (a.displayName).localeCompare(b.displayName))
|
|
34
37
|
.map(tenant => ({
|
|
35
38
|
label: tenant.displayName ?? '',
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
37
39
|
description: `${tenant.tenantId}${isDuplicate(tenant.tenantId) ? ` (${tenant.account.label})` : ''}`,
|
|
38
40
|
detail: tenant.defaultDomain ?? '',
|
|
39
41
|
tenant,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
export function tryGetTokenExpiration(session) {
|
|
6
|
+
try {
|
|
7
|
+
if (!!session?.idToken) {
|
|
8
|
+
const idTokenParts = session.idToken.split('.');
|
|
9
|
+
if (idTokenParts.length === 3) {
|
|
10
|
+
const payload = JSON.parse(Buffer.from(idTokenParts[1], 'base64').toString());
|
|
11
|
+
if (payload.exp !== undefined && Number.isInteger(payload.exp)) {
|
|
12
|
+
return payload.exp * 1000; // Convert to milliseconds
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Best effort only
|
|
19
|
+
}
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=tryGetTokenExpiration.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/vscode-azext-azureauth",
|
|
3
3
|
"author": "Microsoft Corporation",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.0-alpha.1",
|
|
5
5
|
"description": "Azure authentication helpers for Visual Studio Code",
|
|
6
6
|
"tags": [
|
|
7
7
|
"azure",
|
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
"azure",
|
|
12
12
|
"vscode"
|
|
13
13
|
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/esm/src/index.js",
|
|
17
|
+
"require": "./dist/cjs/src/index.js",
|
|
18
|
+
"types": "./dist/esm/src/index.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./azdo": {
|
|
21
|
+
"import": "./dist/esm/src/providers/AzureDevOpsSubscriptionProvider.js",
|
|
22
|
+
"require": "./dist/cjs/src/providers/AzureDevOpsSubscriptionProvider.js",
|
|
23
|
+
"types": "./dist/esm/src/providers/AzureDevOpsSubscriptionProvider.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
14
26
|
"module": "dist/esm/src/index.js",
|
|
15
27
|
"main": "dist/cjs/src/index.js",
|
|
16
28
|
"types": "dist/esm/src/index.d.ts",
|
|
@@ -27,37 +39,35 @@
|
|
|
27
39
|
"build": "npm run build:esm && npm run build:cjs",
|
|
28
40
|
"build:esm": "tsc -p ./",
|
|
29
41
|
"build:cjs": "tsc -p ./ --outDir ./dist/cjs --module nodenext --moduleResolution nodenext --declaration false",
|
|
30
|
-
"lint": "eslint --
|
|
31
|
-
"
|
|
32
|
-
"test": "node ./dist/cjs/test/runTest.js",
|
|
42
|
+
"lint": "eslint --max-warnings 0",
|
|
43
|
+
"test": "mocha",
|
|
33
44
|
"package": "npm pack",
|
|
34
45
|
"l10n": "npx @vscode/l10n-dev export --outDir ./l10n ./src"
|
|
35
46
|
},
|
|
36
47
|
"engines": {
|
|
37
|
-
"vscode": "^1.
|
|
48
|
+
"vscode": "^1.106.0"
|
|
38
49
|
},
|
|
39
50
|
"devDependencies": {
|
|
40
|
-
"@azure/core-auth": "^1.
|
|
41
|
-
"@microsoft/
|
|
42
|
-
"@types/
|
|
43
|
-
"@types/
|
|
44
|
-
"@types/node": "^18.18.7",
|
|
45
|
-
"@types/vscode": "1.105.0",
|
|
46
|
-
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
|
47
|
-
"@vscode/test-electron": "^2.3.8",
|
|
48
|
-
"eslint": "^8.34.0",
|
|
49
|
-
"eslint-plugin-import": "^2.22.1",
|
|
50
|
-
"glob": "^7.1.6",
|
|
51
|
-
"mocha": "^11.1.0",
|
|
52
|
-
"mocha-junit-reporter": "^2.0.2",
|
|
53
|
-
"mocha-multi-reporters": "^1.1.7",
|
|
54
|
-
"typescript": "^5.8.2"
|
|
51
|
+
"@azure/core-auth": "^1.10.1",
|
|
52
|
+
"@microsoft/vscode-azext-eng": "1.0.0-alpha.4",
|
|
53
|
+
"@types/node": "22.x",
|
|
54
|
+
"@types/vscode": "1.106.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@azure/arm-resources-subscriptions": "^2.1.0",
|
|
58
|
-
"@azure/
|
|
59
|
-
"@azure/core-rest-pipeline": "^1.16.0",
|
|
60
|
-
"@azure/identity": "^4.2.0",
|
|
58
|
+
"@azure/identity": "^4.13.0",
|
|
61
59
|
"@azure/ms-rest-azure-env": "^2.0.0"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"tag": "alpha"
|
|
63
|
+
},
|
|
64
|
+
"mocha": {
|
|
65
|
+
"ui": "tdd",
|
|
66
|
+
"node-option": [
|
|
67
|
+
"import=tsx"
|
|
68
|
+
],
|
|
69
|
+
"spec": [
|
|
70
|
+
"test/**/*.test.ts"
|
|
71
|
+
]
|
|
62
72
|
}
|
|
63
73
|
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# Setting up workflow identity federation with Azure DevOps
|
|
2
|
-
|
|
3
|
-
This guide describes how to set up your Azure DevOps (ADO) and Azure environment to leverage [workflow identity federation](https://learn.microsoft.com/entra/workload-id/workload-identity-federation), enabling you to use
|
|
4
|
-
`AzureDevOpsSubscriptionProvider` provided in this section. See the [README](README.md#azure-devops-subscription-provider) for more details.
|
|
5
|
-
|
|
6
|
-
## 1. Create a new service principal
|
|
7
|
-
|
|
8
|
-
Create a new service principal on which you will assign the necessary permissions. In this example, we use an app registration:
|
|
9
|
-
|
|
10
|
-
1. Navigate to the [App Registrations](https://ms.portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) page on the Azure portal
|
|
11
|
-
2. Click on `New Registration`
|
|
12
|
-
|
|
13
|
-

|
|
14
|
-
|
|
15
|
-
3. Assign any name
|
|
16
|
-
4. Make sure to select the first option for the account type (`Accounts in this organization directory only (Microsoft only - Single tenant)`)
|
|
17
|
-
5. Leave the Redirect URI and Service Tree ID fields empty
|
|
18
|
-
6. Click on `Register`
|
|
19
|
-
|
|
20
|
-

|
|
21
|
-
|
|
22
|
-
## 2. Create a new Azure DevOps (ADO) Service Connection:
|
|
23
|
-
|
|
24
|
-
Create a new ADO service connection under your organization's project. In this example, we create it under the DevDiv project:
|
|
25
|
-
|
|
26
|
-
1. Navigate to the [organization's (DevDiv) ADO page](https://devdiv.visualstudio.com/DevDiv)
|
|
27
|
-
2. Navigate to the settings page by clicking on the gear icon on the bottom left
|
|
28
|
-
3. Select the ["service connections"](https://devdiv.visualstudio.com/DevDiv/_settings/adminservices) blade from the panel on the left
|
|
29
|
-
|
|
30
|
-

|
|
31
|
-
|
|
32
|
-
4. Create a new service connection by clicking on the `New service connection` button
|
|
33
|
-
|
|
34
|
-

|
|
35
|
-
|
|
36
|
-
5. Select `Azure Resource Manager` as the type
|
|
37
|
-
6. Select `Workload Identity federation (manual)` for the authentication type
|
|
38
|
-
7. Provide a new name for your new service connection
|
|
39
|
-
8. Click on `Next`
|
|
40
|
-
9. This will create a new draft service connection, with the `issuer` and `subject identifier` fields already filled in.
|
|
41
|
-
10. Leave this window open while you finish the next step, which will require those `issuer` and `subject identifier` fields, then you will return to this window to finish creating the service principal
|
|
42
|
-
|
|
43
|
-

|
|
44
|
-
|
|
45
|
-
## 3. Create a federated credential:
|
|
46
|
-
|
|
47
|
-
Create a new "federated credential" on your service principal to connect it to your new service connection:
|
|
48
|
-
|
|
49
|
-
1. Navigate back to the Azure Portal page for your service connection (app registration) from step 1
|
|
50
|
-
2. Navigate to the `Certificates & secrets` blade
|
|
51
|
-
3. Navigate to the `Federated credentials` tab
|
|
52
|
-
4. Click on the `Add credential` button
|
|
53
|
-
|
|
54
|
-

|
|
55
|
-
|
|
56
|
-
5. For the scenario, select `Other issuer`
|
|
57
|
-
6. For the `issuer` and `subject identifier` fields, fill in with the details of your draft service connection from the previous step
|
|
58
|
-
7. Select a new name for your new federated credential
|
|
59
|
-
|
|
60
|
-

|
|
61
|
-
|
|
62
|
-
8. Click on `Add`
|
|
63
|
-
|
|
64
|
-
## 4. (Temporary but required) Grant your service principal reader role on the desired subscription:
|
|
65
|
-
|
|
66
|
-
This step is not required for running your tests, but _is_ required to finish creating the service connection. This should be revoked after successful creation of the service connection and only necessary roles applied to the service principal.
|
|
67
|
-
|
|
68
|
-
1. On the Azure Portal, navigate to the page for the subscription you want the service principal to have access to.
|
|
69
|
-
2. Navigate to the `Access control (IAM)` blade
|
|
70
|
-
|
|
71
|
-

|
|
72
|
-
|
|
73
|
-
3. Navigate to the `Roles` tab
|
|
74
|
-
4. Click on the `+ Add` button, and choose `Add role assignment`
|
|
75
|
-
|
|
76
|
-

|
|
77
|
-
|
|
78
|
-
5. Choose `Reader` and click `Next`
|
|
79
|
-
6. Choose `User, group, or service principal`, then click on `+ Select members`
|
|
80
|
-
|
|
81
|
-

|
|
82
|
-
|
|
83
|
-
7. Select your service principal from step 1
|
|
84
|
-
8. Click on `Review and assign`
|
|
85
|
-
|
|
86
|
-
## 5. Finish creating your service connection:
|
|
87
|
-
|
|
88
|
-
Finish creating the draft service connection you created in step 2.
|
|
89
|
-
|
|
90
|
-
1. Navigate back to your draft service connection from step 2
|
|
91
|
-
2. For Environment, select `Azure Cloud`
|
|
92
|
-
3. For Scope Level, choose `Subscription`
|
|
93
|
-
4. Under `Subscription Id`, and `Subscription Name`, write the subscription ID and name (must provide both) for the desired subscription
|
|
94
|
-
5. For `Service Principal Id`, provide the `Application (client) ID` of your app registration from step 1 (can be found in the `Overview` blade)
|
|
95
|
-
6. For the `Tenant ID`, provide the `Directory (tenant) ID` of your app registration from step 1 (can be found in `Overview` blade)
|
|
96
|
-
7. Click on `Verify and save`
|
|
97
|
-
|
|
98
|
-
## 6. Revoke unnecessary read access and assign only necessary roles
|
|
99
|
-
|
|
100
|
-
Revoke the `Reader` role on the subscription for the service connection after it is created. This is no longer necessary.
|
|
101
|
-
|
|
102
|
-
1. Navigate to `Access control (IAM)` blade.
|
|
103
|
-
2. Under the `Role assignments` tab, find the role assignment corresponding to the App registered on step 1
|
|
104
|
-
3. Click on `Remove` then `Yes`
|
|
105
|
-
4. You can then assign the required roles to specific resources only if required, instead of assigning `Reader` role to the entire subscription.
|
|
106
|
-
|
|
107
|
-
## 7. Create a dummy Key Vault
|
|
108
|
-
|
|
109
|
-
A dummy Key vault step is required to propagate the necessary environment variables in the context of the pipeline.
|
|
110
|
-
|
|
111
|
-
1. Create a new Key Vault resource in the subscription you want to test on
|
|
112
|
-
2. Give it a new name as appropriate. You can keep the default settings
|
|
113
|
-
|
|
114
|
-

|
|
115
|
-
|
|
116
|
-
## 8. Assign your service principal "key vault reader" role on the dummy Key Vault:
|
|
117
|
-
|
|
118
|
-
1. Navigate to `Access control (IAM)` blade on your newly created dummy key vault
|
|
119
|
-
|
|
120
|
-

|
|
121
|
-
|
|
122
|
-
2. Navigate to the `Roles` tab
|
|
123
|
-
3. Click on the `+ Add` button, and choose `Add role assignment`
|
|
124
|
-
|
|
125
|
-

|
|
126
|
-
|
|
127
|
-
4. Choose `Key Vault Reader` (**NOT** `Reader`) and click `Next`
|
|
128
|
-
5. Choose `User, group, or service principal`, then click on `+ Select members`
|
|
129
|
-
|
|
130
|
-

|
|
131
|
-
|
|
132
|
-
6. Select your app registration from step 1
|
|
133
|
-
7. Click on `Review and assign`
|
|
134
|
-
|
|
135
|
-
## 9. Add the dummy Key Vault step in the pipeline
|
|
136
|
-
|
|
137
|
-
To ensure that the appropriate env variables are propagated in the context of running the pipeline, a dummy Key Vault step is required in that pipeline:
|
|
138
|
-
|
|
139
|
-
1. In the desired pipeline's `.yml` file, add a step as below. The `azureSubscription` field should correspond to the name of your service connection from step 2, while the `keyVaultName` field should correspond to the dummy key vault created in step 7:
|
|
140
|
-
|
|
141
|
-
```yml
|
|
142
|
-
# This gives the TestServiceConnection service connection access to this pipeline.
|
|
143
|
-
- task: AzureKeyVault@1
|
|
144
|
-
displayName: 'Authorize TestServiceConnection service connection'
|
|
145
|
-
inputs:
|
|
146
|
-
azureSubscription: 'TestServiceConnection'
|
|
147
|
-
KeyVaultName: 'TestDummyKeyVault'
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
2. In the step which runs your code (e.g., the npm test step), make sure that the `$(System.AccessToken)` variable is manually propagated as a `SYSTEM_ACCESSTOKEN` environment variable. All other required environment variables should be propagated automatically:
|
|
151
|
-
|
|
152
|
-
```yml
|
|
153
|
-
- task: Npm@1
|
|
154
|
-
displayName: "Test"
|
|
155
|
-
inputs:
|
|
156
|
-
command: custom
|
|
157
|
-
customCommand: test
|
|
158
|
-
env:
|
|
159
|
-
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## 10. Pass the appropriate values to identify your service connection:
|
|
163
|
-
|
|
164
|
-
The constructor for `AzureDevOpsSubscriptionProvider` expects three arguments in an initializer object in order to identify your service connection you setup in step 5.
|
|
165
|
-
|
|
166
|
-
These are:
|
|
167
|
-
|
|
168
|
-
- `serviceConnectionId`: The resource ID of the service connection created in step 2, which can be found on the `resourceId` field of the URL at the address bar, when viewing the service connection in the Azure DevOps portal
|
|
169
|
-
- `domain`: The `Tenant ID` field of the service connection properties, which can be accessed by clicking "Edit" on the service connection page
|
|
170
|
-
- `clientId`: The `Service Principal Id` field of the service connection properties, which can be accessed by clicking "Edit" on the service connection page
|
|
171
|
-
|
|
172
|
-

|
|
173
|
-
|
|
174
|
-
Make sure you pass an object containing these variables for the `new AzureDevOpsServiceProvider()` constructor. These values are _not_ secrets, so they can be set as environment variables, assigned as pipeline variables in ADO, accessed and assigned using an Azure Key Vault step, or even manually hardcoded in code (not recommended).
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*---------------------------------------------------------------------------------------------
|
|
3
|
-
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
4
|
-
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
5
|
-
*--------------------------------------------------------------------------------------------*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.AzureDevOpsSubscriptionProvider = void 0;
|
|
8
|
-
exports.createAzureDevOpsSubscriptionProviderFactory = createAzureDevOpsSubscriptionProviderFactory;
|
|
9
|
-
const configuredAzureEnv_1 = require("./utils/configuredAzureEnv");
|
|
10
|
-
let azureDevOpsSubscriptionProvider;
|
|
11
|
-
function createAzureDevOpsSubscriptionProviderFactory(initializer) {
|
|
12
|
-
return async () => {
|
|
13
|
-
azureDevOpsSubscriptionProvider ??= new AzureDevOpsSubscriptionProvider(initializer);
|
|
14
|
-
return azureDevOpsSubscriptionProvider;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* AzureSubscriptionProvider implemented to authenticate via federated DevOps service connection, using workflow identity federation
|
|
19
|
-
* To learn how to configure your DevOps environment to use this provider, refer to the README.md
|
|
20
|
-
* NOTE: This provider is only available when running in an Azure DevOps pipeline
|
|
21
|
-
* Reference: https://learn.microsoft.com/en-us/entra/workload-id/workload-identity-federation
|
|
22
|
-
*/
|
|
23
|
-
class AzureDevOpsSubscriptionProvider {
|
|
24
|
-
_tokenCredential;
|
|
25
|
-
/**
|
|
26
|
-
* The resource ID of the Azure DevOps federated service connection,
|
|
27
|
-
* which can be found on the `resourceId` field of the URL at the address bar
|
|
28
|
-
* when viewing the service connection in the Azure DevOps portal
|
|
29
|
-
*/
|
|
30
|
-
_SERVICE_CONNECTION_ID;
|
|
31
|
-
/**
|
|
32
|
-
* The `Tenant ID` field of the service connection properties
|
|
33
|
-
*/
|
|
34
|
-
_DOMAIN;
|
|
35
|
-
/**
|
|
36
|
-
* The `Service Principal Id` field of the service connection properties
|
|
37
|
-
*/
|
|
38
|
-
_CLIENT_ID;
|
|
39
|
-
constructor({ serviceConnectionId, domain, clientId }) {
|
|
40
|
-
if (!serviceConnectionId || !domain || !clientId) {
|
|
41
|
-
throw new Error(`Missing initializer values to identify Azure DevOps federated service connection\n
|
|
42
|
-
Values provided:\n
|
|
43
|
-
serviceConnectionId: ${serviceConnectionId ? "✅" : "❌"}\n
|
|
44
|
-
domain: ${domain ? "✅" : "❌"}\n
|
|
45
|
-
clientId: ${clientId ? "✅" : "❌"}\n
|
|
46
|
-
`);
|
|
47
|
-
}
|
|
48
|
-
this._SERVICE_CONNECTION_ID = serviceConnectionId;
|
|
49
|
-
this._DOMAIN = domain;
|
|
50
|
-
this._CLIENT_ID = clientId;
|
|
51
|
-
}
|
|
52
|
-
async getSubscriptions(_filter) {
|
|
53
|
-
// ignore the filter setting because not every consumer of this provider will use the Resources extension
|
|
54
|
-
const results = [];
|
|
55
|
-
for (const tenant of await this.getTenants()) {
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
57
|
-
const tenantId = tenant.tenantId;
|
|
58
|
-
results.push(...await this.getSubscriptionsForTenant(tenantId));
|
|
59
|
-
}
|
|
60
|
-
const sortSubscriptions = (subscriptions) => subscriptions.sort((a, b) => a.name.localeCompare(b.name));
|
|
61
|
-
return sortSubscriptions(results);
|
|
62
|
-
}
|
|
63
|
-
async isSignedIn() {
|
|
64
|
-
return !!this._tokenCredential;
|
|
65
|
-
}
|
|
66
|
-
async signIn() {
|
|
67
|
-
this._tokenCredential = await getTokenCredential(this._SERVICE_CONNECTION_ID, this._DOMAIN, this._CLIENT_ID);
|
|
68
|
-
return !!this._tokenCredential;
|
|
69
|
-
}
|
|
70
|
-
async signOut() {
|
|
71
|
-
this._tokenCredential = undefined;
|
|
72
|
-
}
|
|
73
|
-
async getTenants() {
|
|
74
|
-
return [{
|
|
75
|
-
tenantId: this._tokenCredential?.tenantId,
|
|
76
|
-
account: {
|
|
77
|
-
id: "test-account-id",
|
|
78
|
-
label: "test-account",
|
|
79
|
-
}
|
|
80
|
-
}];
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Gets the subscriptions for a given tenant.
|
|
84
|
-
*
|
|
85
|
-
* @param tenantId The tenant ID to get subscriptions for.
|
|
86
|
-
*
|
|
87
|
-
* @returns The list of subscriptions for the tenant.
|
|
88
|
-
*/
|
|
89
|
-
async getSubscriptionsForTenant(tenantId) {
|
|
90
|
-
const { client, credential, authentication } = await this.getSubscriptionClient(tenantId);
|
|
91
|
-
const environment = (0, configuredAzureEnv_1.getConfiguredAzureEnv)();
|
|
92
|
-
const subscriptions = [];
|
|
93
|
-
for await (const subscription of client.subscriptions.list()) {
|
|
94
|
-
subscriptions.push({
|
|
95
|
-
authentication,
|
|
96
|
-
environment: environment,
|
|
97
|
-
credential: credential,
|
|
98
|
-
isCustomCloud: environment.isCustomCloud,
|
|
99
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
100
|
-
name: subscription.displayName,
|
|
101
|
-
subscriptionId: subscription.subscriptionId,
|
|
102
|
-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
|
103
|
-
tenantId,
|
|
104
|
-
account: {
|
|
105
|
-
id: "test-account-id",
|
|
106
|
-
label: "test-account",
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
return subscriptions;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Gets a fully-configured subscription client for a given tenant ID
|
|
114
|
-
*
|
|
115
|
-
* @param tenantId (Optional) The tenant ID to get a client for
|
|
116
|
-
*
|
|
117
|
-
* @returns A client, the credential used by the client, and the authentication function
|
|
118
|
-
*/
|
|
119
|
-
async getSubscriptionClient(_tenantId, scopes) {
|
|
120
|
-
const armSubs = await import('@azure/arm-resources-subscriptions');
|
|
121
|
-
if (!this._tokenCredential) {
|
|
122
|
-
throw new Error('Not signed in');
|
|
123
|
-
}
|
|
124
|
-
const accessToken = (await this._tokenCredential?.getToken("https://management.azure.com/.default"))?.token || '';
|
|
125
|
-
const getSession = () => {
|
|
126
|
-
return {
|
|
127
|
-
accessToken,
|
|
128
|
-
id: this._tokenCredential?.tenantId || '',
|
|
129
|
-
account: {
|
|
130
|
-
id: this._tokenCredential?.tenantId || '',
|
|
131
|
-
label: this._tokenCredential?.tenantId || '',
|
|
132
|
-
},
|
|
133
|
-
tenantId: this._tokenCredential?.tenantId || '',
|
|
134
|
-
scopes: scopes || [],
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
return {
|
|
138
|
-
client: new armSubs.SubscriptionClient(this._tokenCredential),
|
|
139
|
-
credential: this._tokenCredential,
|
|
140
|
-
authentication: {
|
|
141
|
-
getSession,
|
|
142
|
-
getSessionWithScopes: getSession,
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
onDidSignIn = () => { return { dispose() { } }; };
|
|
147
|
-
onDidSignOut = () => { return { dispose() { } }; };
|
|
148
|
-
}
|
|
149
|
-
exports.AzureDevOpsSubscriptionProvider = AzureDevOpsSubscriptionProvider;
|
|
150
|
-
/*
|
|
151
|
-
* @param serviceConnectionId The resource ID of the Azure DevOps federated service connection,
|
|
152
|
-
* which can be found on the `resourceId` field of the URL at the address bar when viewing the service connection in the Azure DevOps portal
|
|
153
|
-
* @param domain The `Tenant ID` field of the service connection properties
|
|
154
|
-
* @param clientId The `Service Principal Id` field of the service connection properties
|
|
155
|
-
*/
|
|
156
|
-
async function getTokenCredential(serviceConnectionId, domain, clientId) {
|
|
157
|
-
if (!process.env.AGENT_BUILDDIRECTORY) {
|
|
158
|
-
// Assume that AGENT_BUILDDIRECTORY is set if running in an Azure DevOps pipeline.
|
|
159
|
-
// So when not running in an Azure DevOps pipeline, throw an error since we cannot use the DevOps federated service connection credential.
|
|
160
|
-
throw new Error(`Cannot create DevOps federated service connection credential outside of an Azure DevOps pipeline.`);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
console.log(`Creating DevOps federated service connection credential for service connection..`);
|
|
164
|
-
// Pre-defined DevOps variable reference: https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops
|
|
165
|
-
const systemAccessToken = process.env.SYSTEM_ACCESSTOKEN;
|
|
166
|
-
const teamFoundationCollectionUri = process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
167
|
-
const teamProjectId = process.env.SYSTEM_TEAMPROJECTID;
|
|
168
|
-
const planId = process.env.SYSTEM_PLANID;
|
|
169
|
-
const jobId = process.env.SYSTEM_JOBID;
|
|
170
|
-
if (!systemAccessToken || !teamFoundationCollectionUri || !teamProjectId || !planId || !jobId) {
|
|
171
|
-
throw new Error(`Azure DevOps environment variables are not set.\n
|
|
172
|
-
process.env.SYSTEM_ACCESSTOKEN: ${process.env.SYSTEM_ACCESSTOKEN ? "✅" : "❌"}\n
|
|
173
|
-
process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: ${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ? "✅" : "❌"}\n
|
|
174
|
-
process.env.SYSTEM_TEAMPROJECTID: ${process.env.SYSTEM_TEAMPROJECTID ? "✅" : "❌"}\n
|
|
175
|
-
process.env.SYSTEM_PLANID: ${process.env.SYSTEM_PLANID ? "✅" : "❌"}\n
|
|
176
|
-
process.env.SYSTEM_JOBID: ${process.env.SYSTEM_JOBID ? "✅" : "❌"}\n
|
|
177
|
-
REMEMBER: process.env.SYSTEM_ACCESSTOKEN must be explicitly mapped!\n
|
|
178
|
-
https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#systemaccesstoken
|
|
179
|
-
`);
|
|
180
|
-
}
|
|
181
|
-
const oidcRequestUrl = `${teamFoundationCollectionUri}${teamProjectId}/_apis/distributedtask/hubs/build/plans/${planId}/jobs/${jobId}/oidctoken?api-version=7.1-preview.1&serviceConnectionId=${serviceConnectionId}`;
|
|
182
|
-
const { ClientAssertionCredential } = await import("@azure/identity");
|
|
183
|
-
return new ClientAssertionCredential(domain, clientId, async () => await requestOidcToken(oidcRequestUrl, systemAccessToken));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* API reference: https://learn.microsoft.com/en-us/rest/api/azure/devops/distributedtask/oidctoken/create
|
|
188
|
-
*/
|
|
189
|
-
async function requestOidcToken(oidcRequestUrl, systemAccessToken) {
|
|
190
|
-
const { ServiceClient } = await import('@azure/core-client');
|
|
191
|
-
const { createHttpHeaders, createPipelineRequest } = await import('@azure/core-rest-pipeline');
|
|
192
|
-
const genericClient = new ServiceClient();
|
|
193
|
-
const request = createPipelineRequest({
|
|
194
|
-
url: oidcRequestUrl,
|
|
195
|
-
method: "POST",
|
|
196
|
-
headers: createHttpHeaders({
|
|
197
|
-
"Content-Type": "application/json",
|
|
198
|
-
"Authorization": `Bearer ${systemAccessToken}`
|
|
199
|
-
})
|
|
200
|
-
});
|
|
201
|
-
const response = await genericClient.sendRequest(request);
|
|
202
|
-
const body = response.bodyAsText?.toString() || "";
|
|
203
|
-
if (response.status !== 200) {
|
|
204
|
-
throw new Error(`Failed to get OIDC token:\n
|
|
205
|
-
Response status: ${response.status}\n
|
|
206
|
-
Response body: ${body}\n
|
|
207
|
-
Response headers: ${JSON.stringify(response.headers.toJSON())}
|
|
208
|
-
`);
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
console.log(`Successfully got OIDC token with status ${response.status}`);
|
|
212
|
-
}
|
|
213
|
-
return JSON.parse(body).oidcToken;
|
|
214
|
-
}
|
|
215
|
-
//# sourceMappingURL=AzureDevOpsSubscriptionProvider.js.map
|