Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(APIv2): RHINENG-13519 expose the RuleTree on profiles/tailorings #2247

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/controllers/v2/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ def show
end
permission_for_action :show, Rbac::COMPLIANCE_VIEWER

def rule_tree
render json: profile.rule_tree
end
permission_for_action :rule_tree, Rbac::COMPLIANCE_VIEWER
permitted_params_for_action :rule_tree, id: ID_TYPE.required

private

def profiles
Expand All @@ -30,5 +36,9 @@ def resource
def serializer
V2::ProfileSerializer
end

def extra_fields
%i[security_guide_id]
end
end
end
6 changes: 6 additions & 0 deletions app/controllers/v2/tailorings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ def show
end
permission_for_action :show, Rbac::POLICY_READ

def rule_tree
render json: tailoring.rule_tree
end
permission_for_action :rule_tree, Rbac::COMPLIANCE_VIEWER
permitted_params_for_action :rule_tree, id: ID_TYPE.required

def create
# Look up the latest Profile supporting the given OS minor version
new_tailoring = V2::Tailoring.for_policy(policy, permitted_params[:os_minor_version])
Expand Down
25 changes: 25 additions & 0 deletions app/models/concerns/v2/rule_tree.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module V2
# Methods that are related to getting hierarchical structure of rules and rule groups
module RuleTree
extend ActiveSupport::Concern

included do
# Builds the hierarchical structure of groups and rules
def rule_tree
cached_rules = rules.order(:precedence).select(:id, :rule_group_id).group_by(&:rule_group_id)

rule_groups.order(:precedence).select(:id, :ancestry).arrange_serializable do |group, children|
{
id: group.id,
type: :rule_group,
children: children + (cached_rules[group.id]&.map do |rule|
{ id: rule.id, type: :rule }
end || [])
}
end
end
end
end
end
3 changes: 3 additions & 0 deletions app/models/v2/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module V2
# Model for SCAP policies
class Policy < ApplicationRecord
include V2::RuleTree

# FIXME: clean up after the remodel
self.table_name = :v2_policies
self.primary_key = :id
Expand All @@ -18,6 +20,7 @@ class Policy < ApplicationRecord
has_many :rules, through: :tailoring_rules, class_name: 'V2::Rule'
has_many :policy_systems, class_name: 'V2::PolicySystem', dependent: :destroy
has_many :systems, through: :policy_systems, class_name: 'V2::System'
has_many :rule_groups, through: :security_guide, class_name: 'V2::RuleGroup'

validates :account, presence: true
validates :profile, presence: true
Expand Down
3 changes: 3 additions & 0 deletions app/models/v2/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module V2
# Model for Canonical Profile
class Profile < ApplicationRecord
include V2::RuleTree

# FIXME: clean up after the remodel
self.table_name = :canonical_profiles
self.primary_key = :id
Expand All @@ -18,6 +20,7 @@ class Profile < ApplicationRecord
has_many :profile_rules, class_name: 'V2::ProfileRule', dependent: :destroy
has_many :rules, through: :profile_rules, class_name: 'V2::Rule'
has_many :os_minor_versions, class_name: 'V2::ProfileOsMinorVersion', dependent: :destroy
has_many :rule_groups, through: :security_guide, class_name: 'V2::RuleGroup'

def variant_for_minor(version)
self.class.joins(:security_guide, :os_minor_versions)
Expand Down
17 changes: 2 additions & 15 deletions app/models/v2/security_guide.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module V2
# Model for Security Guides
class SecurityGuide < ApplicationRecord
include V2::RuleTree

# FIXME: clean up after the remodel
self.primary_key = :id

Expand Down Expand Up @@ -37,20 +39,5 @@ class SecurityGuide < ApplicationRecord
def self.os_versions
reselect(:os_major_version).distinct.reorder(:os_major_version).map(&:os_major_version)
end

# Builds the hierarchical structure of groups and rules
def rule_tree
cached_rules = rules.order(:precedence).select(:id, :rule_group_id).group_by(&:rule_group_id)

rule_groups.order(:precedence).select(:id, :ancestry).arrange_serializable do |group, children|
{
id: group.id,
type: :rule_group,
children: children + (cached_rules[group.id]&.map do |rule|
{ id: rule.id, type: :rule }
end || [])
}
end
end
end
end
3 changes: 3 additions & 0 deletions app/models/v2/tailoring.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module V2
# Model for profile tailoring
class Tailoring < ApplicationRecord
include V2::RuleTree

