Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(88)

Side by Side Diff: net/tools/testserver/device_management.py

Issue 12235003: Split out policy code from net/tools/testserver. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Saving a file, sherlock? ;) Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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
3 # found in the LICENSE file.
4
5 """A bare-bones test server for testing cloud policy support.
6
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
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
11 of managed users.
12
13 The format of the file is JSON. The root dictionary contains a list under the
14 key "managed_users". It contains auth tokens for which the server will claim
15 that the user is managed. The token string "*" indicates that all users are
16 claimed to be managed. Other keys in the root dictionary identify request
17 scopes. The user-request scope is described by a dictionary that holds two
18 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
19 definitions as key/value stores, their format is identical to what the Linux
20 implementation reads from /etc.
21 The device-scope holds the policy-definition directly as key/value stores in the
22 protobuf-format.
23
24 Example:
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 ]
49 }
50
51 """
52
53 import cgi
54 import hashlib
55 import logging
56 import os
57 import random
58 import re
59 import sys
60 import time
61 import tlslite
62 import tlslite.api
63 import tlslite.utils
64
65 # The name and availability of the json module varies in python versions.
66 try:
67 import simplejson as json
68 except ImportError:
69 try:
70 import json
71 except ImportError:
72 json = None
73
74 import asn1der
75 import device_management_backend_pb2 as dm
76 import cloud_policy_pb2 as cp
77 import chrome_device_policy_pb2 as dp
78
79 # ASN.1 object identifier for PKCS#1/RSA.
80 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
81
82 # SHA256 sum of "0".
83 SHA256_0 = hashlib.sha256('0').digest()
84
85 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
86 # flag to be set set in the policy fetch response.
87 BAD_MACHINE_IDS = [ '123490EN400015' ];
88
89 # List of machines that trigger the server to send kiosk enrollment response
90 # for the register request.
91 KIOSK_MACHINE_IDS = [ 'KIOSK' ];
92
93 class RequestHandler(object):
94 """Decodes and handles device management requests from clients.
95
96 The handler implements all the request parsing and protobuf message decoding
97 and encoding. It calls back into the server to lookup, register, and
98 unregister clients.
99 """
100
101 def __init__(self, server, path, headers, request):
102 """Initialize the handler.
103
104 Args:
105 server: The TestServer object to use for (un)registering clients.
106 path: A string containing the request path and query parameters.
107 headers: A rfc822.Message-like object containing HTTP headers.
108 request: The request data received from the client as a string.
109 """
110 self._server = server
111 self._path = path
112 self._headers = headers
113 self._request = request
114 self._params = None
115
116 def GetUniqueParam(self, name):
117 """Extracts a unique query parameter from the request.
118
119 Args:
120 name: Names the parameter to fetch.
121 Returns:
122 The parameter value or None if the parameter doesn't exist or is not
123 unique.
124 """
125 if not self._params:
126 self._params = cgi.parse_qs(self._path[self._path.find('?') + 1:])
127
128 param_list = self._params.get(name, [])
129 if len(param_list) == 1:
130 return param_list[0]
131 return None;
132
133 def HandleRequest(self):
134 """Handles a request.
135
136 Parses the data supplied at construction time and returns a pair indicating
137 http status code and response data to be sent back to the client.
138
139 Returns:
140 A tuple of HTTP status code and response data to send to the client.
141 """
142 rmsg = dm.DeviceManagementRequest()
143 rmsg.ParseFromString(self._request)
144
145 logging.debug('gaia auth token -> ' +
146 self._headers.getheader('Authorization', ''))
147 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
148 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
149 self.DumpMessage('Request', rmsg)
150
151 request_type = self.GetUniqueParam('request')
152 # Check server side requirements, as defined in
153 # device_management_backend.proto.
154 if (self.GetUniqueParam('devicetype') != '2' or
155 self.GetUniqueParam('apptype') != 'Chrome' or
156 (request_type != 'ping' and
157 len(self.GetUniqueParam('deviceid')) >= 64) or
158 len(self.GetUniqueParam('agent')) >= 64):
159 return (400, 'Invalid request parameter')
160 if request_type == 'register':
161 return self.ProcessRegister(rmsg.register_request)
162 elif request_type == 'unregister':
163 return self.ProcessUnregister(rmsg.unregister_request)
164 elif request_type == 'policy' or request_type == 'ping':
165 return self.ProcessPolicy(rmsg.policy_request, request_type)
166 elif request_type == 'enterprise_check':
167 return self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
168 else:
169 return (400, 'Invalid request parameter')
170
171 def CheckGoogleLogin(self):
172 """Extracts the auth token from the request and returns it. The token may
173 either be a GoogleLogin token from an Authorization header, or an OAuth V2
174 token from the oauth_token query parameter. Returns None if no token is
175 present.
176 """
177 oauth_token = self.GetUniqueParam('oauth_token')
178 if oauth_token:
179 return oauth_token
180
181 match = re.match('GoogleLogin auth=(\\w+)',
182 self._headers.getheader('Authorization', ''))
183 if match:
184 return match.group(1)
185
186 return None
187
188 def ProcessRegister(self, msg):
189 """Handles a register request.
190
191 Checks the query for authorization and device identifier, registers the
192 device with the server and constructs a response.
193
194 Args:
195 msg: The DeviceRegisterRequest message received from the client.
196
197 Returns:
198 A tuple of HTTP status code and response data to send to the client.
199 """
200 # Check the auth token and device ID.
201 auth = self.CheckGoogleLogin()
202 if not auth:
203 return (403, 'No authorization')
204
205 policy = self._server.GetPolicies()
206 if ('*' not in policy['managed_users'] and
207 auth not in policy['managed_users']):
208 return (403, 'Unmanaged')
209
210 device_id = self.GetUniqueParam('deviceid')
211 if not device_id:
212 return (400, 'Missing device identifier')
213
214 token_info = self._server.RegisterDevice(device_id,
215 msg.machine_id,
216 msg.type)
217
218 # Send back the reply.
219 response = dm.DeviceManagementResponse()
220 response.register_response.device_management_token = (
221 token_info['device_token'])
222 response.register_response.machine_name = token_info['machine_name']
223 response.register_response.enrollment_type = token_info['enrollment_mode']
224
225 self.DumpMessage('Response', response)
226
227 return (200, response.SerializeToString())
228
229 def ProcessUnregister(self, msg):
230 """Handles a register request.
231
232 Checks for authorization, unregisters the device and constructs the
233 response.
234
235 Args:
236 msg: The DeviceUnregisterRequest message received from the client.
237
238 Returns:
239 A tuple of HTTP status code and response data to send to the client.
240 """
241 # Check the management token.
242 token, response = self.CheckToken();
243 if not token:
244 return response
245
246 # Unregister the device.
247 self._server.UnregisterDevice(token['device_token']);
248
249 # Prepare and send the response.
250 response = dm.DeviceManagementResponse()
251 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
252
253 self.DumpMessage('Response', response)
254
255 return (200, response.SerializeToString())
256
257 def ProcessPolicy(self, msg, request_type):
258 """Handles a policy request.
259
260 Checks for authorization, encodes the policy into protobuf representation
261 and constructs the response.
262
263 Args:
264 msg: The DevicePolicyRequest 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 for request in msg.request:
270 if (request.policy_type in
271 ('google/chrome/user',
272 'google/chromeos/user',
273 'google/chromeos/device',
274 'google/chromeos/publicaccount')):
275 if request_type != 'policy':
276 return (400, 'Invalid request type')
277 else:
278 return self.ProcessCloudPolicy(request)
279 else:
280 return (400, 'Invalid policy_type')
281
282 def ProcessAutoEnrollment(self, msg):
283 """Handles an auto-enrollment check request.
284
285 The reply depends on the value of the modulus:
286 1: replies with no new modulus and the sha256 hash of "0"
287 2: replies with a new modulus, 4.
288 4: replies with a new modulus, 2.
289 8: fails with error 400.
290 16: replies with a new modulus, 16.
291 32: replies with a new modulus, 1.
292 anything else: replies with no new modulus and an empty list of hashes
293
294 These allow the client to pick the testing scenario its wants to simulate.
295
296 Args:
297 msg: The DeviceAutoEnrollmentRequest message received from the client.
298
299 Returns:
300 A tuple of HTTP status code and response data to send to the client.
301 """
302 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
303
304 if msg.modulus == 1:
305 auto_enrollment_response.hash.append(SHA256_0)
306 elif msg.modulus == 2:
307 auto_enrollment_response.expected_modulus = 4
308 elif msg.modulus == 4:
309 auto_enrollment_response.expected_modulus = 2
310 elif msg.modulus == 8:
311 return (400, 'Server error')
312 elif msg.modulus == 16:
313 auto_enrollment_response.expected_modulus = 16
314 elif msg.modulus == 32:
315 auto_enrollment_response.expected_modulus = 1
316
317 response = dm.DeviceManagementResponse()
318 response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
319 return (200, response.SerializeToString())
320
321 def SetProtobufMessageField(self, group_message, field, field_value):
322 '''Sets a field in a protobuf message.
323
324 Args:
325 group_message: The protobuf message.
326 field: The field of the message to set, it should be a member of
327 group_message.DESCRIPTOR.fields.
328 field_value: The value to set.
329 '''
330 if field.label == field.LABEL_REPEATED:
331 assert type(field_value) == list
332 entries = group_message.__getattribute__(field.name)
333 if field.message_type is None:
334 for list_item in field_value:
335 entries.append(list_item)
336 else:
337 # This field is itself a protobuf.
338 sub_type = field.message_type
339 for sub_value in field_value:
340 assert type(sub_value) == dict
341 # Add a new sub-protobuf per list entry.
342 sub_message = entries.add()
343 # Now iterate over its fields and recursively add them.
344 for sub_field in sub_message.DESCRIPTOR.fields:
345 if sub_field.name in sub_value:
346 value = sub_value[sub_field.name]
347 self.SetProtobufMessageField(sub_message, sub_field, value)
348 return
349 elif field.type == field.TYPE_BOOL:
350 assert type(field_value) == bool
351 elif field.type == field.TYPE_STRING:
352 assert type(field_value) == str or type(field_value) == unicode
353 elif field.type == field.TYPE_INT64:
354 assert type(field_value) == int
355 elif (field.type == field.TYPE_MESSAGE and
356 field.message_type.name == 'StringList'):
357 assert type(field_value) == list
358 entries = group_message.__getattribute__(field.name).entries
359 for list_item in field_value:
360 entries.append(list_item)
361 return
362 else:
363 raise Exception('Unknown field type %s' % field.type)
364 group_message.__setattr__(field.name, field_value)
365
366 def GatherDevicePolicySettings(self, settings, policies):
367 '''Copies all the policies from a dictionary into a protobuf of type
368 CloudDeviceSettingsProto.
369
370 Args:
371 settings: The destination ChromeDeviceSettingsProto protobuf.
372 policies: The source dictionary containing policies in JSON format.
373 '''
374 for group in settings.DESCRIPTOR.fields:
375 # Create protobuf message for group.
376 group_message = eval('dp.' + group.message_type.name + '()')
377 # Indicates if at least one field was set in |group_message|.
378 got_fields = False
379 # Iterate over fields of the message and feed them from the
380 # policy config file.
381 for field in group_message.DESCRIPTOR.fields:
382 field_value = None
383 if field.name in policies:
384 got_fields = True
385 field_value = policies[field.name]
386 self.SetProtobufMessageField(group_message, field, field_value)
387 if got_fields:
388 settings.__getattribute__(group.name).CopyFrom(group_message)
389
390 def GatherUserPolicySettings(self, settings, policies):
391 '''Copies all the policies from a dictionary into a protobuf of type
392 CloudPolicySettings.
393
394 Args:
395 settings: The destination: a CloudPolicySettings protobuf.
396 policies: The source: a dictionary containing policies under keys
397 'recommended' and 'mandatory'.
398 '''
399 for field in settings.DESCRIPTOR.fields:
400 # |field| is the entry for a specific policy in the top-level
401 # CloudPolicySettings proto.
402
403 # Look for this policy's value in the mandatory or recommended dicts.
404 if field.name in policies.get('mandatory', {}):
405 mode = cp.PolicyOptions.MANDATORY
406 value = policies['mandatory'][field.name]
407 elif field.name in policies.get('recommended', {}):
408 mode = cp.PolicyOptions.RECOMMENDED
409 value = policies['recommended'][field.name]
410 else:
411 continue
412
413 # Create protobuf message for this policy.
414 policy_message = eval('cp.' + field.message_type.name + '()')
415 policy_message.policy_options.mode = mode
416 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
417 self.SetProtobufMessageField(policy_message, field_descriptor, value)
418 settings.__getattribute__(field.name).CopyFrom(policy_message)
419
420 def ProcessCloudPolicy(self, msg):
421 """Handles a cloud policy request. (New protocol for policy requests.)
422
423 Checks for authorization, encodes the policy into protobuf representation,
424 signs it and constructs the repsonse.
425
426 Args:
427 msg: The CloudPolicyRequest message received from the client.
428
429 Returns:
430 A tuple of HTTP status code and response data to send to the client.
431 """
432
433 token_info, error = self.CheckToken()
434 if not token_info:
435 return error
436
437 if msg.machine_id:
438 self._server.UpdateMachineId(token_info['device_token'], msg.machine_id)
439
440 # Response is only given if the scope is specified in the config file.
441 # Normally 'google/chromeos/device', 'google/chromeos/user' and
442 # 'google/chromeos/publicaccount' should be accepted.
443 policy = self._server.GetPolicies()
444 policy_value = ''
445 policy_key = msg.policy_type
446 if msg.settings_entity_id:
447 policy_key += '/' + msg.settings_entity_id
448 if msg.policy_type in token_info['allowed_policy_types']:
449 if (msg.policy_type == 'google/chromeos/user' or
450 msg.policy_type == 'google/chrome/user' or
451 msg.policy_type == 'google/chromeos/publicaccount'):
452 settings = cp.CloudPolicySettings()
453 self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
454 elif msg.policy_type == 'google/chromeos/device':
455 settings = dp.ChromeDeviceSettingsProto()
456 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
457
458 # Figure out the key we want to use. If multiple keys are configured, the
459 # server will rotate through them in a round-robin fashion.
460 signing_key = None
461 req_key = None
462 key_version = 1
463 nkeys = len(self._server.keys)
464 if msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and nkeys > 0:
465 if msg.public_key_version in range(1, nkeys + 1):
466 # requested key exists, use for signing and rotate.
467 req_key = self._server.keys[msg.public_key_version - 1]['private_key']
468 key_version = (msg.public_key_version % nkeys) + 1
469 signing_key = self._server.keys[key_version - 1]
470
471 # Fill the policy data protobuf.
472 policy_data = dm.PolicyData()
473 policy_data.policy_type = msg.policy_type
474 policy_data.timestamp = int(time.time() * 1000)
475 policy_data.request_token = token_info['device_token']
476 policy_data.policy_value = settings.SerializeToString()
477 policy_data.machine_name = token_info['machine_name']
478 policy_data.valid_serial_number_missing = (
479 token_info['machine_id'] in BAD_MACHINE_IDS)
480 policy_data.settings_entity_id = msg.settings_entity_id
481
482 if signing_key:
483 policy_data.public_key_version = key_version
484 if msg.policy_type == 'google/chromeos/publicaccount':
485 policy_data.username = msg.settings_entity_id
486 else:
487 # For regular user/device policy, there is no way for the testserver to
488 # know the user name belonging to the GAIA auth token we received (short
489 # of actually talking to GAIA). To address this, we read the username from
490 # the policy configuration dictionary, or use a default.
491 policy_data.username = policy.get('policy_user', 'user@example.com')
492 policy_data.device_id = token_info['device_id']
493 signed_data = policy_data.SerializeToString()
494
495 response = dm.DeviceManagementResponse()
496 fetch_response = response.policy_response.response.add()
497 fetch_response.policy_data = signed_data
498 if signing_key:
499 fetch_response.policy_data_signature = (
500 signing_key['private_key'].hashAndSign(signed_data).tostring())
501 if msg.public_key_version != key_version:
502 fetch_response.new_public_key = signing_key['public_key']
503 if req_key:
504 fetch_response.new_public_key_signature = (
505 req_key.hashAndSign(fetch_response.new_public_key).tostring())
506
507 self.DumpMessage('Response', response)
508
509 return (200, response.SerializeToString())
510
511 def CheckToken(self):
512 """Helper for checking whether the client supplied a valid DM token.
513
514 Extracts the token from the request and passed to the server in order to
515 look up the client.
516
517 Returns:
518 A pair of token information record and error response. If the first
519 element is None, then the second contains an error code to send back to
520 the client. Otherwise the first element is the same structure that is
521 returned by LookupToken().
522 """
523 error = 500
524 dmtoken = None
525 request_device_id = self.GetUniqueParam('deviceid')
526 match = re.match('GoogleDMToken token=(\\w+)',
527 self._headers.getheader('Authorization', ''))
528 if match:
529 dmtoken = match.group(1)
530 if not dmtoken:
531 error = 401
532 else:
533 token_info = self._server.LookupToken(dmtoken)
534 if (not token_info or
535 not request_device_id or
536 token_info['device_id'] != request_device_id):
537 error = 410
538 else:
539 return (token_info, None)
540
541 logging.debug('Token check failed with error %d' % error)
542
543 return (None, (error, 'Server error %d' % error))
544
545 def DumpMessage(self, label, msg):
546 """Helper for logging an ASCII dump of a protobuf message."""
547 logging.debug('%s\n%s' % (label, str(msg)))
548
549 class TestServer(object):
550 """Handles requests and keeps global service state."""
551
552 def __init__(self, policy_path, private_key_paths):
553 """Initializes the server.
554
555 Args:
556 policy_path: Names the file to read JSON-formatted policy from.
557 private_key_paths: List of paths to read private keys from.
558 """
559 self._registered_tokens = {}
560 self.policy_path = policy_path
561
562 self.keys = []
563 if private_key_paths:
564 # Load specified keys from the filesystem.
565 for key_path in private_key_paths:
566 try:
567 key = tlslite.api.parsePEMKey(open(key_path).read(), private=True)
568 except IOError:
569 print 'Failed to load private key from %s' % key_path
570 continue
571
572 assert key is not None
573 self.keys.append({ 'private_key' : key })
574 else:
575 # Generate a key if none were specified.
576 key = tlslite.api.generateRSAKey(1024)
577 assert key is not None
578 self.keys.append({ 'private_key' : key })
579
580 # Derive the public keys from the loaded private keys.
581 for entry in self.keys:
582 key = entry['private_key']
583
584 algorithm = asn1der.Sequence(
585 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
586 asn1der.Data(asn1der.NULL, '') ])
587 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
588 asn1der.Integer(key.e) ])
589 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
590 entry['public_key'] = pubkey;
591
592 def GetPolicies(self):
593 """Returns the policies to be used, reloaded form the backend file every
594 time this is called.
595 """
596 policy = {}
597 if json is None:
598 print 'No JSON module, cannot parse policy information'
599 else :
600 try:
601 policy = json.loads(open(self.policy_path).read())
602 except IOError:
603 print 'Failed to load policy from %s' % self.policy_path
604 return policy
605
606 def HandleRequest(self, path, headers, request):
607 """Handles a request.
608
609 Args:
610 path: The request path and query parameters received from the client.
611 headers: A rfc822.Message-like object containing HTTP headers.
612 request: The request data received from the client as a string.
613 Returns:
614 A pair of HTTP status code and response data to send to the client.
615 """
616 handler = RequestHandler(self, path, headers, request)
617 return handler.HandleRequest()
618
619 def RegisterDevice(self, device_id, machine_id, type):
620 """Registers a device or user and generates a DM token for it.
621
622 Args:
623 device_id: The device identifier provided by the client.
624
625 Returns:
626 The newly generated device token for the device.
627 """
628 dmtoken_chars = []
629 while len(dmtoken_chars) < 32:
630 dmtoken_chars.append(random.choice('0123456789abcdef'))
631 dmtoken = ''.join(dmtoken_chars)
632 allowed_policy_types = {
633 dm.DeviceRegisterRequest.BROWSER: ['google/chrome/user'],
634 dm.DeviceRegisterRequest.USER: ['google/chromeos/user'],
635 dm.DeviceRegisterRequest.DEVICE: [
636 'google/chromeos/device',
637 'google/chromeos/publicaccount'
638 ],
639 dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
640 'google/chrome/user'],
641 }
642 if machine_id in KIOSK_MACHINE_IDS:
643 enrollment_mode = dm.DeviceRegisterResponse.RETAIL
644 else:
645 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
646 self._registered_tokens[dmtoken] = {
647 'device_id': device_id,
648 'device_token': dmtoken,
649 'allowed_policy_types': allowed_policy_types[type],
650 'machine_name': 'chromeos-' + machine_id,
651 'machine_id': machine_id,
652 'enrollment_mode': enrollment_mode,
653 }
654 return self._registered_tokens[dmtoken]
655
656 def UpdateMachineId(self, dmtoken, machine_id):
657 """Updates the machine identifier for a registered device.
658
659 Args:
660 dmtoken: The device management token provided by the client.
661 machine_id: Updated hardware identifier value.
662 """
663 if dmtoken in self._registered_tokens:
664 self._registered_tokens[dmtoken]['machine_id'] = machine_id
665
666 def LookupToken(self, dmtoken):
667 """Looks up a device or a user by DM token.
668
669 Args:
670 dmtoken: The device management token provided by the client.
671
672 Returns:
673 A dictionary with information about a device or user that is registered by
674 dmtoken, or None if the token is not found.
675 """
676 return self._registered_tokens.get(dmtoken, None)
677
678 def UnregisterDevice(self, dmtoken):
679 """Unregisters a device identified by the given DM token.
680
681 Args:
682 dmtoken: The device management token provided by the client.
683 """
684 if dmtoken in self._registered_tokens.keys():
685 del self._registered_tokens[dmtoken]
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698