Skip to content

Commit

Permalink
Use DDM client capabilities to gate SUES
Browse files Browse the repository at this point in the history
  • Loading branch information
np5 committed Dec 22, 2023
1 parent 1285051 commit 67ce9c7
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 13 deletions.
36 changes: 27 additions & 9 deletions tests/mdm/test_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand Down Expand Up @@ -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"])
Expand All @@ -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")
Expand Down Expand Up @@ -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])
Expand All @@ -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
Expand Down
30 changes: 29 additions & 1 deletion tests/mdm/test_mdm_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
100 changes: 100 additions & 0 deletions tests/mdm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}
28 changes: 25 additions & 3 deletions zentral/contrib/mdm/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 67ce9c7

Please sign in to comment.