Basic License feature gating changes

This commit is contained in:
beeankha
2019-04-01 17:24:55 -04:00
committed by mabashian
parent 58966d7368
commit de34a64115
61 changed files with 125 additions and 1015 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.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.versioning import reverse, get_request_version
from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField, from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField,
VerbatimField, DeprecatedCredentialField) VerbatimField, DeprecatedCredentialField)
@@ -918,7 +917,7 @@ class UserSerializer(BaseSerializer):
def _update_password(self, obj, new_password): def _update_password(self, obj, new_password):
# For now we're not raising an error, just not saving password for # For now we're not raising an error, just not saving password for
# users managed by LDAP who already have an unusable password set. # 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: try:
if obj.pk and obj.profile.ldap_dn and not obj.has_usable_password(): if obj.pk and obj.profile.ldap_dn and not obj.has_usable_password():
new_password = None new_password = None
@@ -979,7 +978,7 @@ class UserSerializer(BaseSerializer):
return res return res
def _validate_ldap_managed_field(self, value, field_name): 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 return value
try: try:
is_ldap_user = bool(self.instance and self.instance.profile.ldap_dn) 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: if word not in self.ALLOWED_SCOPES:
return False return False
return True return True
def validate_scope(self, value): def validate_scope(self, value):
if not self._is_valid_scope(value): if not self._is_valid_scope(value):
raise serializers.ValidationError(_( raise serializers.ValidationError(_(
@@ -3170,12 +3169,6 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
def validate_extra_vars(self, value): def validate_extra_vars(self, value):
return vars_validate_or_raise(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): def get_summary_fields(self, obj):
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(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 SubListDestroyAPIView, get_view_name
) )
from awx.api.versioning import reverse, get_request_version 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 import models
from awx.main.utils import ( from awx.main.utils import (
camelcase_to_underscore, camelcase_to_underscore,
@@ -100,10 +100,9 @@ from awx.api.metadata import RoleMetadata, JobTypeMetadata
from awx.main.constants import ACTIVE_STATES from awx.main.constants import ACTIVE_STATES
from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.views.mixin import ( from awx.api.views.mixin import (
ActivityStreamEnforcementMixin, ControlledByScmMixin, ControlledByScmMixin, InstanceGroupMembershipMixin,
InstanceGroupMembershipMixin, OrganizationCountsMixin, OrganizationCountsMixin, RelatedJobsPreventDeleteMixin,
RelatedJobsPreventDeleteMixin, SystemTrackingEnforcementMixin, UnifiedJobDeletionMixin,
UnifiedJobDeletionMixin, WorkflowsEnforcementMixin,
) )
from awx.api.views.organization import ( # noqa from awx.api.views.organization import ( # noqa
OrganizationList, OrganizationList,
@@ -530,12 +529,6 @@ class AuthView(APIView):
# Return auth backends in consistent order: Google, GitHub, SAML. # Return auth backends in consistent order: Google, GitHub, SAML.
auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0]) auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0])
for name, backend in auth_backends: 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,)) login_url = reverse('social:begin', args=(name,))
complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,))) complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,)))
backend_data = { 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]) 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 model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -745,7 +738,7 @@ class ProjectScmInventorySources(SubListAPIView):
parent_key = 'source_project' parent_key = 'source_project'
class ProjectActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class ProjectActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -989,7 +982,7 @@ class ApplicationOAuth2TokenList(SubListCreateAPIView):
swagger_topic = 'Authentication' swagger_topic = 'Authentication'
class OAuth2ApplicationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class OAuth2ApplicationActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -1071,7 +1064,7 @@ class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView):
swagger_topic = 'Authentication' swagger_topic = 'Authentication'
class OAuth2TokenActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class OAuth2TokenActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -1185,7 +1178,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
return my_qs & user_qs return my_qs & user_qs
class UserActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class UserActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -1272,7 +1265,7 @@ class CredentialTypeCredentialList(SubListCreateAPIView):
serializer_class = serializers.CredentialSerializer serializer_class = serializers.CredentialSerializer
class CredentialTypeActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class CredentialTypeActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -1386,7 +1379,7 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView):
filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend] filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend]
class CredentialActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class CredentialActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -1618,7 +1611,7 @@ class HostSmartInventoriesList(SubListAPIView):
relationship = 'smart_inventories' relationship = 'smart_inventories'
class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class HostActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -1633,7 +1626,7 @@ class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView):
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory)) return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
class HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIView): class HostFactVersionsList(ParentMixin, ListAPIView):
model = models.Fact model = models.Fact
serializer_class = serializers.FactVersionSerializer serializer_class = serializers.FactVersionSerializer
@@ -1660,7 +1653,7 @@ class HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIV
return Response(dict(results=self.serializer_class(queryset, many=True).data)) return Response(dict(results=self.serializer_class(queryset, many=True).data))
class HostFactCompareView(SystemTrackingEnforcementMixin, SubDetailAPIView): class HostFactCompareView(SubDetailAPIView):
model = models.Fact model = models.Fact
parent_model = models.Host parent_model = models.Host
@@ -1876,7 +1869,7 @@ class GroupInventorySourcesList(SubListAPIView):
relationship = 'inventory_sources' relationship = 'inventory_sources'
class GroupActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class GroupActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -2119,7 +2112,7 @@ class InventorySourceSchedulesList(SubListCreateAPIView):
parent_key = 'unified_job_template' parent_key = 'unified_job_template'
class InventorySourceActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class InventorySourceActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -2530,21 +2523,11 @@ class JobTemplateSurveySpec(GenericAPIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()
if not feature_enabled('surveys'):
raise LicenseForbids(_('Your license does not allow '
'adding surveys.'))
return Response(obj.display_survey_spec()) return Response(obj.display_survey_spec())
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
obj = self.get_object() 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): if not request.user.can_access(self.model, 'change', obj, None):
raise PermissionDenied() raise PermissionDenied()
response = self._validate_spec_data(request.data, obj.survey_spec) response = self._validate_spec_data(request.data, obj.survey_spec)
@@ -2672,12 +2655,12 @@ class JobTemplateSurveySpec(GenericAPIView):
return Response() return Response()
class WorkflowJobTemplateSurveySpec(WorkflowsEnforcementMixin, JobTemplateSurveySpec): class WorkflowJobTemplateSurveySpec(JobTemplateSurveySpec):
model = models.WorkflowJobTemplate model = models.WorkflowJobTemplate
class JobTemplateActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class JobTemplateActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -2995,14 +2978,14 @@ class JobTemplateCopy(CopyAPIView):
copy_return_serializer_class = serializers.JobTemplateSerializer copy_return_serializer_class = serializers.JobTemplateSerializer
class WorkflowJobNodeList(WorkflowsEnforcementMixin, ListAPIView): class WorkflowJobNodeList(ListAPIView):
model = models.WorkflowJobNode model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeListSerializer serializer_class = serializers.WorkflowJobNodeListSerializer
search_fields = ('unified_job_template__name', 'unified_job_template__description',) search_fields = ('unified_job_template__name', 'unified_job_template__description',)
class WorkflowJobNodeDetail(WorkflowsEnforcementMixin, RetrieveAPIView): class WorkflowJobNodeDetail(RetrieveAPIView):
model = models.WorkflowJobNode model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeDetailSerializer serializer_class = serializers.WorkflowJobNodeDetailSerializer
@@ -3016,14 +2999,14 @@ class WorkflowJobNodeCredentialsList(SubListAPIView):
relationship = 'credentials' relationship = 'credentials'
class WorkflowJobTemplateNodeList(WorkflowsEnforcementMixin, ListCreateAPIView): class WorkflowJobTemplateNodeList(ListCreateAPIView):
model = models.WorkflowJobTemplateNode model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeSerializer serializer_class = serializers.WorkflowJobTemplateNodeSerializer
search_fields = ('unified_job_template__name', 'unified_job_template__description',) search_fields = ('unified_job_template__name', 'unified_job_template__description',)
class WorkflowJobTemplateNodeDetail(WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView): class WorkflowJobTemplateNodeDetail(RetrieveUpdateDestroyAPIView):
model = models.WorkflowJobTemplateNode model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeDetailSerializer serializer_class = serializers.WorkflowJobTemplateNodeDetailSerializer
@@ -3034,7 +3017,7 @@ class WorkflowJobTemplateNodeCredentialsList(LaunchConfigCredentialsBase):
parent_model = models.WorkflowJobTemplateNode parent_model = models.WorkflowJobTemplateNode
class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
model = models.WorkflowJobTemplateNode model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeSerializer serializer_class = serializers.WorkflowJobTemplateNodeSerializer
@@ -3096,7 +3079,7 @@ class WorkflowJobTemplateNodeAlwaysNodesList(WorkflowJobTemplateNodeChildrenBase
relationship = 'always_nodes' relationship = 'always_nodes'
class WorkflowJobNodeChildrenBaseList(WorkflowsEnforcementMixin, SubListAPIView): class WorkflowJobNodeChildrenBaseList(SubListAPIView):
model = models.WorkflowJobNode model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeListSerializer serializer_class = serializers.WorkflowJobNodeListSerializer
@@ -3126,21 +3109,21 @@ class WorkflowJobNodeAlwaysNodesList(WorkflowJobNodeChildrenBaseList):
relationship = 'always_nodes' relationship = 'always_nodes'
class WorkflowJobTemplateList(WorkflowsEnforcementMixin, ListCreateAPIView): class WorkflowJobTemplateList(ListCreateAPIView):
model = models.WorkflowJobTemplate model = models.WorkflowJobTemplate
serializer_class = serializers.WorkflowJobTemplateSerializer serializer_class = serializers.WorkflowJobTemplateSerializer
always_allow_superuser = False always_allow_superuser = False
class WorkflowJobTemplateDetail(RelatedJobsPreventDeleteMixin, WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView): class WorkflowJobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = models.WorkflowJobTemplate model = models.WorkflowJobTemplate
serializer_class = serializers.WorkflowJobTemplateSerializer serializer_class = serializers.WorkflowJobTemplateSerializer
always_allow_superuser = False always_allow_superuser = False
class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, CopyAPIView): class WorkflowJobTemplateCopy(CopyAPIView):
model = models.WorkflowJobTemplate model = models.WorkflowJobTemplate
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
@@ -3185,11 +3168,11 @@ class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, CopyAPIView):
obj.save() obj.save()
class WorkflowJobTemplateLabelList(WorkflowsEnforcementMixin, JobTemplateLabelList): class WorkflowJobTemplateLabelList(JobTemplateLabelList):
parent_model = models.WorkflowJobTemplate parent_model = models.WorkflowJobTemplate
class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView): class WorkflowJobTemplateLaunch(RetrieveAPIView):
model = models.WorkflowJobTemplate model = models.WorkflowJobTemplate
@@ -3238,7 +3221,7 @@ class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView):
return Response(data, status=status.HTTP_201_CREATED, headers=headers) return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView): class WorkflowJobRelaunch(GenericAPIView):
model = models.WorkflowJob model = models.WorkflowJob
obj_permission_type = 'start' obj_permission_type = 'start'
@@ -3270,7 +3253,7 @@ class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView):
return Response(data, status=status.HTTP_201_CREATED, headers=headers) return Response(data, status=status.HTTP_201_CREATED, headers=headers)
class WorkflowJobTemplateWorkflowNodesList(WorkflowsEnforcementMixin, SubListCreateAPIView): class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView):
model = models.WorkflowJobTemplateNode model = models.WorkflowJobTemplateNode
serializer_class = serializers.WorkflowJobTemplateNodeSerializer serializer_class = serializers.WorkflowJobTemplateNodeSerializer
@@ -3283,7 +3266,7 @@ class WorkflowJobTemplateWorkflowNodesList(WorkflowsEnforcementMixin, SubListCre
return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id') return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id')
class WorkflowJobTemplateJobsList(WorkflowsEnforcementMixin, SubListAPIView): class WorkflowJobTemplateJobsList(SubListAPIView):
model = models.WorkflowJob model = models.WorkflowJob
serializer_class = serializers.WorkflowJobListSerializer serializer_class = serializers.WorkflowJobListSerializer
@@ -3292,7 +3275,7 @@ class WorkflowJobTemplateJobsList(WorkflowsEnforcementMixin, SubListAPIView):
parent_key = 'workflow_job_template' parent_key = 'workflow_job_template'
class WorkflowJobTemplateSchedulesList(WorkflowsEnforcementMixin, SubListCreateAPIView): class WorkflowJobTemplateSchedulesList(SubListCreateAPIView):
view_name = _("Workflow Job Template Schedules") view_name = _("Workflow Job Template Schedules")
@@ -3303,7 +3286,7 @@ class WorkflowJobTemplateSchedulesList(WorkflowsEnforcementMixin, SubListCreateA
parent_key = 'unified_job_template' parent_key = 'unified_job_template'
class WorkflowJobTemplateNotificationTemplatesAnyList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView): class WorkflowJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer serializer_class = serializers.NotificationTemplateSerializer
@@ -3311,7 +3294,7 @@ class WorkflowJobTemplateNotificationTemplatesAnyList(WorkflowsEnforcementMixin,
relationship = 'notification_templates_any' relationship = 'notification_templates_any'
class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView): class WorkflowJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer serializer_class = serializers.NotificationTemplateSerializer
@@ -3319,7 +3302,7 @@ class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowsEnforcementMixi
relationship = 'notification_templates_error' relationship = 'notification_templates_error'
class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView): class WorkflowJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView):
model = models.NotificationTemplate model = models.NotificationTemplate
serializer_class = serializers.NotificationTemplateSerializer serializer_class = serializers.NotificationTemplateSerializer
@@ -3327,13 +3310,13 @@ class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowsEnforcementMi
relationship = 'notification_templates_success' relationship = 'notification_templates_success'
class WorkflowJobTemplateAccessList(WorkflowsEnforcementMixin, ResourceAccessList): class WorkflowJobTemplateAccessList(ResourceAccessList):
model = models.User # needs to be User for AccessLists's model = models.User # needs to be User for AccessLists's
parent_model = models.WorkflowJobTemplate parent_model = models.WorkflowJobTemplate
class WorkflowJobTemplateObjectRolesList(WorkflowsEnforcementMixin, SubListAPIView): class WorkflowJobTemplateObjectRolesList(SubListAPIView):
model = models.Role model = models.Role
serializer_class = serializers.RoleSerializer 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) return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView): class WorkflowJobTemplateActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -3362,19 +3345,19 @@ class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityS
Q(workflow_job_template_node__workflow_job_template=parent)).distinct() Q(workflow_job_template_node__workflow_job_template=parent)).distinct()
class WorkflowJobList(WorkflowsEnforcementMixin, ListCreateAPIView): class WorkflowJobList(ListCreateAPIView):
model = models.WorkflowJob model = models.WorkflowJob
serializer_class = serializers.WorkflowJobListSerializer serializer_class = serializers.WorkflowJobListSerializer
class WorkflowJobDetail(WorkflowsEnforcementMixin, UnifiedJobDeletionMixin, RetrieveDestroyAPIView): class WorkflowJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
model = models.WorkflowJob model = models.WorkflowJob
serializer_class = serializers.WorkflowJobSerializer serializer_class = serializers.WorkflowJobSerializer
class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView): class WorkflowJobWorkflowNodesList(SubListAPIView):
model = models.WorkflowJobNode model = models.WorkflowJobNode
serializer_class = serializers.WorkflowJobNodeListSerializer serializer_class = serializers.WorkflowJobNodeListSerializer
@@ -3388,7 +3371,7 @@ class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView):
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id') return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
class WorkflowJobCancel(WorkflowsEnforcementMixin, RetrieveAPIView): class WorkflowJobCancel(RetrieveAPIView):
model = models.WorkflowJob model = models.WorkflowJob
obj_permission_type = 'cancel' obj_permission_type = 'cancel'
@@ -3404,7 +3387,7 @@ class WorkflowJobCancel(WorkflowsEnforcementMixin, RetrieveAPIView):
return self.http_method_not_allowed(request, *args, **kwargs) return self.http_method_not_allowed(request, *args, **kwargs)
class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView): class WorkflowJobNotificationsList(SubListAPIView):
model = models.Notification model = models.Notification
serializer_class = serializers.NotificationSerializer serializer_class = serializers.NotificationSerializer
@@ -3413,7 +3396,7 @@ class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView):
search_fields = ('subject', 'notification_type', 'body',) search_fields = ('subject', 'notification_type', 'body',)
class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView): class WorkflowJobActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -3589,11 +3572,11 @@ class JobLabelList(SubListAPIView):
parent_key = 'job' parent_key = 'job'
class WorkflowJobLabelList(WorkflowsEnforcementMixin, JobLabelList): class WorkflowJobLabelList(JobLabelList):
parent_model = models.WorkflowJob parent_model = models.WorkflowJob
class JobActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class JobActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -4106,7 +4089,7 @@ class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList):
parent_model = models.AdHocCommand parent_model = models.AdHocCommand
class AdHocCommandActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class AdHocCommandActivityStreamList(SubListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
@@ -4402,14 +4385,14 @@ class LabelDetail(RetrieveUpdateAPIView):
serializer_class = serializers.LabelSerializer serializer_class = serializers.LabelSerializer
class ActivityStreamList(ActivityStreamEnforcementMixin, SimpleListAPIView): class ActivityStreamList(SimpleListAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer
search_fields = ('changes',) search_fields = ('changes',)
class ActivityStreamDetail(ActivityStreamEnforcementMixin, RetrieveAPIView): class ActivityStreamDetail(RetrieveAPIView):
model = models.ActivityStream model = models.ActivityStream
serializer_class = serializers.ActivityStreamSerializer serializer_class = serializers.ActivityStreamSerializer

View File

@@ -48,7 +48,6 @@ from awx.api.serializers import (
JobTemplateSerializer, JobTemplateSerializer,
) )
from awx.api.views.mixin import ( from awx.api.views.mixin import (
ActivityStreamEnforcementMixin,
RelatedJobsPreventDeleteMixin, RelatedJobsPreventDeleteMixin,
ControlledByScmMixin, ControlledByScmMixin,
) )
@@ -149,7 +148,7 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, Retri
return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST) return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST)
class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class InventoryActivityStreamList(SubListAPIView):
model = ActivityStream model = ActivityStream
serializer_class = ActivityStreamSerializer 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.projects import Project
from awx.main.models.inventory import Inventory from awx.main.models.inventory import Inventory
from awx.main.models.jobs import JobTemplate from awx.main.models.jobs import JobTemplate
from awx.conf.license import (
feature_enabled,
LicenseForbids,
)
from awx.api.exceptions import ActiveJobConflict from awx.api.exceptions import ActiveJobConflict
logger = logging.getLogger('awx.api.views.mixin') 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): class UnifiedJobDeletionMixin(object):
''' '''
Special handling when deleting a running unified job object. Special handling when deleting a running unified job object.

