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
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
27 "google/chromeos/device" : {
28 "guest_mode_enabled" : false
30 "google/chromeos/user" : {
32 "HomepageLocation" : "http://www.chromium.org",
33 "IncognitoEnabled" : false
36 "JavascriptEnabled": false
39 "google/chromeos/publicaccount/user@example.com" : {
41 "HomepageLocation" : "http://www.chromium.org"
49 "current_key_index": 0,
50 "robot_api_auth_code": "fake_auth_code",
51 "invalidation_source": 1025,
52 "invalidation_name": "UENUPOL"
61 import google
.protobuf
.text_format
72 import tlslite
.utils
.cryptomath
75 # The name and availability of the json module varies in python versions.
77 import simplejson
as json
85 import testserver_base
87 import device_management_backend_pb2
as dm
88 import cloud_policy_pb2
as cp
90 # Policy for extensions is not supported on Android nor iOS.
92 import chrome_extension_policy_pb2
as ep
96 # Device policy is only available on Chrome OS builds.
98 import chrome_device_policy_pb2
as dp
102 # ASN.1 object identifier for PKCS#1/RSA.
103 PKCS1_RSA_OID
= '\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01'
105 # List of bad machine identifiers that trigger the |valid_serial_number_missing|
106 # flag to be set set in the policy fetch response.
107 BAD_MACHINE_IDS
= [ '123490EN400015' ]
109 # List of machines that trigger the server to send kiosk enrollment response
110 # for the register request.
111 KIOSK_MACHINE_IDS
= [ 'KIOSK' ]
113 # Dictionary containing base64-encoded policy signing keys plus per-domain
114 # signatures. Format is:
116 # 'key': <base64-encoded PKCS8-format private key>,
118 # <domain1>: <base64-encdoded SHA256 signature for key + domain1>
119 # <domain2>: <base64-encdoded SHA256 signature for key + domain2>
126 'MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2c3KzcPqvnJ5HCk3OZkf1'
127 'LMO8Ht4dw4FO2U0EmKvpo0zznj4RwUdmKobH1AFWzwZP4CDY2M67MsukE/1Jnbx1QIDAQ'
128 'ABAkBkKcLZa/75hHVz4PR3tZaw34PATlfxEG6RiRIwXlf/FFlfGIZOSxdW/I1A3XRl0/9'
129 'nZMuctBSKBrcTRZQWfT/hAiEA9g8xbQbMO6BEH/XCRSsQbPlvj4c9wDtVEzeAzZ/ht9kC'
130 'IQDiml+/lXS1emqml711jJcYJNYJzdy1lL/ieKogR59oXQIhAK+Pl4xa1U2VxAWpq7r+R'
131 'vH55wdZT03hB4p2h4gvEzXBAiAkw9kvE0eZPiBZoRrrHIFTOH7FnnHlwBmV2+/2RsiVPQ'
132 'IhAKqx/4qisivvmoM/xbzUagfoxwsu1A/4mGjhBKiS0BCq',
135 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn'
136 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS'
137 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1'
138 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl'
139 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J'
141 'chromepolicytest.com':
142 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP'
143 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A'
144 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA'
145 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC'
146 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO'
149 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej'
150 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW'
151 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP'
152 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv'
153 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo'
159 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49'
160 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ'
161 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL'
162 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C'
163 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy'
164 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi'
165 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=',
169 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/'
170 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4'
171 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x'
172 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM'
173 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin'
175 'chromepolicytest.com':
176 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o'
177 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh'
178 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL'
179 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht'
180 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF'
183 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn'
184 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu'
185 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo'
186 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw'
187 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1'
193 class PolicyRequestHandler(BaseHTTPServer
.BaseHTTPRequestHandler
):
194 """Decodes and handles device management requests from clients.
196 The handler implements all the request parsing and protobuf message decoding
197 and encoding. It calls back into the server to lookup, register, and
201 def __init__(self
, request
, client_address
, server
):
202 """Initialize the handler.
205 request: The request data received from the client as a string.
206 client_address: The client address.
207 server: The TestServer object to use for (un)registering clients.
209 BaseHTTPServer
.BaseHTTPRequestHandler
.__init
__(self
, request
,
210 client_address
, server
)
212 def GetUniqueParam(self
, name
):
213 """Extracts a unique query parameter from the request.
216 name: Names the parameter to fetch.
218 The parameter value or None if the parameter doesn't exist or is not
221 if not hasattr(self
, '_params'):
222 self
._params
= cgi
.parse_qs(self
.path
[self
.path
.find('?') + 1:])
224 param_list
= self
._params
.get(name
, [])
225 if len(param_list
) == 1:
230 """Handles GET requests.
232 Currently this is only used to serve external policy data."""
233 sep
= self
.path
.find('?')
234 path
= self
.path
if sep
== -1 else self
.path
[:sep
]
235 if path
== '/externalpolicydata':
236 http_response
, raw_reply
= self
.HandleExternalPolicyDataRequest()
237 elif path
== '/configuration/test/exit':
238 # This is not part of the standard DM server protocol.
239 # This extension is added to make the test server exit gracefully
240 # when the test is complete.
241 self
.server
.stop
= True
244 elif path
== '/test/ping':
245 # This path and reply are used by the test setup of host-driven tests for
246 # Android to determine if the server is up, and are not part of the
249 raw_reply
= 'Policy server is up.'
252 raw_reply
= 'Invalid path'
253 self
.send_response(http_response
)
255 self
.wfile
.write(raw_reply
)
258 http_response
, raw_reply
= self
.HandleRequest()
259 self
.send_response(http_response
)
260 if (http_response
== 200):
261 self
.send_header('Content-Type', 'application/x-protobuffer')
263 self
.wfile
.write(raw_reply
)
265 def HandleExternalPolicyDataRequest(self
):
266 """Handles a request to download policy data for a component."""
267 policy_key
= self
.GetUniqueParam('key')
269 return (400, 'Missing key parameter')
270 data
= self
.server
.ReadPolicyDataFromDataDir(policy_key
)
272 return (404, 'Policy not found for ' + policy_key
)
275 def HandleRequest(self
):
276 """Handles a request.
278 Parses the data supplied at construction time and returns a pair indicating
279 http status code and response data to be sent back to the client.
282 A tuple of HTTP status code and response data to send to the client.
284 rmsg
= dm
.DeviceManagementRequest()
285 length
= int(self
.headers
.getheader('content-length'))
286 rmsg
.ParseFromString(self
.rfile
.read(length
))
288 logging
.debug('gaia auth token -> ' +
289 self
.headers
.getheader('Authorization', ''))
290 logging
.debug('oauth token -> ' + str(self
.GetUniqueParam('oauth_token')))
291 logging
.debug('deviceid -> ' + str(self
.GetUniqueParam('deviceid')))
292 self
.DumpMessage('Request', rmsg
)
294 request_type
= self
.GetUniqueParam('request')
295 # Check server side requirements, as defined in
296 # device_management_backend.proto.
297 if (self
.GetUniqueParam('devicetype') != '2' or
298 self
.GetUniqueParam('apptype') != 'Chrome' or
299 len(self
.GetUniqueParam('deviceid')) >= 64):
300 return (400, 'Invalid request parameter')
301 if request_type
== 'register':
302 response
= self
.ProcessRegister(rmsg
.register_request
)
303 elif request_type
== 'api_authorization':
304 response
= self
.ProcessApiAuthorization(rmsg
.service_api_access_request
)
305 elif request_type
== 'unregister':
306 response
= self
.ProcessUnregister(rmsg
.unregister_request
)
307 elif request_type
== 'policy':
308 response
= self
.ProcessPolicy(rmsg
, request_type
)
309 elif request_type
== 'enterprise_check':
310 response
= self
.ProcessAutoEnrollment(rmsg
.auto_enrollment_request
)
311 elif request_type
== 'device_state_retrieval':
312 response
= self
.ProcessDeviceStateRetrievalRequest(
313 rmsg
.device_state_retrieval_request
)
314 elif request_type
== 'status_upload':
315 response
= self
.ProcessStatusUploadRequest(
316 rmsg
.device_status_report_request
, rmsg
.session_status_report_request
)
318 return (400, 'Invalid request parameter')
320 if isinstance(response
[1], basestring
):
322 elif isinstance(response
[1], google
.protobuf
.message
.Message
):
323 self
.DumpMessage('Response', response
[1])
324 body
= response
[1].SerializeToString()
327 return (response
[0], body
)
329 def CreatePolicyForExternalPolicyData(self
, policy_key
):
330 """Returns an ExternalPolicyData protobuf for policy_key.
332 If there is policy data for policy_key then the download url will be
333 set so that it points to that data, and the appropriate hash is also set.
334 Otherwise, the protobuf will be empty.
337 policy_key: The policy type and settings entity id, joined by '/'.
340 A serialized ExternalPolicyData.
342 settings
= ep
.ExternalPolicyData()
343 data
= self
.server
.ReadPolicyDataFromDataDir(policy_key
)
345 settings
.download_url
= urlparse
.urljoin(
346 self
.server
.GetBaseURL(), 'externalpolicydata?key=%s' % policy_key
)
347 settings
.secure_hash
= hashlib
.sha256(data
).digest()
348 return settings
.SerializeToString()
350 def CheckGoogleLogin(self
):
351 """Extracts the auth token from the request and returns it. The token may
352 either be a GoogleLogin token from an Authorization header, or an OAuth V2
353 token from the oauth_token query parameter. Returns None if no token is
356 oauth_token
= self
.GetUniqueParam('oauth_token')
360 match
= re
.match('GoogleLogin auth=(\\w+)',
361 self
.headers
.getheader('Authorization', ''))
363 return match
.group(1)
367 def ProcessRegister(self
, msg
):
368 """Handles a register request.
370 Checks the query for authorization and device identifier, registers the
371 device with the server and constructs a response.
374 msg: The DeviceRegisterRequest message received from the client.
377 A tuple of HTTP status code and response data to send to the client.
379 # Check the auth token and device ID.
380 auth
= self
.CheckGoogleLogin()
382 return (403, 'No authorization')
384 policy
= self
.server
.GetPolicies()
385 if ('*' not in policy
['managed_users'] and
386 auth
not in policy
['managed_users']):
387 return (403, 'Unmanaged')
389 device_id
= self
.GetUniqueParam('deviceid')
391 return (400, 'Missing device identifier')
393 token_info
= self
.server
.RegisterDevice(device_id
,
397 # Send back the reply.
398 response
= dm
.DeviceManagementResponse()
399 response
.register_response
.device_management_token
= (
400 token_info
['device_token'])
401 response
.register_response
.machine_name
= token_info
['machine_name']
402 response
.register_response
.enrollment_type
= token_info
['enrollment_mode']
404 return (200, response
)
406 def ProcessApiAuthorization(self
, msg
):
407 """Handles an API authorization request.
410 msg: The DeviceServiceApiAccessRequest message received from the client.
413 A tuple of HTTP status code and response data to send to the client.
415 policy
= self
.server
.GetPolicies()
417 # Return the auth code from the config file if it's defined,
418 # else return a descriptive default value.
419 response
= dm
.DeviceManagementResponse()
420 response
.service_api_access_response
.auth_code
= policy
.get(
421 'robot_api_auth_code', 'policy_testserver.py-auth_code')
423 return (200, response
)
425 def ProcessUnregister(self
, msg
):
426 """Handles a register request.
428 Checks for authorization, unregisters the device and constructs the
432 msg: The DeviceUnregisterRequest message received from the client.
435 A tuple of HTTP status code and response data to send to the client.
437 # Check the management token.
438 token
, response
= self
.CheckToken()
442 # Unregister the device.
443 self
.server
.UnregisterDevice(token
['device_token'])
445 # Prepare and send the response.
446 response
= dm
.DeviceManagementResponse()
447 response
.unregister_response
.CopyFrom(dm
.DeviceUnregisterResponse())
449 return (200, response
)
451 def ProcessPolicy(self
, msg
, request_type
):
452 """Handles a policy request.
454 Checks for authorization, encodes the policy into protobuf representation
455 and constructs the response.
458 msg: The DeviceManagementRequest message received from the client.
461 A tuple of HTTP status code and response data to send to the client.
463 token_info
, error
= self
.CheckToken()
467 key_update_request
= msg
.device_state_key_update_request
468 if len(key_update_request
.server_backed_state_key
) > 0:
469 self
.server
.UpdateStateKeys(token_info
['device_token'],
470 key_update_request
.server_backed_state_key
)
472 # If this is a |publicaccount| request, get the |username| now and use
473 # it in every PolicyFetchResponse produced. This is required to validate
474 # policy for extensions in device-local accounts.
475 # Unfortunately, the |username| can't be obtained from |msg| because that
476 # requires interacting with GAIA.
478 for request
in msg
.policy_request
.request
:
479 if request
.policy_type
== 'google/chromeos/publicaccount':
480 username
= request
.settings_entity_id
482 response
= dm
.DeviceManagementResponse()
483 for request
in msg
.policy_request
.request
:
484 if (request
.policy_type
in
485 ('google/android/user',
486 'google/chromeos/device',
487 'google/chromeos/publicaccount',
488 'google/chromeos/user',
489 'google/chrome/user',
491 fetch_response
= response
.policy_response
.response
.add()
492 self
.ProcessCloudPolicy(request
, token_info
, fetch_response
, username
)
493 elif request
.policy_type
== 'google/chrome/extension':
494 self
.ProcessCloudPolicyForExtensions(
495 request
, response
.policy_response
, token_info
, username
)
497 fetch_response
.error_code
= 400
498 fetch_response
.error_message
= 'Invalid policy_type'
500 return (200, response
)
502 def ProcessAutoEnrollment(self
, msg
):
503 """Handles an auto-enrollment check request.
505 The reply depends on the value of the modulus:
506 1: replies with no new modulus and the sha256 hash of "0"
507 2: replies with a new modulus, 4.
508 4: replies with a new modulus, 2.
509 8: fails with error 400.
510 16: replies with a new modulus, 16.
511 32: replies with a new modulus, 1.
512 anything else: replies with no new modulus and an empty list of hashes
514 These allow the client to pick the testing scenario its wants to simulate.
517 msg: The DeviceAutoEnrollmentRequest message received from the client.
520 A tuple of HTTP status code and response data to send to the client.
522 auto_enrollment_response
= dm
.DeviceAutoEnrollmentResponse()
525 auto_enrollment_response
.hash.extend(
526 self
.server
.GetMatchingStateKeyHashes(msg
.modulus
, msg
.remainder
))
527 elif msg
.modulus
== 2:
528 auto_enrollment_response
.expected_modulus
= 4
529 elif msg
.modulus
== 4:
530 auto_enrollment_response
.expected_modulus
= 2
531 elif msg
.modulus
== 8:
532 return (400, 'Server error')
533 elif msg
.modulus
== 16:
534 auto_enrollment_response
.expected_modulus
= 16
535 elif msg
.modulus
== 32:
536 auto_enrollment_response
.expected_modulus
= 1
538 response
= dm
.DeviceManagementResponse()
539 response
.auto_enrollment_response
.CopyFrom(auto_enrollment_response
)
540 return (200, response
)
542 def ProcessDeviceStateRetrievalRequest(self
, msg
):
543 """Handles a device state retrieval request.
545 Response data is taken from server configuration.
548 A tuple of HTTP status code and response data to send to the client.
550 device_state_retrieval_response
= dm
.DeviceStateRetrievalResponse()
552 client
= self
.server
.LookupByStateKey(msg
.server_backed_state_key
)
553 if client
is not None:
554 state
= self
.server
.GetPolicies().get('device_state', {})
561 setattr(device_state_retrieval_response
, field
, state
[field
])
563 response
= dm
.DeviceManagementResponse()
564 response
.device_state_retrieval_response
.CopyFrom(
565 device_state_retrieval_response
)
566 return (200, response
)
568 def ProcessStatusUploadRequest(self
, device_status
, session_status
):
569 """Handles a device/session status upload request.
572 A tuple of HTTP status code and response data to send to the client.
574 # Empty responses indicate a successful upload.
575 device_status_report_response
= dm
.DeviceStatusReportResponse()
576 session_status_report_response
= dm
.SessionStatusReportResponse()
578 response
= dm
.DeviceManagementResponse()
579 response
.device_status_report_response
.CopyFrom(
580 device_status_report_response
)
581 response
.session_status_report_response
.CopyFrom(
582 session_status_report_response
)
584 return (200, response
)
586 def SetProtobufMessageField(self
, group_message
, field
, field_value
):
587 """Sets a field in a protobuf message.
590 group_message: The protobuf message.
591 field: The field of the message to set, it should be a member of
592 group_message.DESCRIPTOR.fields.
593 field_value: The value to set.
595 if field
.label
== field
.LABEL_REPEATED
:
596 assert type(field_value
) == list
597 entries
= group_message
.__getattribute
__(field
.name
)
598 if field
.message_type
is None:
599 for list_item
in field_value
:
600 entries
.append(list_item
)
602 # This field is itself a protobuf.
603 sub_type
= field
.message_type
604 for sub_value
in field_value
:
605 assert type(sub_value
) == dict
606 # Add a new sub-protobuf per list entry.
607 sub_message
= entries
.add()
608 # Now iterate over its fields and recursively add them.
609 for sub_field
in sub_message
.DESCRIPTOR
.fields
:
610 if sub_field
.name
in sub_value
:
611 value
= sub_value
[sub_field
.name
]
612 self
.SetProtobufMessageField(sub_message
, sub_field
, value
)
614 elif field
.type == field
.TYPE_BOOL
:
615 assert type(field_value
) == bool
616 elif field
.type == field
.TYPE_STRING
:
617 assert type(field_value
) == str or type(field_value
) == unicode
618 elif field
.type == field
.TYPE_INT64
:
619 assert type(field_value
) == int
620 elif (field
.type == field
.TYPE_MESSAGE
and
621 field
.message_type
.name
== 'StringList'):
622 assert type(field_value
) == list
623 entries
= group_message
.__getattribute
__(field
.name
).entries
624 for list_item
in field_value
:
625 entries
.append(list_item
)
628 raise Exception('Unknown field type %s' % field
.type)
629 group_message
.__setattr
__(field
.name
, field_value
)
631 def GatherDevicePolicySettings(self
, settings
, policies
):
632 """Copies all the policies from a dictionary into a protobuf of type
633 CloudDeviceSettingsProto.
636 settings: The destination ChromeDeviceSettingsProto protobuf.
637 policies: The source dictionary containing policies in JSON format.
639 for group
in settings
.DESCRIPTOR
.fields
:
640 # Create protobuf message for group.
641 group_message
= eval('dp.' + group
.message_type
.name
+ '()')
642 # Indicates if at least one field was set in |group_message|.
644 # Iterate over fields of the message and feed them from the
645 # policy config file.
646 for field
in group_message
.DESCRIPTOR
.fields
:
648 if field
.name
in policies
:
650 field_value
= policies
[field
.name
]
651 self
.SetProtobufMessageField(group_message
, field
, field_value
)
653 settings
.__getattribute
__(group
.name
).CopyFrom(group_message
)
655 def GatherUserPolicySettings(self
, settings
, policies
):
656 """Copies all the policies from a dictionary into a protobuf of type
660 settings: The destination: a CloudPolicySettings protobuf.
661 policies: The source: a dictionary containing policies under keys
662 'recommended' and 'mandatory'.
664 for field
in settings
.DESCRIPTOR
.fields
:
665 # |field| is the entry for a specific policy in the top-level
666 # CloudPolicySettings proto.
668 # Look for this policy's value in the mandatory or recommended dicts.
669 if field
.name
in policies
.get('mandatory', {}):
670 mode
= cp
.PolicyOptions
.MANDATORY
671 value
= policies
['mandatory'][field
.name
]
672 elif field
.name
in policies
.get('recommended', {}):
673 mode
= cp
.PolicyOptions
.RECOMMENDED
674 value
= policies
['recommended'][field
.name
]
678 # Create protobuf message for this policy.
679 policy_message
= eval('cp.' + field
.message_type
.name
+ '()')
680 policy_message
.policy_options
.mode
= mode
681 field_descriptor
= policy_message
.DESCRIPTOR
.fields_by_name
['value']
682 self
.SetProtobufMessageField(policy_message
, field_descriptor
, value
)
683 settings
.__getattribute
__(field
.name
).CopyFrom(policy_message
)
685 def ProcessCloudPolicyForExtensions(self
, request
, response
, token_info
,
687 """Handles a request for policy for extensions.
689 A request for policy for extensions is slightly different from the other
690 cloud policy requests, because it can trigger 0, one or many
691 PolicyFetchResponse messages in the response.
694 request: The PolicyFetchRequest that triggered this handler.
695 response: The DevicePolicyResponse message for the response. Multiple
696 PolicyFetchResponses will be appended to this message.
697 token_info: The token extracted from the request.
698 username: The username for the response. May be None.
700 # Send one PolicyFetchResponse for each extension that has
701 # configuration data at the server.
702 ids
= self
.server
.ListMatchingComponents('google/chrome/extension')
703 for settings_entity_id
in ids
:
704 # Reuse the extension policy request, to trigger the same signature
705 # type in the response.
706 request
.settings_entity_id
= settings_entity_id
707 fetch_response
= response
.response
.add()
708 self
.ProcessCloudPolicy(request
, token_info
, fetch_response
, username
)
709 # Don't do key rotations for these messages.
710 fetch_response
.ClearField('new_public_key')
711 fetch_response
.ClearField('new_public_key_signature')
712 fetch_response
.ClearField('new_public_key_verification_signature')
714 def ProcessCloudPolicy(self
, msg
, token_info
, response
, username
=None):
715 """Handles a cloud policy request. (New protocol for policy requests.)
717 Encodes the policy into protobuf representation, signs it and constructs
721 msg: The CloudPolicyRequest message received from the client.
722 token_info: The token extracted from the request.
723 response: A PolicyFetchResponse message that should be filled with the
725 username: The username for the response. May be None.
729 self
.server
.UpdateMachineId(token_info
['device_token'], msg
.machine_id
)
731 # Response is only given if the scope is specified in the config file.
732 # Normally 'google/chromeos/device', 'google/chromeos/user' and
733 # 'google/chromeos/publicaccount' should be accepted.
734 policy
= self
.server
.GetPolicies()
736 policy_key
= msg
.policy_type
737 if msg
.settings_entity_id
:
738 policy_key
+= '/' + msg
.settings_entity_id
739 if msg
.policy_type
in token_info
['allowed_policy_types']:
740 if msg
.policy_type
in ('google/android/user',
741 'google/chromeos/publicaccount',
742 'google/chromeos/user',
743 'google/chrome/user',
745 settings
= cp
.CloudPolicySettings()
746 payload
= self
.server
.ReadPolicyFromDataDir(policy_key
, settings
)
748 self
.GatherUserPolicySettings(settings
, policy
.get(policy_key
, {}))
749 payload
= settings
.SerializeToString()
750 elif dp
is not None and msg
.policy_type
== 'google/chromeos/device':
751 settings
= dp
.ChromeDeviceSettingsProto()
752 payload
= self
.server
.ReadPolicyFromDataDir(policy_key
, settings
)
754 self
.GatherDevicePolicySettings(settings
, policy
.get(policy_key
, {}))
755 payload
= settings
.SerializeToString()
756 elif ep
is not None and msg
.policy_type
== 'google/chrome/extension':
757 settings
= ep
.ExternalPolicyData()
758 payload
= self
.server
.ReadPolicyFromDataDir(policy_key
, settings
)
760 payload
= self
.CreatePolicyForExternalPolicyData(policy_key
)
762 response
.error_code
= 400
763 response
.error_message
= 'Invalid policy type'
766 response
.error_code
= 400
767 response
.error_message
= 'Request not allowed for the token used'
770 # Sign with 'current_key_index', defaulting to key 0.
773 current_key_index
= policy
.get('current_key_index', 0)
774 nkeys
= len(self
.server
.keys
)
775 if (msg
.signature_type
== dm
.PolicyFetchRequest
.SHA1_RSA
and
776 current_key_index
in range(nkeys
)):
777 signing_key
= self
.server
.keys
[current_key_index
]
778 if msg
.public_key_version
in range(1, nkeys
+ 1):
779 # requested key exists, use for signing and rotate.
780 req_key
= self
.server
.keys
[msg
.public_key_version
- 1]['private_key']
782 # Fill the policy data protobuf.
783 policy_data
= dm
.PolicyData()
784 policy_data
.policy_type
= msg
.policy_type
785 policy_data
.timestamp
= int(time
.time() * 1000)
786 policy_data
.request_token
= token_info
['device_token']
787 policy_data
.policy_value
= payload
788 policy_data
.machine_name
= token_info
['machine_name']
789 policy_data
.valid_serial_number_missing
= (
790 token_info
['machine_id'] in BAD_MACHINE_IDS
)
791 policy_data
.settings_entity_id
= msg
.settings_entity_id
792 policy_data
.service_account_identity
= policy
.get(
793 'service_account_identity',
794 'policy_testserver.py-service_account_identity')
795 invalidation_source
= policy
.get('invalidation_source')
796 if invalidation_source
is not None:
797 policy_data
.invalidation_source
= invalidation_source
798 # Since invalidation_name is type bytes in the proto, the Unicode name
799 # provided needs to be encoded as ASCII to set the correct byte pattern.
800 invalidation_name
= policy
.get('invalidation_name')
801 if invalidation_name
is not None:
802 policy_data
.invalidation_name
= invalidation_name
.encode('ascii')
805 policy_data
.public_key_version
= current_key_index
+ 1
808 policy_data
.username
= username
810 # For regular user/device policy, there is no way for the testserver to
811 # know the user name belonging to the GAIA auth token we received (short
812 # of actually talking to GAIA). To address this, we read the username from
813 # the policy configuration dictionary, or use a default.
814 policy_data
.username
= policy
.get('policy_user', 'user@example.com')
815 policy_data
.device_id
= token_info
['device_id']
816 signed_data
= policy_data
.SerializeToString()
818 response
.policy_data
= signed_data
820 response
.policy_data_signature
= (
821 bytes(signing_key
['private_key'].hashAndSign(signed_data
)))
822 if msg
.public_key_version
!= current_key_index
+ 1:
823 response
.new_public_key
= signing_key
['public_key']
825 # Set the verification signature appropriate for the policy domain.
826 # TODO(atwilson): Use the enrollment domain for public accounts when
827 # we add key validation for ChromeOS (http://crbug.com/328038).
828 if 'signatures' in signing_key
:
829 verification_sig
= self
.GetSignatureForDomain(
830 signing_key
['signatures'], policy_data
.username
)
833 assert len(verification_sig
) == 256, \
834 'bad signature size: %d' % len(verification_sig
)
835 response
.new_public_key_verification_signature
= verification_sig
838 response
.new_public_key_signature
= (
839 bytes(req_key
.hashAndSign(response
.new_public_key
)))
841 return (200, response
.SerializeToString())
843 def GetSignatureForDomain(self
, signatures
, username
):
844 parsed_username
= username
.split("@", 1)
845 if len(parsed_username
) != 2:
846 logging
.error('Could not extract domain from username: %s' % username
)
848 domain
= parsed_username
[1]
850 # Lookup the domain's signature in the passed dictionary. If none is found,
851 # fallback to a wildcard signature.
852 if domain
in signatures
:
853 return signatures
[domain
]
854 if '*' in signatures
:
855 return signatures
['*']
857 # No key matching this domain.
858 logging
.error('No verification signature matching domain: %s' % domain
)
861 def CheckToken(self
):
862 """Helper for checking whether the client supplied a valid DM token.
864 Extracts the token from the request and passed to the server in order to
868 A pair of token information record and error response. If the first
869 element is None, then the second contains an error code to send back to
870 the client. Otherwise the first element is the same structure that is
871 returned by LookupToken().
875 request_device_id
= self
.GetUniqueParam('deviceid')
876 match
= re
.match('GoogleDMToken token=(\\w+)',
877 self
.headers
.getheader('Authorization', ''))
879 dmtoken
= match
.group(1)
883 token_info
= self
.server
.LookupToken(dmtoken
)
884 if (not token_info
or
885 not request_device_id
or
886 token_info
['device_id'] != request_device_id
):
889 return (token_info
, None)
891 logging
.debug('Token check failed with error %d' % error
)
893 return (None, (error
, 'Server error %d' % error
))
895 def DumpMessage(self
, label
, msg
):
896 """Helper for logging an ASCII dump of a protobuf message."""
897 logging
.debug('%s\n%s' % (label
, str(msg
)))
900 class PolicyTestServer(testserver_base
.BrokenPipeHandlerMixIn
,
901 testserver_base
.StoppableHTTPServer
):
902 """Handles requests and keeps global service state."""
904 def __init__(self
, server_address
, data_dir
, policy_path
, client_state_file
,
905 private_key_paths
, server_base_url
):
906 """Initializes the server.
909 server_address: Server host and port.
910 policy_path: Names the file to read JSON-formatted policy from.
911 private_key_paths: List of paths to read private keys from.
913 testserver_base
.StoppableHTTPServer
.__init
__(self
, server_address
,
914 PolicyRequestHandler
)
915 self
._registered
_tokens
= {}
916 self
.data_dir
= data_dir
917 self
.policy_path
= policy_path
918 self
.client_state_file
= client_state_file
919 self
.server_base_url
= server_base_url
922 if private_key_paths
:
923 # Load specified keys from the filesystem.
924 for key_path
in private_key_paths
:
926 key_str
= open(key_path
).read()
928 print 'Failed to load private key from %s' % key_path
931 key
= tlslite
.api
.parsePEMKey(key_str
, private
=True)
933 key
= tlslite
.utils
.python_rsakey
.Python_RSAKey
._parsePKCS
8(
936 assert key
is not None
937 key_info
= { 'private_key' : key
}
939 # Now try to read in a signature, if one exists.
941 key_sig
= open(key_path
+ '.sig').read()
942 # Create a dictionary with the wildcard domain + signature
943 key_info
['signatures'] = {'*': key_sig
}
945 print 'Failed to read validation signature from %s.sig' % key_path
946 self
.keys
.append(key_info
)
948 # Use the canned private keys if none were passed from the command line.
949 for signing_key
in SIGNING_KEYS
:
950 decoded_key
= base64
.b64decode(signing_key
['key']);
951 key
= tlslite
.utils
.python_rsakey
.Python_RSAKey
._parsePKCS
8(
952 bytearray(decoded_key
))
953 assert key
is not None
954 # Grab the signature dictionary for this key and decode all of the
956 signature_dict
= signing_key
['signatures']
957 decoded_signatures
= {}
958 for domain
in signature_dict
:
959 decoded_signatures
[domain
] = base64
.b64decode(signature_dict
[domain
])
960 self
.keys
.append({'private_key': key
,
961 'signatures': decoded_signatures
})
963 # Derive the public keys from the private keys.
964 for entry
in self
.keys
:
965 key
= entry
['private_key']
967 algorithm
= asn1der
.Sequence(
968 [ asn1der
.Data(asn1der
.OBJECT_IDENTIFIER
, PKCS1_RSA_OID
),
969 asn1der
.Data(asn1der
.NULL
, '') ])
970 rsa_pubkey
= asn1der
.Sequence([ asn1der
.Integer(key
.n
),
971 asn1der
.Integer(key
.e
) ])
972 pubkey
= asn1der
.Sequence([ algorithm
, asn1der
.Bitstring(rsa_pubkey
) ])
973 entry
['public_key'] = pubkey
976 if self
.client_state_file
is not None:
978 file_contents
= open(self
.client_state_file
).read()
979 self
._registered
_tokens
= json
.loads(file_contents
, strict
=False)
983 def GetPolicies(self
):
984 """Returns the policies to be used, reloaded form the backend file every
989 print 'No JSON module, cannot parse policy information'
992 policy
= json
.loads(open(self
.policy_path
).read(), strict
=False)
994 print 'Failed to load policy from %s' % self
.policy_path
997 def RegisterDevice(self
, device_id
, machine_id
, type):
998 """Registers a device or user and generates a DM token for it.
1001 device_id: The device identifier provided by the client.
1004 The newly generated device token for the device.
1007 while len(dmtoken_chars
) < 32:
1008 dmtoken_chars
.append(random
.choice('0123456789abcdef'))
1009 dmtoken
= ''.join(dmtoken_chars
)
1010 allowed_policy_types
= {
1011 dm
.DeviceRegisterRequest
.BROWSER
: [
1012 'google/chrome/user',
1013 'google/chrome/extension'
1015 dm
.DeviceRegisterRequest
.USER
: [
1016 'google/chromeos/user',
1017 'google/chrome/extension'
1019 dm
.DeviceRegisterRequest
.DEVICE
: [
1020 'google/chromeos/device',
1021 'google/chromeos/publicaccount',
1022 'google/chrome/extension'
1024 dm
.DeviceRegisterRequest
.ANDROID_BROWSER
: [
1025 'google/android/user'
1027 dm
.DeviceRegisterRequest
.IOS_BROWSER
: [
1030 dm
.DeviceRegisterRequest
.TT
: ['google/chromeos/user',
1031 'google/chrome/user'],
1033 if machine_id
in KIOSK_MACHINE_IDS
:
1034 enrollment_mode
= dm
.DeviceRegisterResponse
.RETAIL
1036 enrollment_mode
= dm
.DeviceRegisterResponse
.ENTERPRISE
1037 self
._registered
_tokens
[dmtoken
] = {
1038 'device_id': device_id
,
1039 'device_token': dmtoken
,
1040 'allowed_policy_types': allowed_policy_types
[type],
1041 'machine_name': 'chromeos-' + machine_id
,
1042 'machine_id': machine_id
,
1043 'enrollment_mode': enrollment_mode
,
1045 self
.WriteClientState()
1046 return self
._registered
_tokens
[dmtoken
]
1048 def UpdateMachineId(self
, dmtoken
, machine_id
):
1049 """Updates the machine identifier for a registered device.
1052 dmtoken: The device management token provided by the client.
1053 machine_id: Updated hardware identifier value.
1055 if dmtoken
in self
._registered
_tokens
:
1056 self
._registered
_tokens
[dmtoken
]['machine_id'] = machine_id
1057 self
.WriteClientState()
1059 def UpdateStateKeys(self
, dmtoken
, state_keys
):
1060 """Updates the state keys for a given client.
1063 dmtoken: The device management token provided by the client.
1064 state_keys: The state keys to set.
1066 if dmtoken
in self
._registered
_tokens
:
1067 self
._registered
_tokens
[dmtoken
]['state_keys'] = map(
1068 lambda key
: key
.encode('hex'), state_keys
)
1069 self
.WriteClientState()
1071 def LookupToken(self
, dmtoken
):
1072 """Looks up a device or a user by DM token.
1075 dmtoken: The device management token provided by the client.
1078 A dictionary with information about a device or user that is registered by
1079 dmtoken, or None if the token is not found.
1081 return self
._registered
_tokens
.get(dmtoken
, None)
1083 def LookupByStateKey(self
, state_key
):
1084 """Looks up a device or a user by a state key.
1087 state_key: The state key provided by the client.
1090 A dictionary with information about a device or user or None if there is
1093 for client
in self
._registered
_tokens
.values():
1094 if state_key
.encode('hex') in client
.get('state_keys', []):
1099 def GetMatchingStateKeyHashes(self
, modulus
, remainder
):
1100 """Returns all clients registered with the server.
1103 The list of registered clients.
1105 state_keys
= sum([ c
.get('state_keys', [])
1106 for c
in self
._registered
_tokens
.values() ], [])
1107 hashed_keys
= map(lambda key
: hashlib
.sha256(key
.decode('hex')).digest(),
1110 lambda hash : int(hash.encode('hex'), 16) % modulus
== remainder
,
1113 def UnregisterDevice(self
, dmtoken
):
1114 """Unregisters a device identified by the given DM token.
1117 dmtoken: The device management token provided by the client.
1119 if dmtoken
in self
._registered
_tokens
.keys():
1120 del self
._registered
_tokens
[dmtoken
]
1121 self
.WriteClientState()
1123 def WriteClientState(self
):
1124 """Writes the client state back to the file."""
1125 if self
.client_state_file
is not None:
1126 json_data
= json
.dumps(self
._registered
_tokens
)
1127 open(self
.client_state_file
, 'w').write(json_data
)
1129 def GetBaseFilename(self
, policy_selector
):
1130 """Returns the base filename for the given policy_selector.
1133 policy_selector: The policy type and settings entity id, joined by '/'.
1136 The filename corresponding to the policy_selector, without a file
1139 sanitized_policy_selector
= re
.sub('[^A-Za-z0-9.@-]', '_', policy_selector
)
1140 return os
.path
.join(self
.data_dir
or '',
1141 'policy_%s' % sanitized_policy_selector
)
1143 def ListMatchingComponents(self
, policy_type
):
1144 """Returns a list of settings entity IDs that have a configuration file.
1147 policy_type: The policy type to look for. Only settings entity IDs for
1148 file selectors That match this policy_type will be returned.
1151 A list of settings entity IDs for the given |policy_type| that have a
1152 configuration file in this server (either as a .bin, .txt or .data file).
1154 base_name
= self
.GetBaseFilename(policy_type
)
1155 files
= glob
.glob('%s_*.*' % base_name
)
1156 len_base_name
= len(base_name
) + 1
1157 return [ file[len_base_name
:file.rfind('.')] for file in files
]
1159 def ReadPolicyFromDataDir(self
, policy_selector
, proto_message
):
1160 """Tries to read policy payload from a file in the data directory.
1162 First checks for a binary rendition of the policy protobuf in
1163 <data_dir>/policy_<sanitized_policy_selector>.bin. If that exists, returns
1164 it. If that file doesn't exist, tries
1165 <data_dir>/policy_<sanitized_policy_selector>.txt and decodes that as a
1166 protobuf using proto_message. If that fails as well, returns None.
1169 policy_selector: Selects which policy to read.
1170 proto_message: Optional protobuf message object used for decoding the
1174 The binary payload message, or None if not found.
1176 base_filename
= self
.GetBaseFilename(policy_selector
)
1178 # Try the binary payload file first.
1180 return open(base_filename
+ '.bin').read()
1184 # If that fails, try the text version instead.
1185 if proto_message
is None:
1189 text
= open(base_filename
+ '.txt').read()
1190 google
.protobuf
.text_format
.Merge(text
, proto_message
)
1191 return proto_message
.SerializeToString()
1194 except google
.protobuf
.text_format
.ParseError
:
1197 def ReadPolicyDataFromDataDir(self
, policy_selector
):
1198 """Returns the external policy data for |policy_selector| if found.
1201 policy_selector: Selects which policy to read.
1204 The data for the corresponding policy type and entity id, if found.
1206 base_filename
= self
.GetBaseFilename(policy_selector
)
1208 return open(base_filename
+ '.data').read()
1212 def GetBaseURL(self
):
1213 """Returns the server base URL.
1215 Respects the |server_base_url| configuration parameter, if present. Falls
1216 back to construct the URL from the server hostname and port otherwise.
1219 The URL to use for constructing URLs that get returned to clients.
1221 base_url
= self
.server_base_url
1222 if base_url
is None:
1223 base_url
= 'http://%s:%s' % self
.server_address
[:2]
1228 class PolicyServerRunner(testserver_base
.TestServerRunner
):
1231 super(PolicyServerRunner
, self
).__init
__()
1233 def create_server(self
, server_data
):
1234 data_dir
= self
.options
.data_dir
or ''
1235 config_file
= (self
.options
.config_file
or
1236 os
.path
.join(data_dir
, 'device_management'))
1237 server
= PolicyTestServer((self
.options
.host
, self
.options
.port
),
1238 data_dir
, config_file
,
1239 self
.options
.client_state_file
,
1240 self
.options
.policy_keys
,
1241 self
.options
.server_base_url
)
1242 server_data
['port'] = server
.server_port
1245 def add_options(self
):
1246 testserver_base
.TestServerRunner
.add_options(self
)
1247 self
.option_parser
.add_option('--client-state', dest
='client_state_file',
1248 help='File that client state should be '
1249 'persisted to. This allows the server to be '
1250 'seeded by a list of pre-registered clients '
1251 'and restarts without abandoning registered '
1253 self
.option_parser
.add_option('--policy-key', action
='append',
1255 help='Specify a path to a PEM-encoded '
1256 'private key to use for policy signing. May '
1257 'be specified multiple times in order to '
1258 'load multiple keys into the server. If the '
1259 'server has multiple keys, it will rotate '
1260 'through them in at each request in a '
1261 'round-robin fashion. The server will '
1262 'use a canned key if none is specified '
1263 'on the command line. The test server will '
1264 'also look for a verification signature file '
1265 'in the same location: <filename>.sig and if '
1266 'present will add the signature to the '
1267 'policy blob as appropriate via the '
1268 'new_public_key_verification_signature '
1270 self
.option_parser
.add_option('--log-level', dest
='log_level',
1272 help='Log level threshold to use.')
1273 self
.option_parser
.add_option('--config-file', dest
='config_file',
1274 help='Specify a configuration file to use '
1275 'instead of the default '
1276 '<data_dir>/device_management')
1277 self
.option_parser
.add_option('--server-base-url', dest
='server_base_url',
1278 help='The server base URL to use when '
1279 'constructing URLs to return to the client.')
1281 def run_server(self
):
1282 logger
= logging
.getLogger()
1283 logger
.setLevel(getattr(logging
, str(self
.options
.log_level
).upper()))
1284 if (self
.options
.log_to_console
):
1285 logger
.addHandler(logging
.StreamHandler())
1286 if (self
.options
.log_file
):
1287 logger
.addHandler(logging
.FileHandler(self
.options
.log_file
))
1289 testserver_base
.TestServerRunner
.run_server(self
)
1292 if __name__
== '__main__':
1293 sys
.exit(PolicyServerRunner().main())