GROUP_ANCESTRY_IDS = Arel::Nodes::NamedFunction.new(
'CAST',
[
Expand Down Expand Up @@ -40,6 +42,7 @@ class Tailoring < ApplicationRecord
inverse_of: :tailoring
has_many :rules, class_name: 'V2::Rule', through: :tailoring_rules
has_many :test_results, class_name: 'V2::TestResult', dependent: :destroy
has_many :rule_groups, through: :security_guide, class_name: 'V2::RuleGroup'

searchable_by :os_minor_version, %i[eq ne]

Expand Down
4 changes: 4 additions & 0 deletions app/policies/v2/profile_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def show?
true
end

def rule_tree?
true
end

# All users should see all Profiles currently
class Scope < V2::ApplicationPolicy::Scope
end
Expand Down
4 changes: 4 additions & 0 deletions app/policies/v2/tailoring_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def show?
match_account?
end

def rule_tree?
match_account?
end

def update?
match_account?
end
Expand Down
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ def draw_routes(prefix)
resources :rule_groups, only: %i[index show], parents: %i[security_guide]

resources :profiles, only: %i[index show], parents: %i[security_guide] do
get :rule_tree, on: :member, parents: %i[security_guide]

resources :rules, only: %i[index show], parents: %i[security_guide profiles]
end
end

resources :policies, except: %i[new edit] do
resources :tailorings, only: %i[index show create update], parents: %i[policy] do
resources :rules, only: %i[index create update destroy], parents: %i[policies tailorings]
get :tailoring_file, on: :member, defaults: { format: 'xml' }, constraints: { format: /json|xml/ }
get :rule_tree, on: :member, parents: %i[policy]

resources :rules, only: %i[index create update destroy], parents: %i[policies tailorings]
end

resources :systems, only: %i[index create update destroy], parents: %i[policies] do
Expand Down
62 changes: 38 additions & 24 deletions spec/controllers/v2/profiles_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,47 @@
allow(controller).to receive(:rbac_allowed?).and_return(rbac_allowed?)
end

describe 'GET index' do
let(:parent) { FactoryBot.create(:v2_security_guide) }
let(:extra_params) { { security_guide_id: parent.id } }
let(:item_count) { 2 }
let(:items) do
FactoryBot.create_list(
:v2_profile,
item_count,
value_count: 10,
security_guide: parent
).sort_by(&:id)
context '/security_guides/:id/profiles' do
describe 'GET index' do
let(:parent) { FactoryBot.create(:v2_security_guide) }
let(:extra_params) { { security_guide_id: parent.id } }
let(:item_count) { 2 }
let(:items) do
FactoryBot.create_list(
:v2_profile,
item_count,
value_count: 10,
security_guide: parent
).sort_by(&:id)
end

it_behaves_like 'collection', :security_guide
include_examples 'with metadata', :security_guide
it_behaves_like 'paginable', :security_guide
it_behaves_like 'sortable', :security_guide
it_behaves_like 'searchable', :security_guide
end

it_behaves_like 'collection', :security_guide
include_examples 'with metadata', :security_guide
it_behaves_like 'paginable', :security_guide
it_behaves_like 'sortable', :security_guide
it_behaves_like 'searchable', :security_guide
end
describe 'GET show' do
let(:item) { FactoryBot.create(:v2_profile) }
let(:parent) { item.security_guide }
let(:extra_params) { { security_guide_id: parent.id, id: item.id } }
let(:notfound_params) { extra_params.merge(security_guide_id: FactoryBot.create(:v2_security_guide).id) }

it_behaves_like 'individual', :security_guide
it_behaves_like 'indexable', :ref_id, :security_guide
end

describe 'GET rule_tree' do
let(:item) { FactoryBot.create(:v2_profile, rule_count: 5) }
let(:parent) { item.security_guide }

describe 'GET show' do
let(:item) { FactoryBot.create(:v2_profile) }
let(:parent) { item.security_guide }
let(:extra_params) { { security_guide_id: parent.id, id: item.id } }
let(:notfound_params) { extra_params.merge(security_guide_id: FactoryBot.create(:v2_security_guide).id) }
it 'calls the rule tree on the model' do
get :rule_tree, params: { id: item.id, security_guide_id: parent.id, parents: %i[security_guide] }

it_behaves_like 'individual', :security_guide
it_behaves_like 'indexable', :ref_id, :security_guide
expect(response).to have_http_status :ok
expect(response.parsed_body).not_to be_empty
end
end
end
end
20 changes: 20 additions & 0 deletions spec/controllers/v2/tailorings_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@
it_behaves_like 'indexable', :os_minor_version, :policy
end

describe 'GET rule_tree' do
let(:os_minor_version) { SecureRandom.random_number(10) }
let(:parent) { FactoryBot.create(:v2_policy, account: current_user.account, profile: canonical_profile) }

let(:item) do
FactoryBot.create(:v2_tailoring, :with_mixed_rules, policy: parent, os_minor_version: os_minor_version)
end

let(:canonical_profile) do
FactoryBot.create(:v2_profile, rule_count: 5, ref_id_suffix: 'foo', supports_minors: [os_minor_version])
end

it 'calls the rule tree on the model' do
get :rule_tree, params: { id: item.id, policy_id: parent.id, parents: %i[policy] }

expect(response).to have_http_status :ok
expect(response.parsed_body).not_to be_empty
end
end

describe 'POST create' do
let(:os_minor_version) { SecureRandom.random_number(10) }
let(:parent) { FactoryBot.create(:v2_policy, account: current_user.account, profile: canonical_profile) }
Expand Down
35 changes: 35 additions & 0 deletions spec/integration/v2/profiles_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,39 @@
end
end
end

path '/security_guides/{security_guide_id}/profiles/{profile_id}/rule_tree' do
let(:item) { FactoryBot.create(:v2_profile, rule_count: 10) }

get 'Request the Rule Tree of a Profile' do
v2_auth_header
tags 'Content'
description 'Returns the Rule Tree of a Profile'
operationId 'ProfileTree'
content_types

parameter name: :security_guide_id, in: :path, type: :string, required: true
parameter name: :profile_id, in: :path, type: :string, required: true

response '200', 'Returns the Rule Tree of a Profile' do
let(:profile_id) { item.id }
let(:security_guide_id) { item.security_guide.id }
schema ref_schema('rule_tree')

after { |e| autogenerate_examples(e, 'Returns the Rule Tree of a Profile') }

run_test!
end

response '404', 'Returns with Not Found' do
let(:profile_id) { Faker::Internet.uuid }
let(:security_guide_id) { Faker::Internet.uuid }
schema ref_schema('errors')

after { |e| autogenerate_examples(e, 'Description of an error when requesting a non-existing Profile') }

run_test!
end
end
end
end
2 changes: 1 addition & 1 deletion spec/integration/v2/security_guides_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@

parameter name: :security_guide_id, in: :path, type: :string, required: true

response '200', 'Returns a the Rule Tree of Security Guide' do
response '200', 'Returns the Rule Tree of a Security Guide' do
let(:security_guide_id) { item.id }
schema ref_schema('rule_tree')

Expand Down
43 changes: 43 additions & 0 deletions spec/integration/v2/tailorings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,49 @@
end
end

path '/policies/{policy_id}/tailorings/{tailoring_id}/rule_tree' do
let(:policy_id) do
FactoryBot.create(
:v2_policy,
account: user.account,
profile: FactoryBot.create(:v2_profile, rule_count: 10, ref_id_suffix: 'foo', supports_minors: [1])
).id
end

let(:item) do
FactoryBot.create(:v2_tailoring, :with_mixed_rules, policy: V2::Policy.find(policy_id), os_minor_version: 1)
end

get 'Request the Rule Tree of a Tailoring' do
v2_auth_header
tags 'Policies'
description 'Returns the Rule Tree of a Tailoring'
operationId 'TailoringRuleTree'
content_types

parameter name: :policy_id, in: :path, type: :string, required: true
parameter name: :tailoring_id, in: :path, type: :string, required: true

response '200', 'Returns the Rule Tree of a Tailoring' do
let(:tailoring_id) { item.id }
romanblanco marked this conversation as resolved.
Show resolved Hide resolved
schema ref_schema('rule_tree')

after { |e| autogenerate_examples(e, 'Returns the Rule Tree of a Tailoring') }

run_test!
end

response '404', 'Returns with Not Found' do
let(:tailoring_id) { Faker::Internet.uuid }
schema ref_schema('errors')

after { |e| autogenerate_examples(e, 'Description of an error when requesting a non-existing Tailoring') }

run_test!
end
end
end

path '/policies/{policy_id}/tailorings/{tailoring_id}/tailoring_file.json' do
let(:policy_id) do
FactoryBot.create(:v2_policy, :for_tailoring, account: user.account, supports_minors: [1]).id
Expand Down
Loading
Loading