Merge pull request #3581 from beeankha/basic_license_feature

Update Basic License Feature Access

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-04-12 15:57:18 +00:00 committed by GitHub
commit 3611f3491b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 134 additions and 1025 deletions

View File

@ -73,7 +73,6 @@ from awx.main.redact import UriCleaner, REPLACE_STR
from awx.main.validators import vars_validate_or_raise
from awx.conf.license import feature_enabled, LicenseForbids
from awx.api.versioning import reverse, get_request_version
from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField,
VerbatimField, DeprecatedCredentialField)
@ -918,7 +917,7 @@ class UserSerializer(BaseSerializer):
def _update_password(self, obj, new_password):
# For now we're not raising an error, just not saving password for
# users managed by LDAP who already have an unusable password set.
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None) and feature_enabled('ldap'):
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
try:
if obj.pk and obj.profile.ldap_dn and not obj.has_usable_password():
new_password = None
@ -979,7 +978,7 @@ class UserSerializer(BaseSerializer):
return res
def _validate_ldap_managed_field(self, value, field_name):
if not getattr(settings, 'AUTH_LDAP_SERVER_URI', None) or not feature_enabled('ldap'):
if not getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
return value
try:
is_ldap_user = bool(self.instance and self.instance.profile.ldap_dn)
@ -1073,7 +1072,7 @@ class BaseOAuth2TokenSerializer(BaseSerializer):
if word not in self.ALLOWED_SCOPES:
return False
return True
def validate_scope(self, value):
if not self._is_valid_scope(value):
raise serializers.ValidationError(_(
@ -3170,12 +3169,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
def validate_extra_vars(self, value):
return vars_validate_or_raise(value)
def validate_job_slice_count(self, value):
if value > 1 and not feature_enabled('workflows'):
raise LicenseForbids({'job_slice_count': [_(
"Job slicing is a workflows-based feature and your license does not allow use of workflows."
)]})
return value
def get_summary_fields(self, obj):
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)

View File

@ -73,7 +73,7 @@ from awx.api.generics import (
SubListDestroyAPIView, get_view_name
)
from awx.api.versioning import reverse, get_request_version
from awx.conf.license import feature_enabled, feature_exists, LicenseForbids, get_license
from awx.conf.license import get_license
from awx.main import models
from awx.main.utils import (
camelcase_to_underscore,
@ -100,10 +100,9 @@ from awx.api.metadata import RoleMetadata, JobTypeMetadata
from awx.main.constants import ACTIVE_STATES
from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.views.mixin import (
ActivityStreamEnforcementMixin, ControlledByScmMixin,
InstanceGroupMembershipMixin, OrganizationCountsMixin,
RelatedJobsPreventDeleteMixin, SystemTrackingEnforcementMixin,
UnifiedJobDeletionMixin, WorkflowsEnforcementMixin,
ControlledByScmMixin, InstanceGroupMembershipMixin,
OrganizationCountsMixin, RelatedJobsPreventDeleteMixin,
UnifiedJobDeletionMixin,
)
from awx.api.views.organization import ( # noqa
OrganizationList,
@ -530,12 +529,6 @@ class AuthView(APIView):
# Return auth backends in consistent order: Google, GitHub, SAML.
auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0])
for name, backend in auth_backends:
if (not feature_exists('enterprise_auth') and
not feature_enabled('ldap')) or \
(not feature_enabled('enterprise_auth') and
name in ['saml', 'radius']):
continue
login_url = reverse('social:begin', args=(name,))
complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,)))
backend_data = {
@ -649,7 +642,7 @@ class TeamProjectsList(SubListAPIView):
return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in proj_roles])
class TeamActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class TeamActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -745,7 +738,7 @@ class ProjectScmInventorySources(SubListAPIView):
parent_key = 'source_project'
class ProjectActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class ProjectActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -989,7 +982,7 @@ class ApplicationOAuth2TokenList(SubListCreateAPIView):
swagger_topic = 'Authentication'
class OAuth2ApplicationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class OAuth2ApplicationActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -1071,7 +1064,7 @@ class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView):
swagger_topic = 'Authentication'
class OAuth2TokenActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class OAuth2TokenActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -1185,7 +1178,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
return my_qs & user_qs
class UserActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class UserActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -1272,7 +1265,7 @@ class CredentialTypeCredentialList(SubListCreateAPIView):
serializer_class = serializers.CredentialSerializer
class CredentialTypeActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class CredentialTypeActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -1386,7 +1379,7 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView):
filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend]
class CredentialActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class CredentialActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -1618,7 +1611,7 @@ class HostSmartInventoriesList(SubListAPIView):
relationship = 'smart_inventories'
class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class HostActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -1633,7 +1626,7 @@ class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
class HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIView):
class HostFactVersionsList(ParentMixin, ListAPIView):
model = models.Fact
serializer_class = serializers.FactVersionSerializer
@ -1660,7 +1653,7 @@ class HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIV
return Response(dict(results=self.serializer_class(queryset, many=True).data))
class HostFactCompareView(SystemTrackingEnforcementMixin, SubDetailAPIView):
class HostFactCompareView(SubDetailAPIView):
model = models.Fact
parent_model = models.Host
@ -1876,7 +1869,7 @@ class GroupInventorySourcesList(SubListAPIView):
relationship = 'inventory_sources'
class GroupActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class GroupActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -2119,7 +2112,7 @@ class InventorySourceSchedulesList(SubListCreateAPIView):
parent_key = 'unified_job_template'
class InventorySourceActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class InventorySourceActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -2530,21 +2523,11 @@ class JobTemplateSurveySpec(GenericAPIView):
def get(self, request, *args, **kwargs):
obj = self.get_object()
if not feature_enabled('surveys'):
raise LicenseForbids(_('Your license does not allow '
'adding surveys.'))
return Response(obj.display_survey_spec())
def post(self, request, *args, **kwargs):
obj = self.get_object()
# Sanity check: Are surveys available on this license?
# If not, do not allow them to be used.
if not feature_enabled('surveys'):
raise LicenseForbids(_('Your license does not allow '
'adding surveys.'))
if not request.user.can_access(self.model, 'change', obj, None):
raise PermissionDenied()
response = self._validate_spec_data(request.data, obj.survey_spec)
@ -2672,12 +2655,12 @@ class JobTemplateSurveySpec(GenericAPIView):
return Response()
class WorkflowJobTemplateSurveySpec(WorkflowsEnforcementMixin, JobTemplateSurveySpec):
class WorkflowJobTemplateSurveySpec(JobTemplateSurveySpec):
model = models.WorkflowJobTemplate
class JobTemplateActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class JobTemplateActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -2995,14 +2978,14 @@ class JobTemplateCopy(CopyAPIView):
copy_return_serializer_class = serializers.JobTemplateSerializer
class WorkflowJobNodeList(WorkflowsEnforcementMixin, ListAPIView):
class WorkflowJobNodeList(ListAPIView):
model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeListSerializer
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
class WorkflowJobNodeDetail(WorkflowsEnforcementMixin, RetrieveAPIView):
class WorkflowJobNodeDetail(RetrieveAPIView):
model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeDetailSerializer
@ -3016,14 +2999,14 @@ class WorkflowJobNodeCredentialsList(SubListAPIView):
relationship = 'credentials'
class WorkflowJobTemplateNodeList(WorkflowsEnforcementMixin, ListCreateAPIView):
class WorkflowJobTemplateNodeList(ListCreateAPIView):
model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeSerializer
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
class WorkflowJobTemplateNodeDetail(WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView):
class WorkflowJobTemplateNodeDetail(RetrieveUpdateDestroyAPIView):
model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeDetailSerializer
@ -3034,7 +3017,7 @@ class WorkflowJobTemplateNodeCredentialsList(LaunchConfigCredentialsBase):
parent_model = models.WorkflowJobTemplateNode
class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeSerializer
@ -3096,7 +3079,7 @@ class WorkflowJobTemplateNodeAlwaysNodesList(WorkflowJobTemplateNodeChildrenBase
relationship = 'always_nodes'
class WorkflowJobNodeChildrenBaseList(WorkflowsEnforcementMixin, SubListAPIView):
class WorkflowJobNodeChildrenBaseList(SubListAPIView):
model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeListSerializer
@ -3126,21 +3109,21 @@ class WorkflowJobNodeAlwaysNodesList(WorkflowJobNodeChildrenBaseList):
relationship = 'always_nodes'
class WorkflowJobTemplateList(WorkflowsEnforcementMixin, ListCreateAPIView):
class WorkflowJobTemplateList(ListCreateAPIView):
model = models.WorkflowJobTemplate
serializer_class = serializers.WorkflowJobTemplateSerializer
always_allow_superuser = False
class WorkflowJobTemplateDetail(RelatedJobsPreventDeleteMixin, WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView):
class WorkflowJobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = models.WorkflowJobTemplate
serializer_class = serializers.WorkflowJobTemplateSerializer
always_allow_superuser = False
class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, CopyAPIView):
class WorkflowJobTemplateCopy(CopyAPIView):
model = models.WorkflowJobTemplate
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
@ -3185,11 +3168,11 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, CopyAPIView):
obj.save()
class WorkflowJobTemplateLabelList(WorkflowsEnforcementMixin, JobTemplateLabelList):
class WorkflowJobTemplateLabelList(JobTemplateLabelList):
parent_model = models.WorkflowJobTemplate
class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
class WorkflowJobTemplateLaunch(RetrieveAPIView):
model = models.WorkflowJobTemplate
@ -3238,7 +3221,7 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView):
class WorkflowJobRelaunch(GenericAPIView):
model = models.WorkflowJob
obj_permission_type = 'start'
@ -3270,7 +3253,7 @@ class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView):
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class WorkflowJobTemplateWorkflowNodesList(WorkflowsEnforcementMixin, SubListCreateAPIView):
class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView):
model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeSerializer
@ -3283,7 +3266,7 @@ class WorkflowJobTemplateWorkflowNodesList(WorkflowsEnforcementMixin, SubListCre
return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id')
class WorkflowJobTemplateJobsList(WorkflowsEnforcementMixin, SubListAPIView):
class WorkflowJobTemplateJobsList(SubListAPIView):
model = models.WorkflowJob
serializer_class = serializers.WorkflowJobListSerializer
@ -3292,7 +3275,7 @@ class WorkflowJobTemplateJobsList(WorkflowsEnforcementMixin, SubListAPIView):
parent_key = 'workflow_job_template'
class WorkflowJobTemplateSchedulesList(WorkflowsEnforcementMixin, SubListCreateAPIView):
class WorkflowJobTemplateSchedulesList(SubListCreateAPIView):
view_name = _("Workflow Job Template Schedules")
@ -3303,7 +3286,7 @@ class WorkflowJobTemplateSchedulesList(WorkflowsEnforcementMixin, SubListCreateA
parent_key = 'unified_job_template'
class WorkflowJobTemplateNotificationTemplatesAnyList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView):
class WorkflowJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer
@ -3311,7 +3294,7 @@ class WorkflowJobTemplateNotificationTemplatesAnyList(WorkflowsEnforcementMixin,
relationship = 'notification_templates_any'
class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView):
class WorkflowJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer
@ -3319,7 +3302,7 @@ class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowsEnforcementMixi
relationship = 'notification_templates_error'
class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView):
class WorkflowJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer
@ -3327,13 +3310,13 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowsEnforcementMi
relationship = 'notification_templates_success'
class WorkflowJobTemplateAccessList(WorkflowsEnforcementMixin, ResourceAccessList):
class WorkflowJobTemplateAccessList(ResourceAccessList):
model = models.User # needs to be User for AccessLists's
parent_model = models.WorkflowJobTemplate
class WorkflowJobTemplateObjectRolesList(WorkflowsEnforcementMixin, SubListAPIView):
class WorkflowJobTemplateObjectRolesList(SubListAPIView):
model = models.Role
serializer_class = serializers.RoleSerializer
@ -3346,7 +3329,7 @@ class WorkflowJobTemplateObjectRolesList(WorkflowsEnforcementMixin, SubListAPIVi
return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView):
class WorkflowJobTemplateActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -3362,19 +3345,19 @@ class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityS
Q(workflow_job_template_node__workflow_job_template=parent)).distinct()
class WorkflowJobList(WorkflowsEnforcementMixin, ListCreateAPIView):
class WorkflowJobList(ListCreateAPIView):
model = models.WorkflowJob
serializer_class = serializers.WorkflowJobListSerializer
class WorkflowJobDetail(WorkflowsEnforcementMixin, UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
class WorkflowJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
model = models.WorkflowJob
serializer_class = serializers.WorkflowJobSerializer
class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView):
class WorkflowJobWorkflowNodesList(SubListAPIView):
model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeListSerializer
@ -3388,7 +3371,7 @@ class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView):
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
class WorkflowJobCancel(WorkflowsEnforcementMixin, RetrieveAPIView):
class WorkflowJobCancel(RetrieveAPIView):
model = models.WorkflowJob
obj_permission_type = 'cancel'
@ -3404,7 +3387,7 @@ class WorkflowJobCancel(WorkflowsEnforcementMixin, RetrieveAPIView):
return self.http_method_not_allowed(request, *args, **kwargs)
class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView):
class WorkflowJobNotificationsList(SubListAPIView):
model = models.Notification
serializer_class = serializers.NotificationSerializer
@ -3413,7 +3396,7 @@ class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView):
search_fields = ('subject', 'notification_type', 'body',)
class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView):
class WorkflowJobActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -3589,11 +3572,11 @@ class JobLabelList(SubListAPIView):
parent_key = 'job'
class WorkflowJobLabelList(WorkflowsEnforcementMixin, JobLabelList):
class WorkflowJobLabelList(JobLabelList):
parent_model = models.WorkflowJob
class JobActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class JobActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -4106,7 +4089,7 @@ class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList):
parent_model = models.AdHocCommand
class AdHocCommandActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class AdHocCommandActivityStreamList(SubListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
@ -4402,14 +4385,14 @@ class LabelDetail(RetrieveUpdateAPIView):
serializer_class = serializers.LabelSerializer
class ActivityStreamList(ActivityStreamEnforcementMixin, SimpleListAPIView):
class ActivityStreamList(SimpleListAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer
search_fields = ('changes',)
class ActivityStreamDetail(ActivityStreamEnforcementMixin, RetrieveAPIView):
class ActivityStreamDetail(RetrieveAPIView):
model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer

View File

@ -48,7 +48,6 @@ from awx.api.serializers import (
JobTemplateSerializer,
)
from awx.api.views.mixin import (
ActivityStreamEnforcementMixin,
RelatedJobsPreventDeleteMixin,
ControlledByScmMixin,
)
@ -149,7 +148,7 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, Retri
return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST)
class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class InventoryActivityStreamList(SubListAPIView):
model = ActivityStream
serializer_class = ActivityStreamSerializer

View File

@ -31,48 +31,11 @@ from awx.main.models.organization import Team
from awx.main.models.projects import Project
from awx.main.models.inventory import Inventory
from awx.main.models.jobs import JobTemplate
from awx.conf.license import (
feature_enabled,
LicenseForbids,
)
from awx.api.exceptions import ActiveJobConflict
logger = logging.getLogger('awx.api.views.mixin')
class ActivityStreamEnforcementMixin(object):
'''
Mixin to check that license supports activity streams.
'''
def check_permissions(self, request):
ret = super(ActivityStreamEnforcementMixin, self).check_permissions(request)
if not feature_enabled('activity_streams'):
raise LicenseForbids(_('Your license does not allow use of the activity stream.'))
return ret
class SystemTrackingEnforcementMixin(object):
'''
Mixin to check that license supports system tracking.
'''
def check_permissions(self, request):
ret = super(SystemTrackingEnforcementMixin, self).check_permissions(request)
if not feature_enabled('system_tracking'):
raise LicenseForbids(_('Your license does not permit use of system tracking.'))
return ret
class WorkflowsEnforcementMixin(object):
'''
Mixin to check that license supports workflows.
'''
def check_permissions(self, request):
ret = super(WorkflowsEnforcementMixin, self).check_permissions(request)
if not feature_enabled('workflows') and request.method not in ('GET', 'OPTIONS', 'DELETE'):
raise LicenseForbids(_('Your license does not allow use of workflows.'))
return ret
class UnifiedJobDeletionMixin(object):
'''
Special handling when deleting a running unified job object.

View File

@ -7,13 +7,8 @@ import logging
# Django
from django.db.models import Count
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
# AWX
from awx.conf.license import (
feature_enabled,
LicenseForbids,
)
from awx.main.models import (
ActivityStream,
Inventory,
@ -50,7 +45,6 @@ from awx.api.serializers import (
InstanceGroupSerializer,
)
from awx.api.views.mixin import (
ActivityStreamEnforcementMixin,
RelatedJobsPreventDeleteMixin,
OrganizationCountsMixin,
)
@ -69,24 +63,6 @@ class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
qs = qs.prefetch_related('created_by', 'modified_by')
return qs
def create(self, request, *args, **kwargs):
"""Create a new organzation.
If there is already an organization and the license of this
instance does not permit multiple organizations, then raise
LicenseForbids.
"""
# Sanity check: If the multiple organizations feature is disallowed
# by the license, then we are only willing to create this organization
# if no organizations exist in the system.
if (not feature_enabled('multiple_organizations') and
self.model.objects.exists()):
raise LicenseForbids(_('Your license only permits a single '
'organization to exist.'))
# Okay, create the organization as usual.
return super(OrganizationList, self).create(request, *args, **kwargs)
class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
@ -177,7 +153,7 @@ class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
parent_key = 'organization'
class OrganizationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
class OrganizationActivityStreamList(SubListAPIView):
model = ActivityStream
serializer_class = ActivityStreamSerializer
@ -244,4 +220,3 @@ class OrganizationObjectRolesList(SubListAPIView):
po = self.get_parent_object()
content_type = ContentType.objects.get_for_model(self.parent_model)
return Role.objects.filter(content_type=content_type, object_id=po.pk)

View File

@ -26,7 +26,7 @@ from awx.main.utils import (
to_python_boolean,
)
from awx.api.versioning import reverse, get_request_version, drf_reverse
from awx.conf.license import get_license, feature_enabled
from awx.conf.license import get_license
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS
from awx.main.models import (
Project,
@ -57,9 +57,8 @@ class ApiRootView(APIView):
data['current_version'] = v2
data['available_versions'] = dict(v1 = v1, v2 = v2)
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
if feature_enabled('rebranding'):
data['custom_logo'] = settings.CUSTOM_LOGO
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
data['custom_logo'] = settings.CUSTOM_LOGO
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
return Response(data)
@ -213,7 +212,7 @@ class ApiV1ConfigView(APIView):
# If LDAP is enabled, user_ldap_fields will return a list of field
# names that are managed by LDAP and should be read-only for users with
# a non-empty ldap_dn attribute.
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None) and feature_enabled('ldap'):
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
user_ldap_fields = ['username', 'password']
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys())
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys())

View File

@ -78,9 +78,6 @@ register(
# the other settings change, the cached value for this setting will be
# cleared to require it to be recomputed.
depends_on=['ANSIBLE_COW_SELECTION'],
# Optional; licensed feature required to be able to view or modify this
# setting.
feature_required='rebranding',
# Optional; field is stored encrypted in the database and only $encrypted$
# is returned via the API.
encrypted=True,

View File

@ -1,64 +1,19 @@
# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved.
# Django
from django.core.signals import setting_changed
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework.exceptions import APIException
# Tower
from awx.main.utils.common import get_licenser
from awx.main.utils import memoize, memoize_delete
__all__ = ['LicenseForbids', 'get_license', 'get_licensed_features',
'feature_enabled', 'feature_exists']
class LicenseForbids(APIException):
status_code = 402
default_detail = _('Your Tower license does not allow that.')
__all__ = ['get_license']
def _get_validated_license_data():
return get_licenser().validate()
@receiver(setting_changed)
def _on_setting_changed(sender, **kwargs):
# Clear cached result above when license changes.
if kwargs.get('setting', None) == 'LICENSE':
memoize_delete('feature_enabled')
def get_license(show_key=False):
"""Return a dictionary representing the active license on this Tower instance."""
license_data = _get_validated_license_data()
if not show_key:
license_data.pop('license_key', None)
return license_data
def get_licensed_features():
"""Return a set of all features enabled by the active license."""
features = set()
for feature, enabled in _get_validated_license_data().get('features', {}).items():
if enabled:
features.add(feature)
return features
@memoize(track_function=True)
def feature_enabled(name):
"""Return True if the requested feature is enabled, False otherwise."""
validated_license_data = _get_validated_license_data()
if validated_license_data.get('license_type', 'UNLICENSED') == 'open':
return True
return validated_license_data.get('features', {}).get(name, False)
def feature_exists(name):
"""Return True if the requested feature name exists, False otherwise."""
return bool(name in _get_validated_license_data().get('features', {}))

View File

@ -68,7 +68,7 @@ class SettingsRegistry(object):
def get_dependent_settings(self, setting):
return self._dependent_settings.get(setting, set())
def get_registered_categories(self, features_enabled=None):
def get_registered_categories(self):
categories = {
'all': _('All'),
'changed': _('Changed'),
@ -77,10 +77,6 @@ class SettingsRegistry(object):
category_slug = kwargs.get('category_slug', None)
if category_slug is None or category_slug in categories:
continue
if features_enabled is not None:
feature_required = kwargs.get('feature_required', None)
if feature_required and feature_required not in features_enabled:
continue
if category_slug == 'user':
categories['user'] = _('User')
categories['user-defaults'] = _('User-Defaults')
@ -88,7 +84,7 @@ class SettingsRegistry(object):
categories[category_slug] = kwargs.get('category', None) or category_slug
return categories
def get_registered_settings(self, category_slug=None, read_only=None, features_enabled=None, slugs_to_ignore=set()):
def get_registered_settings(self, category_slug=None, read_only=None, slugs_to_ignore=set()):
setting_names = []
if category_slug == 'user-defaults':
category_slug = 'user'
@ -104,10 +100,6 @@ class SettingsRegistry(object):
# Note: Doesn't catch fields that set read_only via __init__;
# read-only field kwargs should always include read_only=True.
continue
if features_enabled is not None:
feature_required = kwargs.get('feature_required', None)
if feature_required and feature_required not in features_enabled:
continue
setting_names.append(setting)
return setting_names
@ -135,7 +127,6 @@ class SettingsRegistry(object):
category = field_kwargs.pop('category', None)
depends_on = frozenset(field_kwargs.pop('depends_on', None) or [])
placeholder = field_kwargs.pop('placeholder', empty)
feature_required = field_kwargs.pop('feature_required', empty)
encrypted = bool(field_kwargs.pop('encrypted', False))
defined_in_file = bool(field_kwargs.pop('defined_in_file', False))
if getattr(field_kwargs.get('child', None), 'source', None) is not None:
@ -146,8 +137,6 @@ class SettingsRegistry(object):
field_instance.depends_on = depends_on
if placeholder is not empty:
field_instance.placeholder = placeholder
if feature_required is not empty:
field_instance.feature_required = feature_required
field_instance.defined_in_file = defined_in_file
if field_instance.defined_in_file:
field_instance.help_text = (

View File

@ -119,20 +119,6 @@ def test_get_registered_read_only_settings(reg):
]
def test_get_registered_settings_with_required_features(reg):
reg.register(
'AWX_SOME_SETTING_ENABLED',
field_class=fields.BooleanField,
category=_('System'),
category_slug='system',
feature_required='superpowers',
)
assert reg.get_registered_settings(features_enabled=[]) == []
assert reg.get_registered_settings(features_enabled=['superpowers']) == [
'AWX_SOME_SETTING_ENABLED'
]
def test_get_dependent_settings(reg):
reg.register(
'AWX_SOME_SETTING_ENABLED',
@ -173,45 +159,6 @@ def test_get_registered_categories(reg):
}
def test_get_registered_categories_with_required_features(reg):
reg.register(
'AWX_SOME_SETTING_ENABLED',
field_class=fields.BooleanField,
category=_('System'),
category_slug='system',
feature_required='superpowers'
)
reg.register(
'AWX_SOME_OTHER_SETTING_ENABLED',
field_class=fields.BooleanField,
category=_('OtherSystem'),
category_slug='other-system',
feature_required='sortapowers'
)
assert reg.get_registered_categories(features_enabled=[]) == {
'all': _('All'),
'changed': _('Changed'),
}
assert reg.get_registered_categories(features_enabled=['superpowers']) == {
'all': _('All'),
'changed': _('Changed'),
'system': _('System'),
}
assert reg.get_registered_categories(features_enabled=['sortapowers']) == {
'all': _('All'),
'changed': _('Changed'),
'other-system': _('OtherSystem'),
}
assert reg.get_registered_categories(
features_enabled=['superpowers', 'sortapowers']
) == {
'all': _('All'),
'changed': _('Changed'),
'system': _('System'),
'other-system': _('OtherSystem'),
}
def test_is_setting_encrypted(reg):
reg.register(
'AWX_SOME_SETTING_ENABLED',
@ -237,7 +184,6 @@ def test_simple_field(reg):
category=_('System'),
category_slug='system',
placeholder='Example Value',
feature_required='superpowers'
)
field = reg.get_setting_field('AWX_SOME_SETTING')
@ -246,7 +192,6 @@ def test_simple_field(reg):
assert field.category_slug == 'system'
assert field.default is empty
assert field.placeholder == 'Example Value'
assert field.feature_required == 'superpowers'
def test_field_with_custom_attribute(reg):

View File

@ -28,7 +28,6 @@ from awx.api.versioning import reverse, get_request_version
from awx.main.utils import camelcase_to_underscore
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
from awx.main.tasks import handle_setting_changes
from awx.conf.license import get_licensed_features
from awx.conf.models import Setting
from awx.conf.serializers import SettingCategorySerializer, SettingSingletonSerializer
from awx.conf import settings_registry
@ -53,7 +52,7 @@ class SettingCategoryList(ListAPIView):
def get_queryset(self):
setting_categories = []
categories = settings_registry.get_registered_categories(features_enabled=get_licensed_features())
categories = settings_registry.get_registered_categories()
if self.request.user.is_superuser or self.request.user.is_system_auditor:
pass # categories = categories
elif 'user' in categories:
@ -77,7 +76,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
def get_queryset(self):
self.category_slug = self.kwargs.get('category_slug', 'all')
all_category_slugs = list(settings_registry.get_registered_categories(features_enabled=get_licensed_features()).keys())
all_category_slugs = list(settings_registry.get_registered_categories().keys())
for slug_to_delete in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]:
all_category_slugs.remove(slug_to_delete)
if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False):
@ -90,7 +89,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
raise PermissionDenied()
registered_settings = settings_registry.get_registered_settings(
category_slug=self.category_slug, read_only=False, features_enabled=get_licensed_features(),
category_slug=self.category_slug, read_only=False,
slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
)
if self.category_slug == 'user':
@ -101,7 +100,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
def get_object(self):
settings_qs = self.get_queryset()
registered_settings = settings_registry.get_registered_settings(
category_slug=self.category_slug, features_enabled=get_licensed_features(),
category_slug=self.category_slug,
slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
)
all_settings = {}

View File

@ -41,8 +41,6 @@ from awx.main.models import (
)
from awx.main.models.mixins import ResourceMixin
from awx.conf.license import LicenseForbids, feature_enabled
__all__ = ['get_user_queryset', 'check_user_access', 'check_user_access_with_errors',
'user_accessible_objects', 'consumer_access',]
@ -324,12 +322,6 @@ class BaseAccess(object):
elif not add_host_name and free_instances < 0:
raise PermissionDenied(_("Host count exceeds available instances."))
if feature is not None:
if "features" in validation_info and not validation_info["features"].get(feature, False):
raise LicenseForbids(_("Feature %s is not enabled in the active license.") % feature)
elif "features" not in validation_info:
raise LicenseForbids(_("Features not found in active license."))
def check_org_host_limit(self, data, add_host_name=None):
validation_info = get_licenser().validate()
if validation_info.get('license_type', 'UNLICENSED') == 'open':
@ -383,9 +375,6 @@ class BaseAccess(object):
if obj.validation_errors:
user_capabilities[display_method] = False
continue
elif isinstance(obj, (WorkflowJobTemplate, WorkflowJob)) and (not feature_enabled('workflows')):
user_capabilities[display_method] = (display_method == 'delete')
continue
elif display_method == 'copy' and isinstance(obj, WorkflowJobTemplate) and obj.organization_id is None:
user_capabilities[display_method] = self.user.is_superuser
continue
@ -776,7 +765,7 @@ class OrganizationAccess(NotificationAttachMixin, BaseAccess):
return self.user in obj.admin_role
def can_delete(self, obj):
self.check_license(feature='multiple_organizations', check_expiration=False)
self.check_license(check_expiration=False)
is_change_possible = self.can_change(obj, None)
if not is_change_possible:
return False
@ -1492,11 +1481,6 @@ class JobTemplateAccess(NotificationAttachMixin, BaseAccess):
# Check the per-org limit
self.check_org_host_limit({'inventory': obj.inventory})
if obj.survey_enabled:
self.check_license(feature='surveys')
if Instance.objects.active_count() > 1:
self.check_license(feature='ha')
# Super users can start any job
if self.user.is_superuser:
return True
@ -2021,10 +2005,6 @@ class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess):
# Check the per-org limit
self.check_org_host_limit({'inventory': obj.inventory})
# if surveys are added to WFJTs, check license here
if obj.survey_enabled:
self.check_license(feature='surveys')
# Super users can start any job
if self.user.is_superuser:
return True
@ -2032,11 +2012,6 @@ class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess):
return self.user in obj.execute_role
def can_change(self, obj, data):
# Check survey license if surveys are added to WFJTs
if (data and 'survey_enabled' in data and
obj.survey_enabled != data['survey_enabled'] and data['survey_enabled']):
self.check_license(feature='surveys')
if self.user.is_superuser:
return True

View File

@ -21,7 +21,6 @@ register(
help_text=_('Enable capturing activity for the activity stream.'),
category=_('System'),
category_slug='system',
feature_required='activity_streams',
)
register(
@ -31,7 +30,6 @@ register(
help_text=_('Enable capturing activity for the activity stream when running inventory sync.'),
category=_('System'),
category_slug='system',
feature_required='activity_streams',
)
register(

View File

@ -13,7 +13,6 @@ from django.utils.timezone import now
# AWX
from awx.main.models.fact import Fact
from awx.conf.license import feature_enabled
OLDER_THAN = 'older_than'
GRANULARITY = 'granularity'
@ -30,7 +29,7 @@ class CleanupFacts(object):
# Delete all except LAST entry (or Delete all except the FIRST entry, it's an arbitrary decision)
#
# pivot -= granularity
# group by host
# group by host
def cleanup(self, older_than_abs, granularity, module=None):
fact_oldest = Fact.objects.all().order_by('timestamp').first()
if not fact_oldest:
@ -114,7 +113,7 @@ class Command(BaseCommand):
def string_time_to_timestamp(self, time_string):
units = {
'y': 'years',
'd': 'days',
'd': 'days',
'w': 'weeks',
'm': 'months'
}
@ -131,8 +130,6 @@ class Command(BaseCommand):
@transaction.atomic
def handle(self, *args, **options):
sys.stderr.write("This command has been deprecated and will be removed in a future release.\n")
if not feature_enabled('system_tracking'):
raise CommandError("The System Tracking feature is not enabled for your instance")
cleanup_facts = CleanupFacts()
if not all([options[GRANULARITY], options[OLDER_THAN]]):
raise CommandError('Both --granularity and --older_than are required.')

View File

@ -1088,7 +1088,7 @@ class Command(BaseCommand):
logger.warning('update computed fields took %d queries',
len(connection.queries) - queries_before2)
# Check if the license is valid.
# If the license is not valid, a CommandError will be thrown,
# If the license is not valid, a CommandError will be thrown,
# and inventory update will be marked as invalid.
# with transaction.atomic() will roll back the changes.
license_fail = True

View File

@ -1,4 +1,3 @@
from unittest import mock
import pytest
from awx.api.versioning import reverse
@ -8,16 +7,12 @@ from awx.main.access import ActivityStreamAccess
from awx.conf.models import Setting
def mock_feature_enabled(feature):
return True
@pytest.fixture
def activity_stream_entry(organization, org_admin):
return ActivityStream.objects.filter(organization__pk=organization.pk, user=org_admin, operation='associate').first()
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_get_activity_stream_list(monkeypatch, organization, get, user, settings):
settings.ACTIVITY_STREAM_ENABLED = True
@ -27,7 +22,6 @@ def test_get_activity_stream_list(monkeypatch, organization, get, user, settings
assert response.status_code == 200
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_basic_fields(monkeypatch, organization, get, user, settings):
settings.ACTIVITY_STREAM_ENABLED = True
@ -48,7 +42,6 @@ def test_basic_fields(monkeypatch, organization, get, user, settings):
assert response.data['summary_fields']['organization'][0]['name'] == 'test-org'
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_ctint_activity_stream(monkeypatch, get, user, settings):
Setting.objects.create(key="FOO", value="bar")
@ -68,7 +61,6 @@ def test_ctint_activity_stream(monkeypatch, get, user, settings):
assert response.data['summary_fields']['setting'][0]['name'] == 'FOO'
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_middleware_actor_added(monkeypatch, post, get, user, settings):
settings.ACTIVITY_STREAM_ENABLED = True
@ -91,7 +83,6 @@ def test_middleware_actor_added(monkeypatch, post, get, user, settings):
assert response.data['summary_fields']['actor']['username'] == 'admin-poster'
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_admin, settings):
settings.ACTIVITY_STREAM_ENABLED = True
@ -101,7 +92,6 @@ def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_adm
assert activity_stream_entry.object_relationship_type == 'awx.main.models.organization.Organization.admin_role'
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin, settings):
settings.ACTIVITY_STREAM_ENABLED = True
@ -113,7 +103,6 @@ def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin,
@pytest.mark.django_db
@pytest.mark.activity_stream_access
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
def test_stream_access_cant_change(activity_stream_entry, organization, org_admin, settings):
settings.ACTIVITY_STREAM_ENABLED = True
access = ActivityStreamAccess(org_admin)
@ -125,7 +114,6 @@ def test_stream_access_cant_change(activity_stream_entry, organization, org_admi
@pytest.mark.django_db
@pytest.mark.activity_stream_access
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
def test_stream_queryset_hides_shows_items(
activity_stream_entry, organization, user, org_admin,
project, org_credential, inventory, label, deploy_jobtemplate,
@ -160,7 +148,6 @@ def test_stream_queryset_hides_shows_items(
@pytest.mark.django_db
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
def test_stream_user_direct_role_updates(get, post, organization_factory):
objects = organization_factory('test_org',
superusers=['admin'],

View File

@ -1,5 +1,4 @@
# Python
from unittest import mock
import pytest
from datetime import timedelta
import urllib.parse
@ -13,14 +12,6 @@ from awx.main.utils import timestamp_apiformat
from django.utils import timezone
def mock_feature_enabled(feature):
return True
def mock_feature_disabled(feature):
return False
def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1):
hosts = hosts(host_count=host_count)
fact_scans(fact_scans=3, timestamp_epoch=epoch)
@ -53,36 +44,7 @@ def check_response_facts(facts_known, response):
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
def check_system_tracking_feature_forbidden(response):
assert 402 == response.status_code
assert 'Your license does not permit use of system tracking.' == response.data['detail']
@mock.patch('awx.api.views.mixin.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_get(hosts, get, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', kwargs={'pk': hosts[0].pk})
response = get(url, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.mixin.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_options(hosts, options, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', kwargs={'pk': hosts[0].pk})
response = options(url, None, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_no_facts_db(hosts, get, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', kwargs={'pk': hosts[0].pk})
@ -94,7 +56,6 @@ def test_no_facts_db(hosts, get, user):
assert response_expected == response.data
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -111,9 +72,7 @@ def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_d
assert 'module' in results[0]
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_basic_options_fields(hosts, fact_scans, options, user, monkeypatch_jsonbfield_get_db_prep_save):
hosts = hosts(host_count=1)
fact_scans(fact_scans=1)
@ -128,7 +87,6 @@ def test_basic_options_fields(hosts, fact_scans, options, user, monkeypatch_json
assert ("packages", "Packages") in response.data['actions']['GET']['module']['choices']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_related_fact_view(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -142,7 +100,6 @@ def test_related_fact_view(hosts, fact_scans, get, user, monkeypatch_jsonbfield_
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_multiple_hosts(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -156,7 +113,6 @@ def test_multiple_hosts(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get
check_url(response.data['results'][i]['related']['fact_view'], fact_known, fact_known.module)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_param_to_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -173,7 +129,6 @@ def test_param_to_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_
check_response_facts(facts_known, response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_param_module(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -189,7 +144,6 @@ def test_param_module(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_d
check_response_facts(facts_known, response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_param_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -205,7 +159,6 @@ def test_param_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_
check_response_facts(facts_known, response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_param_to(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -232,7 +185,6 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
return response
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_normal_user_403(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):
@ -243,7 +195,6 @@ def test_normal_user_403(hosts, fact_scans, get, user, team, monkeypatch_jsonbfi
assert "You do not have permission to perform this action." == response.data['detail']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_super_user_ok(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):
@ -253,7 +204,6 @@ def test_super_user_ok(hosts, fact_scans, get, user, team, monkeypatch_jsonbfiel
assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
@ -265,7 +215,6 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team):
assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):

View File

@ -1,4 +1,3 @@
from unittest import mock
import pytest
import json
@ -8,14 +7,6 @@ from awx.main.utils import timestamp_apiformat
from django.utils import timezone
def mock_feature_enabled(feature):
return True
def mock_feature_disabled(feature):
return False
# TODO: Consider making the fact_scan() fixture a Class, instead of a function, and move this method into it
def find_fact(facts, host_id, module_name, timestamp):
for f in facts:
@ -35,34 +26,6 @@ def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), module_name
return (fact_known, response)
def check_system_tracking_feature_forbidden(response):
assert 402 == response.status_code
assert 'Your license does not permit use of system tracking.' == response.data['detail']
@mock.patch('awx.api.views.mixin.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_get(hosts, get, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_compare_view', kwargs={'pk': hosts[0].pk})
response = get(url, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.mixin.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_license_options(hosts, options, user):
hosts = hosts(host_count=1)
url = reverse('api:host_fact_compare_view', kwargs={'pk': hosts[0].pk})
response = options(url, None, user('admin', True))
check_system_tracking_feature_forbidden(response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_no_fact_found(hosts, get, user):
hosts = hosts(host_count=1)
@ -76,7 +39,6 @@ def test_no_fact_found(hosts, get, user):
assert expected_response == response.data
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
hosts = hosts(host_count=1)
@ -99,7 +61,6 @@ def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_d
assert reverse('api:host_detail', kwargs={'pk': hosts[0].pk}) == response.data['related']['host']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_content(hosts, fact_scans, get, user, fact_ansible_json, monkeypatch_jsonbfield_get_db_prep_save):
(fact_known, response) = setup_common(hosts, fact_scans, get, user)
@ -123,19 +84,16 @@ def _test_search_by_module(hosts, fact_scans, get, user, fact_json, module_name)
assert module_name == response.data['module']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_search_by_module_packages(hosts, fact_scans, get, user, fact_packages_json, monkeypatch_jsonbfield_get_db_prep_save):
_test_search_by_module(hosts, fact_scans, get, user, fact_packages_json, 'packages')
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_search_by_module_services(hosts, fact_scans, get, user, fact_services_json, monkeypatch_jsonbfield_get_db_prep_save):
_test_search_by_module(hosts, fact_scans, get, user, fact_services_json, 'services')
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_search_by_timestamp_and_module(hosts, fact_scans, get, user, fact_packages_json, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now()
@ -160,7 +118,6 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
return response
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_normal_user_403(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):
@ -171,7 +128,6 @@ def test_normal_user_403(hosts, fact_scans, get, user, team, monkeypatch_jsonbfi
assert "You do not have permission to perform this action." == response.data['detail']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_super_user_ok(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):
@ -181,7 +137,6 @@ def test_super_user_ok(hosts, fact_scans, get, user, team, monkeypatch_jsonbfiel
assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):
@ -193,7 +148,6 @@ def test_user_admin_ok(organization, hosts, fact_scans, get, user, team, monkeyp
assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac
@pytest.mark.django_db
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save):

View File

@ -7,7 +7,6 @@ import os
from backports.tempfile import TemporaryDirectory
from django.conf import settings
import pytest
from unittest import mock
# AWX
from awx.main.models import ProjectUpdate
@ -131,10 +130,9 @@ def test_organization_inventory_list(organization, inventory_factory, get, alice
assert get(reverse('api:organization_inventories_list', kwargs={'pk': organization.id}), user=alice).data['count'] == 2
assert get(reverse('api:organization_inventories_list', kwargs={'pk': organization.id}), user=bob).data['count'] == 1
get(reverse('api:organization_inventories_list', kwargs={'pk': organization.id}), user=rando, expect=403)
@pytest.mark.django_db
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
def test_create_organization(post, admin, alice):
new_org = {
'name': 'new org',
@ -146,7 +144,6 @@ def test_create_organization(post, admin, alice):
@pytest.mark.django_db
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
def test_create_organization_xfail(post, alice):
new_org = {
'name': 'new org',
@ -224,27 +221,23 @@ def test_update_organization_max_hosts(get, put, organization, admin, alice, bob
@pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization(delete, organization, admin):
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=admin, expect=204)
@pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization2(delete, organization, alice):
organization.admin_role.members.add(alice)
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=alice, expect=204)
@pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization_xfail1(delete, organization, alice):
organization.member_role.members.add(alice)
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=alice, expect=403)
@pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization_xfail2(delete, organization):
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=None, expect=401)
@ -295,5 +288,3 @@ def test_organization_delete_with_active_jobs(delete, admin, organization, organ
assert resp.data['error'] == u"Resource is being used by running jobs."
assert resp_sorted == expect_sorted

View File

@ -332,13 +332,3 @@ def test_manual_projects_no_update(manual_project, get, admin_user):
response = get(reverse('api:project_detail', kwargs={'pk': manual_project.pk}), admin_user, expect=200)
assert not response.data['summary_fields']['user_capabilities']['start']
assert not response.data['summary_fields']['user_capabilities']['schedule']
@pytest.mark.django_db
def test_license_check_not_called(mocker, job_template, project, org_admin, get):
job_template.project = project
job_template.save() # need this to make the JT visible
mock_license_check = mocker.MagicMock()
with mocker.patch('awx.main.access.BaseAccess.check_license', mock_license_check):
get(reverse('api:job_template_detail', kwargs={'pk': job_template.pk}), org_admin, expect=200)
assert not mock_license_check.called

View File

@ -6,17 +6,10 @@ import json
from awx.api.versioning import reverse
from awx.main.models.jobs import JobTemplate, Job
from awx.main.models.activity_stream import ActivityStream
from awx.conf.license import LicenseForbids
from awx.main.access import JobTemplateAccess
from awx.main.utils.common import get_type_for_model
def mock_no_surveys(self, add_host=False, feature=None, check_expiration=True):
if feature == 'surveys':
raise LicenseForbids("Feature %s is not enabled in the active license." % feature)
else:
pass
@pytest.fixture
def job_template_with_survey(job_template_factory):
@ -24,18 +17,6 @@ def job_template_with_survey(job_template_factory):
return objects.job_template
# Survey license-based denial tests
@mock.patch('awx.api.views.feature_enabled', lambda feature: False)
@pytest.mark.django_db
@pytest.mark.survey
def test_survey_spec_view_denied(job_template_with_survey, get, admin_user):
# TODO: Test non-enterprise license
response = get(reverse('api:job_template_survey_spec',
kwargs={'pk': job_template_with_survey.id}), admin_user, expect=402)
assert response.data['detail'] == 'Your license does not allow adding surveys.'
@mock.patch('awx.main.access.BaseAccess.check_license', mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
@pytest.mark.parametrize("role_field,expected_status_code", [
@ -54,45 +35,7 @@ def test_survey_edit_access(job_template, workflow_job_template, survey_spec_fac
user=rando, data=survey_input_data, expect=expected_status_code)
@mock.patch('awx.main.access.BaseAccess.check_license', mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_deny_enabling_survey(deploy_jobtemplate, patch, admin_user):
response = patch(url=deploy_jobtemplate.get_absolute_url(),
data=dict(survey_enabled=True), user=admin_user, expect=402)
assert response.data['detail'] == 'Feature surveys is not enabled in the active license.'
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_job_start_blocked_without_survey_license(job_template_with_survey, admin_user):
"""Check that user can't start a job with surveys without a survey license."""
access = JobTemplateAccess(admin_user)
with pytest.raises(LicenseForbids):
access.can_start(job_template_with_survey)
@mock.patch('awx.main.access.BaseAccess.check_license', mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_deny_creating_with_survey(project, post, admin_user):
response = post(
url=reverse('api:job_template_list'),
data=dict(
name = 'JT with survey',
job_type = 'run',
project = project.pk,
playbook = 'helloworld.yml',
ask_credential_on_launch = True,
ask_inventory_on_launch = True,
survey_enabled = True),
user=admin_user, expect=402)
assert response.data['detail'] == 'Feature surveys is not enabled in the active license.'
# Test normal operations with survey license work
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
@pytest.mark.survey
def test_survey_spec_view_allowed(deploy_jobtemplate, get, admin_user):
@ -100,7 +43,6 @@ def test_survey_spec_view_allowed(deploy_jobtemplate, get, admin_user):
admin_user, expect=200)
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
@pytest.mark.survey
def test_survey_spec_sucessful_creation(survey_spec_factory, job_template, post, admin_user):
@ -111,7 +53,6 @@ def test_survey_spec_sucessful_creation(survey_spec_factory, job_template, post,
assert updated_jt.survey_spec == survey_input_data
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
@pytest.mark.parametrize('with_default', [True, False])
@pytest.mark.parametrize('value, status', [
@ -154,7 +95,6 @@ def test_survey_spec_passwords_are_encrypted_on_launch(job_template_factory, pos
assert "for 'secret_value' expected to be a string." in json.dumps(resp.data)
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
def test_survey_spec_passwords_with_empty_default(job_template_factory, post, admin_user):
objects = job_template_factory('jt', organization='org1', project='prj',
@ -186,7 +126,6 @@ def test_survey_spec_passwords_with_empty_default(job_template_factory, post, ad
}
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
@pytest.mark.parametrize('default, launch_value, expected_extra_vars, status', [
['', '$encrypted$', {'secret_value': ''}, 201],
@ -238,7 +177,6 @@ def test_survey_spec_passwords_with_default_optional(job_template_factory, post,
assert launch_value not in json.loads(job.extra_vars).values()
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
@pytest.mark.parametrize('default, launch_value, expected_extra_vars, status', [
['', '$encrypted$', {'secret_value': ''}, 201],
@ -281,7 +219,6 @@ def test_survey_spec_passwords_with_default_required(job_template_factory, post,
assert launch_value not in json.loads(job.extra_vars).values()
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
@pytest.mark.parametrize('default, status', [
('SUPERSECRET', 200),
@ -318,7 +255,6 @@ def test_survey_spec_default_passwords_are_encrypted(job_template, post, admin_u
assert "expected to be string." in str(resp.data)
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db
def test_survey_spec_default_passwords_encrypted_on_update(job_template, post, put, admin_user):
input_data = {
@ -344,41 +280,6 @@ def test_survey_spec_default_passwords_encrypted_on_update(job_template, post, p
assert updated_jt.survey_spec == JobTemplate.objects.get(pk=job_template.pk).survey_spec
# Test actions that should be allowed with non-survey license
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_disable_survey_access_without_license(job_template_with_survey, admin_user):
"""Assure that user can disable a JT survey after downgrading license."""
access = JobTemplateAccess(admin_user)
assert access.can_change(job_template_with_survey, dict(survey_enabled=False))
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_delete_survey_access_without_license(job_template_with_survey, admin_user):
"""Assure that access.py allows deleting surveys after downgrading license."""
access = JobTemplateAccess(admin_user)
assert access.can_change(job_template_with_survey, dict(survey_spec=None))
assert access.can_change(job_template_with_survey, dict(survey_spec={}))
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_job_start_allowed_with_survey_spec(job_template_factory, admin_user):
"""After user downgrades survey license and disables survey on the JT,
check that jobs still launch even if the survey_spec data persists."""
objects = job_template_factory('jt', project='prj', survey='submitter_email')
obj = objects.job_template
obj.survey_enabled = False
obj.save()
access = JobTemplateAccess(admin_user)
assert access.can_start(job_template_with_survey, {})
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_job_template_delete_access_with_survey(job_template_with_survey, admin_user):
@ -388,11 +289,9 @@ def test_job_template_delete_access_with_survey(job_template_with_survey, admin_
assert access.can_delete(job_template_with_survey)
@mock.patch('awx.api.views.feature_enabled', lambda feature: False)
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@pytest.mark.django_db
@pytest.mark.survey
def test_delete_survey_spec_without_license(job_template_with_survey, delete, admin_user):
def test_delete_survey_spec(job_template_with_survey, delete, admin_user):
"""Functional delete test through the survey_spec view."""
delete(reverse('api:job_template_survey_spec', kwargs={'pk': job_template_with_survey.pk}),
admin_user, expect=200)
@ -400,7 +299,6 @@ def test_delete_survey_spec_without_license(job_template_with_survey, delete, ad
assert new_jt.survey_spec == {}
@mock.patch('awx.main.access.BaseAccess.check_license', lambda self, **kwargs: True)
@mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job',
lambda self, **kwargs: mock.MagicMock(spec=Job, id=968))
@mock.patch('awx.api.serializers.JobSerializer.to_representation', lambda self, obj: {})
@ -418,24 +316,6 @@ def test_launch_survey_enabled_but_no_survey_spec(job_template_factory, post, ad
assert 'survey_var' in response.data['ignored_fields']['extra_vars']
@mock.patch('awx.main.access.BaseAccess.check_license', new=mock_no_surveys)
@mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job',
lambda self: mock.MagicMock(spec=Job, id=968))
@mock.patch('awx.api.serializers.JobSerializer.to_representation', lambda self, obj: {})
@pytest.mark.django_db
@pytest.mark.survey
def test_launch_with_non_empty_survey_spec_no_license(job_template_factory, post, admin_user):
"""Assure jobs can still be launched from JTs with a survey_spec
when the survey is diabled."""
objects = job_template_factory('jt', organization='org1', project='prj',
inventory='inv', credential='cred',
survey='survey_var')
obj = objects.job_template
obj.survey_enabled = False
obj.save()
post(reverse('api:job_template_launch', kwargs={'pk': obj.pk}), {}, admin_user, expect=201)
@pytest.mark.django_db
@pytest.mark.survey
def test_redact_survey_passwords_in_activity_stream(job_template_with_survey_passwords):

View File

@ -3,7 +3,6 @@
# Python
import pytest
from unittest import mock
from dateutil.relativedelta import relativedelta
from datetime import timedelta
@ -17,13 +16,6 @@ from awx.main.models.fact import Fact
from awx.main.models.inventory import Host
def mock_feature_enabled(feature):
return True
def mock_feature_disabled(feature):
return False
@pytest.mark.django_db
def test_cleanup_granularity(fact_scans, hosts, monkeypatch_jsonbfield_get_db_prep_save):
@ -101,17 +93,7 @@ def test_cleanup_logic(fact_scans, hosts, monkeypatch_jsonbfield_get_db_prep_sav
assert fact.timestamp == timestamp_pivot
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_disabled)
@pytest.mark.django_db
@pytest.mark.license_feature
def test_system_tracking_feature_disabled(mocker):
cmd = Command()
with pytest.raises(CommandError) as err:
cmd.handle(None)
assert 'The System Tracking feature is not enabled for your instance' in str(err.value)
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_parameters_ok(mocker):
run = mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run')
@ -185,7 +167,6 @@ def test_string_time_to_timestamp_invalid():
assert res is None
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db
def test_parameters_fail(mocker):
# Mock run() just in case, but it should never get called because an error should be thrown

View File

@ -6,12 +6,6 @@ from awx.api.versioning import reverse
from awx.main.models.rbac import Role, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
def mock_feature_enabled(feature):
return True
#@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.fixture
def role():

View File

@ -13,7 +13,6 @@ from awx.main.access import (
SystemJobTemplateAccess,
)
from awx.conf.license import LicenseForbids
from awx.main.models import (
Credential,
CredentialType,
@ -21,7 +20,6 @@ from awx.main.models import (
Project,
Role,
Organization,
Instance,
)
@ -204,31 +202,20 @@ def test_jt_add_scan_job_check(job_template_with_ids, user_unit):
else:
raise Exception('Item requested has not been mocked')
with mock.patch.object(JobTemplateAccess, 'check_license', return_value=None):
with mock.patch('awx.main.models.rbac.Role.__contains__', return_value=True):
with mock.patch('awx.main.access.get_object_or_400', mock_get_object):
assert access.can_add({
'project': project.pk,
'inventory': inventory.pk,
'job_type': 'scan'
})
def mock_raise_license_forbids(self, add_host=False, feature=None, check_expiration=True):
raise LicenseForbids("Feature not enabled")
with mock.patch('awx.main.models.rbac.Role.__contains__', return_value=True):
with mock.patch('awx.main.access.get_object_or_400', mock_get_object):
assert access.can_add({
'project': project.pk,
'inventory': inventory.pk,
'job_type': 'scan'
})
def mock_raise_none(self, add_host=False, feature=None, check_expiration=True):
return None
def test_jt_can_start_ha(job_template_with_ids):
with mock.patch.object(Instance.objects, 'active_count', return_value=2):
with mock.patch('awx.main.access.BaseAccess.check_license', new=mock_raise_license_forbids):
with pytest.raises(LicenseForbids):
JobTemplateAccess(user_unit).can_start(job_template_with_ids)
def test_jt_can_add_bad_data(user_unit):
"Assure that no server errors are returned if we call JT can_add with bad data"
access = JobTemplateAccess(user_unit)

View File

@ -990,9 +990,8 @@ def has_model_field_prefetched(model_obj, field_name):
def get_external_account(user):
from django.conf import settings
from awx.conf.license import feature_enabled
account_type = None
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None) and feature_enabled('ldap'):
if getattr(settings, 'AUTH_LDAP_SERVER_URI', None):
try:
if user.pk and user.profile.ldap_dn and not user.has_usable_password():
account_type = "ldap"

View File

@ -31,7 +31,6 @@ from social_core.backends.saml import SAMLAuth as BaseSAMLAuth
from social_core.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityProvider
# Ansible Tower
from awx.conf.license import feature_enabled
from awx.sso.models import UserEnterpriseAuth
logger = logging.getLogger('awx.sso.backends')
@ -94,9 +93,6 @@ class LDAPBackend(BaseLDAPBackend):
if not self.settings.SERVER_URI:
return None
if not feature_enabled('ldap'):
logger.error("Unable to authenticate, license does not support LDAP authentication")
return None
try:
user = User.objects.get(username=username)
if user and (not user.profile or not user.profile.ldap_dn):
@ -121,9 +117,6 @@ class LDAPBackend(BaseLDAPBackend):
def get_user(self, user_id):
if not self.settings.SERVER_URI:
return None
if not feature_enabled('ldap'):
logger.error("Unable to get_user, license does not support LDAP authentication")
return None
return super(LDAPBackend, self).get_user(user_id)
# Disable any LDAP based authorization / permissions checking.
@ -188,20 +181,14 @@ class RADIUSBackend(BaseRADIUSBackend):
Custom Radius backend to verify license status
'''
def authenticate(self, username, password):
def authenticate(self, username, password):
if not django_settings.RADIUS_SERVER:
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to authenticate, license does not support RADIUS authentication")
return None
return super(RADIUSBackend, self).authenticate(None, username, password)
def get_user(self, user_id):
if not django_settings.RADIUS_SERVER:
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to get_user, license does not support RADIUS authentication")
return None
user = super(RADIUSBackend, self).get_user(user_id)
if not user.has_usable_password():
return user
@ -218,9 +205,6 @@ class TACACSPlusBackend(object):
def authenticate(self, username, password):
if not django_settings.TACACSPLUS_HOST:
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to authenticate, license does not support TACACS+ authentication")
return None
try:
# Upstream TACACS+ client does not accept non-string, so convert if needed.
auth = tacacs_plus.TACACSClient(
@ -241,9 +225,6 @@ class TACACSPlusBackend(object):
def get_user(self, user_id):
if not django_settings.TACACSPLUS_HOST:
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to get user, license does not support TACACS+ authentication")
return None
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
@ -294,9 +275,6 @@ class SAMLAuth(BaseSAMLAuth):
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to authenticate, license does not support SAML authentication")
return None
user = super(SAMLAuth, self).authenticate(*args, **kwargs)
# Comes from https://github.com/omab/python-social-auth/blob/v0.2.21/social/backends/base.py#L91
if getattr(user, 'is_new', False):
@ -311,9 +289,6 @@ class SAMLAuth(BaseSAMLAuth):
django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT,
django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS]):
return None
if not feature_enabled('enterprise_auth'):
logger.error("Unable to get_user, license does not support SAML authentication")
return None
return super(SAMLAuth, self).get_user(user_id)

