diff --git a/kitchen-azurerm.gemspec b/kitchen-azurerm.gemspec index 02a41e8..9a3a660 100644 --- a/kitchen-azurerm.gemspec +++ b/kitchen-azurerm.gemspec @@ -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 diff --git a/lib/kitchen/driver/azure_credentials.rb b/lib/kitchen/driver/azure_credentials.rb index 0d38784..4def42c 100644 --- a/lib/kitchen/driver/azure_credentials.rb +++ b/lib/kitchen/driver/azure_credentials.rb @@ -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 @@ -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 # diff --git a/spec/fixtures/azure_credentials b/spec/fixtures/azure_credentials new file mode 100644 index 0000000..7a69d0e --- /dev/null +++ b/spec/fixtures/azure_credentials @@ -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" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b0d915e..b811e25 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1 +1,2 @@ +require "rspec/its" require_relative "../lib/kitchen/driver/azurerm" diff --git a/spec/unit/kitchen/driver/azure_credentials_spec.rb b/spec/unit/kitchen/driver/azure_credentials_spec.rb new file mode 100644 index 0000000..7abeb35 --- /dev/null +++ b/spec/unit/kitchen/driver/azure_credentials_spec.rb @@ -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