Chromium Code Reviews| 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 |