require 'spec_helper' require 'fakefs/spec_helpers' describe Conjur::API do let(:account) { 'api-spec-acount' } let(:remote_ip) { nil } before { allow(Conjur.configuration).to receive_messages account: account } shared_context "logged in", logged_in: true do let(:login) { "bob" } let(:token) { { 'data' => login, 'timestamp' => Time.now.to_s } } subject(:api) { Conjur::API.new_from_token(token, remote_ip: remote_ip) } end shared_context "logged in with an API key", logged_in: :api_key do include_context "logged in" let(:api_key) { "theapikey" } subject(:api) { Conjur::API.new_from_key(login, api_key, account: account ,remote_ip: remote_ip) } end shared_context "logged in with a token file", logged_in: :token_file do include FakeFS::SpecHelpers include_context "logged in" let(:token_file) { "token_file" } subject(:api) { Conjur::API.new_from_token_file(token_file, remote_ip: remote_ip) } end def time_travel delta allow(api.authenticator).to receive(:gettime).and_wrap_original do |m| m[] + delta end allow(api.authenticator).to receive(:monotonic_time).and_wrap_original do |m| m[] + delta end allow(Time).to receive(:now).and_wrap_original do |m| m[] + delta end end describe '#token' do context 'with token file available', logged_in: :token_file do def write_token token File.write token_file, JSON.generate(token) end before do write_token token end it "reads the file to get a token" do expect(api.instance_variable_get("@token")).to eq(nil) expect(api.token).to eq(token) expect(api.credentials).to eq({ headers: { authorization: "Token token=\"#{Base64.strict_encode64(token.to_json)}\"" }, username: login }) end context "after expiration" do it 'it reads a new token' do expect(Time.parse(api.token['timestamp'])).to be_within(5.seconds).of(Time.now) time_travel 6.minutes new_token = token.merge "timestamp" => Time.now.to_s write_token new_token expect(api.token).to eq(new_token) end end end context 'with API key available', logged_in: :api_key do it "authenticates to get a token" do expect(Conjur::API).to receive(:authenticate).with(login, api_key, account: account).and_return token expect(api.instance_variable_get("@token")).to eq(nil) expect(api.token).to eq(token) expect(api.credentials).to eq({ headers: { authorization: "Token token=\"#{Base64.strict_encode64(token.to_json)}\"" }, username: login }) end context "after expiration" do shared_examples "it gets a new token" do it 'by refreshing' do allow(Conjur::API).to receive(:authenticate).with(login, api_key, account: account).and_return token expect(Time.parse(api.token['timestamp'])).to be_within(5.seconds).of(Time.now) time_travel 6.minutes new_token = token.merge "timestamp" => Time.now.to_s expect(Conjur::API).to receive(:authenticate).with(login, api_key, account: account).and_return new_token expect(api.token).to eq(new_token) end end it_should_behave_like "it gets a new token" end end context 'with no API key available', logged_in: true do it "returns the token used to create it" do expect(api.token).to eq token end it "doesn't try to refresh an old token" do expect(Conjur::API).not_to receive :authenticate api.token # vivify time_travel 6.minutes expect { api.token }.not_to raise_error end end end context "credential handling", logged_in: true do context "from token" do describe '#credentials' do subject { super().credentials } it { is_expected.to eq({ headers: { authorization: "Token token=\"#{Base64.strict_encode64(token.to_json)}\"" }, username: login }) } end context "with remote_ip" do let(:remote_ip) { "66.0.0.1" } describe '#credentials' do subject { super().credentials } it { is_expected.to eq({ headers: { authorization: "Token token=\"#{Base64.strict_encode64(token.to_json)}\"", :x_forwarded_for=>"66.0.0.1" }, username: login }) } end end end context "from logged-in RestClient::Resource" do let (:authz_header) { %Q{Token token="#{token_encoded}"} } let (:priv_header) { nil } let (:forwarded_for_header) { nil } let (:audit_roles_header) { nil } let (:audit_resources_header) { nil } let (:username) { 'bob' } subject { resource.conjur_api } shared_examples "it can clone itself" do it "has the authz header" do expect(subject.credentials[:headers][:authorization]).to eq(authz_header) end it "has the username" do expect(subject.credentials[:username]).to eq(username) end end let(:token_encoded) { Base64.strict_encode64(token.to_json) } let(:base_headers) { { authorization: authz_header } } let(:headers) { base_headers } # deepcode ignore InsecureTransmission: This is test code let(:resource) { RestClient::Resource.new("http://example.com", { headers: headers })} context 'basic functioning' do it_behaves_like 'it can clone itself' end context "forwarded for" do let(:forwarded_for_header) { "66.0.0.1" } let(:headers) { base_headers.merge(x_forwarded_for: forwarded_for_header) } it_behaves_like 'it can clone itself' end end end describe "#role_from_username", logged_in: true do it "returns a user role when username is plain" do expect(Conjur::API.role_from_username(api, "plain-username", account).id).to eq("#{account}:user:plain-username") end it "returns an appropriate role kind when username is qualified" do expect(Conjur::API.role_from_username(api, "host/foo/bar", account).id).to eq("#{account}:host:foo/bar") end end describe "#username" do let(:jwt_payload) do 'eyJzdWIiOiJ1c2VyLTlhYjBiYmZiOWJlNjA5Yzk2ZjUyN2Y1YiIsImlhdCI6MTYwMzQ5MDA4MH0=' end let(:jwt_header) do 'eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJraWQiOiI2MWZjOGRiZDM4MjA4NDll' \ 'ZDI4YTZhYTAwMzFjNjM5MjkxZjJmMDQzNDVjYTU0MWI5NzUxMGQ5NjkyM2I3NDlmIn0=' end let(:conjur_token) do { 'data' => 'conjur-user-1234', 'timestamp' => Time.now.to_s } end let(:jwt_token) do { 'protected' => jwt_header, 'payload' => jwt_payload, } end it "can correctly extract the username from old Conjur token" do expect(Conjur::API.new_from_token(conjur_token).username).to( eq('conjur-user-1234') ) end context 'when using JWT token' do it "can correctly extract username" do expect(Conjur::API.new_from_token(jwt_token).username).to( eq('user-9ab0bbfb9be609c96f527f5b') ) end it "returns nil when JWT token has no payload field" do no_payload_jwt_token = { 'protected' => jwt_header } expect(Conjur::API.new_from_token(no_payload_jwt_token).username).to be_nil end it "returns nil when JWT token has no 'sub' field in payload" do no_sub_token = { 'payload' => 'eyJpYXQiOjE2MDM0OTAwODB9' } expect(Conjur::API.new_from_token(no_sub_token).username).to be_nil end end end describe "#current_role", logged_in: true do context "when logged in as user" do let(:login) { 'joerandom' } it "returns a user role" do expect(api.current_role(account).id).to eq("#{account}:user:joerandom") end end context "when logged in as host" do let(:host) { "somehost" } let(:login) { "host/#{host}" } it "returns a host role" do expect(api.current_role(account).id).to eq("#{account}:host:somehost") end end end describe 'url escapes' do let(:urls){[ 'foo/bar@baz', '/test/some group with spaces' ]} describe '#fully_escape' do let(:expected){[ 'foo%2Fbar%40baz', '%2Ftest%2Fsome%20group%20with%20spaces' ]} it 'escapes the urls correctly' do expect(urls.map{|u| Conjur::API.fully_escape u}).to eq(expected) end end end end