View File

@ -157,7 +157,6 @@ def _register_ldap(append=None):
category=_('LDAP'),
category_slug='ldap',
placeholder='ldaps://ldap.example.com:636',
feature_required='ldap',
)
register(
@ -172,7 +171,6 @@ def _register_ldap(append=None):
' user information. Refer to the Ansible Tower documentation for example syntax.'),
category=_('LDAP'),
category_slug='ldap',
feature_required='ldap',
)
register(
@ -184,7 +182,6 @@ def _register_ldap(append=None):
help_text=_('Password used to bind LDAP user account.'),
category=_('LDAP'),
category_slug='ldap',
feature_required='ldap',
encrypted=True,
)
@ -196,7 +193,6 @@ def _register_ldap(append=None):
help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'),
category=_('LDAP'),
category_slug='ldap',
feature_required='ldap',
)
register(
@ -216,7 +212,6 @@ def _register_ldap(append=None):
('OPT_REFERRALS', 0),
('OPT_NETWORK_TIMEOUT', 30)
]),
feature_required='ldap',
)
register(
@ -237,7 +232,6 @@ def _register_ldap(append=None):
'SCOPE_SUBTREE',
'(sAMAccountName=%(user)s)',
),
feature_required='ldap',
)
register(
@ -255,7 +249,6 @@ def _register_ldap(append=None):
category=_('LDAP'),
category_slug='ldap',
placeholder='uid=%(user)s,OU=Users,DC=example,DC=com',
feature_required='ldap',
)
register(
@ -274,7 +267,6 @@ def _register_ldap(append=None):
('last_name', 'sn'),
('email', 'mail'),
]),
feature_required='ldap',
)
register(
@ -292,7 +284,6 @@ def _register_ldap(append=None):
'SCOPE_SUBTREE',
'(objectClass=group)',
),
feature_required='ldap',
)
register(
@ -304,7 +295,6 @@ def _register_ldap(append=None):
'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups'),
category=_('LDAP'),
category_slug='ldap',
feature_required='ldap',
default='MemberDNGroupType',
depends_on=['AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str)],
)
@ -325,7 +315,6 @@ def _register_ldap(append=None):
('member_attr', 'member'),
('name_attr', 'cn'),
]),
feature_required='ldap',
depends_on=['AUTH_LDAP{}_GROUP_TYPE'.format(append_str)],
)
@ -343,7 +332,6 @@ def _register_ldap(append=None):
category=_('LDAP'),
category_slug='ldap',
placeholder='CN=Tower Users,OU=Users,DC=example,DC=com',
feature_required='ldap',
)
register(
@ -359,7 +347,6 @@ def _register_ldap(append=None):
category=_('LDAP'),
category_slug='ldap',
placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com',
feature_required='ldap',
)
register(
@ -376,7 +363,6 @@ def _register_ldap(append=None):
('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'),
('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'),
]),
feature_required='ldap',
)
register(
@ -404,7 +390,6 @@ def _register_ldap(append=None):
('remove_admins', True),
])),
]),
feature_required='ldap',
)
register(
@ -428,7 +413,6 @@ def _register_ldap(append=None):
('remove', False),
])),
]),
feature_required='ldap',
)
@ -454,7 +438,6 @@ register(
category=_('RADIUS'),
category_slug='radius',
placeholder='radius.example.com',
feature_required='enterprise_auth',
)
register(
@ -467,7 +450,6 @@ register(
help_text=_('Port of RADIUS server.'),
category=_('RADIUS'),
category_slug='radius',
feature_required='enterprise_auth',
)
register(
@ -479,7 +461,6 @@ register(
help_text=_('Shared secret for authenticating to RADIUS server.'),
category=_('RADIUS'),
category_slug='radius',
feature_required='enterprise_auth',
encrypted=True,
)
@ -496,7 +477,6 @@ register(
help_text=_('Hostname of TACACS+ server.'),
category=_('TACACS+'),
category_slug='tacacsplus',
feature_required='enterprise_auth',
)
register(
@ -509,7 +489,6 @@ register(
help_text=_('Port number of TACACS+ server.'),
category=_('TACACS+'),
category_slug='tacacsplus',
feature_required='enterprise_auth',
)
register(
@ -522,7 +501,6 @@ register(
help_text=_('Shared secret for authenticating to TACACS+ server.'),
category=_('TACACS+'),
category_slug='tacacsplus',
feature_required='enterprise_auth',
encrypted=True,
)
@ -535,7 +513,6 @@ register(
help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'),
category=_('TACACS+'),
category_slug='tacacsplus',
feature_required='enterprise_auth',
)
register(
@ -547,7 +524,6 @@ register(
help_text=_('Choose the authentication protocol used by TACACS+ client.'),
category=_('TACACS+'),
category_slug='tacacsplus',
feature_required='enterprise_auth',
)
###############################################################################
@ -953,7 +929,6 @@ register(
category=_('SAML'),
category_slug='saml',
depends_on=['TOWER_URL_BASE'],
feature_required='enterprise_auth',
)
register(
@ -966,7 +941,6 @@ register(
'metadata file, you can download one from this URL.'),
category=_('SAML'),
category_slug='saml',
feature_required='enterprise_auth',
)
register(
@ -980,7 +954,6 @@ register(
'This is usually the URL for Tower.'),
category=_('SAML'),
category_slug='saml',
feature_required='enterprise_auth',
depends_on=['TOWER_URL_BASE'],
)
@ -995,7 +968,6 @@ register(
'and include the certificate content here.'),
category=_('SAML'),
category_slug='saml',
feature_required='enterprise_auth',
)
register(
@ -1009,7 +981,6 @@ register(
'and include the private key content here.'),
category=_('SAML'),
category_slug='saml',
feature_required='enterprise_auth',
encrypted=True,
)
@ -1029,7 +1000,6 @@ register(
('url', 'http://www.example.com'),
])),
]),
feature_required='enterprise_auth',
)
register(
@ -1047,7 +1017,6 @@ register(
('givenName', 'Technical Contact'),
('emailAddress', 'techsup@example.com'),
]),
feature_required='enterprise_auth',
)
register(
@ -1065,7 +1034,6 @@ register(
('givenName', 'Support Contact'),
('emailAddress', 'support@example.com'),
]),
feature_required='enterprise_auth',
)
register(
@ -1102,7 +1070,6 @@ register(
('attr_email', 'User.email'),
])),
]),
feature_required='enterprise_auth',
)
register(
@ -1135,7 +1102,6 @@ register(
("signatureAlgorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
("digestAlgorithm", "http://www.w3.org/2000/09/xmldsig#sha1"),
]),
feature_required='enterprise_auth',
)
register(
@ -1149,7 +1115,6 @@ register(
category=_('SAML'),
category_slug='saml',
placeholder=collections.OrderedDict(),
feature_required='enterprise_auth',
)
register(
@ -1167,7 +1132,6 @@ register(
('department', 'department'),
('manager_full_name', 'manager_full_name')
],
feature_required='enterprise_auth',
)
register(
@ -1180,7 +1144,6 @@ register(
category=_('SAML'),
category_slug='saml',
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
feature_required='enterprise_auth',
)
register(
@ -1193,7 +1156,6 @@ register(
category=_('SAML'),
category_slug='saml',
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
feature_required='enterprise_auth',
)
register(
@ -1211,7 +1173,6 @@ register(
('remove', True),
('remove_admins', True),
]),
feature_required='enterprise_auth',
)
register(
@ -1253,7 +1214,6 @@ register(
]),
]),
]),
feature_required='enterprise_auth',
)

