Skip to content

Fixes #39088 - Do not tie policies to hosts#605

Merged
adamlazik1 merged 4 commits into
theforeman:masterfrom
adamruzicka:host-policy
Feb 25, 2026
Merged

Fixes #39088 - Do not tie policies to hosts#605
adamlazik1 merged 4 commits into
theforeman:masterfrom
adamruzicka:host-policy

Conversation

@adamruzicka
Copy link
Copy Markdown
Contributor

No description provided.

JSON.fast_generate is deprecated and will be removed in json  3.0.0,
just use JSON.generate
end

@asset = ForemanOpenscap::Helper::get_asset(params[:cname], policy_id)
@host = ForemanOpenscap::Helper::find_host_by_name_or_uuid(params[:cname])
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative would be to leave the asset find_or_create, but not attach the policy to it if it is already provided by the host's hostgroup or its ancestors. Since this would be more expensive at runtime and would create orphaned records, I decided against it.

This also changes the behaviour in an edge case where the host has neither a hostgroup nor a policy assigned and a report comes in. Previously this would assign the policy to the host directly. In this case, I'd say Foreman is the source of truth about assigned policies and incoming reports should not change that.

.where(policy_id: policy_ids)

scope.each do |asset_policy|
# Composite primary keys are supported in rails >=7.1, since we're on 7.0, raw SQL will have to do
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative would be pulling in a composite_primary_keys gem, but eh

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses #39088 by stopping ARF uploads from implicitly coupling (creating/maintaining) host-level OpenSCAP policy assignments when policies are provided via hostgroups, and adds a cleanup migration to remove existing host policy assignments that shadow hostgroup assignments.

Changes:

  • Update ARF upload flow to resolve a Host directly (instead of fetching/updating an Asset) and create reports against that host.
  • Add a migration to remove host-level AssetPolicy rows that duplicate policies already provided by hostgroups, and prune now-empty host assets.
  • Extend/adjust functional coverage around ARF upload behavior for hostgroup-provided policies.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
app/controllers/api/v2/compliance/arf_reports_controller.rb Switch ARF upload pre-processing from asset lookup to direct host lookup and use @host through the create flow.
app/models/foreman_openscap/arf_report.rb Change create_arf to accept a host directly; minor JSON generation change for fixes.
test/functional/api/v2/compliance/arf_reports_controller_test.rb Update stubbing to match new host-resolution logic and add a regression test for hostgroup-provided policies.
db/migrate/20260218133925_drop_host_policy_assignments_shadowing_hostgroup_policy_assignments.rb Data migration to delete host policy assignments that are redundant with hostgroup inheritance and prune empty host assets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +68 to +69
host = ForemanOpenscap::Helper.stubs(:find_host_by_name_or_uuid).returns(@host)
host.stubs(:openscap_proxy).returns(nil)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test, host = ForemanOpenscap::Helper.stubs(:find_host_by_name_or_uuid)... assigns a Mocha expectation to host, so host.stubs(:openscap_proxy) won’t stub the Host instance and can raise (or silently do nothing). Stub @host.openscap_proxy instead (or drop the stub entirely since openscap_proxy_url is provided).

Suggested change
host = ForemanOpenscap::Helper.stubs(:find_host_by_name_or_uuid).returns(@host)
host.stubs(:openscap_proxy).returns(nil)
ForemanOpenscap::Helper.stubs(:find_host_by_name_or_uuid).returns(@host)
@host.stubs(:openscap_proxy).returns(nil)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

assert_response :success
end

test "should create host asset and tie to policy when policy is from hostgroup" do
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test name says it "should create host asset" but the assertions verify that a host-level asset is not created. Please rename the test to match the expected behavior (or adjust assertions if the intent is different).

Suggested change
test "should create host asset and tie to policy when policy is from hostgroup" do
test "should not create host asset but tie report to hostgroup policy when policy is from hostgroup" do

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what I get for creating a test for the original behaviour first and then tweaking it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +7 to +8
# Take ids of the hostgroup and all its children, all of these provide the policies extracted above
hostgroup_ids = hg_asset.hostgroup.subtree_ids
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hg_asset.hostgroup can be nil if the polymorphic assetable record was deleted (there are migrations cleaning up orphaned asset_policies, but not orphaned assets). Calling subtree_ids on nil would break the migration. Consider skipping assets with missing hostgroups or joining against existing hostgroups to ensure hg_asset.hostgroup is present before calling subtree_ids.

