Skip to content

Commit 17c1dbe

Browse files
committed
more documentation and comments on methods.
1 parent b8c3768 commit 17c1dbe

File tree

10 files changed

+188
-40
lines changed

10 files changed

+188
-40
lines changed

commandment/enroll/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class DeviceAttributes(Enum):
2727
DeviceAttributes.DEVICE_NAME.value,
2828
DeviceAttributes.SERIAL.value,
2929
DeviceAttributes.MODEL.value,
30-
DeviceAttributes.MAC_ADDRESS_EN0.value,
30+
# DeviceAttributes.MAC_ADDRESS_EN0.value,
3131
DeviceAttributes.MEID.value,
3232
DeviceAttributes.IMEI.value,
3333
DeviceAttributes.ICCID.value,

commandment/mdm/decorators.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from functools import wraps
2+
3+
4+
def handle_error_status(func):
5+
"""This decorator looks at the request for an Error status, then handles the error accordingly:
6+
7+
"""
8+
@wraps(func)
9+
def handler(*args, **kwargs):
10+
return func(*args, **kwargs)
11+
return handler
12+
13+

commandment/mdm/handlers.py

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from binascii import hexlify
2-
import uuid
32

43
from cryptography import x509
54
from cryptography.hazmat.backends import default_backend
@@ -22,11 +21,11 @@
2221

2322

2423
@command_router.route('DeviceInformation')
25-
def ack_device_information(command: DBCommand, device: Device, response: dict):
26-
"""Acknowledge a ``DeviceInformation`` response.
24+
def ack_device_information(request: DBCommand, device: Device, response: dict):
25+
"""Acknowledge a response to the ``DeviceInformation`` command.
2726
2827
Args:
29-
request (DeviceInformation): The command instance that generated this response.
28+
request (Command): An instance of the command that prompted the device to come back with this request.
3029
device (Device): The device responding to the command.
3130
response (dict): The raw response dictionary, de-serialized from plist.
3231
Returns:
@@ -135,6 +134,16 @@ def ack_profile_list(request: DBCommand, device: Device, response: dict):
135134

136135
@command_router.route('CertificateList')
137136
def ack_certificate_list(request: DBCommand, device: Device, response: dict):
137+
"""Acknowledge a response to the ``CertificateList`` command.
138+
139+
Args:
140+
request (Command): An instance of the command that prompted the device to come back with this request.
141+
device (Device): The database model of the device responding.
142+
response (dict): The response plist data, as a dictionary.
143+
144+
Returns:
145+
void: Nothing is returned but this behaviour is subject to change.
146+
"""
138147
for c in device.installed_certificates:
139148
db.session.delete(c)
140149

@@ -162,7 +171,7 @@ def ack_certificate_list(request: DBCommand, device: Device, response: dict):
162171

163172
@command_router.route('InstalledApplicationList')
164173
def ack_installed_app_list(request: DBCommand, device: Device, response: dict):
165-
"""Acknowledge a response to ``InstalledApplicationList``.
174+
"""Acknowledge a response to the ``InstalledApplicationList`` command.
166175
167176
.. note:: There is no composite key which can uniquely identify an item in the installed applications list.
168177
Some applications may not contain any version information at all. For this reason, the entire list of installed
@@ -279,39 +288,63 @@ def ack_install_application(request: DBCommand, device: Device, response: dict):
279288
@command_router.route('ManagedApplicationList')
280289
def ack_managed_application_list(request: DBCommand, device: Device, response: dict):
281290
"""Acknowledge a response to `ManagedApplicationList`."""
282-
for bundle_id, status in response['ManagedApplicationList'].items():
283-
try:
284-
ma = db.session.query(ManagedApplication).filter(
285-
Device.id == device.id,
286-
ManagedApplication.bundle_id == bundle_id
287-
).one()
288-
except NoResultFound:
289-
ma = ManagedApplication(bundle_id=bundle_id, device=device)
291+
if response.get('Status', None) == 'Error':
292+
pass
293+
else:
294+
for bundle_id, status in response['ManagedApplicationList'].items():
295+
try:
296+
ma = db.session.query(ManagedApplication).filter(
297+
Device.id == device.id,
298+
ManagedApplication.bundle_id == bundle_id
299+
).one()
300+
except NoResultFound:
301+
ma = ManagedApplication(bundle_id=bundle_id, device=device)
302+
303+
ma.status = ManagedAppStatus(status['Status'])
304+
ma.external_version_id = status.get('ExternalVersionIdentifier', None) # Does not exist in iOS 11.3.1
305+
ma.has_configuration = status['HasConfiguration']
306+
ma.has_feedback = status['HasFeedback']
307+
ma.is_validated = status['IsValidated']
308+
ma.management_flags = status['ManagementFlags']
290309

