Skip to content

Commit

Permalink
Add MDM enrolled device list API
Browse files Browse the repository at this point in the history
  • Loading branch information
np5 committed Mar 28, 2024
1 parent c1c64ab commit db15a37
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 4 deletions.
190 changes: 190 additions & 0 deletions tests/mdm/test_api_enrolled_devices_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,196 @@ def get(self, url, include_token=True):
kwargs["HTTP_AUTHORIZATION"] = f"Token {self.api_key}"
return self.client.get(url, **kwargs)

def post(self, url, data, include_token=True):
kwargs = {}
if include_token:
kwargs["HTTP_AUTHORIZATION"] = f"Token {self.api_key}"
return self.client.post(url, data, **kwargs)

# enrolled devices

def test_enrolled_devices_unauthorized(self):
response = self.get(reverse("mdm_api:enrolled_devices"), include_token=False)
self.assertEqual(response.status_code, 401)

def test_enrolled_devices_permission_denied(self):
response = self.get(reverse("mdm_api:enrolled_devices"))
self.assertEqual(response.status_code, 403)

def test_enrolled_devices_method_not_allowed(self):
self.set_permissions("mdm.add_enrolleddevice")
response = self.post(reverse("mdm_api:enrolled_devices"), {})
self.assertEqual(response.status_code, 405)
self.assertEqual(response.json(), {'detail': 'Method "POST" not allowed.'})

def test_enrolled_devices_default_values(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.get(reverse("mdm_api:enrolled_devices"))
self.assertEqual(response.status_code, 200)
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',
'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}]
)

def test_enrolled_devices_by_serial_number(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.get(
reverse("mdm_api:enrolled_devices")
+ f"?serial_number={self.enrolled_device.serial_number}"
)
self.assertEqual(response.status_code, 200)
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',
'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}]
)

def test_enrolled_devices_by_serial_number_no_result(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.get(
reverse("mdm_api:enrolled_devices")
+ "?serial_number=yolofomo"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])

def test_enrolled_devices_by_udid(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.get(
reverse("mdm_api:enrolled_devices")
+ f"?udid={self.enrolled_device.udid}"
)
self.assertEqual(response.status_code, 200)
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',
'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}]
)

def test_enrolled_devices_by_udid_no_result(self):
self.set_permissions("mdm.view_enrolleddevice")
response = self.get(
reverse("mdm_api:enrolled_devices")
+ "?udid=00000000-0000-0000-0000-000000000000"
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])

