-
Notifications
You must be signed in to change notification settings - Fork 161
feat: Azure Key Vault YAML setup support #285
Changes from 5 commits
f20c7ed
9e5a7bf
652ac07
115e255
9eb6c96
5fca3b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,9 @@ export class FunctionAppResource implements ArmResourceTemplateGenerator { | |
"apiVersion": "2016-03-01", | ||
"name": "[parameters('functionAppName')]", | ||
"location": "[parameters('location')]", | ||
"identity": { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'))]" | ||
|
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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is test name correct? I would imagine it not calling |
||
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(); | ||
}); | ||
}); |
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import Serverless from "serverless"; | ||
import { MockFactory } from "../test/mockFactory"; | ||
import { | ||
AzureKeyVaultService, | ||
AzureKeyVaultConfig | ||
} from "./azureKeyVaultService"; | ||
|
||
import { KeyVaultManagementClient, Vaults } from "@azure/arm-keyvault"; | ||
import { FunctionAppService } from "./functionAppService"; | ||
|
||
describe("Azure Key Vault Service", () => { | ||
const options: Serverless.Options = MockFactory.createTestServerlessOptions(); | ||
const createOrUpdate = jest.fn(() => { | ||
console.log(); | ||
}); | ||
const knownVaults = { | ||
testVault: { | ||
location: "WestUS", | ||
properties: { | ||
accessPolicies: [] | ||
} | ||
} | ||
}; | ||
|
||
let serverless: Serverless = MockFactory.createTestServerless(); | ||
|
||
beforeEach(() => { | ||
// KeyVaultManagementClient.prototype.vaults = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed commented code. |
||
// get: jest.fn(async (resourceGroup, vaultName) => { | ||
// if (!knownVaults.hasOwnProperty(vaultName)) { | ||
// throw new Error("No matching vaults"); | ||
// } | ||
// return knownVaults["testVault"]; | ||
// }), | ||
// createOrUpdate, | ||
// } as any; | ||
|
||
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 correct keyvault name is not specified", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test name seems off. The test shows that the vault doesn't exist - not that it was not specified. |
||
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 | ||
} | ||
); | ||
// fail(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import Serverless from "serverless"; | ||
import { BaseService } from "./baseService"; | ||
import { FunctionAppService } from "./functionAppService"; | ||
import { KeyVaultManagementClient } from "@azure/arm-keyvault"; | ||
import { KeyPermissions, SecretPermissions } 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; | ||
|
||
const keyVaultClient = new KeyVaultManagementClient(this.credentials, subscriptionID); | ||
const vault = await keyVaultClient.vaults.get(keyVaultConfig.resourceGroup, keyVaultConfig.name).catch((e) => {throw new Error("Error: Specified vault not found")}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the |
||
|
||
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}) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.