diff --git a/.github/workflows/lint-unit.yml b/.github/workflows/lint-unit.yml index f9d1eaebc..e97009ec4 100644 --- a/.github/workflows/lint-unit.yml +++ b/.github/workflows/lint-unit.yml @@ -82,7 +82,12 @@ jobs: - name: Run core tests run: | bundle exec rake test:core - + + - name: Run core tests with sqlite + run: | + sed -i 's/adapter: mysql2/adapter: sqlite3/' config/rmt.yml + bundle exec rake test:core + - name: Run PubCloud engines tests run: | bundle exec rake test:engines diff --git a/app/controllers/api/connect/base_controller.rb b/app/controllers/api/connect/base_controller.rb index 1fbeaff07..84de46574 100644 --- a/app/controllers/api/connect/base_controller.rb +++ b/app/controllers/api/connect/base_controller.rb @@ -44,4 +44,14 @@ def authenticate_with_token end end + def system_token_header + headers[SYSTEM_TOKEN_HEADER] = @system.system_token + end + + def refresh_system_token + if system_tokens_enabled? + @system.update(system_token: SecureRandom.uuid) + system_token_header + end + end end diff --git a/app/controllers/api/connect/v3/systems/products_controller.rb b/app/controllers/api/connect/v3/systems/products_controller.rb index 2545a9e92..648443ecd 100644 --- a/app/controllers/api/connect/v3/systems/products_controller.rb +++ b/app/controllers/api/connect/v3/systems/products_controller.rb @@ -5,6 +5,7 @@ class Api::Connect::V3::Systems::ProductsController < Api::Connect::BaseControll before_action :check_product_service_and_repositories, only: %i[show activate] before_action :load_subscription, only: %i[activate upgrade] before_action :check_base_product_dependencies, only: %i[activate upgrade show] + after_action :refresh_system_token, only: %i[activate upgrade], if: -> { request.headers.key?(SYSTEM_TOKEN_HEADER) } def activate create_product_activation @@ -12,7 +13,13 @@ def activate end def show - if @system.products.include? @product + if @product.identifier.casecmp?('sles') + # if system has SLE Micro + # it should access to SLES products + sle_micro = @system.products.any? { |p| p.identifier.downcase.include?('sle-micro') } + sle_micro_same_arch = @system.products.pluck(:arch).include?(@product.arch) if sle_micro + end + if @system.products.include?(@product) || sle_micro_same_arch respond_with( @product, serializer: ::V3::ProductSerializer, diff --git a/app/controllers/api/connect/v3/systems/systems_controller.rb b/app/controllers/api/connect/v3/systems/systems_controller.rb index 863557279..d9a17154d 100644 --- a/app/controllers/api/connect/v3/systems/systems_controller.rb +++ b/app/controllers/api/connect/v3/systems/systems_controller.rb @@ -1,6 +1,7 @@ class Api::Connect::V3::Systems::SystemsController < Api::Connect::BaseController before_action :authenticate_system + after_action :refresh_system_token, only: [:update], if: -> { request.headers.key?(SYSTEM_TOKEN_HEADER) } def update if params[:online_at].present? diff --git a/app/controllers/api/connect/v4/systems/products_controller.rb b/app/controllers/api/connect/v4/systems/products_controller.rb index 7237e5a80..d4c87adfc 100644 --- a/app/controllers/api/connect/v4/systems/products_controller.rb +++ b/app/controllers/api/connect/v4/systems/products_controller.rb @@ -1,5 +1,6 @@ class Api::Connect::V4::Systems::ProductsController < Api::Connect::V3::Systems::ProductsController + after_action :refresh_system_token, only: %i[activate upgrade synchronize destroy], if: -> { request.headers.key?(SYSTEM_TOKEN_HEADER) } def destroy if @product.base? raise ActionController::TranslatedError.new(N_('The product "%s" is a base product and cannot be deactivated'), @product.name) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 686972895..63458615a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -21,10 +21,10 @@ def authenticate_system(skip_on_duplicated: false) update_user_agent # If SYSTEM_TOKEN_HEADER is present, RMT assumes the client uses a SUSEConnect version - # that supports this feature. In this case, refresh the token and include it in the response. + # that supports this feature. if system_tokens_enabled? && request.headers.key?(SYSTEM_TOKEN_HEADER) - @system.update(last_seen_at: Time.zone.now, system_token: SecureRandom.uuid) - headers[SYSTEM_TOKEN_HEADER] = @system.system_token + @system.update(last_seen_at: Time.zone.now) + system_token_header # only update last_seen_at each 3 minutes, # so that a system that calls SCC every second doesn't write + lock the database row elsif !@system.last_seen_at || @system.last_seen_at < 3.minutes.ago diff --git a/app/models/repository.rb b/app/models/repository.rb index 6a7719aa2..c65c602cd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -23,14 +23,15 @@ class Repository < ApplicationRecord class << self def remove_suse_repos_without_tokens! - where(auth_token: nil).where('external_url LIKE ?', 'https://updates.suse.com%').delete_all + where(auth_token: nil).where("external_url LIKE '%.suse.com%'").where(installer_updates: 0).where.not(scc_id: nil).delete_all end # Mangles remote repo URL to make a nicer local path, see specs for examples def make_local_path(url) uri = URI(url) path = uri.path.to_s - path.gsub!(%r{^/repo}, '') if (uri.hostname == 'updates.suse.com') + # drop '/repo' from SLE11 paths, to avoid double /repo/repo in local storage path. + path.gsub!(%r{^/repo/\$RCE/}, '/$RCE/') (path == '') ? '/' : path end diff --git a/config/rmt.yml b/config/rmt.yml index 811e44ff2..6a2df5b5c 100644 --- a/config/rmt.yml +++ b/config/rmt.yml @@ -20,7 +20,7 @@ database_test: database: rmt_test scc: - host: https://scc.suse.com/connect + host: <%= ENV.fetch('SCC_HOST'){ 'https://scc.suse.com/connect' } %> username: <%= ENV['SCC_USERNAME'] %> password: <%= ENV['SCC_PASSWORD'] %> sync_systems: true diff --git a/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb b/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb index 9d00eee26..0217e54d7 100644 --- a/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb +++ b/engines/instance_verification/app/controllers/instance_verification/billing_check_controller.rb @@ -5,7 +5,7 @@ def check # belongs to a PAYG or BYOS instance verification_provider = InstanceVerification.provider.new( logger, - nil, + request, nil, params[:metadata] ) diff --git a/engines/instance_verification/lib/instance_verification/providers/example.rb b/engines/instance_verification/lib/instance_verification/providers/example.rb index b773d7855..e4379c39b 100644 --- a/engines/instance_verification/lib/instance_verification/providers/example.rb +++ b/engines/instance_verification/lib/instance_verification/providers/example.rb @@ -27,10 +27,6 @@ def validate_instance_data(_instance_data) end def parse_instance_data - if @instance_data.include? '' - return { 'instance_data' => 'parsed_instance_data' } - end - if @instance_data.include?('SUSE') if @instance_data.include?('SAP') return { 'billingProducts' => nil, 'marketplaceProductCodes' => ['6789_SUSE_SAP'] } @@ -49,4 +45,14 @@ def payg_billing_code?(iid, identifier) return true if (identifier.casecmp('sles').zero? && instance_billing_info[:billing_product] == SLES_PRODUCT_IDENTIFIER) return true if (identifier.casecmp('sles_sap').zero? && SLES4SAP_PRODUCT_IDENTIFIER.include?(instance_billing_info[:marketplace_code])) end + + def instance_identifier + 'foo' + end + + def allowed_extension? + # method to check if a product (extension) meets the criteria + # to be activated on SCC or not, i.e. LTSS in Azure Basic VM + true + end end 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 9ccbcb1c2..6b7afad35 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 @@ -38,7 +38,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 + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original.at_least(:once) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) @@ -71,13 +71,18 @@ end context 'when verification provider returns false' do + let(:plugin_double) { instance_double('InstanceVerification::Providers::Example') } + before do stub_request(:post, scc_activate_url) .to_return( status: 200, body: { error: 'Unexpected instance verification error has occurred' }.to_json, headers: {} - ) + ) + allow(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_return(false) post url, params: payload, headers: headers end @@ -113,7 +118,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 + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), payload, nil).and_call_original.at_least(:once) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) @@ -141,8 +146,9 @@ 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) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), 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 end @@ -155,8 +161,9 @@ 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) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), 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 end @@ -171,9 +178,9 @@ 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) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), 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 end @@ -227,9 +234,9 @@ end before do - allow(InstanceVerification::Providers::Example).to receive(:new) - .with(nil, nil, nil, instance_data).and_return(plugin_double) + allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double) allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' }) + allow(plugin_double).to receive(:allowed_extension?).and_return(true) FactoryBot.create(:subscription, product_classes: product_classes) stub_request(:post, scc_activate_url) @@ -339,9 +346,10 @@ end before do - allow(InstanceVerification::Providers::Example).to receive(:new) - .with(nil, nil, nil, instance_data).and_return(plugin_double) + allow(InstanceVerification::Providers::Example).to receive(:new).and_return(plugin_double) + allow(plugin_double).to receive(:instance_identifier).and_return('foo') 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) FactoryBot.create(:subscription, product_classes: product_classes) @@ -380,8 +388,9 @@ before do allow(InstanceVerification::Providers::Example).to receive(:new) - .with(nil, nil, nil, instance_data).and_return(plugin_double) + .and_return(plugin_double) 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) FactoryBot.create(:subscription, product_classes: product_classes) @@ -397,7 +406,7 @@ .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) - + allow(plugin_double).to receive(:allowed_extension?).and_return(true) post url, params: payload_no_token, headers: headers end @@ -409,12 +418,16 @@ end context 'when the system is hybrid' do + before do + allow_any_instance_of(InstanceVerification::Providers::Example).to receive(:allowed_extension?).and_return(true) + end + context "when system doesn't have hw_info" do let(:system) { FactoryBot.create(:system, :hybrid) } 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 + .and_call_original.at_least(:once) allow(File).to receive(:directory?) allow(Dir).to receive(:mkdir) allow(FileUtils).to receive(:touch) @@ -442,7 +455,8 @@ 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) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), 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 end @@ -456,7 +470,8 @@ 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) + .with(be_a(ActiveSupport::Logger), be_a(ActionDispatch::Request), 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 end @@ -514,8 +529,9 @@ before do allow(InstanceVerification::Providers::Example).to receive(:new) - .with(nil, nil, nil, instance_data).and_return(plugin_double) + .and_return(plugin_double) allow(plugin_double).to receive(:parse_instance_data).and_return({ InstanceId: 'foo' }) + allow(plugin_double).to receive(:allowed_extension?).and_return(true) FactoryBot.create(:subscription, product_classes: product_classes) stub_request(:post, scc_activate_url) diff --git a/engines/scc_proxy/lib/scc_proxy/engine.rb b/engines/scc_proxy/lib/scc_proxy/engine.rb index e684dc910..47e6557e8 100644 --- a/engines/scc_proxy/lib/scc_proxy/engine.rb +++ b/engines/scc_proxy/lib/scc_proxy/engine.rb @@ -25,12 +25,6 @@ Net::HTTPRetriableError ].freeze -INSTANCE_ID_KEYS = { - amazon: 'instanceId', - google: 'instance_id', - microsoft: 'vmId' -}.freeze - # rubocop:disable Metrics/ModuleLength module SccProxy class << self @@ -43,7 +37,12 @@ class << self # rubocop:disable ThreadSafety/InstanceVariableInClassMethod def headers(auth, params) @instance_id = if params && params.class != String - get_instance_id(params) + InstanceVerification.provider.new( + nil, + nil, + nil, + params['instance_data'] + ).instance_identifier else # if it is not JSON, it is the system_token already # announce system has metadata @@ -61,18 +60,6 @@ def headers(auth, params) end # rubocop:enable ThreadSafety/InstanceVariableInClassMethod - def get_instance_id(params) - verification_provider = InstanceVerification.provider.new( - nil, - nil, - nil, - params['instance_data'] - ) - instance_id_key = INSTANCE_ID_KEYS[params['hwinfo']['cloud_provider'].downcase.to_sym] - iid = verification_provider.parse_instance_data - iid[instance_id_key] - end - def prepare_scc_announce_request(uri_path, auth, params) scc_request = Net::HTTP::Post.new(uri_path, headers(auth, params)) @@ -307,6 +294,7 @@ def scc_upgrade(auth, product, system_login, mode, logger) end end + # rubocop:disable Metrics/ClassLength class Engine < ::Rails::Engine isolate_namespace SccProxy config.generators.api_only = true @@ -372,6 +360,12 @@ def has_no_regcode?(auth_header) protected 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? + error = ActionController::TranslatedError.new(N_('Product not supported for this instance')) + error.status = :forbidden + raise error + end mode = find_mode unless mode.nil? # if system is byos or hybrid and there is a token @@ -540,5 +534,6 @@ def get_system(systems) end end end + # rubocop:enable Metrics/ClassLength end # rubocop:enable Metrics/ModuleLength 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 0ff8417f3..f023a15b9 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 @@ -354,7 +354,7 @@ end let(:product) do FactoryBot.create( - :product, :product_sles, :extension, :with_mirrored_repositories, :with_mirrored_extensions, + :product, :product_sles_ltss, :extension, :with_mirrored_repositories, :with_mirrored_extensions, base_products: [system_payg.products.first] ) end @@ -415,7 +415,120 @@ allow(File).to receive(:directory?) allow(FileUtils).to receive(:mkdir_p) 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(plugin_double).to receive(:instance_valid?).and_return(true) + end + + context 'when LTSS not allowed' do + before do + allow(plugin_double).to receive(:allowed_extension?).and_return(false) + end + + it 'raises an error' do + stub_request(:post, scc_register_system_url) + .to_return(status: 403, body: { ok: 'OK' }.to_json, headers: {}) + + post url, params: payload, headers: headers + data = JSON.parse(response.body) + expect(data['error']).to include('Product not supported for this instance') + end + end + end + end + end + + context 'when system has hw info' do + let(:instance_data) { '{"instanceId": "dummy_instance_data"}' } + let(:new_system_token) { 'BBBBBBBB-BBBB-4BBB-9BBB-BBBBBBBBBBBB' } + let(:serialized_service_json) do + V3::ServiceSerializer.new( + product.service, + base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s + ).to_json + end + + let(:serialized_service_sap_json) do + V3::ServiceSerializer.new( + product_sap.service, + base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s + ).to_json + end + + context 'when system is connected to SCC' do + let(:system_payg) do + FactoryBot.create(:system, :payg, :with_system_information, :with_activated_base_product, instance_data: instance_data, + system_token: new_system_token) + end + let(:product) do + FactoryBot.create( + :product, :product_sles_ltss, :extension, :with_mirrored_repositories, :with_mirrored_extensions, + base_products: [system_payg.products.first] + ) + end + let(:subscription_response) do + { + id: 4206714, + regcode: 'bar', + name: 'SUSE Employee subscription for SUSE Linux Enterprise Server for SAP Applications', + type: 'internal', + status: 'ACTIVE', + starts_at: '2019-03-20T09:48:52.658Z', + expires_at: '2024-03-20T09:48:52.658Z', + system_limit: '100', + systems_count: '156', + virtual_count: nil, + product_classes: [ + 'AiO', + '7261', + 'SLE-HAE-X86', + '7261-BETA', + 'SLE-HAE-X86-BETA', + 'AiO-BETA', + '7261-ALPHA', + 'SLE-HAE-X86-ALPHA', + 'AiO-ALPHA' + ], + product_ids: [ + 1959, + 1421 + ], + skus: [], + systems: [ + { + id: 3021957, + login: 'SCC_foo', + password: '5ee7273ac6ac4d7f', + last_seen_at: '2019-03-20T14:01:05.424Z' + } + ] + } + end + + before do + allow(plugin_double).to( + receive(:instance_valid?) + .and_raise(InstanceVerification::Exception, 'Custom plugin error') + ) + end + context 'with a valid registration code' do + before do + stub_request(:post, scc_activate_url) + .to_return( + status: 201, + body: { id: 'bar' }.to_json, + headers: {} + ) + 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}" ) diff --git a/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb b/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb index 311c38fc0..b3fabfab6 100644 --- a/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb +++ b/engines/strict_authentication/app/controllers/strict_authentication/authentication_controller.rb @@ -7,27 +7,30 @@ class AuthenticationController < ::ApplicationController # This is the endpoint for nginx subrequest auth check def check request_uri = request.headers['X-Original-URI'] - auth_result = path_allowed?(request.headers['X-Original-URI']) + auth_result = path_allowed?(request.headers) logger.info "Authentication subrequest for #{request_uri} -- #{auth_result ? 'allowed' : 'denied'}" head auth_result ? :ok : :forbidden end protected - def path_allowed?(path) + def path_allowed?(headers) + path = headers['X-Original-URI'] return false if path.blank? + return true if path =~ %r{/product\.license/} path = '/' + path.gsub(/^#{RMT::DEFAULT_MIRROR_URL_PREFIX}/, '') - # Allow access to SLES 12 and 12-SP1 repos for systems migrating from SLES 11 has_sles11 = @system.products.where(identifier: 'SUSE_SLES').first return true if (has_sles11 && (path =~ %r{/12/} || path =~ %r{/12-SP1/})) - all_allowed_paths.find { |allowed_path| path =~ /^#{Regexp.escape(allowed_path)}/ } + all_allowed_paths(headers).find { |allowed_path| path =~ /^#{Regexp.escape(allowed_path)}/ } end - def all_allowed_paths + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def all_allowed_paths(headers) # return all versions of the same product and arch # (that the system has available with that subscription) # in order to validate access not only for current product but others @@ -36,10 +39,31 @@ def all_allowed_paths # to them or verify paths all_product_versions = @system.products.map { |p| Product.where(identifier: p.identifier, arch: p.arch) }.flatten allowed_paths = all_product_versions.map { |prod| prod.repositories.pluck(:local_path) }.flatten + # Allow SLE Micro to access all free SLES repositories + sle_micro = @system.products.any? { |p| p.identifier.downcase.include?('sle-micro') } + if sle_micro + system_products_archs = @system.products.pluck(:arch) + product_free_sles_modules_only = Product.where( + "(lower(identifier) like 'sle-module%' or lower(identifier) like 'packagehub') + and lower(identifier) not like '%sap%' + and arch = '#{system_products_archs.first}' + and free = 1" + ) + end + same_arch = product_free_sles_modules_only.any? { |p| system_products_archs.include?(p.arch) } if product_free_sles_modules_only.present? + allowed_paths += product_free_sles_modules_only.map { |prod| prod.repositories.pluck(:local_path) }.flatten if same_arch + # for the SUMa PAYG offers, RMT access verification code allows access # to the SUMa Client Tools channels and SUMa Proxy channels # when product is SUMA_Server and PAYG or SUMA_Server and used as SCC proxy - manager_prod = @system.products.any? { |p| p.identifier.downcase.include?('manager-server') } + manager_prod = @system.products.any? do |p| + manager = p.identifier.downcase.include?('manager-server') + # SUMA 5.0 must have access to SUMA 4.3, 4.2 and so on + micro = p.identifier.downcase.include?('sle-micro') + instance_id_header = headers.fetch('X-Instance-Identifier', '').casecmp('suse-manager-server').zero? + instance_version_header = headers.fetch('X-Instance-Version', '0').split('.')[0] >= '5' + manager || (micro && instance_id_header && instance_version_header) + end if manager_prod # add all SUMA products paths @@ -49,5 +73,7 @@ def all_allowed_paths end allowed_paths end + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/PerceivedComplexity end 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 c921d3855..fcb5f4e03 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 @@ -5,7 +5,7 @@ module StrictAuthentication RSpec.describe AuthenticationController, type: :request do subject { response } - let(:system) { FactoryBot.create(:system, :with_activated_product) } + let(:system) { FactoryBot.create(:system, :with_activated_product_sle_micro) } describe '#check' do context 'without authentication' do @@ -39,6 +39,36 @@ module StrictAuthentication its(:code) { is_expected.to eq '403' } end + context 'when requesting a file in an activated SLES repo on a SLE Micro system' do + let(:free_product) do + prod = FactoryBot.create( + :product, :module, :with_mirrored_repositories + ) + prod.identifier = 'sle-module-foo' + prod.arch = system.products.first.arch + prod.save! + prod + end + let(:requested_uri) { '/repo' + free_product.repositories.first[:local_path] + '/repodata/repomd.xml' } + + its(:code) { is_expected.to eq '200' } + end + + context 'when requesting a file in an activated SLES SAP repo on a SLE Micro system' do + let(:free_product) do + prod = FactoryBot.create( + :product, :module, :with_mirrored_repositories + ) + prod.identifier = 'sle-module-foo-sap' + prod.arch = system.products.first.arch + prod.save! + prod + end + let(:requested_uri) { '/repo' + free_product.repositories.first[:local_path] + '/repodata/repomd.xml' } + + its(:code) { is_expected.to eq '403' } + end + context 'when requesting a file in an activated repo' do let(:requested_uri) { '/repo' + system.repositories.first[:local_path] + '/repodata/repomd.xml' } diff --git a/package/obs/rmt-server.changes b/package/obs/rmt-server.changes index 71ad51f29..c84e3dbf6 100644 --- a/package/obs/rmt-server.changes +++ b/package/obs/rmt-server.changes @@ -1,3 +1,19 @@ +------------------------------------------------------------------- +Wed Oct 30 09:01:32 UTC 2024 - Natnael Getahun + +- Version 2.20 (unreleased) + * RMT packaging: don't overwrite custom sync/mirror timer config on package update + * Extend column size for repository and file paths (bsc#1229152) + * Forward suseconnect client user-agents to SCC + * rmt-server-pubcloud: + * Fix LTSS product verification (bsc#1230154) + * Fix activations check when no product info is available (bsc#1230157) + * Fix Azure SCC connection (bsc#1233314) + * Deny access of Azure Basic type images to LTSS + * Allow SLE Micro system to access SLES repositories (bsc#1230419) + * Skip system token rotation in read-only APIs + * Enable RMT to handle the new dl.suse.com CDN domain (bsc#1234641) + ------------------------------------------------------------------- Wed Aug 21 15:28:43 UTC 2024 - Jesús Bermúdez Velázquez diff --git a/spec/factories/products.rb b/spec/factories/products.rb index 30532e1fc..4612dfb00 100644 --- a/spec/factories/products.rb +++ b/spec/factories/products.rb @@ -57,6 +57,23 @@ friendly_version { '15 SP3' } end + trait :product_sles_ltss do + identifier { 'SLES-LTSS' } + name { 'SUSE Linux Enterprise Server LTSS' } + description { 'SUSE Linux Enterprise offers a comprehensive suite of products...' } + shortname { 'SLES15-SP3-LTSS' } + former_identifier { 'SLES_LTSS' } + product_type { 'extension' } + product_class { 'LTSS' } + release_type { nil } + release_stage { 'released' } + version { '15.3' } + arch { 'x86_64' } + free { false } + cpe { 'cpe:/o:suse:sles:15:sp3' } + friendly_version { '15 SP3' } + end + trait :product_sles_sap do identifier { 'SLES_SAP' } name { 'SUSE Linux Enterprise Server' } @@ -73,6 +90,22 @@ friendly_version { '15 SP3' } end + trait :product_sle_micro do + identifier { 'SLE-Micro' } + name { 'SUSE Linux Enterprise Server' } + description { 'SUSE Linux Enterprise offers a comprehensive suite of products...' } + shortname { 'SLES15-SP6' } + former_identifier { 'SUSE_SLES_MICRO' } + product_type { :base } + release_type { nil } + release_stage { 'released' } + version { '15.6' } + arch { 'x86_64' } + free { false } + cpe { 'cpe:/o:suse:sles_sap:15:sp6' } + friendly_version { '15 SP6' } + end + trait :extension do product_type { 'extension' } end diff --git a/spec/factories/systems.rb b/spec/factories/systems.rb index 8fb81aa38..ceab7d02e 100644 --- a/spec/factories/systems.rb +++ b/spec/factories/systems.rb @@ -57,6 +57,17 @@ end end + trait :with_activated_product_sle_micro do + transient do + product { create(:product, :product_sle_micro, :with_mirrored_repositories) } + subscription { nil } + end + + after :create do |system, evaluator| + create(:activation, system: system, service: evaluator.product.service, subscription: evaluator.subscription) + end + end + trait :with_system_information do system_information do { diff --git a/spec/lib/rmt/cli/smt_importer_spec.rb b/spec/lib/rmt/cli/smt_importer_spec.rb index d77404a52..17063bbc0 100644 --- a/spec/lib/rmt/cli/smt_importer_spec.rb +++ b/spec/lib/rmt/cli/smt_importer_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require 'rmt/cli/smt_importer' -describe RMT::CLI::SMTImporter do +describe RMT::CLI::SMTImporter, :skip_sqlite do let(:data_dir) { File.join(Dir.pwd, 'spec/fixtures/files/csv') } let(:no_systems) { false } let(:importer) { described_class.new(data_dir, no_systems) } diff --git a/spec/lib/rmt/lockfile_spec.rb b/spec/lib/rmt/lockfile_spec.rb index d1b86f968..c06644bc3 100644 --- a/spec/lib/rmt/lockfile_spec.rb +++ b/spec/lib/rmt/lockfile_spec.rb @@ -3,7 +3,7 @@ RSpec.describe RMT::Lockfile do let(:lock_name) { nil } - describe '#lock' do + describe '#lock', :skip_sqlite do subject(:lock) { described_class.lock(lock_name) { nil } } context 'with an unnamed lock' do diff --git a/spec/lib/rmt/scc_spec.rb b/spec/lib/rmt/scc_spec.rb index 802cf1c8a..f1e3b9d61 100644 --- a/spec/lib/rmt/scc_spec.rb +++ b/spec/lib/rmt/scc_spec.rb @@ -238,7 +238,7 @@ described_class.new.sync end - it 'removes existing predecessor associations' do + it 'removes existing predecessor associations', :skip_sqlite do expect { ProductPredecessorAssociation.find(existing_association.id) }.to raise_error(ActiveRecord::RecordNotFound) end end @@ -261,7 +261,18 @@ :repository, :with_products, auth_token: nil, - external_url: 'https://example.com/repos/not/updates.suse.com/' + external_url: 'https://installer-updates.suse.com/repos/not/updates', + installer_updates: true + ) + end + let!(:custom_repo) do + FactoryBot.create( + :repository, + :with_products, + auth_token: nil, + external_url: 'http://customer.com/stuff.suse.com/x86', + installer_updates: false, + scc_id: nil ) end @@ -297,6 +308,10 @@ def scc it 'other repos without auth_tokens persist' do expect { other_repo_without_token.reload }.not_to raise_error end + + it 'custom repos without auth_tokens persist' do + expect { custom_repo.reload }.not_to raise_error + end end describe '#export' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3c958ac15..3c49cd6fa 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -146,7 +146,7 @@ expect(friendly_id).to eq('my-repo-100000') end - it 'appends with unicode within the chain' do + it 'appends with unicode within the chain', :skip_sqlite do create(:repository, friendly_id: 'my-repo') create(:repository, friendly_id: 'my-repö-1') expect(friendly_id).to eq('my-repo-2') @@ -165,13 +165,13 @@ expect(friendly_id).to eq('dümmy-repö') end - it 'appends with unicode within the chain' do + it 'appends with unicode within the chain', :skip_sqlite do create(:repository, friendly_id: 'dummy-repo') create(:repository, friendly_id: 'dümmy-repö-1') expect(friendly_id).to eq('dümmy-repö-2') end - it 'correctly appends' do + it 'correctly appends', :skip_sqlite do create(:repository, friendly_id: 'dummy-repo') create(:repository, friendly_id: 'dummy-repo-1') expect(friendly_id).to eq('dümmy-repö-2') diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 5d71fea9f..9c8bffe57 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -58,6 +58,12 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + # Skipping some tests when running with (experimental) sqlite backend. + # Some tests / code parts are using specific mysql behavior + if ActiveRecord::Base.connection.adapter_name == 'SQLite' + config.filter_run_excluding skip_sqlite: true + end end Shoulda::Matchers.configure do |config| diff --git a/spec/requests/api/connect/base_controller_spec.rb b/spec/requests/api/connect/base_controller_spec.rb index 70ef9aaff..8832909ea 100644 --- a/spec/requests/api/connect/base_controller_spec.rb +++ b/spec/requests/api/connect/base_controller_spec.rb @@ -55,15 +55,6 @@ def require_product end end - shared_examples 'updates the system token' do - it 'updates the system token' do - allow(SecureRandom).to receive(:uuid).and_return(new_system_token) - - expect { get :service, params: { id: 1 } } - .to change { system.reload.system_token } - .from(current_system_token).to(new_system_token) - end - end shared_examples "does not update the old system's token" do it 'does not update the system token' do @@ -74,8 +65,6 @@ def require_product shared_examples 'creates a duplicate system' do it 'creates a new System (duplicate)' do - allow(SecureRandom).to receive(:uuid).and_return(new_system_token) - expect { get :service, params: { id: 1 } } .to change { System.get_by_credentials(system.login, system.password).count } .by(1) @@ -85,7 +74,6 @@ def require_product expect(duplicate_system).not_to eq(system) expect(duplicate_system.activations.count).to eq(system.activations.count) expect(duplicate_system.system_token).not_to eq(system.system_token) - expect(duplicate_system.system_token).to eq(new_system_token) end end @@ -182,8 +170,7 @@ def require_product let(:system) { create(:system, hostname: 'system') } include_examples 'does not create a duplicate system' - include_examples 'updates the system token' - include_examples 'responds with a new token' + include_examples "does not update the old system's token" end context 'when the system has a token and the header matches it' do @@ -193,8 +180,7 @@ def require_product let(:system) { create(:system, hostname: 'system', system_token: current_system_token) } include_examples 'does not create a duplicate system' - include_examples 'updates the system token' - include_examples 'responds with a new token' + include_examples "does not update the old system's token" end context 'when the system has a token and the header is blank' do @@ -208,7 +194,6 @@ def require_product include_examples "does not update the old system's token" include_examples 'creates a duplicate system' - include_examples 'responds with a new token' end context 'when the system has a token and the header does not match it' do diff --git a/spec/requests/api/connect/v3/systems/activations_controller_spec.rb b/spec/requests/api/connect/v3/systems/activations_controller_spec.rb index a4ff0a562..6a62ee2bd 100644 --- a/spec/requests/api/connect/v3/systems/activations_controller_spec.rb +++ b/spec/requests/api/connect/v3/systems/activations_controller_spec.rb @@ -69,5 +69,28 @@ expect(system.scc_synced_at).to be_nil end end + + context 'system token header' do + context 'when system token header is present in request' do + let(:token_headers) do + authenticated_headers.merge({ 'System-Token' => 'some_token' }) + end + + it 'sets system token in response headers' do + get url, headers: token_headers + expect(response.code).to eq '200' + expect(response.headers).to include('System-Token') + expect(response.headers['System-Token']).not_to be_nil + expect(response.headers['System-Token']).not_to be_empty + end + + it 'does not set system token header if no system token header in request' do + get url, headers: authenticated_headers + + expect(response.code).to eq '200' + expect(response.headers).not_to include('System-Token') + end + end + end end end diff --git a/spec/requests/api/connect/v3/systems/products_controller_spec.rb b/spec/requests/api/connect/v3/systems/products_controller_spec.rb index 60c1bbb0d..9f6c47916 100644 --- a/spec/requests/api/connect/v3/systems/products_controller_spec.rb +++ b/spec/requests/api/connect/v3/systems/products_controller_spec.rb @@ -135,6 +135,26 @@ end end + shared_context 'activate with token in request headers' do + let(:payload) do + { + identifier: product.identifier, + version: product.version, + arch: product.arch, + token: regcode + } + end + + before { post url, headers: { 'System-Token' => 'existing_token' }.merge(headers), params: payload } + subject do + Struct.new(:body, :code, :headers).new( + JSON.parse(response.body, symbolize_names: true), + response.status, + response.headers + ) + end + end + context 'unknown subscription' do include_context 'with subscriptions' let(:regcode) { 'NOT-EXISTING-SUBSCRIPTION' } @@ -173,6 +193,17 @@ expect(activation.product).to eq(product) end end + + context 'token update after activation is success' do + let(:subscription) { create :subscription, :with_products } + let(:product) { subscription.products.first } + let(:regcode) { subscription.regcode } + + include_context 'activate with token in request headers' + its(:code) { is_expected.to eq(201) } + its(:headers) { is_expected.to include('System-Token') } + its(:headers['System-Token']) { is_expected.not_to eq('existing_token') } + end end end @@ -225,6 +256,18 @@ its(:body) { is_expected.to eq(serialized_json) } end + describe 'response header should contain token' do + subject { response } + + let(:token_headers) do + headers.merge({ 'System-Token' => 'some_token' }) + end + + before { get url, headers: token_headers, params: payload } + its(:code) { is_expected.to eq('200') } + its(:headers) { is_expected.to include('System-Token') } + end + describe 'response with "-" in version' do subject { response } @@ -246,6 +289,33 @@ end end + context 'when SLE Micro product is activated' do + let(:system) { FactoryBot.create(:system, :with_activated_product_sle_micro) } + let(:product) { FactoryBot.create(:product, :product_sles, :with_mirrored_repositories) } + let(:payload) do + { + identifier: product.identifier, + version: product.version, + arch: system.products.first.arch + } + end + let(:serialized_json) do + V3::ProductSerializer.new( + product, + base_url: URI::HTTP.build({ scheme: response.request.scheme, host: response.request.host }).to_s + ).to_json + end + + describe 'response' do + subject { response } + + before { get url, headers: headers, params: payload } + + its(:code) { is_expected.to eq('200') } + its(:body) { is_expected.to eq(serialized_json) } + end + end + context 'with eula_url' do subject { response } @@ -312,6 +382,18 @@ system.reload end + it 'calls refresh_system_token after upgrade action when system token header is present' do + put url, headers: headers.merge('System-Token' => 'test_token'), params: payload + expect(response.code).to eq('201') + expect(response.headers).to include('System-Token') + expect(response.headers['System-Token']).not_to eq('test_token') + end + + it 'No update in token after upgrade action when system token header is absent' do + put url, headers: headers, params: payload + expect(response.code).to eq('201') + expect(response.headers).not_to include('System-Token') + end context 'new product' do its(:code) { is_expected.to eq('201') } its(:body) { is_expected.to eq(serialized_json) } diff --git a/spec/requests/api/connect/v3/systems/systems_controller_spec.rb b/spec/requests/api/connect/v3/systems/systems_controller_spec.rb index 95eede620..91f73d4f4 100644 --- a/spec/requests/api/connect/v3/systems/systems_controller_spec.rb +++ b/spec/requests/api/connect/v3/systems/systems_controller_spec.rb @@ -125,6 +125,25 @@ expect(system.reload.system_information_hash[:user_agent]).to be_nil end end + + context 'response header should contain token' do + let(:headers) { auth_header.merge('System-Token': 'existing-token') } + + it 'contains refreshed token in response' do + update_action + expect(response.headers).to include('System-Token') + expect(response.headers['System-Token']).not_to equal('existing-token') + end + end + + context 'response header should not contain token' do + let(:headers) { auth_header } + + it 'contains refreshed token in response' do + update_action + expect(response.headers).not_to include('System-Token') + end + end end describe '#deregister' do diff --git a/spec/requests/api/connect/v4/systems/products_controller_spec.rb b/spec/requests/api/connect/v4/systems/products_controller_spec.rb index 8bf9a64f4..6f960a941 100644 --- a/spec/requests/api/connect/v4/systems/products_controller_spec.rb +++ b/spec/requests/api/connect/v4/systems/products_controller_spec.rb @@ -140,4 +140,77 @@ end end end + + describe 'system token refresh' do + let(:system) { FactoryBot.create(:system, :with_activated_base_product) } + let(:headers) { auth_header.merge(version_header).merge('System-Token' => 'existing_token') } + + context 'token refresh for destroy action' do + let(:product) { FactoryBot.create(:product, :extension, :with_mirrored_repositories, :activated, system: system) } + let(:payload) { { identifier: product.identifier, version: product.version, arch: product.arch } } + + it 'refreshes system token when System-Token header is present' do + delete connect_systems_products_url, + headers: headers, + params: payload + + expect(response.status).to eq(200) + expect(response.headers).to include('System-Token') + expect(response.headers['System-Token']).not_to eq('existing_token') + end + + it 'does not refresh token when System-Token header is absent' do + headers_without_token = auth_header.merge(version_header) + expect_any_instance_of(described_class).not_to receive(:refresh_system_token) + + delete connect_systems_products_url, + headers: headers_without_token, + params: payload + + expect(response.status).to eq(200) + expect(response.headers).not_to include('System-Token') + end + end + + context 'token refresh for synchronize action' do + let(:path) { '/connect/systems/products/synchronize' } + + it 'refreshes system token when System-Token header is present' do + params = system.products.map do |product| + { + identifier: product.identifier, + version: product.version, + arch: product.arch, + release_type: product.release_type + } + end + post path, + params: { products: params }, + headers: headers + + expect(response.status).to eq(200) + expect(response.headers).to include('System-Token') + expect(response.headers['System-Token']).not_to eq('existing_token') + end + + it 'does not refresh token when System-Token header is absent' do + headers_without_token = auth_header.merge(version_header) + + params = system.products.map do |product| + { + identifier: product.identifier, + version: product.version, + arch: product.arch, + release_type: product.release_type + } + end + post path, + params: { products: params }, + headers: headers_without_token + + expect(response.status).to eq(200) + expect(response.headers).not_to include('System-Token') + end + end + end end diff --git a/spec/services/repository_service_spec.rb b/spec/services/repository_service_spec.rb index 1387ea3b0..0035395b5 100644 --- a/spec/services/repository_service_spec.rb +++ b/spec/services/repository_service_spec.rb @@ -88,7 +88,7 @@ it('is not custom') { expect(repository.custom?).to eq(true) } - context 'already existing repositories with changing URL' do + context 'already existing repositories with changing URL', :skip_sqlite do subject(:repository) do service.create_repository!(product, url, attributes, custom: custom).reload url = 'https://foo.bar.com/bar/foo'