OLD | NEW |
---|---|
1 #!/usr/bin/python2.5 | 1 #!/usr/bin/python2.5 |
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """A bare-bones test server for testing cloud policy support. | 6 """A bare-bones test server for testing cloud policy support. |
7 | 7 |
8 This implements a simple cloud policy test server that can be used to test | 8 This implements a simple cloud policy test server that can be used to test |
9 chrome's device management service client. The policy information is read from | 9 chrome's device management service client. The policy information is read from |
10 from files in a directory. The files should contain policy definitions in JSON | 10 the file named device_management in the server's data directory. It should |
11 format, using the top-level dictionary as a key/value store. The format is | 11 contain a dictionary with three top-level keys. Keys "enforced" and |
12 identical to what the Linux implementation reads from /etc. Here is an example: | 12 "recommended" hold sub-dictionaries with policy definitions in JSON |
13 format as key/value stores. Their format is identical to what the Linux | |
14 implementation reads from /etc. "managed_users" holds a list of auth tokens for | |
15 which the server will claim that the user is managed. The token string | |
16 "*" indicates that all users are claimed to be managed. Here is an example: | |
13 | 17 |
14 { | 18 { |
15 "HomepageLocation" : "http://www.chromium.org" | 19 "enforced": { |
20 "HomepageLocation" : "http://www.chromium.org" | |
21 }, | |
22 "recommended": { | |
23 "JavascriptEnabled": false, | |
24 }, | |
25 "managed_users": [ | |
26 "secret123456" | |
27 ] | |
16 } | 28 } |
17 | 29 |
30 | |
18 """ | 31 """ |
19 | 32 |
33 import calendar | |
20 import cgi | 34 import cgi |
21 import logging | 35 import logging |
36 import os | |
22 import random | 37 import random |
23 import re | 38 import re |
24 import sys | 39 import sys |
40 import time | |
41 import tlslite | |
42 import tlslite.api | |
25 | 43 |
26 # The name and availability of the json module varies in python versions. | 44 # The name and availability of the json module varies in python versions. |
27 try: | 45 try: |
28 import simplejson as json | 46 import simplejson as json |
29 except ImportError: | 47 except ImportError: |
30 try: | 48 try: |
31 import json | 49 import json |
32 except ImportError: | 50 except ImportError: |
33 json = None | 51 json = None |
34 | 52 |
35 import device_management_backend_pb2 as dm | 53 import device_management_backend_pb2 as dm |
54 import cloud_policy_pb2 as cp | |
55 | |
36 | 56 |
37 class RequestHandler(object): | 57 class RequestHandler(object): |
38 """Decodes and handles device management requests from clients. | 58 """Decodes and handles device management requests from clients. |
39 | 59 |
40 The handler implements all the request parsing and protobuf message decoding | 60 The handler implements all the request parsing and protobuf message decoding |
41 and encoding. It calls back into the server to lookup, register, and | 61 and encoding. It calls back into the server to lookup, register, and |
42 unregister clients. | 62 unregister clients. |
43 """ | 63 """ |
44 | 64 |
45 def __init__(self, server, path, headers, request): | 65 def __init__(self, server, path, headers, request): |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
88 | 108 |
89 self.DumpMessage('Request', rmsg) | 109 self.DumpMessage('Request', rmsg) |
90 | 110 |
91 request_type = self.GetUniqueParam('request') | 111 request_type = self.GetUniqueParam('request') |
92 if request_type == 'register': | 112 if request_type == 'register': |
93 return self.ProcessRegister(rmsg.register_request) | 113 return self.ProcessRegister(rmsg.register_request) |
94 elif request_type == 'unregister': | 114 elif request_type == 'unregister': |
95 return self.ProcessUnregister(rmsg.unregister_request) | 115 return self.ProcessUnregister(rmsg.unregister_request) |
96 elif request_type == 'policy': | 116 elif request_type == 'policy': |
97 return self.ProcessPolicy(rmsg.policy_request) | 117 return self.ProcessPolicy(rmsg.policy_request) |
118 elif request_type == 'cloud_policy': | |
119 return self.ProcessCloudPolicyRequest(rmsg.cloud_policy_request) | |
120 elif request_type == 'managed_check': | |
121 return self.ProcessManagedCheck(rmsg.managed_check_request) | |
98 else: | 122 else: |
99 return (400, 'Invalid request parameter') | 123 return (400, 'Invalid request parameter') |
100 | 124 |
125 def CheckGoogleLogin(self): | |
126 """Extracts the GoogleLogin auth token from the HTTP request, and | |
127 returns it. Returns None if the token is not present. | |
128 """ | |
129 match = re.match('GoogleLogin auth=(\\w+)', | |
130 self._headers.getheader('Authorization', '')) | |
131 if not match: | |
132 return None | |
133 return match.group(1) | |
134 | |
135 def GetDeviceName(self): | |
136 """Returns the name for the currently authenticated device based on its | |
137 device id. | |
138 """ | |
139 return 'chromeos-' + self.GetUniqueParam('deviceid') | |
140 | |
101 def ProcessRegister(self, msg): | 141 def ProcessRegister(self, msg): |
102 """Handles a register request. | 142 """Handles a register request. |
103 | 143 |
104 Checks the query for authorization and device identifier, registers the | 144 Checks the query for authorization and device identifier, registers the |
105 device with the server and constructs a response. | 145 device with the server and constructs a response. |
106 | 146 |
107 Args: | 147 Args: |
108 msg: The DeviceRegisterRequest message received from the client. | 148 msg: The DeviceRegisterRequest message received from the client. |
109 | 149 |
110 Returns: | 150 Returns: |
111 A tuple of HTTP status code and response data to send to the client. | 151 A tuple of HTTP status code and response data to send to the client. |
112 """ | 152 """ |
113 # Check the auth token and device ID. | 153 # Check the auth token and device ID. |
114 match = re.match('GoogleLogin auth=(\\w+)', | 154 match = re.match('GoogleLogin auth=(\\w+)', |
115 self._headers.getheader('Authorization', '')) | 155 self._headers.getheader('Authorization', '')) |
Mattias Nissler (ping if slow)
2011/01/28 10:29:34
Don't need that regex check any longer, no?
gfeher
2011/01/28 13:42:10
Done.
| |
116 if not match: | 156 if not self.CheckGoogleLogin(): |
117 return (403, 'No authorization') | 157 return (403, 'No authorization') |
118 auth_token = match.group(1) | |
119 | 158 |
120 device_id = self.GetUniqueParam('deviceid') | 159 device_id = self.GetUniqueParam('deviceid') |
121 if not device_id: | 160 if not device_id: |
122 return (400, 'Missing device identifier') | 161 return (400, 'Missing device identifier') |
123 | 162 |
124 # Register the device and create a token. | 163 # Register the device and create a token. |
125 dmtoken = self._server.RegisterDevice(device_id) | 164 dmtoken = self._server.RegisterDevice(device_id) |
126 | 165 |
127 # Send back the reply. | 166 # Send back the reply. |
128 response = dm.DeviceManagementResponse() | 167 response = dm.DeviceManagementResponse() |
129 response.error = dm.DeviceManagementResponse.SUCCESS | 168 response.error = dm.DeviceManagementResponse.SUCCESS |
130 response.register_response.device_management_token = dmtoken | 169 response.register_response.device_management_token = dmtoken |
170 response.register_response.device_name = self.GetDeviceName() | |
131 | 171 |
132 self.DumpMessage('Response', response) | 172 self.DumpMessage('Response', response) |
133 | 173 |
134 return (200, response.SerializeToString()) | 174 return (200, response.SerializeToString()) |
135 | 175 |
136 def ProcessUnregister(self, msg): | 176 def ProcessUnregister(self, msg): |
137 """Handles a register request. | 177 """Handles a register request. |
138 | 178 |
139 Checks for authorization, unregisters the device and constructs the | 179 Checks for authorization, unregisters the device and constructs the |
140 response. | 180 response. |
(...skipping 14 matching lines...) Expand all Loading... | |
155 | 195 |
156 # Prepare and send the response. | 196 # Prepare and send the response. |
157 response = dm.DeviceManagementResponse() | 197 response = dm.DeviceManagementResponse() |
158 response.error = dm.DeviceManagementResponse.SUCCESS | 198 response.error = dm.DeviceManagementResponse.SUCCESS |
159 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) | 199 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) |
160 | 200 |
161 self.DumpMessage('Response', response) | 201 self.DumpMessage('Response', response) |
162 | 202 |
163 return (200, response.SerializeToString()) | 203 return (200, response.SerializeToString()) |
164 | 204 |
205 def ProcessManagedCheck(self, msg): | |
206 """Handles a 'managed check' request. | |
207 | |
208 Queries the list of managed users and responds the client if their user | |
209 is managed or not. | |
210 | |
211 Args: | |
212 msg: The ManagedCheckRequest message received from the client. | |
213 | |
214 Returns: | |
215 A tuple of HTTP status code and response data to send to the client. | |
216 """ | |
217 # Check the management token. | |
218 auth = self.CheckGoogleLogin() | |
219 if not auth: | |
220 return (403, 'No authorization') | |
221 | |
222 managed_check_response = dm.ManagedCheckResponse() | |
223 if ('*' in self._server.policy['managed_users'] or | |
224 auth in self._server.policy['managed_users']): | |
225 managed_check_response.mode = dm.ManagedCheckResponse.MANAGED; | |
226 else: | |
227 managed_check_response.mode = dm.ManagedCheckResponse.UNMANAGED; | |
228 | |
229 # Prepare and send the response. | |
230 response = dm.DeviceManagementResponse() | |
231 response.error = dm.DeviceManagementResponse.SUCCESS | |
232 response.managed_check_response.CopyFrom(managed_check_response) | |
233 | |
234 self.DumpMessage('Response', response) | |
235 | |
236 return (200, response.SerializeToString()) | |
237 | |
165 def ProcessPolicy(self, msg): | 238 def ProcessPolicy(self, msg): |
166 """Handles a policy request. | 239 """Handles a policy request. |
167 | 240 |
168 Checks for authorization, encodes the policy into protobuf representation | 241 Checks for authorization, encodes the policy into protobuf representation |
169 and constructs the repsonse. | 242 and constructs the repsonse. |
170 | 243 |
171 Args: | 244 Args: |
172 msg: The DevicePolicyRequest message received from the client. | 245 msg: The DevicePolicyRequest message received from the client. |
173 | 246 |
174 Returns: | 247 Returns: |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
207 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY | 280 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY |
208 for list_entry in value: | 281 for list_entry in value: |
209 entry_value.string_array.append(str(list_entry)) | 282 entry_value.string_array.append(str(list_entry)) |
210 entry.value.CopyFrom(entry_value) | 283 entry.value.CopyFrom(entry_value) |
211 setting.policy_value.CopyFrom(policy_value) | 284 setting.policy_value.CopyFrom(policy_value) |
212 | 285 |
213 self.DumpMessage('Response', response) | 286 self.DumpMessage('Response', response) |
214 | 287 |
215 return (200, response.SerializeToString()) | 288 return (200, response.SerializeToString()) |
216 | 289 |
290 def SetProtobufMessageField(self, group_message, field, field_value): | |
291 '''Sets a field in a protobuf message. | |
292 | |
293 Args: | |
294 group_message: The protobuf message. | |
295 field: The field of the message to set, it shuold be a member of | |
296 group_message.DESCRIPTOR.fields. | |
297 field_value: The value to set. | |
298 ''' | |
299 if field.type == field.TYPE_STRING and field.label == field.LABEL_REPEATED: | |
300 assert type(field_value) == list | |
301 list_field = group_message.__getattribute__(field.name) | |
302 for list_item in field_value: | |
303 list_field.append(list_item) | |
304 else: | |
305 # Simple cases: | |
306 if field.type == field.TYPE_BOOL: | |
307 assert type(field_value) == bool | |
308 elif field.type == field.TYPE_STRING: | |
309 assert type(field_value) == str | |
310 elif field.type == field.TYPE_INT64: | |
311 assert type(field_value) == int | |
312 else: | |
313 raise Exception('Unknown field type %s' % field.type_name) | |
314 group_message.__setattr__(field.name, field_value) | |
Mattias Nissler (ping if slow)
2011/01/28 10:29:34
Could this also handle string lists? In other word
gfeher
2011/01/28 13:42:10
I get this error when I try to use __setattr__ to
| |
315 | |
316 def ProcessCloudPolicyRequest(self, msg): | |
317 """Handles a cloud policy request. (New protocol for policy requests.) | |
318 | |
319 Checks for authorization, encodes the policy into protobuf representation, | |
320 signs it and constructs the repsonse. | |
Jakob Kummerow
2011/01/28 10:45:52
nit: response
gfeher
2011/01/28 13:42:10
Done.
| |
321 | |
322 Args: | |
323 msg: The CloudPolicyRequest message received from the client. | |
324 | |
325 Returns: | |
326 A tuple of HTTP status code and response data to send to the client. | |
327 """ | |
328 token, response = self.CheckToken() | |
329 if not token: | |
330 return response | |
331 | |
332 settings = cp.CloudPolicySettings() | |
333 | |
334 if msg.policy_scope == 'chromeos/device': | |
335 pass | |
Mattias Nissler (ping if slow)
2011/01/28 10:29:34
What's this for?
gfeher
2011/01/28 13:42:10
Forgotten stuff -> I'll add support for device and
| |
336 | |
337 for group in settings.DESCRIPTOR.fields: | |
338 # Create protobuf message for group. | |
339 group_message = eval('cp.' + group.message_type.name + '()') | |
340 # Indiactes if at least one field was set in |group_message|. | |
Jakob Kummerow
2011/01/28 10:45:52
nit: Indicates
gfeher
2011/01/28 13:42:10
Done.
| |
341 got_fields = False | |
342 # Indicates if the current group is recommended (and not enforced.) | |
343 # A group will be considered recommended if all of its present members | |
344 # are set to recommended in the policy config file. | |
345 recommended = True | |
Mattias Nissler (ping if slow)
2011/01/28 10:29:34
can't you just use a PolicyMode enum directly?
gfeher
2011/01/28 13:42:10
Done.
| |
346 # Iterate over fields of the message and feed them from the | |
347 # policy config file. | |
348 for field in group_message.DESCRIPTOR.fields: | |
349 field_value = None | |
350 if field.name in self._server.policy['enforced']: | |
351 got_fields = True | |
352 recommended = False | |
353 field_value = self._server.policy['enforced'][field.name] | |
354 elif field.name in self._server.policy['recommended']: | |
355 got_fields = True | |
356 field_value = self._server.policy['recommended'][field.name] | |
357 if field_value != None: | |
358 self.SetProtobufMessageField(group_message, field, field_value) | |
359 if got_fields: | |
360 if recommended: | |
361 group_message.policy_mode = cp.RECOMMENDED | |
362 settings.__getattribute__(group.name).CopyFrom(group_message) | |
363 | |
364 # Construct response | |
365 signed_response = dm.SignedCloudPolicyResponse() | |
366 signed_response.settings.CopyFrom(settings) | |
367 signed_response.timestamp = calendar.timegm(time.gmtime()) | |
Jakob Kummerow
2011/01/28 10:45:52
you can remove 'import calendar' and just use 'sig
gfeher
2011/01/28 13:42:10
Done.
| |
368 signed_response.request_token = token; | |
369 signed_response.device_name = self.GetDeviceName() | |
370 | |
371 cloud_response = dm.CloudPolicyResponse() | |
372 cloud_response.signed_response = signed_response.SerializeToString() | |
373 signed_data = cloud_response.signed_response | |
374 cloud_response.signature = ( | |
375 self._server.private_key.hashAndSign(signed_data).tostring()) | |
376 for certificate in self._server.cert_chain: | |
377 cloud_response.certificate_chain.append( | |
378 certificate.writeBytes().tostring()) | |
379 | |
380 response = dm.DeviceManagementResponse() | |
381 response.error = dm.DeviceManagementResponse.SUCCESS | |
382 response.cloud_policy_response.CopyFrom(cloud_response) | |
383 | |
384 self.DumpMessage('Response', response) | |
385 | |
386 return (200, response.SerializeToString()) | |
387 | |
217 def CheckToken(self): | 388 def CheckToken(self): |
218 """Helper for checking whether the client supplied a valid DM token. | 389 """Helper for checking whether the client supplied a valid DM token. |
219 | 390 |
220 Extracts the token from the request and passed to the server in order to | 391 Extracts the token from the request and passed to the server in order to |
221 look up the client. Returns a pair of token and error response. If the token | 392 look up the client. Returns a pair of token and error response. If the token |
222 is None, the error response is a pair of status code and error message. | 393 is None, the error response is a pair of status code and error message. |
223 | 394 |
224 Returns: | 395 Returns: |
225 A pair of DM token and error response. If the token is None, the message | 396 A pair of DM token and error response. If the token is None, the message |
226 will contain the error response to send back. | 397 will contain the error response to send back. |
(...skipping 20 matching lines...) Expand all Loading... | |
247 | 418 |
248 return (None, (200, response.SerializeToString())) | 419 return (None, (200, response.SerializeToString())) |
249 | 420 |
250 def DumpMessage(self, label, msg): | 421 def DumpMessage(self, label, msg): |
251 """Helper for logging an ASCII dump of a protobuf message.""" | 422 """Helper for logging an ASCII dump of a protobuf message.""" |
252 logging.debug('%s\n%s' % (label, str(msg))) | 423 logging.debug('%s\n%s' % (label, str(msg))) |
253 | 424 |
254 class TestServer(object): | 425 class TestServer(object): |
255 """Handles requests and keeps global service state.""" | 426 """Handles requests and keeps global service state.""" |
256 | 427 |
257 def __init__(self, policy_path): | 428 def __init__(self, policy_path, policy_cert_chain): |
258 """Initializes the server. | 429 """Initializes the server. |
259 | 430 |
260 Args: | 431 Args: |
261 policy_path: Names the file to read JSON-formatted policy from. | 432 policy_path: Names the file to read JSON-formatted policy from. |
433 policy_cert_chain: List of paths to X.509 certificate files of the | |
434 certificate chain used for signing responses. | |
262 """ | 435 """ |
263 self._registered_devices = {} | 436 self._registered_devices = {} |
264 self.policy = {} | 437 self.policy = {} |
265 if json is None: | 438 if json is None: |
266 print 'No JSON module, cannot parse policy information' | 439 print 'No JSON module, cannot parse policy information' |
267 else : | 440 else : |
268 try: | 441 try: |
269 self.policy = json.loads(open(policy_path).read()) | 442 self.policy = json.loads(open(policy_path).read()) |
270 except IOError: | 443 except IOError: |
271 print 'Failed to load policy from %s' % policy_path | 444 print 'Failed to load policy from %s' % policy_path |
272 | 445 |
446 self.cert_chain = [] | |
447 for cert_path in policy_cert_chain: | |
448 try: | |
449 last_cert = open(cert_path).read() | |
450 except IOError: | |
451 print 'Failed to load certificate from %s' % cert_path | |
452 certificate = tlslite.api.X509() | |
453 certificate.parse(last_cert) | |
454 self.cert_chain.append(certificate) | |
455 self.private_key = tlslite.api.parsePEMKey(last_cert, private=True) | |
456 | |
273 def HandleRequest(self, path, headers, request): | 457 def HandleRequest(self, path, headers, request): |
274 """Handles a request. | 458 """Handles a request. |
275 | 459 |
276 Args: | 460 Args: |
277 path: The request path and query parameters received from the client. | 461 path: The request path and query parameters received from the client. |
278 headers: A rfc822.Message-like object containing HTTP headers. | 462 headers: A rfc822.Message-like object containing HTTP headers. |
279 request: The request data received from the client as a string. | 463 request: The request data received from the client as a string. |
280 Returns: | 464 Returns: |
281 A pair of HTTP status code and response data to send to the client. | 465 A pair of HTTP status code and response data to send to the client. |
282 """ | 466 """ |
(...skipping 28 matching lines...) Expand all Loading... | |
311 return self._registered_devices.get(dmtoken, None) | 495 return self._registered_devices.get(dmtoken, None) |
312 | 496 |
313 def UnregisterDevice(self, dmtoken): | 497 def UnregisterDevice(self, dmtoken): |
314 """Unregisters a device identified by the given DM token. | 498 """Unregisters a device identified by the given DM token. |
315 | 499 |
316 Args: | 500 Args: |
317 dmtoken: The device management token provided by the client. | 501 dmtoken: The device management token provided by the client. |
318 """ | 502 """ |
319 if dmtoken in self._registered_devices: | 503 if dmtoken in self._registered_devices: |
320 del self._registered_devices[dmtoken] | 504 del self._registered_devices[dmtoken] |
OLD | NEW |