View File

@@ -7,13 +7,8 @@ import logging
# Django # Django
from django.db.models import Count from django.db.models import Count
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
# AWX # AWX
from awx.conf.license import (
feature_enabled,
LicenseForbids,
)
from awx.main.models import ( from awx.main.models import (
ActivityStream, ActivityStream,
Inventory, Inventory,
@@ -50,7 +45,6 @@ from awx.api.serializers import (
InstanceGroupSerializer, InstanceGroupSerializer,
) )
from awx.api.views.mixin import ( from awx.api.views.mixin import (
ActivityStreamEnforcementMixin,
RelatedJobsPreventDeleteMixin, RelatedJobsPreventDeleteMixin,
OrganizationCountsMixin, OrganizationCountsMixin,
) )
@@ -69,24 +63,6 @@ class OrganizationList(OrganizationCountsMixin, ListCreateAPIView):
qs = qs.prefetch_related('created_by', 'modified_by') qs = qs.prefetch_related('created_by', 'modified_by')
return qs 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): class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
@@ -177,7 +153,7 @@ class OrganizationTeamsList(SubListCreateAttachDetachAPIView):
parent_key = 'organization' parent_key = 'organization'
class OrganizationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): class OrganizationActivityStreamList(SubListAPIView):
model = ActivityStream model = ActivityStream
serializer_class = ActivityStreamSerializer serializer_class = ActivityStreamSerializer
@@ -244,4 +220,3 @@ class OrganizationObjectRolesList(SubListAPIView):
po = self.get_parent_object() po = self.get_parent_object()
content_type = ContentType.objects.get_for_model(self.parent_model) content_type = ContentType.objects.get_for_model(self.parent_model)
return Role.objects.filter(content_type=content_type, object_id=po.pk) 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, to_python_boolean,
) )
from awx.api.versioning import reverse, get_request_version, drf_reverse 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.constants import PRIVILEGE_ESCALATION_METHODS
from awx.main.models import ( from awx.main.models import (
Project, Project,
@@ -57,9 +57,8 @@ class ApiRootView(APIView):
data['current_version'] = v2 data['current_version'] = v2
data['available_versions'] = dict(v1 = v1, v2 = v2) data['available_versions'] = dict(v1 = v1, v2 = v2)
data['oauth2'] = drf_reverse('api:oauth_authorization_root_view') data['oauth2'] = drf_reverse('api:oauth_authorization_root_view')
if feature_enabled('rebranding'): data['custom_logo'] = settings.CUSTOM_LOGO
data['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
return Response(data) return Response(data)
@@ -213,7 +212,7 @@ class ApiV1ConfigView(APIView):
# If LDAP is enabled, user_ldap_fields will return a list of field # 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 # names that are managed by LDAP and should be read-only for users with
# a non-empty ldap_dn attribute. # 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 = ['username', 'password']
user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys()) 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()) 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 # the other settings change, the cached value for this setting will be
# cleared to require it to be recomputed. # cleared to require it to be recomputed.
depends_on=['ANSIBLE_COW_SELECTION'], 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$ # Optional; field is stored encrypted in the database and only $encrypted$
# is returned via the API. # is returned via the API.
encrypted=True, encrypted=True,

View File

@@ -1,64 +1,19 @@
# Copyright (c) 2016 Ansible, Inc. # Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved. # 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 # Tower
from awx.main.utils.common import get_licenser from awx.main.utils.common import get_licenser
from awx.main.utils import memoize, memoize_delete
__all__ = ['LicenseForbids', 'get_license', 'get_licensed_features', __all__ = ['get_license']
'feature_enabled', 'feature_exists']
class LicenseForbids(APIException):
status_code = 402
default_detail = _('Your Tower license does not allow that.')
def _get_validated_license_data(): def _get_validated_license_data():
return get_licenser().validate() 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): def get_license(show_key=False):
"""Return a dictionary representing the active license on this Tower instance.""" """Return a dictionary representing the active license on this Tower instance."""
license_data = _get_validated_license_data() license_data = _get_validated_license_data()
if not show_key: if not show_key:
license_data.pop('license_key', None) license_data.pop('license_key', None)
return license_data 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): def get_dependent_settings(self, setting):
return self._dependent_settings.get(setting, set()) return self._dependent_settings.get(setting, set())
def get_registered_categories(self, features_enabled=None): def get_registered_categories(self):
categories = { categories = {
'all': _('All'), 'all': _('All'),
'changed': _('Changed'), 'changed': _('Changed'),
@@ -77,10 +77,6 @@ class SettingsRegistry(object):
category_slug = kwargs.get('category_slug', None) category_slug = kwargs.get('category_slug', None)
if category_slug is None or category_slug in categories: if category_slug is None or category_slug in categories:
continue 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': if category_slug == 'user':
categories['user'] = _('User') categories['user'] = _('User')
categories['user-defaults'] = _('User-Defaults') categories['user-defaults'] = _('User-Defaults')
@@ -88,7 +84,7 @@ class SettingsRegistry(object):
categories[category_slug] = kwargs.get('category', None) or category_slug categories[category_slug] = kwargs.get('category', None) or category_slug
return categories 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 = [] setting_names = []
if category_slug == 'user-defaults': if category_slug == 'user-defaults':
category_slug = 'user' category_slug = 'user'
@@ -104,10 +100,6 @@ class SettingsRegistry(object):
# Note: Doesn't catch fields that set read_only via __init__; # Note: Doesn't catch fields that set read_only via __init__;
# read-only field kwargs should always include read_only=True. # read-only field kwargs should always include read_only=True.
continue 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) setting_names.append(setting)
return setting_names return setting_names
@@ -135,7 +127,6 @@ class SettingsRegistry(object):
category = field_kwargs.pop('category', None) category = field_kwargs.pop('category', None)
depends_on = frozenset(field_kwargs.pop('depends_on', None) or []) depends_on = frozenset(field_kwargs.pop('depends_on', None) or [])
placeholder = field_kwargs.pop('placeholder', empty) placeholder = field_kwargs.pop('placeholder', empty)
feature_required = field_kwargs.pop('feature_required', empty)
encrypted = bool(field_kwargs.pop('encrypted', False)) encrypted = bool(field_kwargs.pop('encrypted', False))
defined_in_file = bool(field_kwargs.pop('defined_in_file', False)) defined_in_file = bool(field_kwargs.pop('defined_in_file', False))
if getattr(field_kwargs.get('child', None), 'source', None) is not None: 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 field_instance.depends_on = depends_on
if placeholder is not empty: if placeholder is not empty:
field_instance.placeholder = placeholder field_instance.placeholder = placeholder
if feature_required is not empty:
field_instance.feature_required = feature_required
field_instance.defined_in_file = defined_in_file field_instance.defined_in_file = defined_in_file
if field_instance.defined_in_file: if field_instance.defined_in_file:
field_instance.help_text = ( 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): def test_get_dependent_settings(reg):
reg.register( reg.register(
'AWX_SOME_SETTING_ENABLED', '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): def test_is_setting_encrypted(reg):
reg.register( reg.register(
'AWX_SOME_SETTING_ENABLED', 'AWX_SOME_SETTING_ENABLED',
@@ -237,7 +184,6 @@ def test_simple_field(reg):
category=_('System'), category=_('System'),
category_slug='system', category_slug='system',
placeholder='Example Value', placeholder='Example Value',
feature_required='superpowers'
) )
field = reg.get_setting_field('AWX_SOME_SETTING') field = reg.get_setting_field('AWX_SOME_SETTING')
@@ -246,7 +192,6 @@ def test_simple_field(reg):
assert field.category_slug == 'system' assert field.category_slug == 'system'
assert field.default is empty assert field.default is empty
assert field.placeholder == 'Example Value' assert field.placeholder == 'Example Value'
assert field.feature_required == 'superpowers'
def test_field_with_custom_attribute(reg): 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 import camelcase_to_underscore
from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException
from awx.main.tasks import handle_setting_changes 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.models import Setting
from awx.conf.serializers import SettingCategorySerializer, SettingSingletonSerializer from awx.conf.serializers import SettingCategorySerializer, SettingSingletonSerializer
from awx.conf import settings_registry from awx.conf import settings_registry
@@ -53,7 +52,7 @@ class SettingCategoryList(ListAPIView):
def get_queryset(self): def get_queryset(self):
setting_categories = [] 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: if self.request.user.is_superuser or self.request.user.is_system_auditor:
pass # categories = categories pass # categories = categories
elif 'user' in categories: elif 'user' in categories:
@@ -77,7 +76,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
def get_queryset(self): def get_queryset(self):
self.category_slug = self.kwargs.get('category_slug', 'all') 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)]: for slug_to_delete in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]:
all_category_slugs.remove(slug_to_delete) all_category_slugs.remove(slug_to_delete)
if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False): if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False):
@@ -90,7 +89,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
raise PermissionDenied() raise PermissionDenied()
registered_settings = settings_registry.get_registered_settings( 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)] slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
) )
if self.category_slug == 'user': if self.category_slug == 'user':
@@ -101,7 +100,7 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView):
def get_object(self): def get_object(self):
settings_qs = self.get_queryset() settings_qs = self.get_queryset()
registered_settings = settings_registry.get_registered_settings( 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)] slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]
) )
all_settings = {} all_settings = {}

