device_management.py revision 201ade2fbba22bfb27ae029f4d23fca6ded109a0
1#!/usr/bin/python2.5
2# Copyright (c) 2010 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A bare-bones test server for testing cloud policy support.
7
8This implements a simple cloud policy test server that can be used to test
9chrome's device management service client. The policy information is read from
10from files in a directory. The files should contain policy definitions in JSON
11format, using the top-level dictionary as a key/value store. The format is
12identical to what the Linux implementation reads from /etc. Here is an example:
13
14{
15  "HomepageLocation" : "http://www.chromium.org"
16}
17
18"""
19
20import cgi
21import logging
22import random
23import re
24import sys
25
26# The name and availability of the json module varies in python versions.
27try:
28  import simplejson as json
29except ImportError:
30  try:
31    import json
32  except ImportError:
33    json = None
34
35import device_management_backend_pb2 as dm
36
37class RequestHandler(object):
38  """Decodes and handles device management requests from clients.
39
40  The handler implements all the request parsing and protobuf message decoding
41  and encoding. It calls back into the server to lookup, register, and
42  unregister clients.
43  """
44
45  def __init__(self, server, path, headers, request):
46    """Initialize the handler.
47
48    Args:
49      server: The TestServer object to use for (un)registering clients.
50      path: A string containing the request path and query parameters.
51      headers: A rfc822.Message-like object containing HTTP headers.
52      request: The request data received from the client as a string.
53    """
54    self._server = server
55    self._path = path
56    self._headers = headers
57    self._request = request
58    self._params = None
59
60  def GetUniqueParam(self, name):
61    """Extracts a unique query parameter from the request.
62
63    Args:
64      name: Names the parameter to fetch.
65    Returns:
66      The parameter value or None if the parameter doesn't exist or is not
67      unique.
68    """
69    if not self._params:
70      self._params = cgi.parse_qs(self._path[self._path.find('?')+1:])
71
72    param_list = self._params.get(name, [])
73    if len(param_list) == 1:
74      return param_list[0]
75    return None;
76
77  def HandleRequest(self):
78    """Handles a request.
79
80    Parses the data supplied at construction time and returns a pair indicating
81    http status code and response data to be sent back to the client.
82
83    Returns:
84      A tuple of HTTP status code and response data to send to the client.
85    """
86    rmsg = dm.DeviceManagementRequest()
87    rmsg.ParseFromString(self._request)
88
89    self.DumpMessage('Request', rmsg)
90
91    request_type = self.GetUniqueParam('request')
92    if request_type == 'register':
93      return self.ProcessRegister(rmsg.register_request)
94    elif request_type == 'unregister':
95      return self.ProcessUnregister(rmsg.unregister_request)
96    elif request_type == 'policy':
97      return self.ProcessPolicy(rmsg.policy_request)
98    else:
99      return (400, 'Invalid request parameter')
100
101  def ProcessRegister(self, msg):
102    """Handles a register request.
103
104    Checks the query for authorization and device identifier, registers the
105    device with the server and constructs a response.
106
107    Args:
108      msg: The DeviceRegisterRequest message received from the client.
109
110    Returns:
111      A tuple of HTTP status code and response data to send to the client.
112    """
113    # Check the auth token and device ID.
114    match = re.match('GoogleLogin auth=(\\w+)',
115                     self._headers.getheader('Authorization', ''))
116    if not match:
117      return (403, 'No authorization')
118    auth_token = match.group(1)
119
120    device_id = self.GetUniqueParam('deviceid')
121    if not device_id:
122      return (400, 'Missing device identifier')
123
124    # Register the device and create a token.
125    dmtoken = self._server.RegisterDevice(device_id)
126
127    # Send back the reply.
128    response = dm.DeviceManagementResponse()
129    response.error = dm.DeviceManagementResponse.SUCCESS
130    response.register_response.device_management_token = dmtoken
131
132    self.DumpMessage('Response', response)
133
134    return (200, response.SerializeToString())
135
136  def ProcessUnregister(self, msg):
137    """Handles a register request.
138
139    Checks for authorization, unregisters the device and constructs the
140    response.
141
142    Args:
143      msg: The DeviceUnregisterRequest message received from the client.
144
145    Returns:
146      A tuple of HTTP status code and response data to send to the client.
147    """
148    # Check the management token.
149    token, response = self.CheckToken();
150    if not token:
151      return response
152
153    # Unregister the device.
154    self._server.UnregisterDevice(token);
155
156    # Prepare and send the response.
157    response = dm.DeviceManagementResponse()
158    response.error = dm.DeviceManagementResponse.SUCCESS
159    response.unregister_response.CopyFrom(dm.DeviceUnregisterResponse())
160
161    self.DumpMessage('Response', response)
162
163    return (200, response.SerializeToString())
164
165  def ProcessPolicy(self, msg):
166    """Handles a policy request.
167
168    Checks for authorization, encodes the policy into protobuf representation
169    and constructs the repsonse.
170
171    Args:
172      msg: The DevicePolicyRequest message received from the client.
173
174    Returns:
175      A tuple of HTTP status code and response data to send to the client.
176    """
177    # Check the management token.
178    token, response = self.CheckToken()
179    if not token:
180      return response
181
182    # Stuff the policy dictionary into a response message and send it back.
183    response = dm.DeviceManagementResponse()
184    response.error = dm.DeviceManagementResponse.SUCCESS
185    response.policy_response.CopyFrom(dm.DevicePolicyResponse())
186
187    # Respond only if the client requested policy for the cros/device scope,
188    # since that's where chrome policy is supposed to live in.
189    if msg.policy_scope == 'chromeos/device':
190      setting = response.policy_response.setting.add()
191      setting.policy_key = 'chrome-policy'
192      policy_value = dm.GenericSetting()
193      for (key, value) in self._server.policy.iteritems():
194        entry = policy_value.named_value.add()
195        entry.name = key
196        entry_value = dm.GenericValue()
197        if isinstance(value, bool):
198          entry_value.value_type = dm.GenericValue.VALUE_TYPE_BOOL
199          entry_value.bool_value = value
200        elif isinstance(value, int):
201          entry_value.value_type = dm.GenericValue.VALUE_TYPE_INT64
202          entry_value.int64_value = value
203        elif isinstance(value, str) or isinstance(value, unicode):
204          entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING
205          entry_value.string_value = value
206        elif isinstance(value, list):
207          entry_value.value_type = dm.GenericValue.VALUE_TYPE_STRING_ARRAY
208          for list_entry in value:
209            entry_value.string_array.append(str(list_entry))
210        entry.value.CopyFrom(entry_value)
211      setting.policy_value.CopyFrom(policy_value)
212
213    self.DumpMessage('Response', response)
214
215    return (200, response.SerializeToString())
216
217  def CheckToken(self):
218    """Helper for checking whether the client supplied a valid DM token.
219
220    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
222    is None, the error response is a pair of status code and error message.
223
224    Returns:
225      A pair of DM token and error response. If the token is None, the message
226      will contain the error response to send back.
227    """
228    error = None
229    dmtoken = None
230    request_device_id = self.GetUniqueParam('deviceid')
231    match = re.match('GoogleDMToken token=(\\w+)',
232                     self._headers.getheader('Authorization', ''))
233    if match:
234      dmtoken = match.group(1)
235    if not dmtoken:
236      error = dm.DeviceManagementResponse.DEVICE_MANAGEMENT_TOKEN_INVALID
237    elif (not request_device_id or
238          not self._server.LookupDevice(dmtoken) == request_device_id):
239      error = dm.DeviceManagementResponse.DEVICE_NOT_FOUND
240    else:
241      return (dmtoken, None)
242
243    response = dm.DeviceManagementResponse()
244    response.error = error
245
246    self.DumpMessage('Response', response)
247
248    return (None, (200, response.SerializeToString()))
249
250  def DumpMessage(self, label, msg):
251    """Helper for logging an ASCII dump of a protobuf message."""
252    logging.debug('%s\n%s' % (label, str(msg)))
253
254class TestServer(object):
255  """Handles requests and keeps global service state."""
256
257  def __init__(self, policy_path):
258    """Initializes the server.
259
260    Args:
261      policy_path: Names the file to read JSON-formatted policy from.
262    """
263    self._registered_devices = {}
264    self.policy = {}
265    if json is None:
266      print 'No JSON module, cannot parse policy information'
267    else :
268      try:
269        self.policy = json.loads(open(policy_path).read())
270      except IOError:
271        print 'Failed to load policy from %s' % policy_path
272
273  def HandleRequest(self, path, headers, request):
274    """Handles a request.
275
276    Args:
277      path: The request path and query parameters received from the client.
278      headers: A rfc822.Message-like object containing HTTP headers.
279      request: The request data received from the client as a string.
280    Returns:
281      A pair of HTTP status code and response data to send to the client.
282    """
283    handler = RequestHandler(self, path, headers, request)
284    return handler.HandleRequest()
285
286  def RegisterDevice(self, device_id):
287    """Registers a device and generate a DM token for it.
288
289    Args:
290      device_id: The device identifier provided by the client.
291
292    Returns:
293      The newly generated device token for the device.
294    """
295    dmtoken_chars = []
296    while len(dmtoken_chars) < 32:
297      dmtoken_chars.append(random.choice('0123456789abcdef'))
298    dmtoken= ''.join(dmtoken_chars)
299    self._registered_devices[dmtoken] = device_id
300    return dmtoken
301
302  def LookupDevice(self, dmtoken):
303    """Looks up a device by DMToken.
304
305    Args:
306      dmtoken: The device management token provided by the client.
307
308    Returns:
309      The corresponding device identifier or None if not found.
310    """
311    return self._registered_devices.get(dmtoken, None)
312
313  def UnregisterDevice(self, dmtoken):
314    """Unregisters a device identified by the given DM token.
315
316    Args:
317      dmtoken: The device management token provided by the client.
318    """
319    if dmtoken in self._registered_devices:
320      del self._registered_devices[dmtoken]
321