From 06bdfb7add329be361b9ab85317f025c8f7de1d9 Mon Sep 17 00:00:00 2001 From: np5 Date: Sat, 5 Aug 2023 12:23:47 +0000 Subject: [PATCH] Add MDM recovery password config Terraform export (#776) --- tests/mdm/test_setup_index.py | 22 +++++++++++++++---- tests/mdm/test_terraform.py | 37 ++++++++++++++++++++++++++++++-- zentral/contrib/mdm/terraform.py | 16 +++++++++++++- zentral/utils/terraform.py | 15 +++++++++---- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/tests/mdm/test_setup_index.py b/tests/mdm/test_setup_index.py index fd77636fda..d327271717 100644 --- a/tests/mdm/test_setup_index.py +++ b/tests/mdm/test_setup_index.py @@ -8,7 +8,8 @@ from django.urls import reverse from django.utils.crypto import get_random_string from accounts.models import User -from .utils import force_artifact, force_blueprint, force_blueprint_artifact, force_filevault_config +from .utils import (force_artifact, force_blueprint, force_blueprint_artifact, + force_filevault_config, force_recovery_password_config) @override_settings(STATICFILES_STORAGE='django.contrib.staticfiles.storage.StaticFilesStorage') @@ -83,7 +84,9 @@ def test_terraform_export(self): self._login("mdm.view_blueprint") fv_config1 = force_filevault_config() fv_config2 = force_filevault_config() - blueprint = force_blueprint(filevault_config=fv_config1) + rp_config1 = force_recovery_password_config() + rp_config2 = force_recovery_password_config() + blueprint = force_blueprint(filevault_config=fv_config1, recovery_password_config=rp_config1) required_artifact, (required_profile_av,) = force_artifact() rprofile = required_profile_av.profile rprofile_filename = f"{required_artifact.name.lower()}_{rprofile.pk}_v1.mobileconfig" @@ -106,12 +109,23 @@ def test_terraform_export(self): f' escrow_location_display_name = "{fv_config2.escrow_location_display_name}"\n' '}\n\n' ) + with zf.open("mdm_recovery_password_configs.tf") as fctf: + self.assertEqual( + fctf.read().decode("utf-8"), + f'resource "zentral_mdm_recovery_password_config" "recoverypasswordconfig{rp_config1.pk}" {{\n' + f' name = "{rp_config1.name}"\n' + '}\n\n' + f'resource "zentral_mdm_recovery_password_config" "recoverypasswordconfig{rp_config2.pk}" {{\n' + f' name = "{rp_config2.name}"\n' + '}\n\n' + ) with zf.open("mdm_blueprints.tf") as btf: self.assertEqual( btf.read().decode("utf-8"), f'resource "zentral_mdm_blueprint" "blueprint{blueprint.pk}" {{\n' - f' name = "{blueprint.name}"\n' - f' filevault_config_id = zentral_mdm_filevault_config.filevaultconfig{fv_config1.pk}.id\n' + f' name = "{blueprint.name}"\n' + f' filevault_config_id = zentral_mdm_filevault_config.filevaultconfig{fv_config1.pk}.id\n' + f' recovery_password_config_id = zentral_mdm_recovery_password_config.recoverypasswordconfig{rp_config1.pk}.id\n' # NOQA '}\n\n' f'resource "zentral_mdm_blueprint_artifact" "blueprintartifact{blueprint_artifact.pk}" {{\n' f' blueprint_id = zentral_mdm_blueprint.blueprint{blueprint.pk}.id\n' diff --git a/tests/mdm/test_terraform.py b/tests/mdm/test_terraform.py index 165fe19325..a18aae2e4b 100644 --- a/tests/mdm/test_terraform.py +++ b/tests/mdm/test_terraform.py @@ -5,8 +5,10 @@ from zentral.contrib.mdm.terraform import (ArtifactResource, BlueprintResource, BlueprintArtifactResource, FileVaultConfigResource, - ProfileResource) -from .utils import force_artifact, force_blueprint, force_blueprint_artifact, force_filevault_config + ProfileResource, + RecoveryPasswordConfigResource) +from .utils import (force_artifact, force_blueprint, force_blueprint_artifact, + force_filevault_config, force_recovery_password_config) class MDMTerraformTestCase(TestCase): @@ -135,6 +137,37 @@ def test_filevault_config_resource_full(self): '}' ) + # recovery password config + + def test_recovery_password_resource_defaults(self): + rp_config = force_recovery_password_config() + resource = RecoveryPasswordConfigResource(rp_config) + self.assertEqual( + resource.to_representation(), + f'resource "zentral_mdm_recovery_password_config" "recoverypasswordconfig{rp_config.pk}" {{\n' + f' name = "{rp_config.name}"\n' + '}' + ) + + def test_recovery_password_resource_full(self): + rp_config = force_recovery_password_config( + rotation_interval_days=90, + static_password="12345678", + ) + rp_config.rotate_firmware_password = True + rp_config.save() + resource = RecoveryPasswordConfigResource(rp_config) + self.assertEqual( + resource.to_representation(), + f'resource "zentral_mdm_recovery_password_config" "recoverypasswordconfig{rp_config.pk}" {{\n' + f' name = "{rp_config.name}"\n' + ' dynamic_password = false\n' + f' static_password = var.recoverypasswordconfig{rp_config.pk}_static_password\n' + ' rotation_interval_days = 90\n' + ' rotate_firmware_password = true\n' + '}' + ) + # blueprint def test_blueprint_resource_defaults(self): diff --git a/zentral/contrib/mdm/terraform.py b/zentral/contrib/mdm/terraform.py index cc9bd89b40..8b1b0c108b 100644 --- a/zentral/contrib/mdm/terraform.py +++ b/zentral/contrib/mdm/terraform.py @@ -1,4 +1,4 @@ -from .models import Artifact, Blueprint, FileVaultConfig +from .models import Artifact, Blueprint, FileVaultConfig, RecoveryPasswordConfig from zentral.contrib.inventory.terraform import TagResource from zentral.utils.terraform import BoolAttr, FileBase64Attr, IntAttr, MapAttr, RefAttr, Resource, StringAttr @@ -31,6 +31,17 @@ class FileVaultConfigResource(Resource): prk_rotation_interval_days = IntAttr(default=0) +class RecoveryPasswordConfigResource(Resource): + tf_type = "zentral_mdm_recovery_password_config" + tf_grouping_key = "mdm_recovery_password_configs" + + name = StringAttr(required=True) + dynamic_password = BoolAttr(default=True) + static_password = StringAttr(secret=True) + rotation_interval_days = IntAttr(default=0) + rotate_firmware_password = BoolAttr(default=False) + + class BlueprintResource(Resource): tf_type = "zentral_mdm_blueprint" tf_grouping_key = "mdm_blueprints" @@ -44,6 +55,7 @@ class BlueprintResource(Resource): collect_profiles = StringAttr(default=Blueprint.InventoryItemCollectionOption.NO.name, source="get_collect_profiles_display") filevault_config_id = RefAttr(FileVaultConfigResource) + recovery_password_config_id = RefAttr(RecoveryPasswordConfigResource) # TODO: deduplicate Resource @@ -119,6 +131,8 @@ def iter_resources(): yield BlueprintArtifactResource(blueprint_artifact) for filevault_config in FileVaultConfig.objects.all(): yield FileVaultConfigResource(filevault_config) + for recovery_password_config in RecoveryPasswordConfig.objects.all(): + yield RecoveryPasswordConfigResource(recovery_password_config) for artifact in Artifact.objects.prefetch_related("requires").filter(type=Artifact.Type.PROFILE): yield ArtifactResource(artifact) for artifact_version in artifact.artifactversion_set.select_related("profile").order_by("-version"): diff --git a/zentral/utils/terraform.py b/zentral/utils/terraform.py index 896be863ca..b488a5a9d6 100644 --- a/zentral/utils/terraform.py +++ b/zentral/utils/terraform.py @@ -63,7 +63,7 @@ def quote(i): class Attr: - def __init__(self, many=False, required=False, source=None, default=None, call_value=True): + def __init__(self, many=False, required=False, source=None, default=None, call_value=True, secret=False): self.many = many self.required = required self.source = source @@ -73,6 +73,7 @@ def __init__(self, many=False, required=False, source=None, default=None, call_v default = default() self.default = default self.call_value = call_value + self.secret = secret def value_representation(self, value): raise NotImplementedError @@ -89,11 +90,17 @@ def get_value(self, instance, attr_name): raw_value = raw_value() return raw_value + def get_secret_var(self, instance, attr_name): + return f"{instance._meta.model_name}{instance.pk}_{attr_name}" + def iter_representation_lines(self, instance, attr_name): value = self.get_value(instance, attr_name) if not self.required and (value is None or value == self.default): return - if self.many: + if self.secret: + secret_var = self.get_secret_var(instance, attr_name) + line = f"var.{secret_var}" + elif self.many: line = "[" line += ", ".join(self.value_representation(i) for i in value) line += "]" @@ -103,10 +110,10 @@ def iter_representation_lines(self, instance, attr_name): class StringAttr(Attr): - def __init__(self, many=False, required=False, source=None, default=None): + def __init__(self, many=False, required=False, source=None, default=None, secret=False): if not many and not required and default is None: default = "" - super().__init__(many=many, required=required, source=source, default=default) + super().__init__(many=many, required=required, source=source, default=default, secret=secret) def value_representation(self, value): if not isinstance(value, str):