From f4347d05a904b41f07f447a108908864c6d2f6e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Je=C5=99=C3=A1bek=20=28Jiri=20Jerabek=29?= Date: Mon, 9 Jun 2025 17:47:23 +0200 Subject: [PATCH] cherry-pick 222f387 to release_4.6 (#6971) --- awx/api/serializers.py | 14 +- awx/main/conf.py | 234 +++++++++---------- awx/main/tasks/policy.py | 4 - awx/main/tests/functional/test_policy.py | 4 +- awx/settings/defaults.py | 2 +- awx_collection/test/awx/test_completeness.py | 3 + 6 files changed, 126 insertions(+), 135 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index ad081a34fd..df9f5e4915 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -50,9 +50,6 @@ from ansible_base.lib.utils.models import get_type_for_model from ansible_base.rbac.models import RoleEvaluation, ObjectRole from ansible_base.rbac import permission_registry -# django-flags -from flags.state import flag_enabled - # AWX from awx.main.access import get_user_capabilities from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission @@ -745,13 +742,10 @@ class EmptySerializer(serializers.Serializer): pass -class OpaQueryPathEnabledMixin(serializers.Serializer): +class OpaQueryPathMixin(serializers.Serializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if not flag_enabled("FEATURE_POLICY_AS_CODE_ENABLED") and 'opa_query_path' in self.fields: - self.fields.pop('opa_query_path') - def validate_opa_query_path(self, value): # Decode the URL and re-encode it decoded_value = urllib.parse.unquote(value) @@ -763,7 +757,7 @@ class OpaQueryPathEnabledMixin(serializers.Serializer): return value -class UnifiedJobTemplateSerializer(BaseSerializer, OpaQueryPathEnabledMixin): +class UnifiedJobTemplateSerializer(BaseSerializer, OpaQueryPathMixin): # As a base serializer, the capabilities prefetch is not used directly, # instead they are derived from the Workflow Job Template Serializer and the Job Template Serializer, respectively. capabilities_prefetch = [] @@ -1440,7 +1434,7 @@ class OAuth2ApplicationSerializer(BaseSerializer): return ret -class OrganizationSerializer(BaseSerializer, OpaQueryPathEnabledMixin): +class OrganizationSerializer(BaseSerializer, OpaQueryPathMixin): show_capabilities = ['edit', 'delete'] class Meta: @@ -1800,7 +1794,7 @@ class LabelsListMixin(object): return res -class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathEnabledMixin): +class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin): show_capabilities = ['edit', 'delete', 'adhoc', 'copy'] capabilities_prefetch = ['admin', 'adhoc', {'copy': 'organization.inventory_admin'}] diff --git a/awx/main/conf.py b/awx/main/conf.py index cfeb745109..c93a00b36e 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -4,7 +4,6 @@ import logging # Django from django.core.checks import Error from django.utils.translation import gettext_lazy as _ -from django.conf import settings # Django REST Framework from rest_framework import serializers @@ -997,131 +996,132 @@ def csrf_trusted_origins_validate(serializer, attrs): register_validate('system', csrf_trusted_origins_validate) -if settings.FEATURE_POLICY_AS_CODE_ENABLED: # Unable to use flag_enabled due to AppRegistryNotReady error - register( - 'OPA_HOST', - field_class=fields.CharField, - label=_('OPA server hostname'), - default='', - help_text=_('The hostname used to connect to the OPA server. If empty, policy enforcement will be disabled.'), - category=('PolicyAsCode'), - category_slug='policyascode', - allow_blank=True, - ) +register( + 'OPA_HOST', + field_class=fields.CharField, + label=_('OPA server hostname'), + default='', + help_text=_('The hostname used to connect to the OPA server. If empty, policy enforcement will be disabled.'), + category=('PolicyAsCode'), + category_slug='policyascode', + allow_blank=True, +) - register( - 'OPA_PORT', - field_class=fields.IntegerField, - label=_('OPA server port'), - default=8181, - help_text=_('The port used to connect to the OPA server. Defaults to 8181.'), - category=('PolicyAsCode'), - category_slug='policyascode', - ) +register( + 'OPA_PORT', + field_class=fields.IntegerField, + label=_('OPA server port'), + default=8181, + help_text=_('The port used to connect to the OPA server. Defaults to 8181.'), + category=('PolicyAsCode'), + category_slug='policyascode', +) - register( - 'OPA_SSL', - field_class=fields.BooleanField, - label=_('Use SSL for OPA connection'), - default=False, - help_text=_('Enable or disable the use of SSL to connect to the OPA server. Defaults to false.'), - category=('PolicyAsCode'), - category_slug='policyascode', - ) +register( + 'OPA_SSL', + field_class=fields.BooleanField, + label=_('Use SSL for OPA connection'), + default=False, + help_text=_('Enable or disable the use of SSL to connect to the OPA server. Defaults to false.'), + category=('PolicyAsCode'), + category_slug='policyascode', +) - register( - 'OPA_AUTH_TYPE', - field_class=fields.ChoiceField, - label=_('OPA authentication type'), - choices=[OPA_AUTH_TYPES.NONE, OPA_AUTH_TYPES.TOKEN, OPA_AUTH_TYPES.CERTIFICATE], - default=OPA_AUTH_TYPES.NONE, - help_text=_('The authentication type that will be used to connect to the OPA server: "None", "Token", or "Certificate".'), - category=('PolicyAsCode'), - category_slug='policyascode', - ) +register( + 'OPA_AUTH_TYPE', + field_class=fields.ChoiceField, + label=_('OPA authentication type'), + choices=[OPA_AUTH_TYPES.NONE, OPA_AUTH_TYPES.TOKEN, OPA_AUTH_TYPES.CERTIFICATE], + default=OPA_AUTH_TYPES.NONE, + help_text=_('The authentication type that will be used to connect to the OPA server: "None", "Token", or "Certificate".'), + category=('PolicyAsCode'), + category_slug='policyascode', +) - register( - 'OPA_AUTH_TOKEN', - field_class=fields.CharField, - label=_('OPA authentication token'), - default='', - help_text=_( - 'The token for authentication to the OPA server. Required when OPA_AUTH_TYPE is "Token". If an authorization header is defined in OPA_AUTH_CUSTOM_HEADERS, it will be overridden by OPA_AUTH_TOKEN.' - ), - category=('PolicyAsCode'), - category_slug='policyascode', - allow_blank=True, - encrypted=True, - ) +register( + 'OPA_AUTH_TOKEN', + field_class=fields.CharField, + label=_('OPA authentication token'), + default='', + help_text=_( + 'The token for authentication to the OPA server. Required when OPA_AUTH_TYPE is "Token". If an authorization header is defined in OPA_AUTH_CUSTOM_HEADERS, it will be overridden by OPA_AUTH_TOKEN.' + ), + category=('PolicyAsCode'), + category_slug='policyascode', + allow_blank=True, + encrypted=True, +) - register( - 'OPA_AUTH_CLIENT_CERT', - field_class=fields.CharField, - label=_('OPA client certificate content'), - default='', - help_text=_('The content of the client certificate file for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), - category=('PolicyAsCode'), - category_slug='policyascode', - allow_blank=True, - ) +register( + 'OPA_AUTH_CLIENT_CERT', + field_class=fields.CharField, + label=_('OPA client certificate content'), + default='', + help_text=_('The content of the client certificate file for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), + category=('PolicyAsCode'), + category_slug='policyascode', + allow_blank=True, +) - register( - 'OPA_AUTH_CLIENT_KEY', - field_class=fields.CharField, - label=_('OPA client key content'), - default='', - help_text=_('The content of the client key for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), - category=('PolicyAsCode'), - category_slug='policyascode', - allow_blank=True, - encrypted=True, - ) +register( + 'OPA_AUTH_CLIENT_KEY', + field_class=fields.CharField, + label=_('OPA client key content'), + default='', + help_text=_('The content of the client key for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), + category=('PolicyAsCode'), + category_slug='policyascode', + allow_blank=True, + encrypted=True, +) - register( - 'OPA_AUTH_CA_CERT', - field_class=fields.CharField, - label=_('OPA CA certificate content'), - default='', - help_text=_('The content of the CA certificate for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), - category=('PolicyAsCode'), - category_slug='policyascode', - allow_blank=True, - ) +register( + 'OPA_AUTH_CA_CERT', + field_class=fields.CharField, + label=_('OPA CA certificate content'), + default='', + help_text=_('The content of the CA certificate for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), + category=('PolicyAsCode'), + category_slug='policyascode', + allow_blank=True, +) - register( - 'OPA_AUTH_CUSTOM_HEADERS', - field_class=fields.DictField, - label=_('OPA custom authentication headers'), - default={}, - help_text=_('Optional custom headers included in requests to the OPA server. Defaults to empty dictionary ({}).'), - category=('PolicyAsCode'), - category_slug='policyascode', - ) +register( + 'OPA_AUTH_CUSTOM_HEADERS', + field_class=fields.DictField, + label=_('OPA custom authentication headers'), + default={}, + help_text=_('Optional custom headers included in requests to the OPA server. Defaults to empty dictionary ({}).'), + category=('PolicyAsCode'), + category_slug='policyascode', +) - register( - 'OPA_REQUEST_TIMEOUT', - field_class=fields.FloatField, - label=_('OPA request timeout'), - default=1.5, - help_text=_('The number of seconds after which the connection to the OPA server will time out. Defaults to 1.5 seconds.'), - category=('PolicyAsCode'), - category_slug='policyascode', - ) +register( + 'OPA_REQUEST_TIMEOUT', + field_class=fields.FloatField, + label=_('OPA request timeout'), + default=1.5, + help_text=_('The number of seconds after which the connection to the OPA server will time out. Defaults to 1.5 seconds.'), + category=('PolicyAsCode'), + category_slug='policyascode', +) - register( - 'OPA_REQUEST_RETRIES', - field_class=fields.IntegerField, - label=_('OPA request retry count'), - default=2, - help_text=_('The number of retry attempts for connecting to the OPA server. Default is 2.'), - category=('PolicyAsCode'), - category_slug='policyascode', - ) +register( + 'OPA_REQUEST_RETRIES', + field_class=fields.IntegerField, + label=_('OPA request retry count'), + default=2, + help_text=_('The number of retry attempts for connecting to the OPA server. Default is 2.'), + category=('PolicyAsCode'), + category_slug='policyascode', +) - def policy_as_code_validate(serializer, attrs): - opa_host = attrs.get('OPA_HOST', '') - if opa_host and (opa_host.startswith('http://') or opa_host.startswith('https://')): - raise serializers.ValidationError({'OPA_HOST': _("OPA_HOST should not include 'http://' or 'https://' prefixes. Please enter only the hostname.")}) - return attrs - register_validate('policyascode', policy_as_code_validate) +def policy_as_code_validate(serializer, attrs): + opa_host = attrs.get('OPA_HOST', '') + if opa_host and (opa_host.startswith('http://') or opa_host.startswith('https://')): + raise serializers.ValidationError({'OPA_HOST': _("OPA_HOST should not include 'http://' or 'https://' prefixes. Please enter only the hostname.")}) + return attrs + + +register_validate('policyascode', policy_as_code_validate) diff --git a/awx/main/tasks/policy.py b/awx/main/tasks/policy.py index 1de7b50419..a82410d8cb 100644 --- a/awx/main/tasks/policy.py +++ b/awx/main/tasks/policy.py @@ -8,7 +8,6 @@ from typing import Optional, Union from django.conf import settings from django.utils.translation import gettext_lazy as _ -from flags.state import flag_enabled from opa_client import OpaClient from opa_client.base import BaseClient from requests import HTTPError @@ -364,9 +363,6 @@ def opa_client(headers=None): def evaluate_policy(instance): # Policy evaluation for Policy as Code feature - if not flag_enabled("FEATURE_POLICY_AS_CODE_ENABLED"): - return - if not settings.OPA_HOST: return diff --git a/awx/main/tests/functional/test_policy.py b/awx/main/tests/functional/test_policy.py index 63ff41e0c4..f265a55e64 100644 --- a/awx/main/tests/functional/test_policy.py +++ b/awx/main/tests/functional/test_policy.py @@ -36,11 +36,9 @@ def _parse_exception_message(exception: PolicyEvaluationError): @pytest.fixture(autouse=True) -def enable_flag(): +def setup_opa_settings(): with override_settings( OPA_HOST='opa.example.com', - FLAGS={"FEATURE_POLICY_AS_CODE_ENABLED": [("boolean", True)]}, - FLAG_SOURCES=('flags.sources.SettingsFlagsSource',), ): yield diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 191447d416..12a7929c24 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -1255,8 +1255,8 @@ OPA_REQUEST_RETRIES = 2 # The number of retry attempts for connecting to the OP # feature flags FLAG_SOURCES = ('flags.sources.SettingsFlagsSource',) FLAGS = { - 'FEATURE_POLICY_AS_CODE_ENABLED': [{'condition': 'boolean', 'value': False}], 'FEATURE_INDIRECT_NODE_COUNTING_ENABLED': [{'condition': 'boolean', 'value': False}], + 'FEATURE_DISPATCHERD_ENABLED': [{'condition': 'boolean', 'value': False}], } # Dispatcher worker lifetime. If set to None, workers will never be retired diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index 42d013df6c..fe7807f8e2 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -93,6 +93,9 @@ needs_development = ['inventory_script', 'instance'] needs_param_development = { 'host': ['instance_id'], 'workflow_approval': ['description', 'execution_environment'], + 'inventory': ['opa_query_path'], + 'job_template': ['opa_query_path'], + 'organization': ['opa_query_path'], } # -----------------------------------------------------------------------------------------------------------