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

Commit 0ee7167

Browse files
authored
feat: Azure Key Vault YAML setup support (#285)
* Add key vault linking * Add keyvault service * Update tests * Fix test * Add tests for coverage and address more pr feedback * Address final pr comments
1 parent b6433a6 commit 0ee7167

File tree

9 files changed

+278
-3
lines changed

9 files changed

+278
-3
lines changed

package-lock.json

Lines changed: 25 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@
4242
"dependencies": {
4343
"@azure/arm-apimanagement": "^5.1.0",
4444
"@azure/arm-appservice": "^5.7.0",
45+
"@azure/arm-keyvault": "^1.2.1",
4546
"@azure/arm-resources": "^1.0.1",
4647
"@azure/arm-storage": "^9.0.1",
4748
"@azure/ms-rest-nodeauth": "^1.0.1",
4849
"@azure/storage-blob": "^10.3.0",
50+
"acorn": "^7.0.0",
4951
"axios": "^0.18.0",
5052
"azure-functions-core-tools": "^2.7.1575",
5153
"deep-equal": "^1.0.1",

src/armTemplates/resources/functionApp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator {
5858
"apiVersion": "2016-03-01",
5959
"name": "[parameters('functionAppName')]",
6060
"location": "[parameters('location')]",
61+
"identity": {
62+
"type": "SystemAssigned"
63+
},
6164
"dependsOn": [
6265
"[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]",
6366
"[concat('microsoft.insights/components/', parameters('appInsightsName'))]"

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { AzureApimFunctionPlugin } from "./plugins/apim/azureApimFunctionPlugin"
1616
import { AzureFuncPlugin } from "./plugins/func/azureFuncPlugin";
1717
import { AzureOfflinePlugin } from "./plugins/offline/azureOfflinePlugin"
1818
import { AzureRollbackPlugin } from "./plugins/rollback/azureRollbackPlugin"
19+
import { AzureKeyVaultPlugin } from "./plugins/identity/azureKeyVaultPlugin"
1920

2021

2122
export default class AzureIndex {
@@ -34,6 +35,7 @@ export default class AzureIndex {
3435
this.serverless.pluginManager.addPlugin(AzureFuncPlugin);
3536
this.serverless.pluginManager.addPlugin(AzureOfflinePlugin);
3637
this.serverless.pluginManager.addPlugin(AzureRollbackPlugin);
38+
this.serverless.pluginManager.addPlugin(AzureKeyVaultPlugin);
3739
}
3840
}
3941

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Serverless from "serverless";
2+
import { MockFactory } from "../../test/mockFactory";
3+
import { invokeHook } from "../../test/utils";
4+
import { AzureKeyVaultPlugin } from "./azureKeyVaultPlugin";
5+
6+
jest.mock("../../services/azureKeyVaultService.ts");
7+
import { AzureKeyVaultService } from "../../services/azureKeyVaultService";
8+
9+
describe("Azure Key Vault Plugin", () => {
10+
it("is defined", () => {
11+
expect(AzureKeyVaultPlugin).toBeDefined();
12+
});
13+
14+
it("can be instantiated", () => {
15+
const serverless = MockFactory.createTestServerless();
16+
const options: Serverless.Options = MockFactory.createTestServerlessOptions();
17+
const plugin = new AzureKeyVaultPlugin(serverless, options);
18+
19+
expect(plugin).not.toBeNull();
20+
});
21+
22+
it("calls set policy when key vault specified", async () => {
23+
const setPolicy = jest.fn();
24+
25+
AzureKeyVaultService.prototype.setPolicy = setPolicy;
26+
27+
const sls = MockFactory.createTestServerless();
28+
sls.service.provider["keyVault"] = { name: "testVault", resourceGroup: "testGroup"}
29+
const options = MockFactory.createTestServerlessOptions();
30+
const plugin = new AzureKeyVaultPlugin(sls, options);
31+
32+
await invokeHook(plugin, "after:deploy:deploy");
33+
34+
expect(sls.cli.log).toBeCalledWith("Starting KeyVault service setup")
35+
expect(setPolicy).toBeCalled();
36+
expect(sls.cli.log).lastCalledWith("Finished KeyVault service setup")
37+
});
38+
39+
it("does not call deploy API or deploy functions when \"keyVault\" not included in config", async () => {
40+
const setPolicy = jest.fn();
41+
42+
AzureKeyVaultService.prototype.setPolicy = setPolicy;
43+
44+
const sls = MockFactory.createTestServerless();
45+
const options = MockFactory.createTestServerlessOptions();
46+
const plugin = new AzureKeyVaultPlugin(sls, options);
47+
48+
await invokeHook(plugin, "after:deploy:deploy");
49+
50+
expect(sls.cli.log).not.toBeCalled()
51+
expect(setPolicy).not.toBeCalled();
52+
});
53+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Serverless from "serverless";
2+
import { AzureBasePlugin } from "../azureBasePlugin";
3+
import { AzureKeyVaultService } from "../../services/azureKeyVaultService";
4+
5+
export class AzureKeyVaultPlugin extends AzureBasePlugin {
6+
public constructor(serverless: Serverless, options: Serverless.Options) {
7+
super(serverless, options);
8+
this.hooks = {
9+
"after:deploy:deploy": this.link.bind(this)
10+
};
11+
}
12+
13+
private async link() {
14+
const keyVaultConfig = this.serverless.service.provider["keyVault"];
15+
if (!keyVaultConfig) {
16+
return Promise.resolve();
17+
}
18+
this.serverless.cli.log("Starting KeyVault service setup");
19+
20+
const keyVaultService = new AzureKeyVaultService(this.serverless, this.options);
21+
const result = await keyVaultService.setPolicy(keyVaultConfig);
22+
23+
this.serverless.cli.log("Finished KeyVault service setup");
24+
25+
return result;
26+
}
27+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Serverless from "serverless";
2+
import { MockFactory } from "../test/mockFactory";
3+
import {
4+
AzureKeyVaultService,
5+
AzureKeyVaultConfig
6+
} from "./azureKeyVaultService";
7+
8+
import { Vaults } from "@azure/arm-keyvault";
9+
import { FunctionAppService } from "./functionAppService";
10+
11+
describe("Azure Key Vault Service", () => {
12+
const options: Serverless.Options = MockFactory.createTestServerlessOptions();
13+
const knownVaults = {
14+
testVault: {
15+
location: "WestUS",
16+
properties: {
17+
accessPolicies: []
18+
}
19+
}
20+
};
21+
22+
let serverless: Serverless = MockFactory.createTestServerless();
23+
24+
beforeEach(() => {
25+
Vaults.prototype.createOrUpdate = jest.fn();
26+
Vaults.prototype.get = jest.fn(async (resourceGroup, vaultName) => {
27+
if (!knownVaults.hasOwnProperty(vaultName)) {
28+
throw new Error("No matching vaults");
29+
}
30+
return knownVaults["testVault"];
31+
}) as any;
32+
33+
FunctionAppService.prototype.get = jest.fn(() => {
34+
console.log("testing");
35+
return {
36+
identity: {
37+
tenantId: "tid",
38+
principalId: "oid"
39+
}
40+
} as any;
41+
});
42+
});
43+
44+
it("is defined", () => {
45+
expect(AzureKeyVaultService).toBeDefined();
46+
});
47+
48+
it("can be instantiated", () => {
49+
const service = new AzureKeyVaultService(serverless, options);
50+
expect(service).not.toBeNull();
51+
});
52+
53+
it("Throws an error if keyvault doesn't exist", async () => {
54+
const service = new AzureKeyVaultService(serverless, options);
55+
const keyVault: AzureKeyVaultConfig = MockFactory.createTestKeyVaultConfig(
56+
"fake-vault"
57+
);
58+
59+
await expect(service.setPolicy(keyVault)).rejects.toThrowError(
60+
"Error: Specified vault not found"
61+
);
62+
await expect(Vaults.prototype.createOrUpdate).not.toBeCalled();
63+
});
64+
65+
it("Sets correct policy if correct keyvault name is specified", async () => {
66+
const keyVault: AzureKeyVaultConfig = MockFactory.createTestKeyVaultConfig(
67+
"testVault"
68+
);
69+
const slsConfig: any = {
70+
...MockFactory.createTestService(
71+
MockFactory.createTestSlsFunctionConfig()
72+
),
73+
service: "test-sls",
74+
provider: {
75+
name: "azure",
76+
resourceGroup: "test-sls-rg",
77+
region: "West US",
78+
keyVault
79+
}
80+
};
81+
82+
serverless = MockFactory.createTestServerless({
83+
service: slsConfig
84+
});
85+
86+
const service = new AzureKeyVaultService(serverless, options);
87+
await service.setPolicy(keyVault);
88+
89+
await expect(Vaults.prototype.createOrUpdate).toBeCalledWith(
90+
keyVault.resourceGroup,
91+
keyVault.name,
92+
{
93+
location: knownVaults["testVault"].location,
94+
properties: knownVaults["testVault"].properties
95+
}
96+
);
97+
});
98+
});

src/services/azureKeyVaultService.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Serverless from "serverless";
2+
import { BaseService } from "./baseService";
3+
import { FunctionAppService } from "./functionAppService";
4+
import { KeyVaultManagementClient } from "@azure/arm-keyvault";
5+
import { KeyPermissions, SecretPermissions, Vault } from "@azure/arm-keyvault/esm/models/index";
6+
7+
/**
8+
* Defines the Azure Key Vault configuration
9+
*/
10+
export interface AzureKeyVaultConfig {
11+
/** The name of the azure key vault */
12+
name: string;
13+
/** The name of the azure resource group with the key vault */
14+
resourceGroup: string;
15+
}
16+
17+
/**
18+
* Services for the Key Vault Plugin
19+
*/
20+
export class AzureKeyVaultService extends BaseService {
21+
private funcApp: FunctionAppService;
22+
23+
/**
24+
* Initialize key vault service and get function app
25+
* @param serverless Serverless object
26+
* @param options Serverless CLI options
27+
*/
28+
public constructor(serverless: Serverless, options: Serverless.Options) {
29+
super(serverless, options);
30+
this.funcApp = new FunctionAppService(serverless, options);
31+
}
32+
33+
/**
34+
* Sets the KeyVault policy for the function app to allow secrets permissions.
35+
* @param keyVaultConfig Azure Key Vault settings
36+
*/
37+
public async setPolicy(keyVaultConfig: AzureKeyVaultConfig) {
38+
const subscriptionID = this.subscriptionId;
39+
40+
const func = await this.funcApp.get();
41+
const identity = func.identity;
42+
let vault: Vault;
43+
const keyVaultClient = new KeyVaultManagementClient(this.credentials, subscriptionID);
44+
try {
45+
vault = await keyVaultClient.vaults.get(keyVaultConfig.resourceGroup, keyVaultConfig.name)
46+
} catch (error) {
47+
throw new Error("Error: Specified vault not found")
48+
}
49+
50+
const newEntry = {
51+
tenantId: identity.tenantId,
52+
objectId: identity.principalId,
53+
permissions: {
54+
secrets: ["get" as SecretPermissions],
55+
}
56+
}
57+
vault.properties.accessPolicies.push(newEntry);
58+
59+
return keyVaultClient.vaults.createOrUpdate(keyVaultConfig.resourceGroup, keyVaultConfig.name, {location: vault.location, properties: vault.properties})
60+
}
61+
}

src/test/mockFactory.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ export class MockFactory {
323323
};
324324
}
325325

326+
public static createTestKeyVaultConfig(name: string = "testVault") {
327+
return {
328+
name: name,
329+
resourceGroup: "testGroup",
330+
};
331+
}
332+
326333
public static createTestFunctionMetadata(name: string) {
327334
return {
328335
"handler": `${name}.handler`,

0 commit comments

Comments
 (0)