diff --git a/app/controllers/v2/reports_controller.rb b/app/controllers/v2/reports_controller.rb new file mode 100644 index 000000000..ec46703de --- /dev/null +++ b/app/controllers/v2/reports_controller.rb @@ -0,0 +1,34 @@ +# 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 + end +end diff --git a/app/models/v2/policy.rb b/app/models/v2/policy.rb index dfba00be5..98cf3ca1f 100644 --- a/app/models/v2/policy.rb +++ b/app/models/v2/policy.rb @@ -9,6 +9,7 @@ class Policy < ApplicationRecord belongs_to :profile, class_name: 'V2::Profile' has_one :security_guide, through: :profile, class_name: 'V2::SecurityGuide' + has_one :report, class_name: 'V2::Report', foreign_key: :id has_many :tailorings, class_name: 'V2::Tailoring', dependent: :destroy has_many :tailoring_rules, through: :tailorings, class_name: 'V2::TailoringRule', dependent: :destroy has_many :rules, through: :tailoring_rules, class_name: 'V2::Rule' diff --git a/app/models/v2/report.rb b/app/models/v2/report.rb index e07d0b00a..5dafe34ba 100644 --- a/app/models/v2/report.rb +++ b/app/models/v2/report.rb @@ -7,9 +7,45 @@ 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 + belongs_to :account, class_name: 'Account' + has_many :tailorings, class_name: 'V2::Tailoring', through: :policy - has_many :systems, class_name: 'V2::System', through: :tailorings + has_many :test_results, class_name: 'V2::TestResult', dependent: nil, through: :tailorings + 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 + policy.os_major_version + end + + def ref_id + policy.ref_id + end + + def profile_title + policy.profile_title + end + + # def account_id + # policy.account_id + # end end end diff --git a/app/models/v2/system.rb b/app/models/v2/system.rb index 226358eab..ff43e8224 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', dependent: nil # TODO: is `dependent: nil` correct? + 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 7d462a093..6b98edbb9 100644 --- a/app/models/v2/tailoring.rb +++ b/app/models/v2/tailoring.rb @@ -39,6 +39,7 @@ class Tailoring < ApplicationRecord 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: nil 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 150c643af..a2fce181f 100644 --- a/app/models/v2/test_result.rb +++ b/app/models/v2/test_result.rb @@ -8,7 +8,9 @@ class TestResult < ApplicationRecord self.primary_key = :id belongs_to :system, optional: true - belongs_to :tailoring + belongs_to :tailoring, class_name: 'V2::Tailoring' + belongs_to :report, class_name: 'V2::Report', optional: true + has_one :policy, through: :tailoring has_one :security_guide, through: :tailoring end diff --git a/app/policies/v2/report_policy.rb b/app/policies/v2/report_policy.rb new file mode 100644 index 000000000..569179590 --- /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? + true # TODO: 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 000000000..16d9247dc --- /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 + # TODO: aggregated_attribute :compliant_system_count, :systems, V2::Report::COMPLIANT_SYSTEM_COUNT + # TODO: aggregated_attribute :test_result_system_count, :systems, V2::Report::TEST_RESULT_SYSTEM_COUNT + # TODO: aggregated_attribute :unsupported_system_count, :systems, V2::Report::UNSUPPORTED_SYSTEM_COUNT + end +end diff --git a/config/routes.rb b/config/routes.rb index 5459b0512..8edaaeafc 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 000000000..5f2071de7 --- /dev/null +++ b/spec/controllers/v2/reports_controller_spec.rb @@ -0,0 +1,98 @@ +# 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, + # TODO: system_count: :system_count, + # TODO: compliant_system_count: :compliant_system_count, + # TODO: test_result_system_count: :test_result_system_count, + # TODO: unsupported_system_count: :unsupported_system_count, + compliance_threshold: :compliance_threshold, + # ssg_version: :ssg_version + } + end + + let(:current_user) { FactoryBot.create(:v2_user, :with_cert_auth) } + 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(:item_count) { 3 } + + # TODO: only creates duplicated records + let(:items) do + item_count.times.map do + test_result = FactoryBot.create( + :v2_test_result, + tailoring: FactoryBot.create( + :v2_tailoring, + policy: FactoryBot.create( + :v2_policy, + :for_tailoring, + account: current_user.account, + supports_minors: [0] + ), + os_minor_version: 0 + ) + ) + + FactoryBot.create_list( + :v2_report, item_count, + account: current_user.account, + policy_id: test_result.tailoring.policy.id + ).map(&:reload) + end + 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 31cfa64df..226bd0884 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 000000000..0d36f2646 --- /dev/null +++ b/spec/factories/report.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :v2_report, class: 'V2::Report' do + to_create do |instance, context| + instance.attributes = V2::Policy.find(context.policy_id).attributes + instance.reload + end + + transient do + policy_id { nil } + end + end +end diff --git a/spec/factories/test_result.rb b/spec/factories/test_result.rb new file mode 100644 index 000000000..35900e5f6 --- /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.before(Time.zone.now) } + end_time { 1.minute.before(Time.zone.now) } + score { 0 } + supported { false } + 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 000000000..d870bb93b --- /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 000000000..ddb5b2398 --- /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]