Skip to content

Commit

Permalink
feat: Azure Key Vault YAML setup support (#285)
Browse files Browse the repository at this point in the history
* Add key vault linking

* Add keyvault service

* Update tests

* Fix test

* Add tests for coverage and address more pr feedback

* Address final pr comments
  • Loading branch information
PIC123 authored Sep 4, 2019
1 parent b6433a6 commit 0ee7167
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 3 deletions.
28 changes: 25 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@
"dependencies": {
"@azure/arm-apimanagement": "^5.1.0",
"@azure/arm-appservice": "^5.7.0",
"@azure/arm-keyvault": "^1.2.1",
"@azure/arm-resources": "^1.0.1",
"@azure/arm-storage": "^9.0.1",
"@azure/ms-rest-nodeauth": "^1.0.1",
"@azure/storage-blob": "^10.3.0",
"acorn": "^7.0.0",
"axios": "^0.18.0",
"azure-functions-core-tools": "^2.7.1575",
"deep-equal": "^1.0.1",
Expand Down
3 changes: 3 additions & 0 deletions src/armTemplates/resources/functionApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
"apiVersion": "2016-03-01",
"name": "[parameters('functionAppName')]",
"location": "[parameters('location')]",
"identity": {
"type": "SystemAssigned"
},
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]",
"[concat('microsoft.insights/components/', parameters('appInsightsName'))]"
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AzureApimFunctionPlugin } from "./plugins/apim/azureApimFunctionPlugin"
import { AzureFuncPlugin } from "./plugins/func/azureFuncPlugin";
import { AzureOfflinePlugin } from "./plugins/offline/azureOfflinePlugin"
import { AzureRollbackPlugin } from "./plugins/rollback/azureRollbackPlugin"
import { AzureKeyVaultPlugin } from "./plugins/identity/azureKeyVaultPlugin"


export default class AzureIndex {
Expand All @@ -34,6 +35,7 @@ export default class AzureIndex {
this.serverless.pluginManager.addPlugin(AzureFuncPlugin);
this.serverless.pluginManager.addPlugin(AzureOfflinePlugin);
this.serverless.pluginManager.addPlugin(AzureRollbackPlugin);
this.serverless.pluginManager.addPlugin(AzureKeyVaultPlugin);
}
}

Expand Down
53 changes: 53 additions & 0 deletions src/plugins/identity/azureKeyVaultPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Serverless from "serverless";
import { MockFactory } from "../../test/mockFactory";
import { invokeHook } from "../../test/utils";
import { AzureKeyVaultPlugin } from "./azureKeyVaultPlugin";

jest.mock("../../services/azureKeyVaultService.ts");
import { AzureKeyVaultService } from "../../services/azureKeyVaultService";

describe("Azure Key Vault Plugin", () => {
it("is defined", () => {
expect(AzureKeyVaultPlugin).toBeDefined();
});

it("can be instantiated", () => {
const serverless = MockFactory.createTestServerless();
const options: Serverless.Options = MockFactory.createTestServerlessOptions();
const plugin = new AzureKeyVaultPlugin(serverless, options);

expect(plugin).not.toBeNull();
});

it("calls set policy when key vault specified", async () => {
const setPolicy = jest.fn();

AzureKeyVaultService.prototype.setPolicy = setPolicy;

const sls = MockFactory.createTestServerless();
sls.service.provider["keyVault"] = { name: "testVault", resourceGroup: "testGroup"}
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureKeyVaultPlugin(sls, options);

await invokeHook(plugin, "after:deploy:deploy");

expect(sls.cli.log).toBeCalledWith("Starting KeyVault service setup")
expect(setPolicy).toBeCalled();
expect(sls.cli.log).lastCalledWith("Finished KeyVault service setup")
});

it("does not call deploy API or deploy functions when \"keyVault\" not included in config", async () => {
const setPolicy = jest.fn();

AzureKeyVaultService.prototype.setPolicy = setPolicy;

const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const plugin = new AzureKeyVaultPlugin(sls, options);

await invokeHook(plugin, "after:deploy:deploy");

expect(sls.cli.log).not.toBeCalled()
expect(setPolicy).not.toBeCalled();
});
});
27 changes: 27 additions & 0 deletions src/plugins/identity/azureKeyVaultPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Serverless from "serverless";
import { AzureBasePlugin } from "../azureBasePlugin";
import { AzureKeyVaultService } from "../../services/azureKeyVaultService";

export class AzureKeyVaultPlugin extends AzureBasePlugin {
public constructor(serverless: Serverless, options: Serverless.Options) {
super(serverless, options);
this.hooks = {
"after:deploy:deploy": this.link.bind(this)
};
}

private async link() {
const keyVaultConfig = this.serverless.service.provider["keyVault"];
if (!keyVaultConfig) {
return Promise.resolve();
}
this.serverless.cli.log("Starting KeyVault service setup");

const keyVaultService = new AzureKeyVaultService(this.serverless, this.options);
const result = await keyVaultService.setPolicy(keyVaultConfig);

this.serverless.cli.log("Finished KeyVault service setup");

return result;
}
}
98 changes: 98 additions & 0 deletions src/services/azureKeyVaultService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import Serverless from "serverless";
import { MockFactory } from "../test/mockFactory";
import {
AzureKeyVaultService,
AzureKeyVaultConfig
} from "./azureKeyVaultService";