View File

@ -25,7 +25,6 @@ from awx.sso.ldap_group_types import PosixUIDGroupType # noqa
# Tower
from awx.conf import fields
from awx.conf.license import feature_enabled
from awx.main.validators import validate_certificate
from awx.sso.validators import ( # noqa
validate_ldap_dn,
@ -135,17 +134,6 @@ class AuthenticationBackendsField(fields.StringListField):
('django.contrib.auth.backends.ModelBackend', []),
])
REQUIRED_BACKEND_FEATURE = {
'awx.sso.backends.LDAPBackend': 'ldap',
'awx.sso.backends.LDAPBackend1': 'ldap',
'awx.sso.backends.LDAPBackend2': 'ldap',
'awx.sso.backends.LDAPBackend3': 'ldap',
'awx.sso.backends.LDAPBackend4': 'ldap',
'awx.sso.backends.LDAPBackend5': 'ldap',
'awx.sso.backends.RADIUSBackend': 'enterprise_auth',
'awx.sso.backends.SAMLAuth': 'enterprise_auth',
}
@classmethod
def get_all_required_settings(cls):
all_required_settings = set(['LICENSE'])
@ -164,15 +152,12 @@ class AuthenticationBackendsField(fields.StringListField):
except AttributeError:
backends = self.REQUIRED_BACKEND_SETTINGS.keys()
# Filter which authentication backends are enabled based on their
# required settings being defined and non-empty. Also filter available
# backends based on license features.
# required settings being defined and non-empty.
for backend, required_settings in self.REQUIRED_BACKEND_SETTINGS.items():
if backend not in backends:
continue
required_feature = self.REQUIRED_BACKEND_FEATURE.get(backend, '')
if not required_feature or feature_enabled(required_feature):
if all([getattr(settings, rs, None) for rs in required_settings]):
continue
if all([getattr(settings, rs, None) for rs in required_settings]):
continue
backends = [x for x in backends if x != backend]
return backends
@ -782,4 +767,3 @@ class SAMLTeamAttrField(BaseDictWithChildField):
'remove': fields.BooleanField(required=False),
'saml_attr': fields.CharField(required=False, allow_null=True),
}

