Skip to content

Instantly share code, notes, and snippets.

@jessepeterson
Last active November 18, 2024 13:28
Show Gist options
  • Save jessepeterson/5a633f627bfc23f74153add89aee07f1 to your computer and use it in GitHub Desktop.
Save jessepeterson/5a633f627bfc23f74153add89aee07f1 to your computer and use it in GitHub Desktop.
Toy Declarative Management Flask server
/*.status.json
from flask import Flask, abort, request
from uuid import uuid5, NAMESPACE_DNS
import json
import datetime
import plistlib
import uuid
app = Flask(__name__)
def dict_with_computed_token_key(in_dict, key_name):
out_dict = in_dict.copy()
out_dict[key_name] = str(uuid5(NAMESPACE_DNS, json.dumps(in_dict)))
return out_dict
class Declaration(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.assert_check()
def assert_check(self):
for k in ["Type", "Identifier", "Payload"]:
assert k in self, "missing " + k
def dict_with_computed_token(self):
"""Generate a copy of ourselves with a computed ServerToken"""
return dict_with_computed_token_key(self, "ServerToken")
def dict_with_id_token(self):
"""Return a dict with (just) our Identity and computed ServerToken"""
self_copy = self.dict_with_computed_token()
return {
"Identifier": self_copy["Identifier"],
"ServerToken": self_copy["ServerToken"],
}
def get_type(self):
"""Returns the "bare" declaration type (i.e. 'activation', 'configuration')"""
return self["Type"].split(".")[2]
class DeclarationItems:
def __init__(self, decl_list):
self.decl_list = decl_list
def dict_with_computed_token(self):
decls = {
"Activations": [],
"Assets": [],
"Configurations": [],
"Management": [],
}
for decl in self.decl_list:
decl_type = decl.get_type()
if decl_type == "activation":
decls["Activations"] += [decl.dict_with_id_token()]
elif decl_type == "configuration":
decls["Configurations"] += [decl.dict_with_id_token()]
elif decl_type == "asset":
decls["Assets"] += [decl.dict_with_id_token()]
elif decl_type == "management":
decls["Management"] += [decl.dict_with_id_token()]
resp_dict = {
"Declarations": decls,
}
return dict_with_computed_token_key(resp_dict, "DeclarationsToken")
def get_declaration(self, id, decl_type):
for decl in self.decl_list:
if id == decl["Identifier"]:
if decl_type != decl.get_type():
raise Exception("mismatched type")
return decl
raise KeyError("declaration not found")
management_test = Declaration(
Identifier="0FCD2F56-D5BC-48EA-B98D-E0CCC0C6F9E0",
Type="com.apple.configuration.management.test",
Payload={
"Echo": "Hello, world!",
"ReturnStatus": "Installed",
},
)
status_items = [
"device.identifier.serial-number",
"device.identifier.udid",
"device.model.family",
"device.model.identifier",
"device.model.marketing-name",
"device.operating-system.build-version",
"device.operating-system.family",
"device.operating-system.marketing-name",
"device.operating-system.version",
"management.client-capabilities",
"management.declarations",
"passcode.is-compliant",
"passcode.is-present",
"test.array-value",
"test.boolean-value",
"test.dictionary-value",
"test.error-value",
"test.integer-value",
"test.real-value",
"test.string-value",
]
status_dicts = [{"Name": x} for x in status_items]
subscriptions = Declaration(
Identifier="85B5130A-4D0D-462B-AA0D-0C3B6630E5AA",
Type="com.apple.configuration.management.status-subscriptions",
Payload={
"StatusItems": status_dicts,
},
)
org_info = Declaration(
Identifier="AF0E633E-7ADB-4B2A-A48C-1B20AA271D08",
Type="com.apple.management.organization-info",
Payload={
"Email": "email@example.com",
"Name": "Acme Inc",
"URL": "https://www.example.com",
},
)
legacy_prof_1 = Declaration(
Identifier="4D6F8451-C089-4E65-A615-7C6EFF154F72",
Type="com.apple.configuration.legacy",
Payload={
"ProfileURL": "https://gist.githubusercontent.com/jessepeterson/5a633f627bfc23f74153add89aee07f1/raw/f27458e05fd01b8ff9e7872a54aa2d543131afaa/cert-profile.mobileconfig"
},
)
activation_1 = Declaration(
Identifier="49F6F16A-70EB-4A89-B092-465FAEC5E550",
Type="com.apple.activation.simple",
Payload={
"StandardConfigurations": [
subscriptions["Identifier"],
management_test["Identifier"],
legacy_prof_1["Identifier"],
],
},
)
# the shared set of declarations
decl_items = DeclarationItems(
[
management_test,
subscriptions,
activation_1,
org_info,
legacy_prof_1,
]
)
@app.route("/declaration/<dtype>/<identifier>")
def declaration(dtype, identifier):
decl = decl_items.get_declaration(identifier, dtype)
return decl.dict_with_computed_token()
@app.route("/declaration-items")
def declaration_items():
return decl_items.dict_with_computed_token()
@app.route("/status", methods=["PUT"])
def status():
# fetch the enrollment ID from NanoMDM-provided header
enrollment_id = request.headers.get("X-Enrollment-ID")
strdate = datetime.datetime.now().strftime("%Y%m%d-%H%M%S.%f")
status_data = request.get_data()
with open(f"{enrollment_id}.{strdate}.status.json", "wb") as f:
f.write(status_data)
print(status_data.decode("utf-8"))
return ""
computed_at = datetime.datetime.now()
@app.route("/tokens")
def tokens():
decls_computed = decl_items.dict_with_computed_token()
return {
"SyncTokens": {
"DeclarationsToken": decls_computed["DeclarationsToken"],
"Timestamp": computed_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
}
@app.route("/command")
def command():
return plistlib.dumps(
{
"Command": {
"RequestType": "DeclarativeManagement",
"Data": json.dumps(tokens()).encode("utf-8"),
},
"CommandUUID": str(uuid.uuid4()),
}
)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadCertificateFileName</key>
<string>server</string>
<key>PayloadContent</key>
<data>
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWIy
Z0F3SUJBZ0lKQUxuZTNaeGRyR1lPTUEwR0NTcUdTSWIzRFFFQkN3
VUFNQm94R0RBV0JnTlYKQkFNTUQyMWtiUzVsZUdGdGNHeGxMbTl5
WnpBZUZ3MHlNVEEyTVRFd016VTFNVEJhRncweU1qQTJNVEV3TXpV
MQpNVEJhTUJveEdEQVdCZ05WQkFNTUQyMWtiUzVsZUdGdGNHeGxM
bTl5WnpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCCkJRQURnZ0VQQURD
Q0FRb0NnZ0VCQUx4NElxNjdmampxUDNpczRac3M3TGJrRjZpTmhh
TTl5ZDVrZzhJdjhBcVEKV1BEVFhCVndNNWtBMkpOcm9Bc0VwS3Vp
OVQ4UGY4bW44UWw0cENCVzR2Zmhxb3lyWklWZHpjanNXejUrMHlC
UgpGemdBZ1JTcUpTZUZMMnpyRFFKUHJKRlI4T2Z6bnY0Z1JNeXR0
NXp4RmFTRmpxSjI3QlorTkZrUjM1MkVQM3kzCnkwWkNiSHF2R1hG
NUF0OVVUTnVrcEZ1TzRManBNeE1QY0ljQVNSNUQyWG9EZTVieVc5
L256N09sNHdqbXJ0RUIKeDVFZmtST2JvMXJYZWYvVlAzMEZhdE9S
UkFnbmhGMmsrSXFHbnJDMm50NjdKYnJyK1VCYm5YdUtEV244Z1NT
egpkN2tWUTV3WnA2UXI0N0NGYURHT0RwZlMrYUN1VWJ2OWxhS3c1
TUhhcTgwQ0F3RUFBYU1lTUJ3d0dnWURWUjBSCkJCTXdFWUlQYldS
dExtVjRZVzF3YkdVdWIzSm5NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0
SUJBUUJqeHRDaXFSOW4KQnEyQ2xIZ3ltMDBVWWlrVFBlRzhpcGpZ
N1RRcWl6UGpZSllra01sazk0OEtId0JqLzFoZklqaEV6QS9XZWxx
Tgo0S1htc2RSUkcycHhLb3pUMXFhT0VJODEyeDdBOEV6MnJacnF6
Y2xPRmR4Yjc5SjBXbzFYMEhQeTdmTStHRUU3CnIwVWpBc0R6UHVL
T1NpNU45OFpSM2Jpem53TXQ0U0l0TFhieGI5TUo0bktrbW1Lc2t6
SUVMQW9OUFNWWnR0cTMKaUdjeS9iNk1BU1JFSEl6aVlYYWFYWlZT
TnB1bDVQTGp5cjgxUWpyUDJJRmwwd1FjV0VybngrTTMzTnBzQS9t
agoreTIyWFdkQmVpQ1lXdVV4ZFJ1M2h4ckVEcU9kUTZ2R0R4MWhR
UGZaSEc2TnFlWno0cTVzVGNUT0YvVUczRCswClBGdStvWmFjSmVS
NgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
</data>
<key>PayloadDisplayName</key>
<string>Certificate</string>
<key>PayloadIdentifier</key>
<string>com.apple.security.pem</string>
<key>PayloadType</key>
<string>com.apple.security.pem</string>
<key>PayloadUUID</key>
<string>FA2F630D-BA1C-4F04-9A41-705B407EB62D</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadIdentifier</key>
<string>cert-profile.50FEF50E-EBD8-4225-9F91-4D8569F1AAA7</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>7419D879-5851-40E4-A7A3-767DB9345210</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment