Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / policy / test / policy_testserver.py
blob480124cf9414b512973030fcf899a6ca1242ce67
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": "",
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 json
64 import logging
65 import os
66 import random
67 import re
68 import sys
69 import time
70 import tlslite
71 import tlslite.api
72 import tlslite.utils
73 import tlslite.utils.cryptomath
74 import urllib
75 import urllib2
76 import urlparse
78 import asn1der
79 import testserver_base
81 import device_management_backend_pb2 as dm
82 import cloud_policy_pb2 as cp
84 # Policy for extensions is not supported on Android nor iOS.
85 try:
86 import chrome_extension_policy_pb2 as ep
87 except ImportError:
88 ep = None
90 # Device policy is only available on Chrome OS builds.
91 try:
92 import chrome_device_policy_pb2 as dp
93 except ImportError:
94 dp = None
96 # ASN.1 object identifier for PKCS#1/RSA.
97 PKCS1_RSA_OID = '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
99 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
100 # flag to be set set in the policy fetch response.
101 BAD_MACHINE_IDS = [ '123490EN400015' ]
103 # List of machines that trigger the server to send kiosk enrollment response
104 # for the register request.
105 KIOSK_MACHINE_IDS = [ 'KIOSK' ]
107 # Dictionary containing base64-encoded policy signing keys plus per-domain
108 # signatures. Format is:
110 # 'key': <base64-encoded PKCS8-format private key>,
111 # 'signatures': {
112 # <domain1>: <base64-encdoded SHA256 signature for key + domain1>
113 # <domain2>: <base64-encdoded SHA256 signature for key + domain2>
114 # ...
117 SIGNING_KEYS = [
118 # Key1
119 {'key':
120 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1'
121 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ'
122 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9'
123 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC'
124 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R'
125 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ'
126 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq',
127 'signatures':
128 {'example.com':
129 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn'
130 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS'
131 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1'
132 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl'
133 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J'
134 '3+tfxjmoA/dg==',
135 'chromepolicytest.com':
136 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP'
137 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A'
138 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA'
139 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC'
140 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO'
141 'N+Z7IwElzTKg==',
142 'managedchrome.com':
143 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej'
144 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW'
145 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP'
146 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv'
147 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo'
148 'UExd9roA9a5w==',
151 # Key2
152 {'key':
153 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49'
154 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ'
155 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL'
156 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C'
157 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy'
158 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi'
159 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=',
160 'signatures':
161 # Key2 signatures
162 {'example.com':
163 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/'
164 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4'
165 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x'
166 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM'
167 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin'
168 '9Be9gs3W+Aww==',
169 'chromepolicytest.com':
170 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o'
171 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh'
172 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL'
173 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht'
174 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF'
175 'Z2PGxHTQ9JNA==',
176 'managedchrome.com':
177 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn'
178 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu'
179 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo'
180 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw'
181 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1'
182 'DBvBYwi20vbQ==',
187 class PolicyRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
188 """Decodes and handles device management requests from clients.
190 The handler implements all the request parsing and protobuf message decoding
191 and encoding. It calls back into the server to lookup, register, and
192 unregister clients.
195 def __init__(self, request, client_address, server):
196 """Initialize the handler.
198 Args:
199 request: The request data received from the client as a string.
200 client_address: The client address.
201 server: The TestServer object to use for (un)registering clients.
203 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
204 client_address, server)
206 def GetUniqueParam(self, name):
207 """Extracts a unique query parameter from the request.
209 Args:
210 name: Names the parameter to fetch.
211 Returns:
212 The parameter value or None if the parameter doesn't exist or is not
213 unique.
215 if not hasattr(self, '_params'):
216 self._params = cgi.parse_qs(self.path[self.path.find('?') + 1:])
218 param_list = self._params.get(name, [])
219 if len(param_list) == 1:
220 return param_list[0]
221 return None
223 def do_GET(self):
224 """Handles GET requests.
226 Currently this is only used to serve external policy data."""
227 sep = self.path.find('?')
228 path = self.path if sep == -1 else self.path[:sep]
229 if path == '/externalpolicydata':
230 http_response, raw_reply = self.HandleExternalPolicyDataRequest()
231 elif path == '/configuration/test/exit':
232 # This is not part of the standard DM server protocol.
233 # This extension is added to make the test server exit gracefully
234 # when the test is complete.
235 self.server.stop = True
236 http_response = 200
237 raw_reply = 'OK'
238 elif path == '/test/ping':
239 # This path and reply are used by the test setup of host-driven tests for
240 # Android to determine if the server is up, and are not part of the
241 # DM protocol.
242 http_response = 200
243 raw_reply = 'Policy server is up.'
244 else:
245 http_response = 404
246 raw_reply = 'Invalid path'
247 self.send_response(http_response)
248 self.end_headers()
249 self.wfile.write(raw_reply)
251 def do_POST(self):
252 http_response, raw_reply = self.HandleRequest()
253 self.send_response(http_response)
254 if (http_response == 200):
255 self.send_header('Content-Type', 'application/x-protobuffer')
256 self.end_headers()
257 self.wfile.write(raw_reply)
259 def HandleExternalPolicyDataRequest(self):
260 """Handles a request to download policy data for a component."""
261 policy_key = self.GetUniqueParam('key')
262 if not policy_key:
263 return (400, 'Missing key parameter')
264 data = self.server.ReadPolicyDataFromDataDir(policy_key)
265 if data is None:
266 return (404, 'Policy not found for ' + policy_key)
267 return (200, data)
269 def HandleRequest(self):
270 """Handles a request.
272 Parses the data supplied at construction time and returns a pair indicating
273 http status code and response data to be sent back to the client.
275 Returns:
276 A tuple of HTTP status code and response data to send to the client.
278 rmsg = dm.DeviceManagementRequest()
279 length = int(self.headers.getheader('content-length'))
280 rmsg.ParseFromString(self.rfile.read(length))
282 logging.debug('gaia auth token -> ' +
283 self.headers.getheader('Authorization', ''))
284 logging.debug('oauth token -> ' + str(self.GetUniqueParam('oauth_token')))
285 logging.debug('deviceid -> ' + str(self.GetUniqueParam('deviceid')))
286 self.DumpMessage('Request', rmsg)
288 request_type = self.GetUniqueParam('request')
289 # Check server side requirements, as defined in
290 # device_management_backend.proto.
291 if (self.GetUniqueParam('devicetype') != '2' or
292 self.GetUniqueParam('apptype') != 'Chrome' or
293 len(self.GetUniqueParam('deviceid')) >= 64):
294 return (400, 'Invalid request parameter')
295 if request_type == 'register':
296 response = self.ProcessRegister(rmsg.register_request)
297 elif request_type == 'api_authorization':
298 response = self.ProcessApiAuthorization(rmsg.service_api_access_request)
299 elif request_type == 'unregister':
300 response = self.ProcessUnregister(rmsg.unregister_request)
301 elif request_type == 'policy':
302 response = self.ProcessPolicy(rmsg, request_type)
303 elif request_type == 'enterprise_check':
304 response = self.ProcessAutoEnrollment(rmsg.auto_enrollment_request)
305 elif request_type == 'device_state_retrieval':
306 response = self.ProcessDeviceStateRetrievalRequest(
307 rmsg.device_state_retrieval_request)
308 elif request_type == 'status_upload':
309 response = self.ProcessStatusUploadRequest(
310 rmsg.device_status_report_request, rmsg.session_status_report_request)
311 else:
312 return (400, 'Invalid request parameter')
314 if isinstance(response[1], basestring):
315 body = response[1]
316 elif isinstance(response[1], google.protobuf.message.Message):
317 self.DumpMessage('Response', response[1])
318 body = response[1].SerializeToString()
319 else:
320 body = ''
321 return (response[0], body)
323 def CreatePolicyForExternalPolicyData(self, policy_key):
324 """Returns an ExternalPolicyData protobuf for policy_key.
326 If there is policy data for policy_key then the download url will be
327 set so that it points to that data, and the appropriate hash is also set.
328 Otherwise, the protobuf will be empty.
330 Args:
331 policy_key: The policy type and settings entity id, joined by '/'.
333 Returns:
334 A serialized ExternalPolicyData.
336 settings = ep.ExternalPolicyData()
337 data = self.server.ReadPolicyDataFromDataDir(policy_key)
338 if data:
339 settings.download_url = urlparse.urljoin(
340 self.server.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key)
341 settings.secure_hash = hashlib.sha256(data).digest()
342 return settings.SerializeToString()
344 def CheckGoogleLogin(self):
345 """Extracts the auth token from the request and returns it. The token may
346 either be a GoogleLogin token from an Authorization header, or an OAuth V2
347 token from the oauth_token query parameter. Returns None if no token is
348 present.
350 oauth_token = self.GetUniqueParam('oauth_token')
351 if oauth_token:
352 return oauth_token
354 match = re.match('GoogleLogin auth=(\\w+)',
355 self.headers.getheader('Authorization', ''))
356 if match:
357 return match.group(1)
359 return None
361 def ProcessRegister(self, msg):
362 """Handles a register request.
364 Checks the query for authorization and device identifier, registers the
365 device with the server and constructs a response.
367 Args:
368 msg: The DeviceRegisterRequest message received from the client.
370 Returns:
371 A tuple of HTTP status code and response data to send to the client.
373 # Check the auth token and device ID.
374 auth = self.CheckGoogleLogin()
375 if not auth:
376 return (403, 'No authorization')
378 policy = self.server.GetPolicies()
379 username = self.server.ResolveUser(auth)
380 if ('*' not in policy['managed_users'] and
381 username not in policy['managed_users']):
382 return (403, 'Unmanaged')
384 device_id = self.GetUniqueParam('deviceid')
385 if not device_id:
386 return (400, 'Missing device identifier')
388 token_info = self.server.RegisterDevice(
389 device_id, msg.machine_id, msg.type, username)
391 # Send back the reply.
392 response = dm.DeviceManagementResponse()
393 response.register_response.device_management_token = (
394 token_info['device_token'])
395 response.register_response.machine_name = token_info['machine_name']
396 response.register_response.enrollment_type = token_info['enrollment_mode']
398 return (200, response)
400 def ProcessApiAuthorization(self, msg):
401 """Handles an API authorization request.
403 Args:
404 msg: The DeviceServiceApiAccessRequest message received from the client.
406 Returns:
407 A tuple of HTTP status code and response data to send to the client.
409 policy = self.server.GetPolicies()
411 # Return the auth code from the config file if it's defined. Default to an
412 # empty auth code, which will instruct the enrollment flow to skip robot
413 # auth setup.
414 response = dm.DeviceManagementResponse()
415 response.service_api_access_response.auth_code = policy.get(
416 'robot_api_auth_code', '')
418 return (200, response)
420 def ProcessUnregister(self, msg):
421 """Handles a register request.
423 Checks for authorization, unregisters the device and constructs the
424 response.
426 Args:
427 msg: The DeviceUnregisterRequest message received from the client.
429 Returns:
430 A tuple of HTTP status code and response data to send to the client.
432 # Check the management token.
433 token, response = self.CheckToken()
434 if not token:
435 return response
437 # Unregister the device.
438 self.server.UnregisterDevice(token['device_token'])
440 # Prepare and send the response.
441 response = dm.DeviceManagementResponse()
442 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
444 return (200, response)
446 def ProcessPolicy(self, msg, request_type):
447 """Handles a policy request.
449 Checks for authorization, encodes the policy into protobuf representation
450 and constructs the response.
452 Args:
453 msg: The DeviceManagementRequest message received from the client.
455 Returns:
456 A tuple of HTTP status code and response data to send to the client.
458 token_info, error = self.CheckToken()
459 if not token_info:
460 return error
462 key_update_request = msg.device_state_key_update_request
463 if len(key_update_request.server_backed_state_key) > 0:
464 self.server.UpdateStateKeys(token_info['device_token'],
465 key_update_request.server_backed_state_key)
467 # See whether the |username| for the client is known. During policy
468 # validation, the client verifies that the policy blob is bound to the
469 # appropriate user by comparing against this value. In case the server is
470 # configured to resolve the actual user name from the access token via the
471 # token info endpoint, the resolved |username| has been stored in
472 # |token_info| when the client registered. If not, pass None as the
473 # |username| in which case a value from the configuration file will be used.
474 username = token_info.get('username')
476 # If this is a |publicaccount| request, use the |settings_entity_id| from
477 # the request as the |username|. This is required to validate policy for
478 # extensions in device-local accounts.
479 for request in msg.policy_request.request:
480 if request.policy_type == 'google/chromeos/publicaccount':
481 username = request.settings_entity_id
483 response = dm.DeviceManagementResponse()
484 for request in msg.policy_request.request:
485 if (request.policy_type in
486 ('google/android/user',
487 'google/chromeos/device',
488 'google/chromeos/publicaccount',
489 'google/chromeos/user',
490 'google/chrome/user',
491 'google/ios/user')):
492 fetch_response = response.policy_response.response.add()
493 self.ProcessCloudPolicy(request, token_info, fetch_response, username)
494 elif request.policy_type == 'google/chrome/extension':
495 self.ProcessCloudPolicyForExtensions(
496 request, response.policy_response, token_info, username)
497 else:
498 fetch_response.error_code = 400
499 fetch_response.error_message = 'Invalid policy_type'
501 return (200, response)
503 def ProcessAutoEnrollment(self, msg):
504 """Handles an auto-enrollment check request.
506 The reply depends on the value of the modulus:
507 1: replies with no new modulus and the sha256 hash of "0"
508 2: replies with a new modulus, 4.
509 4: replies with a new modulus, 2.
510 8: fails with error 400.
511 16: replies with a new modulus, 16.
512 32: replies with a new modulus, 1.
513 anything else: replies with no new modulus and an empty list of hashes
515 These allow the client to pick the testing scenario its wants to simulate.
517 Args:
518 msg: The DeviceAutoEnrollmentRequest message received from the client.
520 Returns:
521 A tuple of HTTP status code and response data to send to the client.
523 auto_enrollment_response = dm.DeviceAutoEnrollmentResponse()
525 if msg.modulus == 1:
526 auto_enrollment_response.hash.extend(
527 self.server.GetMatchingStateKeyHashes(msg.modulus, msg.remainder))
528 elif msg.modulus == 2:
529 auto_enrollment_response.expected_modulus = 4
530 elif msg.modulus == 4:
531 auto_enrollment_response.expected_modulus = 2
532 elif msg.modulus == 8:
533 return (400, 'Server error')
534 elif msg.modulus == 16:
535 auto_enrollment_response.expected_modulus = 16
536 elif msg.modulus == 32:
537 auto_enrollment_response.expected_modulus = 1
539 response = dm.DeviceManagementResponse()
540 response.auto_enrollment_response.CopyFrom(auto_enrollment_response)
541 return (200, response)
543 def ProcessDeviceStateRetrievalRequest(self, msg):
544 """Handles a device state retrieval request.
546 Response data is taken from server configuration.
548 Returns:
549 A tuple of HTTP status code and response data to send to the client.
551 device_state_retrieval_response = dm.DeviceStateRetrievalResponse()
553 client = self.server.LookupByStateKey(msg.server_backed_state_key)
554 if client is not None:
555 state = self.server.GetPolicies().get('device_state', {})
556 FIELDS = [
557 'management_domain',
558 'restore_mode',
560 for field in FIELDS:
561 if field in state:
562 setattr(device_state_retrieval_response, field, state[field])
564 response = dm.DeviceManagementResponse()
565 response.device_state_retrieval_response.CopyFrom(
566 device_state_retrieval_response)
567 return (200, response)
569 def ProcessStatusUploadRequest(self, device_status, session_status):
570 """Handles a device/session status upload request.
572 Returns:
573 A tuple of HTTP status code and response data to send to the client.
575 # Empty responses indicate a successful upload.
576 device_status_report_response = dm.DeviceStatusReportResponse()
577 session_status_report_response = dm.SessionStatusReportResponse()
579 response = dm.DeviceManagementResponse()
580 response.device_status_report_response.CopyFrom(
581 device_status_report_response)
582 response.session_status_report_response.CopyFrom(
583 session_status_report_response)
585 return (200, response)
587 def SetProtobufMessageField(self, group_message, field, field_value):
588 """Sets a field in a protobuf message.
590 Args:
591 group_message: The protobuf message.
592 field: The field of the message to set, it should be a member of
593 group_message.DESCRIPTOR.fields.
594 field_value: The value to set.
596 if field.label == field.LABEL_REPEATED:
597 assert type(field_value) == list
598 entries = group_message.__getattribute__(field.name)
599 if field.message_type is None:
600 for list_item in field_value:
601 entries.append(list_item)
602 else:
603 # This field is itself a protobuf.
604 sub_type = field.message_type
605 for sub_value in field_value:
606 assert type(sub_value) == dict
607 # Add a new sub-protobuf per list entry.
608 sub_message = entries.add()
609 # Now iterate over its fields and recursively add them.
610 for sub_field in sub_message.DESCRIPTOR.fields:
611 if sub_field.name in sub_value:
612 value = sub_value[sub_field.name]
613 self.SetProtobufMessageField(sub_message, sub_field, value)
614 return
615 elif field.type == field.TYPE_BOOL:
616 assert type(field_value) == bool
617 elif field.type == field.TYPE_STRING:
618 assert type(field_value) == str or type(field_value) == unicode
619 elif field.type == field.TYPE_INT64:
620 assert type(field_value) == int
621 elif (field.type == field.TYPE_MESSAGE and
622 field.message_type.name == 'StringList'):
623 assert type(field_value) == list
624 entries = group_message.__getattribute__(field.name).entries
625 for list_item in field_value:
626 entries.append(list_item)
627 return
628 else:
629 raise Exception('Unknown field type %s' % field.type)
630 group_message.__setattr__(field.name, field_value)
632 def GatherDevicePolicySettings(self, settings, policies):
633 """Copies all the policies from a dictionary into a protobuf of type
634 CloudDeviceSettingsProto.
636 Args:
637 settings: The destination ChromeDeviceSettingsProto protobuf.
638 policies: The source dictionary containing policies in JSON format.
640 for group in settings.DESCRIPTOR.fields:
641 # Create protobuf message for group.
642 group_message = eval('dp.' + group.message_type.name + '()')
643 # Indicates if at least one field was set in |group_message|.
644 got_fields = False
645 # Iterate over fields of the message and feed them from the
646 # policy config file.
647 for field in group_message.DESCRIPTOR.fields:
648 field_value = None
649 if field.name in policies:
650 got_fields = True
651 field_value = policies[field.name]
652 self.SetProtobufMessageField(group_message, field, field_value)
653 if got_fields:
654 settings.__getattribute__(group.name).CopyFrom(group_message)
656 def GatherUserPolicySettings(self, settings, policies):
657 """Copies all the policies from a dictionary into a protobuf of type
658 CloudPolicySettings.
660 Args:
661 settings: The destination: a CloudPolicySettings protobuf.
662 policies: The source: a dictionary containing policies under keys
663 'recommended' and 'mandatory'.
665 for field in settings.DESCRIPTOR.fields:
666 # |field| is the entry for a specific policy in the top-level
667 # CloudPolicySettings proto.
669 # Look for this policy's value in the mandatory or recommended dicts.
670 if field.name in policies.get('mandatory', {}):
671 mode = cp.PolicyOptions.MANDATORY
672 value = policies['mandatory'][field.name]
673 elif field.name in policies.get('recommended', {}):
674 mode = cp.PolicyOptions.RECOMMENDED
675 value = policies['recommended'][field.name]
676 else:
677 continue
679 # Create protobuf message for this policy.
680 policy_message = eval('cp.' + field.message_type.name + '()')
681 policy_message.policy_options.mode = mode
682 field_descriptor = policy_message.DESCRIPTOR.fields_by_name['value']
683 self.SetProtobufMessageField(policy_message, field_descriptor, value)
684 settings.__getattribute__(field.name).CopyFrom(policy_message)
686 def ProcessCloudPolicyForExtensions(self, request, response, token_info,
687 username=None):
688 """Handles a request for policy for extensions.
690 A request for policy for extensions is slightly different from the other
691 cloud policy requests, because it can trigger 0, one or many
692 PolicyFetchResponse messages in the response.
694 Args:
695 request: The PolicyFetchRequest that triggered this handler.
696 response: The DevicePolicyResponse message for the response. Multiple
697 PolicyFetchResponses will be appended to this message.
698 token_info: The token extracted from the request.
699 username: The username for the response. May be None.
701 # Send one PolicyFetchResponse for each extension that has
702 # configuration data at the server.
703 ids = self.server.ListMatchingComponents('google/chrome/extension')
704 for settings_entity_id in ids:
705 # Reuse the extension policy request, to trigger the same signature
706 # type in the response.
707 request.settings_entity_id = settings_entity_id
708 fetch_response = response.response.add()
709 self.ProcessCloudPolicy(request, token_info, fetch_response, username)
710 # Don't do key rotations for these messages.
711 fetch_response.ClearField('new_public_key')
712 fetch_response.ClearField('new_public_key_signature')
713 fetch_response.ClearField('new_public_key_verification_signature')
715 def ProcessCloudPolicy(self, msg, token_info, response, username=None):
716 """Handles a cloud policy request. (New protocol for policy requests.)
718 Encodes the policy into protobuf representation, signs it and constructs
719 the response.
721 Args:
722 msg: The CloudPolicyRequest message received from the client.
723 token_info: The token extracted from the request.
724 response: A PolicyFetchResponse message that should be filled with the
725 response data.
726 username: The username for the response. May be None.
729 if msg.machine_id:
730 self.server.UpdateMachineId(token_info['device_token'], msg.machine_id)
732 # Response is only given if the scope is specified in the config file.
733 # Normally 'google/chromeos/device', 'google/chromeos/user' and
734 # 'google/chromeos/publicaccount' should be accepted.
735 policy = self.server.GetPolicies()
736 policy_value = ''
737 policy_key = msg.policy_type
738 if msg.settings_entity_id:
739 policy_key += '/' + msg.settings_entity_id
740 if msg.policy_type in token_info['allowed_policy_types']:
741 if msg.policy_type in ('google/android/user',
742 'google/chromeos/publicaccount',
743 'google/chromeos/user',
744 'google/chrome/user',
745 'google/ios/user'):
746 settings = cp.CloudPolicySettings()
747 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
748 if payload is None:
749 self.GatherUserPolicySettings(settings, policy.get(policy_key, {}))
750 payload = settings.SerializeToString()
751 elif msg.policy_type == 'google/chromeos/device':
752 settings = dp.ChromeDeviceSettingsProto()
753 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
754 if payload is None:
755 self.GatherDevicePolicySettings(settings, policy.get(policy_key, {}))
756 payload = settings.SerializeToString()
757 elif msg.policy_type == 'google/chrome/extension':
758 settings = ep.ExternalPolicyData()
759 payload = self.server.ReadPolicyFromDataDir(policy_key, settings)
760 if payload is None:
761 payload = self.CreatePolicyForExternalPolicyData(policy_key)
762 else:
763 response.error_code = 400
764 response.error_message = 'Invalid policy type'
765 return
766 else:
767 response.error_code = 400
768 response.error_message = 'Request not allowed for the token used'
769 return
771 # Sign with 'current_key_index', defaulting to key 0.
772 signing_key = None
773 req_key = None
774 current_key_index = policy.get('current_key_index', 0)
775 nkeys = len(self.server.keys)
776 if (msg.signature_type == dm.PolicyFetchRequest.SHA1_RSA and
777 current_key_index in range(nkeys)):
778 signing_key = self.server.keys[current_key_index]
779 if msg.public_key_version in range(1, nkeys + 1):
780 # requested key exists, use for signing and rotate.
781 req_key = self.server.keys[msg.public_key_version - 1]['private_key']
783 # Fill the policy data protobuf.
784 policy_data = dm.PolicyData()
785 policy_data.policy_type = msg.policy_type
786 policy_data.timestamp = int(time.time() * 1000)
787 policy_data.request_token = token_info['device_token']
788 policy_data.policy_value = payload
789 policy_data.machine_name = token_info['machine_name']
790 policy_data.valid_serial_number_missing = (
791 token_info['machine_id'] in BAD_MACHINE_IDS)
792 policy_data.settings_entity_id = msg.settings_entity_id
793 policy_data.service_account_identity = policy.get(
794 'service_account_identity',
795 'policy_testserver.py-service_account_identity')
796 invalidation_source = policy.get('invalidation_source')
797 if invalidation_source is not None:
798 policy_data.invalidation_source = invalidation_source
799 # Since invalidation_name is type bytes in the proto, the Unicode name
800 # provided needs to be encoded as ASCII to set the correct byte pattern.
801 invalidation_name = policy.get('invalidation_name')
802 if invalidation_name is not None:
803 policy_data.invalidation_name = invalidation_name.encode('ascii')
805 if signing_key:
806 policy_data.public_key_version = current_key_index + 1
808 if username:
809 policy_data.username = username
810 else:
811 # If the correct |username| is unknown, rely on a manually-configured
812 # username from the configuration file or use a default.
813 policy_data.username = policy.get('policy_user', 'user@example.com')
814 policy_data.device_id = token_info['device_id']
816 # Set affiliation IDs so that user was managed on the device.
817 device_affiliation_ids = policy.get('device_affiliation_ids')
818 if device_affiliation_ids:
819 policy_data.device_affiliation_ids.extend(device_affiliation_ids)
821 user_affiliation_ids = policy.get('user_affiliation_ids')
822 if user_affiliation_ids:
823 policy_data.user_affiliation_ids.extend(user_affiliation_ids)
825 signed_data = policy_data.SerializeToString()
827 response.policy_data = signed_data
828 if signing_key:
829 response.policy_data_signature = (
830 bytes(signing_key['private_key'].hashAndSign(signed_data)))
831 if msg.public_key_version != current_key_index + 1:
832 response.new_public_key = signing_key['public_key']
834 # Set the verification signature appropriate for the policy domain.
835 # TODO(atwilson): Use the enrollment domain for public accounts when
836 # we add key validation for ChromeOS (http://crbug.com/328038).
837 if 'signatures' in signing_key:
838 verification_sig = self.GetSignatureForDomain(
839 signing_key['signatures'], policy_data.username)
841 if verification_sig:
842 assert len(verification_sig) == 256, \
843 'bad signature size: %d' % len(verification_sig)
844 response.new_public_key_verification_signature = verification_sig
846 if req_key:
847 response.new_public_key_signature = (
848 bytes(req_key.hashAndSign(response.new_public_key)))
850 return (200, response.SerializeToString())
852 def GetSignatureForDomain(self, signatures, username):
853 parsed_username = username.split("@", 1)
854 if len(parsed_username) != 2:
855 logging.error('Could not extract domain from username: %s' % username)
856 return None
857 domain = parsed_username[1]
859 # Lookup the domain's signature in the passed dictionary. If none is found,
860 # fallback to a wildcard signature.
861 if domain in signatures:
862 return signatures[domain]
863 if '*' in signatures:
864 return signatures['*']
866 # No key matching this domain.
867 logging.error('No verification signature matching domain: %s' % domain)
868 return None
870 def CheckToken(self):
871 """Helper for checking whether the client supplied a valid DM token.
873 Extracts the token from the request and passed to the server in order to
874 look up the client.
876 Returns:
877 A pair of token information record and error response. If the first
878 element is None, then the second contains an error code to send back to
879 the client. Otherwise the first element is the same structure that is
880 returned by LookupToken().
882 error = 500
883 dmtoken = None
884 request_device_id = self.GetUniqueParam('deviceid')
885 match = re.match('GoogleDMToken token=(\\w+)',
886 self.headers.getheader('Authorization', ''))
887 if match:
888 dmtoken = match.group(1)
889 if not dmtoken:
890 error = 401
891 else:
892 token_info = self.server.LookupToken(dmtoken)
893 if (not token_info or
894 not request_device_id or
895 token_info['device_id'] != request_device_id):
896 error = 410
897 else:
898 return (token_info, None)
900 logging.debug('Token check failed with error %d' % error)
902 return (None, (error, 'Server error %d' % error))
904 def DumpMessage(self, label, msg):
905 """Helper for logging an ASCII dump of a protobuf message."""
906 logging.debug('%s\n%s' % (label, str(msg)))
909 class PolicyTestServer(testserver_base.BrokenPipeHandlerMixIn,
910 testserver_base.StoppableHTTPServer):
911 """Handles requests and keeps global service state."""
913 def __init__(self, server_address, data_dir, policy_path, client_state_file,
914 private_key_paths, server_base_url):
915 """Initializes the server.
917 Args:
918 server_address: Server host and port.
919 policy_path: Names the file to read JSON-formatted policy from.
920 private_key_paths: List of paths to read private keys from.
922 testserver_base.StoppableHTTPServer.__init__(self, server_address,
923 PolicyRequestHandler)
924 self._registered_tokens = {}
925 self.data_dir = data_dir
926 self.policy_path = policy_path
927 self.client_state_file = client_state_file
928 self.server_base_url = server_base_url
930 self.keys = []
931 if private_key_paths:
932 # Load specified keys from the filesystem.
933 for key_path in private_key_paths:
934 try:
935 key_str = open(key_path).read()
936 except IOError:
937 print 'Failed to load private key from %s' % key_path
938 continue
939 try:
940 key = tlslite.api.parsePEMKey(key_str, private=True)
941 except SyntaxError:
942 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
943 bytearray(key_str))
945 assert key is not None
946 key_info = { 'private_key' : key }
948 # Now try to read in a signature, if one exists.
949 try:
950 key_sig = open(key_path + '.sig').read()
951 # Create a dictionary with the wildcard domain + signature
952 key_info['signatures'] = {'*': key_sig}
953 except IOError:
954 print 'Failed to read validation signature from %s.sig' % key_path
955 self.keys.append(key_info)
956 else:
957 # Use the canned private keys if none were passed from the command line.
958 for signing_key in SIGNING_KEYS:
959 decoded_key = base64.b64decode(signing_key['key']);
960 key = tlslite.utils.python_rsakey.Python_RSAKey._parsePKCS8(
961 bytearray(decoded_key))
962 assert key is not None
963 # Grab the signature dictionary for this key and decode all of the
964 # signatures.
965 signature_dict = signing_key['signatures']
966 decoded_signatures = {}
967 for domain in signature_dict:
968 decoded_signatures[domain] = base64.b64decode(signature_dict[domain])
969 self.keys.append({'private_key': key,
970 'signatures': decoded_signatures})
972 # Derive the public keys from the private keys.
973 for entry in self.keys:
974 key = entry['private_key']
976 algorithm = asn1der.Sequence(
977 [ asn1der.Data(asn1der.OBJECT_IDENTIFIER, PKCS1_RSA_OID),
978 asn1der.Data(asn1der.NULL, '') ])
979 rsa_pubkey = asn1der.Sequence([ asn1der.Integer(key.n),
980 asn1der.Integer(key.e) ])
981 pubkey = asn1der.Sequence([ algorithm, asn1der.Bitstring(rsa_pubkey) ])
982 entry['public_key'] = pubkey
984 # Load client state.
985 if self.client_state_file is not None:
986 try:
987 file_contents = open(self.client_state_file).read()
988 self._registered_tokens = json.loads(file_contents, strict=False)
989 except IOError:
990 pass
992 def GetPolicies(self):
993 """Returns the policies to be used, reloaded form the backend file every
994 time this is called.
996 policy = {}
997 if json is None:
998 logging.error('No JSON module, cannot parse policy information')
999 else :
1000 try:
1001 policy = json.loads(open(self.policy_path).read(), strict=False)
1002 except IOError:
1003 logging.error('Failed to load policies from %s' % self.policy_path)
1004 return policy
1006 def ResolveUser(self, auth_token):
1007 """Tries to resolve an auth token to the corresponding user name.
1009 If enabled, this makes a request to the token info endpoint to determine the
1010 user ID corresponding to the token. If token resolution is disabled or the
1011 request fails, this will return the policy_user config parameter.
1013 config = self.GetPolicies()
1014 token_info_url = config.get('token_info_url')
1015 if token_info_url is not None:
1016 try:
1017 token_info = urllib2.urlopen(token_info_url + '?' +
1018 urllib.urlencode({'access_token': auth_token})).read()
1019 return json.loads(token_info)['email']
1020 except Exception as e:
1021 logging.info('Failed to resolve user: %s', e)
1023 return config.get('policy_user')
1025 def RegisterDevice(self, device_id, machine_id, type, username):
1026 """Registers a device or user and generates a DM token for it.
1028 Args:
1029 device_id: The device identifier provided by the client.
1031 Returns:
1032 The newly generated device token for the device.
1034 dmtoken_chars = []
1035 while len(dmtoken_chars) < 32:
1036 dmtoken_chars.append(random.choice('0123456789abcdef'))
1037 dmtoken = ''.join(dmtoken_chars)
1038 allowed_policy_types = {
1039 dm.DeviceRegisterRequest.BROWSER: [
1040 'google/chrome/user',
1041 'google/chrome/extension'
1043 dm.DeviceRegisterRequest.USER: [
1044 'google/chromeos/user',
1045 'google/chrome/extension'
1047 dm.DeviceRegisterRequest.DEVICE: [
1048 'google/chromeos/device',
1049 'google/chromeos/publicaccount',
1050 'google/chrome/extension'
1052 dm.DeviceRegisterRequest.ANDROID_BROWSER: [
1053 'google/android/user'
1055 dm.DeviceRegisterRequest.IOS_BROWSER: [
1056 'google/ios/user'
1058 dm.DeviceRegisterRequest.TT: ['google/chromeos/user',
1059 'google/chrome/user'],
1061 if machine_id in KIOSK_MACHINE_IDS:
1062 enrollment_mode = dm.DeviceRegisterResponse.RETAIL
1063 else:
1064 enrollment_mode = dm.DeviceRegisterResponse.ENTERPRISE
1065 self._registered_tokens[dmtoken] = {
1066 'device_id': device_id,
1067 'device_token': dmtoken,
1068 'allowed_policy_types': allowed_policy_types[type],
1069 'machine_name': 'chromeos-' + machine_id,
1070 'machine_id': machine_id,
1071 'enrollment_mode': enrollment_mode,
1072 'username': username,
1074 self.WriteClientState()
1075 return self._registered_tokens[dmtoken]
1077 def UpdateMachineId(self, dmtoken, machine_id):
1078 """Updates the machine identifier for a registered device.
1080 Args:
1081 dmtoken: The device management token provided by the client.
1082 machine_id: Updated hardware identifier value.
1084 if dmtoken in self._registered_tokens:
1085 self._registered_tokens[dmtoken]['machine_id'] = machine_id
1086 self.WriteClientState()
1088 def UpdateStateKeys(self, dmtoken, state_keys):
1089 """Updates the state keys for a given client.
1091 Args:
1092 dmtoken: The device management token provided by the client.
1093 state_keys: The state keys to set.
1095 if dmtoken in self._registered_tokens:
1096 self._registered_tokens[dmtoken]['state_keys'] = map(
1097 lambda key : key.encode('hex'), state_keys)
1098 self.WriteClientState()
1100 def LookupToken(self, dmtoken):
1101 """Looks up a device or a user by DM token.
1103 Args:
1104 dmtoken: The device management token provided by the client.
1106 Returns:
1107 A dictionary with information about a device or user that is registered by
1108 dmtoken, or None if the token is not found.
1110 return self._registered_tokens.get(dmtoken, None)
1112 def LookupByStateKey(self, state_key):
1113 """Looks up a device or a user by a state key.
1115 Args:
1116 state_key: The state key provided by the client.
1118 Returns:
1119 A dictionary with information about a device or user or None if there is
1120 no matching record.
1122 for client in self._registered_tokens.values():
1123 if state_key.encode('hex') in client.get('state_keys', []):
1124 return client
1126 return None
1128 def GetMatchingStateKeyHashes(self, modulus, remainder):
1129 """Returns all clients registered with the server.
1131 Returns:
1132 The list of registered clients.
1134 state_keys = sum([ c.get('state_keys', [])
1135 for c in self._registered_tokens.values() ], [])
1136 hashed_keys = map(lambda key: hashlib.sha256(key.decode('hex')).digest(),
1137 set(state_keys))
1138 return filter(
1139 lambda hash : int(hash.encode('hex'), 16) % modulus == remainder,
1140 hashed_keys)
1142 def UnregisterDevice(self, dmtoken):
1143 """Unregisters a device identified by the given DM token.
1145 Args:
1146 dmtoken: The device management token provided by the client.
1148 if dmtoken in self._registered_tokens.keys():
1149 del self._registered_tokens[dmtoken]
1150 self.WriteClientState()
1152 def WriteClientState(self):
1153 """Writes the client state back to the file."""
1154 if self.client_state_file is not None:
1155 json_data = json.dumps(self._registered_tokens)
1156 open(self.client_state_file, 'w').write(json_data)
1158 def GetBaseFilename(self, policy_selector):
1159 """Returns the base filename for the given policy_selector.
1161 Args:
1162 policy_selector: The policy type and settings entity id, joined by '/'.
1164 Returns:
1165 The filename corresponding to the policy_selector, without a file
1166 extension.
1168 sanitized_policy_selector = re.sub('[^A-Za-z0-9.@-]', '_', policy_selector)
1169 return os.path.join(self.data_dir or '',
1170 'policy_%s' % sanitized_policy_selector)
1172 def ListMatchingComponents(self, policy_type):
1173 """Returns a list of settings entity IDs that have a configuration file.
1175 Args:
1176 policy_type: The policy type to look for. Only settings entity IDs for
1177 file selectors That match this policy_type will be returned.
1179 Returns:
1180 A list of settings entity IDs for the given |policy_type| that have a
1181 configuration file in this server (either as a .bin, .txt or .data file).
1183 base_name = self.GetBaseFilename(policy_type)
1184 files = glob.glob('%s_*.*' % base_name)
1185 len_base_name = len(base_name) + 1
1186 return [ file[len_base_name:file.rfind('.')] for file in files ]
1188 def ReadPolicyFromDataDir(self, policy_selector, proto_message):
1189 """Tries to read policy payload from a file in the data directory.
1191 First checks for a binary rendition of the policy protobuf in
1192 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1193 it. If that file doesn't exist, tries
1194 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1195 protobuf using proto_message. If that fails as well, returns None.
1197 Args:
1198 policy_selector: Selects which policy to read.
1199 proto_message: Optional protobuf message object used for decoding the
1200 proto text format.
1202 Returns:
1203 The binary payload message, or None if not found.
1205 base_filename = self.GetBaseFilename(policy_selector)
1207 # Try the binary payload file first.
1208 try:
1209 return open(base_filename + '.bin').read()
1210 except IOError:
1211 pass
1213 # If that fails, try the text version instead.
1214 if proto_message is None:
1215 return None
1217 try:
1218 text = open(base_filename + '.txt').read()
1219 google.protobuf.text_format.Merge(text, proto_message)
1220 return proto_message.SerializeToString()
1221 except IOError:
1222 return None
1223 except google.protobuf.text_format.ParseError:
1224 return None
1226 def ReadPolicyDataFromDataDir(self, policy_selector):
1227 """Returns the external policy data for |policy_selector| if found.
1229 Args:
1230 policy_selector: Selects which policy to read.
1232 Returns:
1233 The data for the corresponding policy type and entity id, if found.
1235 base_filename = self.GetBaseFilename(policy_selector)
1236 try:
1237 return open(base_filename + '.data').read()
1238 except IOError:
1239 return None
1241 def GetBaseURL(self):
1242 """Returns the server base URL.
1244 Respects the |server_base_url| configuration parameter, if present. Falls
1245 back to construct the URL from the server hostname and port otherwise.
1247 Returns:
1248 The URL to use for constructing URLs that get returned to clients.
1250 base_url = self.server_base_url
1251 if base_url is None:
1252 base_url = 'http://%s:%s' % self.server_address[:2]
1254 return base_url
1257 class PolicyServerRunner(testserver_base.TestServerRunner):
1259 def __init__(self):
1260 super(PolicyServerRunner, self).__init__()
1262 def create_server(self, server_data):
1263 data_dir = self.options.data_dir or ''
1264 config_file = (self.options.config_file or
1265 os.path.join(data_dir, 'device_management'))
1266 server = PolicyTestServer((self.options.host, self.options.port),
1267 data_dir, config_file,
1268 self.options.client_state_file,
1269 self.options.policy_keys,
1270 self.options.server_base_url)
1271 server_data['port'] = server.server_port
1272 return server
1274 def add_options(self):
1275 testserver_base.TestServerRunner.add_options(self)
1276 self.option_parser.add_option('--client-state', dest='client_state_file',
1277 help='File that client state should be '
1278 'persisted to. This allows the server to be '
1279 'seeded by a list of pre-registered clients '
1280 'and restarts without abandoning registered '
1281 'clients.')
1282 self.option_parser.add_option('--policy-key', action='append',
1283 dest='policy_keys',
1284 help='Specify a path to a PEM-encoded '
1285 'private key to use for policy signing. May '
1286 'be specified multiple times in order to '
1287 'load multiple keys into the server. If the '
1288 'server has multiple keys, it will rotate '
1289 'through them in at each request in a '
1290 'round-robin fashion. The server will '
1291 'use a canned key if none is specified '
1292 'on the command line. The test server will '
1293 'also look for a verification signature file '
1294 'in the same location: <filename>.sig and if '
1295 'present will add the signature to the '
1296 'policy blob as appropriate via the '
1297 'new_public_key_verification_signature '
1298 'field.')
1299 self.option_parser.add_option('--log-level', dest='log_level',
1300 default='WARN',
1301 help='Log level threshold to use.')
1302 self.option_parser.add_option('--config-file', dest='config_file',
1303 help='Specify a configuration file to use '
1304 'instead of the default '
1305 '<data_dir>/device_management')
1306 self.option_parser.add_option('--server-base-url', dest='server_base_url',
1307 help='The server base URL to use when '
1308 'constructing URLs to return to the client.')
1310 def run_server(self):
1311 logger = logging.getLogger()
1312 logger.setLevel(getattr(logging, str(self.options.log_level).upper()))
1313 if (self.options.log_to_console):
1314 logger.addHandler(logging.StreamHandler())
1315 if (self.options.log_file):
1316 logger.addHandler(logging.FileHandler(self.options.log_file))
1318 testserver_base.TestServerRunner.run_server(self)
1321 if __name__ == '__main__':
1322 sys.exit(PolicyServerRunner().main())