Skip to content

Commit ab6d759

Browse files
aksm-msmsftgitssuaggarN-Usha
authored
Azure PowerShell Login changes (Azure#25)
* Initial commit * updating package version * adding secret info link in logs * adding user-agent * Update README.md * Added changes for azure powershell login Changes in loginAzurePowerShell.ts using latest version Added getlatestazmodule version changes in runner info code refactor changes in loginAzurePowerShell code refactor Code refactor Code refactor added review comments changes in scriptbuilder changes in setmodulepath added paths in tsconfig.json Revert "added paths in tsconfig.json" This reverts commit cb2f417. changes in action.yml changes in main added review comments Added changes for review comments Modified description in action.yml Added telemetry info Code refactor added review comments added review comments removed tslint from package.json added log in ServicePrincipalLogin added boolean for error log Added Unit tests (Azure#15) * Added unit tests for Azure PowerShell * Added unit tests * changes in utils * removed babel * changed variable name of enable-PSSession * refactor * added ci.yml * changes in utils test making login calls silent (Azure#19) Co-authored-by: Deepak Sattiraju <desattir@microsoft.com> update utils test (Azure#16) * update utils test * update utils test * update serviceprincipallogin test Co-authored-by: Microsoft GitHub User <msftgits@microsoft.com> Co-authored-by: Sumiran Aggarwal <suaggar@microsoft.com> Co-authored-by: UshaN <ushan@microsoft.com>
1 parent d1bf5b7 commit ab6d759

22 files changed

Lines changed: 766 additions & 15 deletions

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
on:
2+
pull_request:
3+
branches:
4+
- master
5+
push:
6+
branches:
7+
- master
8+
9+
jobs:
10+
build_test_job:
11+
name: 'Build and test job'
12+
runs-on: ${{ matrix.os }}
13+
strategy:
14+
matrix:
15+
os: [windows-latest, ubuntu-latest, macos-latest]
16+
steps:
17+
18+
- name: 'Checking out repo code'
19+
uses: actions/checkout@v2
20+
21+
- name: 'Validate build'
22+
run: |
23+
npm install
24+
npm run build
25+
26+
- name: 'Run L0 tests'
27+
run: |
28+
npm run test
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ServicePrincipalLogin } from '../../src/PowerShell/ServicePrincipalLogin';
2+
3+
jest.mock('../../src/PowerShell/Utilities/Utils');
4+
jest.mock('../../src/PowerShell/Utilities/PowerShellToolRunner');
5+
let spnlogin: ServicePrincipalLogin;
6+
7+
beforeAll(() => {
8+
spnlogin = new ServicePrincipalLogin("servicePrincipalID", "servicePrinicipalkey", "tenantId", "subscriptionId");
9+
});
10+
11+
afterEach(() => {
12+
jest.restoreAllMocks();
13+
});
14+
15+
describe('Testing initialize', () => {
16+
let initializeSpy;
17+
18+
beforeEach(() => {
19+
initializeSpy = jest.spyOn(spnlogin, 'initialize');
20+
});
21+
test('ServicePrincipalLogin initialize should pass', async () => {
22+
await spnlogin.initialize();
23+
expect(initializeSpy).toHaveBeenCalled();
24+
});
25+
});
26+
27+
describe('Testing login', () => {
28+
let loginSpy;
29+
30+
beforeEach(() => {
31+
loginSpy = jest.spyOn(spnlogin, 'login');
32+
});
33+
test('ServicePrincipal login should pass', async () => {
34+
loginSpy.mockImplementationOnce(() => Promise.resolve());
35+
await spnlogin.login();
36+
expect(loginSpy).toHaveBeenCalled();
37+
});
38+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Utils from '../../../src/PowerShell/Utilities/Utils';
2+
3+
const version: string = '9.0.0';
4+
const moduleName: string = 'az';
5+
6+
afterEach(() => {
7+
jest.restoreAllMocks();
8+
});
9+
10+
describe('Testing isValidVersion', () => {
11+
const validVersion: string = '1.2.4';
12+
const invalidVersion: string = 'a.bcd';
13+
14+
test('isValidVersion should be true', () => {
15+
expect(Utils.isValidVersion(validVersion)).toBeTruthy();
16+
});
17+
test('isValidVersion should be false', () => {
18+
expect(Utils.isValidVersion(invalidVersion)).toBeFalsy();
19+
});
20+
});
21+
22+
describe('Testing setPSModulePath', () => {
23+
test('PSModulePath with azPSVersion non-empty', () => {
24+
Utils.setPSModulePath(version);
25+
expect(process.env.PSModulePath).toContain(version);
26+
});
27+
test('PSModulePath with azPSVersion empty', () => {
28+
const prevPSModulePath = process.env.PSModulePath;
29+
Utils.setPSModulePath();
30+
expect(process.env.PSModulePath).not.toEqual(prevPSModulePath);
31+
});
32+
});
33+
34+
describe('Testing getLatestModule', () => {
35+
let getLatestModuleSpy;
36+
37+
beforeEach(() => {
38+
getLatestModuleSpy = jest.spyOn(Utils, 'getLatestModule');
39+
});
40+
test('getLatestModule should pass', async () => {
41+
getLatestModuleSpy.mockImplementationOnce((_moduleName: string) => Promise.resolve(version));
42+
await Utils.getLatestModule(moduleName);
43+
expect(getLatestModuleSpy).toHaveBeenCalled();
44+
});
45+
});

action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# Login to Azure subscription
22
name: 'Azure Login'
3-
description: 'Login Azure wraps the az login, allowing Azure actions to log into Azure or to run Az CLI scripts. github.com/Azure/Actions'
3+
description: 'Authenticate to Azure and run your Az CLI or Az PowerShell based Actions or scripts.github.com/Azure/Actions'
44
inputs:
55
creds:
66
description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS'
77
required: true
8+
enable-AzPSSession:
9+
description: 'Set this value to true to enable Azure PowerShell Login in addition to Az CLI login'
10+
required: false
11+
default: false
812
branding:
913
icon: 'login.svg'
1014
color: 'blue'

jest.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// For a detailed explanation regarding each configuration property, visit:
2+
// https://jestjs.io/docs/en/configuration.html
3+
4+
module.exports = {
5+
clearMocks: true,
6+
moduleFileExtensions: ['js', 'ts'],
7+
testEnvironment: 'node',
8+
testMatch: ['**/*.test.ts'],
9+
testRunner: 'jest-circus/runner',
10+
transform: {
11+
'^.+\\.ts$': 'ts-jest'
12+
},
13+
verbose: true
14+
};

lib/PowerShell/Constants.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
class Constants {
4+
}
5+
exports.default = Constants;
6+
Constants.prefix = "az_";
7+
Constants.moduleName = "Az.Accounts";
8+
Constants.versionPattern = /[0-9]\.[0-9]\.[0-9]/;
9+
Constants.AzureCloud = "AzureCloud";
10+
Constants.Subscription = "Subscription";
11+
Constants.ServicePrincipal = "ServicePrincipal";
12+
Constants.Success = "Success";
13+
Constants.Error = "Error";
14+
Constants.AzVersion = "AzVersion";

lib/PowerShell/IAzurePowerShellSession.js

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"use strict";
2+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4+
return new (P || (P = Promise))(function (resolve, reject) {
5+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8+
step((generator = generator.apply(thisArg, _arguments || [])).next());
9+
});
10+
};
11+
var __importStar = (this && this.__importStar) || function (mod) {
12+
if (mod && mod.__esModule) return mod;
13+
var result = {};
14+
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
15+
result["default"] = mod;
16+
return result;
17+
};
18+
var __importDefault = (this && this.__importDefault) || function (mod) {
19+
return (mod && mod.__esModule) ? mod : { "default": mod };
20+
};
21+
Object.defineProperty(exports, "__esModule", { value: true });
22+
const core = __importStar(require("@actions/core"));
23+
const Utils_1 = __importDefault(require("./Utilities/Utils"));
24+
const PowerShellToolRunner_1 = __importDefault(require("./Utilities/PowerShellToolRunner"));
25+
const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder"));
26+
const Constants_1 = __importDefault(require("./Constants"));
27+
class ServicePrincipalLogin {
28+
constructor(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId) {
29+
this.servicePrincipalId = servicePrincipalId;
30+
this.servicePrincipalKey = servicePrincipalKey;
31+
this.tenantId = tenantId;
32+
this.subscriptionId = subscriptionId;
33+
}
34+
initialize() {
35+
return __awaiter(this, void 0, void 0, function* () {
36+
Utils_1.default.setPSModulePath();
37+
const azLatestVersion = yield Utils_1.default.getLatestModule(Constants_1.default.moduleName);
38+
core.debug(`Az Module version used: ${azLatestVersion}`);
39+
Utils_1.default.setPSModulePath(`${Constants_1.default.prefix}${azLatestVersion}`);
40+
});
41+
}
42+
login() {
43+
return __awaiter(this, void 0, void 0, function* () {
44+
let output = "";
45+
const options = {
46+
listeners: {
47+
stdout: (data) => {
48+
output += data.toString();
49+
}
50+
}
51+
};
52+
const args = {
53+
servicePrincipalId: this.servicePrincipalId,
54+
servicePrincipalKey: this.servicePrincipalKey,
55+
subscriptionId: this.subscriptionId,
56+
environment: ServicePrincipalLogin.environment,
57+
scopeLevel: ServicePrincipalLogin.scopeLevel
58+
};
59+
const script = new ScriptBuilder_1.default().getAzPSLoginScript(ServicePrincipalLogin.scheme, this.tenantId, args);
60+
yield PowerShellToolRunner_1.default.init();
61+
yield PowerShellToolRunner_1.default.executePowerShellScriptBlock(script, options);
62+
const result = JSON.parse(output.trim());
63+
if (!(Constants_1.default.Success in result)) {
64+
throw new Error(`Azure PowerShell login failed with error: ${result[Constants_1.default.Error]}`);
65+
}
66+
console.log(`Azure PowerShell session successfully initialized`);
67+
});
68+
}
69+
}
70+
exports.ServicePrincipalLogin = ServicePrincipalLogin;
71+
ServicePrincipalLogin.environment = Constants_1.default.AzureCloud;
72+
ServicePrincipalLogin.scopeLevel = Constants_1.default.Subscription;
73+
ServicePrincipalLogin.scheme = Constants_1.default.ServicePrincipal;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4+
return new (P || (P = Promise))(function (resolve, reject) {
5+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8+
step((generator = generator.apply(thisArg, _arguments || [])).next());
9+
});
10+
};
11+
var __importStar = (this && this.__importStar) || function (mod) {
12+
if (mod && mod.__esModule) return mod;
13+
var result = {};
14+
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
15+
result["default"] = mod;
16+
return result;
17+
};
18+
Object.defineProperty(exports, "__esModule", { value: true });
19+
const io = __importStar(require("@actions/io"));
20+
const exec = __importStar(require("@actions/exec"));
21+
class PowerShellToolRunner {
22+
static init() {
23+
return __awaiter(this, void 0, void 0, function* () {
24+
if (!PowerShellToolRunner.psPath) {
25+
PowerShellToolRunner.psPath = yield io.which("pwsh", true);
26+
}
27+
});
28+
}
29+
static executePowerShellScriptBlock(scriptBlock, options = {}) {
30+
return __awaiter(this, void 0, void 0, function* () {
31+
yield exec.exec(`${PowerShellToolRunner.psPath} -Command`, [scriptBlock], options);
32+
});
33+
}
34+
}
35+
exports.default = PowerShellToolRunner;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use strict";
2+
var __importStar = (this && this.__importStar) || function (mod) {
3+
if (mod && mod.__esModule) return mod;
4+
var result = {};
5+
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
6+
result["default"] = mod;
7+
return result;
8+
};
9+
var __importDefault = (this && this.__importDefault) || function (mod) {
10+
return (mod && mod.__esModule) ? mod : { "default": mod };
11+
};
12+
Object.defineProperty(exports, "__esModule", { value: true });
13+
const core = __importStar(require("@actions/core"));
14+
const Constants_1 = __importDefault(require("../Constants"));
15+
class ScriptBuilder {
16+
constructor() {
17+
this.script = "";
18+
}
19+
getAzPSLoginScript(scheme, tenantId, args) {
20+
let command = `Clear-AzContext -Scope Process;
21+
Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`;
22+
if (scheme === Constants_1.default.ServicePrincipal) {
23+
command += `Connect-AzAccount -ServicePrincipal -Tenant ${tenantId} -Credential \
24+
(New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString ${args.servicePrincipalKey} -AsPlainText -Force))) \
25+
-Environment ${args.environment} | out-null;`;
26+
if (args.scopeLevel === Constants_1.default.Subscription) {
27+
command += `Set-AzContext -SubscriptionId ${args.subscriptionId} -TenantId ${tenantId} | out-null;`;
28+
}
29+
}
30+
this.script += `try {
31+
$ErrorActionPreference = "Stop"
32+
$WarningPreference = "SilentlyContinue"
33+
$output = @{}
34+
${command}
35+
$output['${Constants_1.default.Success}'] = "true"
36+
}
37+
catch {
38+
$output['${Constants_1.default.Error}'] = $_.exception.Message
39+
}
40+
return ConvertTo-Json $output`;
41+
core.debug(`Azure PowerShell Login Script: ${this.script}`);
42+
return this.script;
43+
}
44+
getLatestModuleScript(moduleName) {
45+
const command = `Get-Module -Name ${moduleName} -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1`;
46+
this.script += `try {
47+
$ErrorActionPreference = "Stop"
48+
$WarningPreference = "SilentlyContinue"
49+
$output = @{}
50+
$data = ${command}
51+
$output['${Constants_1.default.AzVersion}'] = $data.Version.ToString()
52+
$output['${Constants_1.default.Success}'] = "true"
53+
}
54+
catch {
55+
$output['${Constants_1.default.Error}'] = $_.exception.Message
56+
}
57+
return ConvertTo-Json $output`;
58+
core.debug(`GetLatestModuleScript: ${this.script}`);
59+
return this.script;
60+
}
61+
}
62+
exports.default = ScriptBuilder;

0 commit comments

Comments
 (0)