-
Notifications
You must be signed in to change notification settings - Fork 162
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
9 changed files
with
278 additions
and
3 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters