Skip to content

Commit

Permalink
Protect FileVaultConfig from deletion
Browse files Browse the repository at this point in the history
… when the FileVaultConfig is used in a Blueprint.
  • Loading branch information
np5 committed Aug 1, 2023
1 parent f451f8b commit 1fb8bd3
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 7 deletions.
10 changes: 9 additions & 1 deletion tests/mdm/test_api_filevault_configs_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from accounts.models import APIToken, User
from zentral.contrib.mdm.models import FileVaultConfig
from zentral.core.events.base import AuditEvent
from .utils import force_filevault_config
from .utils import force_blueprint, force_filevault_config


@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
Expand Down Expand Up @@ -335,6 +335,14 @@ def test_delete_filevault_config_permission_denied(self):
response = self.delete(reverse("mdm_api:filevault_config", args=(fv_config.pk,)))
self.assertEqual(response.status_code, 403)

def test_delete_filevault_config_cannot_be_deleted(self):
fv_config = force_filevault_config()
force_blueprint(filevault_config=fv_config)
self.set_permissions("mdm.delete_filevaultconfig")
response = self.delete(reverse("mdm_api:filevault_config", args=(fv_config.pk,)))
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json(), ["This FileVault configuration cannot be deleted"])

@patch("zentral.core.queues.backends.kombu.EventQueues.post_event")
def test_delete_filevault_config(self, post_event):
fv_config = force_filevault_config()
Expand Down
38 changes: 36 additions & 2 deletions tests/mdm/test_management_filevault_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.utils.crypto import get_random_string
from accounts.models import User
from zentral.core.events.base import AuditEvent
from .utils import force_filevault_config
from .utils import force_blueprint, force_filevault_config


@override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage')
Expand Down Expand Up @@ -51,13 +51,30 @@ def test_filevault_configurations_permission_denied(self):
response = self.client.get(reverse("mdm:filevault_configs"))
self.assertEqual(response.status_code, 403)

