Skip to content

Commit

Permalink
Fix MDM artifact dependencies
Browse files Browse the repository at this point in the history
Do not filter the artifacts in the dependency tree before calculating
what is in scope and what to install.
  • Loading branch information
np5 committed Sep 12, 2023
1 parent d645782 commit 85613a9
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 19 deletions.
75 changes: 73 additions & 2 deletions tests/mdm/test_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"])
Expand All @@ -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
Expand All @@ -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")
Expand Down
43 changes: 27 additions & 16 deletions zentral/contrib/mdm/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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()
Expand All @@ -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"]:
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)}
Expand Down
2 changes: 1 addition & 1 deletion zentral/contrib/mdm/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 85613a9

Please sign in to comment.