Skip to content

Commit 093629a

Browse files
committed
Add late-bound idToken support with telemetry to AzureCLIV3 task
1 parent 61d78b0 commit 093629a

File tree

7 files changed

+409
-2
lines changed

7 files changed

+409
-2
lines changed

Tasks/AzureCLIV3/Tests/L0.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,50 @@ describe('AzureCLIV3 Suite', function () {
260260
done(err);
261261
});
262262
});
263+
264+
it('LateBoundIdToken: Feature Flag ON, Token Present -> Uses Token, Emits Telemetry', async () => {
265+
let tp = path.join(__dirname, 'LateBoundIdToken_FeatureFlagOn_TokenPresent.js');
266+
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
267+
await tr.runAsync();
268+
269+
if (!tr.succeeded) {
270+
console.log('STDOUT:', tr.stdout);
271+
console.log('STDERR:', tr.stderr);
272+
}
273+
274+
assert(tr.succeeded, 'task should have succeeded');
275+
assert(tr.stdout.indexOf('MOCK_TELEMETRY: AzureCLIV2, LateBoundIdToken, {"connectedService":"AzureRM","idTokenPresent":"true"}') >= 0, 'should emit telemetry with idTokenPresent=true');
276+
assert(tr.stdout.indexOf('Using bound idToken from service endpoint.') >= 0, 'should log that it is using bound idToken');
277+
assert(tr.stdout.indexOf('MOCK_CREATE_OIDC_TOKEN_CALLED') === -1, 'should NOT call createOidcToken');
278+
});
279+
280+
it('LateBoundIdToken: Feature Flag ON, Token Missing -> Calls API, Emits Telemetry', async () => {
281+
let tp = path.join(__dirname, 'LateBoundIdToken_FeatureFlagOn_TokenMissing.js');
282+
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
283+
await tr.runAsync();
284+
285+
if (!tr.succeeded) {
286+
console.log('STDOUT:', tr.stdout);
287+
console.log('STDERR:', tr.stderr);
288+
}
289+
290+
assert(tr.succeeded, 'task should have succeeded');
291+
assert(tr.stdout.indexOf('MOCK_TELEMETRY: AzureCLIV2, LateBoundIdToken, {"connectedService":"AzureRM","idTokenPresent":"false"}') >= 0, 'should emit telemetry with idTokenPresent=false');
292+
assert(tr.stdout.indexOf('MOCK_CREATE_OIDC_TOKEN_CALLED') >= 0, 'should call createOidcToken');
293+
});
294+
295+
it('LateBoundIdToken: Feature Flag OFF -> Calls API, No Telemetry', async () => {
296+
let tp = path.join(__dirname, 'LateBoundIdToken_FeatureFlagOff.js');
297+
let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp);
298+
await tr.runAsync();
299+
300+
if (!tr.succeeded) {
301+
console.log('STDOUT:', tr.stdout);
302+
console.log('STDERR:', tr.stderr);
303+
}
304+
305+
assert(tr.succeeded, 'task should have succeeded');
306+
assert(tr.stdout.indexOf('MOCK_TELEMETRY: AzureCLIV2, LateBoundIdToken') === -1, 'should NOT emit LateBoundIdToken telemetry');
307+
assert(tr.stdout.indexOf('MOCK_CREATE_OIDC_TOKEN_CALLED') >= 0, 'should call createOidcToken');
308+
});
263309
});
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();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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
21+
process.env['ENDPOINT_URL_AzureRM'] = 'https://management.azure.com/';
22+
process.env['ENDPOINT_AUTH_AzureRM'] = '{"parameters":{"serviceprincipalid":"spId","serviceprincipalkey":"spKey","tenantid":"tenantId","idToken":"myLateBoundToken"},"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'] = 'myLateBoundToken';
28+
process.env['ENDPOINT_DATA_AzureRM'] = '{"environment":"AzureCloud"}';
29+
30+
// Mock Telemetry
31+
tmr.registerMock('azure-pipelines-tasks-artifacts-common/telemetry', {
32+
emitTelemetry: (area, feature, data) => {
33+
console.log(`MOCK_TELEMETRY: ${area}, ${feature}, ${JSON.stringify(data)}`);
34+
}
35+
});
36+
37+
// Mock WebApi (should not be called in this case)
38+
tmr.registerMock('azure-devops-node-api', {
39+
getHandlerFromToken: () => {},
40+
WebApi: class {
41+
getTaskApi() { return { createOidcToken: () => { throw new Error("Should not be called"); } }; }
42+
}
43+
});
44+
45+
// Mock Utility
46+
tmr.registerMock('./src/Utility', {
47+
Utility: {
48+
checkIfAzurePythonSdkIsInstalled: () => true,
49+
throwIfError: () => {}
50+
}
51+
});
52+
53+
// Mock ScriptType
54+
tmr.registerMock('./src/ScriptType', {
55+
ScriptTypeFactory: {
56+
getSriptType: () => {
57+
return {
58+
getTool: () => {
59+
return {
60+
exec: () => Promise.resolve(0),
61+
on: () => {}
62+
};
63+
},
64+
cleanUp: () => Promise.resolve()
65+
};
66+
}
67+
}
68+
});
69+
70+
// Mock azCliUtility
71+
tmr.registerMock('azure-pipelines-tasks-azure-arm-rest/azCliUtility', {
72+
validateAzModuleVersion: () => Promise.resolve()
73+
});
74+
75+
// Mock toolrunner
76+
tmr.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner'));
77+
78+
// Answers
79+
let a: ma.TaskLibAnswers = <ma.TaskLibAnswers>{
80+
"which": {
81+
"az": "az"
82+
},
83+
"checkPath": {
84+
"az": true
85+
},
86+
"exec": {
87+
"az version": {
88+
"code": 0,
89+
"stdout": "azure-cli 2.66.0"
90+
},
91+
"az --version": {
92+
"code": 0,
93+
"stdout": "azure-cli 2.66.0"
94+
},
95+
"az account clear": {
96+
"code": 0
97+
},
98+
"az login --service-principal -u \"spId\" --tenant \"tenantId\" --allow-no-subscriptions --federated-token \"myLateBoundToken\"": {
99+
"code": 0
100+
}
101+
}
102+
};
103+
tmr.setAnswers(a);
104+
105+
tmr.run();

0 commit comments

Comments
 (0)