From 84ee11d7e13515735afaa874d7535c1dce1860ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Hal=C3=A1sz?= Date: Wed, 23 Oct 2024 21:37:09 +0200 Subject: [PATCH] feat(APIv2): expose the TOML format of the tailoring_file --- Gemfile | 1 + Gemfile.lock | 4 ++ app/controllers/v2/tailorings_controller.rb | 24 ++++------ app/models/v2/fix.rb | 8 ++++ app/services/v2/json_tailoring_file.rb | 16 ++++++- app/services/v2/tailoring_file.rb | 23 ++++++++++ app/services/v2/toml_tailoring_file.rb | 44 +++++++++++++++++++ app/services/v2/xccdf_tailoring_file.rb | 16 ++++++- config/routes.rb | 2 +- .../20241024044139_reset_datastreams_fixes.rb | 5 +++ db/schema.rb | 2 +- .../v2/tailorings_controller_spec.rb | 32 ++++++++++++++ 12 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 app/services/v2/tailoring_file.rb create mode 100644 app/services/v2/toml_tailoring_file.rb create mode 100644 db/migrate/20241024044139_reset_datastreams_fixes.rb diff --git a/Gemfile b/Gemfile index e73fc37cc..abca4c5d1 100644 --- a/Gemfile +++ b/Gemfile @@ -61,6 +61,7 @@ gem 'faraday-retry' gem 'activerecord-import' gem 'friendly_id', '~> 5.2.4' gem 'oj' +gem 'toml' gem 'scoped_search' gem 'uuid' gem 'will_paginate' diff --git a/Gemfile.lock b/Gemfile.lock index 04f70370c..b7dd05a30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -247,6 +247,7 @@ GEM parser (3.3.2.0) ast (~> 2.4.1) racc + parslet (2.0.0) pg (1.5.6) prometheus-client-mmap (0.28.1) rb_sys (~> 0.9) @@ -426,6 +427,8 @@ GEM time (0.3.0) date timeout (0.4.1) + toml (0.3.0) + parslet (>= 1.8.0, < 3.0.0) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) @@ -541,6 +544,7 @@ DEPENDENCIES spring-watcher-listen stronger_parameters time (>= 0.2.2) + toml tzinfo-data uri (>= 0.11.1) uuid diff --git a/app/controllers/v2/tailorings_controller.rb b/app/controllers/v2/tailorings_controller.rb index e842bbb4f..6fc76486b 100644 --- a/app/controllers/v2/tailorings_controller.rb +++ b/app/controllers/v2/tailorings_controller.rb @@ -49,10 +49,11 @@ def update permitted_params_for_action :update, { id: ID_TYPE, **UPDATE_ATTRIBUTES } def tailoring_file - return unless tailoring.tailored? + builder = file_builder(params[:format].to_sym) - format = params[:format].to_sym - send_data(build_file(format), filename: filename(format), type: Mime[format]) + return if builder.empty? # no-content for empty XMLs + + send_data(builder.output, filename: builder.filename, type: builder.mime) end permission_for_action :tailoring_file, Rbac::POLICY_READ permitted_params_for_action :tailoring_file, id: ID_TYPE.required @@ -71,21 +72,14 @@ def policy V2::Policy.find(permitted_params[:policy_id]) end - def build_file(format) - builder = format == :json ? JsonTailoringFile : XccdfTailoringFile - - builder.new( + def file_builder(format) + V2::TailoringFile.new( profile: tailoring, rules: tailoring.rules_added + tailoring.rules_removed, rule_group_ref_ids: tailoring.rule_group_ref_ids, - set_values: tailoring.value_overrides_by_ref_id - ).output - end - - def filename(format) - extension = format == :json ? 'json' : 'xml' - - "#{tailoring.security_guide.ref_id}__#{tailoring.profile.ref_id}__tailoring.#{extension}" + set_values: tailoring.value_overrides_by_ref_id, + format: format + ) end def resource diff --git a/app/models/v2/fix.rb b/app/models/v2/fix.rb index 4a3499257..f38a5b2cd 100644 --- a/app/models/v2/fix.rb +++ b/app/models/v2/fix.rb @@ -7,6 +7,14 @@ class Fix < ApplicationRecord # FIXME: clean up after the remodel self.table_name = :fixes + ANACONDA = 'urn:redhat:anaconda:pre' + BLUEPRINT = 'urn:redhat:osbuild:blueprint' + ANSIBLE = 'urn:xccdf:fix:script:ansible' + IGNITION = 'urn:xccdf:fix:script:ignition' + KUBERNETES = 'urn:xccdf:fix:script:kubernetes' + PUPPET = 'urn:xccdf:fix:script:puppet' + SHELL = 'urn:xccdf:fix:script:sh' + belongs_to :rule has_one :security_guide, through: :rule diff --git a/app/services/v2/json_tailoring_file.rb b/app/services/v2/json_tailoring_file.rb index 72c839838..591d8f09d 100644 --- a/app/services/v2/json_tailoring_file.rb +++ b/app/services/v2/json_tailoring_file.rb @@ -2,18 +2,30 @@ module V2 # A class representing a JSON Tailoring File - class JsonTailoringFile - def initialize(profile:, rules: {}, set_values: {}, rule_group_ref_ids: []) + class JsonTailoringFile < TailoringFile + def initialize(profile:, rules: {}, set_values: {}, rule_group_ref_ids: [], **_hsh) @tailoring = profile @rules = rules @rule_group_ref_ids = rule_group_ref_ids @set_values = set_values end + def mime + Mime[:json] + end + + def extension + 'json' + end + def output Oj.dump('profiles' => [build_profile]) end + def empty? + !@tailoring.tailored? + end + private def build_profile diff --git a/app/services/v2/tailoring_file.rb b/app/services/v2/tailoring_file.rb new file mode 100644 index 000000000..4cdaa0dd8 --- /dev/null +++ b/app/services/v2/tailoring_file.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module V2 + # Factory for selecting the right tailoring file format + class TailoringFile + def self.new(format: :xml, **hsh) + return super if self != V2::TailoringFile + + case format + when :json + JsonTailoringFile.new(**hsh) + when :toml + TomlTailoringFile.new(**hsh) + when :xml + XccdfTailoringFile.new(**hsh) + end + end + + def filename + "#{@tailoring.security_guide.ref_id}__#{@tailoring.profile.ref_id}__tailoring.#{extension}" + end + end +end diff --git a/app/services/v2/toml_tailoring_file.rb b/app/services/v2/toml_tailoring_file.rb new file mode 100644 index 000000000..474f63a67 --- /dev/null +++ b/app/services/v2/toml_tailoring_file.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'deep_merge' + +module V2 + # A class representing a TOML Tailoring File + class TomlTailoringFile < TailoringFile + def initialize(profile:, **_hsh) + @tailoring = profile + end + + def mime + 'application/toml' + end + + def extension + 'toml' + end + + def output + TOML::Generator.new(build_profile).body + end + + def empty? + false + end + + private + + def build_profile + { + 'name' => @tailoring.profile.title, + 'description' => @tailoring.profile.description, + 'version' => @tailoring.profile.security_guide.version + }.merge(build_fixes) + end + + def build_fixes + V2::Fix.where(rule: @tailoring.rules, system: V2::Fix::BLUEPRINT).reduce({}) do |obj, fix| + obj.deep_merge(TOML.load(fix.text)) + end + end + end +end diff --git a/app/services/v2/xccdf_tailoring_file.rb b/app/services/v2/xccdf_tailoring_file.rb index 38c1d4d3d..91b9007cf 100644 --- a/app/services/v2/xccdf_tailoring_file.rb +++ b/app/services/v2/xccdf_tailoring_file.rb @@ -2,22 +2,34 @@ module V2 # A class representing an XCCDF Tailoring File - class XccdfTailoringFile + class XccdfTailoringFile < TailoringFile extend Forwardable XCCDF = 'xccdf' - def initialize(profile:, rules: {}, set_values: {}, rule_group_ref_ids: []) + def initialize(profile:, rules: {}, set_values: {}, rule_group_ref_ids: [], **_hsh) @tailoring = profile @rules = rules @rule_group_ref_ids = rule_group_ref_ids @set_values = set_values end + def mime + Mime[:xml] + end + + def extension + 'xml' + end + def output builder.to_xml end + def empty? + !@tailoring.tailored? + end + private def builder diff --git a/config/routes.rb b/config/routes.rb index 67c605021..ee53dca58 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,7 +41,7 @@ def draw_routes(prefix) resources :policies, except: %i[new edit] do resources :tailorings, only: %i[index show create update], parents: %i[policy] do - get :tailoring_file, on: :member, defaults: { format: 'xml' }, constraints: { format: /json|xml/ } + get :tailoring_file, on: :member, defaults: { format: 'xml' }, constraints: { format: /json|xml|toml/ } get :rule_tree, on: :member, parents: %i[policy] resources :rules, only: %i[index create update destroy], parents: %i[policies tailorings] diff --git a/db/migrate/20241024044139_reset_datastreams_fixes.rb b/db/migrate/20241024044139_reset_datastreams_fixes.rb new file mode 100644 index 000000000..eef82ee4c --- /dev/null +++ b/db/migrate/20241024044139_reset_datastreams_fixes.rb @@ -0,0 +1,5 @@ +class ResetDatastreamsFixes < ActiveRecord::Migration[7.1] + def up + Revision.find_by(name: 'datastreams')&.delete + end +end diff --git a/db/schema.rb b/db/schema.rb index b8d477611..cda00ee2d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_10_14_100839) do +ActiveRecord::Schema[7.1].define(version: 2024_10_24_044139) do create_schema "inventory" # These are extensions that must be enabled in order to support this database diff --git a/spec/controllers/v2/tailorings_controller_spec.rb b/spec/controllers/v2/tailorings_controller_spec.rb index a6104fc9a..a0b4902bd 100644 --- a/spec/controllers/v2/tailorings_controller_spec.rb +++ b/spec/controllers/v2/tailorings_controller_spec.rb @@ -399,5 +399,37 @@ include_examples 'tailoring_file' end + + context 'TOML' do + let(:format) { :toml } + let(:tailoring_file) { TOML.load(response.body) } + let(:extra_params) { { policy_id: parent.id, id: item.id } } + + let(:parent) do + FactoryBot.create( + :v2_policy, + :for_tailoring, + account: current_user.account, + supports_minors: [8] + ) + end + + let(:item) do + FactoryBot.create( + :v2_tailoring, + :without_rules, + value_overrides: {}, # no tailored values + policy: parent, + os_minor_version: 8 + ) + end + + it 'returns tailoring_file' do + get :tailoring_file, params: extra_params.merge(parents: [:policy], format: format) + + expect(response).to have_http_status :ok + expect(tailoring_file).not_to be_empty + end + end end end