def test_enrolled_devices_with_secrets(self):
self.enrolled_device.security_info = {"FDE_Enabled": True}
self.enrolled_device.set_filevault_prk("yolo")
self.enrolled_device.set_bootstrap_token(b"fomo")
self.enrolled_device.save()
self.set_permissions("mdm.view_enrolleddevice")
response = self.get(reverse("mdm_api:enrolled_devices"))
self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(),
[{'activation_lock_manageable': None,
'apple_silicon': None,
'awaiting_configuration': None,
'blocked_at': None,
'blueprint': None,
'bootstrap_token_escrowed': True,
'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': True,
'filevault_prk_escrowed': True,
'id': self.enrolled_device.pk,
'last_notified_at': None,
'last_seen_at': None,
'model': None,
'name': None,
'os_version': '',
'platform': 'macOS',
'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}]
)

# enrolled_device_filevault_prk

def test_enrolled_device_filevault_prk_unauthorized(self):
Expand Down
6 changes: 4 additions & 2 deletions zentral/contrib/mdm/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
BlueprintDetail, BlueprintList,
BlueprintArtifactDetail, BlueprintArtifactList,
EnterpriseAppDetail, EnterpriseAppList,
EnrolledDeviceList,
FileVaultConfigDetail, FileVaultConfigList,
ProfileDetail, ProfileList,
RecoveryPasswordConfigList, RecoveryPasswordConfigDetail,
Expand Down Expand Up @@ -37,9 +38,10 @@

path('dep/virtual_servers/<int:pk>/sync_devices/',
DEPVirtualServerSyncDevicesView.as_view(), name="dep_virtual_server_sync_devices"),
path('enrolled_devices/<int:pk>/filevault_prk/', EnrolledDeviceFileVaultPRK.as_view(),
path('devices/', EnrolledDeviceList.as_view(), name="enrolled_devices"),
path('devices/<int:pk>/filevault_prk/', EnrolledDeviceFileVaultPRK.as_view(),
name="enrolled_device_filevault_prk"),
path('enrolled_devices/<int:pk>/recovery_password/', EnrolledDeviceRecoveryPassword.as_view(),
path('devices/<int:pk>/recovery_password/', EnrolledDeviceRecoveryPassword.as_view(),
name="enrolled_device_recovery_password"),
path('software_updates/sync/',
SyncSoftwareUpdatesView.as_view(), name="sync_software_updates"),
Expand Down
13 changes: 12 additions & 1 deletion zentral/contrib/mdm/api_views/enrolled_devices.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from rest_framework.generics import ListAPIView
from rest_framework.views import APIView
from rest_framework.response import Response
from zentral.contrib.mdm.events import post_filevault_prk_viewed_event, post_recovery_password_viewed_event
from zentral.contrib.mdm.models import EnrolledDevice
from zentral.utils.drf import DjangoPermissionRequired
from zentral.contrib.mdm.serializers import EnrolledDeviceSerializer
from zentral.utils.drf import DefaultDjangoModelPermissions, DjangoPermissionRequired


class EnrolledDeviceList(ListAPIView):
queryset = EnrolledDevice.objects.all()
serializer_class = EnrolledDeviceSerializer
permission_classes = [DefaultDjangoModelPermissions]
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('udid', 'serial_number')


class EnrolledDeviceFileVaultPRK(APIView):
Expand Down
19 changes: 19 additions & 0 deletions zentral/contrib/mdm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,25 @@ def iter_enrollment_session_info(self):
for t in cursor.fetchall():
yield dict(zip(columns, t))

@property
def bootstrap_token_escrowed(self):
if self.bootstrap_token:
return True
return False

@property
def filevault_enabled(self):
try:
return self.security_info["FDE_Enabled"]
except (KeyError, TypeError):
pass

@property
def filevault_prk_escrowed(self):
if self.filevault_prk:
return True
return False


class EnrolledUser(models.Model):
enrolled_device = models.ForeignKey(EnrolledDevice, on_delete=models.CASCADE)
Expand Down
39 changes: 38 additions & 1 deletion zentral/contrib/mdm/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,50 @@
from .artifacts import update_blueprint_serialized_artifacts
from .models import (Artifact, ArtifactVersion, ArtifactVersionTag,
Blueprint, BlueprintArtifact, BlueprintArtifactTag,
EnterpriseApp, FileVaultConfig,
EnrolledDevice, EnterpriseApp, FileVaultConfig,
Platform, Profile,
RecoveryPasswordConfig,
SoftwareUpdateEnforcement)
from .payloads import get_configuration_profile_info


class EnrolledDeviceSerializer(serializers.ModelSerializer):
os_version = serializers.CharField(source="current_os_version")
build_version = serializers.CharField(source="current_build_version")

class Meta:
model = EnrolledDevice
fields = (
"id",
"udid",
"serial_number",
"name",
"model",
"platform",
"os_version",
"build_version",
"apple_silicon",
"cert_not_valid_after",
"blueprint",
"awaiting_configuration",
"declarative_management",
"dep_enrollment",
"user_enrollment",
"user_approved_enrollment",
"supervised",
"bootstrap_token_escrowed",
"filevault_enabled",
"filevault_prk_escrowed",
"activation_lock_manageable",
"last_seen_at",
"last_notified_at",
"checkout_at",
"blocked_at",
"created_at",
"updated_at",
)


class ArtifactSerializer(serializers.ModelSerializer):
class Meta:
model = Artifact
Expand Down

0 comments on commit db15a37

Please sign in to comment.