Skip to content

Commit

Permalink
Add MDM enrolled device block/unblock API
Browse files Browse the repository at this point in the history
  • Loading branch information
np5 committed Apr 4, 2024
1 parent 2741437 commit 55f9846
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 2 deletions.
98 changes: 98 additions & 0 deletions docs/apps/mdm.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,104 @@ Response:
]
```

### `/api/mdm/devices/<int:pk>/block/`

* method: `POST`
* required permission: `mdm.change_enrolleddevice`

Blocks an enrolled device. Releases it from the MDM and denies further MDM enrollments. A serialized device is returned.

Example:

```bash
curl -XPOST \
-H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/mdm/devices/27/block/
```

Response:

```json
{
"id": 27,
"udid": "2A7F9BCE-9B52-4073-BE21-E419C85068E9",
"serial_number": "012345678910",
"name": "John’s Mac mini",
"model": "Macmini8,1",
"platform": "macOS",
"os_version": "14.2.1",
"build_version": "23C71",
"apple_silicon": false,
"cert_not_valid_after": "2024-08-05T14:44:01",
"blueprint": 1,
"awaiting_configuration": false,
"declarative_management": true,
"dep_enrollment": true,
"user_enrollment": false,
"user_approved_enrollment": true,
"supervised": true,
"bootstrap_token_escrowed": true,
"filevault_enabled": true,
"filevault_prk_escrowed": true,
"activation_lock_manageable": true,
"last_seen_at": "2024-02-17T20:31:34.848107",
"last_notified_at": "2024-03-27T20:44:21.751091",
"checkout_at": null,
"blocked_at": "2024-02-04T07:03:12.345678",
"created_at": "2023-08-06T14:44:01.847058",
"updated_at": "2024-02-04T07:03:12.456789",
}
```

### `/api/mdm/devices/<int:pk>/unblock/`

* method: `POST`
* required permission: `mdm.change_enrolleddevice`

Unblocks an enrolled device. The device can enroll again. A serialized device is returned.

Example:

```bash
curl -XPOST \
-H "Authorization: Token $ZTL_API_TOKEN" \
https://$ZTL_FQDN/api/mdm/devices/27/unblock/
```

Response:

```json
{
"id": 27,
"udid": "2A7F9BCE-9B52-4073-BE21-E419C85068E9",
"serial_number": "012345678910",
"name": "John’s Mac mini",
"model": "Macmini8,1",
"platform": "macOS",
"os_version": "14.2.1",
"build_version": "23C71",
"apple_silicon": false,
"cert_not_valid_after": "2024-08-05T14:44:01",
"blueprint": 1,
"awaiting_configuration": false,
"declarative_management": true,
"dep_enrollment": true,
"user_enrollment": false,
"user_approved_enrollment": true,
"supervised": true,
"bootstrap_token_escrowed": true,
"filevault_enabled": true,
"filevault_prk_escrowed": true,
"activation_lock_manageable": true,
"last_seen_at": "2024-02-17T20:31:34.848107",
"last_notified_at": "2024-03-27T20:44:21.751091",
"checkout_at": null,
"blocked_at": null,
"created_at": "2023-08-06T14:44:01.847058",
"updated_at": "2024-02-17T20:31:34.848262"
}
```

### `/api/mdm/devices/<int:pk>/erase/`

* method: `POST`
Expand Down
114 changes: 114 additions & 0 deletions tests/mdm/test_api_enrolled_devices_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,120 @@ def test_enrolled_devices_with_secrets(self):
'user_enrollment': None}]
)

# block enrolled device

def test_block_enrolled_device_unauthorized(self):
response = self.post(reverse("mdm_api:block_enrolled_device", args=(self.enrolled_device.pk,)), None,
include_token=False)
self.assertEqual(response.status_code, 401)

def test_block_enrolled_device_permission_denied(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.post(reverse("mdm_api:block_enrolled_device", args=(self.enrolled_device.pk,)), None)
self.assertEqual(response.status_code, 403)

def test_block_enrolled_device_already_blocked(self):
self.enrolled_device.block()
self.set_permissions("mdm.change_enrolleddevice")
response = self.post(reverse("mdm_api:block_enrolled_device", args=(self.enrolled_device.pk,)), None)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {"detail": "Device already blocked."})

def test_block_enrolled_device(self):
self.enrolled_device.unblock()
self.set_permissions("mdm.change_enrolleddevice")
response = self.post(reverse("mdm_api:block_enrolled_device", args=(self.enrolled_device.pk,)), None)
self.assertEqual(response.status_code, 200)
self.enrolled_device.refresh_from_db()
self.assertEqual(
response.json(),
{'activation_lock_manageable': None,
'apple_silicon': None,
'awaiting_configuration': None,
'blocked_at': self.enrolled_device.blocked_at.isoformat(),
'blueprint': None,
'bootstrap_token_escrowed': False,
'build_version': '',
'cert_not_valid_after': self.enrolled_device.cert_not_valid_after.isoformat(),
'checkout_at': None,
'created_at': self.enrolled_device.created_at.isoformat(),
'declarative_management': False,
'dep_enrollment': None,
'filevault_enabled': None,
'filevault_prk_escrowed': False,
'id': self.enrolled_device.pk,
'last_notified_at': None,
'last_seen_at': None,
'model': None,
'name': None,
'os_version': '',
'platform': 'macOS',
'recovery_password_escrowed': False,
'serial_number': self.enrolled_device.serial_number,
'supervised': None,
'udid': self.enrolled_device.udid,
'updated_at': self.enrolled_device.updated_at.isoformat(),
'user_approved_enrollment': None,
'user_enrollment': None}
)

# unblock enrolled device

def test_unblock_enrolled_device_unauthorized(self):
response = self.post(reverse("mdm_api:unblock_enrolled_device", args=(self.enrolled_device.pk,)), None,
include_token=False)
self.assertEqual(response.status_code, 401)

def test_unblock_enrolled_device_permission_denied(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.post(reverse("mdm_api:unblock_enrolled_device", args=(self.enrolled_device.pk,)), None)
self.assertEqual(response.status_code, 403)

def test_unblock_enrolled_device_already_unblocked(self):
self.enrolled_device.unblock()
self.set_permissions("mdm.change_enrolleddevice")
response = self.post(reverse("mdm_api:unblock_enrolled_device", args=(self.enrolled_device.pk,)), None)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), {"detail": "Device not blocked."})

def test_unblock_enrolled_device(self):
self.enrolled_device.block()
self.set_permissions("mdm.change_enrolleddevice")
response = self.post(reverse("mdm_api:unblock_enrolled_device", args=(self.enrolled_device.pk,)), None)
self.assertEqual(response.status_code, 200)
self.enrolled_device.refresh_from_db()
self.assertEqual(
response.json(),
{'activation_lock_manageable': None,
'apple_silicon': None,
'awaiting_configuration': None,
'blocked_at': None,
'blueprint': None,
'bootstrap_token_escrowed': False,
'build_version': '',
'cert_not_valid_after': self.enrolled_device.cert_not_valid_after.isoformat(),
'checkout_at': None,
'created_at': self.enrolled_device.created_at.isoformat(),
'declarative_management': False,
'dep_enrollment': None,
'filevault_enabled': None,
'filevault_prk_escrowed': False,
'id': self.enrolled_device.pk,
'last_notified_at': None,
'last_seen_at': None,
'model': None,
'name': None,
'os_version': '',
'platform': 'macOS',
'recovery_password_escrowed': False,
'serial_number': self.enrolled_device.serial_number,
'supervised': None,
'udid': self.enrolled_device.udid,
'updated_at': self.enrolled_device.updated_at.isoformat(),
'user_approved_enrollment': None,
'user_enrollment': None}
)

# erase enrolled device

def test_erase_enrolled_device_unauthorized(self):
Expand Down
3 changes: 3 additions & 0 deletions zentral/contrib/mdm/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
EnterpriseAppDetail, EnterpriseAppList,
EnrolledDeviceList,
LocationList, LocationAssetList,
BlockEnrolledDevice, UnblockEnrolledDevice,
LockEnrolledDevice, EraseEnrolledDevice,
FileVaultConfigDetail, FileVaultConfigList,
ProfileDetail, ProfileList,
Expand Down Expand Up @@ -43,6 +44,8 @@
path('dep/virtual_servers/<int:pk>/sync_devices/',
DEPVirtualServerSyncDevicesView.as_view(), name="dep_virtual_server_sync_devices"),
path('devices/', EnrolledDeviceList.as_view(), name="enrolled_devices"),
path('devices/<int:pk>/block/', BlockEnrolledDevice.as_view(), name="block_enrolled_device"),
path('devices/<int:pk>/unblock/', UnblockEnrolledDevice.as_view(), name="unblock_enrolled_device"),
path('devices/<int:pk>/erase/', EraseEnrolledDevice.as_view(), name="erase_enrolled_device"),
path('devices/<int:pk>/lock/', LockEnrolledDevice.as_view(), name="lock_enrolled_device"),
path('devices/<int:pk>/filevault_prk/', EnrolledDeviceFileVaultPRK.as_view(),
Expand Down
32 changes: 30 additions & 2 deletions zentral/contrib/mdm/api_views/enrolled_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,34 @@ class EnrolledDeviceList(ListAPIView):
filterset_fields = ('udid', 'serial_number')


class BlockEnrolledDevice(APIView):
permission_required = "mdm.change_enrolleddevice"
permission_classes = [DjangoPermissionRequired]

def post(self, request, *args, **kwargs):
enrolled_device = get_object_or_404(EnrolledDevice, pk=kwargs["pk"])
if enrolled_device.blocked_at:
return Response({"detail": "Device already blocked."}, status=status.HTTP_400_BAD_REQUEST)
enrolled_device.block()
enrolled_device.refresh_from_db()
serializer = EnrolledDeviceSerializer(enrolled_device)
return Response(serializer.data, status=status.HTTP_200_OK)


class UnblockEnrolledDevice(APIView):
permission_required = "mdm.change_enrolleddevice"
permission_classes = [DjangoPermissionRequired]

def post(self, request, *args, **kwargs):
enrolled_device = get_object_or_404(EnrolledDevice, pk=kwargs["pk"])
if not enrolled_device.blocked_at:
return Response({"detail": "Device not blocked."}, status=status.HTTP_400_BAD_REQUEST)
enrolled_device.unblock()
enrolled_device.refresh_from_db()
serializer = EnrolledDeviceSerializer(enrolled_device)
return Response(serializer.data, status=status.HTTP_200_OK)


class CreateEnrolledDeviceCommandView(APIView):
permission_required = "mdm.add_devicecommand"
permission_classes = [DjangoPermissionRequired]
Expand All @@ -42,8 +70,8 @@ def post(self, request, *args, **kwargs):
queue=True,
uuid=uuid
)
cmd_serializer = DeviceCommandSerializer(command.db_command)
return Response(cmd_serializer.data, status=status.HTTP_201_CREATED)
serializer = DeviceCommandSerializer(command.db_command)
return Response(serializer.data, status=status.HTTP_201_CREATED)


class EraseEnrolledDevice(CreateEnrolledDeviceCommandView):
Expand Down

0 comments on commit 55f9846

Please sign in to comment.