def test_filevault_configurations(self):
def test_filevault_configurations_no_links(self):
fv_config = force_filevault_config()
self._login("mdm.view_filevaultconfig")
response = self.client.get(reverse("mdm:filevault_configs"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "mdm/filevaultconfig_list.html")
self.assertContains(response, fv_config.name)
self.assertNotContains(response, reverse("mdm:update_filevault_config", args=(fv_config.pk,)))
self.assertNotContains(response, reverse("mdm:delete_filevault_config", args=(fv_config.pk,)))

def test_filevault_configurations_all_links(self):
fv_config1 = force_filevault_config()
force_blueprint(filevault_config=fv_config1)
fv_config2 = force_filevault_config()
self._login("mdm.view_filevaultconfig", "mdm.change_filevaultconfig", "mdm.delete_filevaultconfig")
response = self.client.get(reverse("mdm:filevault_configs"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "mdm/filevaultconfig_list.html")
self.assertContains(response, fv_config1.name)
self.assertContains(response, fv_config2.name)
self.assertContains(response, reverse("mdm:update_filevault_config", args=(fv_config1.pk,)))
self.assertNotContains(response, reverse("mdm:delete_filevault_config", args=(fv_config1.pk,)))
self.assertContains(response, reverse("mdm:update_filevault_config", args=(fv_config2.pk,)))
self.assertContains(response, reverse("mdm:delete_filevault_config", args=(fv_config2.pk,)))

# create FileVault configuration

Expand Down Expand Up @@ -190,6 +207,16 @@ def test_filevault_configuration_get_no_perm_no_delete_link(self):
self.assertContains(response, fv_config.name)
self.assertNotContains(response, reverse("mdm:delete_filevault_config", args=(fv_config.pk,)))

def test_filevault_configuration_get_cannot_be_deleted_no_delete_link(self):
fv_config = force_filevault_config()
force_blueprint(filevault_config=fv_config)
self._login("mdm.view_filevaultconfig", "mdm.delete_filevaultconfig")
response = self.client.get(reverse("mdm:filevault_config", args=(fv_config.pk,)))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "mdm/filevaultconfig_detail.html")
self.assertContains(response, fv_config.name)
self.assertNotContains(response, reverse("mdm:delete_filevault_config", args=(fv_config.pk,)))

# update FileVault configuration

def test_update_filevault_configuration_redirect(self):
Expand Down Expand Up @@ -284,6 +311,13 @@ def test_delete_filevault_configuration_permission_denied(self):
response = self.client.get(reverse("mdm:delete_filevault_config", args=(fv_config.pk,)))
self.assertEqual(response.status_code, 403)

def test_delete_filevault_configuration_404(self):
fv_config = force_filevault_config()
force_blueprint(filevault_config=fv_config)
self._login("mdm.delete_filevaultconfig")
response = self.client.get(reverse("mdm:delete_filevault_config", args=(fv_config.pk,)))
self.assertEqual(response.status_code, 404)

def test_delete_filevault_configuration_get(self):
fv_config = force_filevault_config()
self._login("mdm.delete_filevaultconfig")
Expand Down
7 changes: 7 additions & 0 deletions zentral/contrib/mdm/api_views/filevault_configs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from rest_framework.exceptions import ValidationError
from zentral.utils.drf import ListCreateAPIViewWithAudit, RetrieveUpdateDestroyAPIViewWithAudit
from zentral.contrib.mdm.models import FileVaultConfig
from zentral.contrib.mdm.serializers import FileVaultConfigSerializer
Expand All @@ -18,3 +19,9 @@ class FileVaultConfigDetail(RetrieveUpdateDestroyAPIViewWithAudit):
"""
queryset = FileVaultConfig.objects.all()
serializer_class = FileVaultConfigSerializer

def perform_destroy(self, instance):
if not instance.can_be_deleted():
raise ValidationError('This FileVault configuration cannot be deleted')
else:
return super().perform_destroy(instance)
10 changes: 10 additions & 0 deletions zentral/contrib/mdm/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ def rewrap_secrets(self):
# FileVault


class FileVaultConfigManager(models.Manager):
def can_be_deleted(self):
return self.annotate(bp_count=Count("blueprint")).filter(bp_count=0)


class FileVaultConfig(models.Model):
name = models.CharField(max_length=256, unique=True)
escrow_location_display_name = models.CharField(
Expand Down Expand Up @@ -133,6 +138,8 @@ class FileVaultConfig(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

objects = FileVaultConfigManager()

class Meta:
ordering = ("name",)
verbose_name = "filevault config"
Expand All @@ -150,6 +157,9 @@ def uuid(self):
f"{self.destroy_key_on_standby}|{self.prk_rotation_interval_days}".encode("utf-8"))
return uuid.UUID(hex=h.hexdigest())

def can_be_deleted(self):
return FileVaultConfig.objects.can_be_deleted().filter(pk=self.pk).exists()

def serialize_for_event(self, keys_only=False):
d = {"pk": self.pk, "name": self.name}
if keys_only:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ <h3>{{ object }}</h3>
Edit
</a>
{% endif %}
{% if perms.mdm.delete_filevaultconfig %}
{% if perms.mdm.delete_filevaultconfig and object.can_be_deleted %}
<a class="btn btn-danger"
href="{% url 'mdm:delete_filevault_config' object.pk %}">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ <h3>{{ page_obj.paginator.count }} FileVault configuration{{ page_obj.paginator.
<span class="glyphicon glyphicon-edit" aria-hidden="true"></span>
</a>
{% endif %}
{% if perms.mdm.delete_filevaultconfig %}
{% if perms.mdm.delete_filevaultconfig and filevault_config.can_be_deleted %}
<a href="{% url 'mdm:delete_filevault_config' filevault_config.pk %}" class="btn btn-xs btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
</a>
Expand Down
8 changes: 6 additions & 2 deletions zentral/contrib/mdm/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -974,9 +974,11 @@ def form_valid(self, form):

class DeleteBlueprintView(PermissionRequiredMixin, DeleteViewWithAudit):
permission_required = "mdm.delete_blueprint"
queryset = Blueprint.objects.can_be_deleted()
success_url = reverse_lazy("mdm:blueprints")

def get_queryset(self):
return Blueprint.objects.can_be_deleted()


# FileVault Configurations

Expand Down Expand Up @@ -1012,9 +1014,11 @@ class UpdateFileVaultConfigView(PermissionRequiredMixin, UpdateViewWithAudit):

class DeleteFileVaultConfigView(PermissionRequiredMixin, DeleteViewWithAudit):
permission_required = "mdm.delete_filevaultconfig"
model = FileVaultConfig
success_url = reverse_lazy("mdm:filevault_configs")

def get_queryset(self):
return FileVaultConfig.objects.can_be_deleted()


# SCEP Configurations

Expand Down

0 comments on commit 1fb8bd3

Please sign in to comment.