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