cherry-pick 222f387 to release_4.6 (#6971)

This commit is contained in:
Jiří Jeřábek (Jiri Jerabek)
2025-06-09 17:47:23 +02:00
committed by Hao Liu
parent 4eefce622d
commit f4347d05a9
6 changed files with 126 additions and 135 deletions

View File

@@ -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.models import RoleEvaluation, ObjectRole
from ansible_base.rbac import permission_registry from ansible_base.rbac import permission_registry
# django-flags
from flags.state import flag_enabled
# AWX # AWX
from awx.main.access import get_user_capabilities from awx.main.access import get_user_capabilities
from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission
@@ -745,13 +742,10 @@ class EmptySerializer(serializers.Serializer):
pass pass
class OpaQueryPathEnabledMixin(serializers.Serializer): class OpaQueryPathMixin(serializers.Serializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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): def validate_opa_query_path(self, value):
# Decode the URL and re-encode it # Decode the URL and re-encode it
decoded_value = urllib.parse.unquote(value) decoded_value = urllib.parse.unquote(value)
@@ -763,7 +757,7 @@ class OpaQueryPathEnabledMixin(serializers.Serializer):
return value return value
class UnifiedJobTemplateSerializer(BaseSerializer, OpaQueryPathEnabledMixin): class UnifiedJobTemplateSerializer(BaseSerializer, OpaQueryPathMixin):
# As a base serializer, the capabilities prefetch is not used directly, # 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. # instead they are derived from the Workflow Job Template Serializer and the Job Template Serializer, respectively.
capabilities_prefetch = [] capabilities_prefetch = []
@@ -1440,7 +1434,7 @@ class OAuth2ApplicationSerializer(BaseSerializer):
return ret return ret
class OrganizationSerializer(BaseSerializer, OpaQueryPathEnabledMixin): class OrganizationSerializer(BaseSerializer, OpaQueryPathMixin):
show_capabilities = ['edit', 'delete'] show_capabilities = ['edit', 'delete']
class Meta: class Meta:
@@ -1800,7 +1794,7 @@ class LabelsListMixin(object):
return res return res
class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathEnabledMixin): class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables, OpaQueryPathMixin):
show_capabilities = ['edit', 'delete', 'adhoc', 'copy'] show_capabilities = ['edit', 'delete', 'adhoc', 'copy']
capabilities_prefetch = ['admin', 'adhoc', {'copy': 'organization.inventory_admin'}] capabilities_prefetch = ['admin', 'adhoc', {'copy': 'organization.inventory_admin'}]

View File

