Adding instrumentation to locate the source of jankiness
[chromium-blink-merge.git] / chrome / browser / policy / test / policy_testserver.py
bloba08eac10b01645ad7fbfc651423ce6dedc368146
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.
5 """A bare-bones test server for testing cloud policy support.
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.
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.
24 Example:
27 "google/chromeos/device" : {
28 "guest_mode_enabled" : false
30 "google/chromeos/user" : {
31 "mandatory" : {
32 "HomepageLocation" : "http://www.chromium.org",
33 "IncognitoEnabled" : false
35 "recommended" : {
36 "JavascriptEnabled": false
39 "google/chromeos/publicaccount/user@example.com" : {
40 "mandatory" : {
41 "HomepageLocation" : "http://www.chromium.org"
43 "recommended" : {
46 "managed_users" : [
47 "secret123456"
49 "current_key_index": 0,
50 "robot_api_auth_code": "fake_auth_code",
51 "invalidation_source": 1025,
52 "invalidation_name": "UENUPOL"
55 """
57 import base64
58 import BaseHTTPServer
59 import cgi
60 import glob
61 import google.protobuf.text_format
62 import hashlib
63 import logging
64 import os
65 import random
66 import re
67 import sys
68 import time
69 import tlslite
70 import tlslite.api
71 import tlslite.utils
72 import tlslite.utils.cryptomath
73 import urlparse
75 # The name and availability of the json module varies in python versions.
76 try:
77 import simplejson as json
78 except ImportError:
79 try:
80 import json
81 except ImportError:
82 json = None
84 import asn1der
85 import testserver_base
87 import device_management_backend_pb2 as dm
88 import cloud_policy_pb2 as cp
89 import chrome_extension_policy_pb2 as ep
91 # Device policy is only available on Chrome OS builds.
92 try:
93 import chrome_device_policy_pb2 as dp
94 except ImportError:
95 dp = None
97 # ASN.1 object identifier for PKCS#1/RSA.
98 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
100 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
101 # flag to be set set in the policy fetch response.
102 BAD_MACHINE_IDS = [ '123490EN400015' ]
104 # List of machines that trigger the server to send kiosk enrollment response
105 # for the register request.
106 KIOSK_MACHINE_IDS = [ 'KIOSK' ]
108 # Dictionary containing base64-encoded policy signing keys plus per-domain
109 # signatures. Format is:
111 # 'key': <base64-encoded PKCS8-format private key>,
112 # 'signatures': {
113 # <domain1>: <base64-encdoded SHA256 signature for key + domain1>
114 # <domain2>: <base64-encdoded SHA256 signature for key + domain2>
115 # ...
118 SIGNING_KEYS = [
119 # Key1
120 {'key':
121 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1'
122 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ'
123 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9'
124 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC'
125 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R'
126 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ'
127 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq',
128 'signatures':
129 {'example.com':
130 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn'
131 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS'
132 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1'
133 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl'
134 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J'
135 '3+tfxjmoA/dg==',
136 'chromepolicytest.com':
137 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP'
138 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A'
139 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA'
140 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC'
141 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO'
142 'N+Z7IwElzTKg==',
143 'managedchrome.com':
144 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej'
145 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW'
146 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP'
147 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv'
148 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo'
149 'UExd9roA9a5w==',
152 # Key2
153 {'key':
154 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49'
155 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ'
156 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL'
157 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C'
158 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy'
159 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi'
160 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=',
161 'signatures':
162 # Key2 signatures
163 {'example.com':
164 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/'
165 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4'
166 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x'
167 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM'
168 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin'
169 '9Be9gs3W+Aww==',
170 'chromepolicytest.com':
171 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o'
172 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh'
173 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL'
174 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht'
175 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF'
176 'Z2PGxHTQ9JNA==',
177 'managedchrome.com':
178 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn'
179 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu'
180 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo'
181 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw'
182 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1'
183 'DBvBYwi20vbQ==',
188 class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
189 """Decodes and handles device management requests from clients.
191 The handler implements all the request parsing and protobuf message decoding
192 and encoding. It calls back into the server to lookup, register, and
193 unregister clients.
196 def __init__(self, request, client_address, server):
197 """Initialize the handler.
199 Args:
200 request: The request data received from the client as a string.
201 client_address: The client address.
202 server: The TestServer object to use for (un)registering clients.
204 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
205 client_address, server)
207 def GetUniqueParam(self, name):
208 """Extracts a unique query parameter from the request.
210 Args:
211 name: Names the parameter to fetch.
212 Returns:
213 The parameter value or None if the parameter doesn't exist or is not
214 unique.
216 if not hasattr(self, '_params'):
217 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:])
219 param_list = self._params.get(name, [])
220 if len(param_list) == 1:
221 return param_list[0]
222 return None
224 def do_GET(self):
225 """Handles GET requests.
227 Currently this is only used to serve external policy data."""
228 sep = self.path.find('?')
229 path = self.path if sep == -1 else self.path[:sep]
230 if path == '/externalpolicydata':
231 http_response, raw_reply = self.HandleExternalPolicyDataRequest()
232 elif path == '/configuration/test/exit':
233 # This is not part of the standard DM server protocol.
234 # This extension is added to make the test server exit gracefully
235 # when the test is complete.
236 self.server.stop = True
237 http_response = 200
238 raw_reply = 'OK'
239 else:
240 http_response = 404
241 raw_reply = 'Invalid path'
242 self.send_response(http_response)
243 self.end_headers()
244 self.wfile.write(raw_reply)
246 def do_POST(self):
247 http_response, raw_reply = self.HandleRequest()
248 self.send_response(http_response)
249 if (http_response == 200):
250 self.send_header('Content-Type', 'application/x-protobuffer')
251 self.end_headers()
252 self.wfile.write(raw_reply)
254 def HandleExternalPolicyDataRequest(self):
255 """Handles a request to download policy data for a component."""
256 policy_key = self.GetUniqueParam('key')
257 if not policy_key:
258 return (400, 'Missing key parameter')
259 data = self.server.ReadPolicyDataFromDataDir(policy_key)
260 if data is None:
261 return (404, 'Policy not found for ' + policy_key)
262 return (200, data)
264 def HandleRequest(self):
265 """Handles a request.
267 Parses the data supplied at construction time and returns a pair indicating
268 http status code and response data to be sent back to the client.
270 Returns:
271 A tuple of HTTP status code and response data to send to the client.
273 rmsg = dm.DeviceManagementRequest()
274 length = int(self.headers.getheader('content-length'))
275 rmsg.ParseFromString(self.rfile.read(length))
277 logging.debug('gaia auth token -> ' +
278 self.headers.getheader('Authorization', ''))
279 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
280 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
281 self.DumpMessage('Request', rmsg)
283 request_type = self.GetUniqueParam('request')
284 # Check server side requirements, as defined in
285 # device_management_backend.proto.
286 if (self.GetUniqueParam('devicetype') != '2' or
287 self.GetUniqueParam('apptype') != 'Chrome' or
288 len(self.GetUniqueParam('deviceid')) >= 64):
289 return (400, 'Invalid request parameter')
290 if request_type == 'register':
291 response = self.ProcessRegister(rmsg.register_request)
292 elif request_type == 'api_authorization':
293 response = self.ProcessApiAuthorization(rmsg.service_api_access_request)
294 elif request_type == 'unregister':
295 response = self.ProcessUnregister(rmsg.unregister_request)
296 elif request_type == 'policy':
297 response = self.ProcessPolicy(rmsg, request_type)
298 elif request_type == 'enterprise_check':
299 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
300 elif request_type == 'device_state_retrieval':
301 response = self.ProcessDeviceStateRetrievalRequest(
302 rmsg.device_state_retrieval_request)
303 else:
304 return (400, 'Invalid request parameter')
306 self.DumpMessage('Response', response[1])
307 return (response[0], response[1].SerializeToString())
309 def CreatePolicyForExternalPolicyData(self, policy_key):
310 """Returns an ExternalPolicyData protobuf for policy_key.
312 If there is policy data for policy_key then the download url will be
313 set so that it points to that data, and the appropriate hash is also set.
314 Otherwise, the protobuf will be empty.
316 Args:
317 policy_key: The policy type and settings entity id, joined by '/'.
319 Returns:
320 A serialized ExternalPolicyData.
322 settings = ep.ExternalPolicyData()
323 data = self.server.ReadPolicyDataFromDataDir(policy_key)
324 if data:
325 settings.download_url = urlparse.urljoin(
326 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key)
327 settings.secure_hash = hashlib.sha256(data).digest()
328 return settings.SerializeToString()
330 def CheckGoogleLogin(self):
331 """Extracts the auth token from the request and returns it. The token may
332 either be a GoogleLogin token from an Authorization header, or an OAuth V2
333 token from the oauth_token query parameter. Returns None if no token is
334 present.
336 oauth_token = self.GetUniqueParam('oauth_token')
337 if oauth_token:
338 return oauth_token
340 match = re.match('GoogleLogin auth=(\\w+)',
341 self.headers.getheader('Authorization', ''))
342 if match:
343 return match.group(1)
345 return None
347 def ProcessRegister(self, msg):
348 """Handles a register request.
350 Checks the query for authorization and device identifier, registers the
351 device with the server and constructs a response.
353 Args:
354 msg: The DeviceRegisterRequest message received from the client.
356 Returns:
357 A tuple of HTTP status code and response data to send to the client.
359 # Check the auth token and device ID.
360 auth = self.CheckGoogleLogin()
361 if not auth:
362 return (403, 'No authorization')
364 policy = self.server.GetPolicies()
365 if ('*' not in policy['managed_users'] and
366 auth not in policy['managed_users']):
367 return (403, 'Unmanaged')
369 device_id = self.GetUniqueParam('deviceid')
370 if not device_id:
371 return (400, 'Missing device identifier')
373 token_info = self.server.RegisterDevice(device_id,
374 msg.machine_id,
375 msg.type)
377 # Send back the reply.
378 response = dm.DeviceManagementResponse()
379 response.register_response.device_management_token = (
380 token_info['device_token'])
381 response.register_response.machine_name = token_info['machine_name']
382 response.register_response.enrollment_type = token_info['enrollment_mode']
384 return (200, response)
386 def ProcessApiAuthorization(self, msg):
387 """Handles an API authorization request.
389 Args:
390 msg: The DeviceServiceApiAccessRequest message received from the client.
392 Returns:
393 A tuple of HTTP status code and response data to send to the client.
395 policy = self.server.GetPolicies()
397 # Return the auth code from the config file if it's defined,
398 # else return a descriptive default value.
399 response = dm.DeviceManagementResponse()
400 response.service_api_access_response.auth_code = policy.get(
401 'robot_api_auth_code', 'policy_testserver.py-auth_code')
403 return (200, response)
405 def ProcessUnregister(self, msg):
406 """Handles a register request.
408 Checks for authorization, unregisters the device and constructs the
409 response.
411 Args:
412 msg: The DeviceUnregisterRequest message received from the client.
414 Returns:
415 A tuple of HTTP status code and response data to send to the client.
417 # Check the management token.
418 token, response = self.CheckToken()
419 if not token:
420 return response
422 # Unregister the device.
423 self.server.UnregisterDevice(token['device_token'])
425 # Prepare and send the response.
426 response = dm.DeviceManagementResponse()
427 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
429 return (200, response)
431 def ProcessPolicy(self, msg, request_type):
432 """Handles a policy request.
434 Checks for authorization, encodes the policy into protobuf representation
435 and constructs the response.
437 Args:
438 msg: The DeviceManagementRequest message received from the client.
440 Returns:
441 A tuple of HTTP status code and response data to send to the client.
443 token_info, error = self.CheckToken()
444 if not token_info:
445 return error
447 key_update_request = msg.device_state_key_update_request
448 if len(key_update_request.server_backed_state_key) > 0:
449 self.server.UpdateStateKeys(token_info['device_token'],
450 key_update_request.server_backed_state_key)
452 # If this is a |publicaccount| request, get the |username| now and use
453 # it in every PolicyFetchResponse produced. This is required to validate
454 # policy for extensions in device-local accounts.
455 # Unfortunately, the |username| can't be obtained from |msg| because that
456 # requires interacting with GAIA.
457 username = None
458 for request in msg.policy_request.request:
459 if request.policy_type == 'google/chromeos/publicaccount':
460 username = request.settings_entity_id
462 response = dm.DeviceManagementResponse()
463 for request in msg.policy_request.request:
464 if (request.policy_type in
465 ('google/android/user',
466 'google/chromeos/device',
467 'google/chromeos/publicaccount',
468 'google/chromeos/user',
469 'google/chrome/user',
470 'google/ios/user')):
471 fetch_response = response.policy_response.response.add()
472 self.ProcessCloudPolicy(request, token_info, fetch_response, username)
473 elif request.policy_type == 'google/chrome/extension':
474 self.ProcessCloudPolicyForExtensions(
475 request, response.policy_response, token_info, username)
476 else:
477 fetch_response.error_code = 400
478 fetch_response.error_message = 'Invalid policy_type'
480 return (200, response)
482 def ProcessAutoEnrollment(self, msg):
483 """Handles an auto-enrollment check request.
485 The reply depends on the value of the modulus:
486 1: replies with no new modulus and the sha256 hash of "0"
487 2: replies with a new modulus, 4.
488 4: replies with a new modulus, 2.
489 8: fails with error 400.
490 16: replies with a new modulus, 16.
491 32: replies with a new modulus, 1.
492 anything else: replies with no new modulus and an empty list of hashes
494 These allow the client to pick the testing scenario its wants to simulate.
496 Args:
497 msg: The DeviceAutoEnrollmentRequest message received from the client.
499 Returns:
500 A tuple of HTTP status code and response data to send to the client.
502 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
504 if msg.modulus == 1:
505 auto_enrollment_response.hash.extend(
506 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder))
507 elif msg.modulus == 2:
508 auto_enrollment_response.expected_modulus = 4
509 elif msg.modulus == 4:
510 auto_enrollment_response.expected_modulus = 2
511 elif msg.modulus == 8:
512 return (400, 'Server error')
513 elif msg.modulus == 16:
514 auto_enrollment_response.expected_modulus = 16
515 elif msg.modulus == 32:
516 auto_enrollment_response.expected_modulus = 1
518 response = dm.DeviceManagementResponse()
519 response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
520 return (200, response)
522 def ProcessDeviceStateRetrievalRequest(self, msg):
523 """Handles a device state retrieval request.
525 Response data is taken from server configuration.
527 Returns:
528 A tuple of HTTP status code and response data to send to the client.
530 device_state_retrieval_response = dm.DeviceStateRetrievalResponse()
532 client = self.server.LookupByStateKey(msg.server_backed_state_key)
533 if client is not None:
534 state = self.server.GetPolicies().get('device_state', {})
535 FIELDS = [
536 'management_domain',
537 'restore_mode',
539 for field in FIELDS:
540 if field in state:
541 setattr(device_state_retrieval_response, field, state[field])
543 response = dm.DeviceManagementResponse()
544 response.device_state_retrieval_response.CopyFrom(
545 device_state_retrieval_response)
546 return (200, response)
548 def SetProtobufMessageField(self, group_message, field, field_value):
549 """Sets a field in a protobuf message.
551 Args:
552 group_message: The protobuf message.
553 field: The field of the message to set, it should be a member of
554 group_message.DESCRIPTOR.fields.
555 field_value: The value to set.
557 if field.label == field.LABEL_REPEATED:
558 assert type(field_value) == list
559 entries = group_message.__getattribute__(field.name)
560 if field.message_type is None:
561 for list_item in field_value:
562 entries.append(list_item)
563 else:
564 # This field is itself a protobuf.
565 sub_type = field.message_type
566 for sub_value in field_value:
567 assert type(sub_value) == dict
568 # Add a new sub-protobuf per list entry.
569 sub_message = entries.add()
570 # Now iterate over its fields and recursively add them.
571 for sub_field in sub_message.DESCRIPTOR.fields:
572 if sub_field.name in sub_value:
573 value = sub_value[sub_field.name]
574 self.SetProtobufMessageField(sub_message, sub_field, value)
575 return
576 elif field.type == field.TYPE_BOOL:
577 assert type(field_value) == bool
578 elif field.type == field.TYPE_STRING:
579 assert type(field_value) == str or type(field_value) == unicode
580 elif field.type == field.TYPE_INT64:
581 assert type(field_value) == int
582 elif (field.type == field.TYPE_MESSAGE and
583 field.message_type.name == 'StringList'):
584 assert type(field_value) == list
585 entries = group_message.__getattribute__(field.name).entries
586 for list_item in field_value:
587 entries.append(list_item)
588 return
589 else:
590 raise Exception('Unknown field type %s' % field.type)
591 group_message.__setattr__(field.name, field_value)
593 def GatherDevicePolicySettings(self, settings, policies):
594 """Copies all the policies from a dictionary into a protobuf of type
595 CloudDeviceSettingsProto.
597 Args:
598 settings: The destination ChromeDeviceSettingsProto protobuf.
599 policies: The source dictionary containing policies in JSON format.
601 for group in settings.DESCRIPTOR.fields:
602 # Create protobuf message for group.
603 group_message = eval('dp.' + group.message_type.name + '()')
604 # Indicates if at least one field was set in |group_message|.
605 got_fields = False
606 # Iterate over fields of the message and feed them from the
607 # policy config file.
608 for field in group_message.DESCRIPTOR.fields:
609 field_value = None
610 if field.name in policies:
611 got_fields = True
612 field_value = policies[field.name]
613 self.SetProtobufMessageField(group_message, field, field_value)
614 if got_fields:
615 settings.__getattribute__(group.name).CopyFrom(group_message)
617 def GatherUserPolicySettings(self, settings, policies):
618 """Copies all the policies from a dictionary into a protobuf of type
619 CloudPolicySettings.
621 Args:
622 settings: The destination: a CloudPolicySettings protobuf.
623 policies: The source: a dictionary containing policies under keys
624 'recommended' and 'mandatory'.
626 for field in settings.DESCRIPTOR.fields:
627 # |field| is the entry for a specific policy in the top-level
628 # CloudPolicySettings proto.
630 # Look for this policy's value in the mandatory or recommended dicts.
631 if field.name in policies.get('mandatory', {}):
632 mode = cp.PolicyOptions.MANDATORY
633 value = policies['mandatory'][field.name]
634 elif field.name in policies.get('recommended', {}):
635 mode = cp.PolicyOptions.RECOMMENDED
636 value = policies['recommended'][field.name]
637 else:
638 continue
640 # Create protobuf message for this policy.
641 policy_message = eval('cp.' + field.message_type.name + '()')
642 policy_message.policy_options.mode = mode
643 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
644 self.SetProtobufMessageField(policy_message, field_descriptor, value)
645 settings.__getattribute__(field.name).CopyFrom(policy_message)
647 def ProcessCloudPolicyForExtensions(self, request, response, token_info,
648 username=None):
649 """Handles a request for policy for extensions.
651 A request for policy for extensions is slightly different from the other
652 cloud policy requests, because it can trigger 0, one or many
653 PolicyFetchResponse messages in the response.
655 Args:
656 request: The PolicyFetchRequest that triggered this handler.
657 response: The DevicePolicyResponse message for the response. Multiple
658 PolicyFetchResponses will be appended to this message.
659 token_info: The token extracted from the request.
660 username: The username for the response. May be None.
662 # Send one PolicyFetchResponse for each extension that has
663 # configuration data at the server.
664 ids = self.server.ListMatchingComponents('google/chrome/extension')
665 for settings_entity_id in ids:
666 # Reuse the extension policy request, to trigger the same signature
667 # type in the response.
668 request.settings_entity_id = settings_entity_id
669 fetch_response = response.response.add()
670 self.ProcessCloudPolicy(request, token_info, fetch_response, username)
671 # Don't do key rotations for these messages.
672 fetch_response.ClearField('new_public_key')
673 fetch_response.ClearField('new_public_key_signature')
674 fetch_response.ClearField('new_public_key_verification_signature')
676 def ProcessCloudPolicy(self, msg, token_info, response, username=None):
677 """Handles a cloud policy request. (New protocol for policy requests.)
679 Encodes the policy into protobuf representation, signs it and constructs
680 the response.
682 Args:
683 msg: The CloudPolicyRequest message received from the client.
684 token_info: The token extracted from the request.
685 response: A PolicyFetchResponse message that should be filled with the
686 response data.
687 username: The username for the response. May be None.
690 if msg.machine_id:
691 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
693 # Response is only given if the scope is specified in the config file.
694 # Normally 'google/chromeos/device', 'google/chromeos/user' and
695 # 'google/chromeos/publicaccount' should be accepted.
696 policy = self.server.GetPolicies()
697 policy_value = ''
698 policy_key = msg.policy_type
699 if msg.settings_entity_id:
700 policy_key += '/' + msg.settings_entity_id
701 if msg.policy_type in token_info['allowed_policy_types']:
702 if msg.policy_type in ('google/android/user',
703 'google/chromeos/publicaccount',
704 'google/chromeos/user',
705 'google/chrome/user',
706 'google/ios/user'):
707 settings = cp.CloudPolicySettings()
708 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
709 if payload is None:
710 self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
711 payload = settings.SerializeToString()
712 elif dp is not None and msg.policy_type == 'google/chromeos/device':
713 settings = dp.ChromeDeviceSettingsProto()
714 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
715 if payload is None:
716 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
717 payload = settings.SerializeToString()
718 elif msg.policy_type == 'google/chrome/extension':
719 settings = ep.ExternalPolicyData()
720 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
721 if payload is None:
722 payload = self.CreatePolicyForExternalPolicyData(policy_key)
723 else:
724 response.error_code = 400
725 response.error_message = 'Invalid policy type'
726 return
727 else:
728 response.error_code = 400
729 response.error_message = 'Request not allowed for the token used'
730 return
732 # Sign with 'current_key_index', defaulting to key 0.
733 signing_key = None
734 req_key = None
735 current_key_index = policy.get('current_key_index', 0)
736 nkeys = len(self.server.keys)
737 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
738 current_key_index in range(nkeys)):
739 signing_key = self.server.keys[current_key_index]
740 if msg.public_key_version in range(1, nkeys + 1):
741 # requested key exists, use for signing and rotate.
742 req_key = self.server.keys[msg.public_key_version - 1]['private_key']
744 # Fill the policy data protobuf.
745 policy_data = dm.PolicyData()
746 policy_data.policy_type = msg.policy_type
747 policy_data.timestamp = int(time.time() * 1000)
748 policy_data.request_token = token_info['device_token']
749 policy_data.policy_value = payload
750 policy_data.machine_name = token_info['machine_name']
751 policy_data.valid_serial_number_missing = (
752 token_info['machine_id'] in BAD_MACHINE_IDS)
753 policy_data.settings_entity_id = msg.settings_entity_id
754 policy_data.service_account_identity = policy.get(
755 'service_account_identity',
756 'policy_testserver.py-service_account_identity')
757 invalidation_source = policy.get('invalidation_source')
758 if invalidation_source is not None:
759 policy_data.invalidation_source = invalidation_source
760 # Since invalidation_name is type bytes in the proto, the Unicode name
761 # provided needs to be encoded as ASCII to set the correct byte pattern.
762 invalidation_name = policy.get('invalidation_name')
763 if invalidation_name is not None:
764 policy_data.invalidation_name = invalidation_name.encode('ascii')
766 if signing_key:
767 policy_data.public_key_version = current_key_index + 1
769 if username:
770 policy_data.username = username
771 else:
772 # For regular user/device policy, there is no way for the testserver to
773 # know the user name belonging to the GAIA auth token we received (short
774 # of actually talking to GAIA). To address this, we read the username from
775 # the policy configuration dictionary, or use a default.
776 policy_data.username = policy.get('policy_user', 'user@example.com')
777 policy_data.device_id = token_info['device_id']
778 signed_data = policy_data.SerializeToString()
780 response.policy_data = signed_data
781 if signing_key:
782 response.policy_data_signature = (
783 bytes(signing_key['private_key'].hashAndSign(signed_data)))
784 if msg.public_key_version != current_key_index + 1:
785 response.new_public_key = signing_key['public_key']
787 # Set the verification signature appropriate for the policy domain.
788 # TODO(atwilson): Use the enrollment domain for public accounts when
789 # we add key validation for ChromeOS (http://crbug.com/328038).
790 if 'signatures' in signing_key:
791 verification_sig = self.GetSignatureForDomain(
792 signing_key['signatures'], policy_data.username)
794 if verification_sig:
795 assert len(verification_sig) == 256, \
796 'bad signature size: %d' % len(verification_sig)
797 response.new_public_key_verification_signature = verification_sig
799 if req_key:
800 response.new_public_key_signature = (
801 bytes(req_key.hashAndSign(response.new_public_key)))
803 return (200, response.SerializeToString())
805 def GetSignatureForDomain(self, signatures, username):
806 parsed_username = username.split("@", 1)
807 if len(parsed_username) != 2:
808 logging.error('Could not extract domain from username: %s' % username)
809 return None
810 domain = parsed_username[1]
812 # Lookup the domain's signature in the passed dictionary. If none is found,
813 # fallback to a wildcard signature.
814 if domain in signatures:
815 return signatures[domain]
816 if '*' in signatures:
817 return signatures['*']
819 # No key matching this domain.
820 logging.error('No verification signature matching domain: %s' % domain)
821 return None
823 def CheckToken(self):
824 """Helper for checking whether the client supplied a valid DM token.
826 Extracts the token from the request and passed to the server in order to
827 look up the client.
829 Returns:
830 A pair of token information record and error response. If the first
831 element is None, then the second contains an error code to send back to
832 the client. Otherwise the first element is the same structure that is
833 returned by LookupToken().
835 error = 500
836 dmtoken = None
837 request_device_id = self.GetUniqueParam('deviceid')
838 match = re.match('GoogleDMToken token=(\\w+)',
839 self.headers.getheader('Authorization', ''))
840 if match:
841 dmtoken = match.group(1)
842 if not dmtoken:
843 error = 401
844 else:
845 token_info = self.server.LookupToken(dmtoken)
846 if (not token_info or
847 not request_device_id or
848 token_info['device_id'] != request_device_id):
849 error = 410
850 else:
851 return (token_info, None)
853 logging.debug('Token check failed with error %d' % error)
855 return (None, (error, 'Server error %d' % error))
857 def DumpMessage(self, label, msg):
858 """Helper for logging an ASCII dump of a protobuf message."""
859 logging.debug('%s\n%s' % (label, str(msg)))
862 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
863 testserver_base.StoppableHTTPServer):
864 """Handles requests and keeps global service state."""
866 def __init__(self, server_address, data_dir, policy_path, client_state_file,
867 private_key_paths, server_base_url):
868 """Initializes the server.
870 Args:
871 server_address: Server host and port.
872 policy_path: Names the file to read JSON-formatted policy from.
873 private_key_paths: List of paths to read private keys from.
875 testserver_base.StoppableHTTPServer.__init__(self, server_address,
876 PolicyRequestHandler)
877 self._registered_tokens = {}
878 self.data_dir = data_dir
879 self.policy_path = policy_path
880 self.client_state_file = client_state_file
881 self.server_base_url = server_base_url
883 self.keys = []
884 if private_key_paths:
885 # Load specified keys from the filesystem.
886 for key_path in private_key_paths:
887 try:
888 key_str = open(key_path).read()
889 except IOError:
890 print 'Failed to load private key from %s' % key_path
891 continue
892 try:
893 key = tlslite.api.parsePEMKey(key_str, private=True)
894 except SyntaxError:
895 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
896 bytearray(key_str))
898 assert key is not None
899 key_info = { 'private_key' : key }
901 # Now try to read in a signature, if one exists.
902 try:
903 key_sig = open(key_path + '.sig').read()
904 # Create a dictionary with the wildcard domain + signature
905 key_info['signatures'] = {'*': key_sig}
906 except IOError:
907 print 'Failed to read validation signature from %s.sig' % key_path
908 self.keys.append(key_info)
909 else:
910 # Use the canned private keys if none were passed from the command line.
911 for signing_key in SIGNING_KEYS:
912 decoded_key = base64.b64decode(signing_key['key']);
913 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
914 bytearray(decoded_key))
915 assert key is not None
916 # Grab the signature dictionary for this key and decode all of the
917 # signatures.
918 signature_dict = signing_key['signatures']
919 decoded_signatures = {}
920 for domain in signature_dict:
921 decoded_signatures[domain] = base64.b64decode(signature_dict[domain])
922 self.keys.append({'private_key': key,
923 'signatures': decoded_signatures})
925 # Derive the public keys from the private keys.
926 for entry in self.keys:
927 key = entry['private_key']
929 algorithm = asn1der.Sequence(
930 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
931 asn1der.Data(asn1der.NULL, '') ])
932 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
933 asn1der.Integer(key.e) ])
934 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
935 entry['public_key'] = pubkey
937 # Load client state.
938 if self.client_state_file is not None:
939 try:
940 file_contents = open(self.client_state_file).read()
941 self._registered_tokens = json.loads(file_contents, strict=False)
942 except IOError:
943 pass
945 def GetPolicies(self):
946 """Returns the policies to be used, reloaded form the backend file every
947 time this is called.
949 policy = {}
950 if json is None:
951 print 'No JSON module, cannot parse policy information'
952 else :
953 try:
954 policy = json.loads(open(self.policy_path).read(), strict=False)
955 except IOError:
956 print 'Failed to load policy from %s' % self.policy_path
957 return policy
959 def RegisterDevice(self, device_id, machine_id, type):
960 """Registers a device or user and generates a DM token for it.
962 Args:
963 device_id: The device identifier provided by the client.
965 Returns:
966 The newly generated device token for the device.
968 dmtoken_chars = []
969 while len(dmtoken_chars) < 32:
970 dmtoken_chars.append(random.choice('0123456789abcdef'))
971 dmtoken = ''.join(dmtoken_chars)
972 allowed_policy_types = {
973 dm.DeviceRegisterRequest.BROWSER: [
974 'google/chrome/user',
975 'google/chrome/extension'
977 dm.DeviceRegisterRequest.USER: [
978 'google/chromeos/user',
979 'google/chrome/extension'
981 dm.DeviceRegisterRequest.DEVICE: [
982 'google/chromeos/device',
983 'google/chromeos/publicaccount',
984 'google/chrome/extension'
986 dm.DeviceRegisterRequest.ANDROID_BROWSER: [
987 'google/android/user'
989 dm.DeviceRegisterRequest.IOS_BROWSER: [
990 'google/ios/user'
992 dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
993 'google/chrome/user'],
995 if machine_id in KIOSK_MACHINE_IDS:
996 enrollment_mode = dm.DeviceRegisterResponse.RETAIL
997 else:
998 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
999 self._registered_tokens[dmtoken] = {
1000 'device_id': device_id,
1001 'device_token': dmtoken,
1002 'allowed_policy_types': allowed_policy_types[type],
1003 'machine_name': 'chromeos-' + machine_id,
1004 'machine_id': machine_id,
1005 'enrollment_mode': enrollment_mode,
1007 self.WriteClientState()
1008 return self._registered_tokens[dmtoken]
1010 def UpdateMachineId(self, dmtoken, machine_id):
1011 """Updates the machine identifier for a registered device.
1013 Args:
1014 dmtoken: The device management token provided by the client.
1015 machine_id: Updated hardware identifier value.
1017 if dmtoken in self._registered_tokens:
1018 self._registered_tokens[dmtoken]['machine_id'] = machine_id
1019 self.WriteClientState()
1021 def UpdateStateKeys(self, dmtoken, state_keys):
1022 """Updates the state keys for a given client.
1024 Args:
1025 dmtoken: The device management token provided by the client.
1026 state_keys: The state keys to set.
1028 if dmtoken in self._registered_tokens:
1029 self._registered_tokens[dmtoken]['state_keys'] = map(
1030 lambda key : key.encode('hex'), state_keys)
1031 self.WriteClientState()
1033 def LookupToken(self, dmtoken):
1034 """Looks up a device or a user by DM token.
1036 Args:
1037 dmtoken: The device management token provided by the client.
1039 Returns:
1040 A dictionary with information about a device or user that is registered by
1041 dmtoken, or None if the token is not found.
1043 return self._registered_tokens.get(dmtoken, None)
1045 def LookupByStateKey(self, state_key):
1046 """Looks up a device or a user by a state key.
1048 Args:
1049 state_key: The state key provided by the client.
1051 Returns:
1052 A dictionary with information about a device or user or None if there is
1053 no matching record.
1055 for client in self._registered_tokens.values():
1056 if state_key.encode('hex') in client.get('state_keys', []):
1057 return client
1059 return None
1061 def GetMatchingStateKeyHashes(self, modulus, remainder):
1062 """Returns all clients registered with the server.
1064 Returns:
1065 The list of registered clients.
1067 state_keys = sum([ c.get('state_keys', [])
1068 for c in self._registered_tokens.values() ], [])
1069 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(),
1070 set(state_keys))
1071 return filter(
1072 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder,
1073 hashed_keys)
1075 def UnregisterDevice(self, dmtoken):
1076 """Unregisters a device identified by the given DM token.
1078 Args:
1079 dmtoken: The device management token provided by the client.
1081 if dmtoken in self._registered_tokens.keys():
1082 del self._registered_tokens[dmtoken]
1083 self.WriteClientState()
1085 def WriteClientState(self):
1086 """Writes the client state back to the file."""
1087 if self.client_state_file is not None:
1088 json_data = json.dumps(self._registered_tokens)
1089 open(self.client_state_file, 'w').write(json_data)
1091 def GetBaseFilename(self, policy_selector):
1092 """Returns the base filename for the given policy_selector.
1094 Args:
1095 policy_selector: The policy type and settings entity id, joined by '/'.
1097 Returns:
1098 The filename corresponding to the policy_selector, without a file
1099 extension.
1101 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
1102 return os.path.join(self.data_dir or '',
1103 'policy_%s' % sanitized_policy_selector)
1105 def ListMatchingComponents(self, policy_type):
1106 """Returns a list of settings entity IDs that have a configuration file.
1108 Args:
1109 policy_type: The policy type to look for. Only settings entity IDs for
1110 file selectors That match this policy_type will be returned.
1112 Returns:
1113 A list of settings entity IDs for the given |policy_type| that have a
1114 configuration file in this server (either as a .bin, .txt or .data file).
1116 base_name = self.GetBaseFilename(policy_type)
1117 files = glob.glob('%s_*.*' % base_name)
1118 len_base_name = len(base_name) + 1
1119 return [ file[len_base_name:file.rfind('.')] for file in files ]
1121 def ReadPolicyFromDataDir(self, policy_selector, proto_message):
1122 """Tries to read policy payload from a file in the data directory.
1124 First checks for a binary rendition of the policy protobuf in
1125 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1126 it. If that file doesn't exist, tries
1127 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1128 protobuf using proto_message. If that fails as well, returns None.
1130 Args:
1131 policy_selector: Selects which policy to read.
1132 proto_message: Optional protobuf message object used for decoding the
1133 proto text format.
1135 Returns:
1136 The binary payload message, or None if not found.
1138 base_filename = self.GetBaseFilename(policy_selector)
1140 # Try the binary payload file first.
1141 try:
1142 return open(base_filename + '.bin').read()
1143 except IOError:
1144 pass
1146 # If that fails, try the text version instead.
1147 if proto_message is None:
1148 return None
1150 try:
1151 text = open(base_filename + '.txt').read()
1152 google.protobuf.text_format.Merge(text, proto_message)
1153 return proto_message.SerializeToString()
1154 except IOError:
1155 return None
1156 except google.protobuf.text_format.ParseError:
1157 return None
1159 def ReadPolicyDataFromDataDir(self, policy_selector):
1160 """Returns the external policy data for |policy_selector| if found.
1162 Args:
1163 policy_selector: Selects which policy to read.
1165 Returns:
1166 The data for the corresponding policy type and entity id, if found.
1168 base_filename = self.GetBaseFilename(policy_selector)
1169 try:
1170 return open(base_filename + '.data').read()
1171 except IOError:
1172 return None
1174 def GetBaseURL(self):
1175 """Returns the server base URL.
1177 Respects the |server_base_url| configuration parameter, if present. Falls
1178 back to construct the URL from the server hostname and port otherwise.
1180 Returns:
1181 The URL to use for constructing URLs that get returned to clients.
1183 base_url = self.server_base_url
1184 if base_url is None:
1185 base_url = 'http://%s:%s' % self.server_address[:2]
1187 return base_url
1190 class PolicyServerRunner(testserver_base.TestServerRunner):
1192 def __init__(self):
1193 super(PolicyServerRunner, self).__init__()
1195 def create_server(self, server_data):
1196 data_dir = self.options.data_dir or ''
1197 config_file = (self.options.config_file or
1198 os.path.join(data_dir, 'device_management'))
1199 server = PolicyTestServer((self.options.host, self.options.port),
1200 data_dir, config_file,
1201 self.options.client_state_file,
1202 self.options.policy_keys,
1203 self.options.server_base_url)
1204 server_data['port'] = server.server_port
1205 return server
1207 def add_options(self):
1208 testserver_base.TestServerRunner.add_options(self)
1209 self.option_parser.add_option('--client-state', dest='client_state_file',
1210 help='File that client state should be '
1211 'persisted to. This allows the server to be '
1212 'seeded by a list of pre-registered clients '
1213 'and restarts without abandoning registered '
1214 'clients.')
1215 self.option_parser.add_option('--policy-key', action='append',
1216 dest='policy_keys',
1217 help='Specify a path to a PEM-encoded '
1218 'private key to use for policy signing. May '
1219 'be specified multiple times in order to '
1220 'load multiple keys into the server. If the '
1221 'server has multiple keys, it will rotate '
1222 'through them in at each request in a '
1223 'round-robin fashion. The server will '
1224 'use a canned key if none is specified '
1225 'on the command line. The test server will '
1226 'also look for a verification signature file '
1227 'in the same location: <filename>.sig and if '
1228 'present will add the signature to the '
1229 'policy blob as appropriate via the '
1230 'new_public_key_verification_signature '
1231 'field.')
1232 self.option_parser.add_option('--log-level', dest='log_level',
1233 default='WARN',
1234 help='Log level threshold to use.')
1235 self.option_parser.add_option('--config-file', dest='config_file',
1236 help='Specify a configuration file to use '
1237 'instead of the default '
1238 '<data_dir>/device_management')
1239 self.option_parser.add_option('--server-base-url', dest='server_base_url',
1240 help='The server base URL to use when '
1241 'constructing URLs to return to the client.')
1243 def run_server(self):
1244 logger = logging.getLogger()
1245 logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1246 if (self.options.log_to_console):
1247 logger.addHandler(logging.StreamHandler())
1248 if (self.options.log_file):
1249 logger.addHandler(logging.FileHandler(self.options.log_file))
1251 testserver_base.TestServerRunner.run_server(self)
1254 if __name__ == '__main__':
1255 sys.exit(PolicyServerRunner().main())