OLD | NEW |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """A bare-bones test server for testing cloud policy support. | 5 """A bare-bones test server for testing cloud policy support. |
6 | 6 |
7 This implements a simple cloud policy test server that can be used to test | 7 This implements a simple cloud policy test server that can be used to test |
8 chrome's device management service client. The policy information is read from | 8 chrome's device management service client. The policy information is read from |
9 the file named device_management in the server's data directory. It contains | 9 the file named device_management in the server's data directory. It contains |
10 enforced and recommended policies for the device and user scope, and a list | 10 enforced and recommended policies for the device and user scope, and a list |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
50 "robot_api_auth_code": "fake_auth_code", | 50 "robot_api_auth_code": "fake_auth_code", |
51 "invalidation_source": 1025, | 51 "invalidation_source": 1025, |
52 "invalidation_name": "UENUPOL" | 52 "invalidation_name": "UENUPOL" |
53 } | 53 } |
54 | 54 |
55 """ | 55 """ |
56 | 56 |
57 import base64 | 57 import base64 |
58 import BaseHTTPServer | 58 import BaseHTTPServer |
59 import cgi | 59 import cgi |
| 60 import glob |
60 import google.protobuf.text_format | 61 import google.protobuf.text_format |
61 import hashlib | 62 import hashlib |
62 import logging | 63 import logging |
63 import os | 64 import os |
64 import random | 65 import random |
65 import re | 66 import re |
66 import sys | 67 import sys |
67 import time | 68 import time |
68 import tlslite | 69 import tlslite |
69 import tlslite.api | 70 import tlslite.api |
(...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
270 self.headers.getheader('Authorization', '')) | 271 self.headers.getheader('Authorization', '')) |
271 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token'))) | 272 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token'))) |
272 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid'))) | 273 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid'))) |
273 self.DumpMessage('Request', rmsg) | 274 self.DumpMessage('Request', rmsg) |
274 | 275 |
275 request_type = self.GetUniqueParam('request') | 276 request_type = self.GetUniqueParam('request') |
276 # Check server side requirements, as defined in | 277 # Check server side requirements, as defined in |
277 # device_management_backend.proto. | 278 # device_management_backend.proto. |
278 if (self.GetUniqueParam('devicetype') != '2' or | 279 if (self.GetUniqueParam('devicetype') != '2' or |
279 self.GetUniqueParam('apptype') != 'Chrome' or | 280 self.GetUniqueParam('apptype') != 'Chrome' or |
280 (request_type != 'ping' and | 281 len(self.GetUniqueParam('deviceid')) >= 64 or |
281 len(self.GetUniqueParam('deviceid')) >= 64) or | |
282 len(self.GetUniqueParam('agent')) >= 64): | 282 len(self.GetUniqueParam('agent')) >= 64): |
283 return (400, 'Invalid request parameter') | 283 return (400, 'Invalid request parameter') |
284 if request_type == 'register': | 284 if request_type == 'register': |
285 response = self.ProcessRegister(rmsg.register_request) | 285 response = self.ProcessRegister(rmsg.register_request) |
286 elif request_type == 'api_authorization': | 286 elif request_type == 'api_authorization': |
287 response = self.ProcessApiAuthorization(rmsg.service_api_access_request) | 287 response = self.ProcessApiAuthorization(rmsg.service_api_access_request) |
288 elif request_type == 'unregister': | 288 elif request_type == 'unregister': |
289 response = self.ProcessUnregister(rmsg.unregister_request) | 289 response = self.ProcessUnregister(rmsg.unregister_request) |
290 elif request_type == 'policy' or request_type == 'ping': | 290 elif request_type == 'policy': |
291 response = self.ProcessPolicy(rmsg, request_type) | 291 response = self.ProcessPolicy(rmsg, request_type) |
292 elif request_type == 'enterprise_check': | 292 elif request_type == 'enterprise_check': |
293 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request) | 293 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request) |
294 elif request_type == 'device_state_retrieval': | 294 elif request_type == 'device_state_retrieval': |
295 response = self.ProcessDeviceStateRetrievalRequest( | 295 response = self.ProcessDeviceStateRetrievalRequest( |
296 rmsg.device_state_retrieval_request) | 296 rmsg.device_state_retrieval_request) |
297 else: | 297 else: |
298 return (400, 'Invalid request parameter') | 298 return (400, 'Invalid request parameter') |
299 | 299 |
300 self.DumpMessage('Response', response[1]) | 300 self.DumpMessage('Response', response[1]) |
301 return (response[0], response[1].SerializeToString()) | 301 return (response[0], response[1].SerializeToString()) |
302 | 302 |
303 def CreatePolicyForExternalPolicyData(self, policy_key): | 303 def CreatePolicyForExternalPolicyData(self, policy_key): |
304 """Returns an ExternalPolicyData protobuf for policy_key. | 304 """Returns an ExternalPolicyData protobuf for policy_key. |
305 | 305 |
306 If there is policy data for policy_key then the download url will be | 306 If there is policy data for policy_key then the download url will be |
307 set so that it points to that data, and the appropriate hash is also set. | 307 set so that it points to that data, and the appropriate hash is also set. |
308 Otherwise, the protobuf will be empty. | 308 Otherwise, the protobuf will be empty. |
309 | 309 |
310 Args: | 310 Args: |
311 policy_key: the policy type and settings entity id, joined by '/'. | 311 policy_key: The policy type and settings entity id, joined by '/'. |
312 | 312 |
313 Returns: | 313 Returns: |
314 A serialized ExternalPolicyData. | 314 A serialized ExternalPolicyData. |
315 """ | 315 """ |
316 settings = ep.ExternalPolicyData() | 316 settings = ep.ExternalPolicyData() |
317 data = self.server.ReadPolicyDataFromDataDir(policy_key) | 317 data = self.server.ReadPolicyDataFromDataDir(policy_key) |
318 if data: | 318 if data: |
319 settings.download_url = urlparse.urljoin( | 319 settings.download_url = urlparse.urljoin( |
320 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key) | 320 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key) |
321 settings.secure_hash = hashlib.sha256(data).digest() | 321 settings.secure_hash = hashlib.sha256(data).digest() |
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
438 if not token_info: | 438 if not token_info: |
439 return error | 439 return error |
440 | 440 |
441 key_update_request = msg.device_state_key_update_request | 441 key_update_request = msg.device_state_key_update_request |
442 if len(key_update_request.server_backed_state_key) > 0: | 442 if len(key_update_request.server_backed_state_key) > 0: |
443 self.server.UpdateStateKeys(token_info['device_token'], | 443 self.server.UpdateStateKeys(token_info['device_token'], |
444 key_update_request.server_backed_state_key) | 444 key_update_request.server_backed_state_key) |
445 | 445 |
446 response = dm.DeviceManagementResponse() | 446 response = dm.DeviceManagementResponse() |
447 for request in msg.policy_request.request: | 447 for request in msg.policy_request.request: |
448 fetch_response = response.policy_response.response.add() | |
449 if (request.policy_type in | 448 if (request.policy_type in |
450 ('google/android/user', | 449 ('google/android/user', |
451 'google/chrome/extension', | |
452 'google/chromeos/device', | 450 'google/chromeos/device', |
453 'google/chromeos/publicaccount', | 451 'google/chromeos/publicaccount', |
454 'google/chromeos/user', | 452 'google/chromeos/user', |
455 'google/chrome/user', | 453 'google/chrome/user', |
456 'google/ios/user')): | 454 'google/ios/user')): |
457 if request_type != 'policy': | 455 fetch_response = response.policy_response.response.add() |
458 fetch_response.error_code = 400 | 456 self.ProcessCloudPolicy(request, token_info, fetch_response) |
459 fetch_response.error_message = 'Invalid request type' | 457 elif request.policy_type == 'google/chrome/extension': |
460 else: | 458 self.ProcessCloudPolicyForExtensions( |
461 self.ProcessCloudPolicy(request, token_info, fetch_response) | 459 request, response.policy_response, token_info) |
462 else: | 460 else: |
463 fetch_response.error_code = 400 | 461 fetch_response.error_code = 400 |
464 fetch_response.error_message = 'Invalid policy_type' | 462 fetch_response.error_message = 'Invalid policy_type' |
465 | 463 |
466 return (200, response) | 464 return (200, response) |
467 | 465 |
468 def ProcessAutoEnrollment(self, msg): | 466 def ProcessAutoEnrollment(self, msg): |
469 """Handles an auto-enrollment check request. | 467 """Handles an auto-enrollment check request. |
470 | 468 |
471 The reply depends on the value of the modulus: | 469 The reply depends on the value of the modulus: |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
525 for field in FIELDS: | 523 for field in FIELDS: |
526 if field in state: | 524 if field in state: |
527 setattr(device_state_retrieval_response, field, state[field]) | 525 setattr(device_state_retrieval_response, field, state[field]) |
528 | 526 |
529 response = dm.DeviceManagementResponse() | 527 response = dm.DeviceManagementResponse() |
530 response.device_state_retrieval_response.CopyFrom( | 528 response.device_state_retrieval_response.CopyFrom( |
531 device_state_retrieval_response) | 529 device_state_retrieval_response) |
532 return (200, response) | 530 return (200, response) |
533 | 531 |
534 def SetProtobufMessageField(self, group_message, field, field_value): | 532 def SetProtobufMessageField(self, group_message, field, field_value): |
535 '''Sets a field in a protobuf message. | 533 """Sets a field in a protobuf message. |
536 | 534 |
537 Args: | 535 Args: |
538 group_message: The protobuf message. | 536 group_message: The protobuf message. |
539 field: The field of the message to set, it should be a member of | 537 field: The field of the message to set, it should be a member of |
540 group_message.DESCRIPTOR.fields. | 538 group_message.DESCRIPTOR.fields. |
541 field_value: The value to set. | 539 field_value: The value to set. |
542 ''' | 540 """ |
543 if field.label == field.LABEL_REPEATED: | 541 if field.label == field.LABEL_REPEATED: |
544 assert type(field_value) == list | 542 assert type(field_value) == list |
545 entries = group_message.__getattribute__(field.name) | 543 entries = group_message.__getattribute__(field.name) |
546 if field.message_type is None: | 544 if field.message_type is None: |
547 for list_item in field_value: | 545 for list_item in field_value: |
548 entries.append(list_item) | 546 entries.append(list_item) |
549 else: | 547 else: |
550 # This field is itself a protobuf. | 548 # This field is itself a protobuf. |
551 sub_type = field.message_type | 549 sub_type = field.message_type |
552 for sub_value in field_value: | 550 for sub_value in field_value: |
(...skipping 17 matching lines...) Expand all Loading... |
570 assert type(field_value) == list | 568 assert type(field_value) == list |
571 entries = group_message.__getattribute__(field.name).entries | 569 entries = group_message.__getattribute__(field.name).entries |
572 for list_item in field_value: | 570 for list_item in field_value: |
573 entries.append(list_item) | 571 entries.append(list_item) |
574 return | 572 return |
575 else: | 573 else: |
576 raise Exception('Unknown field type %s' % field.type) | 574 raise Exception('Unknown field type %s' % field.type) |
577 group_message.__setattr__(field.name, field_value) | 575 group_message.__setattr__(field.name, field_value) |
578 | 576 |
579 def GatherDevicePolicySettings(self, settings, policies): | 577 def GatherDevicePolicySettings(self, settings, policies): |
580 '''Copies all the policies from a dictionary into a protobuf of type | 578 """Copies all the policies from a dictionary into a protobuf of type |
581 CloudDeviceSettingsProto. | 579 CloudDeviceSettingsProto. |
582 | 580 |
583 Args: | 581 Args: |
584 settings: The destination ChromeDeviceSettingsProto protobuf. | 582 settings: The destination ChromeDeviceSettingsProto protobuf. |
585 policies: The source dictionary containing policies in JSON format. | 583 policies: The source dictionary containing policies in JSON format. |
586 ''' | 584 """ |
587 for group in settings.DESCRIPTOR.fields: | 585 for group in settings.DESCRIPTOR.fields: |
588 # Create protobuf message for group. | 586 # Create protobuf message for group. |
589 group_message = eval('dp.' + group.message_type.name + '()') | 587 group_message = eval('dp.' + group.message_type.name + '()') |
590 # Indicates if at least one field was set in |group_message|. | 588 # Indicates if at least one field was set in |group_message|. |
591 got_fields = False | 589 got_fields = False |
592 # Iterate over fields of the message and feed them from the | 590 # Iterate over fields of the message and feed them from the |
593 # policy config file. | 591 # policy config file. |
594 for field in group_message.DESCRIPTOR.fields: | 592 for field in group_message.DESCRIPTOR.fields: |
595 field_value = None | 593 field_value = None |
596 if field.name in policies: | 594 if field.name in policies: |
597 got_fields = True | 595 got_fields = True |
598 field_value = policies[field.name] | 596 field_value = policies[field.name] |
599 self.SetProtobufMessageField(group_message, field, field_value) | 597 self.SetProtobufMessageField(group_message, field, field_value) |
600 if got_fields: | 598 if got_fields: |
601 settings.__getattribute__(group.name).CopyFrom(group_message) | 599 settings.__getattribute__(group.name).CopyFrom(group_message) |
602 | 600 |
603 def GatherUserPolicySettings(self, settings, policies): | 601 def GatherUserPolicySettings(self, settings, policies): |
604 '''Copies all the policies from a dictionary into a protobuf of type | 602 """Copies all the policies from a dictionary into a protobuf of type |
605 CloudPolicySettings. | 603 CloudPolicySettings. |
606 | 604 |
607 Args: | 605 Args: |
608 settings: The destination: a CloudPolicySettings protobuf. | 606 settings: The destination: a CloudPolicySettings protobuf. |
609 policies: The source: a dictionary containing policies under keys | 607 policies: The source: a dictionary containing policies under keys |
610 'recommended' and 'mandatory'. | 608 'recommended' and 'mandatory'. |
611 ''' | 609 """ |
612 for field in settings.DESCRIPTOR.fields: | 610 for field in settings.DESCRIPTOR.fields: |
613 # |field| is the entry for a specific policy in the top-level | 611 # |field| is the entry for a specific policy in the top-level |
614 # CloudPolicySettings proto. | 612 # CloudPolicySettings proto. |
615 | 613 |
616 # Look for this policy's value in the mandatory or recommended dicts. | 614 # Look for this policy's value in the mandatory or recommended dicts. |
617 if field.name in policies.get('mandatory', {}): | 615 if field.name in policies.get('mandatory', {}): |
618 mode = cp.PolicyOptions.MANDATORY | 616 mode = cp.PolicyOptions.MANDATORY |
619 value = policies['mandatory'][field.name] | 617 value = policies['mandatory'][field.name] |
620 elif field.name in policies.get('recommended', {}): | 618 elif field.name in policies.get('recommended', {}): |
621 mode = cp.PolicyOptions.RECOMMENDED | 619 mode = cp.PolicyOptions.RECOMMENDED |
622 value = policies['recommended'][field.name] | 620 value = policies['recommended'][field.name] |
623 else: | 621 else: |
624 continue | 622 continue |
625 | 623 |
626 # Create protobuf message for this policy. | 624 # Create protobuf message for this policy. |
627 policy_message = eval('cp.' + field.message_type.name + '()') | 625 policy_message = eval('cp.' + field.message_type.name + '()') |
628 policy_message.policy_options.mode = mode | 626 policy_message.policy_options.mode = mode |
629 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value'] | 627 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value'] |
630 self.SetProtobufMessageField(policy_message, field_descriptor, value) | 628 self.SetProtobufMessageField(policy_message, field_descriptor, value) |
631 settings.__getattribute__(field.name).CopyFrom(policy_message) | 629 settings.__getattribute__(field.name).CopyFrom(policy_message) |
632 | 630 |
| 631 def ProcessCloudPolicyForExtensions(self, request, response, token_info): |
| 632 """Handles a request for policy for extensions. |
| 633 |
| 634 A request for policy for extensions is slightly different from the other |
| 635 cloud policy requests, because it can trigger 0, one or many |
| 636 PolicyFetchResponse messages in the response. |
| 637 |
| 638 Args: |
| 639 request: The PolicyFetchRequest that triggered this handler. |
| 640 response: The DevicePolicyResponse message for the response. Multiple |
| 641 PolicyFetchResponses will be appended to this message. |
| 642 token_info: The token extracted from the request. |
| 643 """ |
| 644 # Send one PolicyFetchResponse for each extension that has |
| 645 # configuration data at the server. |
| 646 ids = self.server.ListMatchingComponents('google/chrome/extension') |
| 647 for settings_entity_id in ids: |
| 648 # Reuse the extension policy request, to trigger the same signature |
| 649 # type in the response. |
| 650 request.settings_entity_id = settings_entity_id |
| 651 fetch_response = response.response.add() |
| 652 self.ProcessCloudPolicy(request, token_info, fetch_response) |
| 653 # Don't do key rotations for these messages. |
| 654 fetch_response.ClearField('new_public_key') |
| 655 fetch_response.ClearField('new_public_key_signature') |
| 656 fetch_response.ClearField('new_public_key_verification_signature') |
| 657 |
633 def ProcessCloudPolicy(self, msg, token_info, response): | 658 def ProcessCloudPolicy(self, msg, token_info, response): |
634 """Handles a cloud policy request. (New protocol for policy requests.) | 659 """Handles a cloud policy request. (New protocol for policy requests.) |
635 | 660 |
636 Encodes the policy into protobuf representation, signs it and constructs | 661 Encodes the policy into protobuf representation, signs it and constructs |
637 the response. | 662 the response. |
638 | 663 |
639 Args: | 664 Args: |
640 msg: The CloudPolicyRequest message received from the client. | 665 msg: The CloudPolicyRequest message received from the client. |
641 token_info: the token extracted from the request. | 666 token_info: The token extracted from the request. |
642 response: A PolicyFetchResponse message that should be filled with the | 667 response: A PolicyFetchResponse message that should be filled with the |
643 response data. | 668 response data. |
644 """ | 669 """ |
645 | 670 |
646 if msg.machine_id: | 671 if msg.machine_id: |
647 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id) | 672 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id) |
648 | 673 |
649 # Response is only given if the scope is specified in the config file. | 674 # Response is only given if the scope is specified in the config file. |
650 # Normally 'google/chromeos/device', 'google/chromeos/user' and | 675 # Normally 'google/chromeos/device', 'google/chromeos/user' and |
651 # 'google/chromeos/publicaccount' should be accepted. | 676 # 'google/chromeos/publicaccount' should be accepted. |
(...skipping 387 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1039 def WriteClientState(self): | 1064 def WriteClientState(self): |
1040 """Writes the client state back to the file.""" | 1065 """Writes the client state back to the file.""" |
1041 if self.client_state_file is not None: | 1066 if self.client_state_file is not None: |
1042 json_data = json.dumps(self._registered_tokens) | 1067 json_data = json.dumps(self._registered_tokens) |
1043 open(self.client_state_file, 'w').write(json_data) | 1068 open(self.client_state_file, 'w').write(json_data) |
1044 | 1069 |
1045 def GetBaseFilename(self, policy_selector): | 1070 def GetBaseFilename(self, policy_selector): |
1046 """Returns the base filename for the given policy_selector. | 1071 """Returns the base filename for the given policy_selector. |
1047 | 1072 |
1048 Args: | 1073 Args: |
1049 policy_selector: the policy type and settings entity id, joined by '/'. | 1074 policy_selector: The policy type and settings entity id, joined by '/'. |
1050 | 1075 |
1051 Returns: | 1076 Returns: |
1052 The filename corresponding to the policy_selector, without a file | 1077 The filename corresponding to the policy_selector, without a file |
1053 extension. | 1078 extension. |
1054 """ | 1079 """ |
1055 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector) | 1080 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector) |
1056 return os.path.join(self.data_dir or '', | 1081 return os.path.join(self.data_dir or '', |
1057 'policy_%s' % sanitized_policy_selector) | 1082 'policy_%s' % sanitized_policy_selector) |
1058 | 1083 |
| 1084 def ListMatchingComponents(self, policy_type): |
| 1085 """Returns a list of settings entity IDs that have a configuration file. |
| 1086 |
| 1087 Args: |
| 1088 policy_type: The policy type to look for. Only settings entity IDs for |
| 1089 file selectors That match this policy_type will be returned. |
| 1090 |
| 1091 Returns: |
| 1092 A list of settings entity IDs for the given |policy_type| that have a |
| 1093 configuration file in this server (either as a .bin, .txt or .data file). |
| 1094 """ |
| 1095 base_name = self.GetBaseFilename(policy_type) |
| 1096 files = glob.glob('%s_*.*' % base_name) |
| 1097 len_base_name = len(base_name) + 1 |
| 1098 return [ file[len_base_name:file.rfind('.')] for file in files ] |
| 1099 |
1059 def ReadPolicyFromDataDir(self, policy_selector, proto_message): | 1100 def ReadPolicyFromDataDir(self, policy_selector, proto_message): |
1060 """Tries to read policy payload from a file in the data directory. | 1101 """Tries to read policy payload from a file in the data directory. |
1061 | 1102 |
1062 First checks for a binary rendition of the policy protobuf in | 1103 First checks for a binary rendition of the policy protobuf in |
1063 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns | 1104 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns |
1064 it. If that file doesn't exist, tries | 1105 it. If that file doesn't exist, tries |
1065 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a | 1106 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a |
1066 protobuf using proto_message. If that fails as well, returns None. | 1107 protobuf using proto_message. If that fails as well, returns None. |
1067 | 1108 |
1068 Args: | 1109 Args: |
(...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1184 if (self.options.log_to_console): | 1225 if (self.options.log_to_console): |
1185 logger.addHandler(logging.StreamHandler()) | 1226 logger.addHandler(logging.StreamHandler()) |
1186 if (self.options.log_file): | 1227 if (self.options.log_file): |
1187 logger.addHandler(logging.FileHandler(self.options.log_file)) | 1228 logger.addHandler(logging.FileHandler(self.options.log_file)) |
1188 | 1229 |
1189 testserver_base.TestServerRunner.run_server(self) | 1230 testserver_base.TestServerRunner.run_server(self) |
1190 | 1231 |
1191 | 1232 |
1192 if __name__ == '__main__': | 1233 if __name__ == '__main__': |
1193 sys.exit(PolicyServerRunner().main()) | 1234 sys.exit(PolicyServerRunner().main()) |
OLD | NEW |