291-
ma.status = ManagedAppStatus(status['Status'])
292-
ma.external_version_id = status.get('ExternalVersionIdentifier', None) # Does not exist in iOS 11.3.1
293-
ma.has_configuration = status['HasConfiguration']
294-
ma.has_feedback = status['HasFeedback']
295-
ma.is_validated = status['IsValidated']
296-
ma.management_flags = status['ManagementFlags']
310+
db.session.add(ma)
297311

298-
db.session.add(ma)
312+
db.session.commit()
299313

300-
db.session.commit()
314+
for tag in device.tags:
315+
for app in tag.applications:
316+
# TODO: need to check with new versions being available. This is very primitive.
317+
if app.bundle_id in response['ManagedApplicationList'].keys():
318+
continue
301319

302-
for tag in device.tags:
303-
for app in tag.applications:
304-
# TODO: need to check with new versions being available. This is very primitive.
305-
if app.bundle_id in response['ManagedApplicationList'].keys():
306-
continue
320+
c = commands.InstallApplication(application=app)
321+
dbc = DBCommand.from_model(c)
322+
dbc.device = device
323+
db.session.add(dbc)
307324

308-
c = commands.InstallApplication(application=app)
309-
dbc = DBCommand.from_model(c)
310-
dbc.device = device
311-
db.session.add(dbc)
325+
ma = ManagedApplication(device=device, application=app, ia_command=dbc, status=ManagedAppStatus.Queued)
326+
db.session.add(ma)
312327

313-
ma = ManagedApplication(device=device, application=app, ia_command=dbc, status=ManagedAppStatus.Queued)
314-
db.session.add(ma)
328+
db.session.commit()
315329

316-
db.session.commit()
317330

331+
@command_router.route('RestartDevice')
332+
def ack_restart_device(request: DBCommand, device: Device, response: dict):
333+
"""Acknowledge a response to `RestartDevice`.
334+
335+
On macOS 10.13.6, the MDM client comes back with an `Idle` check in upon restart as part of launchd starting up.
336+
This happens BEFORE the loginwindow (at about 40% of the progress bar at startup). This is the same Power-on
337+
behaviour.
338+
"""
339+
pass
340+
341+
342+
@command_router.route('ShutDownDevice')
343+
def ack_restart_device(request: DBCommand, device: Device, response: dict):
344+
"""Acknowledge a response to `ShutDownDevice`.
345+
346+
On macOS 10.13.6, the MDM client comes back with an `Idle` check in upon restart as part of launchd starting up.
347+
This happens BEFORE the loginwindow (at about 40% of the progress bar at startup). This is the same Power-on
348+
behaviour.
349+
"""
350+
pass

commandment/mdm/routers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class CommandRouter(object):
1919
that was registered for the RequestType associated with that command. The handler is then called with the specific
2020
instance of the command that generated the response, and an instance of the device that is making the request to
2121
the MDM endpoint.
22+
23+
Not handling the error status here allows handlers to freely interpret the error conditions of each response, which
24+
is generally a better approach as some errors are command specific.
2225
2326
Args:
2427
app (app): The flask application or blueprint instance