@@ -4,7 +4,6 @@ import logging
# Django # Django
from django.core.checks import Error from django.core.checks import Error
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings
# Django REST Framework # Django REST Framework
from rest_framework import serializers from rest_framework import serializers
@@ -997,131 +996,132 @@ def csrf_trusted_origins_validate(serializer, attrs):
register_validate('system', csrf_trusted_origins_validate) register_validate('system', csrf_trusted_origins_validate)
if settings.FEATURE_POLICY_AS_CODE_ENABLED: # Unable to use flag_enabled due to AppRegistryNotReady error register(
register( 'OPA_HOST',
'OPA_HOST', field_class=fields.CharField,
field_class=fields.CharField, label=_('OPA server hostname'),
label=_('OPA server hostname'), default='',
default='', help_text=_('The hostname used to connect to the OPA server. If empty, policy enforcement will be disabled.'),
help_text=_('The hostname used to connect to the OPA server. If empty, policy enforcement will be disabled.'), category=('PolicyAsCode'),
category=('PolicyAsCode'), category_slug='policyascode',
category_slug='policyascode', allow_blank=True,
allow_blank=True, )
)
register( register(
'OPA_PORT', 'OPA_PORT',
field_class=fields.IntegerField, field_class=fields.IntegerField,
label=_('OPA server port'), label=_('OPA server port'),
default=8181, default=8181,
help_text=_('The port used to connect to the OPA server. Defaults to 8181.'), help_text=_('The port used to connect to the OPA server. Defaults to 8181.'),
category=('PolicyAsCode'), category=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
) )
register( register(
'OPA_SSL', 'OPA_SSL',
field_class=fields.BooleanField, field_class=fields.BooleanField,
label=_('Use SSL for OPA connection'), label=_('Use SSL for OPA connection'),
default=False, default=False,
help_text=_('Enable or disable the use of SSL to connect to the OPA server. Defaults to false.'), help_text=_('Enable or disable the use of SSL to connect to the OPA server. Defaults to false.'),
category=('PolicyAsCode'), category=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
) )
register( register(
'OPA_AUTH_TYPE', 'OPA_AUTH_TYPE',
field_class=fields.ChoiceField, field_class=fields.ChoiceField,
label=_('OPA authentication type'), label=_('OPA authentication type'),
choices=[OPA_AUTH_TYPES.NONE, OPA_AUTH_TYPES.TOKEN, OPA_AUTH_TYPES.CERTIFICATE], choices=[OPA_AUTH_TYPES.NONE, OPA_AUTH_TYPES.TOKEN, OPA_AUTH_TYPES.CERTIFICATE],
default=OPA_AUTH_TYPES.NONE, default=OPA_AUTH_TYPES.NONE,
help_text=_('The authentication type that will be used to connect to the OPA server: "None", "Token", or "Certificate".'), help_text=_('The authentication type that will be used to connect to the OPA server: "None", "Token", or "Certificate".'),
category=('PolicyAsCode'), category=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
) )
register( register(
'OPA_AUTH_TOKEN', 'OPA_AUTH_TOKEN',
field_class=fields.CharField, field_class=fields.CharField,
label=_('OPA authentication token'), label=_('OPA authentication token'),
default='', default='',
help_text=_( 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.' '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=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
allow_blank=True, allow_blank=True,
encrypted=True, encrypted=True,
) )
register( register(
'OPA_AUTH_CLIENT_CERT', 'OPA_AUTH_CLIENT_CERT',
field_class=fields.CharField, field_class=fields.CharField,
label=_('OPA client certificate content'), label=_('OPA client certificate content'),
default='', default='',
help_text=_('The content of the client certificate file for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), 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=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
allow_blank=True, allow_blank=True,
) )
register( register(
'OPA_AUTH_CLIENT_KEY', 'OPA_AUTH_CLIENT_KEY',
field_class=fields.CharField, field_class=fields.CharField,
label=_('OPA client key content'), label=_('OPA client key content'),
default='', default='',
help_text=_('The content of the client key for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), 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=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
allow_blank=True, allow_blank=True,
encrypted=True, encrypted=True,
) )
register( register(
'OPA_AUTH_CA_CERT', 'OPA_AUTH_CA_CERT',
field_class=fields.CharField, field_class=fields.CharField,
label=_('OPA CA certificate content'), label=_('OPA CA certificate content'),
default='', default='',
help_text=_('The content of the CA certificate for mTLS authentication to the OPA server. Required when OPA_AUTH_TYPE is "Certificate".'), 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=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
allow_blank=True, allow_blank=True,
) )
register( register(
'OPA_AUTH_CUSTOM_HEADERS', 'OPA_AUTH_CUSTOM_HEADERS',
field_class=fields.DictField, field_class=fields.DictField,
label=_('OPA custom authentication headers'), label=_('OPA custom authentication headers'),
default={}, default={},
help_text=_('Optional custom headers included in requests to the OPA server. Defaults to empty dictionary ({}).'), help_text=_('Optional custom headers included in requests to the OPA server. Defaults to empty dictionary ({}).'),
category=('PolicyAsCode'), category=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
) )
register( register(
'OPA_REQUEST_TIMEOUT', 'OPA_REQUEST_TIMEOUT',
field_class=fields.FloatField, field_class=fields.FloatField,
label=_('OPA request timeout'), label=_('OPA request timeout'),
default=1.5, 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.'), 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=('PolicyAsCode'),
category_slug='policyascode', category_slug='policyascode',
) )
register( register(
'OPA_REQUEST_RETRIES', 'OPA_REQUEST_RETRIES',
field_class=fields.IntegerField, field_class=fields.IntegerField,
label=_('OPA request retry count'), label=_('OPA request retry count'),
default=2, default=2,
help_text=_('The number of retry attempts for connecting to the OPA server. Default is 2.'), help_text=_('The number of retry attempts for connecting to the OPA server. Default is 2.'),
category=('PolicyAsCode'), category=('PolicyAsCode'),
category_slug='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)

View File

@@ -8,7 +8,6 @@ from typing import Optional, Union
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from flags.state import flag_enabled
from opa_client import OpaClient from opa_client import OpaClient
from opa_client.base import BaseClient from opa_client.base import BaseClient
from requests import HTTPError from requests import HTTPError
@@ -364,9 +363,6 @@ def opa_client(headers=None):
def evaluate_policy(instance): def evaluate_policy(instance):
# Policy evaluation for Policy as Code feature # Policy evaluation for Policy as Code feature
if not flag_enabled("FEATURE_POLICY_AS_CODE_ENABLED"):
return
if not settings.OPA_HOST: if not settings.OPA_HOST:
return return

View File

@@ -36,11 +36,9 @@ def _parse_exception_message(exception: PolicyEvaluationError):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def enable_flag(): def setup_opa_settings():
with override_settings( with override_settings(
OPA_HOST='opa.example.com', OPA_HOST='opa.example.com',
FLAGS={"FEATURE_POLICY_AS_CODE_ENABLED": [("boolean", True)]},
FLAG_SOURCES=('flags.sources.SettingsFlagsSource',),
): ):
yield yield

View File

@@ -1255,8 +1255,8 @@ OPA_REQUEST_RETRIES = 2 # The number of retry attempts for connecting to the OP
# feature flags # feature flags
FLAG_SOURCES = ('flags.sources.SettingsFlagsSource',) FLAG_SOURCES = ('flags.sources.SettingsFlagsSource',)
FLAGS = { FLAGS = {
'FEATURE_POLICY_AS_CODE_ENABLED': [{'condition': 'boolean', 'value': False}],
'FEATURE_INDIRECT_NODE_COUNTING_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 # Dispatcher worker lifetime. If set to None, workers will never be retired

View File

@@ -93,6 +93,9 @@ needs_development = ['inventory_script', 'instance']
needs_param_development = { needs_param_development = {
'host': ['instance_id'], 'host': ['instance_id'],
'workflow_approval': ['description', 'execution_environment'], 'workflow_approval': ['description', 'execution_environment'],
'inventory': ['opa_query_path'],
'job_template': ['opa_query_path'],
'organization': ['opa_query_path'],
} }
# ----------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------