View File

@ -13,9 +13,6 @@ from social_core.exceptions import AuthException
from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
# Tower
from awx.conf.license import feature_enabled
logger = logging.getLogger('awx.sso.pipeline')
@ -83,18 +80,11 @@ def _update_m2m_from_expression(user, rel, expr, remove=True):
def _update_org_from_attr(user, rel, attr, remove, remove_admins):
from awx.main.models import Organization
multiple_orgs = feature_enabled('multiple_organizations')
org_ids = []
for org_name in attr:
if multiple_orgs:
org = Organization.objects.get_or_create(name=org_name)[0]
else:
try:
org = Organization.objects.order_by('pk')[0]
except IndexError:
continue
org = Organization.objects.get_or_create(name=org_name)[0]
org_ids.append(org.id)
getattr(org, rel).members.add(user)
@ -116,19 +106,10 @@ def update_user_orgs(backend, details, user=None, *args, **kwargs):
if not user:
return
from awx.main.models import Organization
multiple_orgs = feature_enabled('multiple_organizations')
org_map = backend.setting('ORGANIZATION_MAP') or {}
for org_name, org_opts in org_map.items():
org = Organization.objects.get_or_create(name=org_name)[0]
# Get or create the org to update. If the license only allows for one
# org, always use the first active org, unless no org exists.
if multiple_orgs:
org = Organization.objects.get_or_create(name=org_name)[0]
else:
try:
org = Organization.objects.order_by('pk')[0]
except IndexError:
continue
# Update org admins from expression(s).
remove = bool(org_opts.get('remove', True))
@ -150,21 +131,13 @@ def update_user_teams(backend, details, user=None, *args, **kwargs):
if not user:
return
from awx.main.models import Organization, Team
multiple_orgs = feature_enabled('multiple_organizations')
team_map = backend.setting('TEAM_MAP') or {}
for team_name, team_opts in team_map.items():
# Get or create the org to update.
if 'organization' not in team_opts:
continue
org = Organization.objects.get_or_create(name=team_opts['organization'])[0]
# Get or create the org to update. If the license only allows for one
# org, always use the first active org, unless no org exists.
if multiple_orgs:
if 'organization' not in team_opts:
continue
org = Organization.objects.get_or_create(name=team_opts['organization'])[0]
else:
try:
org = Organization.objects.order_by('pk')[0]
except IndexError:
continue
# Update team members from expression(s).
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
@ -196,7 +169,6 @@ def update_user_teams_by_saml_attr(backend, details, user=None, *args, **kwargs)
return
from awx.main.models import Organization, Team
from django.conf import settings
multiple_orgs = feature_enabled('multiple_organizations')
team_map = settings.SOCIAL_AUTH_SAML_TEAM_ATTR
if team_map.get('saml_attr') is None:
return
@ -210,17 +182,11 @@ def update_user_teams_by_saml_attr(backend, details, user=None, *args, **kwargs)
for team_name_map in team_map.get('team_org_map', []):
team_name = team_name_map.get('team', '')
if team_name in saml_team_names:
if multiple_orgs:
if not team_name_map.get('organization', ''):
# Settings field validation should prevent this.
logger.error("organization name invalid for team {}".format(team_name))
continue
org = Organization.objects.get_or_create(name=team_name_map['organization'])[0]
else:
try:
org = Organization.objects.order_by('pk')[0]
except IndexError:
continue
if not team_name_map.get('organization', ''):
# Settings field validation should prevent this.
logger.error("organization name invalid for team {}".format(team_name))
continue
org = Organization.objects.get_or_create(name=team_name_map['organization'])[0]
team = Team.objects.get_or_create(name=team_name, organization=org)[0]
team_ids.append(team.id)

