-
Notifications
You must be signed in to change notification settings - Fork 161
feat: Azure Key Vault YAML setup support #285
Changes from all 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,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 | ||
} | ||
); | ||
}); | ||
}); |
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"; | ||
|
||
wbreza marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* 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}) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.