View File

@@ -41,8 +41,6 @@ from awx.main.models import (
) )
from awx.main.models.mixins import ResourceMixin 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', __all__ = ['get_user_queryset', 'check_user_access', 'check_user_access_with_errors',
'user_accessible_objects', 'consumer_access',] 'user_accessible_objects', 'consumer_access',]
@@ -324,12 +322,6 @@ class BaseAccess(object):
elif not add_host_name and free_instances < 0: elif not add_host_name and free_instances < 0:
raise PermissionDenied(_("Host count exceeds available instances.")) 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): def check_org_host_limit(self, data, add_host_name=None):
validation_info = get_licenser().validate() validation_info = get_licenser().validate()
if validation_info.get('license_type', 'UNLICENSED') == 'open': if validation_info.get('license_type', 'UNLICENSED') == 'open':
@@ -383,9 +375,6 @@ class BaseAccess(object):
if obj.validation_errors: if obj.validation_errors:
user_capabilities[display_method] = False user_capabilities[display_method] = False
continue 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: elif display_method == 'copy' and isinstance(obj, WorkflowJobTemplate) and obj.organization_id is None:
user_capabilities[display_method] = self.user.is_superuser user_capabilities[display_method] = self.user.is_superuser
continue continue
@@ -776,7 +765,7 @@ class OrganizationAccess(NotificationAttachMixin, BaseAccess):
return self.user in obj.admin_role return self.user in obj.admin_role
def can_delete(self, obj): 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) is_change_possible = self.can_change(obj, None)
if not is_change_possible: if not is_change_possible:
return False return False
@@ -1492,11 +1481,6 @@ class JobTemplateAccess(NotificationAttachMixin, BaseAccess):
# Check the per-org limit # Check the per-org limit
self.check_org_host_limit({'inventory': obj.inventory}) 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 # Super users can start any job
if self.user.is_superuser: if self.user.is_superuser:
return True return True
@@ -2021,10 +2005,6 @@ class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess):
# Check the per-org limit # Check the per-org limit
self.check_org_host_limit({'inventory': obj.inventory}) 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 # Super users can start any job
if self.user.is_superuser: if self.user.is_superuser:
return True return True
@@ -2032,11 +2012,6 @@ class WorkflowJobTemplateAccess(NotificationAttachMixin, BaseAccess):
return self.user in obj.execute_role return self.user in obj.execute_role
def can_change(self, obj, data): 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: if self.user.is_superuser:
return True return True