View File

@ -31,21 +31,3 @@ def existing_tacacsplus_user():
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
enterprise_auth.save()
return user
@pytest.fixture
def feature_enabled():
def func(feature):
def inner(name):
return name == feature
return inner
return func
@pytest.fixture
def feature_disabled():
def func(feature):
def inner(name):
return False
return inner
return func

View File

@ -8,24 +8,11 @@ def test_empty_host_fails_auth(tacacsplus_backend):
assert ret_user is None
def test_disabled_enterprise_auth_fails_auth(tacacsplus_backend, feature_disabled):
with mock.patch('awx.sso.backends.django_settings') as settings,\
mock.patch('awx.sso.backends.logger') as logger,\
mock.patch('awx.sso.backends.feature_enabled', feature_disabled('enterprise_auth')):
settings.TACACSPLUS_HOST = 'localhost'
ret_user = tacacsplus_backend.authenticate(u"user", u"pass")
assert ret_user is None
logger.error.assert_called_once_with(
"Unable to authenticate, license does not support TACACS+ authentication"
)
def test_client_raises_exception(tacacsplus_backend, feature_enabled):
def test_client_raises_exception(tacacsplus_backend):
client = mock.MagicMock()
client.authenticate.side_effect=Exception("foo")
with mock.patch('awx.sso.backends.django_settings') as settings,\
mock.patch('awx.sso.backends.logger') as logger,\
mock.patch('awx.sso.backends.feature_enabled', feature_enabled('enterprise_auth')),\
mock.patch('tacacs_plus.TACACSClient', return_value=client):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
@ -36,13 +23,12 @@ def test_client_raises_exception(tacacsplus_backend, feature_enabled):
)
def test_client_return_invalid_fails_auth(tacacsplus_backend, feature_enabled):
def test_client_return_invalid_fails_auth(tacacsplus_backend):
auth = mock.MagicMock()
auth.valid = False
client = mock.MagicMock()
client.authenticate.return_value = auth
with mock.patch('awx.sso.backends.django_settings') as settings,\
mock.patch('awx.sso.backends.feature_enabled', feature_enabled('enterprise_auth')),\
mock.patch('tacacs_plus.TACACSClient', return_value=client):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
@ -50,7 +36,7 @@ def test_client_return_invalid_fails_auth(tacacsplus_backend, feature_enabled):
assert ret_user is None
def test_client_return_valid_passes_auth(tacacsplus_backend, feature_enabled):
def test_client_return_valid_passes_auth(tacacsplus_backend):
auth = mock.MagicMock()
auth.valid = True
client = mock.MagicMock()
@ -58,7 +44,6 @@ def test_client_return_valid_passes_auth(tacacsplus_backend, feature_enabled):
user = mock.MagicMock()
user.has_usable_password = mock.MagicMock(return_value=False)
with mock.patch('awx.sso.backends.django_settings') as settings,\
mock.patch('awx.sso.backends.feature_enabled', feature_enabled('enterprise_auth')),\
mock.patch('tacacs_plus.TACACSClient', return_value=client),\
mock.patch('awx.sso.backends._get_or_set_enterprise_user', return_value=user):
settings.TACACSPLUS_HOST = 'localhost'

