Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

feat: Azure Key Vault YAML setup support #285

Merged
merged 6 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always going to be the case? Is there any situation where someone would not want system assigned identity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, I don't know if there is any issue with always assigning the 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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is test name correct? I would imagine it not calling setPolicy or similar, right?

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 @@ -322,6 +322,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