diff --git a/app/helpers/foreman_rh_cloud_permissions_helper.rb b/app/helpers/foreman_rh_cloud_permissions_helper.rb new file mode 100644 index 000000000..0d7993023 --- /dev/null +++ b/app/helpers/foreman_rh_cloud_permissions_helper.rb @@ -0,0 +1,67 @@ +module ForemanRhCloudPermissionsHelper + # Mapping from Foreman permissions to Insights permissions + # Based on the permission mapping table: + # + # Foreman Permission | Insights Permissions | Paths + # ---------------------|---------------------------------------------------|---------------------------------- + # view_vulnerability | inventory:hosts:read | GET /api/inventory/v1/hosts(/*) + # view_vulnerability | vulnerability:vulnerability_results:read | GET /api/vulnerability/v1/* + # | vulnerability:system.opt_out:read | POST /api/vulnerability/v1/vulnerabilities/cves + # | vulnerability:report_and_export:read | + # | vulnerability:advanced_report:read | + # edit_vulnerability | vulnerability:system.cve.status:write | PATCH /api/vulnerability/v1/status + # edit_vulnerability | vulnerability:cve.business_risk_and_status:write | PATCH /api/vulnerability/v1/cves/status + # | | PATCH /api/vulnerability/v1/cves/business_risk + # edit_vulnerability | vulnerability:system.opt_out:write | PATCH /api/vulnerability/v1/systems/opt_out + # + # view_advisor | advisor:recommendation-results:read | GET /api/insights/v1/* + # | advisor:exports:read | + # edit_advisor | advisor:disable-recommendations:write | POST /api/insights/v1/ack/ + # | | DELETE /api/insights/v1/ack/{rule_id}/ + # | | POST /api/insights/v1/hostack/ + # | | DELETE /api/insights/v1/hostack/{id}/ + # | | POST /api/insights/v1/rule/{rule_id}/unack_hosts/ + # + RH_CLOUD_TO_INSIGHTS_PERMISSIONS = { + view_vulnerability: [ + 'inventory:hosts:read', + 'vulnerability:vulnerability_results:read', + 'vulnerability:system.opt_out:read', + 'vulnerability:report_and_export:read', + 'vulnerability:advanced_report:read', + ], + edit_vulnerability: [ + 'vulnerability:system.cve.status:write', + 'vulnerability:cve.business_risk_and_status:write', + 'vulnerability:system.opt_out:write', + ], + view_advisor: [ + 'advisor:recommendation-results:read', + 'advisor:exports:read', + ], + edit_advisor: [ + 'advisor:disable-recommendations:write', + ], + }.freeze + + # Returns user permissions in Chrome API format for Insights applications + # @return [Array] Array of permission objects with :permission and :resourceDefinitions keys + def insights_user_permissions + return [] unless User.current + + permissions = [] + + RH_CLOUD_TO_INSIGHTS_PERMISSIONS.each do |foreman_permission, insights_permissions| + next unless User.current.can?(foreman_permission) + + insights_permissions.each do |insights_permission| + permissions << { + permission: insights_permission, + resourceDefinitions: [], + } + end + end + + permissions + end +end diff --git a/app/services/foreman_rh_cloud/insights_api_forwarder.rb b/app/services/foreman_rh_cloud/insights_api_forwarder.rb index 73aa2f141..996b0c9ff 100644 --- a/app/services/foreman_rh_cloud/insights_api_forwarder.rb +++ b/app/services/foreman_rh_cloud/insights_api_forwarder.rb @@ -4,17 +4,125 @@ module ForemanRhCloud class InsightsApiForwarder include ForemanRhCloud::CertAuth + # Permission mapping for API paths: + # + # Foreman Permission | Paths + # ---------------------|-------------------------------------------------- + # view_vulnerability | GET /api/inventory/v1/hosts(/*) + # view_vulnerability | GET /api/vulnerability/v1/* + # | POST /api/vulnerability/v1/vulnerabilities/cves + # edit_vulnerability | PATCH /api/vulnerability/v1/status + # edit_vulnerability | PATCH /api/vulnerability/v1/cves/status + # | PATCH /api/vulnerability/v1/cves/business_risk + # edit_vulnerability | PATCH /api/vulnerability/v1/systems/opt_out + # + # view_advisor | GET /api/insights/v1/* + # edit_advisor | POST /api/insights/v1/ack/ + # | DELETE /api/insights/v1/ack/{rule_id}/ + # | POST /api/insights/v1/hostack/ + # | DELETE /api/insights/v1/hostack/{id}/ + # | POST /api/insights/v1/rule/{rule_id}/unack_hosts/ + # SCOPED_REQUESTS = [ - { test: %r{api/vulnerability/v1/vulnerabilities/cves}, tag_name: :tags }, + # Inventory hosts - requires view_vulnerability for GET + { + test: %r{api/inventory/v1/hosts(/.*)?$}, + tag_name: :tags, + permissions: { + 'GET' => :view_vulnerability, + }, + }, + # Vulnerability CVEs list - POST requires view_vulnerability (for filtering) + { + test: %r{api/vulnerability/v1/vulnerabilities/cves}, + tag_name: :tags, + permissions: { + 'POST' => :view_vulnerability, + }, + }, + # Vulnerability status - PATCH requires edit_vulnerability + { + test: %r{api/vulnerability/v1/status}, + tag_name: :tags, + permissions: { + 'PATCH' => :edit_vulnerability, + }, + }, + # CVE status - PATCH requires edit_vulnerability + { + test: %r{api/vulnerability/v1/cves/status}, + tag_name: :tags, + permissions: { + 'PATCH' => :edit_vulnerability, + }, + }, + # CVE business risk - PATCH requires edit_vulnerability + { + test: %r{api/vulnerability/v1/cves/business_risk}, + tag_name: :tags, + permissions: { + 'PATCH' => :edit_vulnerability, + }, + }, + # Systems opt out - PATCH requires edit_vulnerability + { + test: %r{api/vulnerability/v1/systems/opt_out}, + tag_name: :tags, + permissions: { + 'PATCH' => :edit_vulnerability, + }, + }, + # Other vulnerability endpoints (no specific permission enforcement, just tagging) { test: %r{api/vulnerability/v1/dashbar}, tag_name: :tags }, { test: %r{api/vulnerability/v1/cves/[^/]+/affected_systems}, tag_name: :tags }, { test: %r{api/vulnerability/v1/systems/[^/]+/cves}, tag_name: :tags }, - { test: %r{api/insights/.*}, tag_name: :tags }, + # Advisor ack endpoints - POST/DELETE require edit_advisor + { + test: %r{api/insights/v1/ack(/[^/]*)?$}, + tag_name: :tags, + permissions: { + 'POST' => :edit_advisor, + 'DELETE' => :edit_advisor, + }, + }, + # Advisor hostack endpoints - POST/DELETE require edit_advisor + { + test: %r{api/insights/v1/hostack(/[^/]*)?$}, + tag_name: :tags, + permissions: { + 'POST' => :edit_advisor, + 'DELETE' => :edit_advisor, + }, + }, + # Advisor rule unack_hosts - POST requires edit_advisor + { + test: %r{api/insights/v1/rule/[^/]+/unack_hosts}, + tag_name: :tags, + permissions: { + 'POST' => :edit_advisor, + }, + }, + # Other Advisor/Insights endpoints - GET requires view_advisor + { + test: %r{api/insights/v1/.*}, + tag_name: :tags, + permissions: { + 'GET' => :view_advisor, + }, + }, + # Other API endpoints (tagging only, no permission enforcement) { test: %r{api/inventory/.*}, tag_name: :tags }, { test: %r{api/tasks/.*}, tag_name: :tags }, ].freeze def forward_request(original_request, path, controller_name, user, organization, location) + # Check permissions before forwarding + permission = required_permission_for(path, original_request.request_method) + if permission && !user&.can?(permission) + logger.warn("User #{user.login} lacks permission #{permission} for #{original_request.request_method} #{path}") + raise ::Foreman::Exception.new(N_("You do not have permission to perform this action")) + end + TagsAuth.new(user, organization, location, logger).update_tag if scope_request?(original_request, path) forward_params = prepare_forward_params(original_request, path, user: user, organization: organization, location: location).to_a @@ -116,5 +224,19 @@ def http_user_agent(original_request) def logger Foreman::Logging.logger('app') end + + # Returns the required permission for the given path and HTTP method + # @param path [String] The request path + # @param http_method [String] The HTTP method (GET, POST, etc.) + # @return [Symbol, nil] The required permission symbol or nil if no permission required + def required_permission_for(path, http_method) + request_pattern = SCOPED_REQUESTS.find { |pattern| pattern[:test].match?(path) } + return nil unless request_pattern + + permissions = request_pattern[:permissions] + return nil unless permissions + + permissions[http_method] + end end end diff --git a/lib/foreman_rh_cloud/plugin.rb b/lib/foreman_rh_cloud/plugin.rb index d32b95b8a..ee0d6959e 100644 --- a/lib/foreman_rh_cloud/plugin.rb +++ b/lib/foreman_rh_cloud/plugin.rb @@ -71,9 +71,37 @@ def self.register :control_organization_insights, 'insights_cloud/settings': [:set_org_parameter] ) + # Insights Vulnerability permissions + permission( + :view_vulnerability, + {}, + :resource_type => 'ForemanRhCloud' + ) + permission( + :edit_vulnerability, + {}, + :resource_type => 'ForemanRhCloud' + ) + # Insights Advisor permissions + permission( + :view_advisor, + {}, + :resource_type => 'ForemanRhCloud' + ) + permission( + :edit_advisor, + {}, + :resource_type => 'ForemanRhCloud' + ) end - plugin_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits, :dispatch_cloud_requests, :control_organization_insights] + # Core RH Cloud permissions for inventory upload and sync + rh_cloud_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits, :dispatch_cloud_requests, :control_organization_insights] + + # Insights application permissions (Vulnerability, Advisor) + insights_permissions = [:view_vulnerability, :edit_vulnerability, :view_advisor, :edit_advisor] + + plugin_permissions = rh_cloud_permissions + insights_permissions role 'ForemanRhCloud', plugin_permissions, 'Role granting permissions to view the hosts inventory, generate a report, upload it to the cloud and download it locally' diff --git a/test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb b/test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb new file mode 100644 index 000000000..6906f448e --- /dev/null +++ b/test/unit/helpers/foreman_rh_cloud_permissions_helper_test.rb @@ -0,0 +1,178 @@ +require 'test_plugin_helper' + +class ForemanRhCloudPermissionsHelperTest < ActionView::TestCase + include ForemanRhCloudPermissionsHelper + + setup do + @user = FactoryBot.create(:user) + User.current = @user + end + + teardown do + User.current = nil + end + + test 'RH_CLOUD_TO_INSIGHTS_PERMISSIONS contains all expected permissions' do + expected_keys = [:view_vulnerability, :edit_vulnerability, :view_advisor, :edit_advisor] + assert_equal expected_keys.sort, RH_CLOUD_TO_INSIGHTS_PERMISSIONS.keys.sort + end + + test 'view_vulnerability maps to correct Insights permissions' do + expected = [ + 'inventory:hosts:read', + 'vulnerability:vulnerability_results:read', + 'vulnerability:system.opt_out:read', + 'vulnerability:report_and_export:read', + 'vulnerability:advanced_report:read', + ] + assert_equal expected, RH_CLOUD_TO_INSIGHTS_PERMISSIONS[:view_vulnerability] + end + + test 'edit_vulnerability maps to correct Insights permissions' do + expected = [ + 'vulnerability:system.cve.status:write', + 'vulnerability:cve.business_risk_and_status:write', + 'vulnerability:system.opt_out:write', + ] + assert_equal expected, RH_CLOUD_TO_INSIGHTS_PERMISSIONS[:edit_vulnerability] + end + + test 'view_advisor maps to correct Insights permissions' do + expected = [ + 'advisor:recommendation-results:read', + 'advisor:exports:read', + ] + assert_equal expected, RH_CLOUD_TO_INSIGHTS_PERMISSIONS[:view_advisor] + end + + test 'edit_advisor maps to correct Insights permissions' do + expected = [ + 'advisor:disable-recommendations:write', + ] + assert_equal expected, RH_CLOUD_TO_INSIGHTS_PERMISSIONS[:edit_advisor] + end + + test 'insights_user_permissions should return empty array when no user' do + User.current = nil + permissions = insights_user_permissions + assert_empty(permissions) + end + + test 'insights_user_permissions should return view permissions when user has view_vulnerability' do + @user.stubs(:can?).with(:view_vulnerability).returns(true) + @user.stubs(:can?).with(:edit_vulnerability).returns(false) + @user.stubs(:can?).with(:view_advisor).returns(false) + @user.stubs(:can?).with(:edit_advisor).returns(false) + + permissions = insights_user_permissions + + assert_equal 5, permissions.length + + permission_strings = permissions.map { |p| p[:permission] } + assert_includes permission_strings, 'inventory:hosts:read' + assert_includes permission_strings, 'vulnerability:vulnerability_results:read' + assert_includes permission_strings, 'vulnerability:system.opt_out:read' + assert_includes permission_strings, 'vulnerability:report_and_export:read' + assert_includes permission_strings, 'vulnerability:advanced_report:read' + end + + test 'insights_user_permissions should return write permissions when user has edit_vulnerability' do + @user.stubs(:can?).with(:view_vulnerability).returns(false) + @user.stubs(:can?).with(:edit_vulnerability).returns(true) + @user.stubs(:can?).with(:view_advisor).returns(false) + @user.stubs(:can?).with(:edit_advisor).returns(false) + + permissions = insights_user_permissions + + assert_equal 3, permissions.length + + permission_strings = permissions.map { |p| p[:permission] } + assert_includes permission_strings, 'vulnerability:system.cve.status:write' + assert_includes permission_strings, 'vulnerability:cve.business_risk_and_status:write' + assert_includes permission_strings, 'vulnerability:system.opt_out:write' + end + + test 'insights_user_permissions should return all vulnerability permissions when user has both view and edit' do + @user.stubs(:can?).with(:view_vulnerability).returns(true) + @user.stubs(:can?).with(:edit_vulnerability).returns(true) + @user.stubs(:can?).with(:view_advisor).returns(false) + @user.stubs(:can?).with(:edit_advisor).returns(false) + + permissions = insights_user_permissions + + assert_equal 8, permissions.length + + permission_strings = permissions.map { |p| p[:permission] } + # View permissions + assert_includes permission_strings, 'inventory:hosts:read' + assert_includes permission_strings, 'vulnerability:vulnerability_results:read' + # Write permissions + assert_includes permission_strings, 'vulnerability:system.cve.status:write' + assert_includes permission_strings, 'vulnerability:cve.business_risk_and_status:write' + end + + test 'insights_user_permissions should return advisor view permissions when user has view_advisor' do + @user.stubs(:can?).with(:view_vulnerability).returns(false) + @user.stubs(:can?).with(:edit_vulnerability).returns(false) + @user.stubs(:can?).with(:view_advisor).returns(true) + @user.stubs(:can?).with(:edit_advisor).returns(false) + + permissions = insights_user_permissions + + assert_equal 2, permissions.length + + permission_strings = permissions.map { |p| p[:permission] } + assert_includes permission_strings, 'advisor:recommendation-results:read' + assert_includes permission_strings, 'advisor:exports:read' + end + + test 'insights_user_permissions should return advisor write permissions when user has edit_advisor' do + @user.stubs(:can?).with(:view_vulnerability).returns(false) + @user.stubs(:can?).with(:edit_vulnerability).returns(false) + @user.stubs(:can?).with(:view_advisor).returns(false) + @user.stubs(:can?).with(:edit_advisor).returns(true) + + permissions = insights_user_permissions + + assert_equal 1, permissions.length + + permission_strings = permissions.map { |p| p[:permission] } + assert_includes permission_strings, 'advisor:disable-recommendations:write' + end + + test 'insights_user_permissions should return all permissions when user has all' do + @user.stubs(:can?).with(:view_vulnerability).returns(true) + @user.stubs(:can?).with(:edit_vulnerability).returns(true) + @user.stubs(:can?).with(:view_advisor).returns(true) + @user.stubs(:can?).with(:edit_advisor).returns(true) + + permissions = insights_user_permissions + + # 5 view_vulnerability + 3 edit_vulnerability + 2 view_advisor + 1 edit_advisor = 11 + assert_equal 11, permissions.length + + permission_strings = permissions.map { |p| p[:permission] } + # Vulnerability + assert_includes permission_strings, 'inventory:hosts:read' + assert_includes permission_strings, 'vulnerability:vulnerability_results:read' + assert_includes permission_strings, 'vulnerability:system.cve.status:write' + # Advisor + assert_includes permission_strings, 'advisor:recommendation-results:read' + assert_includes permission_strings, 'advisor:exports:read' + assert_includes permission_strings, 'advisor:disable-recommendations:write' + end + + test 'insights_user_permissions should have correct Chrome API structure' do + @user.stubs(:can?).with(:view_vulnerability).returns(true) + @user.stubs(:can?).with(:edit_vulnerability).returns(false) + @user.stubs(:can?).with(:view_advisor).returns(false) + @user.stubs(:can?).with(:edit_advisor).returns(false) + + permissions = insights_user_permissions + + permission = permissions.first + assert permission.key?(:permission), 'Permission object must have :permission key' + assert permission.key?(:resourceDefinitions), 'Permission object must have :resourceDefinitions key' + assert_empty(permission[:resourceDefinitions]) + end +end diff --git a/test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb b/test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb index 4adba1046..9379216e2 100644 --- a/test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb +++ b/test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb @@ -214,4 +214,429 @@ def tag_name(param_value) tag_string = CGI.unescape(param_value) tag_string.split('=')[0] end + + # Permission enforcement tests + + # GET /api/inventory/v1/hosts requires view_vulnerability + test 'should allow GET request to inventory hosts when user has view_vulnerability permission' do + user_agent = { :foo => :bar } + params = {} + + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/inventory/v1/hosts', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => user_agent, + 'rack.input' => ::Puma::NullIO.new, + 'action_dispatch.request.query_parameters' => params + ) + + @user.stubs(:can?).with(:view_vulnerability).returns(true) + ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location) + end + + test 'should deny GET request to inventory hosts when user lacks view_vulnerability permission' do + user_agent = { :foo => :bar } + params = {} + + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/inventory/v1/hosts', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => user_agent, + 'rack.input' => ::Puma::NullIO.new, + 'action_dispatch.request.query_parameters' => params + ) + + @user.stubs(:can?).with(:view_vulnerability).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location) + end + end + + # POST /api/vulnerability/v1/vulnerabilities/cves requires view_vulnerability + test 'should allow POST request to vulnerabilities cves when user has view_vulnerability permission' do + post_data = '{"test": "data"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/vulnerabilities/cves', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:view_vulnerability).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/vulnerability/v1/vulnerabilities/cves', 'test_controller', @user, @organization, @location) + end + + test 'should deny POST request to vulnerabilities cves when user lacks view_vulnerability permission' do + post_data = '{"test": "data"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/vulnerabilities/cves', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:view_vulnerability).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/vulnerability/v1/vulnerabilities/cves', 'test_controller', @user, @organization, @location) + end + end + + # PATCH /api/vulnerability/v1/status requires edit_vulnerability + test 'should allow PATCH request to vulnerability status when user has edit_vulnerability permission' do + patch_data = '{"status": "resolved"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/status', + 'REQUEST_METHOD' => 'PATCH', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => patch_data, + "action_dispatch.request.path_parameters" => { :format => "json" } + ) + + @user.stubs(:can?).with(:edit_vulnerability).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/vulnerability/v1/status', 'test_controller', @user, @organization, @location) + end + + test 'should deny PATCH request to vulnerability status when user lacks edit_vulnerability permission' do + patch_data = '{"status": "resolved"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/status', + 'REQUEST_METHOD' => 'PATCH', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => patch_data, + "action_dispatch.request.path_parameters" => { :format => "json" } + ) + + @user.stubs(:can?).with(:edit_vulnerability).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/vulnerability/v1/status', 'test_controller', @user, @organization, @location) + end + end + + # PATCH /api/vulnerability/v1/cves/business_risk requires edit_vulnerability + test 'should allow PATCH request to cves business_risk when user has edit_vulnerability permission' do + patch_data = '{"business_risk": 3}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/cves/business_risk', + 'REQUEST_METHOD' => 'PATCH', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => patch_data, + "action_dispatch.request.path_parameters" => { :format => "json" } + ) + + @user.stubs(:can?).with(:edit_vulnerability).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/vulnerability/v1/cves/business_risk', 'test_controller', @user, @organization, @location) + end + + # PATCH /api/vulnerability/v1/systems/opt_out requires edit_vulnerability + test 'should allow PATCH request to systems opt_out when user has edit_vulnerability permission' do + patch_data = '{"opt_out": true}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/systems/opt_out', + 'REQUEST_METHOD' => 'PATCH', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => patch_data, + "action_dispatch.request.path_parameters" => { :format => "json" } + ) + + @user.stubs(:can?).with(:edit_vulnerability).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/vulnerability/v1/systems/opt_out', 'test_controller', @user, @organization, @location) + end + + # Unprotected endpoints (no permission required) + test 'should allow GET requests to vulnerability dashbar without permission checks' do + user_agent = { :foo => :bar } + params = {} + + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/vulnerability/v1/dashbar', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => user_agent, + 'rack.input' => ::Puma::NullIO.new, + 'action_dispatch.request.query_parameters' => params + ) + + ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/vulnerability/v1/dashbar', 'test_controller', @user, @organization, @location) + end + + # Helper method tests + test 'required_permission_for should return view_vulnerability for inventory hosts GET' do + permission = @forwarder.send(:required_permission_for, 'api/inventory/v1/hosts', 'GET') + assert_equal :view_vulnerability, permission + end + + test 'required_permission_for should return view_vulnerability for vulnerabilities cves POST' do + permission = @forwarder.send(:required_permission_for, 'api/vulnerability/v1/vulnerabilities/cves', 'POST') + assert_equal :view_vulnerability, permission + end + + test 'required_permission_for should return edit_vulnerability for status PATCH' do + permission = @forwarder.send(:required_permission_for, 'api/vulnerability/v1/status', 'PATCH') + assert_equal :edit_vulnerability, permission + end + + test 'required_permission_for should return edit_vulnerability for cves status PATCH' do + permission = @forwarder.send(:required_permission_for, 'api/vulnerability/v1/cves/status', 'PATCH') + assert_equal :edit_vulnerability, permission + end + + test 'required_permission_for should return edit_vulnerability for cves business_risk PATCH' do + permission = @forwarder.send(:required_permission_for, 'api/vulnerability/v1/cves/business_risk', 'PATCH') + assert_equal :edit_vulnerability, permission + end + + test 'required_permission_for should return edit_vulnerability for systems opt_out PATCH' do + permission = @forwarder.send(:required_permission_for, 'api/vulnerability/v1/systems/opt_out', 'PATCH') + assert_equal :edit_vulnerability, permission + end + + test 'required_permission_for should return nil for unprotected endpoint' do + permission = @forwarder.send(:required_permission_for, 'api/vulnerability/v1/dashbar', 'GET') + assert_nil permission + end + + test 'required_permission_for should return nil for unknown endpoint' do + permission = @forwarder.send(:required_permission_for, 'api/unknown/endpoint', 'GET') + assert_nil permission + end + + # Advisor permission tests + + # GET /api/insights/v1/* requires view_advisor + test 'should allow GET request to insights endpoint when user has view_advisor permission' do + user_agent = { :foo => :bar } + params = {} + + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/stats/systems', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => user_agent, + 'rack.input' => ::Puma::NullIO.new, + 'action_dispatch.request.query_parameters' => params + ) + + @user.stubs(:can?).with(:view_advisor).returns(true) + ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/insights/v1/stats/systems', 'test_controller', @user, @organization, @location) + end + + test 'should deny GET request to insights endpoint when user lacks view_advisor permission' do + user_agent = { :foo => :bar } + params = {} + + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/stats/systems', + 'REQUEST_METHOD' => 'GET', + 'HTTP_USER_AGENT' => user_agent, + 'rack.input' => ::Puma::NullIO.new, + 'action_dispatch.request.query_parameters' => params + ) + + @user.stubs(:can?).with(:view_advisor).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/insights/v1/stats/systems', 'test_controller', @user, @organization, @location) + end + end + + # POST /api/insights/v1/ack/ requires edit_advisor + test 'should allow POST request to insights ack when user has edit_advisor permission' do + post_data = '{"rule_id": "test|RULE"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/ack/', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:edit_advisor).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/insights/v1/ack/', 'test_controller', @user, @organization, @location) + end + + test 'should deny POST request to insights ack when user lacks edit_advisor permission' do + post_data = '{"rule_id": "test|RULE"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/ack/', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:edit_advisor).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/insights/v1/ack/', 'test_controller', @user, @organization, @location) + end + end + + # DELETE /api/insights/v1/ack/{rule_id}/ requires edit_advisor + test 'should allow DELETE request to insights ack rule when user has edit_advisor permission' do + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/ack/test_rule_id', + 'REQUEST_METHOD' => 'DELETE', + 'rack.input' => ::Puma::NullIO.new + ) + + @user.stubs(:can?).with(:edit_advisor).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/insights/v1/ack/test_rule_id', 'test_controller', @user, @organization, @location) + end + + test 'should deny DELETE request to insights ack rule when user lacks edit_advisor permission' do + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/ack/test_rule_id', + 'REQUEST_METHOD' => 'DELETE', + 'rack.input' => ::Puma::NullIO.new + ) + + @user.stubs(:can?).with(:edit_advisor).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/insights/v1/ack/test_rule_id', 'test_controller', @user, @organization, @location) + end + end + + # POST /api/insights/v1/hostack/ requires edit_advisor + test 'should allow POST request to insights hostack when user has edit_advisor permission' do + post_data = '{"host_id": "test-uuid", "rule_id": "test|RULE"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/hostack/', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:edit_advisor).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/insights/v1/hostack/', 'test_controller', @user, @organization, @location) + end + + test 'should deny POST request to insights hostack when user lacks edit_advisor permission' do + post_data = '{"host_id": "test-uuid", "rule_id": "test|RULE"}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/hostack/', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:edit_advisor).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/insights/v1/hostack/', 'test_controller', @user, @organization, @location) + end + end + + # DELETE /api/insights/v1/hostack/{id}/ requires edit_advisor + test 'should allow DELETE request to insights hostack id when user has edit_advisor permission' do + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/hostack/12345', + 'REQUEST_METHOD' => 'DELETE', + 'rack.input' => ::Puma::NullIO.new + ) + + @user.stubs(:can?).with(:edit_advisor).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/insights/v1/hostack/12345', 'test_controller', @user, @organization, @location) + end + + test 'should deny DELETE request to insights hostack id when user lacks edit_advisor permission' do + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/hostack/12345', + 'REQUEST_METHOD' => 'DELETE', + 'rack.input' => ::Puma::NullIO.new + ) + + @user.stubs(:can?).with(:edit_advisor).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/insights/v1/hostack/12345', 'test_controller', @user, @organization, @location) + end + end + + # POST /api/insights/v1/rule/{rule_id}/unack_hosts/ requires edit_advisor + test 'should allow POST request to rule unack_hosts when user has edit_advisor permission' do + post_data = '{"host_ids": ["uuid1", "uuid2"]}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/rule/test_rule/unack_hosts', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:edit_advisor).returns(true) + @forwarder.expects(:execute_cloud_request).returns(true) + + @forwarder.forward_request(req, 'api/insights/v1/rule/test_rule/unack_hosts', 'test_controller', @user, @organization, @location) + end + + test 'should deny POST request to rule unack_hosts when user lacks edit_advisor permission' do + post_data = '{"host_ids": ["uuid1", "uuid2"]}' + req = ActionDispatch::Request.new( + 'REQUEST_URI' => '/api/insights/v1/rule/test_rule/unack_hosts', + 'REQUEST_METHOD' => 'POST', + 'rack.input' => ::Puma::NullIO.new, + 'RAW_POST_DATA' => post_data + ) + + @user.stubs(:can?).with(:edit_advisor).returns(false) + + assert_raises(::Foreman::Exception) do + @forwarder.forward_request(req, 'api/insights/v1/rule/test_rule/unack_hosts', 'test_controller', @user, @organization, @location) + end + end + + # Helper method tests for advisor + test 'required_permission_for should return view_advisor for insights GET' do + permission = @forwarder.send(:required_permission_for, 'api/insights/v1/stats/systems', 'GET') + assert_equal :view_advisor, permission + end + + test 'required_permission_for should return edit_advisor for ack POST' do + permission = @forwarder.send(:required_permission_for, 'api/insights/v1/ack/', 'POST') + assert_equal :edit_advisor, permission + end + + test 'required_permission_for should return edit_advisor for ack DELETE' do + permission = @forwarder.send(:required_permission_for, 'api/insights/v1/ack/rule_id', 'DELETE') + assert_equal :edit_advisor, permission + end + + test 'required_permission_for should return edit_advisor for hostack POST' do + permission = @forwarder.send(:required_permission_for, 'api/insights/v1/hostack/', 'POST') + assert_equal :edit_advisor, permission + end + + test 'required_permission_for should return edit_advisor for hostack DELETE' do + permission = @forwarder.send(:required_permission_for, 'api/insights/v1/hostack/123', 'DELETE') + assert_equal :edit_advisor, permission + end + + test 'required_permission_for should return edit_advisor for rule unack_hosts POST' do + permission = @forwarder.send(:required_permission_for, 'api/insights/v1/rule/test_rule/unack_hosts', 'POST') + assert_equal :edit_advisor, permission + end end