View File

@ -61,27 +61,6 @@ export default {
return qs.search(path, stateParams);
}
],
features: ['FeaturesService', '$state', '$rootScope',
function(FeaturesService, $state, $rootScope) {
var features = FeaturesService.get();
if (features) {
if (FeaturesService.featureEnabled('activity_streams')) {
return features;
} else {
$state.go('dashboard');
}
}
$rootScope.featuresConfigured.promise.then(function(features) {
if (features) {
if (FeaturesService.featureEnabled('activity_streams')) {
return features;
} else {
$state.go('dashboard');
}
}
});
}
],
subTitle: ['$stateParams', 'Rest', 'ModelToBasePathKey', 'GetBasePath',
'ProcessErrors',
function($stateParams, rest, ModelToBasePathKey, getBasePath,

View File

@ -91,7 +91,6 @@ angular
'templates',
'PromptDialog',
'AWDirectives',
'features',
instanceGroups,
atFeatures,
@ -166,11 +165,11 @@ angular
'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer',
'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest',
'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
'FeaturesService', '$filter', 'SocketService', 'AppStrings', '$transitions',
'$filter', 'SocketService', 'AppStrings', '$transitions',
function($stateExtender, $q, $compile, $cookies, $rootScope, $log, $stateParams,
CheckLicense, $location, Authorization, LoadBasePaths, Timer,
LoadConfig, Store, pendoService, Prompt, Rest, Wait,
ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService,
ProcessErrors, $state, GetBasePath, ConfigService,
$filter, SocketService, AppStrings, $transitions) {
$rootScope.$state = $state;
@ -381,7 +380,6 @@ angular
SocketService.init();
pendoService.issuePendoIdentity();
CheckLicense.test();
FeaturesService.get();
if ($location.$$path === "/home" && $state.current && $state.current.name === "") {
$state.go('dashboard');
} else if ($location.$$path === "/portal" && $state.current && $state.current.name === "") {
@ -413,9 +411,9 @@ angular
// create a promise that will resolve state $AnsibleConfig is loaded
$rootScope.loginConfig = $q.defer();
}
if (!$rootScope.featuresConfigured) {
// create a promise that will resolve when features are loaded
$rootScope.featuresConfigured = $q.defer();
if (!$rootScope.basePathsLoaded) {
// create a promise that will resolve when base paths are loaded
$rootScope.basePathsLoaded = $q.defer();
}
$rootScope.licenseMissing = true;
//the authorization controller redirects to the home page automatcially if there is no last path defined. in order to override

View File

@ -1,6 +1,6 @@
export default
['templateUrl', '$state', 'FeaturesService','$rootScope', 'Store', 'Empty', '$window', 'BreadCrumbService', 'i18n', '$transitions',
function(templateUrl, $state, FeaturesService, $rootScope, Store, Empty, $window, BreadCrumbService, i18n, $transitions) {
['templateUrl', '$state', '$rootScope', 'Store', 'Empty', '$window', 'BreadCrumbService', 'i18n', '$transitions',
function(templateUrl, $state, $rootScope, Store, Empty, $window, BreadCrumbService, i18n, $transitions) {
return {
restrict: 'E',
templateUrl: templateUrl('bread-crumb/bread-crumb'),
@ -28,40 +28,23 @@ export default
streamConfig = (trans.to() && trans.to().data) ? trans.to().data : {};
if(streamConfig && streamConfig.activityStream) {
// Check to see if activity_streams is an enabled feature. $transition.onSuccess fires
// after the resolve on the state declaration so features should be available at this
// point. We use the get() function call here just in case the features aren't available.
// The get() function will only fire off the server call if the features aren't already
// attached to the $rootScope.
var features = FeaturesService.get();
if(features){
scope.loadingLicense = false;
scope.activityStreamActive = (trans.to().name === 'activityStream') ? true : false;
scope.activityStreamTooltip = (trans.to().name === 'activityStream') ? i18n._('Hide Activity Stream') : i18n._('View Activity Stream');
scope.showActivityStreamButton = (FeaturesService.featureEnabled('activity_streams') || trans.to().name ==='activityStream') ? true : false;
}
scope.loadingLicense = false;
scope.activityStreamActive = (trans.to().name === 'activityStream') ? true : false;
scope.activityStreamTooltip = (trans.to().name === 'activityStream') ? i18n._('Hide Activity Stream') : i18n._('View Activity Stream');
scope.showActivityStreamButton = true;
}
else {
scope.showActivityStreamButton = false;
}
scope.showRefreshButton = (streamConfig && streamConfig.refreshButton) ? true : false;
scope.alwaysShowRefreshButton = (streamConfig && streamConfig.alwaysShowRefreshButton) ? true: false;
});
// scope.$on('featuresLoaded', function(){
$rootScope.featuresConfigured.promise.then(function(features){
// var features = FeaturesService.get();
if(features){
scope.loadingLicense = false;
scope.activityStreamActive = ($state.current.name === 'activityStream') ? true : false;
scope.activityStreamTooltip = ($state.current.name === 'activityStream') ? 'Hide Activity Stream' : 'View Activity Stream';
scope.showActivityStreamButton = ((FeaturesService.featureEnabled('activity_streams') && streamConfig && streamConfig.activityStream) || $state.current.name ==='activityStream') ? true : false;
}
});
scope.loadingLicense = false;
scope.activityStreamActive = ($state.current.name === 'activityStream') ? true : false;
scope.activityStreamTooltip = ($state.current.name === 'activityStream') ? 'Hide Activity Stream' : 'View Activity Stream';
scope.showActivityStreamButton = ((streamConfig && streamConfig.activityStream) || $state.current.name ==='activityStream') ? true : false;
function onResize(){
BreadCrumbService.truncateCrumbs();

View File

@ -107,7 +107,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "!ldap_auth || configuration_ldap_template_form.$invalid || configuration_ldap_template_form.$pending"
ngDisabled: "configuration_ldap_template_form.$invalid || configuration_ldap_template_form.$pending"
}
}
};

View File

@ -107,7 +107,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "!ldap_auth || configuration_ldap1_template_form.$invalid || configuration_ldap1_template_form.$pending"
ngDisabled: "configuration_ldap1_template_form.$invalid || configuration_ldap1_template_form.$pending"
}
}
};

View File

@ -107,7 +107,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "!ldap_auth || configuration_ldap2_template_form.$invalid || configuration_ldap2_template_form.$pending"
ngDisabled: "configuration_ldap2_template_form.$invalid || configuration_ldap2_template_form.$pending"
}
}
};

