Skip to content

Commit 3664afe

Browse files
authored
Users/davidmiri/2233178 support late bound idtoken (#21580)
* Support idToken late bidning in service endpoint for WIF SC * Add unit tests * Polish message * Try to fix tests attempt 1 * Fix tests attempt 2 * Standardize environment variable names for late-bound ID token feature flags
1 parent 7b31eac commit 3664afe

File tree

7 files changed

+410
-5
lines changed

7 files changed

+410
-5
lines changed

Tasks/AzureCLIV2/Tests/L0.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,60 @@
11
import fs = require('fs');
22
import assert = require('assert');
33
import path = require('path');
4+
import * as ttm from 'azure-pipelines-task-lib/mock-test';
45

56
describe('AzureCLIV2 Suite', function () {
7+
this.timeout(20000);
8+
69
before(() => {
710
});
811

912
after(() => {
1013
});
1114

12-
it('Does a basic hello world test', function (done: MochaDone) {
13-
// TODO - add real tests
14-
done();
15+
it('LateBoundIdToken: Feature Flag ON, Token Present -> Uses Token, Emits Telemetry', async () => {
16+
let tp = path.join(__dirname, 'LateBoundIdToken_FeatureFlagOn_TokenPresent.js');
17+
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
18+
await tr.runAsync();
19+
20+
if (!tr.succeeded) {
21+
console.log('STDOUT:', tr.stdout);
22+
console.log('STDERR:', tr.stderr);
23+
}
24+
25+
assert(tr.succeeded, 'task should have succeeded');
26+
assert(tr.stdout.indexOf('MOCK_TELEMETRY: AzureCLIV2, LateBoundIdToken, {"connectedService":"AzureRM","idTokenPresent":"true"}') >= 0, 'should emit telemetry with idTokenPresent=true');
27+
assert(tr.stdout.indexOf('Using bound idToken from service endpoint.') >= 0, 'should log that it is using bound idToken');
28+
assert(tr.stdout.indexOf('MOCK_CREATE_OIDC_TOKEN_CALLED') === -1, 'should NOT call createOidcToken');
29+
});
30+
31+
it('LateBoundIdToken: Feature Flag ON, Token Missing -> Calls API, Emits Telemetry', async () => {
32+
let tp = path.join(__dirname, 'LateBoundIdToken_FeatureFlagOn_TokenMissing.js');
33+
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
34+
await tr.runAsync();
35+
36+
if (!tr.succeeded) {
37+
console.log('STDOUT:', tr.stdout);
38+
console.log('STDERR:', tr.stderr);
39+
}
40+
41+
assert(tr.succeeded, 'task should have succeeded');
42+
assert(tr.stdout.indexOf('MOCK_TELEMETRY: AzureCLIV2, LateBoundIdToken, {"connectedService":"AzureRM","idTokenPresent":"false"}') >= 0, 'should emit telemetry with idTokenPresent=false');
43+
assert(tr.stdout.indexOf('MOCK_CREATE_OIDC_TOKEN_CALLED') >= 0, 'should call createOidcToken');
44+
});
45+
46+
it('LateBoundIdToken: Feature Flag OFF -> Calls API, No Telemetry', async () => {
47+
let tp = path.join(__dirname, 'LateBoundIdToken_FeatureFlagOff.js');
48+
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
49+
await tr.runAsync();
50+
51+
if (!tr.succeeded) {
52+
console.log('STDOUT:', tr.stdout);
53+
console.log('STDERR:', tr.stderr);
54+
}
55+
56+
assert(tr.succeeded, 'task should have succeeded');
57+
assert(tr.stdout.indexOf('MOCK_TELEMETRY: AzureCLIV2, LateBoundIdToken') === -1, 'should NOT emit LateBoundIdToken telemetry');
58+
assert(tr.stdout.indexOf('MOCK_CREATE_OIDC_TOKEN_CALLED') >= 0, 'should call createOidcToken');
1559
});
1660
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import ma = require('azure-pipelines-task-lib/mock-answer');
2+
import tmrm = require('azure-pipelines-task-lib/mock-run');
3+
import path = require('path');
4+
5+
let taskPath = path.join(__dirname, '..', 'azureclitask.js');
6+
let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
7+
8+
// Inputs
9+
tmr.setInput('connectedServiceNameARM', 'AzureRM');
10+
tmr.setInput('scriptType', 'bash');
11+
tmr.setInput('scriptLocation', 'inlineScript');
12+
tmr.setInput('inlineScript', 'echo hello');
13+
tmr.setInput('cwd', '/tmp');
14+
tmr.setInput('visibleAzLogin', 'true');
15+
16+
// Environment variables for Feature Flag (OFF)
17+
process.env['DISTRIBUTEDTASK_TASKS_ENABLELATEBOUNDIDTOKEN'] = 'false';
18+
process.env['DISTRIBUTEDTASK_TASKS_USEAZVERSION'] = 'false';
19+
20+
// Mock Endpoint (idToken present but should be ignored)
21+
process.env['ENDPOINT_URL_AzureRM'] = 'https://management.azure.com/';
22+
process.env['ENDPOINT_AUTH_AzureRM'] = '{"parameters":{"serviceprincipalid":"spId","serviceprincipalkey":"spKey","tenantid":"tenantId","idToken":"ignoredToken"},"scheme":"WorkloadIdentityFederation"}';
23+
process.env['ENDPOINT_AUTH_SCHEME_AzureRM'] = 'WorkloadIdentityFederation';
24+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_SERVICEPRINCIPALID'] = 'spId';
25+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_SERVICEPRINCIPALKEY'] = 'spKey';
26+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_TENANTID'] = 'tenantId';
27+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_IDTOKEN'] = 'ignoredToken';
28+
process.env['ENDPOINT_DATA_AzureRM'] = '{"environment":"AzureCloud"}';
29+
process.env['ENDPOINT_AUTH_SYSTEMVSSCONNECTION'] = '{"parameters":{"AccessToken":"token"},"scheme":"OAuth"}';
30+
process.env['ENDPOINT_AUTH_SCHEME_SYSTEMVSSCONNECTION'] = 'OAuth';
31+
process.env['ENDPOINT_AUTH_PARAMETER_SYSTEMVSSCONNECTION_ACCESSTOKEN'] = 'token';
32+
33+
// Mock Telemetry
34+
tmr.registerMock('azure-pipelines-tasks-artifacts-common/telemetry', {
35+
emitTelemetry: (area, feature, data) => {
36+
console.log(`MOCK_TELEMETRY: ${area}, ${feature}, ${JSON.stringify(data)}`);
37+
}
38+
});
39+
40+
// Mock WebApi (Should be called)
41+
tmr.registerMock('azure-devops-node-api', {
42+
getHandlerFromToken: () => {},
43+
WebApi: class {
44+
getTaskApi() {
45+
return Promise.resolve({
46+
createOidcToken: () => {
47+
console.log("MOCK_CREATE_OIDC_TOKEN_CALLED");
48+
return Promise.resolve({ oidcToken: "oidcTokenFromApi" });
49+
}
50+
});
51+
}
52+
}
53+
});
54+
55+
// Mock Utility
56+
tmr.registerMock('./src/Utility', {
57+
Utility: {
58+
checkIfAzurePythonSdkIsInstalled: () => true,
59+
throwIfError: () => {}
60+
}
61+
});
62+
63+
// Mock ScriptType
64+
tmr.registerMock('./src/ScriptType', {
65+
ScriptTypeFactory: {
66+
getSriptType: () => {
67+
return {
68+
getTool: () => {
69+
return {
70+
exec: () => Promise.resolve(0),
71+
on: () => {}
72+
};
73+
},
74+
cleanUp: () => Promise.resolve()
75+
};
76+
}
77+
}
78+
});
79+
80+
// Mock azCliUtility
81+
tmr.registerMock('azure-pipelines-tasks-azure-arm-rest/azCliUtility', {
82+
validateAzModuleVersion: () => Promise.resolve()
83+
});
84+
85+
// Mock toolrunner
86+
tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner'));
87+
88+
// Answers
89+
let a: ma.TaskLibAnswers = <ma.TaskLibAnswers>{
90+
"which": {
91+
"az": "az"
92+
},
93+
"checkPath": {
94+
"az": true
95+
},
96+
"exec": {
97+
"az version": {
98+
"code": 0,
99+
"stdout": "azure-cli 2.66.0"
100+
},
101+
"az --version": {
102+
"code": 0,
103+
"stdout": "azure-cli 2.66.0"
104+
},
105+
"az account clear": {
106+
"code": 0
107+
},
108+
"az login --service-principal -u \"spId\" --tenant \"tenantId\" --allow-no-subscriptions --federated-token \"oidcTokenFromApi\"": {
109+
"code": 0
110+
}
111+
}
112+
};
113+
tmr.setAnswers(a);
114+
115+
tmr.run();
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import ma = require('azure-pipelines-task-lib/mock-answer');
2+
import tmrm = require('azure-pipelines-task-lib/mock-run');
3+
import path = require('path');
4+
5+
let taskPath = path.join(__dirname, '..', 'azureclitask.js');
6+
let tmr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
7+
8+
// Inputs
9+
tmr.setInput('connectedServiceNameARM', 'AzureRM');
10+
tmr.setInput('scriptType', 'bash');
11+
tmr.setInput('scriptLocation', 'inlineScript');
12+
tmr.setInput('inlineScript', 'echo hello');
13+
tmr.setInput('cwd', '/tmp');
14+
tmr.setInput('visibleAzLogin', 'true');
15+
16+
// Environment variables for Feature Flag
17+
process.env['DISTRIBUTEDTASK_TASKS_ENABLELATEBOUNDIDTOKEN'] = 'true';
18+
process.env['DISTRIBUTEDTASK_TASKS_USEAZVERSION'] = 'false';
19+
20+
// Mock Endpoint (Missing idToken)
21+
process.env['ENDPOINT_URL_AzureRM'] = 'https://management.azure.com/';
22+
process.env['ENDPOINT_AUTH_AzureRM'] = '{"parameters":{"serviceprincipalid":"spId","serviceprincipalkey":"spKey","tenantid":"tenantId"},"scheme":"WorkloadIdentityFederation"}';
23+
process.env['ENDPOINT_AUTH_SCHEME_AzureRM'] = 'WorkloadIdentityFederation';
24+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_SERVICEPRINCIPALID'] = 'spId';
25+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_SERVICEPRINCIPALKEY'] = 'spKey';
26+
process.env['ENDPOINT_AUTH_PARAMETER_AzureRM_TENANTID'] = 'tenantId';
27+
process.env['ENDPOINT_DATA_AzureRM'] = '{"environment":"AzureCloud"}';
28+
process.env['ENDPOINT_AUTH_SYSTEMVSSCONNECTION'] = '{"parameters":{"AccessToken":"token"},"scheme":"OAuth"}';
29+
process.env['ENDPOINT_AUTH_SCHEME_SYSTEMVSSCONNECTION'] = 'OAuth';
30+
process.env['ENDPOINT_AUTH_PARAMETER_SYSTEMVSSCONNECTION_ACCESSTOKEN'] = 'token';
31+
32+
// Mock Telemetry
33+
tmr.registerMock('azure-pipelines-tasks-artifacts-common/telemetry', {
34+
emitTelemetry: (area, feature, data) => {
35+
console.log(`MOCK_TELEMETRY: ${area}, ${feature}, ${JSON.stringify(data)}`);
36+
}
37+
});
38+
39+
// Mock WebApi (Should be called)
40+
tmr.registerMock('azure-devops-node-api', {
41+
getHandlerFromToken: () => {},
42+
WebApi: class {
43+
getTaskApi() {
44+
return Promise.resolve({
45+
createOidcToken: () => {
46+
console.log("MOCK_CREATE_OIDC_TOKEN_CALLED");
47+
return Promise.resolve({ oidcToken: "oidcTokenFromApi" });
48+
}
49+
});
50+
}
51+
}
52+
});
53+
54+
// Mock Utility
55+
tmr.registerMock('./src/Utility', {
56+
Utility: {
57+
checkIfAzurePythonSdkIsInstalled: () => true,
58+
throwIfError: () => {}
59+
}
60+
});
61+
62+
// Mock ScriptType
63+
tmr.registerMock('./src/ScriptType', {
64+
ScriptTypeFactory: {
65+
getSriptType: () => {
66+
return {
67+
getTool: () => {
68+
return {
69+
exec: () => Promise.resolve(0),
70+
on: () => {}
71+
};
72+
},
73+
cleanUp: () => Promise.resolve()
74+
};
75+
}
76+
}
77+
});
78+
79+
// Mock azCliUtility
80+
tmr.registerMock('azure-pipelines-tasks-azure-arm-rest/azCliUtility', {
81+
validateAzModuleVersion: () => Promise.resolve()
82+
});
83+
84+
// Mock toolrunner
85+
tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner'));
86+
87+
// Answers
88+
let a: ma.TaskLibAnswers = <ma.TaskLibAnswers>{
89+
"which": {
90+
"az": "az"
91+
},
92+
"checkPath": {
93+
"az": true
94+
},
95+
"exec": {
96+
"az version": {
97+
"code": 0,
98+
"stdout": "azure-cli 2.66.0"
99+
},
100+
"az --version": {
101+
"code": 0,
102+
"stdout": "azure-cli 2.66.0"
103+
},
104+
"az account clear": {
105+
"code": 0
106+
},
107+
"az login --service-principal -u \"spId\" --tenant \"tenantId\" --allow-no-subscriptions --federated-token \"oidcTokenFromApi\"": {
108+
"code": 0
109+
}
110+
}
111+
};
112+
tmr.setAnswers(a);
113+
114+
tmr.run();

0 commit comments

Comments
 (0)