mirror of
https://github.com/ansible/awx.git
synced 2026-03-31 07:45:08 -02:30
Compare commits
1 Commits
24.6.0
...
awxkit-sup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d0a3149f1 |
17
Makefile
17
Makefile
@@ -64,9 +64,6 @@ DEV_DOCKER_OWNER_LOWER = $(shell echo $(DEV_DOCKER_OWNER) | tr A-Z a-z)
|
|||||||
DEV_DOCKER_TAG_BASE ?= ghcr.io/$(DEV_DOCKER_OWNER_LOWER)
|
DEV_DOCKER_TAG_BASE ?= ghcr.io/$(DEV_DOCKER_OWNER_LOWER)
|
||||||
DEVEL_IMAGE_NAME ?= $(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG)
|
DEVEL_IMAGE_NAME ?= $(DEV_DOCKER_TAG_BASE)/awx_devel:$(COMPOSE_TAG)
|
||||||
|
|
||||||
# Common command to use for running ansible-playbook
|
|
||||||
ANSIBLE_PLAYBOOK ?= ansible-playbook -e ansible_python_interpreter=$(PYTHON)
|
|
||||||
|
|
||||||
RECEPTOR_IMAGE ?= quay.io/ansible/receptor:devel
|
RECEPTOR_IMAGE ?= quay.io/ansible/receptor:devel
|
||||||
|
|
||||||
# Python packages to install only from source (not from binary wheels)
|
# Python packages to install only from source (not from binary wheels)
|
||||||
@@ -371,7 +368,7 @@ symlink_collection:
|
|||||||
ln -s $(shell pwd)/awx_collection $(COLLECTION_INSTALL)
|
ln -s $(shell pwd)/awx_collection $(COLLECTION_INSTALL)
|
||||||
|
|
||||||
awx_collection_build: $(shell find awx_collection -type f)
|
awx_collection_build: $(shell find awx_collection -type f)
|
||||||
$(ANSIBLE_PLAYBOOK) -i localhost, awx_collection/tools/template_galaxy.yml \
|
ansible-playbook -i localhost, awx_collection/tools/template_galaxy.yml \
|
||||||
-e collection_package=$(COLLECTION_PACKAGE) \
|
-e collection_package=$(COLLECTION_PACKAGE) \
|
||||||
-e collection_namespace=$(COLLECTION_NAMESPACE) \
|
-e collection_namespace=$(COLLECTION_NAMESPACE) \
|
||||||
-e collection_version=$(COLLECTION_VERSION) \
|
-e collection_version=$(COLLECTION_VERSION) \
|
||||||
@@ -525,10 +522,10 @@ endif
|
|||||||
|
|
||||||
docker-compose-sources: .git/hooks/pre-commit
|
docker-compose-sources: .git/hooks/pre-commit
|
||||||
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
|
@if [ $(MINIKUBE_CONTAINER_GROUP) = true ]; then\
|
||||||
$(ANSIBLE_PLAYBOOK) -i tools/docker-compose/inventory -e minikube_setup=$(MINIKUBE_SETUP) tools/docker-compose-minikube/deploy.yml; \
|
ansible-playbook -i tools/docker-compose/inventory -e minikube_setup=$(MINIKUBE_SETUP) tools/docker-compose-minikube/deploy.yml; \
|
||||||
fi;
|
fi;
|
||||||
|
|
||||||
$(ANSIBLE_PLAYBOOK) -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/sources.yml \
|
||||||
-e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \
|
-e awx_image=$(DEV_DOCKER_TAG_BASE)/awx_devel \
|
||||||
-e awx_image_tag=$(COMPOSE_TAG) \
|
-e awx_image_tag=$(COMPOSE_TAG) \
|
||||||
-e receptor_image=$(RECEPTOR_IMAGE) \
|
-e receptor_image=$(RECEPTOR_IMAGE) \
|
||||||
@@ -552,7 +549,7 @@ docker-compose-sources: .git/hooks/pre-commit
|
|||||||
|
|
||||||
docker-compose: awx/projects docker-compose-sources
|
docker-compose: awx/projects docker-compose-sources
|
||||||
ansible-galaxy install --ignore-certs -r tools/docker-compose/ansible/requirements.yml;
|
ansible-galaxy install --ignore-certs -r tools/docker-compose/ansible/requirements.yml;
|
||||||
$(ANSIBLE_PLAYBOOK) -i tools/docker-compose/inventory tools/docker-compose/ansible/initialize_containers.yml \
|
ansible-playbook -i tools/docker-compose/inventory tools/docker-compose/ansible/initialize_containers.yml \
|
||||||
-e enable_vault=$(VAULT) \
|
-e enable_vault=$(VAULT) \
|
||||||
-e vault_tls=$(VAULT_TLS) \
|
-e vault_tls=$(VAULT_TLS) \
|
||||||
-e enable_ldap=$(LDAP); \
|
-e enable_ldap=$(LDAP); \
|
||||||
@@ -595,7 +592,7 @@ docker-compose-container-group-clean:
|
|||||||
.PHONY: Dockerfile.dev
|
.PHONY: Dockerfile.dev
|
||||||
## Generate Dockerfile.dev for awx_devel image
|
## Generate Dockerfile.dev for awx_devel image
|
||||||
Dockerfile.dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
Dockerfile.dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
||||||
$(ANSIBLE_PLAYBOOK) tools/ansible/dockerfile.yml \
|
ansible-playbook tools/ansible/dockerfile.yml \
|
||||||
-e dockerfile_name=Dockerfile.dev \
|
-e dockerfile_name=Dockerfile.dev \
|
||||||
-e build_dev=True \
|
-e build_dev=True \
|
||||||
-e receptor_image=$(RECEPTOR_IMAGE)
|
-e receptor_image=$(RECEPTOR_IMAGE)
|
||||||
@@ -670,7 +667,7 @@ version-for-buildyml:
|
|||||||
.PHONY: Dockerfile
|
.PHONY: Dockerfile
|
||||||
## Generate Dockerfile for awx image
|
## Generate Dockerfile for awx image
|
||||||
Dockerfile: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
Dockerfile: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
||||||
$(ANSIBLE_PLAYBOOK) tools/ansible/dockerfile.yml \
|
ansible-playbook tools/ansible/dockerfile.yml \
|
||||||
-e receptor_image=$(RECEPTOR_IMAGE) \
|
-e receptor_image=$(RECEPTOR_IMAGE) \
|
||||||
-e headless=$(HEADLESS)
|
-e headless=$(HEADLESS)
|
||||||
|
|
||||||
@@ -700,7 +697,7 @@ awx-kube-buildx: Dockerfile
|
|||||||
.PHONY: Dockerfile.kube-dev
|
.PHONY: Dockerfile.kube-dev
|
||||||
## Generate Docker.kube-dev for awx_kube_devel image
|
## Generate Docker.kube-dev for awx_kube_devel image
|
||||||
Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
Dockerfile.kube-dev: tools/ansible/roles/dockerfile/templates/Dockerfile.j2
|
||||||
$(ANSIBLE_PLAYBOOK) tools/ansible/dockerfile.yml \
|
ansible-playbook tools/ansible/dockerfile.yml \
|
||||||
-e dockerfile_name=Dockerfile.kube-dev \
|
-e dockerfile_name=Dockerfile.kube-dev \
|
||||||
-e kube_dev=True \
|
-e kube_dev=True \
|
||||||
-e template_dest=_build_kube_dev \
|
-e template_dest=_build_kube_dev \
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ from ansible_base.lib.utils.models import get_all_field_names
|
|||||||
from ansible_base.lib.utils.requests import get_remote_host
|
from ansible_base.lib.utils.requests import get_remote_host
|
||||||
from ansible_base.rbac.models import RoleEvaluation, RoleDefinition
|
from ansible_base.rbac.models import RoleEvaluation, RoleDefinition
|
||||||
from ansible_base.rbac.permission_registry import permission_registry
|
from ansible_base.rbac.permission_registry import permission_registry
|
||||||
from ansible_base.jwt_consumer.common.util import validate_x_trusted_proxy_header
|
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
|
||||||
@@ -44,7 +43,6 @@ from awx.main.models.rbac import give_creator_permissions
|
|||||||
from awx.main.access import optimize_queryset
|
from awx.main.access import optimize_queryset
|
||||||
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
|
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
|
||||||
from awx.main.utils.licensing import server_product_name
|
from awx.main.utils.licensing import server_product_name
|
||||||
from awx.main.utils.proxy import is_proxy_in_headers, delete_headers_starting_with_http
|
|
||||||
from awx.main.views import ApiErrorView
|
from awx.main.views import ApiErrorView
|
||||||
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer
|
from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer
|
||||||
from awx.api.versioning import URLPathVersioning
|
from awx.api.versioning import URLPathVersioning
|
||||||
@@ -155,23 +153,22 @@ class APIView(views.APIView):
|
|||||||
Store the Django REST Framework Request object as an attribute on the
|
Store the Django REST Framework Request object as an attribute on the
|
||||||
normal Django request, store time the request started.
|
normal Django request, store time the request started.
|
||||||
"""
|
"""
|
||||||
remote_headers = ['REMOTE_ADDR', 'REMOTE_HOST']
|
|
||||||
|
|
||||||
self.time_started = time.time()
|
self.time_started = time.time()
|
||||||
if getattr(settings, 'SQL_DEBUG', False):
|
if getattr(settings, 'SQL_DEBUG', False):
|
||||||
self.queries_before = len(connection.queries)
|
self.queries_before = len(connection.queries)
|
||||||
|
|
||||||
if 'HTTP_X_TRUSTED_PROXY' in request.environ:
|
|
||||||
if validate_x_trusted_proxy_header(request.environ['HTTP_X_TRUSTED_PROXY']):
|
|
||||||
remote_headers = settings.REMOTE_HOST_HEADERS
|
|
||||||
else:
|
|
||||||
logger.warning("Request appeared to be a trusted upstream proxy but failed to provide a matching shared secret.")
|
|
||||||
|
|
||||||
# If there are any custom headers in REMOTE_HOST_HEADERS, make sure
|
# If there are any custom headers in REMOTE_HOST_HEADERS, make sure
|
||||||
# they respect the allowed proxy list
|
# they respect the allowed proxy list
|
||||||
if settings.PROXY_IP_ALLOWED_LIST:
|
if all(
|
||||||
if not is_proxy_in_headers(self.request, settings.PROXY_IP_ALLOWED_LIST, remote_headers):
|
[
|
||||||
delete_headers_starting_with_http(request, settings.REMOTE_HOST_HEADERS)
|
settings.PROXY_IP_ALLOWED_LIST,
|
||||||
|
request.environ.get('REMOTE_ADDR') not in settings.PROXY_IP_ALLOWED_LIST,
|
||||||
|
request.environ.get('REMOTE_HOST') not in settings.PROXY_IP_ALLOWED_LIST,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
for custom_header in settings.REMOTE_HOST_HEADERS:
|
||||||
|
if custom_header.startswith('HTTP_'):
|
||||||
|
request.environ.pop(custom_header, None)
|
||||||
|
|
||||||
drf_request = super(APIView, self).initialize_request(request, *args, **kwargs)
|
drf_request = super(APIView, self).initialize_request(request, *args, **kwargs)
|
||||||
request.drf_request = drf_request
|
request.drf_request = drf_request
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import pytz
|
|||||||
from wsgiref.util import FileWrapper
|
from wsgiref.util import FileWrapper
|
||||||
|
|
||||||
# django-ansible-base
|
# django-ansible-base
|
||||||
from ansible_base.lib.utils.requests import get_remote_hosts
|
|
||||||
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
||||||
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
|
from ansible_base.resource_registry.shared_types import OrganizationType, TeamType, UserType
|
||||||
|
|
||||||
@@ -2771,7 +2770,12 @@ class JobTemplateCallback(GenericAPIView):
|
|||||||
host for the current request.
|
host for the current request.
|
||||||
"""
|
"""
|
||||||
# Find the list of remote host names/IPs to check.
|
# Find the list of remote host names/IPs to check.
|
||||||
remote_hosts = set(get_remote_hosts(self.request))
|
remote_hosts = set()
|
||||||
|
for header in settings.REMOTE_HOST_HEADERS:
|
||||||
|
for value in self.request.META.get(header, '').split(','):
|
||||||
|
value = value.strip()
|
||||||
|
if value:
|
||||||
|
remote_hosts.add(value)
|
||||||
# Add the reverse lookup of IP addresses.
|
# Add the reverse lookup of IP addresses.
|
||||||
for rh in list(remote_hosts):
|
for rh in list(remote_hosts):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ class InstanceGroupAccess(BaseAccess):
|
|||||||
- a superuser
|
- a superuser
|
||||||
- admin role on the Instance group
|
- admin role on the Instance group
|
||||||
I can add/delete Instance Groups:
|
I can add/delete Instance Groups:
|
||||||
- a superuser(system administrator), because these are not org-scoped
|
- a superuser(system administrator)
|
||||||
I can use Instance Groups when I have:
|
I can use Instance Groups when I have:
|
||||||
- use_role on the instance group
|
- use_role on the instance group
|
||||||
"""
|
"""
|
||||||
@@ -627,7 +627,7 @@ class InstanceGroupAccess(BaseAccess):
|
|||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
if obj.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
|
if obj.name in [settings.DEFAULT_EXECUTION_QUEUE_NAME, settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME]:
|
||||||
return False
|
return False
|
||||||
return self.user.has_obj_perm(obj, 'delete')
|
return self.user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
class UserAccess(BaseAccess):
|
class UserAccess(BaseAccess):
|
||||||
@@ -2628,7 +2628,7 @@ class ScheduleAccess(UnifiedCredentialsMixin, BaseAccess):
|
|||||||
|
|
||||||
class NotificationTemplateAccess(BaseAccess):
|
class NotificationTemplateAccess(BaseAccess):
|
||||||
"""
|
"""
|
||||||
Run standard logic from DAB RBAC
|
I can see/use a notification_template if I have permission to
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = NotificationTemplate
|
model = NotificationTemplate
|
||||||
@@ -2649,7 +2649,10 @@ class NotificationTemplateAccess(BaseAccess):
|
|||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_change(self, obj, data):
|
def can_change(self, obj, data):
|
||||||
return self.user.has_obj_perm(obj, 'change') and self.check_related('organization', Organization, data, obj=obj, role_field='notification_admin_role')
|
if obj.organization is None:
|
||||||
|
# only superusers are allowed to edit orphan notification templates
|
||||||
|
return False
|
||||||
|
return self.check_related('organization', Organization, data, obj=obj, role_field='notification_admin_role', mandatory=True)
|
||||||
|
|
||||||
def can_admin(self, obj, data):
|
def can_admin(self, obj, data):
|
||||||
return self.can_change(obj, data)
|
return self.can_change(obj, data)
|
||||||
@@ -2659,7 +2662,9 @@ class NotificationTemplateAccess(BaseAccess):
|
|||||||
|
|
||||||
@check_superuser
|
@check_superuser
|
||||||
def can_start(self, obj, validate_license=True):
|
def can_start(self, obj, validate_license=True):
|
||||||
return self.can_change(obj, None)
|
if obj.organization is None:
|
||||||
|
return False
|
||||||
|
return self.user in obj.organization.notification_admin_role
|
||||||
|
|
||||||
|
|
||||||
class NotificationAccess(BaseAccess):
|
class NotificationAccess(BaseAccess):
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ def setup_managed_role_definitions(apps, schema_editor):
|
|||||||
managed_role_definitions = []
|
managed_role_definitions = []
|
||||||
|
|
||||||
org_perms = set()
|
org_perms = set()
|
||||||
for cls in permission_registry.all_registered_models:
|
for cls in permission_registry._registry:
|
||||||
ct = ContentType.objects.get_for_model(cls)
|
ct = ContentType.objects.get_for_model(cls)
|
||||||
object_perms = set(Permission.objects.filter(content_type=ct))
|
object_perms = set(Permission.objects.filter(content_type=ct))
|
||||||
# Special case for InstanceGroup which has an organiation field, but is not an organization child object
|
# Special case for InstanceGroup which has an organiation field, but is not an organization child object
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from awx.api.versioning import reverse
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
from django.test.utils import override_settings
|
|
||||||
|
|
||||||
from ansible_base.jwt_consumer.common.util import generate_x_trusted_proxy_header
|
|
||||||
from ansible_base.lib.testing.fixtures import rsa_keypair_factory, rsa_keypair # noqa: F401; pylint: disable=unused-import
|
|
||||||
|
|
||||||
|
|
||||||
class HeaderTrackingMiddleware(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.environ = {}
|
|
||||||
|
|
||||||
def process_request(self, request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def process_response(self, request, response):
|
|
||||||
self.environ = request.environ
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_proxy_ip_allowed(get, patch, admin):
|
def test_proxy_ip_allowed(get, patch, admin):
|
||||||
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'system'})
|
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'system'})
|
||||||
patch(url, user=admin, data={'REMOTE_HOST_HEADERS': ['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST']})
|
patch(url, user=admin, data={'REMOTE_HOST_HEADERS': ['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST']})
|
||||||
|
|
||||||
|
class HeaderTrackingMiddleware(object):
|
||||||
|
environ = {}
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_response(self, request, response):
|
||||||
|
self.environ = request.environ
|
||||||
|
|
||||||
# By default, `PROXY_IP_ALLOWED_LIST` is disabled, so custom `REMOTE_HOST_HEADERS`
|
# By default, `PROXY_IP_ALLOWED_LIST` is disabled, so custom `REMOTE_HOST_HEADERS`
|
||||||
# should just pass through
|
# should just pass through
|
||||||
middleware = HeaderTrackingMiddleware()
|
middleware = HeaderTrackingMiddleware()
|
||||||
@@ -53,51 +45,6 @@ def test_proxy_ip_allowed(get, patch, admin):
|
|||||||
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestTrustedProxyAllowListIntegration:
|
|
||||||
@pytest.fixture
|
|
||||||
def url(self, patch, admin):
|
|
||||||
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'system'})
|
|
||||||
patch(url, user=admin, data={'REMOTE_HOST_HEADERS': ['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST']})
|
|
||||||
patch(url, user=admin, data={'PROXY_IP_ALLOWED_LIST': ['my.proxy.example.org']})
|
|
||||||
return url
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def middleware(self):
|
|
||||||
return HeaderTrackingMiddleware()
|
|
||||||
|
|
||||||
def test_x_trusted_proxy_valid_signature(self, get, admin, rsa_keypair, url, middleware): # noqa: F811
|
|
||||||
# Headers should NOT get deleted
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_TRUSTED_PROXY': generate_x_trusted_proxy_header(rsa_keypair.private),
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'some-actual-ip',
|
|
||||||
}
|
|
||||||
with mock.patch('ansible_base.jwt_consumer.common.cache.JWTCache.get_key_from_cache', lambda self: None):
|
|
||||||
with override_settings(ANSIBLE_BASE_JWT_KEY=rsa_keypair.public, PROXY_IP_ALLOWED_LIST=[]):
|
|
||||||
get(url, user=admin, middleware=middleware, **headers)
|
|
||||||
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
|
||||||
|
|
||||||
def test_x_trusted_proxy_invalid_signature(self, get, admin, url, patch, middleware):
|
|
||||||
# Headers should NOT get deleted
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_TRUSTED_PROXY': 'DEAD-BEEF',
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'some-actual-ip',
|
|
||||||
}
|
|
||||||
with override_settings(PROXY_IP_ALLOWED_LIST=[]):
|
|
||||||
get(url, user=admin, middleware=middleware, **headers)
|
|
||||||
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
|
||||||
|
|
||||||
def test_x_trusted_proxy_invalid_signature_valid_proxy(self, get, admin, url, middleware):
|
|
||||||
# A valid explicit proxy SHOULD result in sensitive headers NOT being deleted, regardless of the trusted proxy signature results
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_TRUSTED_PROXY': 'DEAD-BEEF',
|
|
||||||
'REMOTE_ADDR': 'my.proxy.example.org',
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'some-actual-ip',
|
|
||||||
}
|
|
||||||
get(url, user=admin, middleware=middleware, **headers)
|
|
||||||
assert middleware.environ['HTTP_X_FROM_THE_LOAD_BALANCER'] == 'some-actual-ip'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestDeleteViews:
|
class TestDeleteViews:
|
||||||
def test_sublist_delete_permission_check(self, inventory_source, host, rando, delete):
|
def test_sublist_delete_permission_check(self, inventory_source, host, rando, delete):
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ def node_type_instance():
|
|||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def instance_group(job_factory):
|
||||||
|
ig = InstanceGroup(name="east")
|
||||||
|
ig.save()
|
||||||
|
return ig
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def containerized_instance_group(instance_group, kube_credential):
|
def containerized_instance_group(instance_group, kube_credential):
|
||||||
ig = InstanceGroup(name="container")
|
ig = InstanceGroup(name="container")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.api.serializers import JobTemplateSerializer
|
from awx.api.serializers import JobTemplateSerializer
|
||||||
@@ -9,15 +8,10 @@ from awx.main.migrations import _save_password_keys as save_password_keys
|
|||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.test.utils import override_settings
|
|
||||||
|
|
||||||
# DRF
|
# DRF
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
# DAB
|
|
||||||
from ansible_base.jwt_consumer.common.util import generate_x_trusted_proxy_header
|
|
||||||
from ansible_base.lib.testing.fixtures import rsa_keypair_factory, rsa_keypair # noqa: F401; pylint: disable=unused-import
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -375,113 +369,3 @@ def test_job_template_missing_inventory(project, inventory, admin_user, post):
|
|||||||
)
|
)
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
assert "Cannot start automatically, an inventory is required." in str(r.data)
|
assert "Cannot start automatically, an inventory is required." in str(r.data)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestJobTemplateCallbackProxyIntegration:
|
|
||||||
"""
|
|
||||||
Test the interaction of provision job template callback feature and:
|
|
||||||
settings.PROXY_IP_ALLOWED_LIST
|
|
||||||
x-trusted-proxy http header
|
|
||||||
"""
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def job_template(self, inventory, project):
|
|
||||||
jt = JobTemplate.objects.create(name='test-jt', inventory=inventory, project=project, playbook='helloworld.yml', host_config_key='abcd')
|
|
||||||
return jt
|
|
||||||
|
|
||||||
@override_settings(REMOTE_HOST_HEADERS=['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST'], PROXY_IP_ALLOWED_LIST=['my.proxy.example.org'])
|
|
||||||
def test_host_not_found(self, job_template, admin_user, post, rsa_keypair): # noqa: F811
|
|
||||||
job_template.inventory.hosts.create(name='foobar')
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'baz',
|
|
||||||
'REMOTE_HOST': 'baz',
|
|
||||||
'REMOTE_ADDR': 'baz',
|
|
||||||
}
|
|
||||||
r = post(
|
|
||||||
url=reverse('api:job_template_callback', kwargs={'pk': job_template.pk}), data={'host_config_key': 'abcd'}, user=admin_user, expect=400, **headers
|
|
||||||
)
|
|
||||||
assert r.data['msg'] == 'No matching host could be found!'
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
'headers, expected',
|
|
||||||
(
|
|
||||||
pytest.param(
|
|
||||||
{
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'foobar',
|
|
||||||
'REMOTE_HOST': 'my.proxy.example.org',
|
|
||||||
},
|
|
||||||
201,
|
|
||||||
),
|
|
||||||
pytest.param(
|
|
||||||
{
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'foobar',
|
|
||||||
'REMOTE_HOST': 'not-my-proxy.org',
|
|
||||||
},
|
|
||||||
400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@override_settings(REMOTE_HOST_HEADERS=['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST'], PROXY_IP_ALLOWED_LIST=['my.proxy.example.org'])
|
|
||||||
def test_proxy_ip_allowed_list(self, job_template, admin_user, post, headers, expected): # noqa: F811
|
|
||||||
job_template.inventory.hosts.create(name='my.proxy.example.org')
|
|
||||||
|
|
||||||
post(
|
|
||||||
url=reverse('api:job_template_callback', kwargs={'pk': job_template.pk}),
|
|
||||||
data={'host_config_key': 'abcd'},
|
|
||||||
user=admin_user,
|
|
||||||
expect=expected,
|
|
||||||
**headers
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(REMOTE_HOST_HEADERS=['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST'], PROXY_IP_ALLOWED_LIST=[])
|
|
||||||
def test_no_proxy_trust_all_headers(self, job_template, admin_user, post):
|
|
||||||
job_template.inventory.hosts.create(name='foobar')
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'foobar',
|
|
||||||
'REMOTE_ADDR': 'bar',
|
|
||||||
'REMOTE_HOST': 'baz',
|
|
||||||
}
|
|
||||||
post(url=reverse('api:job_template_callback', kwargs={'pk': job_template.pk}), data={'host_config_key': 'abcd'}, user=admin_user, expect=201, **headers)
|
|
||||||
|
|
||||||
@override_settings(REMOTE_HOST_HEADERS=['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST'], PROXY_IP_ALLOWED_LIST=['my.proxy.example.org'])
|
|
||||||
def test_trusted_proxy(self, job_template, admin_user, post, rsa_keypair): # noqa: F811
|
|
||||||
job_template.inventory.hosts.create(name='foobar')
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_TRUSTED_PROXY': generate_x_trusted_proxy_header(rsa_keypair.private),
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'foobar, my.proxy.example.org',
|
|
||||||
}
|
|
||||||
|
|
||||||
with mock.patch('ansible_base.jwt_consumer.common.cache.JWTCache.get_key_from_cache', lambda self: None):
|
|
||||||
with override_settings(ANSIBLE_BASE_JWT_KEY=rsa_keypair.public):
|
|
||||||
post(
|
|
||||||
url=reverse('api:job_template_callback', kwargs={'pk': job_template.pk}),
|
|
||||||
data={'host_config_key': 'abcd'},
|
|
||||||
user=admin_user,
|
|
||||||
expect=201,
|
|
||||||
**headers
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(REMOTE_HOST_HEADERS=['HTTP_X_FROM_THE_LOAD_BALANCER', 'REMOTE_ADDR', 'REMOTE_HOST'], PROXY_IP_ALLOWED_LIST=['my.proxy.example.org'])
|
|
||||||
def test_trusted_proxy_host_not_found(self, job_template, admin_user, post, rsa_keypair): # noqa: F811
|
|
||||||
job_template.inventory.hosts.create(name='foobar')
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'HTTP_X_TRUSTED_PROXY': generate_x_trusted_proxy_header(rsa_keypair.private),
|
|
||||||
'HTTP_X_FROM_THE_LOAD_BALANCER': 'baz, my.proxy.example.org',
|
|
||||||
'REMOTE_ADDR': 'bar',
|
|
||||||
'REMOTE_HOST': 'baz',
|
|
||||||
}
|
|
||||||
|
|
||||||
with mock.patch('ansible_base.jwt_consumer.common.cache.JWTCache.get_key_from_cache', lambda self: None):
|
|
||||||
with override_settings(ANSIBLE_BASE_JWT_KEY=rsa_keypair.public):
|
|
||||||
post(
|
|
||||||
url=reverse('api:job_template_callback', kwargs={'pk': job_template.pk}),
|
|
||||||
data={'host_config_key': 'abcd'},
|
|
||||||
user=admin_user,
|
|
||||||
expect=400,
|
|
||||||
**headers
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from awx.main.migrations._dab_rbac import setup_managed_role_definitions
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models.projects import Project
|
from awx.main.models.projects import Project
|
||||||
from awx.main.models.ha import Instance, InstanceGroup
|
from awx.main.models.ha import Instance
|
||||||
|
|
||||||
from rest_framework.test import (
|
from rest_framework.test import (
|
||||||
APIRequestFactory,
|
APIRequestFactory,
|
||||||
@@ -730,11 +730,6 @@ def jt_linked(organization, project, inventory, machine_credential, credential,
|
|||||||
return jt
|
return jt
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def instance_group():
|
|
||||||
return InstanceGroup.objects.create(name="east")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def workflow_job_template(organization):
|
def workflow_job_template(organization):
|
||||||
wjt = WorkflowJobTemplate.objects.create(name='test-workflow_job_template', organization=organization)
|
wjt = WorkflowJobTemplate.objects.create(name='test-workflow_job_template', organization=organization)
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from awx.main.access import InstanceGroupAccess, NotificationTemplateAccess
|
|
||||||
|
|
||||||
from ansible_base.rbac.models import RoleDefinition
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_instance_group_object_role_delete(rando, instance_group, setup_managed_roles):
|
|
||||||
"""Basic functionality of IG object-level admin role function AAP-25506"""
|
|
||||||
rd = RoleDefinition.objects.get(name='InstanceGroup Admin')
|
|
||||||
rd.give_permission(rando, instance_group)
|
|
||||||
access = InstanceGroupAccess(rando)
|
|
||||||
assert access.can_delete(instance_group)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_notification_template_object_role_change(rando, notification_template, setup_managed_roles):
|
|
||||||
"""Basic functionality of NT object-level admin role function AAP-25493"""
|
|
||||||
rd = RoleDefinition.objects.get(name='NotificationTemplate Admin')
|
|
||||||
rd.give_permission(rando, notification_template)
|
|
||||||
access = NotificationTemplateAccess(rando)
|
|
||||||
assert access.can_change(notification_template, {'name': 'new name'})
|
|
||||||
@@ -99,9 +99,7 @@ def test_notification_template_access_org_user(notification_template, user):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_notificaiton_template_orphan_access_org_admin(notification_template, organization, org_admin):
|
def test_notificaiton_template_orphan_access_org_admin(notification_template, organization, org_admin):
|
||||||
notification_template.organization = None
|
notification_template.organization = None
|
||||||
notification_template.save(update_fields=['organization'])
|
|
||||||
access = NotificationTemplateAccess(org_admin)
|
access = NotificationTemplateAccess(org_admin)
|
||||||
assert not org_admin.has_obj_perm(notification_template, 'change')
|
|
||||||
assert not access.can_change(notification_template, {'organization': organization.id})
|
assert not access.can_change(notification_template, {'organization': organization.id})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# Copyright (c) 2024 Ansible, Inc.
|
|
||||||
# All Rights Reserved.
|
|
||||||
|
|
||||||
|
|
||||||
# DRF
|
|
||||||
from rest_framework.request import Request
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Note that these methods operate on request.environ. This data is from uwsgi.
|
|
||||||
It is the source data from which request.headers (read-only) is constructed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def is_proxy_in_headers(request: Request, proxy_list: list[str], headers: list[str]) -> bool:
|
|
||||||
"""
|
|
||||||
Determine if the request went through at least one proxy in the list.
|
|
||||||
Example:
|
|
||||||
request.environ = {
|
|
||||||
"HTTP_X_FOO": "8.8.8.8, 192.168.2.1",
|
|
||||||
"REMOTE_ADDR": "192.168.2.1",
|
|
||||||
"REMOTE_HOST": "foobar"
|
|
||||||
}
|
|
||||||
proxy_list = ["192.168.2.1"]
|
|
||||||
headers = ["HTTP_X_FOO", "REMOTE_ADDR", "REMOTE_HOST"]
|
|
||||||
|
|
||||||
The above would return True since 192.168.2.1 is a value for the header HTTP_X_FOO
|
|
||||||
|
|
||||||
request: The DRF/Django request. request.environ dict will be used for searching for proxies
|
|
||||||
proxy_list: A list of known and trusted proxies may be ip or hostnames
|
|
||||||
headers: A list of keys for which to consider values that may contain a proxy
|
|
||||||
"""
|
|
||||||
|
|
||||||
remote_hosts = set()
|
|
||||||
|
|
||||||
for header in headers:
|
|
||||||
for value in request.environ.get(header, '').split(','):
|
|
||||||
value = value.strip()
|
|
||||||
if value:
|
|
||||||
remote_hosts.add(value)
|
|
||||||
|
|
||||||
return bool(remote_hosts.intersection(set(proxy_list)))
|
|
||||||
|
|
||||||
|
|
||||||
def delete_headers_starting_with_http(request: Request, headers: list[str]):
|
|
||||||
for header in headers:
|
|
||||||
if header.startswith('HTTP_'):
|
|
||||||
request.environ.pop(header, None)
|
|
||||||
48
awxkit/awxkit/api/pages/role_assignments.py
Normal file
48
awxkit/awxkit/api/pages/role_assignments.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
# from awxkit.api.mixins import DSAdapter, HasCreate, HasCopy
|
||||||
|
# from awxkit.api.pages import (
|
||||||
|
# Credential,
|
||||||
|
# Organization,
|
||||||
|
# )
|
||||||
|
from awxkit.api.resources import resources
|
||||||
|
|
||||||
|
# from awxkit.utils import random_title, PseudoNamespace, filter_by_class
|
||||||
|
|
||||||
|
from . import base
|
||||||
|
from . import page
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleTeamAssignment(base.Base):
|
||||||
|
NATURAL_KEY = ('team', 'content_object', 'role_definition')
|
||||||
|
|
||||||
|
|
||||||
|
page.register_page(
|
||||||
|
[resources.role_team_assignment, (resources.role_definition_team_assignments, 'post'), (resources.role_team_assignments, 'post')], RoleTeamAssignment
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUserAssignment(base.Base):
|
||||||
|
NATURAL_KEY = ('user', 'content_object', 'role_definition')
|
||||||
|
|
||||||
|
|
||||||
|
page.register_page(
|
||||||
|
[resources.role_user_assignment, (resources.role_definition_user_assignments, 'post'), (resources.role_user_assignments, 'post')], RoleUserAssignment
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleTeamAssignments(page.PageList, RoleTeamAssignment):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
page.register_page([resources.role_definition_team_assignments, resources.role_team_assignments], RoleTeamAssignments)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUserAssignments(page.PageList, RoleUserAssignment):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
page.register_page([resources.role_definition_user_assignments, resources.role_user_assignments], RoleUserAssignments)
|
||||||
30
awxkit/awxkit/api/pages/role_definitions.py
Normal file
30
awxkit/awxkit/api/pages/role_definitions.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
# from awxkit.api.mixins import DSAdapter, HasCreate, HasCopy
|
||||||
|
# from awxkit.api.pages import (
|
||||||
|
# Credential,
|
||||||
|
# Organization,
|
||||||
|
# )
|
||||||
|
from awxkit.api.resources import resources
|
||||||
|
|
||||||
|
# from awxkit.utils import random_title, PseudoNamespace, filter_by_class
|
||||||
|
|
||||||
|
from . import base
|
||||||
|
from . import page
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinition(base.Base):
|
||||||
|
NATURAL_KEY = ('name',)
|
||||||
|
|
||||||
|
|
||||||
|
page.register_page([resources.role_definition, (resources.role_definitions, 'post')], RoleDefinition)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitions(page.PageList, RoleDefinition):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
page.register_page([resources.role_definitions], RoleDefinitions)
|
||||||
@@ -197,6 +197,14 @@ class Resources(object):
|
|||||||
_related_users = r'\w+/\d+/users/'
|
_related_users = r'\w+/\d+/users/'
|
||||||
_related_workflow_job_templates = r'\w+/\d+/workflow_job_templates/'
|
_related_workflow_job_templates = r'\w+/\d+/workflow_job_templates/'
|
||||||
_role = r'roles/\d+/'
|
_role = r'roles/\d+/'
|
||||||
|
_role_definition = r'role_definitions/\d+/'
|
||||||
|
_role_definitions = r'role_definitions/'
|
||||||
|
_role_definition_team_assignments = r'role_definitions/\d+/team_assignments/'
|
||||||
|
_role_definition_user_assignments = r'role_definitions/\d+/user_assignments/'
|
||||||
|
_role_team_assignment = r'role_team_assignments/\d+/'
|
||||||
|
_role_team_assignments = r'role_team_assignments/'
|
||||||
|
_role_user_assignment = r'role_user_assignments/\d+/'
|
||||||
|
_role_user_assignments = r'role_user_assignments/'
|
||||||
_roles = 'roles/'
|
_roles = 'roles/'
|
||||||
_roles_related_teams = r'roles/\d+/teams/'
|
_roles_related_teams = r'roles/\d+/teams/'
|
||||||
_schedule = r'schedules/\d+/'
|
_schedule = r'schedules/\d+/'
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ def resolve(obj, path):
|
|||||||
if new_obj is None:
|
if new_obj is None:
|
||||||
return set()
|
return set()
|
||||||
if not path:
|
if not path:
|
||||||
return {
|
return {new_obj,}
|
||||||
new_obj,
|
|
||||||
}
|
|
||||||
|
|
||||||
if isinstance(new_obj, ManyToManyDescriptor):
|
if isinstance(new_obj, ManyToManyDescriptor):
|
||||||
return {x for o in new_obj.all() for x in resolve(o, path)}
|
return {x for o in new_obj.all() for x in resolve(o, path)}
|
||||||
@@ -55,9 +53,7 @@ for ct in ContentType.objects.order_by('id'):
|
|||||||
crosslinked[ct.id][obj.id][f'{f.name}_id'] = None
|
crosslinked[ct.id][obj.id][f'{f.name}_id'] = None
|
||||||
continue
|
continue
|
||||||
if r.content_object != obj:
|
if r.content_object != obj:
|
||||||
sys.stderr.write(
|
sys.stderr.write(f"{cls.__name__} id={obj.id} {f.name} is pointing to a Role that is assigned to a different object: role.id={r.id} {r.content_type!r} {r.object_id} {r.role_field}\n")
|
||||||
f"{cls.__name__} id={obj.id} {f.name} is pointing to a Role that is assigned to a different object: role.id={r.id} {r.content_type!r} {r.object_id} {r.role_field}\n"
|
|
||||||
)
|
|
||||||
crosslinked[ct.id][obj.id][f'{f.name}_id'] = None
|
crosslinked[ct.id][obj.id][f'{f.name}_id'] = None
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -91,23 +87,16 @@ for r in Role.objects.exclude(role_field__startswith='system_').order_by('id'):
|
|||||||
|
|
||||||
# Check the resource's role field parents for consistency with Role.parents.all().
|
# Check the resource's role field parents for consistency with Role.parents.all().
|
||||||
f = r.content_object._meta.get_field(r.role_field)
|
f = r.content_object._meta.get_field(r.role_field)
|
||||||
f_parent = (
|
f_parent = set(f.parent_role) if isinstance(f.parent_role, list) else {f.parent_role,}
|
||||||
set(f.parent_role)
|
|
||||||
if isinstance(f.parent_role, list)
|
|
||||||
else {
|
|
||||||
f.parent_role,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
dotted = {x for p in f_parent if '.' in p for x in resolve(r.content_object, p)}
|
dotted = {x for p in f_parent if '.' in p for x in resolve(r.content_object, p)}
|
||||||
plus = set()
|
plus = set()
|
||||||
for p in r.parents.all():
|
for p in r.parents.all():
|
||||||
if p.singleton_name:
|
if p.singleton_name:
|
||||||
if f'singleton:{p.singleton_name}' not in f_parent:
|
if f'singleton:{p.singleton_name}' not in f_parent:
|
||||||
plus.add(p)
|
plus.add(p)
|
||||||
elif p.content_type == team_ct:
|
elif (p.content_type, p.role_field) == (team_ct, 'member_role'):
|
||||||
# Team has been granted this role; probably legitimate.
|
# Team has been granted this role; probably legitimate.
|
||||||
if p.role_field in ('admin_role', 'member_role'):
|
continue
|
||||||
continue
|
|
||||||
elif (p.content_type, p.object_id) == (r.content_type, r.object_id):
|
elif (p.content_type, p.object_id) == (r.content_type, r.object_id):
|
||||||
if p.role_field not in f_parent:
|
if p.role_field not in f_parent:
|
||||||
plus.add(p)
|
plus.add(p)
|
||||||
@@ -129,17 +118,13 @@ for r in Role.objects.exclude(role_field__startswith='system_').order_by('id'):
|
|||||||
continue
|
continue
|
||||||
if rev is None or r.id != rev.id:
|
if rev is None or r.id != rev.id:
|
||||||
if rev and (r.content_type_id, r.object_id, r.role_field) == (rev.content_type_id, rev.object_id, rev.role_field):
|
if rev and (r.content_type_id, r.object_id, r.role_field) == (rev.content_type_id, rev.object_id, rev.role_field):
|
||||||
sys.stderr.write(
|
sys.stderr.write(f"Role id={r.id} {r.content_type!r} {r.object_id} {r.role_field} is an orphaned duplicate of Role id={rev.id}, which is actually being used by the assigned resource\n")
|
||||||
f"Role id={r.id} {r.content_type!r} {r.object_id} {r.role_field} is an orphaned duplicate of Role id={rev.id}, which is actually being used by the assigned resource\n"
|
|
||||||
)
|
|
||||||
orphaned_roles.add(r.id)
|
orphaned_roles.add(r.id)
|
||||||
elif not rev:
|
elif not rev:
|
||||||
sys.stderr.write(f"Role id={r.id} {r.content_type!r} {r.object_id} {r.role_field} is pointing to an object currently using no role\n")
|
sys.stderr.write(f"Role id={r.id} {r.content_type!r} {r.object_id} {r.role_field} is pointing to an object currently using no role\n")
|
||||||
crosslinked[r.content_type_id][r.object_id][f'{r.role_field}_id'] = r.id
|
crosslinked[r.content_type_id][r.object_id][f'{r.role_field}_id'] = r.id
|
||||||
else:
|
else:
|
||||||
sys.stderr.write(
|
sys.stderr.write(f"Role id={r.id} {r.content_type!r} {r.object_id} {r.role_field} is pointing to an object using a different role: id={rev.id} {rev.content_type!r} {rev.object_id} {rev.role_field}\n")
|
||||||
f"Role id={r.id} {r.content_type!r} {r.object_id} {r.role_field} is pointing to an object using a different role: id={rev.id} {rev.content_type!r} {rev.object_id} {rev.role_field}\n"
|
|
||||||
)
|
|
||||||
crosslinked[r.content_type_id][r.object_id][f'{r.role_field}_id'] = r.id
|
crosslinked[r.content_type_id][r.object_id][f'{r.role_field}_id'] = r.id
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -147,8 +132,7 @@ for r in Role.objects.exclude(role_field__startswith='system_').order_by('id'):
|
|||||||
sys.stderr.write('===================================\n')
|
sys.stderr.write('===================================\n')
|
||||||
|
|
||||||
|
|
||||||
print(
|
print(f"""\
|
||||||
f"""\
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@@ -160,8 +144,7 @@ from awx.main.models.rbac import Role
|
|||||||
delete_counts = Counter()
|
delete_counts = Counter()
|
||||||
update_counts = Counter()
|
update_counts = Counter()
|
||||||
|
|
||||||
"""
|
""")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
print("# Resource objects that are pointing to the wrong Role. Some of these")
|
print("# Resource objects that are pointing to the wrong Role. Some of these")
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ with connection.cursor() as cursor:
|
|||||||
cursor.execute("UPDATE main_instancegroup SET use_role_id = NULL WHERE name = 'red'")
|
cursor.execute("UPDATE main_instancegroup SET use_role_id = NULL WHERE name = 'red'")
|
||||||
cursor.execute(f"UPDATE main_instancegroup SET use_role_id = {green.use_role_id} WHERE name = 'yellow'")
|
cursor.execute(f"UPDATE main_instancegroup SET use_role_id = {green.use_role_id} WHERE name = 'yellow'")
|
||||||
|
|
||||||
cursor.execute(
|
cursor.execute("ALTER TABLE main_instancegroup ADD CONSTRAINT main_instancegroup_use_role_id_48ea7ecc_fk_main_rbac_roles_id FOREIGN KEY (use_role_id) REFERENCES public.main_rbac_roles(id) DEFERRABLE INITIALLY DEFERRED NOT VALID")
|
||||||
"ALTER TABLE main_instancegroup ADD CONSTRAINT main_instancegroup_use_role_id_48ea7ecc_fk_main_rbac_roles_id FOREIGN KEY (use_role_id) REFERENCES public.main_rbac_roles(id) DEFERRABLE INITIALLY DEFERRED NOT VALID"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("=====================================")
|
print("=====================================")
|
||||||
for ig in InstanceGroup.objects.all():
|
for ig in InstanceGroup.objects.all():
|
||||||
|
|||||||
Reference in New Issue
Block a user