View File

@ -39,7 +39,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "!enterprise_auth || configuration_radius_template_form.$invalid || configuration_radius_template_form.$pending"
ngDisabled: "configuration_radius_template_form.$invalid || configuration_radius_template_form.$pending"
}
}
};

View File

@ -126,7 +126,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "!enterprise_auth || configuration_saml_template_form.$invalid || configuration_saml_template_form.$pending"
ngDisabled: "configuration_saml_template_form.$invalid || configuration_saml_template_form.$pending"
}
}
};

View File

@ -52,7 +52,7 @@ export default ['i18n', function(i18n) {
},
save: {
ngClick: 'vm.formSave()',
ngDisabled: "!enterprise_auth || configuration_tacacs_template_form.$invalid || configuration_tacacs_template_form.$pending"
ngDisabled: "configuration_tacacs_template_form.$invalid || configuration_tacacs_template_form.$pending"
}
}
};

View File

@ -81,13 +81,13 @@ export default [
$scope.configDataResolve = configDataResolve;
$scope.formDefs = formDefs;
// check if it's auditor, show messageBar
// check if it's auditor, show messageBar
$scope.show_auditor_bar = false;
if($rootScope.user_is_system_auditor && Store('show_auditor_bar') !== false) {
$scope.show_auditor_bar = true;
} else {
$scope.show_auditor_bar = false;
}
}
var populateFromApi = function() {
SettingsService.getCurrentValues()
@ -145,19 +145,6 @@ export default [
});
$scope.$broadcast('populated', data);
});
ConfigService.getConfig()
.then(function(data) {
$scope.ldap_auth = data.license_info.features.ldap;
$scope.enterprise_auth = data.license_info.features.enterprise_auth;
})
.catch(function(data, status) {
ProcessErrors($scope, data, status, null,
{
hdr: i18n._('Error'),
msg: i18n._('There was an error getting config values: ') + status
}
);
});
};
populateFromApi();

View File