View File

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

View File

@@ -13,7 +13,6 @@ from django.utils.timezone import now
# AWX # AWX
from awx.main.models.fact import Fact from awx.main.models.fact import Fact
from awx.conf.license import feature_enabled
OLDER_THAN = 'older_than' OLDER_THAN = 'older_than'
GRANULARITY = 'granularity' 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) # Delete all except LAST entry (or Delete all except the FIRST entry, it's an arbitrary decision)
# #
# pivot -= granularity # pivot -= granularity
# group by host # group by host
def cleanup(self, older_than_abs, granularity, module=None): def cleanup(self, older_than_abs, granularity, module=None):
fact_oldest = Fact.objects.all().order_by('timestamp').first() fact_oldest = Fact.objects.all().order_by('timestamp').first()
if not fact_oldest: if not fact_oldest:
@@ -114,7 +113,7 @@ class Command(BaseCommand):
def string_time_to_timestamp(self, time_string): def string_time_to_timestamp(self, time_string):
units = { units = {
'y': 'years', 'y': 'years',
'd': 'days', 'd': 'days',
'w': 'weeks', 'w': 'weeks',
'm': 'months' 'm': 'months'
} }
@@ -131,8 +130,6 @@ class Command(BaseCommand):
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
sys.stderr.write("This command has been deprecated and will be removed in a future release.\n") 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() cleanup_facts = CleanupFacts()
if not all([options[GRANULARITY], options[OLDER_THAN]]): if not all([options[GRANULARITY], options[OLDER_THAN]]):
raise CommandError('Both --granularity and --older_than are required.') 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', logger.warning('update computed fields took %d queries',
len(connection.queries) - queries_before2) len(connection.queries) - queries_before2)
# Check if the license is valid. # 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. # and inventory update will be marked as invalid.
# with transaction.atomic() will roll back the changes. # with transaction.atomic() will roll back the changes.
license_fail = True license_fail = True

View File