Suggested change
# Take ids of the hostgroup and all its children, all of these provide the policies extracted above
hostgroup_ids = hg_asset.hostgroup.subtree_ids
# Skip assets with missing hostgroups to avoid calling subtree_ids on nil
hostgroup = hg_asset.hostgroup
next if hostgroup.nil?
# Take ids of the hostgroup and all its children, all of these provide the policies extracted above
hostgroup_ids = hostgroup.subtree_ids

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +17 to +21

scope.each do |asset_policy|
# Composite primary keys are supported in rails >=7.1, since we're on 7.0, raw SQL will have to do
ActiveRecord::Base.connection.execute("DELETE FROM foreman_openscap_asset_policies WHERE asset_id = #{asset_policy.asset_id} AND policy_id = #{asset_policy.policy_id}")
end
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does a per-row connection.execute DELETE with string interpolation. Even if IDs come from the DB, it’s inefficient (N deletes) and bypasses quoting/sanitization. Prefer a set-based delete (e.g., where(asset_id: ..., policy_id: ...).delete_all or a single DELETE ... WHERE asset_id IN (...) AND policy_id IN (...)) and use bind params / quoting helpers if raw SQL is necessary.

Suggested change
scope.each do |asset_policy|
# Composite primary keys are supported in rails >=7.1, since we're on 7.0, raw SQL will have to do
ActiveRecord::Base.connection.execute("DELETE FROM foreman_openscap_asset_policies WHERE asset_id = #{asset_policy.asset_id} AND policy_id = #{asset_policy.policy_id}")
end
asset_policy_keys = scope.pluck(:asset_id, :policy_id)
next if asset_policy_keys.empty?
# Composite primary keys are supported in rails >=7.1, since we're on 7.0, raw SQL will have to do.
# Use a single, set-based DELETE with proper quoting for (asset_id, policy_id) tuples.
connection = ActiveRecord::Base.connection
tuples_sql = asset_policy_keys.map { |asset_id, policy_id|
"(#{connection.quote(asset_id)}, #{connection.quote(policy_id)})"
}.join(', ')
connection.execute("DELETE FROM foreman_openscap_asset_policies WHERE (asset_id, policy_id) IN (#{tuples_sql})")

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer a set-based delete (e.g., where(asset_id: ..., policy_id: ...).delete_all or a single DELETE ... WHERE asset_id IN (...) AND policy_id IN (...))

Except we want to delete specific asset_id-policy_id combinations, which this wouldn't really do.

The code suggestion (which does something different) would probably work if we added some sort of batching to prevent the query from growing too big to be processed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# Prune host assets which do not have any policies assigned to them
scope = ForemanOpenscap::Asset.where(assetable_type: 'Host::Base').left_outer_joins(:asset_policies)
scope = scope.where('foreman_openscap_asset_policies.asset_id IS NULL')
scope.destroy_all
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destroy_all will instantiate each Asset and run callbacks, which can be slow for large datasets in a data cleanup migration. If callbacks aren’t required here, prefer delete_all (or batched deletes) for pruning host assets with no policies.

Suggested change
scope.destroy_all
scope.delete_all

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

end

test "should not create host asset but tie report to hostgroup policy when policy is from hostgroup" do
reports_cleanup
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to PR, asking to broaden my understanding of ruby tests: Wouldn't reports be deleted automatically as a teardown action? My understanding is that objects in the testing db do not persist.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have that none, but that may not have always been the case. I dropped it from the new test I added here, we should revisit this and do a cleanup in a followup pr.

Copy link
Copy Markdown
Contributor

@adamlazik1 adamlazik1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM, functionality verified on packit.

@adamlazik1 adamlazik1 merged commit 9b4995a into theforeman:master Feb 25, 2026
11 of 12 checks passed
@adamlazik1
Copy link
Copy Markdown
Contributor

Thanks @adamruzicka !

@adamruzicka adamruzicka deleted the host-policy branch March 13, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants