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": "",
51 "invalidation_source": 1025,
52 "invalidation_name": "UENUPOL"
61 import google
.protobuf
.text_format
73 import tlslite
.utils
.cryptomath
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.
86 import chrome_extension_policy_pb2
as ep
90 # Device policy is only available on Chrome OS builds.
92 import chrome_device_policy_pb2
as dp
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>,
112 # <domain1>: <base64-encdoded SHA256 signature for key + domain1>
113 # <domain2>: <base64-encdoded SHA256 signature for key + domain2>
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',
129 'l+sT5mziei/GbmiP7VtRCCfwpZcg7uKbW2OlnK5B/TTELutjEIAMdHduNBwbO44qOn'
130 '/5c7YrtkXbBehaaDYFPGI6bGTbDmG9KRxhS+DaB7opgfCQWLi79Gn/jytKLZhRN/VS'
131 'y+PEbezqMi3d1/xDxlThwWZDNwnhv9ER/Nu/32ZTjzgtqonSn2CQtwXCIILm4FdV/1'
132 '/BdmZG+Ge4i4FTqYtInir5YFe611KXU/AveGhQGBIAXo4qYg1IqbVrvKBSU9dlI6Sl'
133 '9TJJLbJ3LGaXuljgFhyMAl3gcy7ftC9MohEmwa+sc7y2mOAgYQ5SSmyAtQwQgAkX9J'
135 'chromepolicytest.com':
136 'TzBiigZKwBdr6lyP6tUDsw+Q9wYO1Yepyxm0O4JZ4RID32L27sWzC1/hwC51fRcCvP'
137 'luEVIW6mH+BFODXMrteUFWfbbG7jgV+Wg+QdzMqgJjxhNKFXPTsZ7/286LAd1vBY/A'
138 'nGd8Wog6AhzfrgMbLNsH794GD0xIUwRvXUWFNP8pClj5VPgQnJrIA9aZwW8FNGbteA'
139 'HacFB0T/oqP5s7XT4Qvkj14RLmCgTwEM8Vcpqy5teJaF8yN17wniveddoOQGH6s0HC'
140 'ocprEccrH5fP/WVAPxCfx4vVYQY5q4CZ4K3f6dTC2FV4IDelM6dugEkvSS02YCzDaO'
143 'T0wXC5w3GXyovA09pyOLX7ui/NI603UfbZXYyTbHI7xtzCIaHVPH35Nx4zdqVrdsej'
144 'ErQ12yVLDDIJokY4Yl+/fj/zrkAPxThI+TNQ+jo0i+al05PuopfpzvCzIXiZBbkbyW'
145 '3XfedxXP3IPN2XU2/3vX+ZXUNG6pxeETem64kGezkjkUraqnHw3JVzwJYHhpMcwdLP'
146 'PYK6V23BbEHEVBtQZd/ledXacz7gOzm1zGni4e+vxA2roAdJWyhbjU0dTKNNUsZmMv'
147 'ryQH9Af1Jw+dqs0RAbhcJXm2i8EUWIgNv6aMn1Z2DzZwKKjXsKgcYSRo8pdYa8RZAo'
153 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmZhreV04M3knCi6wibr49'
154 'oDesHny1G33PKOX9ko8pcxAiu9ZqsKCj7wNW2PGqnLi81fddACwQtYn5xdhCtzB9wIDAQ'
155 'ABAkA0z8m0cy8N08xundspoFZWO71WJLgv/peSDBYGI0RzJR1l9Np355EukQUQwRs5XrL'
156 '3vRQZy2vDqeiR96epkAhRAiEAzJ4DVI8k3pAl7CGv5icqFkJ02viExIwehhIEXBcB6p0C'
157 'IQDAKmzpoRpBEZRQ9xrTvPOi+Ea8Jnd478BU7CI/LFfgowIgMfLIoVWoDGRnvXKju60Hy'
158 'xNB70oHLut9cADp64j6QMkCIDrgxN4QbmrhaAAmtiGKE1wrlgCwCIsVamiasSOKAqLhAi'
159 'EAo/ItVcFtQPod97qG71CY/O4JzOciuU6AMhprs181vfM=',
163 'cO0nQjRptkeefKDw5QpJSQDavHABxUvbR9Wvoa235OG9Whw1RFqq2ye6pKnI3ezW6/'
164 '7b4ANcpi5a7HV5uF8K7gWyYdxY8NHLeyrbwXxg5j6HAmHmkP1UZcf/dAnWqo7cW8g4'
165 'DIQOhC43KkveMYJ2HnelwdXt/7zqkbe8/3Yj4nhjAUeARx86Sb8Nzydwkrvqs5Jw/x'
166 '5LG+BODExrXXcGu/ubDlW4ivJFqfNUPQysqBXSMY2XCHPJDx3eECLGVVN/fFAWWgjM'
167 'HFObAriAt0b18cc9Nr0mAt4Qq1oDzWcAHCPHE+5dr8Uf46BUrMLJRNRKCY7rrsoIin'
169 'chromepolicytest.com':
170 'mr+9CCYvR0cTvPwlzkxqlpGYy55gY7cPiIkPAPoql51yHK1tkMTOSFru8Dy/nMt+0o'
171 '4z7WO60F1wnIBGkQxnTj/DsO6QpCYi7oHqtLmZ2jsLQFlMyvPGUtpJEFvRwjr/TNbh'
172 '6RqUtz1LQFuJQ848kBrx7nkte1L8SuPDExgx+Q3LtbNj4SuTdvMUBMvEERXiLuwfFL'
173 'BefGjtsqfWETQVlJTCW7xcqOLedIX8UYgEDBpDOZ23A3GzCShuBsIut5m87R5mODht'
174 'EUmKNDK1+OMc6SyDpf+r48Wph4Db1bVaKy8fcpSNJOwEgsrmH7/+owKPGcN7I5jYAF'
177 'o5MVSo4bRwIJ/aooGyXpRXsEsWPG8fNA2UTG8hgwnLYhNeJCCnLs/vW2vdp0URE8jn'
178 'qiG4N8KjbuiGw0rJtO1EygdLfpnMEtqYlFjrOie38sy92l/AwohXj6luYzMWL+FqDu'
179 'WQeXasjgyY4s9BOLQVDEnEj3pvqhrk/mXvMwUeXGpbxTNbWAd0C8BTZrGOwU/kIXxo'
180 'vAMGg8L+rQaDwBTEnMsMZcvlrIyqSg5v4BxCWuL3Yd2xvUqZEUWRp1aKetsHRnz5hw'
181 'H7WK7DzvKepDn06XjPG9lchi448U3HB3PRKtCzfO3nD9YXMKTuqRpKPF8PeK11CWh1'
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
195 def __init__(self
, request
, client_address
, server
):
196 """Initialize the handler.
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.
210 name: Names the parameter to fetch.
212 The parameter value or None if the parameter doesn't exist or is not
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:
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
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
243 raw_reply
= 'Policy server is up.'
246 raw_reply
= 'Invalid path'
247 self
.send_response(http_response
)
249 self
.wfile
.write(raw_reply
)
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')
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')
263 return (400, 'Missing key parameter')
264 data
= self
.server
.ReadPolicyDataFromDataDir(policy_key
)
266 return (404, 'Policy not found for ' + policy_key
)
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.
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
)
312 return (400, 'Invalid request parameter')
314 if isinstance(response
[1], basestring
):
316 elif isinstance(response
[1], google
.protobuf
.message
.Message
):
317 self
.DumpMessage('Response', response
[1])
318 body
= response
[1].SerializeToString()
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.
331 policy_key: The policy type and settings entity id, joined by '/'.
334 A serialized ExternalPolicyData.
336 settings
= ep
.ExternalPolicyData()
337 data
= self
.server
.ReadPolicyDataFromDataDir(policy_key
)
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
350 oauth_token
= self
.GetUniqueParam('oauth_token')
354 match
= re
.match('GoogleLogin auth=(\\w+)',
355 self
.headers
.getheader('Authorization', ''))
357 return match
.group(1)
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.
368 msg: The DeviceRegisterRequest message received from the client.
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()
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')
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.
404 msg: The DeviceServiceApiAccessRequest message received from the client.
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
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
427 msg: The DeviceUnregisterRequest message received from the client.
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()
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.
453 msg: The DeviceManagementRequest message received from the client.
456 A tuple of HTTP status code and response data to send to the client.
458 token_info
, error
= self
.CheckToken()
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',
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
)
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.
518 msg: The DeviceAutoEnrollmentRequest message received from the client.
521 A tuple of HTTP status code and response data to send to the client.
523 auto_enrollment_response
= dm
.DeviceAutoEnrollmentResponse()
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.
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', {})
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.
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.
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
)
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
)
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
)
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.
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|.
645 # Iterate over fields of the message and feed them from the
646 # policy config file.
647 for field
in group_message
.DESCRIPTOR
.fields
:
649 if field
.name
in policies
:
651 field_value
= policies
[field
.name
]
652 self
.SetProtobufMessageField(group_message
, field
, field_value
)
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
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
]
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
,
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.
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
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
726 username: The username for the response. May be None.
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()
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',
746 settings
= cp
.CloudPolicySettings()
747 payload
= self
.server
.ReadPolicyFromDataDir(policy_key
, settings
)
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
)
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
)
761 payload
= self
.CreatePolicyForExternalPolicyData(policy_key
)
763 response
.error_code
= 400
764 response
.error_message
= 'Invalid policy type'
767 response
.error_code
= 400
768 response
.error_message
= 'Request not allowed for the token used'
771 # Sign with 'current_key_index', defaulting to key 0.
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')
806 policy_data
.public_key_version
= current_key_index
+ 1
809 policy_data
.username
= username
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
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
)
842 assert len(verification_sig
) == 256, \
843 'bad signature size: %d' % len(verification_sig
)
844 response
.new_public_key_verification_signature
= verification_sig
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
)
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
)
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
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().
884 request_device_id
= self
.GetUniqueParam('deviceid')
885 match
= re
.match('GoogleDMToken token=(\\w+)',
886 self
.headers
.getheader('Authorization', ''))
888 dmtoken
= match
.group(1)
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
):
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.
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
931 if private_key_paths
:
932 # Load specified keys from the filesystem.
933 for key_path
in private_key_paths
:
935 key_str
= open(key_path
).read()
937 print 'Failed to load private key from %s' % key_path
940 key
= tlslite
.api
.parsePEMKey(key_str
, private
=True)
942 key
= tlslite
.utils
.python_rsakey
.Python_RSAKey
._parsePKCS
8(
945 assert key
is not None
946 key_info
= { 'private_key' : key
}
948 # Now try to read in a signature, if one exists.
950 key_sig
= open(key_path
+ '.sig').read()
951 # Create a dictionary with the wildcard domain + signature
952 key_info
['signatures'] = {'*': key_sig
}
954 print 'Failed to read validation signature from %s.sig' % key_path
955 self
.keys
.append(key_info
)
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
._parsePKCS
8(
961 bytearray(decoded_key
))
962 assert key
is not None
963 # Grab the signature dictionary for this key and decode all of the
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
985 if self
.client_state_file
is not None:
987 file_contents
= open(self
.client_state_file
).read()
988 self
._registered
_tokens
= json
.loads(file_contents
, strict
=False)
992 def GetPolicies(self
):
993 """Returns the policies to be used, reloaded form the backend file every
998 logging
.error('No JSON module, cannot parse policy information')
1001 policy
= json
.loads(open(self
.policy_path
).read(), strict
=False)
1003 logging
.error('Failed to load policies from %s' % self
.policy_path
)
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:
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.
1029 device_id: The device identifier provided by the client.
1032 The newly generated device token for the device.
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
: [
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
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.
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.
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.
1104 dmtoken: The device management token provided by the client.
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.
1116 state_key: The state key provided by the client.
1119 A dictionary with information about a device or user or None if there is
1122 for client
in self
._registered
_tokens
.values():
1123 if state_key
.encode('hex') in client
.get('state_keys', []):
1128 def GetMatchingStateKeyHashes(self
, modulus
, remainder
):
1129 """Returns all clients registered with the server.
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(),
1139 lambda hash : int(hash.encode('hex'), 16) % modulus
== remainder
,
1142 def UnregisterDevice(self
, dmtoken
):
1143 """Unregisters a device identified by the given DM token.
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.
1162 policy_selector: The policy type and settings entity id, joined by '/'.
1165 The filename corresponding to the policy_selector, without a file
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.
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.
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.
1198 policy_selector: Selects which policy to read.
1199 proto_message: Optional protobuf message object used for decoding the
1203 The binary payload message, or None if not found.
1205 base_filename
= self
.GetBaseFilename(policy_selector
)
1207 # Try the binary payload file first.
1209 return open(base_filename
+ '.bin').read()
1213 # If that fails, try the text version instead.
1214 if proto_message
is None:
1218 text
= open(base_filename
+ '.txt').read()
1219 google
.protobuf
.text_format
.Merge(text
, proto_message
)
1220 return proto_message
.SerializeToString()
1223 except google
.protobuf
.text_format
.ParseError
:
1226 def ReadPolicyDataFromDataDir(self
, policy_selector
):
1227 """Returns the external policy data for |policy_selector| if found.
1230 policy_selector: Selects which policy to read.
1233 The data for the corresponding policy type and entity id, if found.
1235 base_filename
= self
.GetBaseFilename(policy_selector
)
1237 return open(base_filename
+ '.data').read()
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.
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]
1257 class PolicyServerRunner(testserver_base
.TestServerRunner
):
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
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 '
1282 self
.option_parser
.add_option('--policy-key', action
='append',
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 '
1299 self
.option_parser
.add_option('--log-level', dest
='log_level',
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())