From 67ce9c71e04088d0512c5bdb8a0635299398ff14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Falconnier?= Date: Fri, 22 Dec 2023 16:15:32 +0100 Subject: [PATCH] Use DDM client capabilities to gate SUES --- tests/mdm/test_artifacts.py | 36 ++++++++--- tests/mdm/test_mdm_views.py | 30 +++++++++- tests/mdm/utils.py | 100 +++++++++++++++++++++++++++++++ zentral/contrib/mdm/artifacts.py | 28 ++++++++- 4 files changed, 181 insertions(+), 13 deletions(-) diff --git a/tests/mdm/test_artifacts.py b/tests/mdm/test_artifacts.py index 3b1264162..09bc99eab 100644 --- a/tests/mdm/test_artifacts.py +++ b/tests/mdm/test_artifacts.py @@ -17,7 +17,8 @@ Platform, Profile, PushCertificate, StoreApp, TargetArtifact, UserArtifact) -from .utils import force_software_update, force_software_update_enforcement +from .utils import (force_software_update, force_software_update_enforcement, + MACOS_13_CLIENT_CAPABILITIES, MACOS_14_CLIENT_CAPABILITIES) PROFILE_TEMPLATE = { @@ -80,7 +81,7 @@ def setUpTestData(cls): udid=get_random_string(36), token=get_random_string(32).encode("utf-8"), push_magic=get_random_string(73), - unlock_token=get_random_string(32).encode("utf-8") + unlock_token=get_random_string(32).encode("utf-8"), ) cls.enrolled_device = EnrolledDevice.objects.create( push_certificate=push_certificate, @@ -91,7 +92,7 @@ def setUpTestData(cls): udid=get_random_string(36), token=get_random_string(32).encode("utf-8"), push_magic=get_random_string(73), - unlock_token=get_random_string(32).encode("utf-8") + unlock_token=get_random_string(32).encode("utf-8"), ) cls.enrolled_user = EnrolledUser.objects.create( enrolled_device=cls.enrolled_device, @@ -807,6 +808,7 @@ def test_device_activation_software_update_enforcement_latest_wrong_platform_not self.assertIn(f"zentral.blueprint.{self.blueprint1.pk}.management-status-subscriptions", scs) def test_device_activation_software_update_enforcement_latest_included(self): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES target = Target(self.enrolled_device) force_software_update(device_id=self.enrolled_device.device_information["SoftwareUpdateDeviceID"], version="14.1.0", @@ -822,6 +824,7 @@ def test_device_activation_software_update_enforcement_latest_included(self): self.assertIn(f"zentral.blueprint.{self.blueprint1.pk}.softwareupdate-enforcement-specific", scs) def test_device_activation_software_update_enforcement_one_time_included(self): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES target = Target(self.enrolled_device) sue = force_software_update_enforcement(os_version="15", local_datetime=datetime.utcnow()) self.blueprint1.software_update_enforcements.add(sue) @@ -892,6 +895,7 @@ def test_device_declaration_items_software_update_enforcement_latest_included(se posting_date=date(2023, 10, 25)) sue = force_software_update_enforcement(max_os_version="15", local_time=time(9, 30), delay_days=15) self.blueprint1.software_update_enforcements.add(sue) + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES target = Target(self.enrolled_device) declaration_items = target.declaration_items self.assertEqual(sorted(declaration_items.keys()), ["Declarations", "DeclarationsToken"]) @@ -908,7 +912,7 @@ def test_device_declaration_items_software_update_enforcement_latest_included(se self.assertEqual(len(configurations), 2) self.assertEqual(configurations[0]["Identifier"], f"zentral.blueprint.{self.blueprint1.pk}.management-status-subscriptions") - self.assertEqual(configurations[0]["ServerToken"], "0ed215547af3061ce18ea6cf7a69dac4a3d52f3f") + self.assertEqual(configurations[0]["ServerToken"], "d012ad4032e941a8c1f4a61dc6691372d946a4d2") self.assertEqual(configurations[1]["Identifier"], f"zentral.blueprint.{self.blueprint1.pk}.softwareupdate-enforcement-specific") self.assertEqual(configurations[1]["ServerToken"], "3aa402ccdcc30fbe9fc24a437da2fac09f709243") @@ -1420,22 +1424,35 @@ def test_update_user_target_with_status_report(self, send_enrolled_user_notifica # software update enforcement - def test_software_update_enforcement_not_a_device(self): + def test_supports_software_update_enforcement_specific_not_a_device(self): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES target = Target(self.enrolled_device, self.enrolled_user) - self.assertFalse(target.is_device) - self.assertIsNone(target.software_update_enforcement) + self.assertFalse(target.supports_software_update_enforcement_specific()) - def test_software_update_enforcement_no_blueprint(self): + def test_supports_update_enforcement_specific_no_blueprint(self): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES target = Target(self.enrolled_device_no_blueprint) self.assertIsNone(target.blueprint) - self.assertIsNone(target.software_update_enforcement) + self.assertFalse(target.supports_software_update_enforcement_specific()) + + def test_supports_update_enforcement_specific_missing_capabilities(self): + self.assertIsNone(self.enrolled_device.client_capabilities) + target = Target(self.enrolled_device) + self.assertFalse(target.supports_software_update_enforcement_specific()) + + def test_supports_update_enforcement_specific_missing_capability(self): + self.enrolled_device.client_capabilities = MACOS_13_CLIENT_CAPABILITIES + target = Target(self.enrolled_device) + self.assertFalse(target.supports_software_update_enforcement_specific()) def test_software_update_enforcement_no_sue(self): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES target = Target(self.enrolled_device) self.assertEqual(target.blueprint.software_update_enforcements.count(), 0) self.assertIsNone(target.software_update_enforcement) def test_software_update_enforcement_tags(self): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES tags = [Tag.objects.create(name=get_random_string(12)) for _ in range(3)] sue = force_software_update_enforcement() sue1 = force_software_update_enforcement(tags=tags[:1]) @@ -1448,6 +1465,7 @@ def test_software_update_enforcement_tags(self): @patch("zentral.contrib.mdm.artifacts.logger.warning") def test_software_update_enforcement_tag_conflict(self, logger_warning): + self.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES tags = [Tag.objects.create(name=get_random_string(12)) for _ in range(3)] sue = force_software_update_enforcement(tags=tags[:2]) sue2 = force_software_update_enforcement(tags=tags[-2:]) # same matching tags number diff --git a/tests/mdm/test_mdm_views.py b/tests/mdm/test_mdm_views.py index 123266cd5..1db966673 100644 --- a/tests/mdm/test_mdm_views.py +++ b/tests/mdm/test_mdm_views.py @@ -28,7 +28,8 @@ force_ota_enrollment_session, force_software_update, force_software_update_enforcement, - force_user_enrollment_session) + force_user_enrollment_session, + MACOS_14_CLIENT_CAPABILITIES) @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage') @@ -729,9 +730,33 @@ def test_declarative_management_status_subscriptions_declaration_user_channel(se 'Type': 'com.apple.configuration.management.status-subscriptions'} ) + def test_declarative_management_softwareupdate_enforcement_specific_err(self, post_event): + session, udid, serial_number = force_dep_enrollment_session(self.mbu, authenticated=True, completed=True) + device_id = get_random_string(8) + session.enrolled_device.device_information = {"SoftwareUpdateDeviceID": device_id} + session.enrolled_device.save() + force_software_update(device_id=device_id, version="14.1.0", posting_date=date(2023, 10, 25)) + sue = force_software_update_enforcement( + details_url="https://www.example.com", + max_os_version="15", local_time=time(9, 30), delay_days=15 + ) + blueprint = self._add_blueprint(session) + blueprint.software_update_enforcements.add(sue) + payload = { + "UDID": udid, + "MessageType": "DeclarativeManagement", + "Data": json.dumps({"un": 2}), + "Endpoint": f"declaration/configuration/zentral.blueprint.{blueprint.pk}." + "softwareupdate-enforcement-specific" + } + self._put(reverse("mdm_public:checkin"), payload, session) + self._assertAbort(post_event, "Could not build specific software update enforcement", + udid=udid, serial_number=serial_number) + def test_declarative_management_softwareupdate_enforcement_specific_latest(self, post_event): session, udid, serial_number = force_dep_enrollment_session(self.mbu, authenticated=True, completed=True) device_id = get_random_string(8) + session.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES session.enrolled_device.device_information = {"SoftwareUpdateDeviceID": device_id} session.enrolled_device.save() force_software_update(device_id=device_id, version="14.1.0", posting_date=date(2023, 10, 25)) @@ -765,6 +790,7 @@ def test_declarative_management_softwareupdate_enforcement_specific_latest(self, def test_declarative_management_softwareupdate_enforcement_specific_latest_same(self, post_event): session, udid, serial_number = force_dep_enrollment_session(self.mbu, authenticated=True, completed=True) device_id = get_random_string(8) + session.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES session.enrolled_device.device_information = {"SoftwareUpdateDeviceID": device_id} session.enrolled_device.os_version = "14.1" session.enrolled_device.build_version = "23B74" @@ -801,6 +827,7 @@ def test_declarative_management_softwareupdate_enforcement_specific_latest_same( def test_declarative_management_softwareupdate_enforcement_specific_latest_not_found(self, post_event): session, udid, serial_number = force_dep_enrollment_session(self.mbu, authenticated=True, completed=True) device_id = get_random_string(8) + session.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES session.enrolled_device.device_information = {"SoftwareUpdateDeviceID": device_id} session.enrolled_device.save() sue = force_software_update_enforcement( @@ -824,6 +851,7 @@ def test_declarative_management_softwareupdate_enforcement_specific_latest_not_f def test_declarative_management_softwareupdate_enforcement_specific_one_time(self, post_event): session, udid, serial_number = force_dep_enrollment_session(self.mbu, authenticated=True, completed=True) device_id = get_random_string(8) + session.enrolled_device.client_capabilities = MACOS_14_CLIENT_CAPABILITIES session.enrolled_device.device_information = {"SoftwareUpdateDeviceID": device_id} session.enrolled_device.save() sue = force_software_update_enforcement( diff --git a/tests/mdm/utils.py b/tests/mdm/utils.py index 9ac39866a..395dca848 100644 --- a/tests/mdm/utils.py +++ b/tests/mdm/utils.py @@ -643,3 +643,103 @@ def force_blueprint_artifact( ) update_blueprint_serialized_artifacts(blueprint) return blueprint_artifact, artifact, artifact_versions + + +MACOS_14_CLIENT_CAPABILITIES = { + 'supported-features': {}, + 'supported-payloads': { + 'declarations': { + 'activations': [ + 'com.apple.activation.simple' + ], + 'assets': ['com.apple.asset.credential.acme', + 'com.apple.asset.credential.certificate', + 'com.apple.asset.credential.identity', + 'com.apple.asset.credential.scep', + 'com.apple.asset.credential.userpassword', + 'com.apple.asset.data', + 'com.apple.asset.useridentity'], + 'configurations': ['com.apple.configuration.legacy', + 'com.apple.configuration.legacy.interactive', + 'com.apple.configuration.management.status-subscriptions', + 'com.apple.configuration.management.test', + 'com.apple.configuration.passcode.settings', + 'com.apple.configuration.screensharing.connection', + 'com.apple.configuration.screensharing.connection.group', + 'com.apple.configuration.screensharing.host.settings', + 'com.apple.configuration.security.certificate', + 'com.apple.configuration.security.identity', + 'com.apple.configuration.services.configuration-files', + 'com.apple.configuration.softwareupdate.enforcement.specific'], + 'management': ['com.apple.management.organization-info', + 'com.apple.management.properties', + 'com.apple.management.server-capabilities']}, + 'status-items': ['device.identifier.serial-number', + 'device.identifier.udid', + 'device.model.family', + 'device.model.identifier', + 'device.model.marketing-name', + 'device.model.number', + 'device.operating-system.build-version', + 'device.operating-system.family', + 'device.operating-system.marketing-name', + 'device.operating-system.supplemental.build-version', + 'device.operating-system.supplemental.extra-version', + 'device.operating-system.version', + 'diskmanagement.filevault.enabled', + 'management.client-capabilities', + 'management.declarations', + 'screensharing.connection.group.unresolved-connection', + 'security.certificate.list', + 'services.background-task', + 'softwareupdate.failure-reason', + 'softwareupdate.install-reason', + 'softwareupdate.install-state', + 'softwareupdate.pending-version', + 'test.array-value', + 'test.boolean-value', + 'test.dictionary-value', + 'test.error-value', + 'test.integer-value', + 'test.real-value', + 'test.string-value']}, + 'supported-versions': ['1.0.0'] +} + + +MACOS_13_CLIENT_CAPABILITIES = { + 'supported-features': {}, + 'supported-payloads': { + 'declarations': { + 'activations': ['com.apple.activation.simple'], + 'assets': [], + 'configurations': ['com.apple.configuration.legacy', + 'com.apple.configuration.legacy.interactive', + 'com.apple.configuration.management.status-subscriptions', + 'com.apple.configuration.management.test', + 'com.apple.configuration.passcode.settings'], + 'management': ['com.apple.management.organization-info', + 'com.apple.management.properties', + 'com.apple.management.server-capabilities']}, + 'status-items': ['device.identifier.serial-number', + 'device.identifier.udid', + 'device.model.family', + 'device.model.identifier', + 'device.model.marketing-name', + 'device.operating-system.build-version', + 'device.operating-system.family', + 'device.operating-system.marketing-name', + 'device.operating-system.supplemental.build-version', + 'device.operating-system.supplemental.extra-version', + 'device.operating-system.version', + 'management.client-capabilities', + 'management.declarations', + 'test.array-value', + 'test.boolean-value', + 'test.dictionary-value', + 'test.error-value', + 'test.integer-value', + 'test.real-value', + 'test.string-value']}, + 'supported-versions': ['1.0.0'] +} diff --git a/zentral/contrib/mdm/artifacts.py b/zentral/contrib/mdm/artifacts.py index 0e50776c8..2ea3f83b9 100644 --- a/zentral/contrib/mdm/artifacts.py +++ b/zentral/contrib/mdm/artifacts.py @@ -563,12 +563,33 @@ def update_target_artifacts_with_status_report(self, status_report): # declarations - @cached_property - def software_update_enforcement(self): + def supports_software_update_enforcement_specific(self): if not self.is_device: - return + return False + if not self.blueprint: + return False + + client_capabilities = self.enrolled_device.client_capabilities + if not isinstance(client_capabilities, dict): + return + supported_configurations = client_capabilities.get( + "supported-payloads", {} + ).get( + "declarations", {} + ).get( + "configurations", [] + ) + if "com.apple.configuration.softwareupdate.enforcement.specific" not in supported_configurations: + return False + + return True + + @cached_property + def software_update_enforcement(self): + if not self.supports_software_update_enforcement_specific(): return + matching_tag_count = 0 selected_sue = None for sue in (self.blueprint.software_update_enforcements @@ -582,6 +603,7 @@ def software_update_enforcement(self): selected_sue = sue elif sue_tag_ids and common_tag_count == matching_tag_count: logger.warning("Machine %s: software update enforcement conflict", self.serial_number) + return selected_sue # https://developer.apple.com/documentation/devicemanagement/activationsimple