diff --git a/awx/api/permissions.py b/awx/api/permissions.py index a655360dc8..b534fbf824 100644 --- a/awx/api/permissions.py +++ b/awx/api/permissions.py @@ -4,9 +4,6 @@ # Python import logging -# Django -from django.http import Http404 - # Django REST Framework from rest_framework.exceptions import MethodNotAllowed, PermissionDenied from rest_framework import permissions @@ -19,7 +16,8 @@ from awx.main.utils import get_object_or_400 logger = logging.getLogger('awx.api.permissions') __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', - 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission'] + 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission', + 'HostPermission',] class ModelAccessPermission(permissions.BasePermission): @@ -96,13 +94,6 @@ class ModelAccessPermission(permissions.BasePermission): method based on the request method. ''' - # Check that obj (if given) is active, otherwise raise a 404. - active = getattr(obj, 'active', getattr(obj, 'is_active', True)) - if callable(active): - active = active() - if not active: - raise Http404() - # Don't allow anonymous users. 401, not 403, hence no raised exception. if not request.user or request.user.is_anonymous(): return False @@ -216,3 +207,25 @@ class UserPermission(ModelAccessPermission): elif request.user.is_superuser: return True raise PermissionDenied() + + +class HostPermission(ModelAccessPermission): + ''' + Allow super super for all operations that don't add or update data. + Allow the request to flow through access.py so that even a super-user can't + violate the license host count restriction. + ''' + + def check_options_permissions(self, request, view, obj=None): + view.always_allow_superuser = True + return super(HostPermission, self).check_options_permissions(request, view, obj) + + def check_head_permissions(self, request, view, obj=None): + view.always_allow_superuser = True + return super(HostPermission, self).check_head_permissions(request, view, obj) + + def check_get_permissions(self, request, view, obj=None): + view.always_allow_superuser = True + return super(HostPermission, self).check_get_permissions(request, view, obj) + + diff --git a/awx/api/views.py b/awx/api/views.py index 0b223228e0..da9a2df2fa 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1684,8 +1684,10 @@ class HostList(ListCreateAPIView): class HostDetail(RetrieveUpdateDestroyAPIView): + always_allow_superuser = False model = Host serializer_class = HostSerializer + permission_classes = (HostPermission,) class InventoryHostsList(SubListCreateAttachDetachAPIView): diff --git a/awx/main/access.py b/awx/main/access.py index 460dfe7b4c..52794eee7a 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -285,7 +285,7 @@ class BaseAccess(object): return True # User has access to both, permission check passed - def check_license(self, add_host=False, feature=None, check_expiration=True): + def check_license(self, add_host_name=None, feature=None, check_expiration=True): validation_info = TaskEnhancer().validate_enhancements() if ('test' in sys.argv or 'py.test' in sys.argv[0] or 'jenkins' in sys.argv) and not os.environ.get('SKIP_LICENSE_FIXUP_FOR_TEST', ''): validation_info['free_instances'] = 99999999 @@ -299,11 +299,14 @@ class BaseAccess(object): free_instances = validation_info.get('free_instances', 0) available_instances = validation_info.get('available_instances', 0) - if add_host and free_instances == 0: - raise PermissionDenied(_("License count of %s instances has been reached.") % available_instances) - elif add_host and free_instances < 0: - raise PermissionDenied(_("License count of %s instances has been exceeded.") % available_instances) - elif not add_host and free_instances < 0: + + if add_host_name: + host_exists = Host.objects.filter(name=add_host_name).exists() + if not host_exists and free_instances == 0: + raise PermissionDenied(_("License count of %s instances has been reached.") % available_instances) + elif not host_exists and free_instances < 0: + raise PermissionDenied(_("License count of %s instances has been exceeded.") % available_instances) + elif not add_host_name and free_instances < 0: raise PermissionDenied(_("Host count exceeds available instances.")) if feature is not None: @@ -611,7 +614,7 @@ class HostAccess(BaseAccess): return False # Check to see if we have enough licenses - self.check_license(add_host=True) + self.check_license(add_host_name=data['name']) return True def can_change(self, obj, data): @@ -619,6 +622,11 @@ class HostAccess(BaseAccess): inventory_pk = get_pk_from_dict(data, 'inventory') if obj and inventory_pk and obj.inventory.pk != inventory_pk: raise PermissionDenied(_('Unable to change inventory on a host.')) + + # Prevent renaming a host that might exceed license count + if 'name' in data: + self.check_license(add_host_name=data['name']) + # Checks for admin or change permission on inventory, controls whether # the user can edit variable data. return obj and self.user in obj.inventory.admin_role