diff --git a/awx/main/access.py b/awx/main/access.py index a8eca06717..0693590a21 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -902,6 +902,10 @@ class HostAccess(BaseAccess): ) prefetch_related = ('groups', 'inventory_sources') + def get_queryset(self): + qs = super().get_queryset() + return qs.exclude(inventory__kind='constructed') + def filtered_queryset(self): return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) diff --git a/awx/main/tests/functional/rbac/test_rbac_inventory.py b/awx/main/tests/functional/rbac/test_rbac_inventory.py index 87804d1c74..5c481b9f56 100644 --- a/awx/main/tests/functional/rbac/test_rbac_inventory.py +++ b/awx/main/tests/functional/rbac/test_rbac_inventory.py @@ -101,6 +101,34 @@ def test_host_access(organization, inventory, group, user, group_factory): assert inventory_admin_access.can_read(host) is False +@pytest.mark.django_db +def test_host_access_excludes_constructed_inventory_hosts(organization, inventory, user): + """ + Exclude hosts from constructed inventory for all users. + """ + constructed_inv = organization.inventories.create(name='constructed-inv', kind='constructed') + real_host = Host.objects.create(inventory=inventory, name='hostA') + shadow_host = Host.objects.create(inventory=constructed_inv, name='hostA') + + # Non-superuser with read on both inventories + reader = user('reader', False) + inventory.read_role.members.add(reader) + constructed_inv.read_role.members.add(reader) + + reader_qs = HostAccess(reader).get_queryset() + assert real_host in reader_qs + assert shadow_host not in reader_qs + + # Superuser path: should get the same result + superuser = user('super', True) + super_qs = HostAccess(superuser).get_queryset() + assert real_host in super_qs + assert shadow_host not in super_qs + + # Sanity: shadow rows still exist in the DB and are reachable via inventory filtering + assert Host.objects.filter(inventory=constructed_inv, name='hostA').exists() + + @pytest.mark.django_db def test_inventory_source_credential_check(rando, inventory_source, credential): inventory_source.inventory.admin_role.members.add(rando)