From 85613a99b9e60bfa62d35b36b4b14ec2eaeb614c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Falconnier?= Date: Tue, 12 Sep 2023 20:43:23 +0200 Subject: [PATCH] Fix MDM artifact dependencies Do not filter the artifacts in the dependency tree before calculating what is in scope and what to install. --- tests/mdm/test_artifacts.py | 75 ++++++++++++++++++++++++++++- zentral/contrib/mdm/artifacts.py | 43 +++++++++++------ zentral/contrib/mdm/declarations.py | 2 +- 3 files changed, 101 insertions(+), 19 deletions(-) diff --git a/tests/mdm/test_artifacts.py b/tests/mdm/test_artifacts.py index 292ce7dc1c..26b1876009 100644 --- a/tests/mdm/test_artifacts.py +++ b/tests/mdm/test_artifacts.py @@ -293,6 +293,20 @@ def test_blueprint_install_device_profile_requires(self): self._force_blueprint_artifact(requires=required_artifact) self.assertEqual(Target(self.enrolled_device).next_to_install(), av) + def test_blueprint_enterprise_app_requires_installed_profile(self): + ra, (ra_av,) = self._force_artifact( + artifact_type=Artifact.Type.PROFILE, + version_count=1, + ) + target = Target(self.enrolled_device) + target.update_target_artifact(ra_av, TargetArtifact.Status.INSTALLED) + _, a, (av,) = self._force_blueprint_artifact( + artifact_type=Artifact.Type.ENTERPRISE_APP, + version_count=1, + requires=ra, + ) + self.assertEqual(target.next_to_install(included_types=(Artifact.Type.ENTERPRISE_APP,)), av) + def test_blueprint_install_device_profile_awaiting_configuration_false(self): _, _, artifact_versions = self._force_blueprint_artifact() _, _, artifact_versions_2 = self._force_blueprint_artifact(install_during_setup_assistant=True) @@ -729,8 +743,23 @@ def test_empty_blueprint_remove_one_store_app_exclude_profile(self): # activation - def test_device_activation_store_app_not_included(self): + def test_device_activation_enterprise_app_not_included(self): _, profile_a, (profile_av,) = self._force_blueprint_artifact() + self._force_blueprint_artifact(artifact_type=Artifact.Type.ENTERPRISE_APP) + activation = Target(self.enrolled_device).activation + self.assertEqual(sorted(activation.keys()), ["Identifier", "Payload", "ServerToken", "Type"]) + self.assertEqual(sorted(activation["Payload"].keys()), ["StandardConfigurations"]) + scs = activation["Payload"]["StandardConfigurations"] + self.assertEqual(len(scs), 2) + self.assertIn(f"zentral.blueprint.{self.blueprint1.pk}.management-status-subscriptions", scs) + self.assertIn(f"zentral.legacy-profile.{profile_a.pk}", scs) + + def test_device_activation_required_profile_included(self): + profile_a, _ = self._force_artifact( + artifact_type=Artifact.Type.PROFILE, + version_count=1, + ) + self._force_blueprint_artifact(artifact_type=Artifact.Type.ENTERPRISE_APP, requires=profile_a) activation = Target(self.enrolled_device).activation self.assertEqual(sorted(activation.keys()), ["Identifier", "Payload", "ServerToken", "Type"]) self.assertEqual(sorted(activation["Payload"].keys()), ["StandardConfigurations"]) @@ -739,7 +768,23 @@ def test_device_activation_store_app_not_included(self): self.assertIn(f"zentral.blueprint.{self.blueprint1.pk}.management-status-subscriptions", scs) self.assertIn(f"zentral.legacy-profile.{profile_a.pk}", scs) - def test_user_declaration_items_store_app_not_included(self): + def test_device_activation_required_enterprise_app_installed_profile_included(self): + ea_a, (ea_av,) = self._force_artifact( + artifact_type=Artifact.Type.ENTERPRISE_APP, + version_count=1, + ) + _, profile_a, _ = self._force_blueprint_artifact(artifact_type=Artifact.Type.PROFILE, requires=ea_a) + target = Target(self.enrolled_device) + target.update_target_artifact(ea_av, TargetArtifact.Status.INSTALLED) + activation = target.activation + self.assertEqual(sorted(activation.keys()), ["Identifier", "Payload", "ServerToken", "Type"]) + self.assertEqual(sorted(activation["Payload"].keys()), ["StandardConfigurations"]) + scs = activation["Payload"]["StandardConfigurations"] + self.assertEqual(len(scs), 2) + self.assertIn(f"zentral.blueprint.{self.blueprint1.pk}.management-status-subscriptions", scs) + self.assertIn(f"zentral.legacy-profile.{profile_a.pk}", scs) + + def test_user_declaration_items_enterprise_app_not_included(self): _, profile_a, (profile_av,) = self._force_blueprint_artifact(channel=Channel.USER) profile_a.reinstall_on_os_update = Artifact.ReinstallOnOSUpdate.PATCH profile_a.reinstall_interval = 100000 @@ -766,6 +811,32 @@ def test_user_declaration_items_store_app_not_included(self): self.assertEqual(configurations[1]["Identifier"], f"zentral.legacy-profile.{profile_a.pk}") self.assertEqual(configurations[1]["ServerToken"], f"{profile_av.pk}.ov-13.1.0.ri-0") + def test_device_declaration_items_required_profile_included(self): + profile_a, (profile_av,) = self._force_artifact( + artifact_type=Artifact.Type.PROFILE, + version_count=1, + ) + self._force_blueprint_artifact(artifact_type=Artifact.Type.ENTERPRISE_APP, requires=profile_a) + target = Target(self.enrolled_device) + declaration_items = target.declaration_items + self.assertEqual(sorted(declaration_items.keys()), ["Declarations", "DeclarationsToken"]) + declarations = declaration_items["Declarations"] + self.assertEqual(sorted(declarations.keys()), ["Activations", "Assets", "Configurations", "Management"]) + self.assertEqual(len(declarations["Assets"]), 0) + self.assertEqual(len(declarations["Management"]), 0) + self.assertEqual( + declarations["Activations"], + [{"Identifier": target.activation["Identifier"], + "ServerToken": target.activation["ServerToken"]}], + ) + configurations = declarations["Configurations"] + 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[1]["Identifier"], f"zentral.legacy-profile.{profile_a.pk}") + self.assertEqual(configurations[1]["ServerToken"], f"{profile_av.pk}") + # update_target_artifact @patch("zentral.contrib.mdm.artifacts.datetime") diff --git a/zentral/contrib/mdm/artifacts.py b/zentral/contrib/mdm/artifacts.py index 61f8b737c3..7390309670 100644 --- a/zentral/contrib/mdm/artifacts.py +++ b/zentral/contrib/mdm/artifacts.py @@ -227,7 +227,7 @@ def _test_filtered_blueprint_item(self, item): return False - def _build_topological_sorter(self, included_types=None): + def _build_topological_sorter(self): def _add_artifact_to_topological_sorter(artifact, ts, seen_artifacts): requires = artifact["requires"] @@ -251,21 +251,18 @@ def _add_artifact_to_topological_sorter(artifact, ts, seen_artifacts): # awaiting configuration if self.awaiting_configuration and not artifact["install_during_setup_assistant"]: continue - # type - if included_types and Artifact.Type(artifact["type"]) not in included_types: - continue # common blueprint item scoping if self._test_filtered_blueprint_item(artifact): _add_artifact_to_topological_sorter(artifact, ts, seen_artifacts) ts.prepare() return ts - def _walk_artifact_versions(self, callback, included_types=None): + def _walk_artifact_versions(self, callback): if self.blueprint is None: return # iterate other the tree - ts = self._build_topological_sorter(included_types) + ts = self._build_topological_sorter() iterate = True while iterate: artifact_pks = ts.get_ready() @@ -276,8 +273,6 @@ def _walk_artifact_versions(self, callback, included_types=None): if Channel(artifact["channel"]) != self.channel: # should never happen continue - if included_types and artifact["type"] not in included_types: - continue # we have an artifact in scope stop, done = False, False for artifact_version in artifact["versions"]: @@ -370,16 +365,18 @@ def _all_to_install_pks(self, included_types=None, only_first=False): def all_to_install_callback(artifact, artifact_version): nonlocal only_first - if self._test_artifact_version_to_install(artifact, artifact_version): + if ( + (not included_types or artifact["type"] in included_types) + and self._test_artifact_version_to_install(artifact, artifact_version) + ): artifact_to_install_pks.append((artifact["pk"], artifact_version["pk"])) - # stop if returning only the first to install, and do not mark the artifact as done, - # even if, because we stop, it doesn't matter. + # stop if returning only the first to install, and do not mark the artifact as done return only_first, False else: # continue, but mark artifact as done only if present return False, self._serialized_target_artifacts.get(artifact["pk"], {}).get("present", False) - self._walk_artifact_versions(all_to_install_callback, included_types) + self._walk_artifact_versions(all_to_install_callback) return artifact_to_install_pks def all_to_install(self, included_types=None, only_first=False): @@ -394,14 +391,28 @@ def next_to_install(self, included_types=None): return self.all_to_install(included_types, only_first=True).first() @lru_cache - def all_in_scope_serialized(self, included_types=None): + def all_installed_or_to_install_serialized(self, included_types): + artifacts = [] + + def all_installed_or_to_install_callback(artifact, artifact_version): + if artifact["type"] not in included_types: + # if not the type, only mark as done if present + return False, self._serialized_target_artifacts.get(artifact["pk"], {}).get("present", False) + else: + artifacts.append((artifact, artifact_version)) + return False, True + + self._walk_artifact_versions(all_installed_or_to_install_callback) + return artifacts + + def all_in_scope_serialized(self): artifacts_in_scope = [] def all_in_scope_callback(artifact, artifact_version): artifacts_in_scope.append((artifact, artifact_version)) return False, True - self._walk_artifact_versions(all_in_scope_callback, included_types) + self._walk_artifact_versions(all_in_scope_callback) return artifacts_in_scope def next_to_remove(self, included_types=None): @@ -547,7 +558,7 @@ def activation(self): get_declaration_identifier(self.blueprint, "management-status-subscriptions"), ] } - for artifact, _ in self.all_in_scope_serialized(included_types=(Artifact.Type.PROFILE,)): + for artifact, _ in self.all_installed_or_to_install_serialized((Artifact.Type.PROFILE,)): payload["StandardConfigurations"].append(get_legacy_profile_identifier(artifact)) payload["StandardConfigurations"].sort() h = hashlib.sha1() @@ -577,7 +588,7 @@ def declaration_items(self): ], "Management": [] } - for artifact, artifact_version in self.all_in_scope_serialized(included_types=(Artifact.Type.PROFILE,)): + for artifact, artifact_version in self.all_installed_or_to_install_serialized((Artifact.Type.PROFILE,)): declarations["Configurations"].append( {"Identifier": get_legacy_profile_identifier(artifact), "ServerToken": get_legacy_profile_server_token(self, artifact, artifact_version)} diff --git a/zentral/contrib/mdm/declarations.py b/zentral/contrib/mdm/declarations.py index 96de469dab..fb92826ecf 100644 --- a/zentral/contrib/mdm/declarations.py +++ b/zentral/contrib/mdm/declarations.py @@ -118,7 +118,7 @@ def build_legacy_profile(enrollment_session, target, declaration_identifier): # artifact version artifact_pk = declaration_identifier.split(".")[-1] profile_artifact_version = profile_artifact = None - for artifact, artifact_version in target.all_in_scope_serialized(included_types=(Artifact.Type.PROFILE,)): + for artifact, artifact_version in target.all_installed_or_to_install_serialized((Artifact.Type.PROFILE,)): if artifact["pk"] == artifact_pk: profile_artifact = artifact profile_artifact_version = artifact_version