@@ -1,4 +1,3 @@
from unittest import mock
import pytest import pytest
from awx.api.versioning import reverse from awx.api.versioning import reverse
@@ -8,16 +7,12 @@ from awx.main.access import ActivityStreamAccess
from awx.conf.models import Setting from awx.conf.models import Setting
def mock_feature_enabled(feature):
return True
@pytest.fixture @pytest.fixture
def activity_stream_entry(organization, org_admin): def activity_stream_entry(organization, org_admin):
return ActivityStream.objects.filter(organization__pk=organization.pk, user=org_admin, operation='associate').first() 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 @pytest.mark.django_db
def test_get_activity_stream_list(monkeypatch, organization, get, user, settings): def test_get_activity_stream_list(monkeypatch, organization, get, user, settings):
settings.ACTIVITY_STREAM_ENABLED = True 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 assert response.status_code == 200
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_basic_fields(monkeypatch, organization, get, user, settings): def test_basic_fields(monkeypatch, organization, get, user, settings):
settings.ACTIVITY_STREAM_ENABLED = True 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' 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 @pytest.mark.django_db
def test_ctint_activity_stream(monkeypatch, get, user, settings): def test_ctint_activity_stream(monkeypatch, get, user, settings):
Setting.objects.create(key="FOO", value="bar") 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' assert response.data['summary_fields']['setting'][0]['name'] == 'FOO'
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_middleware_actor_added(monkeypatch, post, get, user, settings): def test_middleware_actor_added(monkeypatch, post, get, user, settings):
settings.ACTIVITY_STREAM_ENABLED = True 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' assert response.data['summary_fields']['actor']['username'] == 'admin-poster'
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_admin, settings): def test_rbac_stream_resource_roles(activity_stream_entry, organization, org_admin, settings):
settings.ACTIVITY_STREAM_ENABLED = True 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' 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 @pytest.mark.django_db
def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin, settings): def test_rbac_stream_user_roles(activity_stream_entry, organization, org_admin, settings):
settings.ACTIVITY_STREAM_ENABLED = True 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.django_db
@pytest.mark.activity_stream_access @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): def test_stream_access_cant_change(activity_stream_entry, organization, org_admin, settings):
settings.ACTIVITY_STREAM_ENABLED = True settings.ACTIVITY_STREAM_ENABLED = True
access = ActivityStreamAccess(org_admin) 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.django_db
@pytest.mark.activity_stream_access @pytest.mark.activity_stream_access
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
def test_stream_queryset_hides_shows_items( def test_stream_queryset_hides_shows_items(
activity_stream_entry, organization, user, org_admin, activity_stream_entry, organization, user, org_admin,
project, org_credential, inventory, label, deploy_jobtemplate, project, org_credential, inventory, label, deploy_jobtemplate,
@@ -160,7 +148,6 @@ def test_stream_queryset_hides_shows_items(
@pytest.mark.django_db @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): def test_stream_user_direct_role_updates(get, post, organization_factory):
objects = organization_factory('test_org', objects = organization_factory('test_org',
superusers=['admin'], superusers=['admin'],

View File

@@ -1,5 +1,4 @@
# Python # Python
from unittest import mock
import pytest import pytest
from datetime import timedelta from datetime import timedelta
import urllib.parse import urllib.parse
@@ -13,14 +12,6 @@ from awx.main.utils import timestamp_apiformat
from django.utils import timezone 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): def setup_common(hosts, fact_scans, get, user, epoch=timezone.now(), get_params={}, host_count=1):
hosts = hosts(host_count=host_count) hosts = hosts(host_count=host_count)
fact_scans(fact_scans=3, timestamp_epoch=epoch) 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) 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.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): def test_no_facts_db(hosts, get, user):
hosts = hosts(host_count=1) hosts = hosts(host_count=1)
url = reverse('api:host_fact_versions_list', kwargs={'pk': hosts[0].pk}) 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 assert response_expected == response.data
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() 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] assert 'module' in results[0]
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.license_feature
def test_basic_options_fields(hosts, fact_scans, options, user, monkeypatch_jsonbfield_get_db_prep_save): def test_basic_options_fields(hosts, fact_scans, options, user, monkeypatch_jsonbfield_get_db_prep_save):
hosts = hosts(host_count=1) hosts = hosts(host_count=1)
fact_scans(fact_scans=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'] 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 @pytest.mark.django_db
def test_related_fact_view(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_related_fact_view(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() 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) 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 @pytest.mark.django_db
def test_multiple_hosts(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_multiple_hosts(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() 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) 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 @pytest.mark.django_db
def test_param_to_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_param_to_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() 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) check_response_facts(facts_known, response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_param_module(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_param_module(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() 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) check_response_facts(facts_known, response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_param_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_param_from(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() 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) check_response_facts(facts_known, response)
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_param_to(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_param_to(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() epoch = timezone.now()
@@ -232,7 +185,6 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
return response return response
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac @pytest.mark.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_normal_user_403(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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'] 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.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_super_user_ok(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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 assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac @pytest.mark.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team): 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 assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac @pytest.mark.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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 pytest
import json import json
@@ -8,14 +7,6 @@ from awx.main.utils import timestamp_apiformat
from django.utils import timezone 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 # 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): def find_fact(facts, host_id, module_name, timestamp):
for f in facts: 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) 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 @pytest.mark.django_db
def test_no_fact_found(hosts, get, user): def test_no_fact_found(hosts, get, user):
hosts = hosts(host_count=1) hosts = hosts(host_count=1)
@@ -76,7 +39,6 @@ def test_no_fact_found(hosts, get, user):
assert expected_response == response.data assert expected_response == response.data
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save): def test_basic_fields(hosts, fact_scans, get, user, monkeypatch_jsonbfield_get_db_prep_save):
hosts = hosts(host_count=1) 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'] 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 @pytest.mark.django_db
def test_content(hosts, fact_scans, get, user, fact_ansible_json, monkeypatch_jsonbfield_get_db_prep_save): 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) (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'] assert module_name == response.data['module']
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_search_by_module_packages(hosts, fact_scans, get, user, fact_packages_json, monkeypatch_jsonbfield_get_db_prep_save): 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') _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 @pytest.mark.django_db
def test_search_by_module_services(hosts, fact_scans, get, user, fact_services_json, monkeypatch_jsonbfield_get_db_prep_save): 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') _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 @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): def test_search_by_timestamp_and_module(hosts, fact_scans, get, user, fact_packages_json, monkeypatch_jsonbfield_get_db_prep_save):
epoch = timezone.now() epoch = timezone.now()
@@ -160,7 +118,6 @@ def _test_user_access_control(hosts, fact_scans, get, user_obj, team_obj):
return response return response
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac @pytest.mark.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_normal_user_403(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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'] 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.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_super_user_ok(hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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 assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac @pytest.mark.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin_ok(organization, hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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 assert 200 == response.status_code
@mock.patch('awx.api.views.feature_enabled', new=mock_feature_enabled)
@pytest.mark.ac @pytest.mark.ac
@pytest.mark.django_db @pytest.mark.django_db
def test_user_admin_403(organization, organizations, hosts, fact_scans, get, user, team, monkeypatch_jsonbfield_get_db_prep_save): 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 backports.tempfile import TemporaryDirectory
from django.conf import settings from django.conf import settings
import pytest import pytest
from unittest import mock
# AWX # AWX
from awx.main.models import ProjectUpdate 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=alice).data['count'] == 2
assert get(reverse('api:organization_inventories_list', kwargs={'pk': organization.id}), user=bob).data['count'] == 1 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) get(reverse('api:organization_inventories_list', kwargs={'pk': organization.id}), user=rando, expect=403)
@pytest.mark.django_db @pytest.mark.django_db
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
def test_create_organization(post, admin, alice): def test_create_organization(post, admin, alice):
new_org = { new_org = {
'name': 'new org', 'name': 'new org',
@@ -146,7 +144,6 @@ def test_create_organization(post, admin, alice):
@pytest.mark.django_db @pytest.mark.django_db
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
def test_create_organization_xfail(post, alice): def test_create_organization_xfail(post, alice):
new_org = { new_org = {
'name': '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 @pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization(delete, organization, admin): def test_delete_organization(delete, organization, admin):
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=admin, expect=204) delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=admin, expect=204)
@pytest.mark.django_db @pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization2(delete, organization, alice): def test_delete_organization2(delete, organization, alice):
organization.admin_role.members.add(alice) organization.admin_role.members.add(alice)
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=alice, expect=204) delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=alice, expect=204)
@pytest.mark.django_db @pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization_xfail1(delete, organization, alice): def test_delete_organization_xfail1(delete, organization, alice):
organization.member_role.members.add(alice) organization.member_role.members.add(alice)
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=alice, expect=403) delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=alice, expect=403)
@pytest.mark.django_db @pytest.mark.django_db
@mock.patch('awx.main.access.BaseAccess.check_license', lambda *a, **kw: True)
def test_delete_organization_xfail2(delete, organization): def test_delete_organization_xfail2(delete, organization):
delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=None, expect=401) 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.data['error'] == u"Resource is being used by running jobs."
assert resp_sorted == expect_sorted 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) 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']['start']
assert not response.data['summary_fields']['user_capabilities']['schedule'] 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.api.versioning import reverse
from awx.main.models.jobs import JobTemplate, Job from awx.main.models.jobs import JobTemplate, Job
from awx.main.models.activity_stream import ActivityStream from awx.main.models.activity_stream import ActivityStream
from awx.conf.license import LicenseForbids
from awx.main.access import JobTemplateAccess from awx.main.access import JobTemplateAccess
from awx.main.utils.common import get_type_for_model 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 @pytest.fixture
def job_template_with_survey(job_template_factory): def job_template_with_survey(job_template_factory):
@@ -24,18 +17,6 @@ def job_template_with_survey(job_template_factory):
return objects.job_template 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.django_db
@pytest.mark.survey @pytest.mark.survey
@pytest.mark.parametrize("role_field,expected_status_code", [ @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) 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 # Test normal operations with survey license work
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.survey @pytest.mark.survey
def test_survey_spec_view_allowed(deploy_jobtemplate, get, admin_user): 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) admin_user, expect=200)
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.survey @pytest.mark.survey
def test_survey_spec_sucessful_creation(survey_spec_factory, job_template, post, admin_user): 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 assert updated_jt.survey_spec == survey_input_data
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize('with_default', [True, False]) @pytest.mark.parametrize('with_default', [True, False])
@pytest.mark.parametrize('value, status', [ @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) 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 @pytest.mark.django_db
def test_survey_spec_passwords_with_empty_default(job_template_factory, post, admin_user): def test_survey_spec_passwords_with_empty_default(job_template_factory, post, admin_user):
objects = job_template_factory('jt', organization='org1', project='prj', 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.django_db
@pytest.mark.parametrize('default, launch_value, expected_extra_vars, status', [ @pytest.mark.parametrize('default, launch_value, expected_extra_vars, status', [
['', '$encrypted$', {'secret_value': ''}, 201], ['', '$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() 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.django_db
@pytest.mark.parametrize('default, launch_value, expected_extra_vars, status', [ @pytest.mark.parametrize('default, launch_value, expected_extra_vars, status', [
['', '$encrypted$', {'secret_value': ''}, 201], ['', '$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() 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.django_db
@pytest.mark.parametrize('default, status', [ @pytest.mark.parametrize('default, status', [
('SUPERSECRET', 200), ('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) assert "expected to be string." in str(resp.data)
@mock.patch('awx.api.views.feature_enabled', lambda feature: True)
@pytest.mark.django_db @pytest.mark.django_db
def test_survey_spec_default_passwords_encrypted_on_update(job_template, post, put, admin_user): def test_survey_spec_default_passwords_encrypted_on_update(job_template, post, put, admin_user):
input_data = { 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 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.django_db
@pytest.mark.survey @pytest.mark.survey
def test_job_template_delete_access_with_survey(job_template_with_survey, admin_user): 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) 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.django_db
@pytest.mark.survey @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.""" """Functional delete test through the survey_spec view."""
delete(reverse('api:job_template_survey_spec', kwargs={'pk': job_template_with_survey.pk}), delete(reverse('api:job_template_survey_spec', kwargs={'pk': job_template_with_survey.pk}),
admin_user, expect=200) 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 == {} 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', @mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.create_unified_job',
lambda self, **kwargs: mock.MagicMock(spec=Job, id=968)) lambda self, **kwargs: mock.MagicMock(spec=Job, id=968))
@mock.patch('awx.api.serializers.JobSerializer.to_representation', lambda self, obj: {}) @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'] 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.django_db
@pytest.mark.survey @pytest.mark.survey
def test_redact_survey_passwords_in_activity_stream(job_template_with_survey_passwords): def test_redact_survey_passwords_in_activity_stream(job_template_with_survey_passwords):

View File

@@ -3,7 +3,6 @@
# Python # Python
import pytest import pytest
from unittest import mock
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from datetime import timedelta from datetime import timedelta
@@ -17,13 +16,6 @@ from awx.main.models.fact import Fact
from awx.main.models.inventory import Host 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 @pytest.mark.django_db
def test_cleanup_granularity(fact_scans, hosts, monkeypatch_jsonbfield_get_db_prep_save): 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 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 @pytest.mark.django_db
def test_parameters_ok(mocker): def test_parameters_ok(mocker):
run = mocker.patch('awx.main.management.commands.cleanup_facts.CleanupFacts.run') 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 assert res is None
@mock.patch('awx.main.management.commands.cleanup_facts.feature_enabled', new=mock_feature_enabled)
@pytest.mark.django_db @pytest.mark.django_db
def test_parameters_fail(mocker): def test_parameters_fail(mocker):
# Mock run() just in case, but it should never get called because an error should be thrown # 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 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 @pytest.fixture
def role(): def role():

View File

@@ -13,7 +13,6 @@ from awx.main.access import (
SystemJobTemplateAccess, SystemJobTemplateAccess,
) )
from awx.conf.license import LicenseForbids
from awx.main.models import ( from awx.main.models import (
Credential, Credential,
CredentialType, CredentialType,
@@ -21,7 +20,6 @@ from awx.main.models import (
Project, Project,
Role, Role,
Organization, Organization,
Instance,
) )
@@ -204,31 +202,20 @@ def test_jt_add_scan_job_check(job_template_with_ids, user_unit):
else: else:
raise Exception('Item requested has not been mocked') 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'
})
with mock.patch('awx.main.models.rbac.Role.__contains__', return_value=True):
def mock_raise_license_forbids(self, add_host=False, feature=None, check_expiration=True): with mock.patch('awx.main.access.get_object_or_400', mock_get_object):
raise LicenseForbids("Feature not enabled") 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): def mock_raise_none(self, add_host=False, feature=None, check_expiration=True):
return None 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): 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" "Assure that no server errors are returned if we call JT can_add with bad data"
access = JobTemplateAccess(user_unit) access = JobTemplateAccess(user_unit)

View File

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

View File

@@ -157,7 +157,6 @@ def _register_ldap(append=None):
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
placeholder='ldaps://ldap.example.com:636', placeholder='ldaps://ldap.example.com:636',
feature_required='ldap',
) )
register( register(
@@ -172,7 +171,6 @@ def _register_ldap(append=None):
' user information. Refer to the Ansible Tower documentation for example syntax.'), ' user information. Refer to the Ansible Tower documentation for example syntax.'),
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
feature_required='ldap',
) )
register( register(
@@ -184,7 +182,6 @@ def _register_ldap(append=None):
help_text=_('Password used to bind LDAP user account.'), help_text=_('Password used to bind LDAP user account.'),
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
feature_required='ldap',
encrypted=True, 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.'), help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'),
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
feature_required='ldap',
) )
register( register(
@@ -216,7 +212,6 @@ def _register_ldap(append=None):
('OPT_REFERRALS', 0), ('OPT_REFERRALS', 0),
('OPT_NETWORK_TIMEOUT', 30) ('OPT_NETWORK_TIMEOUT', 30)
]), ]),
feature_required='ldap',
) )
register( register(
@@ -237,7 +232,6 @@ def _register_ldap(append=None):
'SCOPE_SUBTREE', 'SCOPE_SUBTREE',
'(sAMAccountName=%(user)s)', '(sAMAccountName=%(user)s)',
), ),
feature_required='ldap',
) )
register( register(
@@ -255,7 +249,6 @@ def _register_ldap(append=None):
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
placeholder='uid=%(user)s,OU=Users,DC=example,DC=com', placeholder='uid=%(user)s,OU=Users,DC=example,DC=com',
feature_required='ldap',
) )
register( register(
@@ -274,7 +267,6 @@ def _register_ldap(append=None):
('last_name', 'sn'), ('last_name', 'sn'),
('email', 'mail'), ('email', 'mail'),
]), ]),
feature_required='ldap',
) )
register( register(
@@ -292,7 +284,6 @@ def _register_ldap(append=None):
'SCOPE_SUBTREE', 'SCOPE_SUBTREE',
'(objectClass=group)', '(objectClass=group)',
), ),
feature_required='ldap',
) )
register( register(
@@ -304,7 +295,6 @@ def _register_ldap(append=None):
'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups'), 'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups'),
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
feature_required='ldap',
default='MemberDNGroupType', default='MemberDNGroupType',
depends_on=['AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str)], depends_on=['AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str)],
) )
@@ -325,7 +315,6 @@ def _register_ldap(append=None):
('member_attr', 'member'), ('member_attr', 'member'),
('name_attr', 'cn'), ('name_attr', 'cn'),
]), ]),
feature_required='ldap',
depends_on=['AUTH_LDAP{}_GROUP_TYPE'.format(append_str)], depends_on=['AUTH_LDAP{}_GROUP_TYPE'.format(append_str)],
) )
@@ -343,7 +332,6 @@ def _register_ldap(append=None):
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
placeholder='CN=Tower Users,OU=Users,DC=example,DC=com', placeholder='CN=Tower Users,OU=Users,DC=example,DC=com',
feature_required='ldap',
) )
register( register(
@@ -359,7 +347,6 @@ def _register_ldap(append=None):
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com', placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com',
feature_required='ldap',
) )
register( register(
@@ -376,7 +363,6 @@ def _register_ldap(append=None):
('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), ('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'),
('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'), ('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'),
]), ]),
feature_required='ldap',
) )
register( register(
@@ -404,7 +390,6 @@ def _register_ldap(append=None):
('remove_admins', True), ('remove_admins', True),
])), ])),
]), ]),
feature_required='ldap',
) )
register( register(
@@ -428,7 +413,6 @@ def _register_ldap(append=None):
('remove', False), ('remove', False),
])), ])),
]), ]),
feature_required='ldap',
) )
@@ -454,7 +438,6 @@ register(
category=_('RADIUS'), category=_('RADIUS'),
category_slug='radius', category_slug='radius',
placeholder='radius.example.com', placeholder='radius.example.com',
feature_required='enterprise_auth',
) )
register( register(
@@ -467,7 +450,6 @@ register(
help_text=_('Port of RADIUS server.'), help_text=_('Port of RADIUS server.'),
category=_('RADIUS'), category=_('RADIUS'),
category_slug='radius', category_slug='radius',
feature_required='enterprise_auth',
) )
register( register(
@@ -479,7 +461,6 @@ register(
help_text=_('Shared secret for authenticating to RADIUS server.'), help_text=_('Shared secret for authenticating to RADIUS server.'),
category=_('RADIUS'), category=_('RADIUS'),
category_slug='radius', category_slug='radius',
feature_required='enterprise_auth',
encrypted=True, encrypted=True,
) )
@@ -496,7 +477,6 @@ register(
help_text=_('Hostname of TACACS+ server.'), help_text=_('Hostname of TACACS+ server.'),
category=_('TACACS+'), category=_('TACACS+'),
category_slug='tacacsplus', category_slug='tacacsplus',
feature_required='enterprise_auth',
) )
register( register(
@@ -509,7 +489,6 @@ register(
help_text=_('Port number of TACACS+ server.'), help_text=_('Port number of TACACS+ server.'),
category=_('TACACS+'), category=_('TACACS+'),
category_slug='tacacsplus', category_slug='tacacsplus',
feature_required='enterprise_auth',
) )
register( register(
@@ -522,7 +501,6 @@ register(
help_text=_('Shared secret for authenticating to TACACS+ server.'), help_text=_('Shared secret for authenticating to TACACS+ server.'),
category=_('TACACS+'), category=_('TACACS+'),
category_slug='tacacsplus', category_slug='tacacsplus',
feature_required='enterprise_auth',
encrypted=True, encrypted=True,
) )
@@ -535,7 +513,6 @@ register(
help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'), help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'),
category=_('TACACS+'), category=_('TACACS+'),
category_slug='tacacsplus', category_slug='tacacsplus',
feature_required='enterprise_auth',
) )
register( register(
@@ -547,7 +524,6 @@ register(
help_text=_('Choose the authentication protocol used by TACACS+ client.'), help_text=_('Choose the authentication protocol used by TACACS+ client.'),
category=_('TACACS+'), category=_('TACACS+'),
category_slug='tacacsplus', category_slug='tacacsplus',
feature_required='enterprise_auth',
) )
############################################################################### ###############################################################################
@@ -953,7 +929,6 @@ register(
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
depends_on=['TOWER_URL_BASE'], depends_on=['TOWER_URL_BASE'],
feature_required='enterprise_auth',
) )
register( register(
@@ -966,7 +941,6 @@ register(
'metadata file, you can download one from this URL.'), 'metadata file, you can download one from this URL.'),
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
feature_required='enterprise_auth',
) )
register( register(
@@ -980,7 +954,6 @@ register(
'This is usually the URL for Tower.'), 'This is usually the URL for Tower.'),
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
feature_required='enterprise_auth',
depends_on=['TOWER_URL_BASE'], depends_on=['TOWER_URL_BASE'],
) )
@@ -995,7 +968,6 @@ register(
'and include the certificate content here.'), 'and include the certificate content here.'),
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
feature_required='enterprise_auth',
) )
register( register(
@@ -1009,7 +981,6 @@ register(
'and include the private key content here.'), 'and include the private key content here.'),
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
feature_required='enterprise_auth',
encrypted=True, encrypted=True,
) )
@@ -1029,7 +1000,6 @@ register(
('url', 'http://www.example.com'), ('url', 'http://www.example.com'),
])), ])),
]), ]),
feature_required='enterprise_auth',
) )
register( register(
@@ -1047,7 +1017,6 @@ register(
('givenName', 'Technical Contact'), ('givenName', 'Technical Contact'),
('emailAddress', 'techsup@example.com'), ('emailAddress', 'techsup@example.com'),
]), ]),
feature_required='enterprise_auth',
) )
register( register(
@@ -1065,7 +1034,6 @@ register(
('givenName', 'Support Contact'), ('givenName', 'Support Contact'),
('emailAddress', 'support@example.com'), ('emailAddress', 'support@example.com'),
]), ]),
feature_required='enterprise_auth',
) )
register( register(
@@ -1102,7 +1070,6 @@ register(
('attr_email', 'User.email'), ('attr_email', 'User.email'),
])), ])),
]), ]),
feature_required='enterprise_auth',
) )
register( register(
@@ -1135,7 +1102,6 @@ register(
("signatureAlgorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"), ("signatureAlgorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"),
("digestAlgorithm", "http://www.w3.org/2000/09/xmldsig#sha1"), ("digestAlgorithm", "http://www.w3.org/2000/09/xmldsig#sha1"),
]), ]),
feature_required='enterprise_auth',
) )
register( register(
@@ -1149,7 +1115,6 @@ register(
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
placeholder=collections.OrderedDict(), placeholder=collections.OrderedDict(),
feature_required='enterprise_auth',
) )
register( register(
@@ -1167,7 +1132,6 @@ register(
('department', 'department'), ('department', 'department'),
('manager_full_name', 'manager_full_name') ('manager_full_name', 'manager_full_name')
], ],
feature_required='enterprise_auth',
) )
register( register(
@@ -1180,7 +1144,6 @@ register(
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER,
feature_required='enterprise_auth',
) )
register( register(
@@ -1193,7 +1156,6 @@ register(
category=_('SAML'), category=_('SAML'),
category_slug='saml', category_slug='saml',
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
feature_required='enterprise_auth',
) )
register( register(
@@ -1211,7 +1173,6 @@ register(
('remove', True), ('remove', True),
('remove_admins', True), ('remove_admins', True),
]), ]),
feature_required='enterprise_auth',
) )
register( 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 # Tower
from awx.conf import fields from awx.conf import fields
from awx.conf.license import feature_enabled
from awx.main.validators import validate_certificate from awx.main.validators import validate_certificate
from awx.sso.validators import ( # noqa from awx.sso.validators import ( # noqa
validate_ldap_dn, validate_ldap_dn,
@@ -164,13 +163,12 @@ class AuthenticationBackendsField(fields.StringListField):
except AttributeError: except AttributeError:
backends = self.REQUIRED_BACKEND_SETTINGS.keys() backends = self.REQUIRED_BACKEND_SETTINGS.keys()
# Filter which authentication backends are enabled based on their # Filter which authentication backends are enabled based on their
# required settings being defined and non-empty. Also filter available # required settings being defined and non-empty.
# backends based on license features.
for backend, required_settings in self.REQUIRED_BACKEND_SETTINGS.items(): for backend, required_settings in self.REQUIRED_BACKEND_SETTINGS.items():
if backend not in backends: if backend not in backends:
continue continue
required_feature = self.REQUIRED_BACKEND_FEATURE.get(backend, '') required_feature = self.REQUIRED_BACKEND_FEATURE.get(backend, '')
if not required_feature or feature_enabled(required_feature): if not required_feature:
if all([getattr(settings, rs, None) for rs in required_settings]): if all([getattr(settings, rs, None) for rs in required_settings]):
continue continue
backends = [x for x in backends if x != backend] backends = [x for x in backends if x != backend]
@@ -782,4 +780,3 @@ class SAMLTeamAttrField(BaseDictWithChildField):
'remove': fields.BooleanField(required=False), 'remove': fields.BooleanField(required=False),
'saml_attr': fields.CharField(required=False, allow_null=True), '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.utils.translation import ugettext_lazy as _
from django.db.models import Q from django.db.models import Q
# Tower
from awx.conf.license import feature_enabled
logger = logging.getLogger('awx.sso.pipeline') 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): def _update_org_from_attr(user, rel, attr, remove, remove_admins):
from awx.main.models import Organization from awx.main.models import Organization
multiple_orgs = feature_enabled('multiple_organizations')
org_ids = [] org_ids = []
for org_name in attr: for org_name in attr:
if multiple_orgs: org = Organization.objects.get_or_create(name=org_name)[0]
org = Organization.objects.get_or_create(name=org_name)[0]
else:
try:
org = Organization.objects.order_by('pk')[0]
except IndexError:
continue
org_ids.append(org.id) org_ids.append(org.id)
getattr(org, rel).members.add(user) getattr(org, rel).members.add(user)
@@ -116,19 +106,10 @@ def update_user_orgs(backend, details, user=None, *args, **kwargs):
if not user: if not user:
return return
from awx.main.models import Organization from awx.main.models import Organization
multiple_orgs = feature_enabled('multiple_organizations')
org_map = backend.setting('ORGANIZATION_MAP') or {} org_map = backend.setting('ORGANIZATION_MAP') or {}
for org_name, org_opts in org_map.items(): 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). # Update org admins from expression(s).
remove = bool(org_opts.get('remove', True)) remove = bool(org_opts.get('remove', True))
@@ -150,21 +131,13 @@ def update_user_teams(backend, details, user=None, *args, **kwargs):
if not user: if not user:
return return
from awx.main.models import Organization, Team from awx.main.models import Organization, Team
multiple_orgs = feature_enabled('multiple_organizations')
team_map = backend.setting('TEAM_MAP') or {} team_map = backend.setting('TEAM_MAP') or {}
for team_name, team_opts in team_map.items(): 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). # Update team members from expression(s).
team = Team.objects.get_or_create(name=team_name, organization=org)[0] 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 return
from awx.main.models import Organization, Team from awx.main.models import Organization, Team
from django.conf import settings from django.conf import settings
multiple_orgs = feature_enabled('multiple_organizations')
team_map = settings.SOCIAL_AUTH_SAML_TEAM_ATTR team_map = settings.SOCIAL_AUTH_SAML_TEAM_ATTR
if team_map.get('saml_attr') is None: if team_map.get('saml_attr') is None:
return 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', []): for team_name_map in team_map.get('team_org_map', []):
team_name = team_name_map.get('team', '') team_name = team_name_map.get('team', '')
if team_name in saml_team_names: if team_name in saml_team_names:
if multiple_orgs: if not team_name_map.get('organization', ''):
if not team_name_map.get('organization', ''): # Settings field validation should prevent this.
# Settings field validation should prevent this. logger.error("organization name invalid for team {}".format(team_name))
logger.error("organization name invalid for team {}".format(team_name)) continue
continue org = Organization.objects.get_or_create(name=team_name_map['organization'])[0]
org = Organization.objects.get_or_create(name=team_name_map['organization'])[0]
else:
try:
org = Organization.objects.order_by('pk')[0]
except IndexError:
continue
team = Team.objects.get_or_create(name=team_name, organization=org)[0] team = Team.objects.get_or_create(name=team_name, organization=org)[0]
team_ids.append(team.id) team_ids.append(team.id)

View File

@@ -31,21 +31,3 @@ def existing_tacacsplus_user():
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+') enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
enterprise_auth.save() enterprise_auth.save()
return user 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 assert ret_user is None
def test_disabled_enterprise_auth_fails_auth(tacacsplus_backend, feature_disabled): def test_client_raises_exception(tacacsplus_backend):
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):
client = mock.MagicMock() client = mock.MagicMock()
client.authenticate.side_effect=Exception("foo") client.authenticate.side_effect=Exception("foo")
with mock.patch('awx.sso.backends.django_settings') as settings,\ with mock.patch('awx.sso.backends.django_settings') as settings,\
mock.patch('awx.sso.backends.logger') as logger,\ 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): mock.patch('tacacs_plus.TACACSClient', return_value=client):
settings.TACACSPLUS_HOST = 'localhost' settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' 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 = mock.MagicMock()
auth.valid = False auth.valid = False
client = mock.MagicMock() client = mock.MagicMock()
client.authenticate.return_value = auth client.authenticate.return_value = auth
with mock.patch('awx.sso.backends.django_settings') as settings,\ 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('tacacs_plus.TACACSClient', return_value=client):
settings.TACACSPLUS_HOST = 'localhost' settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' 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 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 = mock.MagicMock()
auth.valid = True auth.valid = True
client = mock.MagicMock() client = mock.MagicMock()
@@ -58,7 +44,6 @@ def test_client_return_valid_passes_auth(tacacsplus_backend, feature_enabled):
user = mock.MagicMock() user = mock.MagicMock()
user.has_usable_password = mock.MagicMock(return_value=False) user.has_usable_password = mock.MagicMock(return_value=False)
with mock.patch('awx.sso.backends.django_settings') as settings,\ 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('tacacs_plus.TACACSClient', return_value=client),\
mock.patch('awx.sso.backends._get_or_set_enterprise_user', return_value=user): mock.patch('awx.sso.backends._get_or_set_enterprise_user', return_value=user):
settings.TACACSPLUS_HOST = 'localhost' settings.TACACSPLUS_HOST = 'localhost'

View File

@@ -61,27 +61,6 @@ export default {
return qs.search(path, stateParams); 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', subTitle: ['$stateParams', 'Rest', 'ModelToBasePathKey', 'GetBasePath',
'ProcessErrors', 'ProcessErrors',
function($stateParams, rest, ModelToBasePathKey, getBasePath, function($stateParams, rest, ModelToBasePathKey, getBasePath,

View File

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

View File

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

View File

@@ -107,7 +107,7 @@ export default ['i18n', function(i18n) {
}, },
save: { save: {
ngClick: 'vm.formSave()', 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: { save: {
ngClick: 'vm.formSave()', 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: { save: {
ngClick: 'vm.formSave()', 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: { save: {
ngClick: 'vm.formSave()', 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: { save: {
ngClick: 'vm.formSave()', 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: { save: {
ngClick: 'vm.formSave()', 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.configDataResolve = configDataResolve;
$scope.formDefs = formDefs; $scope.formDefs = formDefs;
// check if it's auditor, show messageBar // check if it's auditor, show messageBar
$scope.show_auditor_bar = false; $scope.show_auditor_bar = false;
if($rootScope.user_is_system_auditor && Store('show_auditor_bar') !== false) { if($rootScope.user_is_system_auditor && Store('show_auditor_bar') !== false) {
$scope.show_auditor_bar = true; $scope.show_auditor_bar = true;
} else { } else {
$scope.show_auditor_bar = false; $scope.show_auditor_bar = false;
} }
var populateFromApi = function() { var populateFromApi = function() {
SettingsService.getCurrentValues() SettingsService.getCurrentValues()
@@ -145,19 +145,6 @@ export default [
}); });
$scope.$broadcast('populated', data); $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(); populateFromApi();

View File

@@ -21,12 +21,10 @@ export default {
label: N_("DASHBOARD") label: N_("DASHBOARD")
}, },
resolve: { resolve: {
graphData: ['$q', 'jobStatusGraphData', '$rootScope', graphData: ['$q', 'jobStatusGraphData',
function($q, jobStatusGraphData, $rootScope) { function($q, jobStatusGraphData) {
return $rootScope.featuresConfigured.promise.then(function() { return $q.all({
return $q.all({ jobStatus: jobStatusGraphData.get("month", "all"),
jobStatus: jobStatusGraphData.get("month", "all"),
});
}); });
} }
] ]

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,6 @@ register(
'custom HTML or other markup languages are not supported.'), 'custom HTML or other markup languages are not supported.'),
category=_('UI'), category=_('UI'),
category_slug='ui', category_slug='ui',
feature_required='rebranding',
) )
register( register(
@@ -50,7 +49,6 @@ register(
placeholder='data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=', placeholder='data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',
category=_('UI'), category=_('UI'),
category_slug='ui', category_slug='ui',
feature_required='rebranding',
) )
register( 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, category=None,
depends_on=None, depends_on=None,
placeholder=rest_framework.fields.empty, placeholder=rest_framework.fields.empty,
feature_required=rest_framework.fields.empty,
encrypted=False, encrypted=False,
defined_in_file=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. | | `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. | | `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 | | `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 | | `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. | | `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 addopts = --reuse-db --nomigrations --tb=native
markers = markers =
ac: access control test ac: access control test
license_feature: ensure license features are accessible or not depending on license
survey: tests related to survey feature survey: tests related to survey feature
inventory_import: tests of code used by inventory import command inventory_import: tests of code used by inventory import command