diff --git a/engines/instance_verification/config/environments/test.rb b/engines/instance_verification/config/environments/test.rb index 13d5f20ae..f16ce3b7d 100644 --- a/engines/instance_verification/config/environments/test.rb +++ b/engines/instance_verification/config/environments/test.rb @@ -5,6 +5,12 @@ Rails.application.configure do config.cache_config_file = Rails.root.join('engines/registry/spec/data/rmt-cache-trim.sh') - config.repo_cache_dir = 'repo/cache' + config.repo_payg_cache_dir = 'repo/payg/cache' + config.repo_byos_cache_dir = 'repo/byos/cache' + config.repo_hybrid_cache_dir = 'repo/hybrid/cache' config.registry_cache_dir = 'registry/cache' + config.expire_repo_payg_cache = 20 + config.expire_repo_byos_cache = 1440 # 24h in minutes + config.expire_repo_hybrid_cache = 1440 # 24h in minutes + config.expire_registry_cache = 840 # 8h in minutes end diff --git a/engines/instance_verification/lib/instance_verification/engine.rb b/engines/instance_verification/lib/instance_verification/engine.rb index 546fce6b9..8afd57523 100644 --- a/engines/instance_verification/lib/instance_verification/engine.rb +++ b/engines/instance_verification/lib/instance_verification/engine.rb @@ -1,25 +1,75 @@ +require 'base64' require 'fileutils' module InstanceVerification - def self.update_cache(remote_ip, system_login, product_id, registry: false) - # TODO: BYOS scenario - # to be addressed on a different PR + def self.update_cache(cache_entry, mode, registry: false) unless registry - InstanceVerification.write_cache_file( - Rails.application.config.repo_cache_dir, - [remote_ip, system_login, product_id].join('-') - ) + cache_path = InstanceVerification.get_cache_path(mode) + InstanceVerification.write_cache_file(cache_path, cache_entry) end + # update the registry cache every time InstanceVerification.write_cache_file( - Rails.application.config.registry_cache_dir, - [remote_ip, system_login].join('-') + InstanceVerification.get_cache_path('registry'), + cache_entry ) end + def self.build_cache_entry(remote_ip, system_login, encoded_reg_code, mode, product) + if mode == 'payg' + [remote_ip, system_login, product.id].join('-') + elsif mode == 'registry' + [remote_ip, system_login].join('-') + else + # for byos or hybrid cache + product_hash = product.attributes.symbolize_keys.slice(:identifier, :version, :arch) + product_triplet = "#{product_hash[:identifier]}_#{product_hash[:version]}_#{product_hash[:arch]}" + "#{encoded_reg_code}-#{product_triplet}-active" + end + end + def self.write_cache_file(cache_dir, cache_key) FileUtils.mkdir_p(cache_dir) FileUtils.touch(File.join(cache_dir, cache_key)) + Rails.logger.info "#{cache_dir} updated for #{cache_key}" + end + + def self.get_cache_path(mode) + if mode == 'byos' + Rails.application.config.repo_byos_cache_dir + elsif mode == 'hybrid' + Rails.application.config.repo_hybrid_cache_dir + elsif mode == 'payg' + Rails.application.config.repo_payg_cache_dir + else + Rails.application.config.registry_cache_dir + end + end + + def self.get_cache_entries(mode) + cache_path = InstanceVerification.get_cache_path(mode) + Dir.children(cache_path) + rescue SystemCallError + Rails.logger.info "#{cache_path} does not exist" + [] + end + + def self.reg_code_in_cache?(cache_key, mode) + cache_entries = InstanceVerification.get_cache_entries(mode) + cache_entries.find { |cache_entry| cache_entry.include?(cache_key) } + end + + def self.remove_entry_from_cache(cache_key, mode) + cache_path = InstanceVerification.get_cache_path(mode) + full_path_cache_key = File.join(cache_path, cache_key) + File.unlink(full_path_cache_key) if File.exist?(full_path_cache_key) + end + + def self.set_cache_inactive(cache_key, mode) + InstanceVerification.remove_entry_from_cache(cache_key, mode) + *all, _ = cache_key.split('-') + cache_key = [all, 'inactive'].join('-') + InstanceVerification.update_cache(cache_key, mode) end class Engine < ::Rails::Engine @@ -124,7 +174,8 @@ def verify_payg_extension_activation!(product) ) end logger.info "Product #{@product.product_string} available for this instance" - InstanceVerification.update_cache(request.remote_ip, @system.login, product.id) + cache_key = InstanceVerification.build_cache_entry(request.remote_ip, @system.login, nil, 'payg', product) + InstanceVerification.update_cache(cache_key, 'payg') end def verify_base_product_activation(product) @@ -136,7 +187,16 @@ def verify_base_product_activation(product) ) raise 'Unspecified error' unless verification_provider.instance_valid? - InstanceVerification.update_cache(request.remote_ip, @system.login, product.id) + + encoded_reg_code = @system.pubcloud_reg_code + # we use the token sent from the client if present + # instead of the value stored in the DB + encoded_reg_code = Base64.strict_encode64(params[:token]) if params[:token].present? + + cache_key = InstanceVerification.build_cache_entry( + request.remote_ip, @system.login, encoded_reg_code, @system.proxy_byos_mode, product + ) + InstanceVerification.update_cache(cache_key, @system.proxy_byos_mode) end # Verify that the base product doesn't change in the offline migration diff --git a/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb index 6ae99686c..755313236 100644 --- a/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb +++ b/engines/instance_verification/spec/requests/api/connect/v3/systems/products_controller_spec.rb @@ -1,3 +1,4 @@ +require 'base64' require 'rails_helper' # rubocop:disable RSpec/NestedGroups @@ -10,13 +11,21 @@ let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions) } let(:product_sap) { FactoryBot.create(:product, :product_sles_sap, :with_mirrored_repositories, :with_mirrored_extensions) } let(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' } - let(:payload) do + let(:expected_payload) do { identifier: product.identifier, version: product.version, arch: product.arch } end + let(:payload) do + { + identifier: product.identifier, + version: product.version, + arch: product.arch, + token: 'super_reg_code' + } + end describe '#activate' do let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') } @@ -38,11 +47,38 @@ it 'class instance verification provider' do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original.at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, nil).and_call_original.at_least(:once) + allow(Dir).to receive(:mkdir) + allow(FileUtils).to receive(:touch) + post url, params: payload, headers: headers + end + end + + context "when system doesn't have hw_info and cache is inactive" do + let(:system) { FactoryBot.create(:system, :byos, pubcloud_reg_code: Base64.strict_encode64('super_token')) } + + before do + stub_request(:post, 'https://scc.suse.com/connect/systems/products') + .to_return( + status: 201, + body: { ok: 'ok' }.to_json, + headers: {} + ) + end + + it 'class instance verification provider' do + expect(InstanceVerification::Providers::Example).to receive(:new) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, nil).and_call_original.at_least(:once) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) + allow(Dir).to receive(:children) allow(FileUtils).to receive(:touch) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return( + "#{system.pubcloud_reg_code}-inactive" + ) post url, params: payload, headers: headers + expect(response.body).to include('Subscription inactive') + expect(response).to have_http_status(403) end end @@ -94,6 +130,10 @@ context 'when verification provider raises an unhandled exception' do before do + expect(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double) + allow(plugin_double).to receive(:allowed_extension?).and_return(true) + allow(plugin_double).to receive(:instance_valid?).and_raise('not expected') + allow(InstanceVerification).to receive(:set_cache_inactive).and_return(nil) stub_request(:post, scc_activate_url) .to_return( status: 422, @@ -118,7 +158,7 @@ it 'class instance verification provider' do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original.at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, nil).and_call_original.at_least(:once) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) @@ -146,7 +186,7 @@ context 'when verification provider returns false' do before do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, instance_data).and_return(plugin_double).at_least(:once) expect(plugin_double).to receive(:instance_valid?).and_return(false) allow(plugin_double).to receive(:allowed_extension?).and_return(true) post url, params: payload, headers: headers @@ -161,7 +201,7 @@ context 'when verification provider raises an unhandled exception' do before do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, instance_data).and_return(plugin_double).at_least(:once) expect(plugin_double).to receive(:instance_valid?).and_raise('Custom plugin error') allow(plugin_double).to receive(:allowed_extension?).and_return(true) post url, params: payload, headers: headers @@ -178,7 +218,7 @@ before do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, instance_data).and_return(plugin_double).at_least(:once) expect(plugin_double).to receive(:instance_valid?).and_raise(InstanceVerification::Exception, 'Custom plugin error') allow(plugin_double).to receive(:allowed_extension?).and_return(true) post url, params: payload, headers: headers @@ -277,7 +317,13 @@ let(:instance_data) { 'dummy_instance_data' } let(:system) do FactoryBot.create( - :system, :payg, :with_system_information, :with_activated_product, product: base_product, instance_data: instance_data + :system, + :payg, + :with_system_information, + :with_activated_product, + product: base_product, + instance_data: instance_data, + pubcloud_reg_code: Base64.strict_encode64('super_token_different') ) end let(:serialized_service_json) do @@ -288,7 +334,7 @@ end let(:scc_activate_url) { 'https://scc.suse.com/connect/systems/products' } let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') } - let(:payload_no_token) do + let(:payload_token) do { identifier: product.identifier, version: product.version, @@ -345,6 +391,11 @@ 'User-Agent' => 'Ruby' } end + let(:cache_entry) do + product_hash = product.attributes.symbolize_keys.slice(:identifier, :version, :arch) + product_triplet = "#{product_hash[:identifier]}_#{product_hash[:version]}_#{product_hash[:arch]}" + "#{Base64.strict_encode64(payload_token[:token])}-#{product_triplet}-active" + end before do allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double) @@ -352,7 +403,10 @@ allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' }) allow(plugin_double).to receive(:allowed_extension?).and_return(true) - allow(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id) + allow(InstanceVerification).to receive(:update_cache).with("127.0.0.1-#{system.login}-#{product.id}", 'payg') + allow(InstanceVerification).to receive(:get_cache_entries).and_return( + [File.join(Rails.application.config.repo_hybrid_cache_dir, cache_entry)] + ) FactoryBot.create(:subscription, product_classes: product_classes) stub_request(:post, scc_activate_url) .to_return( @@ -366,14 +420,18 @@ .with({ headers: scc_announce_headers, body: scc_annouce_body.to_json }) .to_return(status: 201, body: scc_response_body, headers: {}) - expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id) + expect(InstanceVerification).to receive(:update_cache).with(cache_entry, 'hybrid') - post url, params: payload_no_token, headers: headers + post url, params: payload_token, headers: headers end context 'when regcode is provided' do it 'returns service JSON' do expect(response.body).to eq(serialized_service_json) + updated_system = System.find_by(login: system.login) + expect(updated_system.pubcloud_reg_code).to include(',') + expect(updated_system.pubcloud_reg_code).to include(Base64.strict_encode64(payload_token[:token]).to_s) + expect(updated_system.pubcloud_reg_code).to include(system.pubcloud_reg_code) end end end @@ -393,7 +451,7 @@ allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' }) allow(plugin_double).to receive(:allowed_extension?).and_return(true) - allow(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, product.id) + allow(InstanceVerification).to receive(:update_cache).with("127.0.0.1-#{system.login}-#{product.id}", 'foo') FactoryBot.create(:subscription, product_classes: product_classes) stub_request(:post, scc_activate_url) .to_return( @@ -406,9 +464,9 @@ stub_request(:post, 'https://scc.suse.com/connect/subscriptions/systems') .to_return(status: 201, body: scc_response_body, headers: {}) - expect(InstanceVerification).not_to receive(:update_cache).with('127.0.0.1', system.login, product.id) + expect(InstanceVerification).not_to receive(:update_cache) # .with('127.0.0.1', system.login, product.id) allow(plugin_double).to receive(:allowed_extension?).and_return(true) - post url, params: payload_no_token, headers: headers + post url, params: payload_token, headers: headers end it 'returns service JSON' do @@ -456,7 +514,7 @@ context 'when verification provider returns false' do before do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, instance_data).and_return(plugin_double).at_least(:once) allow(plugin_double).to receive(:allowed_extension?).and_return(true) expect(plugin_double).to receive(:instance_valid?).and_return(false) post url, params: payload, headers: headers @@ -471,7 +529,7 @@ context 'when verification provider raises an unhandled exception' do before do expect(InstanceVerification::Providers::Example).to receive(:new) - .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, instance_data).and_return(plugin_double).at_least(:once) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), expected_payload, instance_data).and_return(plugin_double).at_least(:once) allow(plugin_double).to receive(:allowed_extension?).and_return(true) expect(plugin_double).to receive(:instance_valid?).and_raise('Custom plugin error') post url, params: payload, headers: headers diff --git a/engines/registry/lib/registry/engine.rb b/engines/registry/lib/registry/engine.rb index 4aacf2f59..b5b851bfc 100644 --- a/engines/registry/lib/registry/engine.rb +++ b/engines/registry/lib/registry/engine.rb @@ -1,10 +1,8 @@ module Registry + # rubocop:disable Lint/EmptyClass class << self - def remove_auth_cache(registry_cache_key) - cache_path = File.join(Rails.application.config.registry_cache_dir, registry_cache_key) - File.unlink(cache_path) if File.exist?(cache_path) - end end + # rubocop:enable Lint/EmptyClass class Engine < ::Rails::Engine isolate_namespace Registry @@ -27,8 +25,8 @@ def refresh_auth_cache before_action :remove_auth_cache, only: %w[deregister] def remove_auth_cache - registry_cache_key = [request.remote_ip, @system.login].join('-') - Registry.remove_auth_cache(registry_cache_key) + registry_cache_key = InstanceVerification.build_cache_entry(request.remote_ip, @system.login, @system.pubcloud_reg_code, 'registry', nil) + InstanceVerification.remove_entry_from_cache(registry_cache_key, 'registry') end end end diff --git a/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb b/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb index 3e66f95b8..5c22fd713 100644 --- a/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb +++ b/engines/registry/spec/requests/api/connect/v3/systems/activations_controller_spec.rb @@ -27,6 +27,8 @@ end it 'does not update InstanceVerification cache' do + FileUtils.rm_rf('registry/cache') if File.exist?('registry/cache') + FileUtils.rm_rf('repo/payg/cache') if File.exist?('repo/payg/cache') allow(plugin_double).to( receive(:instance_valid?) .and_raise(InstanceVerification::Exception, 'Custom plugin error') @@ -40,7 +42,8 @@ end context 'with repository cache valid' do - let(:cache_name) { "repo/cache/127.0.0.1-#{system.login}-#{system.products.first.id}" } + let(:cache_path) { "repo/#{system.proxy_byos_mode}/cache" } + let(:cache_name) { "#{cache_path}/127.0.0.1-#{system.login}-#{system.products.first.id}" } before do allow(File).to receive(:join).and_call_original @@ -50,20 +53,22 @@ end it 'refreshes registry cache key only' do - FileUtils.mkdir_p('repo/cache') + FileUtils.mkdir_p(cache_path) FileUtils.touch(cache_name) - expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, nil, registry: true) + expect(InstanceVerification).to receive(:update_cache).with( + "127.0.0.1-#{system.login}-#{system.products.first.id}", system.proxy_byos_mode, registry: true + ) get '/connect/systems/activations', headers: headers - FileUtils.rm_rf('repo/cache') + FileUtils.rm_rf(cache_path) data = JSON.parse(response.body) expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud}) end end context 'system is hybrid' do - let(:system) { FactoryBot.create(:system, :hybrid, :with_activated_product) } + let(:system) { FactoryBot.create(:system, :hybrid, :with_activated_product, pubcloud_reg_code: Base64.strict_encode64('foo')) } let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') } - let(:cache_name) { "repo/cache/127.0.0.1-#{system.login}-#{system.products.first.id}" } + let(:cache_name) { "repo/payg/cache/127.0.0.1-#{system.login}-#{system.products.first.id}" } let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' } let(:body_active) do { @@ -83,6 +88,8 @@ } } end + let(:product_hash) { system.activations.first.product.attributes.symbolize_keys.slice(:identifier, :version, :arch) } + let(:product_triplet) { "#{product_hash[:identifier]}_#{product_hash[:version]}_#{product_hash[:arch]}" } before do allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double) @@ -90,7 +97,6 @@ allow(plugin_double).to( receive(:instance_valid?).and_return(true) ) - allow(File).to receive(:join).and_call_original allow(InstanceVerification).to receive(:update_cache) allow(ZypperAuth).to receive(:verify_instance).and_call_original stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_active].to_json, headers: {}) @@ -99,10 +105,14 @@ context 'no registry' do it 'refreshes registry cache key only' do - FileUtils.mkdir_p('repo/cache') - expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, system.activations.first.product.id) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) + FileUtils.mkdir_p('repo/payg/cache') + expect(InstanceVerification).to receive(:update_cache).with( + "#{system.pubcloud_reg_code}-#{product_triplet}-active", + system.proxy_byos_mode + ) get '/connect/systems/activations', headers: headers - FileUtils.rm_rf('repo/cache') + FileUtils.rm_rf('repo/payg/cache') data = JSON.parse(response.body) expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud}) end @@ -110,11 +120,14 @@ context 'registry' do it 'refreshes registry cache key only' do - FileUtils.mkdir_p('repo/cache') - FileUtils.touch(cache_name) - expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, nil, registry: true) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(true) + expect(InstanceVerification).to receive(:update_cache).with( + "#{system.pubcloud_reg_code}-#{product_triplet}-active", + system.proxy_byos_mode, + registry: true + ) get '/connect/systems/activations', headers: headers - FileUtils.rm_rf('repo/cache') + FileUtils.rm_rf('repo/payg/cache') data = JSON.parse(response.body) expect(data[0]['service']['url']).to match(%r{^plugin:/susecloud}) end @@ -123,7 +136,7 @@ end context 'byos' do - let(:system) { FactoryBot.create(:system, :byos, :with_activated_product) } + let(:system) { FactoryBot.create(:system, :byos, :with_activated_product, pubcloud_reg_code: Base64.strict_encode64('bar')) } let(:headers) { auth_header.merge(version_header) } let(:scc_systems_activations_url) { 'https://scc.suse.com/connect/systems/activations' } let(:body_active) do @@ -226,11 +239,8 @@ context 'without valid repository cache' do context 'with X-Instance-Data headers and bad metadata and good subscription on SCC' do let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') } - let(:scc_response) do - { - is_active: true - } - end + let(:product_hash) { system.activations.first.product.attributes.symbolize_keys.slice(:identifier, :version, :arch) } + let(:product_triplet) { "#{product_hash[:identifier]}_#{product_hash[:version]}_#{product_hash[:arch]}" } before do allow(InstanceVerification).to receive(:update_cache) @@ -243,9 +253,10 @@ receive(:instance_valid?) .and_raise(InstanceVerification::Exception, 'Custom plugin error') ) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) stub_request(:get, scc_systems_activations_url).to_return(status: 200, body: [body_active].to_json, headers: {}) allow(ZypperAuth).to receive(:verify_instance).and_call_original - expect(InstanceVerification).to receive(:update_cache).with('127.0.0.1', system.login, system.activations.first.product.id) + expect(InstanceVerification).to receive(:update_cache).with("#{system.pubcloud_reg_code}-#{product_triplet}-active", 'byos') get '/connect/systems/activations', headers: headers data = JSON.parse(response.body) @@ -256,6 +267,38 @@ expect(data[0]['system_id']).to match(system.activations.first.system_id) end end + + context 'with X-Instance-Data headers and bad metadata and bad subscription on SCC' do + let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') } + let(:product_hash) { system.activations.first.product.attributes.symbolize_keys.slice(:identifier, :version, :arch) } + let(:product_triplet) { "#{product_hash[:identifier]}_#{product_hash[:version]}_#{product_hash[:arch]}" } + let(:scc_response) do + { + is_active: false, + message: 'error' + } + end + + before do + allow(InstanceVerification).to receive(:update_cache) + headers['X-Instance-Data'] = 'IMDS' + Thread.current[:logger] = RMT::Logger.new('/dev/null') + end + + it 'set InstanceVerification cache inactive' do + allow(plugin_double).to( + receive(:instance_valid?) + .and_raise(InstanceVerification::Exception, 'Custom plugin error') + ) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) + allow(SccProxy).to receive(:scc_check_subscription_expiration).and_return(scc_response) + allow(ZypperAuth).to receive(:verify_instance).and_call_original + expect(InstanceVerification).to receive(:update_cache).with("#{system.pubcloud_reg_code}-#{product_triplet}-inactive", 'byos') + get '/connect/systems/activations', headers: headers + + expect(response.body).to include('Instance verification failed') + end + end end end end diff --git a/engines/scc_proxy/lib/scc_proxy/engine.rb b/engines/scc_proxy/lib/scc_proxy/engine.rb index 1169ff942..28e4e22f4 100644 --- a/engines/scc_proxy/lib/scc_proxy/engine.rb +++ b/engines/scc_proxy/lib/scc_proxy/engine.rb @@ -133,6 +133,10 @@ def scc_activate_product(system, product, auth, params, mode) scc_request = prepare_scc_request(uri.path, product, auth, params, mode) response = http.request(scc_request) unless response.code_type == Net::HTTPCreated + # if product can not be activated + # set the registration code as invalid in the cache + cache_key = InstanceVerification.build_cache_entry(nil, nil, Base64.strict_encode64(params[:token]), mode, product) + InstanceVerification.set_cache_inactive(cache_key, mode) error = JSON.parse(response.body) Rails.logger.info "Could not activate #{product.product_string}, error: #{error['error']} #{response.code}" error['error'] = SccProxy.parse_error(error['error']) if error['error'].include? 'json' @@ -325,6 +329,7 @@ def has_no_regcode?(auth_header) protected + # rubocop:disable Metrics/PerceivedComplexity def scc_activate_product product_hash = @product.attributes.symbolize_keys.slice(:identifier, :version, :arch) unless InstanceVerification.provider.new(logger, request, product_hash, @system.instance_data).allowed_extension? @@ -334,22 +339,42 @@ def scc_activate_product end mode = find_mode unless mode.nil? - # if system is byos or hybrid and there is a token - # make a request to SCC - logger.info "Activating product #{@product.product_string} to SCC" - logger.info 'No token provided' if params[:token].blank? - SccProxy.scc_activate_product( - @system, @product, request.headers['HTTP_AUTHORIZATION'], params, mode + # check cache first + encoded_reg_code = Base64.strict_encode64(params[:token]) + cache_entry = InstanceVerification.build_cache_entry( + request.remote_ip, @system.login, encoded_reg_code, mode, @product ) - # if the system is PAYG and the registration code is valid for the extension, - # then the system is hybrid - # update the system to HYBRID mode if HYBRID MODE and system not HYBRID already - @system.hybrid! if mode == 'hybrid' && @system.payg? - - logger.info "Product #{@product.product_string} successfully activated with SCC" - InstanceVerification.update_cache(request.remote_ip, @system.login, @product.id) + found_cache_entry = InstanceVerification.reg_code_in_cache?(cache_entry, mode) + if found_cache_entry.present? && found_cache_entry.include?('-inactive') + error = ActionController::TranslatedError.new(N_('Subscription inactive')) + error.status = :forbidden + raise error + elsif found_cache_entry.blank? + # if system is byos or hybrid and + # there is a token + # and not found in the cache + # make a request to SCC + logger.info "Activating product #{@product.product_string} to SCC" + logger.info 'No token provided' if params[:token].blank? + SccProxy.scc_activate_product( + @system, @product, request.headers['HTTP_AUTHORIZATION'], params, mode + ) + logger.info "Product #{@product.product_string} successfully activated with SCC" + # if the system is PAYG and the registration code is valid for the extension, + # then the system is hybrid + # update the system to HYBRID mode if HYBRID MODE and system not HYBRID already + @system.hybrid! if mode == 'hybrid' && @system.payg? + end + InstanceVerification.update_cache(cache_entry, mode) + if @system.pubcloud_reg_code.present? && @system.pubcloud_reg_code != encoded_reg_code + combination_reg_code = @system.pubcloud_reg_code + ',' + encoded_reg_code + @system.update(pubcloud_reg_code: combination_reg_code) + elsif @system.pubcloud_reg_code.nil? + @system.update(pubcloud_reg_code: encoded_reg_code) + end end end + # rubocop:enable Metrics/PerceivedComplexity def find_mode if @system.byos? diff --git a/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb index f023a15b9..2e638953b 100644 --- a/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb +++ b/engines/scc_proxy/spec/requests/api/connect/v3/systems/products_controller_spec.rb @@ -1,3 +1,4 @@ +require 'base64' require 'rails_helper' # rubocop:disable RSpec/NestedGroups @@ -19,6 +20,8 @@ include_context 'version header', 3 let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories, :with_mirrored_extensions) } let(:headers) { auth_header.merge(version_header) } + let(:product_hash) { product.attributes.symbolize_keys.slice(:identifier, :version, :arch) } + let(:product_triplet) { "#{product_hash[:identifier]}_#{product_hash[:version]}_#{product_hash[:arch]}" } let(:payload_byos) do { @@ -122,11 +125,12 @@ allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:touch) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}" + allow(InstanceVerification).to receive(:write_cache_file).with( + Rails.application.config.repo_byos_cache_dir, "#{Base64.strict_encode64(payload_byos[:token])}-#{product_triplet}-active" ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}" + allow(InstanceVerification).to receive(:write_cache_file).with( + Rails.application.config.registry_cache_dir, + "#{Base64.strict_encode64(payload_byos[:token])}-#{product_triplet}-active" ) end @@ -144,12 +148,7 @@ body: { error: 'No product found on SCC for: foo bar x86_64 json api' }.to_json, headers: {} ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}" - ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}" - ) + allow(InstanceVerification).to receive(:write_cache_file) allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:touch) @@ -181,12 +180,7 @@ body: { id: 'bar' }.to_json, headers: {} ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}" - ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}" - ) + allow(InstanceVerification).to receive(:write_cache_file) allow(File).to receive(:directory?) allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:touch) @@ -221,12 +215,7 @@ headers: {} ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_byos.login}-#{product.id}" - ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_byos.login}" - ) + allow(InstanceVerification).to receive(:write_cache_file) allow(File).to receive(:directory?) allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:touch) @@ -417,12 +406,7 @@ allow(FileUtils).to receive(:touch) allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double) allow(plugin_double).to receive(:allowed_extension?).and_return(true) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_payg.login}-#{product.id}" - ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_payg.login}" - ) + allow(InstanceVerification).to receive(:write_cache_file) allow(plugin_double).to receive(:instance_valid?).and_return(true) end @@ -529,12 +513,8 @@ allow(File).to receive(:directory?) allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:touch) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_payg.login}-#{product.id}" - ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_payg.login}" - ) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return('') + allow(InstanceVerification).to receive(:write_cache_file) allow(plugin_double).to receive(:instance_valid?).and_return(true) end @@ -555,14 +535,10 @@ body: { error: 'No product found on SCC for: foo bar x86_64 json api' }.to_json, headers: {} ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.repo_cache_dir, "127.0.0.1-#{system_payg.login}-#{product.id}" - ) - allow(InstanceVerification).to receive(:write_cache_file).twice.with( - Rails.application.config.registry_cache_dir, "127.0.0.1-#{system_payg.login}" - ) + allow(InstanceVerification).to receive(:write_cache_file) allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:touch) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return('') end context 'when de-register system from SCC suceeds' do diff --git a/engines/strict_authentication/spec/requests/services_controller_spec.rb b/engines/strict_authentication/spec/requests/services_controller_spec.rb index 7cedbd3b5..c286a5a17 100644 --- a/engines/strict_authentication/spec/requests/services_controller_spec.rb +++ b/engines/strict_authentication/spec/requests/services_controller_spec.rb @@ -4,7 +4,7 @@ RSpec.describe ServicesController, type: :request do describe '#show' do - let(:system) { FactoryBot.create(:system) } + let(:system) { FactoryBot.create(:system, :payg) } let(:service) { FactoryBot.create(:service, :with_repositories) } let(:activated_service) do service = FactoryBot.create(:service, :with_repositories) @@ -61,6 +61,7 @@ allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(InstanceVerification).to receive(:update_cache) get "/services/#{activated_service.id}", headers: headers end @@ -80,6 +81,7 @@ allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(InstanceVerification).to receive(:update_cache) get "/services/#{activated_service.id}", headers: headers end diff --git a/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb b/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb index 8fad34aaa..d51ffaaee 100644 --- a/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb +++ b/engines/strict_authentication/spec/requests/strict_authentication/authentication_controller_spec.rb @@ -26,6 +26,7 @@ module StrictAuthentication allow_any_instance_of(InstanceVerification::Providers::Example).to( receive(:instance_valid?).and_return(true) ) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(InstanceVerification).to receive(:update_cache) get '/api/auth/check', headers: auth_header.merge({ 'X-Original-URI': requested_uri, 'X-Instance-Data': 'IMDS' }) allow(File).to receive(:directory?) diff --git a/engines/zypper_auth/lib/zypper_auth/engine.rb b/engines/zypper_auth/lib/zypper_auth/engine.rb index 41817b41f..77a2f4156 100644 --- a/engines/zypper_auth/lib/zypper_auth/engine.rb +++ b/engines/zypper_auth/lib/zypper_auth/engine.rb @@ -13,11 +13,10 @@ def verify_instance(request, logger, system) return false unless base_product # check the cache for the system (20 min) - cache_key = [request.remote_ip, system.login, base_product.id].join('-') - cache_path = File.join(Rails.application.config.repo_cache_dir, cache_key) - if File.exist?(cache_path) + cache_key = InstanceVerification.build_cache_entry(request.remote_ip, system.login, system.pubcloud_reg_code, system.proxy_byos_mode, base_product) + if InstanceVerification.reg_code_in_cache?(cache_key, system.proxy_byos_mode) # only update registry cache key - InstanceVerification.update_cache(request.remote_ip, system.login, nil, registry: true) + InstanceVerification.update_cache(cache_key, system.proxy_byos_mode, registry: true) return true end @@ -30,17 +29,19 @@ def verify_instance(request, logger, system) is_valid = verification_provider.instance_valid? # update repository and registry cache - InstanceVerification.update_cache(request.remote_ip, system.login, base_product.id) + InstanceVerification.update_cache(cache_key, system.proxy_byos_mode) is_valid rescue InstanceVerification::Exception => e if system.byos? result = SccProxy.scc_check_subscription_expiration(request.headers, system, base_product.product_class) if result[:is_active] - InstanceVerification.update_cache(request.remote_ip, system.login, base_product.id) + # update the cache for the base product + InstanceVerification.update_cache(cache_key, 'byos') return true end + # if can not get the activations, set the cache inactive + InstanceVerification.set_cache_inactive(cache_key, system.proxy_byos_mode) end - ZypperAuth.zypper_auth_message(request, system, verification_provider, e.message) false rescue StandardError => e diff --git a/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb b/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb index 10500b262..c0aecf6bb 100644 --- a/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb +++ b/engines/zypper_auth/spec/requests/api/connect/v3/systems/activations_controller_spec.rb @@ -10,6 +10,7 @@ before do allow_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(true) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(InstanceVerification).to receive(:update_cache) get '/connect/systems/activations', headers: headers end diff --git a/engines/zypper_auth/spec/requests/services_controller_spec.rb b/engines/zypper_auth/spec/requests/services_controller_spec.rb index 349c38aa3..fc235548b 100644 --- a/engines/zypper_auth/spec/requests/services_controller_spec.rb +++ b/engines/zypper_auth/spec/requests/services_controller_spec.rb @@ -37,6 +37,7 @@ context 'when instance verification succeeds' do before do expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(true) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(InstanceVerification).to receive(:update_cache) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) @@ -61,6 +62,7 @@ before do expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(false) allow(InstanceVerification).to receive(:update_cache) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) @@ -80,6 +82,7 @@ before do expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_raise('Test') allow(InstanceVerification).to receive(:update_cache) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) @@ -99,6 +102,7 @@ before do expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_raise(InstanceVerification::Exception, 'Test') allow(File).to receive(:directory?).twice + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) diff --git a/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb b/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb index d8bac081d..9431ca6e8 100644 --- a/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb +++ b/engines/zypper_auth/spec/requests/strict_authentication/authentication_controller_spec.rb @@ -37,6 +37,7 @@ let(:headers) { auth_header.merge({ 'X-Original-URI': requested_uri, 'X-Instance-Data': 'test' }) } before do + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) Rails.cache.clear expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(false) allow(File).to receive(:directory?) @@ -278,6 +279,7 @@ before do Rails.cache.clear expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(true) + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(InstanceVerification).to receive(:update_cache) allow(DataExport::Handlers::Example).to receive(:new).and_return(data_export_double) allow(File).to receive(:directory?) @@ -501,6 +503,7 @@ let(:data_export_double) { instance_double('DataExport::Handlers::Example') } before do + allow(InstanceVerification).to receive(:reg_code_in_cache?).and_return(nil) allow(DataExport::Handlers::Example).to receive(:new).and_return(data_export_double) expect_any_instance_of(InstanceVerification::Providers::Example).to receive(:instance_valid?).and_return(true) expect(data_export_double).to receive(:export_rmt_data)