import { Vaults } from "@azure/arm-keyvault";
import { FunctionAppService } from "./functionAppService";

describe("Azure Key Vault Service", () => {
const options: Serverless.Options = MockFactory.createTestServerlessOptions();
const knownVaults = {
testVault: {
location: "WestUS",
properties: {
accessPolicies: []
}
}
};

let serverless: Serverless = MockFactory.createTestServerless();

beforeEach(() => {
Vaults.prototype.createOrUpdate = jest.fn();
Vaults.prototype.get = jest.fn(async (resourceGroup, vaultName) => {
if (!knownVaults.hasOwnProperty(vaultName)) {
throw new Error("No matching vaults");
}
return knownVaults["testVault"];
}) as any;

FunctionAppService.prototype.get = jest.fn(() => {
console.log("testing");
return {
identity: {
tenantId: "tid",
principalId: "oid"
}
} as any;
});
});

it("is defined", () => {
expect(AzureKeyVaultService).toBeDefined();
});

it("can be instantiated", () => {
const service = new AzureKeyVaultService(serverless, options);
expect(service).not.toBeNull();
});

it("Throws an error if keyvault doesn't exist", async () => {
const service = new AzureKeyVaultService(serverless, options);
const keyVault: AzureKeyVaultConfig = MockFactory.createTestKeyVaultConfig(
"fake-vault"
);

await expect(service.setPolicy(keyVault)).rejects.toThrowError(
"Error: Specified vault not found"
);
await expect(Vaults.prototype.createOrUpdate).not.toBeCalled();
});

it("Sets correct policy if correct keyvault name is specified", async () => {
const keyVault: AzureKeyVaultConfig = MockFactory.createTestKeyVaultConfig(
"testVault"
);
const slsConfig: any = {
...MockFactory.createTestService(
MockFactory.createTestSlsFunctionConfig()
),
service: "test-sls",
provider: {
name: "azure",
resourceGroup: "test-sls-rg",
region: "West US",
keyVault
}
};

serverless = MockFactory.createTestServerless({
service: slsConfig
});

const service = new AzureKeyVaultService(serverless, options);
await service.setPolicy(keyVault);

await expect(Vaults.prototype.createOrUpdate).toBeCalledWith(
keyVault.resourceGroup,
keyVault.name,
{
location: knownVaults["testVault"].location,
properties: knownVaults["testVault"].properties
}
);
});
});
61 changes: 61 additions & 0 deletions src/services/azureKeyVaultService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Serverless from "serverless";
import { BaseService } from "./baseService";
import { FunctionAppService } from "./functionAppService";
import { KeyVaultManagementClient } from "@azure/arm-keyvault";
import { KeyPermissions, SecretPermissions, Vault } from "@azure/arm-keyvault/esm/models/index";

/**
* Defines the Azure Key Vault configuration
*/
export interface AzureKeyVaultConfig {
/** The name of the azure key vault */
name: string;
/** The name of the azure resource group with the key vault */
resourceGroup: string;
}

/**
* Services for the Key Vault Plugin
*/
export class AzureKeyVaultService extends BaseService {
private funcApp: FunctionAppService;

/**
* Initialize key vault service and get function app
* @param serverless Serverless object
* @param options Serverless CLI options
*/
public constructor(serverless: Serverless, options: Serverless.Options) {
super(serverless, options);
this.funcApp = new FunctionAppService(serverless, options);
}

/**
* Sets the KeyVault policy for the function app to allow secrets permissions.
* @param keyVaultConfig Azure Key Vault settings
*/
public async setPolicy(keyVaultConfig: AzureKeyVaultConfig) {
const subscriptionID = this.subscriptionId;

const func = await this.funcApp.get();
const identity = func.identity;
let vault: Vault;
const keyVaultClient = new KeyVaultManagementClient(this.credentials, subscriptionID);
try {
vault = await keyVaultClient.vaults.get(keyVaultConfig.resourceGroup, keyVaultConfig.name)
} catch (error) {
throw new Error("Error: Specified vault not found")
}

const newEntry = {
tenantId: identity.tenantId,
objectId: identity.principalId,
permissions: {
secrets: ["get" as SecretPermissions],
}
}
vault.properties.accessPolicies.push(newEntry);

return keyVaultClient.vaults.createOrUpdate(keyVaultConfig.resourceGroup, keyVaultConfig.name, {location: vault.location, properties: vault.properties})
}
}
7 changes: 7 additions & 0 deletions src/test/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,13 @@ export class MockFactory {
};
}

public static createTestKeyVaultConfig(name: string = "testVault") {
return {
name: name,
resourceGroup: "testGroup",
};
}

public static createTestFunctionMetadata(name: string) {
return {
"handler": `${name}.handler`,
Expand Down

0 comments on commit 0ee7167

Please sign in to comment.