Skip to content

Commit

Permalink
Merge pull request #134 from jasonwbarnett/support-msi
Browse files Browse the repository at this point in the history
Add MSI Support
  • Loading branch information
tas50 committed Apr 23, 2020
2 parents 9b04c9f + 150b7f2 commit 17a2170
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 9 deletions.
1 change: 1 addition & 0 deletions kitchen-azurerm.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "rspec", "~> 3.5"
spec.add_development_dependency "rspec-mocks", "~> 3.5"
spec.add_development_dependency "rspec-expectations", "~> 3.5"
spec.add_development_dependency "rspec-its", "~> 1.3.0"
end
35 changes: 26 additions & 9 deletions lib/kitchen/driver/azure_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def initialize(subscription_id:, environment: "Azure")
if File.file?(config_file)
@credentials = IniFile.load(File.expand_path(config_file))
else
warn "#{CONFIG_PATH} was not found or not accessible."
warn "#{CONFIG_PATH} was not found or not accessible. Will use environment variables or MSI."
end
end

Expand All @@ -38,33 +38,50 @@ def initialize(subscription_id:, environment: "Azure")
# @return [Object] Object that can be supplied along with all Azure client requests.
#
def azure_options
options = { tenant_id: tenant_id,
client_id: client_id,
client_secret: client_secret,
options = { tenant_id: tenant_id!,
subscription_id: subscription_id,
credentials: ::MsRest::TokenCredentials.new(token_provider),
active_directory_settings: ad_settings,
base_url: endpoint_settings.resource_manager_endpoint_url }

options[:client_id] = client_id if client_id
options[:client_secret] = client_secret if client_secret
options
end

private

def credentials
@credentials ||= {}
end

def credentials_property(property)
credentials[subscription_id]&.[](property)
end

def tenant_id!
tenant_id || raise("Must provide tenant id. Use AZURE_TENANT_ID environment variable or set it in credentials file")
end

def tenant_id
ENV["AZURE_TENANT_ID"] || @credentials[subscription_id]["tenant_id"]
ENV["AZURE_TENANT_ID"] || credentials_property("tenant_id")
end

def client_id
ENV["AZURE_CLIENT_ID"] || @credentials[subscription_id]["client_id"]
ENV["AZURE_CLIENT_ID"] || credentials_property("client_id")
end

def client_secret
ENV["AZURE_CLIENT_SECRET"] || @credentials[subscription_id]["client_secret"]
ENV["AZURE_CLIENT_SECRET"] || credentials_property("client_secret")
end

def token_provider
::MsRestAzure::ApplicationTokenProvider.new(tenant_id, client_id, client_secret, ad_settings)
if client_id && client_secret
::MsRestAzure::ApplicationTokenProvider.new(tenant_id, client_id, client_secret, ad_settings)
elsif client_id
::MsRestAzure::MSITokenProvider.new(50342, ad_settings, { client_id: client_id })
else
::MsRestAzure::MSITokenProvider.new(50342, ad_settings)
end
end

#
Expand Down
14 changes: 14 additions & 0 deletions spec/fixtures/azure_credentials
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# For client_id && client_secret tests
[f02932df-7e1d-410f-b094-c626d447f4dc]
client_id = "b5f3d6df-00bf-4451-a4f2-db3bc7731b58"
client_secret = ":Qnt[7?:7RXzdMXrXE0ygBROA1hY1iV["
tenant_id = "19d3ea3e-ea8f-48f3-9f7a-00ae2810991f"

# For MSI test, with client_id
[5d801ddc-acf4-406b-9830-587ca2c6fd80]
client_id = "2801f9e6-c4c2-4667-a6e1-479f8827b0af"
tenant_id = "1ba5986d-52e1-49eb-a77e-155b7440695f"

# For MSI test, no client_id
[7c664d3f-6dca-4e6d-9637-13dadbbe59d3]
tenant_id = "76d8fa56-d867-4819-9894-f6dd4e1d2079"
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require "rspec/its"
require_relative "../lib/kitchen/driver/azurerm"
204 changes: 204 additions & 0 deletions spec/unit/kitchen/driver/azure_credentials_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
require "spec_helper"

describe Kitchen::Driver::AzureCredentials do
CLIENT_ID_AND_SECRET_SUB = 0
CLIENT_ID_SUB = 1
NO_CLIENT_SUB = 2

let(:instance) do
opts = {}
opts[:subscription_id] = subscription_id
opts[:environment] = environment if environment
described_class.new(**opts)
end

let(:environment) { "Azure" }
let(:fixtures_path) { File.expand_path("../../../../fixtures", __FILE__) }
let(:subscription_id) { ini_credentials.sections[CLIENT_ID_AND_SECRET_SUB] }
let(:client_id) { ini_credentials[subscription_id]["client_id"] }
let(:client_secret) { ini_credentials[subscription_id]["client_secret"] }
let(:tenant_id) { ini_credentials[subscription_id]["tenant_id"] }
let(:config_path) { File.expand_path(described_class::CONFIG_PATH) }
let(:ini_credentials) { IniFile.load("#{fixtures_path}/azure_credentials") }

before do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("AZURE_CONFIG_FILE").and_return(nil)
allow(ENV).to receive(:[]).with("AZURE_TENANT_ID").and_return(nil)
allow(ENV).to receive(:[]).with("AZURE_CLIENT_ID").and_return(nil)
allow(ENV).to receive(:[]).with("AZURE_CLIENT_SECRET").and_return(nil)

allow(File).to receive(:file?).and_call_original
allow(File).to receive(:file?).with(config_path).and_return(true)

allow(IniFile).to receive(:load).with(config_path).and_return(ini_credentials)
end

subject { instance }

it { is_expected.to respond_to(:subscription_id) }
it { is_expected.to respond_to(:environment) }
it { is_expected.to respond_to(:azure_options) }

describe "::new" do
it "sets subscription_id" do
expect(subject.subscription_id).to eq(subscription_id)
end

context "when an environment is provided" do
let(:environment) { "AzureChina" }

it "sets environment, when one is provided" do
expect(subject.environment).to eq(environment)
end
end

context "no environment is provided" do
let(:environment) { nil }

it "sets Azure as the environment" do
expect(subject.environment).to eq("Azure")
end
end
end

describe "#azure_options" do
subject { azure_options }

let(:azure_options) { instance.azure_options }
let(:credentials) { azure_options[:credentials] }
let(:token_provider) { credentials.instance_variable_get(:@token_provider) }
let(:active_directory_settings) { azure_options[:active_directory_settings] }

context "when environment is Azure" do
let(:environment) { "Azure" }

its([:base_url]) { is_expected.to eq("https://management.azure.com/") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.com/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.windows.net/")
end
end
end

context "when environment is AzureUSGovernment" do
let(:environment) { "AzureUSGovernment" }

its([:base_url]) { is_expected.to eq("https://management.usgovcloudapi.net") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login-us.microsoftonline.com/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.usgovcloudapi.net/")
end
end
end

context "when environment is AzureChina" do
let(:environment) { "AzureChina" }

its([:base_url]) { is_expected.to eq("https://management.chinacloudapi.cn") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login.chinacloudapi.cn/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.chinacloudapi.cn/")
end
end
end

context "when environment is AzureGermanCloud" do
let(:environment) { "AzureGermanCloud" }

its([:base_url]) { is_expected.to eq("https://management.microsoftazure.de") }

context "active_directory_settings" do
it "sets the authentication_endpoint correctly" do
expect(active_directory_settings.authentication_endpoint).to eq("https://login.microsoftonline.de/")
end

it "sets the token_audience correctly" do
expect(active_directory_settings.token_audience).to eq("https://management.core.cloudapi.de/")
end
end
end

shared_examples "common option specs" do
it { is_expected.to be_instance_of(Hash) }
its([:tenant_id]) { is_expected.to eq(tenant_id) }
its([:subscription_id]) { is_expected.to eq(subscription_id) }
its([:credentials]) { is_expected.to be_instance_of(MsRest::TokenCredentials) }
its([:client_id]) { is_expected.to eq(client_id) }
its([:client_secret]) { is_expected.to eq(client_secret) }
its([:base_url]) { is_expected.to eq("https://management.azure.com/") }
end

context "when using client_id and client_secret" do
let(:subscription_id) { ini_credentials.sections[CLIENT_ID_AND_SECRET_SUB] }

include_examples "common option specs"

it "uses token provider: MsRestAzure::ApplicationTokenProvider" do
expect(token_provider).to be_instance_of(MsRestAzure::ApplicationTokenProvider)
end

it "sets the client_id" do
expect(token_provider.instance_variables).to include(:@client_id)
expect(token_provider.send(:client_id)).to eq(client_id)
end

it "sets the client_secret" do
expect(token_provider.instance_variables).to include(:@client_secret)
expect(token_provider.send(:client_secret)).to eq(client_secret)
end
end

context "when using client_id, without client_secret" do
let(:subscription_id) { ini_credentials.sections[CLIENT_ID_SUB] }

include_examples "common option specs"

it "uses token provider: MsRestAzure::MSITokenProvider" do
expect(token_provider).to be_instance_of(MsRestAzure::MSITokenProvider)
end

it "sets the client_id" do
expect(token_provider.instance_variables).to include(:@client_id)
expect(token_provider.send(:client_id)).to eq(client_id)
end

it "does not set client_secret" do
expect(token_provider.instance_variables).not_to include(:@client_secret)
end
end

context "when not using client_id or client_secret" do
let(:subscription_id) { ini_credentials.sections[NO_CLIENT_SUB] }

include_examples "common option specs"

it "uses token provider: MsRestAzure::MSITokenProvider" do
expect(token_provider).to be_instance_of(MsRestAzure::MSITokenProvider)
end

it "does not set the client_id" do
expect(token_provider.instance_variables).not_to include(:@client_id)
end

it "does not set client_secret" do
expect(token_provider.instance_variables).not_to include(:@client_secret)
end
end
end
end

0 comments on commit 17a2170

Please sign in to comment.