From 51ccf2da33522129ed02894be46a451dd334ad2e Mon Sep 17 00:00:00 2001 From: Roman Blanco Date: Thu, 11 Apr 2024 10:22:00 +0200 Subject: [PATCH] [wip] --- app/controllers/v2/reports_controller.rb | 38 +++++ app/models/v2/report.rb | 39 +++++- app/models/v2/system.rb | 3 +- app/models/v2/tailoring.rb | 3 +- app/models/v2/test_result.rb | 9 +- app/policies/v2/report_policy.rb | 23 ++++ app/serializers/v2/report_serializer.rb | 17 +++ config/routes.rb | 1 + .../controllers/v2/reports_controller_spec.rb | 79 +++++++++++ spec/factories/policy.rb | 2 +- spec/factories/report.rb | 30 ++++ spec/factories/test_result.rb | 12 ++ .../files/searchable/reports_controller.yaml | 130 ++++++++++++++++++ .../files/sortable/reports_controller.yaml | 36 +++++ 14 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 app/controllers/v2/reports_controller.rb create mode 100644 app/policies/v2/report_policy.rb create mode 100644 app/serializers/v2/report_serializer.rb create mode 100644 spec/controllers/v2/reports_controller_spec.rb create mode 100644 spec/factories/report.rb create mode 100644 spec/factories/test_result.rb create mode 100644 spec/fixtures/files/searchable/reports_controller.yaml create mode 100644 spec/fixtures/files/sortable/reports_controller.yaml diff --git a/app/controllers/v2/reports_controller.rb b/app/controllers/v2/reports_controller.rb new file mode 100644 index 0000000000..bb6b59a553 --- /dev/null +++ b/app/controllers/v2/reports_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module V2 + # API for Reports (rule results) + class ReportsController < ApplicationController + def index + render_json reports + end + permission_for_action :index, Rbac::REPORT_READ + + def show + render_json report + end + permission_for_action :show, Rbac::REPORT_READ + + private + + def reports + @reports ||= authorize(fetch_collection) + end + + def report + @report ||= authorize(expand_resource.find(permitted_params[:id])) + end + + def resource + V2::Report + end + + def serializer + V2::ReportSerializer + end + + def extra_fields + %i[account_id] + end + end +end diff --git a/app/models/v2/report.rb b/app/models/v2/report.rb index e07d0b00a3..73e4d0a3cb 100644 --- a/app/models/v2/report.rb +++ b/app/models/v2/report.rb @@ -7,9 +7,44 @@ class Report < ApplicationRecord self.table_name = :reports self.primary_key = :id + SYSTEM_COUNT = lambda do + AN::NamedFunction.new( + 'COUNT', [V2::System.arel_table[:id]] + ).filter(Pundit.policy_scope(User.current, V2::System).arel.ast.cores.first.wheres.first) + end + # To prevent an autojoin with itself, there should not be an inverse relationship specified belongs_to :policy, class_name: 'V2::Policy', foreign_key: :id # rubocop:disable Rails/InverseOf - has_many :tailorings, class_name: 'V2::Tailoring', through: :policy - has_many :systems, class_name: 'V2::System', through: :tailorings + belongs_to :account + + belongs_to :profile, class_name: 'V2::Profile' + has_one :security_guide, through: :profile, class_name: 'V2::SecurityGuide' + has_many :tailorings, class_name: 'V2::Tailoring', foreign_key: :policy_id + has_many :test_results, class_name: 'V2::TestResult', dependent: nil, through: :tailorings + has_many :policy_systems, class_name: 'V2::PolicySystem', foreign_key: :policy_id + has_many :systems, class_name: 'V2::System', through: :test_results + + sortable_by :title + sortable_by :os_major_version + sortable_by :total_host_count + sortable_by :business_objective + sortable_by :compliance_threshold + + searchable_by :title, %i[like unlike eq ne in notin] + + validates :account, presence: true + # TODO: validates :test_result, presense: true + + def os_major_version + attributes['security_guide__os_major_version'] || security_guide.os_major_version + end + + def ref_id + attributes['profile__ref_id'] || profile.ref_id + end + + def profile_title + attributes['profile__title'] || profile.title + end end end diff --git a/app/models/v2/system.rb b/app/models/v2/system.rb index 226358eab2..60d522ae48 100644 --- a/app/models/v2/system.rb +++ b/app/models/v2/system.rb @@ -12,7 +12,8 @@ class System < ApplicationRecord # rubocop:enable Rails/InverseOf has_many :policy_systems, class_name: 'V2::PolicySystem', dependent: nil has_many :policies, through: :policy_systems - has_many :reports, class_name: 'V2::Report', dependent: nil + has_many :reports, class_name: 'V2::Report', through: :policies + has_many :test_results, class_name: 'V2::TestResult' OS_VERSION = AN::InfixOperation.new('->', Host.arel_table[:system_profile], AN::Quoted.new('operating_system')) OS_MINOR_VERSION = AN::InfixOperation.new('->', OS_VERSION, AN::Quoted.new('minor')).as('os_minor_version') diff --git a/app/models/v2/tailoring.rb b/app/models/v2/tailoring.rb index 7d462a0931..72cc8770fd 100644 --- a/app/models/v2/tailoring.rb +++ b/app/models/v2/tailoring.rb @@ -33,12 +33,13 @@ class Tailoring < ApplicationRecord belongs_to :profile, class_name: 'V2::Profile' has_one :security_guide, through: :profile, class_name: 'V2::SecurityGuide' has_one :account, through: :policy, class_name: 'Account' + has_one :report, class_name: 'V2::Report', through: :policy, dependent: nil has_many :tailoring_rules, class_name: 'V2::TailoringRule', dependent: :destroy, inverse_of: :tailoring has_many :rules, class_name: 'V2::Rule', through: :tailoring_rules - has_many :reports, class_name: 'V2::Report', dependent: nil + has_many :test_results, class_name: 'V2::TestResult', dependent: :destroy searchable_by :os_minor_version, %i[eq ne] diff --git a/app/models/v2/test_result.rb b/app/models/v2/test_result.rb index 150c643afa..cdd560b098 100644 --- a/app/models/v2/test_result.rb +++ b/app/models/v2/test_result.rb @@ -7,9 +7,10 @@ class TestResult < ApplicationRecord self.table_name = :v2_test_results self.primary_key = :id - belongs_to :system, optional: true - belongs_to :tailoring - has_one :policy, through: :tailoring - has_one :security_guide, through: :tailoring + belongs_to :system, class_name: 'V2::System', optional: true + belongs_to :tailoring, class_name: 'V2::Tailoring' + + has_one :policy, class_name: 'V2::Policy', through: :tailoring + has_one :security_guide, class_name: 'V2::SecurityGuide', through: :tailoring end end diff --git a/app/policies/v2/report_policy.rb b/app/policies/v2/report_policy.rb new file mode 100644 index 0000000000..e6fce431d0 --- /dev/null +++ b/app/policies/v2/report_policy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module V2 + class ReportPolicy < V2::ApplicationPolicy + def index? + true # FIXME: this is handled in scoping + end + + def show? + match_account? + end + + # Only show Reports in our user account + class Scope < V2::ApplicationPolicy::Scope + def resolve + return scope.where('1=0') if user&.account_id.blank? + + scope.where(account_id: user.account_id) + end + end + end +end + diff --git a/app/serializers/v2/report_serializer.rb b/app/serializers/v2/report_serializer.rb new file mode 100644 index 0000000000..4add860fff --- /dev/null +++ b/app/serializers/v2/report_serializer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module V2 + # JSON serialization for Reports + class ReportSerializer < V2::ApplicationSerializer + attributes :title, :description, :business_objective, :compliance_threshold + + derived_attribute :os_major_version, security_guide: [:os_major_version] + derived_attribute :profile_title, profile: [:title] + derived_attribute :ref_id, profile: [:ref_id] + + aggregated_attribute :system_count, :systems, V2::Report::SYSTEM_COUNT + # aggregated_attribute :test_result_system_count, :test_results, V2::Report::TEST_RESULT_SYSTEM_COUNT + # aggregated_attribute :compliant_system_count, :test_results, V2::Report::COMPLIANT_SYSTEM_COUNT + # aggregated_attribute :unsupported_system_count, :test_results, V2::Report::UNSUPPORTED_SYSTEM_COUNT + end +end diff --git a/config/routes.rb b/config/routes.rb index 5459b0512a..8edaaeafcd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,6 +47,7 @@ def draw_routes(prefix) resources :systems, only: [:index, :show] do resources :policies, only: [:index], parents: [:systems] end + resources :reports, only: [:index, :show] end end diff --git a/spec/controllers/v2/reports_controller_spec.rb b/spec/controllers/v2/reports_controller_spec.rb new file mode 100644 index 0000000000..841d83424a --- /dev/null +++ b/spec/controllers/v2/reports_controller_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe V2::ReportsController do + before do + stub_rbac_permissions( + Rbac::INVENTORY_HOSTS_READ, + Rbac::SYSTEM_READ, + Rbac::REPORT_READ + ) + end + + let(:attributes) do + { + title: :title, + os_major_version: :os_major_version, + ref_id: :ref_id, + description: :description, + profile_title: :profile_title, + business_objective: :business_objective, + system_count: -> { 0 }, # TODO + # TODO: compliant_system_count: -> { 0 }, + # TODO: test_result_system_count: -> { 0 }, + # TODO: unsupported_system_count: -> { 0 }, + compliance_threshold: :compliance_threshold + } + end + + let(:current_user) { FactoryBot.create(:v2_user) } + let(:rbac_allowed?) { true } + + before do + request.headers['X-RH-IDENTITY'] = current_user.account.identity_header.raw + allow(StrongerParameters::InvalidValue).to receive(:new) { |value, _| value.to_sym } + allow(controller).to receive(:rbac_allowed?).and_return(rbac_allowed?) + end + + context '/reports' do + describe 'GET index' do + let(:extra_params) { { account: current_user.account } } + let(:parents) { nil } + + + let(:items) do + FactoryBot.create_list( + :v2_report, item_count, + account: current_user.account, + ).sort_by(&:id) + end + + it_behaves_like 'collection' + include_examples 'with metadata' + it_behaves_like 'paginable' + it_behaves_like 'sortable' + it_behaves_like 'searchable' + end + + describe 'GET show' do + let(:extra_params) { { account: current_user.account, id: item.id } } + let(:parent) { nil } + + let(:policy) do + FactoryBot.create( + :v2_policy, + :for_tailoring, + account: current_user.account, + supports_minors: [0] + ) + end + let(:tailoring) { FactoryBot.create(:v2_tailoring, policy: policy, os_minor_version: 0) } + let(:test_result) { FactoryBot.create(:v2_test_result, tailoring: tailoring) } + + let(:item) { FactoryBot.create(:v2_report, account: current_user.account, policy_id: test_result.tailoring.policy.id) } + + it_behaves_like 'individual' + end + end +end diff --git a/spec/factories/policy.rb b/spec/factories/policy.rb index 31cfa64dfb..226bd08840 100644 --- a/spec/factories/policy.rb +++ b/spec/factories/policy.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :v2_policy, class: 'V2::Policy' do - account { association(:v2_account) } + account { association :v2_account } title { Faker::Lorem.sentence } description { Faker::Lorem.paragraph } profile { association :v2_profile, os_major_version: os_major_version, supports_minors: supports_minors } diff --git a/spec/factories/report.rb b/spec/factories/report.rb new file mode 100644 index 0000000000..5976a1bdbc --- /dev/null +++ b/spec/factories/report.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :v2_report, class: 'V2::Report' do + before(:create) do |instance, context| + policy = FactoryBot.create( + :v2_policy, + :for_tailoring, + account: context.account, + supports_minors: [0] + ) + + FactoryBot.create( + :v2_test_result, + tailoring: FactoryBot.create( + :v2_tailoring, + policy: policy, + os_minor_version: 0 + ) + ) + + instance.id = policy.id + end + + to_create do |instance, context| + instance.attributes = V2::Policy.find(instance.id).attributes + instance.reload + end + end +end diff --git a/spec/factories/test_result.rb b/spec/factories/test_result.rb new file mode 100644 index 0000000000..ceed89f8d0 --- /dev/null +++ b/spec/factories/test_result.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :v2_test_result, class: 'V2::TestResult' do + tailoring { association :v2_tailoring } + system { association :system } + start_time { 5.minute.ago } + end_time { 1.minute.ago } + score { SecureRandom.rand(98) + 1 } + supported { true } + end +end diff --git a/spec/fixtures/files/searchable/reports_controller.yaml b/spec/fixtures/files/searchable/reports_controller.yaml new file mode 100644 index 0000000000..d870bb93b2 --- /dev/null +++ b/spec/fixtures/files/searchable/reports_controller.yaml @@ -0,0 +1,130 @@ +--- + +- :name: "equality search by title" + :entities: + :found: + - :factory: :v2_report + :title: searched title + :system_id: ${system_id} + :os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: not this title + :system_id: ${system_id} + :os_major_version: 8 + :query: (title = "searched title") +- :name: "non-equality search by title" + :entities: + :found: + - :factory: :v2_report + :title: not this title + :system_id: ${system_id} + :os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: searched title + :system_id: ${system_id} + :os_major_version: 8 + :query: (title != "searched title") +- :name: "in search by title" + :entities: + :found: + - :factory: :v2_report + :title: searched title + :system_id: ${system_id} + :os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: not this title + :system_id: ${system_id} + :os_major_version: 8 + :query: (title ^ "searched title") +- :name: "not-in search by title" + :entities: + :found: + - :factory: :v2_report + :title: not this title + :system_id: ${system_id} + :os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: searched title + :system_id: ${system_id} + :os_major_version: 8 + :query: (title !^ "searched title") +- :name: "like search by title" + :entities: + :found: + - :factory: :v2_report + :title: searched title + :system_id: ${system_id} + :os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: not this title + :system_id: ${system_id} + :os_major_version: 8 + :query: (title ~ "searched title") +- :name: "unlike search by title" + :entities: + :found: + - :factory: :v2_report + :title: not this title + :system_id: ${system_id} + :os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: searched title + :system_id: ${system_id} + :os_major_version: 8 + :query: (title !~ "searched title") +- :name: "equality search by os_major_version" + :entities: + :found: + - :factory: :v2_report + :title: searched title + :os_major_version: 7 + :not_found: + - :factory: :v2_report + :title: not this title + os_major_version: 8 + :query: (os_major_version = 7) +- :name: "non-equality search by os_major_version" + :entities: + :found: + - :factory: :v2_report + :title: not this title + os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: searched title + os_major_version: 7 + :query: (os_major_version != 7) +- :name: "in search by os_major_version" + :entities: + :found: + - :factory: :v2_report + :title: searched title + :os_major_version: 7 + - :factory: :v2_report + :title: searched title + :os_major_version: 9 + :not_found: + - :factory: :v2_report + :title: not this title + os_major_version: 8 + :query: (os_major_version ^ "7 9") +- :name: "not-in search by os_major_version" + :entities: + :found: + - :factory: :v2_report + :title: not this title + os_major_version: 8 + :not_found: + - :factory: :v2_report + :title: searched title + :os_major_version: 7 + - :factory: :v2_report + :title: searched title + :os_major_version: 9 + :query: (os_major_version !^ "7 9") diff --git a/spec/fixtures/files/sortable/reports_controller.yaml b/spec/fixtures/files/sortable/reports_controller.yaml new file mode 100644 index 0000000000..ddb5b23986 --- /dev/null +++ b/spec/fixtures/files/sortable/reports_controller.yaml @@ -0,0 +1,36 @@ +:entries: + - :factory: :v2_report + :title: 'aba' + :business_objective: 'aba' + :compliance_threshold: 90 + :os_major_version: ${system_9.os_major_version} + + - :factory: :v2_report + :title: 'bac' + :business_objective: 'bac' + :compliance_threshold: 85 + :os_major_version: ${system_8.os_major_version} + + - :factory: :v2_report + :title: 'aab' + :business_objective: 'aab' + :compliance_threshold: 90 + :os_major_version: ${system_7.os_major_version} + +:queries: + - :sort_by: + - 'title' + :result: [0, 1, 2] + - :sort_by: + - 'business_objective' + :result: [0, 1, 2] + - :sort_by: + - 'compliance_threshold' + :result: [0, 1, 2] + - :sort_by: + - 'os_major_version' + :result: [0, 1, 2] + - :sort_by: + - 'os_major_version' + - 'compliance_threshold' + :result: [0, 1, 2]