Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(310)

Side by Side Diff: net/tools/testserver/device_management.py

Issue 6161007: New protocol and testserver for the Chrome-DMServer protocol (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: address comments + some cleanup Created 9 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « net/test/test_server.cc ('k') | net/tools/testserver/testserver.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python2.5 1 #!/usr/bin/python2.5
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """A bare-bones test server for testing cloud policy support. 6 """A bare-bones test server for testing cloud policy support.
7 7
8 This implements a simple cloud policy test server that can be used to test 8 This implements a simple cloud policy test server that can be used to test
9 chrome's device management service client. The policy information is read from 9 chrome's device management service client. The policy information is read from
10 from files in a directory. The files should contain policy definitions in JSON 10 the file named device_management in the server's data directory. It contains
11 format, using the top-level dictionary as a key/value store. The format is 11 enforced and recommended policies for the device and user scope, and a list
12 identical to what the Linux implementation reads from /etc. Here is an example: 12 of managed users.
13
14 The format of the file is JSON. The root dictionary contains a list under the
15 key "managed_users". It contains auth tokens for which the server will claim
16 that the user is managed. The token string "*" indicates that all users are
17 claimed to be managed. Other keys in the root dictionary identify request
18 scopes. Each request scope is described by a dictionary that holds two
19 sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
20 definitions as key/value stores, their format is identical to what the Linux
21 implementation reads from /etc.
22
23 Example:
13 24
14 { 25 {
15 "HomepageLocation" : "http://www.chromium.org" 26 "chromeos/device": {
27 "mandatory": {
28 "HomepageLocation" : "http://www.chromium.org"
29 },
30 "recommended": {
31 "JavascriptEnabled": false,
32 },
33 },
34 "managed_users": [
35 "secret123456"
36 ]
16 } 37 }
17 38
39
18 """ 40 """
19 41
20 import cgi 42 import cgi
21 import logging 43 import logging
44 import os
22 import random 45 import random
23 import re 46 import re
24 import sys 47 import sys
48 import time
49 import tlslite
50 import tlslite.api
25 51
26 # The name and availability of the json module varies in python versions. 52 # The name and availability of the json module varies in python versions.
27 try: 53 try:
28 import simplejson as json 54 import simplejson as json
29 except ImportError: 55 except ImportError:
30 try: 56 try:
31 import json 57 import json
32 except ImportError: 58 except ImportError:
33 json = None 59 json = None
34 60
35 import device_management_backend_pb2 as dm 61 import device_management_backend_pb2 as dm
62 import cloud_policy_pb2 as cp
63
36 64
37 class RequestHandler(object): 65 class RequestHandler(object):
38 """Decodes and handles device management requests from clients. 66 """Decodes and handles device management requests from clients.
39 67
40 The handler implements all the request parsing and protobuf message decoding 68 The handler implements all the request parsing and protobuf message decoding
41 and encoding. It calls back into the server to lookup, register, and 69 and encoding. It calls back into the server to lookup, register, and
42 unregister clients. 70 unregister clients.
43 """ 71 """
44 72
45 def __init__(self, server, path, headers, request): 73 def __init__(self, server, path, headers, request):
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
88 116
89 self.DumpMessage('Request', rmsg) 117 self.DumpMessage('Request', rmsg)
90 118
91 request_type = self.GetUniqueParam('request') 119 request_type = self.GetUniqueParam('request')
92 if request_type == 'register': 120 if request_type == 'register':
93 return self.ProcessRegister(rmsg.register_request) 121 return self.ProcessRegister(rmsg.register_request)
94 elif request_type == 'unregister': 122 elif request_type == 'unregister':
95 return self.ProcessUnregister(rmsg.unregister_request) 123 return self.ProcessUnregister(rmsg.unregister_request)
96 elif request_type == 'policy': 124 elif request_type == 'policy':
97 return self.ProcessPolicy(rmsg.policy_request) 125 return self.ProcessPolicy(rmsg.policy_request)
126 elif request_type == 'cloud_policy':
127 return self.ProcessCloudPolicyRequest(rmsg.cloud_policy_request)
128 elif request_type == 'managed_check':
129 return self.ProcessManagedCheck(rmsg.managed_check_request)
98 else: 130 else:
99 return (400, 'Invalid request parameter') 131 return (400, 'Invalid request parameter')
100 132
133 def CheckGoogleLogin(self):
134 """Extracts the GoogleLogin auth token from the HTTP request, and
135 returns it. Returns None if the token is not present.
136 """
137 match = re.match('GoogleLogin auth=(\\w+)',
138 self._headers.getheader('Authorization', ''))
139 if not match:
140 return None
141 return match.group(1)
142
143 def GetDeviceName(self):
144 """Returns the name for the currently authenticated device based on its
145 device id.
146 """
147 return 'chromeos-' + self.GetUniqueParam('deviceid')
148
101 def ProcessRegister(self, msg): 149 def ProcessRegister(self, msg):
102 """Handles a register request. 150 """Handles a register request.
103 151
104 Checks the query for authorization and device identifier, registers the 152 Checks the query for authorization and device identifier, registers the
105 device with the server and constructs a response. 153 device with the server and constructs a response.
106 154
107 Args: 155 Args:
108 msg: The DeviceRegisterRequest message received from the client. 156 msg: The DeviceRegisterRequest message received from the client.
109 157
110 Returns: 158 Returns:
111 A tuple of HTTP status code and response data to send to the client. 159 A tuple of HTTP status code and response data to send to the client.
112 """ 160 """
113 # Check the auth token and device ID. 161 # Check the auth token and device ID.
114 match = re.match('GoogleLogin auth=(\\w+)', 162 if not self.CheckGoogleLogin():
115 self._headers.getheader('Authorization', ''))
116 if not match:
117 return (403, 'No authorization') 163 return (403, 'No authorization')
118 auth_token = match.group(1)
119 164
120 device_id = self.GetUniqueParam('deviceid') 165 device_id = self.GetUniqueParam('deviceid')
121 if not device_id: 166 if not device_id:
122 return (400, 'Missing device identifier') 167 return (400, 'Missing device identifier')
123 168
124 # Register the device and create a token. 169 # Register the device and create a token.
125 dmtoken = self._server.RegisterDevice(device_id) 170 dmtoken = self._server.RegisterDevice(device_id)
126 171
127 # Send back the reply. 172 # Send back the reply.
128 response = dm.DeviceManagementResponse() 173 response = dm.DeviceManagementResponse()
129 response.error = dm.DeviceManagementResponse.SUCCESS 174 response.error = dm.DeviceManagementResponse.SUCCESS
130 response.register_response.device_management_token = dmtoken 175 response.register_response.device_management_token = dmtoken
176 response.register_response.device_name = self.GetDeviceName()
131 177
132 self.DumpMessage('Response', response) 178 self.DumpMessage('Response', response)
133 179
134 return (200, response.SerializeToString()) 180 return (200, response.SerializeToString())
135 181
136 def ProcessUnregister(self, msg): 182 def ProcessUnregister(self, msg):
137 """Handles a register request. 183 """Handles a register request.
138 184
139 Checks for authorization, unregisters the device and constructs the 185 Checks for authorization, unregisters the device and constructs the
140 response. 186 response.
(...skipping 14 matching lines...) Expand all
155 201
156 # Prepare and send the response. 202 # Prepare and send the response.
157 response = dm.DeviceManagementResponse() 203 response = dm.DeviceManagementResponse()
158 response.error = dm.DeviceManagementResponse.SUCCESS 204 response.error = dm.DeviceManagementResponse.SUCCESS
159 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) 205 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
160 206
161 self.DumpMessage('Response', response) 207 self.DumpMessage('Response', response)
162 208
163 return (200, response.SerializeToString()) 209 return (200, response.SerializeToString())
164 210
211 def ProcessManagedCheck(self, msg):
212 """Handles a 'managed check' request.
213
214 Queries the list of managed users and responds the client if their user
215 is managed or not.
216
217 Args:
218 msg: The ManagedCheckRequest message received from the client.
219
220 Returns:
221 A tuple of HTTP status code and response data to send to the client.
222 """
223 # Check the management token.
224 auth = self.CheckGoogleLogin()
225 if not auth:
226 return (403, 'No authorization')
227
228 managed_check_response = dm.ManagedCheckResponse()
229 if ('*' in self._server.policy['managed_users'] or
230 auth in self._server.policy['managed_users']):
231 managed_check_response.mode = dm.ManagedCheckResponse.MANAGED;
232 else:
233 managed_check_response.mode = dm.ManagedCheckResponse.UNMANAGED;
234
235 # Prepare and send the response.
236 response = dm.DeviceManagementResponse()
237 response.error = dm.DeviceManagementResponse.SUCCESS
238 response.managed_check_response.CopyFrom(managed_check_response)
239
240 self.DumpMessage('Response', response)
241
242 return (200, response.SerializeToString())
243
165 def ProcessPolicy(self, msg): 244 def ProcessPolicy(self, msg):
166 """Handles a policy request. 245 """Handles a policy request.
167 246
168 Checks for authorization, encodes the policy into protobuf representation 247 Checks for authorization, encodes the policy into protobuf representation
169 and constructs the repsonse. 248 and constructs the response.
170 249
171 Args: 250 Args:
172 msg: The DevicePolicyRequest message received from the client. 251 msg: The DevicePolicyRequest message received from the client.
173 252
174 Returns: 253 Returns:
175 A tuple of HTTP status code and response data to send to the client. 254 A tuple of HTTP status code and response data to send to the client.
176 """ 255 """
177 # Check the management token. 256 # Check the management token.
178 token, response = self.CheckToken() 257 token, response = self.CheckToken()
179 if not token: 258 if not token:
(...skipping 27 matching lines...) Expand all
207 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY 286 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY
208 for list_entry in value: 287 for list_entry in value:
209 entry_value.string_array.append(str(list_entry)) 288 entry_value.string_array.append(str(list_entry))
210 entry.value.CopyFrom(entry_value) 289 entry.value.CopyFrom(entry_value)
211 setting.policy_value.CopyFrom(policy_value) 290 setting.policy_value.CopyFrom(policy_value)
212 291
213 self.DumpMessage('Response', response) 292 self.DumpMessage('Response', response)
214 293
215 return (200, response.SerializeToString()) 294 return (200, response.SerializeToString())
216 295
296 def SetProtobufMessageField(self, group_message, field, field_value):
297 '''Sets a field in a protobuf message.
298
299 Args:
300 group_message: The protobuf message.
301 field: The field of the message to set, it shuold be a member of
302 group_message.DESCRIPTOR.fields.
303 field_value: The value to set.
304 '''
305 if field.label == field.LABEL_REPEATED:
306 assert type(field_value) == list
307 assert field.type == field.TYPE_STRING
308 list_field = group_message.__getattribute__(field.name)
309 for list_item in field_value:
310 list_field.append(list_item)
311 else:
312 # Simple cases:
313 if field.type == field.TYPE_BOOL:
314 assert type(field_value) == bool
315 elif field.type == field.TYPE_STRING:
316 assert type(field_value) == str
317 elif field.type == field.TYPE_INT64:
318 assert type(field_value) == int
319 else:
320 raise Exception('Unknown field type %s' % field.type_name)
321 group_message.__setattr__(field.name, field_value)
322
323 def GatherPolicySettings(self, settings, policies):
324 '''Copies all the policies from a dictionary into a protobuf of type
325 CloudPolicySettings.
326
327 Args:
328 settings: The destination: a CloudPolicySettings protobuf.
329 policies: The source: a dictionary containing policies under keys
330 'recommended' and 'mandatory'.
331 '''
332 for group in settings.DESCRIPTOR.fields:
333 # Create protobuf message for group.
334 group_message = eval('cp.' + group.message_type.name + '()')
335 # We assume that this policy group will be recommended, and only switch
336 # it to mandatory if at least one of its members is mandatory.
337 group_message.policy_options.mode = cp.PolicyOptions.RECOMMENDED
338 # Indicates if at least one field was set in |group_message|.
339 got_fields = False
340 # Iterate over fields of the message and feed them from the
341 # policy config file.
342 for field in group_message.DESCRIPTOR.fields:
343 field_value = None
344 if field.name in policies['mandatory']:
345 group_message.policy_options.mode = cp.PolicyOptions.MANDATORY
346 field_value = policies['mandatory'][field.name]
347 elif field.name in policies['recommended']:
348 field_value = policies['recommended'][field.name]
349 if field_value != None:
350 got_fields = True
351 self.SetProtobufMessageField(group_message, field, field_value)
352 if got_fields:
353 settings.__getattribute__(group.name).CopyFrom(group_message)
354
355 def ProcessCloudPolicyRequest(self, msg):
356 """Handles a cloud policy request. (New protocol for policy requests.)
357
358 Checks for authorization, encodes the policy into protobuf representation,
359 signs it and constructs the repsonse.
360
361 Args:
362 msg: The CloudPolicyRequest message received from the client.
363
364 Returns:
365 A tuple of HTTP status code and response data to send to the client.
366 """
367 token, response = self.CheckToken()
368 if not token:
369 return response
370
371 settings = cp.CloudPolicySettings()
372
373 if msg.policy_scope in self._server.policy:
374 # Respond is only given if the scope is specified in the config file.
375 # Normally 'chromeos/device' and 'chromeos/user' should be accepted.
376 self.GatherPolicySettings(settings,
377 self._server.policy[msg.policy_scope])
378
379 # Construct response
380 signed_response = dm.SignedCloudPolicyResponse()
381 signed_response.settings.CopyFrom(settings)
382 signed_response.timestamp = int(time.time())
383 signed_response.request_token = token;
384 signed_response.device_name = self.GetDeviceName()
385
386 cloud_response = dm.CloudPolicyResponse()
387 cloud_response.signed_response = signed_response.SerializeToString()
388 signed_data = cloud_response.signed_response
389 cloud_response.signature = (
390 self._server.private_key.hashAndSign(signed_data).tostring())
391 for certificate in self._server.cert_chain:
392 cloud_response.certificate_chain.append(
393 certificate.writeBytes().tostring())
394
395 response = dm.DeviceManagementResponse()
396 response.error = dm.DeviceManagementResponse.SUCCESS
397 response.cloud_policy_response.CopyFrom(cloud_response)
398
399 self.DumpMessage('Response', response)
400
401 return (200, response.SerializeToString())
402
217 def CheckToken(self): 403 def CheckToken(self):
218 """Helper for checking whether the client supplied a valid DM token. 404 """Helper for checking whether the client supplied a valid DM token.
219 405
220 Extracts the token from the request and passed to the server in order to 406 Extracts the token from the request and passed to the server in order to
221 look up the client. Returns a pair of token and error response. If the token 407 look up the client. Returns a pair of token and error response. If the token
222 is None, the error response is a pair of status code and error message. 408 is None, the error response is a pair of status code and error message.
223 409
224 Returns: 410 Returns:
225 A pair of DM token and error response. If the token is None, the message 411 A pair of DM token and error response. If the token is None, the message
226 will contain the error response to send back. 412 will contain the error response to send back.
(...skipping 20 matching lines...) Expand all
247 433
248 return (None, (200, response.SerializeToString())) 434 return (None, (200, response.SerializeToString()))
249 435
250 def DumpMessage(self, label, msg): 436 def DumpMessage(self, label, msg):
251 """Helper for logging an ASCII dump of a protobuf message.""" 437 """Helper for logging an ASCII dump of a protobuf message."""
252 logging.debug('%s\n%s' % (label, str(msg))) 438 logging.debug('%s\n%s' % (label, str(msg)))
253 439
254 class TestServer(object): 440 class TestServer(object):
255 """Handles requests and keeps global service state.""" 441 """Handles requests and keeps global service state."""
256 442
257 def __init__(self, policy_path): 443 def __init__(self, policy_path, policy_cert_chain):
258 """Initializes the server. 444 """Initializes the server.
259 445
260 Args: 446 Args:
261 policy_path: Names the file to read JSON-formatted policy from. 447 policy_path: Names the file to read JSON-formatted policy from.
448 policy_cert_chain: List of paths to X.509 certificate files of the
449 certificate chain used for signing responses.
262 """ 450 """
263 self._registered_devices = {} 451 self._registered_devices = {}
264 self.policy = {} 452 self.policy = {}
265 if json is None: 453 if json is None:
266 print 'No JSON module, cannot parse policy information' 454 print 'No JSON module, cannot parse policy information'
267 else : 455 else :
268 try: 456 try:
269 self.policy = json.loads(open(policy_path).read()) 457 self.policy = json.loads(open(policy_path).read())
270 except IOError: 458 except IOError:
271 print 'Failed to load policy from %s' % policy_path 459 print 'Failed to load policy from %s' % policy_path
272 460
461 self.private_key = None
462 self.cert_chain = []
463 for cert_path in policy_cert_chain:
464 try:
465 cert_text = open(cert_path).read()
466 except IOError:
467 print 'Failed to load certificate from %s' % cert_path
468 certificate = tlslite.api.X509()
469 certificate.parse(cert_text)
470 self.cert_chain.append(certificate)
471 if self.private_key is None:
472 self.private_key = tlslite.api.parsePEMKey(cert_text, private=True)
473 assert self.private_key != None
474
273 def HandleRequest(self, path, headers, request): 475 def HandleRequest(self, path, headers, request):
274 """Handles a request. 476 """Handles a request.
275 477
276 Args: 478 Args:
277 path: The request path and query parameters received from the client. 479 path: The request path and query parameters received from the client.
278 headers: A rfc822.Message-like object containing HTTP headers. 480 headers: A rfc822.Message-like object containing HTTP headers.
279 request: The request data received from the client as a string. 481 request: The request data received from the client as a string.
280 Returns: 482 Returns:
281 A pair of HTTP status code and response data to send to the client. 483 A pair of HTTP status code and response data to send to the client.
282 """ 484 """
(...skipping 28 matching lines...) Expand all
311 return self._registered_devices.get(dmtoken, None) 513 return self._registered_devices.get(dmtoken, None)
312 514
313 def UnregisterDevice(self, dmtoken): 515 def UnregisterDevice(self, dmtoken):
314 """Unregisters a device identified by the given DM token. 516 """Unregisters a device identified by the given DM token.
315 517
316 Args: 518 Args:
317 dmtoken: The device management token provided by the client. 519 dmtoken: The device management token provided by the client.
318 """ 520 """
319 if dmtoken in self._registered_devices: 521 if dmtoken in self._registered_devices:
320 del self._registered_devices[dmtoken] 522 del self._registered_devices[dmtoken]
OLDNEW
« no previous file with comments | « net/test/test_server.cc ('k') | net/tools/testserver/testserver.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698