@ -23,7 +23,7 @@ export default {
resolve: {
graphData: ['$q', 'jobStatusGraphData', '$rootScope',
function($q, jobStatusGraphData, $rootScope) {
return $rootScope.featuresConfigured.promise.then(function() {
return $rootScope.basePathsLoaded.promise.then(function() {
return $q.all({
jobStatus: jobStatusGraphData.get("month", "all"),
});

View File

@ -9,9 +9,9 @@ import {N_} from "../i18n";
export default
['Wait', '$state', '$scope', '$rootScope',
'ProcessErrors', 'CheckLicense', 'moment','$window',
'ConfigService', 'FeaturesService', 'pendoService', 'insightsEnablementService', 'i18n', 'config',
'ConfigService', 'pendoService', 'insightsEnablementService', 'i18n', 'config',
function(Wait, $state, $scope, $rootScope, ProcessErrors, CheckLicense, moment,
$window, ConfigService, FeaturesService, pendoService, insightsEnablementService, i18n, config) {
$window, ConfigService, pendoService, insightsEnablementService, i18n, config) {
const calcDaysRemaining = function(seconds) {
// calculate the number of days remaining on the license
@ -105,8 +105,6 @@ export default
ConfigService.delete();
ConfigService.getConfig(licenseInfo)
.then(function(config) {
delete($rootScope.features);
FeaturesService.get();
if ($rootScope.licenseMissing === true) {
if ($scope.newLicense.pendo) {

View File

@ -42,10 +42,10 @@
export default ['$log', '$cookies', '$compile', '$rootScope',
'$location', 'Authorization', 'Alert', 'Wait', 'Timer',
'Empty', '$scope', 'pendoService', 'ConfigService',
'CheckLicense', 'FeaturesService', 'SocketService',
'CheckLicense', 'SocketService',
function ($log, $cookies, $compile, $rootScope, $location,
Authorization, Alert, Wait, Timer, Empty,
scope, pendoService, ConfigService, CheckLicense, FeaturesService,
scope, pendoService, ConfigService, CheckLicense,
SocketService) {
var lastPath, lastUser, sessionExpired, loginAgain, preAuthUrl;
@ -97,7 +97,6 @@ export default ['$log', '$cookies', '$compile', '$rootScope',
ConfigService.getConfig().then(function(){
CheckLicense.test();
pendoService.issuePendoIdentity();
FeaturesService.get();
Wait("stop");
if(!Empty(preAuthUrl)){
$location.path(preAuthUrl);

View File

@ -48,9 +48,6 @@ let lists = [{
activityStreamTarget: 'organization'
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
OrgUsersDataset: ['OrgUserList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.basePath) || list.basePath;
@ -98,9 +95,6 @@ let lists = [{
label: N_("TEAMS")
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
OrgTeamList: ['TeamList', 'GetBasePath', '$stateParams', 'i18n', function(TeamList, GetBasePath, $stateParams, i18n) {
let list = _.cloneDeep(TeamList);
delete list.actions.add;
@ -144,9 +138,6 @@ let lists = [{
label: N_("INVENTORIES")
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
OrgInventoryList: ['InventoryList', 'GetBasePath', '$stateParams', 'i18n', function(InventoryList, GetBasePath, $stateParams, i18n) {
let list = _.cloneDeep(InventoryList);
delete list.actions.add;
@ -196,9 +187,6 @@ let lists = [{
label: N_("PROJECTS")
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
OrgProjectList: ['ProjectList', 'GetBasePath', '$stateParams', 'i18n', function(ProjectList, GetBasePath, $stateParams, i18n) {
let list = _.cloneDeep(ProjectList);
delete list.actions;
@ -258,9 +246,6 @@ let lists = [{
label: N_("ADMINS")
},
resolve: {
features: ['FeaturesService', function(FeaturesService) {
return FeaturesService.get();
}],
OrgAdminsDataset: ['OrgAdminList', 'QuerySet', '$stateParams', 'GetBasePath',
function(list, qs, $stateParams, GetBasePath) {
let path = GetBasePath(list.basePath) || list.basePath;

View File

@ -32,7 +32,6 @@ export default [function() {
mode: 'all', // One of: edit, select, all
ngClick: 'addOrganization()',
awToolTip: 'Create a new organization',
awFeature: 'multiple_organizations',
actionClass: 'at-Button--add',
actionId: 'button-add'
}

View File

@ -36,11 +36,17 @@ angular.module('ApiLoader', ['Utilities'])
data.base = base;
$rootScope.defaultUrls = data;
Store('api', data);
if($rootScope.basePathsLoaded){
$rootScope.basePathsLoaded.resolve();
}
})
.catch(({data, status}) => {
$rootScope.defaultUrls = {
status: 'error'
};
if($rootScope.basePathsLoaded){
$rootScope.basePathsLoaded.reject();
}
ProcessErrors(null, data, status, null, {
hdr: 'Error',
msg: 'Failed to read ' + base + '. GET status: ' + status

View File

@ -1,16 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$rootScope', function ($rootScope) {
this.isFeatureEnabled = function(feature){
if(_.isEmpty($rootScope.features)){
return false;
} else{
return $rootScope.features[feature] || false;
}
};
}];

View File

@ -1,41 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc overview
* @name features
* @scope
* @description enables/disables features based on license
*
* @ngdoc directive
* @name features.directive:awFeature
* @description The aw-feature directive works by taking in a string
* that maps to a license feature, and removes that feature from the
* DOM if it is a feature not supported by the user's license.
* For example, adding `aw-feature="system-tracking"` will enable or disable
* the system tracking button based on the license configuration on the
* /config endpoint.
*
*
*/
import featureController from './features.controller';
export default [ '$rootScope', function($rootScope) {
return {
restrict: 'A',
controller: featureController,
link: function (scope, element, attrs, controller){
if(attrs.awFeature.length > 0){
$rootScope.featuresConfigured.promise.then(function() {
if(!controller.isFeatureEnabled(attrs.awFeature)){
element.remove();
}
});
}
}
};
}];

View File

@ -1,35 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['$rootScope', 'ConfigService',
function ($rootScope, ConfigService) {
return {
get: function(){
if (_.isEmpty($rootScope.features)) {
var config = ConfigService.get();
if(config){
$rootScope.features = config.license_info.features;
if($rootScope.featuresConfigured){
$rootScope.featuresConfigured.resolve($rootScope.features);
}
return $rootScope.features;
}
}
else{
return $rootScope.features;
}
},
featureEnabled: function(feature) {
if($rootScope.features && $rootScope.features[feature] && $rootScope.features[feature] === true) {
return true;
}
else {
return false;
}
}
};
}];

View File

@ -1,13 +0,0 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import awFeatureDirective from './features.directive';
import FeaturesService from './features.service';
export default
angular.module('features', [])
.directive('awFeature', awFeatureDirective)
.service('FeaturesService', FeaturesService);

View File

@ -718,7 +718,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "'";
html += (field.ngShow) ? this.attr(field, 'ngShow') : "";
html += (field.ngHide) ? this.attr(field, 'ngHide') : "";
html += (field.awFeature) ? "aw-feature=\"" + field.awFeature + "\" " : "";
html += ">\n";
var definedInFileMessage = i18n._('This setting has been set manually in a settings file and is now disabled.');
@ -1511,9 +1510,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (button.ngClick) {
html += this.attr(button, 'ngClick');
}
if (button.awFeature) {
html += this.attr(button, 'awFeature');
}
if (button.ngDisabled) {
ngDisabled = (button.ngDisabled===true) ? this.form.name+"_form.$invalid" : button.ngDisabled;
if (itm !== 'reset') {
@ -1561,9 +1557,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if(button.ngShow){
html += this.attr(button, 'ngShow');
}
if (button.awFeature) {
html += this.attr(button, 'awFeature');
}
if(button.awToolTip) {
html += " aw-tool-tip='" + button.awToolTip + "' data-placement='" + button.dataPlacement + "' data-tip-watch='" + button.dataTipWatch + "'";
}
@ -1728,9 +1721,6 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (button.ngClick) {
html += this.attr(button, 'ngClick');
}
if (button.awFeature) {
html += this.attr(button, 'awFeature');
}
if (button.ngDisabled) {
ngDisabled = (button.ngDisabled===true) ? `${this.form.name}_form.$invalid || ${this.form.name}_form.$pending`: button.ngDisabled;
if (btn !== 'reset') {

View File

@ -718,7 +718,6 @@ angular.module('GeneratorHelpers', [systemStatus.name])
html += (options.ngClick) ? "ng-click=\"$eval(" + options.ngClick + ")\" " : "";
html += (options.ngShow) ? "ng-show=\"" + options.ngShow + "\" " : "";
html += (options.ngHide) ? "ng-hide=\"" + options.ngHide + "\" " : "";
html += (options.awFeature) ? "aw-feature=\"" + options.awFeature + "\" " : "";
html += '>';
html += '<span translate>';
html += (options.buttonContent) ? options.buttonContent : "";

View File

@ -21,8 +21,7 @@
ng-disabled="{{options.ngDisabled}}"
ng-show="{{options.ngShow}}"
ng-click="$eval(options.ngClick)"
toolbar="true"
aw-feature="{{options.awFeature}}">
toolbar="true">
<span ng-bind-html="options.buttonContent" translate></span>
</button>
</span>
@ -49,8 +48,7 @@
ng-disabled="{{options.ngDisabled}}"
ng-click="$eval(options.ngClick)"
ng-show="{{options.ngShow}}"
toolbar="true"
aw-feature="{{options.awFeature}}">
toolbar="true">
<span ng-if="options.buttonContent" ng-bind-html="options.buttonContent" translate></span>
</button>
</span>

View File

@ -28,7 +28,6 @@ import moment from './moment/main';
import config from './config/main';
import PromptDialog from './prompt-dialog';
import directives from './directives';
import features from './features/main';
import orgAdminLookup from './org-admin-lookup/main';
import limitPanels from './limit-panels/main';
import multiSelectPreview from './multi-select-preview/main';
@ -60,7 +59,6 @@ angular.module('shared', [
PromptDialog.name,
directives.name,
filters.name,
features.name,
orgAdminLookup.name,
limitPanels.name,
multiSelectPreview.name,

View File

@ -4,10 +4,8 @@
* All Rights Reserved
*************************************************/
// import awFeatureDirective from './features.directive';
import socketService from './socket.service';
export default
angular.module('socket', [])
// .directive('awFeature', awFeatureDirective)
.service('SocketService', socketService);

View File

@ -480,7 +480,6 @@ function(NotificationsList, i18n) {
relatedButtons: {
view_survey: {
ngClick: 'editSurvey()',
awFeature: 'surveys',
ngShow: '($state.is(\'templates.addJobTemplate\') || $state.is(\'templates.editJobTemplate\')) && survey_exists && !(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
label: i18n._('View Survey'),
class: 'Form-primaryButton'
@ -488,7 +487,6 @@ function(NotificationsList, i18n) {
add_survey: {
ngClick: 'addSurvey()',
ngShow: '($state.is(\'templates.addJobTemplate\') || $state.is(\'templates.editJobTemplate\')) && !survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
awFeature: 'surveys',
awToolTip: '{{surveyTooltip}}',
dataPlacement: 'top',
label: i18n._('Add Survey'),
@ -496,7 +494,6 @@ function(NotificationsList, i18n) {
},
edit_survey: {
ngClick: 'editSurvey()',
awFeature: 'surveys',
ngShow: '($state.is(\'templates.addJobTemplate\') || $state.is(\'templates.editJobTemplate\')) && survey_exists && (job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
label: i18n._('Edit Survey'),
class: 'Form-primaryButton',

View File

@ -226,7 +226,6 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
relatedButtons: {
view_survey: {
ngClick: 'editSurvey()',
awFeature: 'surveys',
ngShow: '($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\')) && survey_exists && !(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)',
label: i18n._('View Survey'),
class: 'Form-primaryButton'
@ -234,7 +233,6 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
add_survey: {
ngClick: 'addSurvey()',
ngShow: '!survey_exists && ($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\'))',
awFeature: 'surveys',
awToolTip: '{{surveyTooltip}}',
dataPlacement: 'top',
label: i18n._('Add Survey'),
@ -242,7 +240,6 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) {
},
edit_survey: {
ngClick: 'editSurvey()',
awFeature: 'surveys',
ngShow: 'survey_exists && ($state.is(\'templates.addWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate\') || $state.is(\'templates.editWorkflowJobTemplate.workflowMaker\'))',
label: i18n._('Edit Survey'),
class: 'Form-primaryButton',

View File

@ -35,7 +35,6 @@ register(
'custom HTML or other markup languages are not supported.'),
category=_('UI'),
category_slug='ui',
feature_required='rebranding',
)
register(
@ -50,7 +49,6 @@ register(
placeholder='data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',
category=_('UI'),
category_slug='ui',
feature_required='rebranding',
)
register(

View File

@ -1,23 +0,0 @@
'use strict';
describe('Directive: Features enabled/disabled', () => {
let $compile,
$scope,
q;
beforeEach(angular.mock.module('features'));
beforeEach(angular.mock.inject((_$compile_, _$rootScope_, $q) => {
$compile = _$compile_;
$scope = _$rootScope_;
q = $q;
$scope.featuresConfigured = q.defer();
}));
it('Removes the element if feature is disabled', () => {
let element = $compile("<div aw-feature='system-tracking'></div")($scope);
$scope.$digest();
expect(element.html()).toBe('');
});
});

View File

@ -62,7 +62,6 @@ register(
category=None,
depends_on=None,
placeholder=rest_framework.fields.empty,
feature_required=rest_framework.fields.empty,
encrypted=False,
defined_in_file=False,
)
@ -78,7 +77,6 @@ Here is the details of each argument:
| `category` | transformable string, like `_('foobar')` | The human-readable form of `category_slug`, mainly for display. |
| `depends_on` | `list` of `str`s | A list of setting names this setting depends on. A setting this setting depends on is another tower configuration setting whose changes may affect the value of this setting. |
| `placeholder` | transformable string, like `_('foobar')` | A human-readable string displaying a typical value for the setting, mainly used by UI |
| `feature_required` | `str` | Indicator of which feature this setting belongs, a user whose license does not allow a feature cannot access its related settings. |
| `encrypted` | `boolean` | Flag determining whether the setting value should be encrypted |
| `defined_in_file` | `boolean` | Flag determining whether a value has been manually set in settings file. |

View File

@ -6,6 +6,5 @@ python_files = *.py
addopts = --reuse-db --nomigrations --tb=native
markers =
ac: access control test
license_feature: ensure license features are accessible or not depending on license
survey: tests related to survey feature
inventory_import: tests of code used by inventory import command