doc/developer/microservices.rst

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
Microservices Architecture
2+
==========================
3+
4+
MDM only has certain limitations which means that microservices have only a limited range of definition in terms of where
5+
dependent services can live.
6+
7+
Here's some ideas for services
8+
9+
10+
DEP Scanner + Default Profiler
11+
------------------------------
12+
13+
- Some process needs to scan/sync DEP
14+
- This is a good point in time to evaluate which DEP profile should be assigned to the devices as they come in.
15+
- If there was a rules based evaluation of DEP profile assignment, that could also happen here.
16+
- Manual DEP assignment does NOT have to live here, because it is performed imperatively against collections of objects.
17+
- This process can create new device records when it finds new DEP records. These can exist in a "pre-enrolled" state.
18+
19+
20+
APNS Pusher
21+
-----------
22+
23+
- Most MDM systems have some sort of Queue monitor/APNS push watcher.
24+
- After a certain amount of time, devices with >0 commands to send are evaluated.
25+
- Some commands are imperative and you would expect them to happen almost immediately (Shutdown, Restart). with exception
26+
to device collections larger than 100, where the push may take some time.
27+
- Some commands are expected to happen in good time (InstallProfile, InstallApplication).
28+
29+
30+
Inventory
31+
---------
32+
33+
- End users expect REASONABLY recent device inventory.
34+
- Some process needs to Queue inventory commands at a refresh interval, but not queue all devices at once.
35+
- It must also not queue commands if they are already queued.
36+
- It must also not queue commands for recently refreshed inventory.
37+
38+
39+
Profiles
40+
--------
41+
42+
- Try not to introspect profile payload structure because it can literally be anything almost.
43+
- Examine desired profiles vs installed profiles and create a command for it.
44+
45+
46+
Applications
47+
------------
48+
49+
- Same theoretical application as PRofiles but with a different object type.
50+
51+
Calculated Groups (Classifier)
52+
------------------------------
53+
54+
- Isolate a sub-population of devices by attribute predicates.
55+
- Many cloud providers de-prioritise the calculation of these groups in order to reduce impact, but this also results in
56+
sluggish feedback.
57+
- Tactics for speeding up or lessening impact of calculated groups:
58+
- Do not recalculate if inventory data did not change: therefore track devices which did change in the last x duration.
59+
- Newly created groups must force a recalculation of membership immediately to provide feedback to the user.
60+
- Compound predicates are the union intersection of simple predicates, so maybe this can be exploited to lower the cost
61+
of group calculation.
62+
- Consider groupable attributes for indexing
63+
- Groups can be used to functionally identify the workflow state of a device from its pre-enrolled DEP state through
64+
DeviceConfigured into enrolled.
65+
- Pre-defined groups:
66+
- By form factor (Desktop, Tablet, Phone, ATV)
67+
- Workflow state (DEP -> AwaitConfiguration -> Enrolled -> Stale -> Unenrolled)
68+
- OS Flavour+Major Version (becomes a derivative of form factor groups)
69+
- Minor Version (becomes a derivative of major version)
70+
- Cellular v non cellular (subset of union Tablet+Phone)
71+
- Freestyle composite groups:
72+
- Nominate a pre-defined group to limit calculation results.
73+
- Enforce a predicate on that.
74+
75+
76+
Reaper
77+
------
78+
79+
- Scan age of devices and mark them as Stale if no communications recently.
80+
- Unenroll devices once they have not communicated in a long amount of time,

doc/installing/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
==========
2+
Installing
3+
==========
4+
File renamed without changes.

doc/installing/macos.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,17 @@ at :file:`/usr/local/commandment/server.key` and a certificate, located at :file
3131
For a production instance, you will require an SSL certificate issued by a 3rd party for the chosen domain. However,
3232
as this is a macOS installation guide, You may also use a self-signed certificate.
3333

34-
3534
.. note:: Creating SSL certificates is outside of the scope of this document.
3635

36+
Handy tip for extracting PEM/key pair out of a .p12 exported by Keychain Assistant::
37+
38+
openssl pkcs12 -in yourP12File.p12 -nocerts -out privatekey.pem
39+
openssl pkcs12 -in yourP12File.p12 -clcerts -nokeys -out certificate.pem
40+
41+
For converting DER to PEM::
42+
43+
openssl x509 -inform DER -outform PEM -text -in mykey.der -out mykey.pem
44+
3745

3846
Push Notification Certificate
3947
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

doc/installing/ubuntu-server.rst

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,22 @@ Symlink to **sites-enabled**::
151151

152152
sudo ln -s /etc/nginx/sites-available/commandment.conf /etc/nginx/sites-enabled/commandment.conf
153153

154-
2.3 SSL Certificate(s)
155-
^^^^^^^^^^^^^^^^^^^^^^
154+
3 SSL Certificate(s)
155+
--------------------
156156

157157
NGiNX will fail to start until we actually create an SSL certificate for this site.
158158

159-
If this is a non-public, development, sandbox environment you can use a self-signed certificate. If you ever intend to
160-
make it public (internet) facing, you need to sort out SSL certificates, maybe with LetsEncrypt.
159+
If this is a non-public, development, sandbox environment you can use a self-signed certificate.
160+
This means that you're either developing with commandment or you dont mind being restricted to a home (W)LAN network.
161+
I usually couple this with Bonjour for a hassle free testbed, hosting on something like computer.local.
161162

163+
If you ever intend to make it public (internet) facing, you need to sort out an SSL certificate and DNS name that are
164+
externally verifiable and visible. This means having a DNS name or hosting on a service which gives you a static domain
165+
name for your site AND getting a 3rd party certificate issued. The cheapest recommended option for that is to use
166+
LetsEncrypt.
167+
168+
3.1 Self-Signed Certificate(s)
169+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
162170

163171
To use self-signed certificates, first check that your hostname will be the fqdn that devices can access your machine with::
164172

ui/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
"babel-runtime": "^6.26.0",
2323
"eslint": "^5.16.0",
2424
"eslint-plugin-react": "^7.13.0",
25-
"tslint": "^5.17.0",
2625
"webpack-bundle-analyzer": "^3.3.2",
2726
"webpack-cli": "^3.1.2",
2827
"webpack-dev-server": "^3.1.4",

0 commit comments

Comments
 (0)