blob: 1ccf35af435efb67463deedf4c2f3897f644de83 [file] [log] [blame]
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +00001# 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
3# found in the LICENSE file.
4
5"""A bare-bones test server for testing cloud policy support.
6
7This implements a simple cloud policy test server that can be used to test
8chrome's device management service client. The policy information is read from
9the file named device_management in the server's data directory. It contains
10enforced and recommended policies for the device and user scope, and a list
11of managed users.
12
13The format of the file is JSON. The root dictionary contains a list under the
14key "managed_users". It contains auth tokens for which the server will claim
15that the user is managed. The token string "*" indicates that all users are
16claimed to be managed. Other keys in the root dictionary identify request
17scopes. The user-request scope is described by a dictionary that holds two
18sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
19definitions as key/value stores, their format is identical to what the Linux
20implementation reads from /etc.
21The device-scope holds the policy-definition directly as key/value stores in the
22protobuf-format.
23
24Example:
25
26{
27 "google/chromeos/device" : {
28 "guest_mode_enabled" : false
29 },
30 "google/chromeos/user" : {
31 "mandatory" : {
32 "HomepageLocation" : "http://www.chromium.org",
33 "IncognitoEnabled" : false
34 },
35 "recommended" : {
36 "JavascriptEnabled": false
37 }
38 },
39 "google/chromeos/publicaccount/user@example.com" : {
40 "mandatory" : {
41 "HomepageLocation" : "http://www.chromium.org"
42 },
43 "recommended" : {
44 }
45 },
46 "managed_users" : [
47 "secret123456"
48 ],
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +010049 "current_key_index": 0,
50 "robot_api_auth_code": "fake_auth_code"
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +000051}
52
53"""
54
55import BaseHTTPServer
56import cgi
57import google.protobuf.text_format
58import hashlib
59import logging
60import os
61import random
62import re
63import sys
64import time
65import tlslite
66import tlslite.api
67import tlslite.utils
68import tlslite.utils.cryptomath
69
70# The name and availability of the json module varies in python versions.
71try:
72 import simplejson as json
73except ImportError:
74 try:
75 import json
76 except ImportError:
77 json = None
78
79import asn1der
80import testserver_base
81
82import device_management_backend_pb2 as dm
83import cloud_policy_pb2 as cp
84import chrome_extension_policy_pb2 as ep
85
86# Device policy is only available on Chrome OS builds.
87try:
88 import chrome_device_policy_pb2 as dp
89except ImportError:
90 dp = None
91
92# ASN.1 object identifier for PKCS#1/RSA.
93PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
94
95# SHA256 sum of "0".
96SHA256_0 = hashlib.sha256('0').digest()
97
98# List of bad machine identifiers that trigger the |valid_serial_number_missing|
99# flag to be set set in the policy fetch response.
100BAD_MACHINE_IDS = [ '123490EN400015' ]
101
102# List of machines that trigger the server to send kiosk enrollment response
103# for the register request.
104KIOSK_MACHINE_IDS = [ 'KIOSK' ]
105
106
107class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
108 """Decodes and handles device management requests from clients.
109
110 The handler implements all the request parsing and protobuf message decoding
111 and encoding. It calls back into the server to lookup, register, and
112 unregister clients.
113 """
114
115 def __init__(self, request, client_address, server):
116 """Initialize the handler.
117
118 Args:
119 request: The request data received from the client as a string.
120 client_address: The client address.
121 server: The TestServer object to use for (un)registering clients.
122 """
123 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
124 client_address, server)
125
126 def GetUniqueParam(self, name):
127 """Extracts a unique query parameter from the request.
128
129 Args:
130 name: Names the parameter to fetch.
131 Returns:
132 The parameter value or None if the parameter doesn't exist or is not
133 unique.
134 """
135 if not hasattr(self, '_params'):
136 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:])
137
138 param_list = self._params.get(name, [])
139 if len(param_list) == 1:
140 return param_list[0]
141 return None
142
143 def do_GET(self):
144 """Handles GET requests.
145
146 Currently this is only used to serve external policy data."""
147 sep = self.path.find('?')
148 path = self.path if sep == -1 else self.path[:sep]
149 if path == '/externalpolicydata':
150 http_response, raw_reply = self.HandleExternalPolicyDataRequest()
151 else:
152 http_response = 404
153 raw_reply = 'Invalid path'
154 self.send_response(http_response)
155 self.end_headers()
156 self.wfile.write(raw_reply)
157
158 def do_POST(self):
159 http_response, raw_reply = self.HandleRequest()
160 self.send_response(http_response)
161 if (http_response == 200):
162 self.send_header('Content-Type', 'application/x-protobuffer')
163 self.end_headers()
164 self.wfile.write(raw_reply)
165
166 def HandleExternalPolicyDataRequest(self):
167 """Handles a request to download policy data for a component."""
168 policy_key = self.GetUniqueParam('key')
169 if not policy_key:
170 return (400, 'Missing key parameter')
171 data = self.server.ReadPolicyDataFromDataDir(policy_key)
172 if data is None:
173 return (404, 'Policy not found for ' + policy_key)
174 return (200, data)
175
176 def HandleRequest(self):
177 """Handles a request.
178
179 Parses the data supplied at construction time and returns a pair indicating
180 http status code and response data to be sent back to the client.
181
182 Returns:
183 A tuple of HTTP status code and response data to send to the client.
184 """
185 rmsg = dm.DeviceManagementRequest()
186 length = int(self.headers.getheader('content-length'))
187 rmsg.ParseFromString(self.rfile.read(length))
188
189 logging.debug('gaia auth token -> ' +
190 self.headers.getheader('Authorization', ''))
191 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
192 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
193 self.DumpMessage('Request', rmsg)
194
195 request_type = self.GetUniqueParam('request')
196 # Check server side requirements, as defined in
197 # device_management_backend.proto.
198 if (self.GetUniqueParam('devicetype') != '2' or
199 self.GetUniqueParam('apptype') != 'Chrome' or
200 (request_type != 'ping' and
201 len(self.GetUniqueParam('deviceid')) >= 64) or
202 len(self.GetUniqueParam('agent')) >= 64):
203 return (400, 'Invalid request parameter')
204 if request_type == 'register':
205 return self.ProcessRegister(rmsg.register_request)
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100206 if request_type == 'api_authorization':
207 return self.ProcessApiAuthorization(rmsg.service_api_access_request)
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000208 elif request_type == 'unregister':
209 return self.ProcessUnregister(rmsg.unregister_request)
210 elif request_type == 'policy' or request_type == 'ping':
211 return self.ProcessPolicy(rmsg.policy_request, request_type)
212 elif request_type == 'enterprise_check':
213 return self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
214 else:
215 return (400, 'Invalid request parameter')
216
217 def CreatePolicyForExternalPolicyData(self, policy_key):
218 """Returns an ExternalPolicyData protobuf for policy_key.
219
220 If there is policy data for policy_key then the download url will be
221 set so that it points to that data, and the appropriate hash is also set.
222 Otherwise, the protobuf will be empty.
223
224 Args:
225 policy_key: the policy type and settings entity id, joined by '/'.
226
227 Returns:
228 A serialized ExternalPolicyData.
229 """
230 settings = ep.ExternalPolicyData()
231 data = self.server.ReadPolicyDataFromDataDir(policy_key)
232 if data:
233 settings.download_url = ('http://%s:%s/externalpolicydata?key=%s' %
234 (self.server.server_name,
235 self.server.server_port,
236 policy_key) )
237 settings.secure_hash = hashlib.sha1(data).digest()
238 return settings.SerializeToString()
239
240 def CheckGoogleLogin(self):
241 """Extracts the auth token from the request and returns it. The token may
242 either be a GoogleLogin token from an Authorization header, or an OAuth V2
243 token from the oauth_token query parameter. Returns None if no token is
244 present.
245 """
246 oauth_token = self.GetUniqueParam('oauth_token')
247 if oauth_token:
248 return oauth_token
249
250 match = re.match('GoogleLogin auth=(\\w+)',
251 self.headers.getheader('Authorization', ''))
252 if match:
253 return match.group(1)
254
255 return None
256
257 def ProcessRegister(self, msg):
258 """Handles a register request.
259
260 Checks the query for authorization and device identifier, registers the
261 device with the server and constructs a response.
262
263 Args:
264 msg: The DeviceRegisterRequest message received from the client.
265
266 Returns:
267 A tuple of HTTP status code and response data to send to the client.
268 """
269 # Check the auth token and device ID.
270 auth = self.CheckGoogleLogin()
271 if not auth:
272 return (403, 'No authorization')
273
274 policy = self.server.GetPolicies()
275 if ('*' not in policy['managed_users'] and
276 auth not in policy['managed_users']):
277 return (403, 'Unmanaged')
278
279 device_id = self.GetUniqueParam('deviceid')
280 if not device_id:
281 return (400, 'Missing device identifier')
282
283 token_info = self.server.RegisterDevice(device_id,
284 msg.machine_id,
285 msg.type)
286
287 # Send back the reply.
288 response = dm.DeviceManagementResponse()
289 response.register_response.device_management_token = (
290 token_info['device_token'])
291 response.register_response.machine_name = token_info['machine_name']
292 response.register_response.enrollment_type = token_info['enrollment_mode']
293
294 self.DumpMessage('Response', response)
295
296 return (200, response.SerializeToString())
297
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100298 def ProcessApiAuthorization(self, msg):
299 """Handles an API authorization request.
300
301 Args:
302 msg: The DeviceServiceApiAccessRequest message received from the client.
303
304 Returns:
305 A tuple of HTTP status code and response data to send to the client.
306 """
307 policy = self.server.GetPolicies()
308
309 # Return the auth code from the config file if it's defined,
310 # else return a descriptive default value.
311 response = dm.DeviceManagementResponse()
312 response.service_api_access_response.auth_code = policy.get(
Ben Murdochbb1529c2013-08-08 10:24:53 +0100313 'robot_api_auth_code', 'policy_testserver.py-auth_code')
Torne (Richard Coles)c2e0dbd2013-05-09 18:35:53 +0100314 self.DumpMessage('Response', response)
315
316 return (200, response.SerializeToString())
317
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000318 def ProcessUnregister(self, msg):
319 """Handles a register request.
320
321 Checks for authorization, unregisters the device and constructs the
322 response.
323
324 Args:
325 msg: The DeviceUnregisterRequest message received from the client.
326
327 Returns:
328 A tuple of HTTP status code and response data to send to the client.
329 """
330 # Check the management token.
331 token, response = self.CheckToken()
332 if not token:
333 return response
334
335 # Unregister the device.
336 self.server.UnregisterDevice(token['device_token'])
337
338 # Prepare and send the response.
339 response = dm.DeviceManagementResponse()
340 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
341
342 self.DumpMessage('Response', response)
343
344 return (200, response.SerializeToString())
345
346 def ProcessPolicy(self, msg, request_type):
347 """Handles a policy request.
348
349 Checks for authorization, encodes the policy into protobuf representation
350 and constructs the response.
351
352 Args:
353 msg: The DevicePolicyRequest message received from the client.
354
355 Returns:
356 A tuple of HTTP status code and response data to send to the client.
357 """
358 token_info, error = self.CheckToken()
359 if not token_info:
360 return error
361
362 response = dm.DeviceManagementResponse()
363 for request in msg.request:
364 fetch_response = response.policy_response.response.add()
365 if (request.policy_type in
366 ('google/chrome/user',
367 'google/chromeos/user',
368 'google/chromeos/device',
369 'google/chromeos/publicaccount',
370 'google/chrome/extension')):
371 if request_type != 'policy':
372 fetch_response.error_code = 400
373 fetch_response.error_message = 'Invalid request type'
374 else:
375 self.ProcessCloudPolicy(request, token_info, fetch_response)
376 else:
377 fetch_response.error_code = 400
378 fetch_response.error_message = 'Invalid policy_type'
379
380 return (200, response.SerializeToString())
381
382 def ProcessAutoEnrollment(self, msg):
383 """Handles an auto-enrollment check request.
384
385 The reply depends on the value of the modulus:
386 1: replies with no new modulus and the sha256 hash of "0"
387 2: replies with a new modulus, 4.
388 4: replies with a new modulus, 2.
389 8: fails with error 400.
390 16: replies with a new modulus, 16.
391 32: replies with a new modulus, 1.
392 anything else: replies with no new modulus and an empty list of hashes
393
394 These allow the client to pick the testing scenario its wants to simulate.
395
396 Args:
397 msg: The DeviceAutoEnrollmentRequest message received from the client.
398
399 Returns:
400 A tuple of HTTP status code and response data to send to the client.
401 """
402 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
403
404 if msg.modulus == 1:
405 auto_enrollment_response.hash.append(SHA256_0)
406 elif msg.modulus == 2:
407 auto_enrollment_response.expected_modulus = 4
408 elif msg.modulus == 4:
409 auto_enrollment_response.expected_modulus = 2
410 elif msg.modulus == 8:
411 return (400, 'Server error')
412 elif msg.modulus == 16:
413 auto_enrollment_response.expected_modulus = 16
414 elif msg.modulus == 32:
415 auto_enrollment_response.expected_modulus = 1
416
417 response = dm.DeviceManagementResponse()
418 response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
419 return (200, response.SerializeToString())
420
421 def SetProtobufMessageField(self, group_message, field, field_value):
422 '''Sets a field in a protobuf message.
423
424 Args:
425 group_message: The protobuf message.
426 field: The field of the message to set, it should be a member of
427 group_message.DESCRIPTOR.fields.
428 field_value: The value to set.
429 '''
430 if field.label == field.LABEL_REPEATED:
431 assert type(field_value) == list
432 entries = group_message.__getattribute__(field.name)
433 if field.message_type is None:
434 for list_item in field_value:
435 entries.append(list_item)
436 else:
437 # This field is itself a protobuf.
438 sub_type = field.message_type
439 for sub_value in field_value:
440 assert type(sub_value) == dict
441 # Add a new sub-protobuf per list entry.
442 sub_message = entries.add()
443 # Now iterate over its fields and recursively add them.
444 for sub_field in sub_message.DESCRIPTOR.fields:
445 if sub_field.name in sub_value:
446 value = sub_value[sub_field.name]
447 self.SetProtobufMessageField(sub_message, sub_field, value)
448 return
449 elif field.type == field.TYPE_BOOL:
450 assert type(field_value) == bool
451 elif field.type == field.TYPE_STRING:
452 assert type(field_value) == str or type(field_value) == unicode
453 elif field.type == field.TYPE_INT64:
454 assert type(field_value) == int
455 elif (field.type == field.TYPE_MESSAGE and
456 field.message_type.name == 'StringList'):
457 assert type(field_value) == list
458 entries = group_message.__getattribute__(field.name).entries
459 for list_item in field_value:
460 entries.append(list_item)
461 return
462 else:
463 raise Exception('Unknown field type %s' % field.type)
464 group_message.__setattr__(field.name, field_value)
465
466 def GatherDevicePolicySettings(self, settings, policies):
467 '''Copies all the policies from a dictionary into a protobuf of type
468 CloudDeviceSettingsProto.
469
470 Args:
471 settings: The destination ChromeDeviceSettingsProto protobuf.
472 policies: The source dictionary containing policies in JSON format.
473 '''
474 for group in settings.DESCRIPTOR.fields:
475 # Create protobuf message for group.
476 group_message = eval('dp.' + group.message_type.name + '()')
477 # Indicates if at least one field was set in |group_message|.
478 got_fields = False
479 # Iterate over fields of the message and feed them from the
480 # policy config file.
481 for field in group_message.DESCRIPTOR.fields:
482 field_value = None
483 if field.name in policies:
484 got_fields = True
485 field_value = policies[field.name]
486 self.SetProtobufMessageField(group_message, field, field_value)
487 if got_fields:
488 settings.__getattribute__(group.name).CopyFrom(group_message)
489
490 def GatherUserPolicySettings(self, settings, policies):
491 '''Copies all the policies from a dictionary into a protobuf of type
492 CloudPolicySettings.
493
494 Args:
495 settings: The destination: a CloudPolicySettings protobuf.
496 policies: The source: a dictionary containing policies under keys
497 'recommended' and 'mandatory'.
498 '''
499 for field in settings.DESCRIPTOR.fields:
500 # |field| is the entry for a specific policy in the top-level
501 # CloudPolicySettings proto.
502
503 # Look for this policy's value in the mandatory or recommended dicts.
504 if field.name in policies.get('mandatory', {}):
505 mode = cp.PolicyOptions.MANDATORY
506 value = policies['mandatory'][field.name]
507 elif field.name in policies.get('recommended', {}):
508 mode = cp.PolicyOptions.RECOMMENDED
509 value = policies['recommended'][field.name]
510 else:
511 continue
512
513 # Create protobuf message for this policy.
514 policy_message = eval('cp.' + field.message_type.name + '()')
515 policy_message.policy_options.mode = mode
516 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
517 self.SetProtobufMessageField(policy_message, field_descriptor, value)
518 settings.__getattribute__(field.name).CopyFrom(policy_message)
519
520 def ProcessCloudPolicy(self, msg, token_info, response):
521 """Handles a cloud policy request. (New protocol for policy requests.)
522
523 Encodes the policy into protobuf representation, signs it and constructs
524 the response.
525
526 Args:
527 msg: The CloudPolicyRequest message received from the client.
528 token_info: the token extracted from the request.
529 response: A PolicyFetchResponse message that should be filled with the
530 response data.
531 """
532
533 if msg.machine_id:
534 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
535
536 # Response is only given if the scope is specified in the config file.
537 # Normally 'google/chromeos/device', 'google/chromeos/user' and
538 # 'google/chromeos/publicaccount' should be accepted.
539 policy = self.server.GetPolicies()
540 policy_value = ''
541 policy_key = msg.policy_type
542 if msg.settings_entity_id:
543 policy_key += '/' + msg.settings_entity_id
544 if msg.policy_type in token_info['allowed_policy_types']:
545 if (msg.policy_type == 'google/chromeos/user' or
546 msg.policy_type == 'google/chrome/user' or
547 msg.policy_type == 'google/chromeos/publicaccount'):
548 settings = cp.CloudPolicySettings()
549 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
550 if payload is None:
551 self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
552 payload = settings.SerializeToString()
553 elif dp is not None and msg.policy_type == 'google/chromeos/device':
554 settings = dp.ChromeDeviceSettingsProto()
555 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
556 if payload is None:
557 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
558 payload = settings.SerializeToString()
559 elif msg.policy_type == 'google/chrome/extension':
560 settings = ep.ExternalPolicyData()
561 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
562 if payload is None:
563 payload = self.CreatePolicyForExternalPolicyData(policy_key)
564 else:
565 response.error_code = 400
566 response.error_message = 'Invalid policy type'
567 return
568 else:
569 response.error_code = 400
570 response.error_message = 'Request not allowed for the token used'
571 return
572
573 # Sign with 'current_key_index', defaulting to key 0.
574 signing_key = None
575 req_key = None
576 current_key_index = policy.get('current_key_index', 0)
577 nkeys = len(self.server.keys)
578 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
579 current_key_index in range(nkeys)):
580 signing_key = self.server.keys[current_key_index]
581 if msg.public_key_version in range(1, nkeys + 1):
582 # requested key exists, use for signing and rotate.
583 req_key = self.server.keys[msg.public_key_version - 1]['private_key']
584
585 # Fill the policy data protobuf.
586 policy_data = dm.PolicyData()
587 policy_data.policy_type = msg.policy_type
588 policy_data.timestamp = int(time.time() * 1000)
589 policy_data.request_token = token_info['device_token']
590 policy_data.policy_value = payload
591 policy_data.machine_name = token_info['machine_name']
592 policy_data.valid_serial_number_missing = (
593 token_info['machine_id'] in BAD_MACHINE_IDS)
594 policy_data.settings_entity_id = msg.settings_entity_id
Ben Murdochbb1529c2013-08-08 10:24:53 +0100595 policy_data.service_account_identity = policy.get(
596 'service_account_identity',
597 'policy_testserver.py-service_account_identity')
Torne (Richard Coles)2a99a7e2013-03-28 15:31:22 +0000598
599 if signing_key:
600 policy_data.public_key_version = current_key_index + 1
601 if msg.policy_type == 'google/chromeos/publicaccount':
602 policy_data.username = msg.settings_entity_id
603 else:
604 # For regular user/device policy, there is no way for the testserver to
605 # know the user name belonging to the GAIA auth token we received (short
606 # of actually talking to GAIA). To address this, we read the username from
607 # the policy configuration dictionary, or use a default.
608 policy_data.username = policy.get('policy_user', 'user@example.com')
609 policy_data.device_id = token_info['device_id']
610 signed_data = policy_data.SerializeToString()
611
612 response.policy_data = signed_data
613 if signing_key:
614 response.policy_data_signature = (
615 signing_key['private_key'].hashAndSign(signed_data).tostring())
616 if msg.public_key_version != current_key_index + 1:
617 response.new_public_key = signing_key['public_key']
618 if req_key:
619 response.new_public_key_signature = (
620 req_key.hashAndSign(response.new_public_key).tostring())
621
622 self.DumpMessage('Response', response)
623
624 return (200, response.SerializeToString())
625
626 def CheckToken(self):
627 """Helper for checking whether the client supplied a valid DM token.
628
629 Extracts the token from the request and passed to the server in order to
630 look up the client.
631
632 Returns:
633 A pair of token information record and error response. If the first
634 element is None, then the second contains an error code to send back to
635 the client. Otherwise the first element is the same structure that is
636 returned by LookupToken().
637 """
638 error = 500
639 dmtoken = None
640 request_device_id = self.GetUniqueParam('deviceid')
641 match = re.match('GoogleDMToken token=(\\w+)',
642 self.headers.getheader('Authorization', ''))
643 if match:
644 dmtoken = match.group(1)
645 if not dmtoken:
646 error = 401
647 else:
648 token_info = self.server.LookupToken(dmtoken)
649 if (not token_info or
650 not request_device_id or
651 token_info['device_id'] != request_device_id):
652 error = 410
653 else:
654 return (token_info, None)
655
656 logging.debug('Token check failed with error %d' % error)
657
658 return (None, (error, 'Server error %d' % error))
659
660 def DumpMessage(self, label, msg):
661 """Helper for logging an ASCII dump of a protobuf message."""
662 logging.debug('%s\n%s' % (label, str(msg)))
663
664
665class PolicyTestServer(testserver_base.ClientRestrictingServerMixIn,
666 testserver_base.BrokenPipeHandlerMixIn,
667 testserver_base.StoppableHTTPServer):
668 """Handles requests and keeps global service state."""
669
670 def __init__(self, server_address, data_dir, policy_path, client_state_file,
671 private_key_paths):
672 """Initializes the server.
673
674 Args:
675 server_address: Server host and port.
676 policy_path: Names the file to read JSON-formatted policy from.
677 private_key_paths: List of paths to read private keys from.
678 """
679 testserver_base.StoppableHTTPServer.__init__(self, server_address,
680 PolicyRequestHandler)
681 self._registered_tokens = {}
682 self.data_dir = data_dir
683 self.policy_path = policy_path
684 self.client_state_file = client_state_file
685
686 self.keys = []
687 if private_key_paths:
688 # Load specified keys from the filesystem.
689 for key_path in private_key_paths:
690 try:
691 key_str = open(key_path).read()
692 except IOError:
693 print 'Failed to load private key from %s' % key_path
694 continue
695
696 try:
697 key = tlslite.api.parsePEMKey(key_str, private=True)
698 except SyntaxError:
699 key = tlslite.utils.Python_RSAKey.Python_RSAKey._parsePKCS8(
700 tlslite.utils.cryptomath.stringToBytes(key_str))
701
702 assert key is not None
703 self.keys.append({ 'private_key' : key })
704 else:
705 # Generate 2 private keys if none were passed from the command line.
706 for i in range(2):
707 key = tlslite.api.generateRSAKey(512)
708 assert key is not None
709 self.keys.append({ 'private_key' : key })
710
711 # Derive the public keys from the private keys.
712 for entry in self.keys:
713 key = entry['private_key']
714
715 algorithm = asn1der.Sequence(
716 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
717 asn1der.Data(asn1der.NULL, '') ])
718 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
719 asn1der.Integer(key.e) ])
720 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
721 entry['public_key'] = pubkey
722
723 # Load client state.
724 if self.client_state_file is not None:
725 try:
726 file_contents = open(self.client_state_file).read()
727 self._registered_tokens = json.loads(file_contents)
728 except IOError:
729 pass
730
731 def GetPolicies(self):
732 """Returns the policies to be used, reloaded form the backend file every
733 time this is called.
734 """
735 policy = {}
736 if json is None:
737 print 'No JSON module, cannot parse policy information'
738 else :
739 try:
740 policy = json.loads(open(self.policy_path).read())
741 except IOError:
742 print 'Failed to load policy from %s' % self.policy_path
743 return policy
744
745 def RegisterDevice(self, device_id, machine_id, type):
746 """Registers a device or user and generates a DM token for it.
747
748 Args:
749 device_id: The device identifier provided by the client.
750
751 Returns:
752 The newly generated device token for the device.
753 """
754 dmtoken_chars = []
755 while len(dmtoken_chars) < 32:
756 dmtoken_chars.append(random.choice('0123456789abcdef'))
757 dmtoken = ''.join(dmtoken_chars)
758 allowed_policy_types = {
759 dm.DeviceRegisterRequest.BROWSER: ['google/chrome/user'],
760 dm.DeviceRegisterRequest.USER: [
761 'google/chromeos/user',
762 'google/chrome/extension'
763 ],
764 dm.DeviceRegisterRequest.DEVICE: [
765 'google/chromeos/device',
766 'google/chromeos/publicaccount'
767 ],
768 dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
769 'google/chrome/user'],
770 }
771 if machine_id in KIOSK_MACHINE_IDS:
772 enrollment_mode = dm.DeviceRegisterResponse.RETAIL
773 else:
774 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
775 self._registered_tokens[dmtoken] = {
776 'device_id': device_id,
777 'device_token': dmtoken,
778 'allowed_policy_types': allowed_policy_types[type],
779 'machine_name': 'chromeos-' + machine_id,
780 'machine_id': machine_id,
781 'enrollment_mode': enrollment_mode,
782 }
783 self.WriteClientState()
784 return self._registered_tokens[dmtoken]
785
786 def UpdateMachineId(self, dmtoken, machine_id):
787 """Updates the machine identifier for a registered device.
788
789 Args:
790 dmtoken: The device management token provided by the client.
791 machine_id: Updated hardware identifier value.
792 """
793 if dmtoken in self._registered_tokens:
794 self._registered_tokens[dmtoken]['machine_id'] = machine_id
795 self.WriteClientState()
796
797 def LookupToken(self, dmtoken):
798 """Looks up a device or a user by DM token.
799
800 Args:
801 dmtoken: The device management token provided by the client.
802
803 Returns:
804 A dictionary with information about a device or user that is registered by
805 dmtoken, or None if the token is not found.
806 """
807 return self._registered_tokens.get(dmtoken, None)
808
809 def UnregisterDevice(self, dmtoken):
810 """Unregisters a device identified by the given DM token.
811
812 Args:
813 dmtoken: The device management token provided by the client.
814 """
815 if dmtoken in self._registered_tokens.keys():
816 del self._registered_tokens[dmtoken]
817 self.WriteClientState()
818
819 def WriteClientState(self):
820 """Writes the client state back to the file."""
821 if self.client_state_file is not None:
822 json_data = json.dumps(self._registered_tokens)
823 open(self.client_state_file, 'w').write(json_data)
824
825 def GetBaseFilename(self, policy_selector):
826 """Returns the base filename for the given policy_selector.
827
828 Args:
829 policy_selector: the policy type and settings entity id, joined by '/'.
830
831 Returns:
832 The filename corresponding to the policy_selector, without a file
833 extension.
834 """
835 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
836 return os.path.join(self.data_dir or '',
837 'policy_%s' % sanitized_policy_selector)
838
839 def ReadPolicyFromDataDir(self, policy_selector, proto_message):
840 """Tries to read policy payload from a file in the data directory.
841
842 First checks for a binary rendition of the policy protobuf in
843 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
844 it. If that file doesn't exist, tries
845 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
846 protobuf using proto_message. If that fails as well, returns None.
847
848 Args:
849 policy_selector: Selects which policy to read.
850 proto_message: Optional protobuf message object used for decoding the
851 proto text format.
852
853 Returns:
854 The binary payload message, or None if not found.
855 """
856 base_filename = self.GetBaseFilename(policy_selector)
857
858 # Try the binary payload file first.
859 try:
860 return open(base_filename + '.bin').read()
861 except IOError:
862 pass
863
864 # If that fails, try the text version instead.
865 if proto_message is None:
866 return None
867
868 try:
869 text = open(base_filename + '.txt').read()
870 google.protobuf.text_format.Merge(text, proto_message)
871 return proto_message.SerializeToString()
872 except IOError:
873 return None
874 except google.protobuf.text_format.ParseError:
875 return None
876
877 def ReadPolicyDataFromDataDir(self, policy_selector):
878 """Returns the external policy data for |policy_selector| if found.
879
880 Args:
881 policy_selector: Selects which policy to read.
882
883 Returns:
884 The data for the corresponding policy type and entity id, if found.
885 """
886 base_filename = self.GetBaseFilename(policy_selector)
887 try:
888 return open(base_filename + '.data').read()
889 except IOError:
890 return None
891
892
893class PolicyServerRunner(testserver_base.TestServerRunner):
894
895 def __init__(self):
896 super(PolicyServerRunner, self).__init__()
897
898 def create_server(self, server_data):
899 data_dir = self.options.data_dir or ''
900 config_file = (self.options.config_file or
901 os.path.join(data_dir, 'device_management'))
902 server = PolicyTestServer((self.options.host, self.options.port),
903 data_dir, config_file,
904 self.options.client_state_file,
905 self.options.policy_keys)
906 server_data['port'] = server.server_port
907 return server
908
909 def add_options(self):
910 testserver_base.TestServerRunner.add_options(self)
911 self.option_parser.add_option('--client-state', dest='client_state_file',
912 help='File that client state should be '
913 'persisted to. This allows the server to be '
914 'seeded by a list of pre-registered clients '
915 'and restarts without abandoning registered '
916 'clients.')
917 self.option_parser.add_option('--policy-key', action='append',
918 dest='policy_keys',
919 help='Specify a path to a PEM-encoded '
920 'private key to use for policy signing. May '
921 'be specified multiple times in order to '
922 'load multipe keys into the server. If the '
923 'server has multiple keys, it will rotate '
924 'through them in at each request in a '
925 'round-robin fashion. The server will '
926 'generate a random key if none is specified '
927 'on the command line.')
928 self.option_parser.add_option('--log-level', dest='log_level',
929 default='WARN',
930 help='Log level threshold to use.')
931 self.option_parser.add_option('--config-file', dest='config_file',
932 help='Specify a configuration file to use '
933 'instead of the default '
934 '<data_dir>/device_management')
935
936 def run_server(self):
937 logger = logging.getLogger()
938 logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
939 if (self.options.log_to_console):
940 logger.addHandler(logging.StreamHandler())
941 if (self.options.log_file):
942 logger.addHandler(logging.FileHandler(self.options.log_file))
943
944 testserver_base.TestServerRunner.run_server(self)
945
946
947if __name__ == '__main__':
948 sys.exit(PolicyServerRunner().main())