diff options
Diffstat (limited to 'spec/unit/rest/auth_credentials_spec.rb')
-rw-r--r-- | spec/unit/rest/auth_credentials_spec.rb | 419 |
1 files changed, 419 insertions, 0 deletions
diff --git a/spec/unit/rest/auth_credentials_spec.rb b/spec/unit/rest/auth_credentials_spec.rb new file mode 100644 index 0000000000..d4e533919f --- /dev/null +++ b/spec/unit/rest/auth_credentials_spec.rb @@ -0,0 +1,419 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' +require 'uri' +require 'net/https' + +KEY_DOT_PEM=<<-END_RSA_KEY +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA49TA0y81ps0zxkOpmf5V4/c4IeR5yVyQFpX3JpxO4TquwnRh +8VSUhrw8kkTLmB3cS39Db+3HadvhoqCEbqPE6915kXSuk/cWIcNozujLK7tkuPEy +YVsyTioQAddSdfe+8EhQVf3oHxaKmUd6waXrWqYCnhxgOjxocenREYNhZ/OETIei +PbOku47vB4nJK/0GhKBytL2XnsRgfKgDxf42BqAi1jglIdeq8lAWZNF9TbNBU21A +O1iuT7Pm6LyQujhggPznR5FJhXKRUARXBJZawxpGV4dGtdcahwXNE4601aXPra+x +PcRd2puCNoEDBzgVuTSsLYeKBDMSfs173W1QYwIDAQABAoIBAGF05q7vqOGbMaSD +2Q7YbuE/JTHKTBZIlBI1QC2x+0P5GDxyEFttNMOVzcs7xmNhkpRw8eX1LrInrpMk +WsIBKAFFEfWYlf0RWtRChJjNl+szE9jQxB5FJnWtJH/FHa78tR6PsF24aQyzVcJP +g0FGujBihwgfV0JSCNOBkz8MliQihjQA2i8PGGmo4R4RVzGfxYKTIq9vvRq/+QEa +Q4lpVLoBqnENpnY/9PTl6JMMjW2b0spbLjOPVwDaIzXJ0dChjNXo15K5SHI5mALJ +I5gN7ODGb8PKUf4619ez194FXq+eob5YJdilTFKensIUvt3YhP1ilGMM+Chi5Vi/ +/RCTw3ECgYEA9jTw4wv9pCswZ9wbzTaBj9yZS3YXspGg26y6Ohq3ZmvHz4jlT6uR +xK+DDcUiK4072gci8S4Np0fIVS7q6ivqcOdzXPrTF5/j+MufS32UrBbUTPiM1yoO +ECcy+1szl/KoLEV09bghPbvC58PFSXV71evkaTETYnA/F6RK12lEepcCgYEA7OSy +bsMrGDVU/MKJtwqyGP9ubA53BorM4Pp9VVVSCrGGVhb9G/XNsjO5wJC8J30QAo4A +s59ZzCpyNRy046AB8jwRQuSwEQbejSdeNgQGXhZ7aIVUtuDeFFdaIz/zjVgxsfj4 +DPOuzieMmJ2MLR4F71ocboxNoDI7xruPSE8dDhUCgYA3vx732cQxgtHwAkeNPJUz +dLiE/JU7CnxIoSB9fYUfPLI+THnXgzp7NV5QJN2qzMzLfigsQcg3oyo6F2h7Yzwv +GkjlualIRRzCPaCw4Btkp7qkPvbs1QngIHALt8fD1N69P3DPHkTwjG4COjKWgnJq +qoHKS6Fe/ZlbigikI6KsuwKBgQCTlSLoyGRHr6oj0hqz01EDK9ciMJzMkZp0Kvn8 +OKxlBxYW+jlzut4MQBdgNYtS2qInxUoAnaz2+hauqhSzntK3k955GznpUatCqx0R +b857vWviwPX2/P6+E3GPdl8IVsKXCvGWOBZWTuNTjQtwbDzsUepWoMgXnlQJSn5I +YSlLxQKBgQD16Gw9kajpKlzsPa6XoQeGmZALT6aKWJQlrKtUQIrsIWM0Z6eFtX12 +2jjHZ0awuCQ4ldqwl8IfRogWMBkHOXjTPVK0YKWWlxMpD/5+bGPARa5fir8O1Zpo +Y6S6MeZ69Rp89ma4ttMZ+kwi1+XyHqC/dlcVRW42Zl5Dc7BALRlJjQ== +-----END RSA PRIVATE KEY----- + END_RSA_KEY + + +describe Chef::REST::AuthCredentials do + before do + @key_file_fixture = CHEF_SPEC_DATA + '/ssl/private_key.pem' + @key = OpenSSL::PKey::RSA.new(IO.read(@key_file_fixture).strip) + @auth_credentials = Chef::REST::AuthCredentials.new("client-name", @key) + end + + it "has a client name" do + @auth_credentials.client_name.should == "client-name" + end + + it "loads the private key when initialized with the path to the key" do + @auth_credentials.key.should respond_to(:private_encrypt) + @auth_credentials.key.to_s.should == KEY_DOT_PEM + end + + describe "when loading the private key" do + it "strips extra whitespace before checking the key" do + key_file_fixture = CHEF_SPEC_DATA + '/ssl/private_key_with_whitespace.pem' + lambda {Chef::REST::AuthCredentials.new("client-name", @key_file_fixture)}.should_not raise_error + end + end + + describe "generating signature headers for a request" do + before do + @request_time = Time.at(1270920860) + @request_params = {:http_method => :POST, :path => "/clients", :body => '{"some":"json"}', :host => "localhost"} + end + + it "generates signature headers for the request" do + Time.stub!(:now).and_return(@request_time) + actual = @auth_credentials.signature_headers(@request_params) + actual["HOST"].should == "localhost" + actual["X-OPS-AUTHORIZATION-1"].should == "kBssX1ENEwKtNYFrHElN9vYGWS7OeowepN9EsYc9csWfh8oUovryPKDxytQ/" + actual["X-OPS-AUTHORIZATION-2"].should == "Wc2/nSSyxdWJjjfHzrE+YrqNQTaArOA7JkAf5p75eTUonCWcvNPjFrZVgKGS" + actual["X-OPS-AUTHORIZATION-3"].should == "yhzHJQh+lcVA9wwARg5Hu9q+ddS8xBOdm3Vp5atl5NGHiP0loiigMYvAvzPO" + actual["X-OPS-AUTHORIZATION-4"].should == "r9853eIxwYMhn5hLGhAGFQznJbE8+7F/lLU5Zmk2t2MlPY8q3o1Q61YD8QiJ" + actual["X-OPS-AUTHORIZATION-5"].should == "M8lIt53ckMyUmSU0DDURoiXLVkE9mag/6Yq2tPNzWq2AdFvBqku9h2w+DY5k" + actual["X-OPS-AUTHORIZATION-6"].should == "qA5Rnzw5rPpp3nrWA9jKkPw4Wq3+4ufO2Xs6w7GCjA==" + actual["X-OPS-CONTENT-HASH"].should == "1tuzs5XKztM1ANrkGNPah6rW9GY=" + actual["X-OPS-SIGN"].should =~ %r{(version=1\.0)|(algorithm=sha1;version=1.0;)} + actual["X-OPS-TIMESTAMP"].should == "2010-04-10T17:34:20Z" + actual["X-OPS-USERID"].should == "client-name" + + end + + describe "when configured for version 1.1 of the authn protocol" do + before do + Chef::Config[:authentication_protocol_version] = "1.1" + end + + after do + Chef::Config[:authentication_protocol_version] = "1.0" + end + + it "generates the correct signature for version 1.1" do + Time.stub!(:now).and_return(@request_time) + actual = @auth_credentials.signature_headers(@request_params) + actual["HOST"].should == "localhost" + actual["X-OPS-CONTENT-HASH"].should == "1tuzs5XKztM1ANrkGNPah6rW9GY=" + actual["X-OPS-SIGN"].should == "algorithm=sha1;version=1.1;" + actual["X-OPS-TIMESTAMP"].should == "2010-04-10T17:34:20Z" + actual["X-OPS-USERID"].should == "client-name" + + # mixlib-authN will test the actual signature stuff for each version of + # the protocol so we won't test it again here. + end + end + end +end + +describe Chef::REST::RESTRequest do + def new_request(method=nil) + method ||= :POST + Chef::REST::RESTRequest.new(method, @url, @req_body, @headers) + end + + before do + @auth_credentials = Chef::REST::AuthCredentials.new("client-name", CHEF_SPEC_DATA + '/ssl/private_key.pem') + @url = URI.parse("http://chef.example.com:4000/?q=chef_is_awesome") + @req_body = '{"json_data":"as_a_string"}' + @headers = {"Content-type" =>"application/json", "Accept"=>"application/json", "Accept-Encoding" => Chef::REST::RESTRequest::ENCODING_GZIP_DEFLATE} + @request = Chef::REST::RESTRequest.new(:POST, @url, @req_body, @headers) + end + + it "stores the url it was created with" do + @request.url.should == @url + end + + it "stores the HTTP method" do + @request.method.should == :POST + end + + it "adds the chef version header" do + @request.headers.should == @headers.merge("X-Chef-Version" => ::Chef::VERSION) + end + + describe "configuring the HTTP request" do + it "configures GET requests" do + @req_body = nil + rest_req = new_request(:GET) + rest_req.http_request.should be_a_kind_of(Net::HTTP::Get) + rest_req.http_request.path.should == "/?q=chef_is_awesome" + rest_req.http_request.body.should be_nil + end + + it "configures POST requests, including the body" do + @request.http_request.should be_a_kind_of(Net::HTTP::Post) + @request.http_request.path.should == "/?q=chef_is_awesome" + @request.http_request.body.should == @req_body + end + + it "configures PUT requests, including the body" do + rest_req = new_request(:PUT) + rest_req.http_request.should be_a_kind_of(Net::HTTP::Put) + rest_req.http_request.path.should == "/?q=chef_is_awesome" + rest_req.http_request.body.should == @req_body + end + + it "configures DELETE requests" do + rest_req = new_request(:DELETE) + rest_req.http_request.should be_a_kind_of(Net::HTTP::Delete) + rest_req.http_request.path.should == "/?q=chef_is_awesome" + rest_req.http_request.body.should be_nil + end + + it "configures HTTP basic auth" do + @url = URI.parse("http://homie:theclown@chef.example.com:4000/?q=chef_is_awesome") + rest_req = new_request(:GET) + rest_req.http_request.to_hash["authorization"].should == ["Basic aG9taWU6dGhlY2xvd24="] + end + end + + describe "configuring the HTTP client" do + it "configures the HTTP client for the host and port" do + http_client = new_request.http_client + http_client.address.should == "chef.example.com" + http_client.port.should == 4000 + end + + it "configures the HTTP client with the read timeout set in the config file" do + Chef::Config[:rest_timeout] = 9001 + new_request.http_client.read_timeout.should == 9001 + end + + describe "for SSL" do + before do + Chef::Config[:ssl_client_cert] = nil + Chef::Config[:ssl_client_key] = nil + Chef::Config[:ssl_ca_path] = nil + Chef::Config[:ssl_ca_file] = nil + end + + after do + Chef::Config[:ssl_client_cert] = nil + Chef::Config[:ssl_client_key] = nil + Chef::Config[:ssl_ca_path] = nil + Chef::Config[:ssl_verify_mode] = :verify_none + Chef::Config[:ssl_ca_file] = nil + end + + describe "when configured with :ssl_verify_mode set to :verify peer" do + before do + @url = URI.parse("https://chef.example.com:4443/") + Chef::Config[:ssl_verify_mode] = :verify_peer + @request = new_request + end + + it "configures the HTTP client to use SSL when given a URL with the https protocol" do + @request.http_client.use_ssl?.should be_true + end + + it "sets the OpenSSL verify mode to verify_peer" do + @request.http_client.verify_mode.should == OpenSSL::SSL::VERIFY_PEER + end + + it "raises a ConfigurationError if :ssl_ca_path is set to a path that doesn't exist" do + Chef::Config[:ssl_ca_path] = "/dev/null/nothing_here" + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "should set the CA path if that is set in the configuration" do + Chef::Config[:ssl_ca_path] = File.join(CHEF_SPEC_DATA, "ssl") + new_request.http_client.ca_path.should == File.join(CHEF_SPEC_DATA, "ssl") + end + + it "raises a ConfigurationError if :ssl_ca_file is set to a file that does not exist" do + Chef::Config[:ssl_ca_file] = "/dev/null/nothing_here" + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "should set the CA file if that is set in the configuration" do + Chef::Config[:ssl_ca_file] = CHEF_SPEC_DATA + '/ssl/5e707473.0' + new_request.http_client.ca_file.should == CHEF_SPEC_DATA + '/ssl/5e707473.0' + end + end + + describe "when configured with :ssl_verify_mode set to :verify peer" do + before do + @url = URI.parse("https://chef.example.com:4443/") + Chef::Config[:ssl_verify_mode] = :verify_none + end + + it "sets the OpenSSL verify mode to :verify_none" do + new_request.http_client.verify_mode.should == OpenSSL::SSL::VERIFY_NONE + end + end + + describe "when configured with a client certificate" do + before {@url = URI.parse("https://chef.example.com:4443/")} + + it "raises ConfigurationError if the certificate file doesn't exist" do + Chef::Config[:ssl_client_cert] = "/dev/null/nothing_here" + Chef::Config[:ssl_client_key] = CHEF_SPEC_DATA + '/ssl/chef-rspec.key' + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "raises ConfigurationError if the certificate file doesn't exist" do + Chef::Config[:ssl_client_cert] = CHEF_SPEC_DATA + '/ssl/chef-rspec.cert' + Chef::Config[:ssl_client_key] = "/dev/null/nothing_here" + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "raises a ConfigurationError if one of :ssl_client_cert and :ssl_client_key is set but not both" do + Chef::Config[:ssl_client_cert] = "/dev/null/nothing_here" + Chef::Config[:ssl_client_key] = nil + lambda {new_request}.should raise_error(Chef::Exceptions::ConfigurationError) + end + + it "configures the HTTP client's cert and private key" do + Chef::Config[:ssl_client_cert] = CHEF_SPEC_DATA + '/ssl/chef-rspec.cert' + Chef::Config[:ssl_client_key] = CHEF_SPEC_DATA + '/ssl/chef-rspec.key' + http_client = new_request.http_client + http_client.cert.to_s.should == OpenSSL::X509::Certificate.new(IO.read(CHEF_SPEC_DATA + '/ssl/chef-rspec.cert')).to_s + http_client.key.to_s.should == IO.read(CHEF_SPEC_DATA + '/ssl/chef-rspec.key') + end + end + end + + describe "for proxy" do + before do + Chef::Config[:http_proxy] = "http://proxy.example.com:3128" + Chef::Config[:https_proxy] = "http://sproxy.example.com:3129" + Chef::Config[:http_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:https_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:no_proxy] = nil + end + + after do + Chef::Config[:http_proxy] = nil + Chef::Config[:https_proxy] = nil + Chef::Config[:http_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:https_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + Chef::Config[:no_proxy] = nil + end + + describe "with :no_proxy nil" do + it "configures the proxy address and port when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_address.should == "proxy.example.com" + http_client.proxy_port.should == 3128 + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + + it "configures the proxy address and port when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_address.should == "sproxy.example.com" + http_client.proxy_port.should == 3129 + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + end + + describe "with :no_proxy set" do + before do + Chef::Config[:no_proxy] = "10.*,*.example.com" + end + + it "does not configure the proxy address and port when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == false + http_client.proxy_address.should be_nil + http_client.proxy_port.should be_nil + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + + it "does not configure the proxy address and port when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == false + http_client.proxy_address.should be_nil + http_client.proxy_port.should be_nil + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + end + + describe "with :http_proxy_user and :http_proxy_pass set" do + before do + Chef::Config[:http_proxy_user] = "homie" + Chef::Config[:http_proxy_pass] = "theclown" + end + + after do + Chef::Config[:http_proxy_user] = nil + Chef::Config[:http_proxy_pass] = nil + end + + it "configures the proxy user and pass when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should == "homie" + http_client.proxy_pass.should == "theclown" + end + + it "does not configure the proxy user and pass when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + end + + describe "with :https_proxy_user and :https_proxy_pass set" do + before do + Chef::Config[:https_proxy_user] = "homie" + Chef::Config[:https_proxy_pass] = "theclown" + end + + after do + Chef::Config[:https_proxy_user] = nil + Chef::Config[:https_proxy_pass] = nil + end + + it "does not configure the proxy user and pass when using http scheme" do + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should be_nil + http_client.proxy_pass.should be_nil + end + + it "configures the proxy user and pass when using https scheme" do + @url.scheme = "https" + http_client = new_request.http_client + http_client.proxy?.should == true + http_client.proxy_user.should == "homie" + http_client.proxy_pass.should == "theclown" + end + end + end + end + +end |