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

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: serialized list protos -> repeated fields Created 9 years, 11 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
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 should
11 format, using the top-level dictionary as a key/value store. The format is 11 contain a dictionary with three top-level keys. Keys "enforced" and
12 identical to what the Linux implementation reads from /etc. Here is an example: 12 "recommended" hold sub-dictionaries with policy definitions in JSON
13 format as key/value stores. Their format is identical to what the Linux
14 implementation reads from /etc. "managed_users" holds a list of auth tokens for
15 which the server will claim that the user is managed. The token string
16 "*" indicates that all users are claimed to be managed. Here is an example:
13 17
14 { 18 {
15 "HomepageLocation" : "http://www.chromium.org" 19 "enforced": {
20 "HomepageLocation" : "http://www.chromium.org"
21 },
22 "recommended": {
23 "JavascriptEnabled": false,
24 },
25 "managed_users": [
26 "secret123456"
27 ]
16 } 28 }
17 29
30
18 """ 31 """
19 32
33 import calendar
20 import cgi 34 import cgi
21 import logging 35 import logging
36 import os
22 import random 37 import random
23 import re 38 import re
24 import sys 39 import sys
40 import time
41 import tlslite
42 import tlslite.api
25 43
26 # The name and availability of the json module varies in python versions. 44 # The name and availability of the json module varies in python versions.
27 try: 45 try:
28 import simplejson as json 46 import simplejson as json
29 except ImportError: 47 except ImportError:
30 try: 48 try:
31 import json 49 import json
32 except ImportError: 50 except ImportError:
33 json = None 51 json = None
34 52
35 import device_management_backend_pb2 as dm 53 import device_management_backend_pb2 as dm
54 import cloud_policy_pb2 as cp
55
36 56
37 class RequestHandler(object): 57 class RequestHandler(object):
38 """Decodes and handles device management requests from clients. 58 """Decodes and handles device management requests from clients.
39 59
40 The handler implements all the request parsing and protobuf message decoding 60 The handler implements all the request parsing and protobuf message decoding
41 and encoding. It calls back into the server to lookup, register, and 61 and encoding. It calls back into the server to lookup, register, and
42 unregister clients. 62 unregister clients.
43 """ 63 """
44 64
45 def __init__(self, server, path, headers, request): 65 def __init__(self, server, path, headers, request):
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
88 108
89 self.DumpMessage('Request', rmsg) 109 self.DumpMessage('Request', rmsg)
90 110
91 request_type = self.GetUniqueParam('request') 111 request_type = self.GetUniqueParam('request')
92 if request_type == 'register': 112 if request_type == 'register':
93 return self.ProcessRegister(rmsg.register_request) 113 return self.ProcessRegister(rmsg.register_request)
94 elif request_type == 'unregister': 114 elif request_type == 'unregister':
95 return self.ProcessUnregister(rmsg.unregister_request) 115 return self.ProcessUnregister(rmsg.unregister_request)
96 elif request_type == 'policy': 116 elif request_type == 'policy':
97 return self.ProcessPolicy(rmsg.policy_request) 117 return self.ProcessPolicy(rmsg.policy_request)
118 elif request_type == 'cloud_policy':
119 return self.ProcessCloudPolicyRequest(rmsg.cloud_policy_request)
120 elif request_type == 'managed_check':
121 return self.ProcessManagedCheck(rmsg.managed_check_request)
98 else: 122 else:
99 return (400, 'Invalid request parameter') 123 return (400, 'Invalid request parameter')
100 124
125 def CheckGoogleLogin(self):
126 """Extracts the GoogleLogin auth token from the HTTP request, and
127 returns it. Returns None if the token is not present.
128 """
129 match = re.match('GoogleLogin auth=(\\w+)',
130 self._headers.getheader('Authorization', ''))
131 if not match:
132 return None
133 return match.group(1)
134
135 def GetDeviceName(self):
136 """Returns the name for the currently authenticated device based on its
137 device id.
138 """
139 return 'chromeos-' + self.GetUniqueParam('deviceid')
140
101 def ProcessRegister(self, msg): 141 def ProcessRegister(self, msg):
102 """Handles a register request. 142 """Handles a register request.
103 143
104 Checks the query for authorization and device identifier, registers the 144 Checks the query for authorization and device identifier, registers the
105 device with the server and constructs a response. 145 device with the server and constructs a response.
106 146
107 Args: 147 Args:
108 msg: The DeviceRegisterRequest message received from the client. 148 msg: The DeviceRegisterRequest message received from the client.
109 149
110 Returns: 150 Returns:
111 A tuple of HTTP status code and response data to send to the client. 151 A tuple of HTTP status code and response data to send to the client.
112 """ 152 """
113 # Check the auth token and device ID. 153 # Check the auth token and device ID.
114 match = re.match('GoogleLogin auth=(\\w+)', 154 match = re.match('GoogleLogin auth=(\\w+)',
115 self._headers.getheader('Authorization', '')) 155 self._headers.getheader('Authorization', ''))
Mattias Nissler (ping if slow) 2011/01/28 10:29:34 Don't need that regex check any longer, no?
gfeher 2011/01/28 13:42:10 Done.
116 if not match: 156 if not self.CheckGoogleLogin():
117 return (403, 'No authorization') 157 return (403, 'No authorization')
118 auth_token = match.group(1)
119 158
120 device_id = self.GetUniqueParam('deviceid') 159 device_id = self.GetUniqueParam('deviceid')
121 if not device_id: 160 if not device_id:
122 return (400, 'Missing device identifier') 161 return (400, 'Missing device identifier')
123 162
124 # Register the device and create a token. 163 # Register the device and create a token.
125 dmtoken = self._server.RegisterDevice(device_id) 164 dmtoken = self._server.RegisterDevice(device_id)
126 165
127 # Send back the reply. 166 # Send back the reply.
128 response = dm.DeviceManagementResponse() 167 response = dm.DeviceManagementResponse()
129 response.error = dm.DeviceManagementResponse.SUCCESS 168 response.error = dm.DeviceManagementResponse.SUCCESS
130 response.register_response.device_management_token = dmtoken 169 response.register_response.device_management_token = dmtoken
170 response.register_response.device_name = self.GetDeviceName()
131 171
132 self.DumpMessage('Response', response) 172 self.DumpMessage('Response', response)
133 173
134 return (200, response.SerializeToString()) 174 return (200, response.SerializeToString())
135 175
136 def ProcessUnregister(self, msg): 176 def ProcessUnregister(self, msg):
137 """Handles a register request. 177 """Handles a register request.
138 178
139 Checks for authorization, unregisters the device and constructs the 179 Checks for authorization, unregisters the device and constructs the
140 response. 180 response.
(...skipping 14 matching lines...) Expand all
155 195
156 # Prepare and send the response. 196 # Prepare and send the response.
157 response = dm.DeviceManagementResponse() 197 response = dm.DeviceManagementResponse()
158 response.error = dm.DeviceManagementResponse.SUCCESS 198 response.error = dm.DeviceManagementResponse.SUCCESS
159 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse()) 199 response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
160 200
161 self.DumpMessage('Response', response) 201 self.DumpMessage('Response', response)
162 202
163 return (200, response.SerializeToString()) 203 return (200, response.SerializeToString())
164 204
205 def ProcessManagedCheck(self, msg):
206 """Handles a 'managed check' request.
207
208 Queries the list of managed users and responds the client if their user
209 is managed or not.
210
211 Args:
212 msg: The ManagedCheckRequest message received from the client.
213
214 Returns:
215 A tuple of HTTP status code and response data to send to the client.
216 """
217 # Check the management token.
218 auth = self.CheckGoogleLogin()
219 if not auth:
220 return (403, 'No authorization')
221
222 managed_check_response = dm.ManagedCheckResponse()
223 if ('*' in self._server.policy['managed_users'] or
224 auth in self._server.policy['managed_users']):
225 managed_check_response.mode = dm.ManagedCheckResponse.MANAGED;
226 else:
227 managed_check_response.mode = dm.ManagedCheckResponse.UNMANAGED;
228
229 # Prepare and send the response.
230 response = dm.DeviceManagementResponse()
231 response.error = dm.DeviceManagementResponse.SUCCESS
232 response.managed_check_response.CopyFrom(managed_check_response)
233
234 self.DumpMessage('Response', response)
235
236 return (200, response.SerializeToString())
237
165 def ProcessPolicy(self, msg): 238 def ProcessPolicy(self, msg):
166 """Handles a policy request. 239 """Handles a policy request.
167 240
168 Checks for authorization, encodes the policy into protobuf representation 241 Checks for authorization, encodes the policy into protobuf representation
169 and constructs the repsonse. 242 and constructs the repsonse.
170 243
171 Args: 244 Args:
172 msg: The DevicePolicyRequest message received from the client. 245 msg: The DevicePolicyRequest message received from the client.
173 246
174 Returns: 247 Returns:
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
207 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY 280 entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY
208 for list_entry in value: 281 for list_entry in value:
209 entry_value.string_array.append(str(list_entry)) 282 entry_value.string_array.append(str(list_entry))
210 entry.value.CopyFrom(entry_value) 283 entry.value.CopyFrom(entry_value)
211 setting.policy_value.CopyFrom(policy_value) 284 setting.policy_value.CopyFrom(policy_value)
212 285
213 self.DumpMessage('Response', response) 286 self.DumpMessage('Response', response)
214 287
215 return (200, response.SerializeToString()) 288 return (200, response.SerializeToString())
216 289
290 def SetProtobufMessageField(self, group_message, field, field_value):
291 '''Sets a field in a protobuf message.
292
293 Args:
294 group_message: The protobuf message.
295 field: The field of the message to set, it shuold be a member of
296 group_message.DESCRIPTOR.fields.
297 field_value: The value to set.
298 '''
299 if field.type == field.TYPE_STRING and field.label == field.LABEL_REPEATED:
300 assert type(field_value) == list
301 list_field = group_message.__getattribute__(field.name)
302 for list_item in field_value:
303 list_field.append(list_item)
304 else:
305 # Simple cases:
306 if field.type == field.TYPE_BOOL:
307 assert type(field_value) == bool
308 elif field.type == field.TYPE_STRING:
309 assert type(field_value) == str
310 elif field.type == field.TYPE_INT64:
311 assert type(field_value) == int
312 else:
313 raise Exception('Unknown field type %s' % field.type_name)
314 group_message.__setattr__(field.name, field_value)
Mattias Nissler (ping if slow) 2011/01/28 10:29:34 Could this also handle string lists? In other word
gfeher 2011/01/28 13:42:10 I get this error when I try to use __setattr__ to
315
316 def ProcessCloudPolicyRequest(self, msg):
317 """Handles a cloud policy request. (New protocol for policy requests.)
318
319 Checks for authorization, encodes the policy into protobuf representation,
320 signs it and constructs the repsonse.
Jakob Kummerow 2011/01/28 10:45:52 nit: response
gfeher 2011/01/28 13:42:10 Done.
321
322 Args:
323 msg: The CloudPolicyRequest message received from the client.
324
325 Returns:
326 A tuple of HTTP status code and response data to send to the client.
327 """
328 token, response = self.CheckToken()
329 if not token:
330 return response
331
332 settings = cp.CloudPolicySettings()
333
334 if msg.policy_scope == 'chromeos/device':
335 pass
Mattias Nissler (ping if slow) 2011/01/28 10:29:34 What's this for?
gfeher 2011/01/28 13:42:10 Forgotten stuff -> I'll add support for device and
336
337 for group in settings.DESCRIPTOR.fields:
338 # Create protobuf message for group.
339 group_message = eval('cp.' + group.message_type.name + '()')
340 # Indiactes if at least one field was set in |group_message|.
Jakob Kummerow 2011/01/28 10:45:52 nit: Indicates
gfeher 2011/01/28 13:42:10 Done.
341 got_fields = False
342 # Indicates if the current group is recommended (and not enforced.)
343 # A group will be considered recommended if all of its present members
344 # are set to recommended in the policy config file.
345 recommended = True
Mattias Nissler (ping if slow) 2011/01/28 10:29:34 can't you just use a PolicyMode enum directly?
gfeher 2011/01/28 13:42:10 Done.
346 # Iterate over fields of the message and feed them from the
347 # policy config file.
348 for field in group_message.DESCRIPTOR.fields:
349 field_value = None
350 if field.name in self._server.policy['enforced']:
351 got_fields = True
352 recommended = False
353 field_value = self._server.policy['enforced'][field.name]
354 elif field.name in self._server.policy['recommended']:
355 got_fields = True
356 field_value = self._server.policy['recommended'][field.name]
357 if field_value != None:
358 self.SetProtobufMessageField(group_message, field, field_value)
359 if got_fields:
360 if recommended:
361 group_message.policy_mode = cp.RECOMMENDED
362 settings.__getattribute__(group.name).CopyFrom(group_message)
363
364 # Construct response
365 signed_response = dm.SignedCloudPolicyResponse()
366 signed_response.settings.CopyFrom(settings)
367 signed_response.timestamp = calendar.timegm(time.gmtime())
Jakob Kummerow 2011/01/28 10:45:52 you can remove 'import calendar' and just use 'sig
gfeher 2011/01/28 13:42:10 Done.
368 signed_response.request_token = token;
369 signed_response.device_name = self.GetDeviceName()
370
371 cloud_response = dm.CloudPolicyResponse()
372 cloud_response.signed_response = signed_response.SerializeToString()
373 signed_data = cloud_response.signed_response
374 cloud_response.signature = (
375 self._server.private_key.hashAndSign(signed_data).tostring())
376 for certificate in self._server.cert_chain:
377 cloud_response.certificate_chain.append(
378 certificate.writeBytes().tostring())
379
380 response = dm.DeviceManagementResponse()
381 response.error = dm.DeviceManagementResponse.SUCCESS
382 response.cloud_policy_response.CopyFrom(cloud_response)
383
384 self.DumpMessage('Response', response)
385
386 return (200, response.SerializeToString())
387
217 def CheckToken(self): 388 def CheckToken(self):
218 """Helper for checking whether the client supplied a valid DM token. 389 """Helper for checking whether the client supplied a valid DM token.
219 390
220 Extracts the token from the request and passed to the server in order to 391 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 392 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. 393 is None, the error response is a pair of status code and error message.
223 394
224 Returns: 395 Returns:
225 A pair of DM token and error response. If the token is None, the message 396 A pair of DM token and error response. If the token is None, the message
226 will contain the error response to send back. 397 will contain the error response to send back.
(...skipping 20 matching lines...) Expand all
247 418
248 return (None, (200, response.SerializeToString())) 419 return (None, (200, response.SerializeToString()))
249 420
250 def DumpMessage(self, label, msg): 421 def DumpMessage(self, label, msg):
251 """Helper for logging an ASCII dump of a protobuf message.""" 422 """Helper for logging an ASCII dump of a protobuf message."""
252 logging.debug('%s\n%s' % (label, str(msg))) 423 logging.debug('%s\n%s' % (label, str(msg)))
253 424
254 class TestServer(object): 425 class TestServer(object):
255 """Handles requests and keeps global service state.""" 426 """Handles requests and keeps global service state."""
256 427
257 def __init__(self, policy_path): 428 def __init__(self, policy_path, policy_cert_chain):
258 """Initializes the server. 429 """Initializes the server.
259 430
260 Args: 431 Args:
261 policy_path: Names the file to read JSON-formatted policy from. 432 policy_path: Names the file to read JSON-formatted policy from.
433 policy_cert_chain: List of paths to X.509 certificate files of the
434 certificate chain used for signing responses.
262 """ 435 """
263 self._registered_devices = {} 436 self._registered_devices = {}
264 self.policy = {} 437 self.policy = {}
265 if json is None: 438 if json is None:
266 print 'No JSON module, cannot parse policy information' 439 print 'No JSON module, cannot parse policy information'
267 else : 440 else :
268 try: 441 try:
269 self.policy = json.loads(open(policy_path).read()) 442 self.policy = json.loads(open(policy_path).read())
270 except IOError: 443 except IOError:
271 print 'Failed to load policy from %s' % policy_path 444 print 'Failed to load policy from %s' % policy_path
272 445
446 self.cert_chain = []
447 for cert_path in policy_cert_chain:
448 try:
449 last_cert = open(cert_path).read()
450 except IOError:
451 print 'Failed to load certificate from %s' % cert_path
452 certificate = tlslite.api.X509()
453 certificate.parse(last_cert)
454 self.cert_chain.append(certificate)
455 self.private_key = tlslite.api.parsePEMKey(last_cert, private=True)
456
273 def HandleRequest(self, path, headers, request): 457 def HandleRequest(self, path, headers, request):
274 """Handles a request. 458 """Handles a request.
275 459
276 Args: 460 Args:
277 path: The request path and query parameters received from the client. 461 path: The request path and query parameters received from the client.
278 headers: A rfc822.Message-like object containing HTTP headers. 462 headers: A rfc822.Message-like object containing HTTP headers.
279 request: The request data received from the client as a string. 463 request: The request data received from the client as a string.
280 Returns: 464 Returns:
281 A pair of HTTP status code and response data to send to the client. 465 A pair of HTTP status code and response data to send to the client.
282 """ 466 """
(...skipping 28 matching lines...) Expand all
311 return self._registered_devices.get(dmtoken, None) 495 return self._registered_devices.get(dmtoken, None)
312 496
313 def UnregisterDevice(self, dmtoken): 497 def UnregisterDevice(self, dmtoken):
314 """Unregisters a device identified by the given DM token. 498 """Unregisters a device identified by the given DM token.
315 499
316 Args: 500 Args:
317 dmtoken: The device management token provided by the client. 501 dmtoken: The device management token provided by the client.
318 """ 502 """
319 if dmtoken in self._registered_devices: 503 if dmtoken in self._registered_devices:
320 del self._registered_devices[dmtoken] 504 del self._registered_devices[dmtoken]
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698