diff --git a/Gemfile b/Gemfile index 85cf95ea..d2be77a3 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'marginalia', git: 'https://github.com/travis-ci/marginalia' gem 'cl' gem 'sidekiq-pro', require: 'sidekiq-pro', source: 'https://gems.contribsys.com' gem 'redis-namespace' -gem 'activerecord', '~> 4.2.7' +gem 'activerecord', '~> 6.1.7' gem 'bunny', '~> 2.9.2' gem 'pg' gem 'concurrent-ruby' diff --git a/Gemfile.lock b/Gemfile.lock index 99e4b61e..9b230416 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,26 +75,29 @@ GIT virtus GEM - remote: https://rubygems.org/ remote: https://gems.contribsys.com/ + specs: + sidekiq-pro (3.4.0) + sidekiq (>= 4.1.5) + +GEM + remote: https://rubygems.org/ specs: HDRHistogram (0.1.3) - activemodel (4.2.10) - activesupport (= 4.2.10) - builder (~> 3.1) - activerecord (4.2.10) - activemodel (= 4.2.10) - activesupport (= 4.2.10) - arel (~> 6.0) - activesupport (4.2.10) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) + activemodel (6.1.7.1) + activesupport (= 6.1.7.1) + activerecord (6.1.7.1) + activemodel (= 6.1.7.1) + activesupport (= 6.1.7.1) + activesupport (6.1.7.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) amq-protocol (2.3.0) - arel (6.0.4) atomic (1.1.99) avl_tree (1.2.1) atomic (~> 1.1) @@ -103,7 +106,6 @@ GEM ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) backports (3.10.3) - builder (3.2.3) bunny (2.9.2) amq-protocol (~> 2.3.0) cl (0.0.4) @@ -139,14 +141,14 @@ GEM domain_name (~> 0.5) http-form_data (1.0.3) http_parser.rb (0.6.0) - i18n (0.9.3) + i18n (1.12.0) concurrent-ruby (~> 1.0) ice_nine (0.11.2) libhoney (1.3.2) http (~> 2.0) metaclass (0.0.4) method_source (0.9.0) - minitest (5.11.3) + minitest (5.17.0) mocha (0.10.5) metaclass (~> 0.0.1) multi_json (1.12.1) @@ -189,14 +191,12 @@ GEM connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) - sidekiq-pro (3.4.0) - sidekiq (>= 4.1.5) thread_safe (0.3.6) travis-config (1.1.3) hashr (~> 2.0) travis-lock (0.1.1) - tzinfo (1.2.5) - thread_safe (~> 0.1) + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext unf_ext (0.0.7.4) @@ -209,12 +209,13 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff + zeitwerk (2.6.6) PLATFORMS ruby DEPENDENCIES - activerecord (~> 4.2.7) + activerecord (~> 6.1.7) bunny (~> 2.9.2) cl coder! diff --git a/lib/travis/scheduler/record.rb b/lib/travis/scheduler/record.rb index 32c95524..9c5e3e0c 100644 --- a/lib/travis/scheduler/record.rb +++ b/lib/travis/scheduler/record.rb @@ -3,9 +3,11 @@ require 'travis/scheduler/record/branch' require 'travis/scheduler/record/build' require 'travis/scheduler/record/commit' +require 'travis/scheduler/record/custom_key' require 'travis/scheduler/record/installation' require 'travis/scheduler/record/job' require 'travis/scheduler/record/log' +require 'travis/scheduler/record/membership' require 'travis/scheduler/record/organization' require 'travis/scheduler/record/permission' require 'travis/scheduler/record/pull_request' diff --git a/lib/travis/scheduler/record/custom_key.rb b/lib/travis/scheduler/record/custom_key.rb new file mode 100644 index 00000000..a4c7ae64 --- /dev/null +++ b/lib/travis/scheduler/record/custom_key.rb @@ -0,0 +1,7 @@ +require 'travis/support/encrypted_column' +require 'travis/support/secure_config' + +class CustomKey < ActiveRecord::Base + serialize :private_key, Travis::EncryptedColumn.new + belongs_to :owner, polymorphic: true +end diff --git a/lib/travis/scheduler/record/membership.rb b/lib/travis/scheduler/record/membership.rb new file mode 100644 index 00000000..1812894c --- /dev/null +++ b/lib/travis/scheduler/record/membership.rb @@ -0,0 +1,4 @@ +class Membership < ActiveRecord::Base + belongs_to :user + belongs_to :organization +end diff --git a/lib/travis/scheduler/serialize/worker.rb b/lib/travis/scheduler/serialize/worker.rb index 9f87de05..6fbc89bb 100644 --- a/lib/travis/scheduler/serialize/worker.rb +++ b/lib/travis/scheduler/serialize/worker.rb @@ -19,7 +19,7 @@ def data vm_size: job.vm_size, queue: job.queue, config: job.decrypted_config, - env_vars: job.env_vars, + env_vars: env_vars_with_custom_keys, job: job_data, host: Travis::Scheduler.config.host, source: build_data, @@ -238,6 +238,39 @@ def allowed_repositories def travis_vcs_proxy? repo.vcs_type == 'TravisproxyRepository' end + + def env_vars_with_custom_keys + job.env_vars + custom_keys + end + + def custom_keys + return [] if job.decrypted_config[:keys].blank? + + if job.source.event_type == 'pull_request' && job.source.request.pull_request.head_repo_slug != job.source.request.pull_request.base_repo_slug + base_repo_owner_name, base_repo_name = job.source.request.pull_request.base_repo_slug.to_s.split('/') + return [] unless base_repo_owner_name && base_repo_name + + + base_repo = ::Repository.find_by(owner_name: base_repo_owner_name, name: base_repo_name) + return [] unless base_repo && base_repo.settings.share_ssh_keys_with_forks? + + end + + job.decrypted_config[:keys].map do |key| + custom_key = CustomKey.where(name: key, owner_id: build.sender_id, owner_type: 'User').first + if custom_key.nil? + org_ids = Membership.where(user_id: build.sender_id).map(&:organization_id) + if !base_repo.nil? && base_repo.owner_type == 'Organization' + org_ids.reject! { |id| id != base_repo.owner_id } + elsif repo.owner_type == 'Organization' + org_ids.reject! { |id| id != repo.owner_id } + end + + custom_key = CustomKey.where(name: key, owner_id: org_ids, owner_type: 'Organization').first unless org_ids.empty? + end + custom_key.nil? ? nil : { name: "TRAVIS_#{key}", value: Base64.strict_encode64(custom_key.private_key), public: false, branch: nil } + end.compact + end end end end diff --git a/lib/travis/scheduler/serialize/worker/config/decrypt.rb b/lib/travis/scheduler/serialize/worker/config/decrypt.rb index 160bc039..0a9439b6 100644 --- a/lib/travis/scheduler/serialize/worker/config/decrypt.rb +++ b/lib/travis/scheduler/serialize/worker/config/decrypt.rb @@ -9,12 +9,18 @@ def apply config[key] = process_env(config[key]) if config[key] end + force_vault_to_be_secure!(config) + config[:vault] = decryptor.decrypt(config[:vault]) if config[:vault] config[:addons] = decryptor.decrypt(config[:addons]) if config[:addons] config end private + def force_vault_to_be_secure!(config) + config[:vault].delete(:token) if config.dig(:vault, :token).is_a?(String) + end + def secure_env? !!options[:secure_env] end diff --git a/lib/travis/scheduler/serialize/worker/job.rb b/lib/travis/scheduler/serialize/worker/job.rb index 45200c49..452528d4 100644 --- a/lib/travis/scheduler/serialize/worker/job.rb +++ b/lib/travis/scheduler/serialize/worker/job.rb @@ -19,7 +19,7 @@ def env_vars end def secure_env? - defined?(@secure_env) ? @secure_env : @secure_env = !pull_request? || secure_env_allowed_in_pull_request? + defined?(@secure_env) ? @secure_env : (@secure_env = (!pull_request? || secure_env_allowed_in_pull_request?)) end def pull_request? diff --git a/lib/travis/scheduler/serialize/worker/repo.rb b/lib/travis/scheduler/serialize/worker/repo.rb index 6e1fd6e2..552a2c1a 100644 --- a/lib/travis/scheduler/serialize/worker/repo.rb +++ b/lib/travis/scheduler/serialize/worker/repo.rb @@ -56,6 +56,14 @@ def travis_vcs_proxy? vcs_type == 'TravisproxyRepository' end + def owner_type + repo.owner_type + end + + def owner_id + repo.owner_id + end + private # If the repo does not have a custom timeout, look to the repo's diff --git a/spec/support/factories.rb b/spec/support/factories.rb index ff636b6a..c02eb35e 100644 --- a/spec/support/factories.rb +++ b/spec/support/factories.rb @@ -87,5 +87,14 @@ def public=(value) author_name 'Sven Fuchs' author_email 'me@svenfuchs.com' end + + factory :membership do + association :organization + association :user + end + + factory :custom_key, :class => 'CustomKey' do + name 'key' + end end diff --git a/spec/travis/scheduler/serialize/worker/config_spec.rb b/spec/travis/scheduler/serialize/worker/config_spec.rb index aec1cfb0..fe2fd553 100644 --- a/spec/travis/scheduler/serialize/worker/config_spec.rb +++ b/spec/travis/scheduler/serialize/worker/config_spec.rb @@ -71,6 +71,16 @@ def encrypt(string) let(:env) { [{ FOO: 'foo', BAR: 'bar' }, encrypt('BAZ=baz')] } it { should eql env: ['FOO=foo', 'BAR=bar', 'SECURE BAZ=baz'], global_env: ['FOO=foo', 'BAR=bar', 'SECURE BAZ=baz'] } end + + describe 'decrypts vault secure token' do + let(:config) { { vault: { token: { secure: encrypt('my_key') } } } } + it { should eql vault: {token: 'my_key'} } + end + + describe 'clears vault unsecure token' do + let(:config) { { vault: { token: 'my_key' } } } + it { should eql vault: {} } + end end describe 'with secure env disabled' do diff --git a/spec/travis/scheduler/serialize/worker_spec.rb b/spec/travis/scheduler/serialize/worker_spec.rb index 2cc084a4..4371fbf5 100644 --- a/spec/travis/scheduler/serialize/worker_spec.rb +++ b/spec/travis/scheduler/serialize/worker_spec.rb @@ -449,4 +449,45 @@ def encrypted(value) it { expect(data[:keep_netrc]).to be false } end end + + context 'custom_keys' do + let!(:organization1) {FactoryGirl.create(:org, login: "org1", id: 1)} + let!(:organization2) {FactoryGirl.create(:org, login: "org2", id: 2)} + let!(:repo) { FactoryGirl.create(:repo, default_branch: 'main') } + let!(:membership1) {FactoryGirl.create(:membership, user: repo.owner, organization: organization1) } + let!(:membership2) {FactoryGirl.create(:membership, user: repo.owner, organization: organization2) } + let!(:custom_key1) {FactoryGirl.create(:custom_key, name: 'key1', owner_id: organization1.id, owner_type: 'Organization', private_key: 'abc')} + let!(:custom_key2) {FactoryGirl.create(:custom_key, name: 'key1', owner_id: organization2.id, owner_type: 'Organization', private_key: 'def')} + + describe 'when two organization have the same key name' do + before { + build.update(sender_id: repo.owner.id) + job.update(config: {:keys => ['key1']}) + repo.update_attributes(owner: organization2, owner_name: 'org2') + } + + it { expect(data[:env_vars]).to include({:name=>"TRAVIS_key1", :value=>"ZGVm", :public=>false, :branch=>nil})} + end + + describe 'when user has no access to organization' do + let!(:organization3) {FactoryGirl.create(:org, login: "org3", id: 3)} + let!(:custom_key3) {FactoryGirl.create(:custom_key, name: 'key1', owner_id: organization3.id, owner_type: 'Organization', private_key: 'ghi')} + let(:raw_settings) do + { + env_vars: nil, + timeout_hard_limit: 180, + timeout_log_silence: 20, + share_ssh_keys_with_forks: false + } + end + + before { + build.update(sender_id: repo.owner.id) + job.update(config: {:keys => ['key1']}) + repo.update_attributes(owner: organization3, owner_name: 'org3') + } + + it { expect(data[:env_vars]).to eq([])} + end + end end