From d7c6edec183960c2c7d202384dfad82db4afefa8 Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Tue, 29 Jul 2014 03:02:54 +0200 Subject: [PATCH] Bring back ssh key endpoint, but make it configurable --- lib/travis/api/app.rb | 3 + lib/travis/api/v2/http.rb | 1 + lib/travis/api/v2/http/ssh_key.rb | 27 ++++ spec/integration/v2/settings/ssh_key_spec.rb | 122 +++++++++++++++++++ spec/spec_helper.rb | 30 +++++ 5 files changed, 183 insertions(+) create mode 100644 lib/travis/api/v2/http/ssh_key.rb create mode 100644 spec/integration/v2/settings/ssh_key_spec.rb diff --git a/lib/travis/api/app.rb b/lib/travis/api/app.rb index 38982f7d..15668ec9 100644 --- a/lib/travis/api/app.rb +++ b/lib/travis/api/app.rb @@ -115,6 +115,9 @@ module Travis::Api use Travis::Api::App::Middleware::Rewrite SettingsEndpoint.subclass :env_vars + if Travis.config.endpoints.ssh_key + SingletonSettingsEndpoint.subclass :ssh_key + end Endpoint.subclasses.each do |e| next if e == SettingsEndpoint # TODO: add something like abstract? method to check if diff --git a/lib/travis/api/v2/http.rb b/lib/travis/api/v2/http.rb index 306057c0..a733b364 100644 --- a/lib/travis/api/v2/http.rb +++ b/lib/travis/api/v2/http.rb @@ -26,6 +26,7 @@ module Travis require 'travis/api/v2/http/env_vars' require 'travis/api/v2/http/user' require 'travis/api/v2/http/validation_error' + require 'travis/api/v2/http/ssh_key' end end end diff --git a/lib/travis/api/v2/http/ssh_key.rb b/lib/travis/api/v2/http/ssh_key.rb new file mode 100644 index 00000000..25908dd6 --- /dev/null +++ b/lib/travis/api/v2/http/ssh_key.rb @@ -0,0 +1,27 @@ +require 'openssl' + +module Travis + module Api + module V2 + module Http + class SshKey < Travis::Api::Serializer + attributes :id, :description, :fingerprint + + def id + object.repository_id + end + + def fingerprint + value = object.value.decrypt + return unless value + key = OpenSSL::PKey::RSA.new(value) + ssh_rsa = "\x00\x00\x00\x07ssh-rsa" + key.e.to_s(0) + key.n.to_s(0) + OpenSSL::Digest::MD5.new(ssh_rsa).hexdigest.scan(/../).join(':') + rescue OpenSSL::PKey::RSAError + nil + end + end + end + end + end +end diff --git a/spec/integration/v2/settings/ssh_key_spec.rb b/spec/integration/v2/settings/ssh_key_spec.rb new file mode 100644 index 00000000..9f7ae8d1 --- /dev/null +++ b/spec/integration/v2/settings/ssh_key_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +describe 'ssh keys endpoint' do + let(:repo) { Factory(:repository) } + let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } + + describe 'without an authenticated user' do + let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json' } } + let(:user) { Factory(:user) } + + describe 'GET /ssh_key' do + it 'responds with 401' do + response = get "/settings/ssh_key/#{repo.id}", {}, headers + response.should_not be_successful + response.status.should == 401 + end + end + + describe 'PATCH /ssh_key' do + it 'responds with 401' do + response = patch "/settings/ssh_key/#{repo.id}", {}, headers + response.should_not be_successful + response.status.should == 401 + end + end + + describe 'DELETE /ssh_key' do + it 'responds with 401' do + response = delete "/settings/ssh_key/#{repo.id}", {}, headers + response.should_not be_successful + response.status.should == 401 + end + end + end + + describe 'with authenticated user' do + let(:user) { Factory(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: -1) } + let(:headers) { { 'HTTP_ACCEPT' => 'application/vnd.travis-ci.2+json', 'HTTP_AUTHORIZATION' => "token #{token}" } } + + before { user.permissions.create!(:repository_id => repo.id, :admin => true, :push => true) } + + describe 'GET /ssh_key' do + it 'returns an item' do + settings = repo.settings + record = settings.create(:ssh_key, description: 'key for my repo', value: TEST_PRIVATE_KEY) + settings.save + + response = get "/settings/ssh_key/#{repo.id}", {}, headers + json = JSON.parse(response.body) + json['ssh_key']['description'].should == 'key for my repo' + json['ssh_key']['id'].should == repo.id + json['ssh_key'].should_not have_key('value') + json['ssh_key']['fingerprint'].should == '57:78:65:c2:c9:c8:c9:f7:dd:2b:35:39:40:27:d2:40' + end + + it 'returns 404 if ssh_key can\'t be found' do + response = get "/settings/ssh_key/#{repo.id}", {}, headers + json = JSON.parse(response.body) + json['error'].should == "Could not find a requested setting" + end + end + + describe 'PATCH /settings/ssh_key' do + it 'should update a key' do + settings = repo.settings + ssh_key = settings.create(:ssh_key, description: 'foo', value: TEST_PRIVATE_KEY) + settings.save + + new_key = OpenSSL::PKey::RSA.generate(2048).to_s + body = { ssh_key: { description: 'bar', value: new_key } }.to_json + response = patch "/settings/ssh_key/#{repo.id}", body, headers + json = JSON.parse(response.body) + json['ssh_key']['description'].should == 'bar' + json['ssh_key'].should_not have_key('value') + + updated_ssh_key = repo.reload.settings.ssh_key + updated_ssh_key.description.should == 'bar' + updated_ssh_key.repository_id.should == repo.id + updated_ssh_key.value.decrypt.should == new_key + end + + it 'returns an error message if ssh_key is invalid' do + settings = repo.settings + ssh_key = settings.create(:ssh_key, description: 'foo', value: 'the key') + settings.save + + body = { ssh_key: { value: nil } }.to_json + response = patch "/settings/ssh_key/#{repo.id}", body, headers + + response.status.should == 422 + + json = JSON.parse(response.body) + json['message'].should == 'Validation failed' + json['errors'].should == [{ + 'field' => 'value', + 'code' => 'missing_field' + }] + + ssh_key = repo.reload.settings.ssh_key + ssh_key.description.should == 'foo' + ssh_key.repository_id.should == repo.id + ssh_key.value.decrypt.should == 'the key' + end + end + + describe 'DELETE /ssh_keys/:id' do + it 'should nullify an ssh_key' do + settings = repo.settings + ssh_key = settings.create(:ssh_key, description: 'foo', value: 'the key') + settings.save + + response = delete "/settings/ssh_key/#{repo.id}", {}, headers + json = JSON.parse(response.body) + json['ssh_key']['description'].should == 'foo' + json['ssh_key'].should_not have_key('value') + + repo.reload.settings.ssh_key.should be_nil + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c98039c2..508b6333 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,6 +18,7 @@ require 'support/formats' Travis.logger = Logger.new(StringIO.new) Travis::Api::App.setup Travis.config.client_domain = "www.example.com" +Travis.config.endpoints.ssh_key = true module TestHelpers include Sinatra::TestHelpers @@ -73,3 +74,32 @@ RSpec.configure do |c| end end end + +TEST_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6Dm1n+fc0ILeLWeiwqsWs1MZaGAfccrmpvuxfcE9UaJp2POy +079g+mdiBgtWfnQlU84YX31rU2x9GJwnb8G6UcvkEjqczOgHHmELtaNmrRH1g8qO +fJpzXB8XiNib1L3TDs7qYMKLDCbl2bWrcO7Dol9bSqIeb7f9rzkCd4tuXObL3pMD +/VIW5uzeVqLBAc0Er+qw6U7clnMnHHMekXt4JSRfauSCxktR2FzigoQbJc8t4iWO +rmNi5Q84VkXB3X7PO/eajUw+RJOl6FnPN1Zh08ceqcqmSMM4RzeVQaczXg7P92P4 +mRF41R97jIJyzUGwheb2Z4Q2rltck4V7R5BvMwIDAQABAoIBAE4O3+MRH+MiqiXe ++RGwSqAaZab08Hzic+dbIQ0hQEhJbITVXZ3ZbXKd/5ACjZ9R0R47X2vxj3rqM55r +FsJ0/vjxrQcHlp81uvbWLgZvF1tDdyBGnOB7Vh14AgQoszCuYdxPZu8BVZXPGWG1 +tBvw1eelX91VYx+wW+BjLFYckws8kPCPY6WEnng0wQGShGqyTOJa1T4M1ethHYF+ +ddNx+fVLkEf2vL59popuJMOAyVa1jvU7D3VZ67qhlxWAvQxZeEP0vFZHeWPjvRF1 +orxiGuwLCG+Rgq1XSVJjMNf1qE3gZTlDg+u3ORKbRx2xlhiqpkHxLx7QtCmELwtD +Dqvf8ukCgYEA/SoQwyfMp4t19FLI4tV0rp3Yn7ZSAqRtMVrLVAIQoJzDXv9BaJFS +xb6enxRAjy+Rg10H8ijh8Z9Z3a4g3JViHQsWMrf9rL2/7M07vraIUIQoVo7yTeGa +MXnTuKmBZFGEAM9CzqAVao1Om10TRFNLgiLAU3ZEFi8J1DYWkhzrJp0CgYEA6tOa +V15MP3sJSlOTszshXKbwf6iXfjHbdpGUXmd9X3AMzOvl/CEGS2q46lwJISubHWKF +BOKk1thumM4Zu6dx89hLEoXhFycgUV/KJYl54ZfhY079Ri7SZUYIqDR04BRJC2d6 +mO16Y//UwqgTaZ/lS/S791iWPTjVNEgSlRbQHA8CgYALiOEeoy+V6qrDKQpyG1un +oRV/oWT3LdqzxvlAqJ9tUfcs2uB2DTkCPX8orFmMrJQqshBsniQ9SA9mJErnAf9o +Z1rpkKyENFkMRwWT2Ok5EexslTLBDahi3LQi08ZLddNX3hmjJHQVWL7eIU2BbXIh +ScgNhXPwts/x1U0N9zdXmQKBgQC4O6W2cAQQNd5XEvUpQ/XrtAmxjjq0xjbxckve +OQFy0/0m9NiuE9bVaniDXgvHm2eKCVZlO8+pw4oZlnE3+an8brCParvrJ0ZCsY1u +H8qgxEEPYdRxsKBe1jBKj0U23JNmQBw+SOqh9AAfbDA2yTzjd7HU4AqXI7SZ3QW/ +NHO33wKBgQCqxUmocyqKy5NEBPMmeHWapuSY47bdDaE139vRWV6M47oxzxF8QnQV +1TGWsshK04QO8wsfzIa9/SjZkU17QVkz7LXbq4hPmiZjhP/H+roCeoDEyHFdkq6B +bm/edpYemlJlQhEYtecwvD57NZbVuaqX4Culz9WdSsw4I56hD+QjHQ== +-----END RSA PRIVATE KEY----- +"