diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e8b0706789..38c3253b13 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2826,6 +2826,8 @@ class ScheduleSerializer(BaseSerializer): def validate_unified_job_template(self, value): if type(value) == InventorySource and value.source not in SCHEDULEABLE_PROVIDERS: raise serializers.ValidationError(_('Inventory Source must be a cloud resource.')) + elif type(value) == Project and value.scm_type == '': + raise serializers.ValidationError(_('Manual Project can not have a schedule set.')) return value # We reject rrules if: diff --git a/awx/main/access.py b/awx/main/access.py index 002d61bedc..375f286d9d 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -305,7 +305,8 @@ class BaseAccess(object): if display_method not in method_list: continue - # Actions not possible for reason unrelated to RBAC: validation, etc. + # Actions not possible for reason unrelated to RBAC + # Cannot copy with validation errors, or update a manual group/project if display_method == 'copy' and isinstance(obj, JobTemplate): validation_errors, resources_needed_to_start = obj.resource_validation_data() if validation_errors: @@ -315,6 +316,10 @@ class BaseAccess(object): if obj.inventory_source and not obj.inventory_source._can_update(): user_capabilities[display_method] = False continue + elif display_method in ['start', 'schedule'] and isinstance(obj, (Project)): + if obj.scm_type == '': + user_capabilities[display_method] = False + continue # Grab the answer from the cache, if available if hasattr(obj, 'capabilities_cache') and display_method in obj.capabilities_cache: @@ -341,10 +346,6 @@ class BaseAccess(object): elif display_method == 'copy' and isinstance(obj, (Group, Host)): user_capabilities['copy'] = user_capabilities['edit'] continue - elif display_method == 'start' and isinstance(obj, (Project)) and obj.scm_type == '': - # Special case to return False for a manual project - user_capabilities['start'] = False - continue # Preprocessing before the access method is called data = {} diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index cd336c66c8..e1255ffea9 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -320,6 +320,7 @@ def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_c def test_manual_projects_no_update(project, get, admin_user): response = get(reverse('api:project_detail', args=[project.pk]), admin_user, expect=200) assert not response.data['summary_fields']['user_capabilities']['start'] + assert not response.data['summary_fields']['user_capabilities']['schedule'] @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_projects.py b/awx/main/tests/functional/test_projects.py index cd3f6e57ff..6d66c4e346 100644 --- a/awx/main/tests/functional/test_projects.py +++ b/awx/main/tests/functional/test_projects.py @@ -136,3 +136,12 @@ def test_patch_project_null_organization(patch, organization, project, admin): @pytest.mark.django_db() def test_patch_project_null_organization_xfail(patch, project, org_admin): patch(reverse('api:project_detail', args=(project.id,)), { 'name': 't', 'organization': None}, org_admin, expect=400) + +@pytest.mark.django_db +def test_cannot_schedule_manual_project(project, admin_user, post): + response = post( + reverse('api:project_schedules_list', args=(project.pk,)), + {"name": "foo", "description": "", "enabled": True, + "rrule": "DTSTART:20160926T040000Z RRULE:FREQ=HOURLY;INTERVAL=1", + "extra_data": {}}, admin_user, expect=400) + assert 'Manual' in response.data['unified_job_template'][0]