| 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 |