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 contains |
11 format, using the top-level dictionary as a key/value store. The format is | 11 enforced and recommended policies for the device and user scope, and a list |
12 identical to what the Linux implementation reads from /etc. Here is an example: | 12 of managed users. |
| 13 |
| 14 The format of the file is JSON. The root dictionary contains a list under the |
| 15 key "managed_users". It contains auth tokens for which the server will claim |
| 16 that the user is managed. The token string "*" indicates that all users are |
| 17 claimed to be managed. Other keys in the root dictionary identify request |
| 18 scopes. Each request scope is described by a dictionary that holds two |
| 19 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy |
| 20 definitions as key/value stores, their format is identical to what the Linux |
| 21 implementation reads from /etc. |
| 22 |
| 23 Example: |
13 | 24 |
14 { | 25 { |
15 "HomepageLocation" : "http://www.chromium.org" | 26 "chromeos/device": { |
| 27 "mandatory": { |
| 28 "HomepageLocation" : "http://www.chromium.org" |
| 29 }, |
| 30 "recommended": { |
| 31 "JavascriptEnabled": false, |
| 32 }, |
| 33 }, |
| 34 "managed_users": [ |
| 35 "secret123456" |
| 36 ] |
16 } | 37 } |
17 | 38 |
| 39 |
18 """ | 40 """ |
19 | 41 |
20 import cgi | 42 import cgi |
21 import logging | 43 import logging |
| 44 import os |
22 import random | 45 import random |
23 import re | 46 import re |
24 import sys | 47 import sys |
| 48 import time |
| 49 import tlslite |
| 50 import tlslite.api |
25 | 51 |
26 # The name and availability of the json module varies in python versions. | 52 # The name and availability of the json module varies in python versions. |
27 try: | 53 try: |
28 import simplejson as json | 54 import simplejson as json |
29 except ImportError: | 55 except ImportError: |
30 try: | 56 try: |
31 import json | 57 import json |
32 except ImportError: | 58 except ImportError: |
33 json = None | 59 json = None |
34 | 60 |
35 import device_management_backend_pb2 as dm | 61 import device_management_backend_pb2 as dm |
| 62 import cloud_policy_pb2 as cp |
| 63 |
36 | 64 |
37 class RequestHandler(object): | 65 class RequestHandler(object): |
38 """Decodes and handles device management requests from clients. | 66 """Decodes and handles device management requests from clients. |
39 | 67 |
40 The handler implements all the request parsing and protobuf message decoding | 68 The handler implements all the request parsing and protobuf message decoding |
41 and encoding. It calls back into the server to lookup, register, and | 69 and encoding. It calls back into the server to lookup, register, and |
42 unregister clients. | 70 unregister clients. |
43 """ | 71 """ |
44 | 72 |
45 def __init__(self, server, path, headers, request): | 73 def __init__(self, server, path, headers, request): |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
88 | 116 |
89 self.DumpMessage('Request', rmsg) | 117 self.DumpMessage('Request', rmsg) |
90 | 118 |
91 request_type = self.GetUniqueParam('request') | 119 request_type = self.GetUniqueParam('request') |
92 if request_type == 'register': | 120 if request_type == 'register': |
93 return self.ProcessRegister(rmsg.register_request) | 121 return self.ProcessRegister(rmsg.register_request) |
94 elif request_type == 'unregister': | 122 elif request_type == 'unregister': |
95 return self.ProcessUnregister(rmsg.unregister_request) | 123 return self.ProcessUnregister(rmsg.unregister_request) |
96 elif request_type == 'policy': | 124 elif request_type == 'policy': |
97 return self.ProcessPolicy(rmsg.policy_request) | 125 return self.ProcessPolicy(rmsg.policy_request) |
| 126 elif request_type == 'cloud_policy': |
| 127 return self.ProcessCloudPolicyRequest(rmsg.cloud_policy_request) |
| 128 elif request_type == 'managed_check': |
| 129 return self.ProcessManagedCheck(rmsg.managed_check_request) |
98 else: | 130 else: |
99 return (400, 'Invalid request parameter') | 131 return (400, 'Invalid request parameter') |
100 | 132 |
| 133 def CheckGoogleLogin(self): |
| 134 """Extracts the GoogleLogin auth token from the HTTP request, and |
| 135 returns it. Returns None if the token is not present. |
| 136 """ |
| 137 match = re.match('GoogleLogin auth=(\\w+)', |
| 138 self._headers.getheader('Authorization', '')) |
| 139 if not match: |
| 140 return None |
| 141 return match.group(1) |
| 142 |
| 143 def GetDeviceName(self): |
| 144 """Returns the name for the currently authenticated device based on its |
| 145 device id. |
| 146 """ |
| 147 return 'chromeos-' + self.GetUniqueParam('deviceid') |
| 148 |
101 def ProcessRegister(self, msg): | 149 def ProcessRegister(self, msg): |
102 """Handles a register request. | 150 """Handles a register request. |
103 | 151 |
104 Checks the query for authorization and device identifier, registers the | 152 Checks the query for authorization and device identifier, registers the |
105 device with the server and constructs a response. | 153 device with the server and constructs a response. |
106 | 154 |
107 Args: | 155 Args: |
108 msg: The DeviceRegisterRequest message received from the client. | 156 msg: The DeviceRegisterRequest message received from the client. |
109 | 157 |
110 Returns: | 158 Returns: |
111 A tuple of HTTP status code and response data to send to the client. | 159 A tuple of HTTP status code and response data to send to the client. |
112 """ | 160 """ |
113 # Check the auth token and device ID. | 161 # Check the auth token and device ID. |
114 match = re.match('GoogleLogin auth=(\\w+)', | 162 if not self.CheckGoogleLogin(): |
115 self._headers.getheader('Authorization', '')) | |
116 if not match: | |
117 return (403, 'No authorization') | 163 return (403, 'No authorization') |
118 auth_token = match.group(1) | |
119 | 164 |
120 device_id = self.GetUniqueParam('deviceid') | 165 device_id = self.GetUniqueParam('deviceid') |
121 if not device_id: | 166 if not device_id: |
122 return (400, 'Missing device identifier') | 167 return (400, 'Missing device identifier') |
123 | 168 |
124 # Register the device and create a token. | 169 # Register the device and create a token. |
125 dmtoken = self._server.RegisterDevice(device_id) | 170 dmtoken = self._server.RegisterDevice(device_id) |
126 | 171 |
127 # Send back the reply. | 172 # Send back the reply. |
128 response = dm.DeviceManagementResponse() | 173 response = dm.DeviceManagementResponse() |
129 response.error = dm.DeviceManagementResponse.SUCCESS | 174 response.error = dm.DeviceManagementResponse.SUCCESS |
130 response.register_response.device_management_token = dmtoken | 175 response.register_response.device_management_token = dmtoken |
| 176 response.register_response.device_name = self.GetDeviceName() |
131 | 177 |
132 self.DumpMessage('Response', response) | 178 self.DumpMessage('Response', response) |
133 | 179 |
134 return (200, response.SerializeToString()) | 180 return (200, response.SerializeToString()) |
135 | 181 |
136 def ProcessUnregister(self, msg): | 182 def ProcessUnregister(self, msg): |
137 """Handles a register request. | 183 """Handles a register request. |
138 | 184 |
139 Checks for authorization, unregisters the device and constructs the | 185 Checks for authorization, unregisters the device and constructs the |
140 response. | 186 response. |
(...skipping 14 matching lines...) Expand all Loading... |
155 | 201 |
156 # Prepare and send the response. | 202 # Prepare and send the response. |
157 response = dm.DeviceManagementResponse() | 203 response = dm.DeviceManagementResponse() |
158 response.error = dm.DeviceManagementResponse.SUCCESS | 204 response.error = dm.DeviceManagementResponse.SUCCESS |
159 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) | 205 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) |
160 | 206 |
161 self.DumpMessage('Response', response) | 207 self.DumpMessage('Response', response) |
162 | 208 |
163 return (200, response.SerializeToString()) | 209 return (200, response.SerializeToString()) |
164 | 210 |
| 211 def ProcessManagedCheck(self, msg): |
| 212 """Handles a 'managed check' request. |
| 213 |
| 214 Queries the list of managed users and responds the client if their user |
| 215 is managed or not. |
| 216 |
| 217 Args: |
| 218 msg: The ManagedCheckRequest message received from the client. |
| 219 |
| 220 Returns: |
| 221 A tuple of HTTP status code and response data to send to the client. |
| 222 """ |
| 223 # Check the management token. |
| 224 auth = self.CheckGoogleLogin() |
| 225 if not auth: |
| 226 return (403, 'No authorization') |
| 227 |
| 228 managed_check_response = dm.ManagedCheckResponse() |
| 229 if ('*' in self._server.policy['managed_users'] or |
| 230 auth in self._server.policy['managed_users']): |
| 231 managed_check_response.mode = dm.ManagedCheckResponse.MANAGED; |
| 232 else: |
| 233 managed_check_response.mode = dm.ManagedCheckResponse.UNMANAGED; |
| 234 |
| 235 # Prepare and send the response. |
| 236 response = dm.DeviceManagementResponse() |
| 237 response.error = dm.DeviceManagementResponse.SUCCESS |
| 238 response.managed_check_response.CopyFrom(managed_check_response) |
| 239 |
| 240 self.DumpMessage('Response', response) |
| 241 |
| 242 return (200, response.SerializeToString()) |
| 243 |
165 def ProcessPolicy(self, msg): | 244 def ProcessPolicy(self, msg): |
166 """Handles a policy request. | 245 """Handles a policy request. |
167 | 246 |
168 Checks for authorization, encodes the policy into protobuf representation | 247 Checks for authorization, encodes the policy into protobuf representation |
169 and constructs the repsonse. | 248 and constructs the response. |
170 | 249 |
171 Args: | 250 Args: |
172 msg: The DevicePolicyRequest message received from the client. | 251 msg: The DevicePolicyRequest message received from the client. |
173 | 252 |
174 Returns: | 253 Returns: |
175 A tuple of HTTP status code and response data to send to the client. | 254 A tuple of HTTP status code and response data to send to the client. |
176 """ | 255 """ |
177 # Check the management token. | 256 # Check the management token. |
178 token, response = self.CheckToken() | 257 token, response = self.CheckToken() |
179 if not token: | 258 if not token: |
(...skipping 27 matching lines...) Expand all Loading... |
207 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY | 286 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY |
208 for list_entry in value: | 287 for list_entry in value: |
209 entry_value.string_array.append(str(list_entry)) | 288 entry_value.string_array.append(str(list_entry)) |
210 entry.value.CopyFrom(entry_value) | 289 entry.value.CopyFrom(entry_value) |
211 setting.policy_value.CopyFrom(policy_value) | 290 setting.policy_value.CopyFrom(policy_value) |
212 | 291 |
213 self.DumpMessage('Response', response) | 292 self.DumpMessage('Response', response) |
214 | 293 |
215 return (200, response.SerializeToString()) | 294 return (200, response.SerializeToString()) |
216 | 295 |
| 296 def SetProtobufMessageField(self, group_message, field, field_value): |
| 297 '''Sets a field in a protobuf message. |
| 298 |
| 299 Args: |
| 300 group_message: The protobuf message. |
| 301 field: The field of the message to set, it shuold be a member of |
| 302 group_message.DESCRIPTOR.fields. |
| 303 field_value: The value to set. |
| 304 ''' |
| 305 if field.label == field.LABEL_REPEATED: |
| 306 assert type(field_value) == list |
| 307 assert field.type == field.TYPE_STRING |
| 308 list_field = group_message.__getattribute__(field.name) |
| 309 for list_item in field_value: |
| 310 list_field.append(list_item) |
| 311 else: |
| 312 # Simple cases: |
| 313 if field.type == field.TYPE_BOOL: |
| 314 assert type(field_value) == bool |
| 315 elif field.type == field.TYPE_STRING: |
| 316 assert type(field_value) == str |
| 317 elif field.type == field.TYPE_INT64: |
| 318 assert type(field_value) == int |
| 319 else: |
| 320 raise Exception('Unknown field type %s' % field.type_name) |
| 321 group_message.__setattr__(field.name, field_value) |
| 322 |
| 323 def GatherPolicySettings(self, settings, policies): |
| 324 '''Copies all the policies from a dictionary into a protobuf of type |
| 325 CloudPolicySettings. |
| 326 |
| 327 Args: |
| 328 settings: The destination: a CloudPolicySettings protobuf. |
| 329 policies: The source: a dictionary containing policies under keys |
| 330 'recommended' and 'mandatory'. |
| 331 ''' |
| 332 for group in settings.DESCRIPTOR.fields: |
| 333 # Create protobuf message for group. |
| 334 group_message = eval('cp.' + group.message_type.name + '()') |
| 335 # We assume that this policy group will be recommended, and only switch |
| 336 # it to mandatory if at least one of its members is mandatory. |
| 337 group_message.policy_options.mode = cp.PolicyOptions.RECOMMENDED |
| 338 # Indicates if at least one field was set in |group_message|. |
| 339 got_fields = False |
| 340 # Iterate over fields of the message and feed them from the |
| 341 # policy config file. |
| 342 for field in group_message.DESCRIPTOR.fields: |
| 343 field_value = None |
| 344 if field.name in policies['mandatory']: |
| 345 group_message.policy_options.mode = cp.PolicyOptions.MANDATORY |
| 346 field_value = policies['mandatory'][field.name] |
| 347 elif field.name in policies['recommended']: |
| 348 field_value = policies['recommended'][field.name] |
| 349 if field_value != None: |
| 350 got_fields = True |
| 351 self.SetProtobufMessageField(group_message, field, field_value) |
| 352 if got_fields: |
| 353 settings.__getattribute__(group.name).CopyFrom(group_message) |
| 354 |
| 355 def ProcessCloudPolicyRequest(self, msg): |
| 356 """Handles a cloud policy request. (New protocol for policy requests.) |
| 357 |
| 358 Checks for authorization, encodes the policy into protobuf representation, |
| 359 signs it and constructs the repsonse. |
| 360 |
| 361 Args: |
| 362 msg: The CloudPolicyRequest message received from the client. |
| 363 |
| 364 Returns: |
| 365 A tuple of HTTP status code and response data to send to the client. |
| 366 """ |
| 367 token, response = self.CheckToken() |
| 368 if not token: |
| 369 return response |
| 370 |
| 371 settings = cp.CloudPolicySettings() |
| 372 |
| 373 if msg.policy_scope in self._server.policy: |
| 374 # Respond is only given if the scope is specified in the config file. |
| 375 # Normally 'chromeos/device' and 'chromeos/user' should be accepted. |
| 376 self.GatherPolicySettings(settings, |
| 377 self._server.policy[msg.policy_scope]) |
| 378 |
| 379 # Construct response |
| 380 signed_response = dm.SignedCloudPolicyResponse() |
| 381 signed_response.settings.CopyFrom(settings) |
| 382 signed_response.timestamp = int(time.time()) |
| 383 signed_response.request_token = token; |
| 384 signed_response.device_name = self.GetDeviceName() |
| 385 |
| 386 cloud_response = dm.CloudPolicyResponse() |
| 387 cloud_response.signed_response = signed_response.SerializeToString() |
| 388 signed_data = cloud_response.signed_response |
| 389 cloud_response.signature = ( |
| 390 self._server.private_key.hashAndSign(signed_data).tostring()) |
| 391 for certificate in self._server.cert_chain: |
| 392 cloud_response.certificate_chain.append( |
| 393 certificate.writeBytes().tostring()) |
| 394 |
| 395 response = dm.DeviceManagementResponse() |
| 396 response.error = dm.DeviceManagementResponse.SUCCESS |
| 397 response.cloud_policy_response.CopyFrom(cloud_response) |
| 398 |
| 399 self.DumpMessage('Response', response) |
| 400 |
| 401 return (200, response.SerializeToString()) |
| 402 |
217 def CheckToken(self): | 403 def CheckToken(self): |
218 """Helper for checking whether the client supplied a valid DM token. | 404 """Helper for checking whether the client supplied a valid DM token. |
219 | 405 |
220 Extracts the token from the request and passed to the server in order to | 406 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 | 407 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. | 408 is None, the error response is a pair of status code and error message. |
223 | 409 |
224 Returns: | 410 Returns: |
225 A pair of DM token and error response. If the token is None, the message | 411 A pair of DM token and error response. If the token is None, the message |
226 will contain the error response to send back. | 412 will contain the error response to send back. |
(...skipping 20 matching lines...) Expand all Loading... |
247 | 433 |
248 return (None, (200, response.SerializeToString())) | 434 return (None, (200, response.SerializeToString())) |
249 | 435 |
250 def DumpMessage(self, label, msg): | 436 def DumpMessage(self, label, msg): |
251 """Helper for logging an ASCII dump of a protobuf message.""" | 437 """Helper for logging an ASCII dump of a protobuf message.""" |
252 logging.debug('%s\n%s' % (label, str(msg))) | 438 logging.debug('%s\n%s' % (label, str(msg))) |
253 | 439 |
254 class TestServer(object): | 440 class TestServer(object): |
255 """Handles requests and keeps global service state.""" | 441 """Handles requests and keeps global service state.""" |
256 | 442 |
257 def __init__(self, policy_path): | 443 def __init__(self, policy_path, policy_cert_chain): |
258 """Initializes the server. | 444 """Initializes the server. |
259 | 445 |
260 Args: | 446 Args: |
261 policy_path: Names the file to read JSON-formatted policy from. | 447 policy_path: Names the file to read JSON-formatted policy from. |
| 448 policy_cert_chain: List of paths to X.509 certificate files of the |
| 449 certificate chain used for signing responses. |
262 """ | 450 """ |
263 self._registered_devices = {} | 451 self._registered_devices = {} |
264 self.policy = {} | 452 self.policy = {} |
265 if json is None: | 453 if json is None: |
266 print 'No JSON module, cannot parse policy information' | 454 print 'No JSON module, cannot parse policy information' |
267 else : | 455 else : |
268 try: | 456 try: |
269 self.policy = json.loads(open(policy_path).read()) | 457 self.policy = json.loads(open(policy_path).read()) |
270 except IOError: | 458 except IOError: |
271 print 'Failed to load policy from %s' % policy_path | 459 print 'Failed to load policy from %s' % policy_path |
272 | 460 |
| 461 self.private_key = None |
| 462 self.cert_chain = [] |
| 463 for cert_path in policy_cert_chain: |
| 464 try: |
| 465 cert_text = open(cert_path).read() |
| 466 except IOError: |
| 467 print 'Failed to load certificate from %s' % cert_path |
| 468 certificate = tlslite.api.X509() |
| 469 certificate.parse(cert_text) |
| 470 self.cert_chain.append(certificate) |
| 471 if self.private_key is None: |
| 472 self.private_key = tlslite.api.parsePEMKey(cert_text, private=True) |
| 473 assert self.private_key != None |
| 474 |
273 def HandleRequest(self, path, headers, request): | 475 def HandleRequest(self, path, headers, request): |
274 """Handles a request. | 476 """Handles a request. |
275 | 477 |
276 Args: | 478 Args: |
277 path: The request path and query parameters received from the client. | 479 path: The request path and query parameters received from the client. |
278 headers: A rfc822.Message-like object containing HTTP headers. | 480 headers: A rfc822.Message-like object containing HTTP headers. |
279 request: The request data received from the client as a string. | 481 request: The request data received from the client as a string. |
280 Returns: | 482 Returns: |
281 A pair of HTTP status code and response data to send to the client. | 483 A pair of HTTP status code and response data to send to the client. |
282 """ | 484 """ |
(...skipping 28 matching lines...) Expand all Loading... |
311 return self._registered_devices.get(dmtoken, None) | 513 return self._registered_devices.get(dmtoken, None) |
312 | 514 |
313 def UnregisterDevice(self, dmtoken): | 515 def UnregisterDevice(self, dmtoken): |
314 """Unregisters a device identified by the given DM token. | 516 """Unregisters a device identified by the given DM token. |
315 | 517 |
316 Args: | 518 Args: |
317 dmtoken: The device management token provided by the client. | 519 dmtoken: The device management token provided by the client. |
318 """ | 520 """ |
319 if dmtoken in self._registered_devices: | 521 if dmtoken in self._registered_devices: |
320 del self._registered_devices[dmtoken] | 522 del self._registered_devices[dmtoken] |
OLD | NEW |