| Index: net/tools/testserver/device_management.py
|
| diff --git a/net/tools/testserver/device_management.py b/net/tools/testserver/device_management.py
|
| index d71522741a950dcc2cf2eab2ca6373a7bf10fd52..183451bb515bbc1bbe1a79437f3324dd5249d88f 100644
|
| --- a/net/tools/testserver/device_management.py
|
| +++ b/net/tools/testserver/device_management.py
|
| @@ -1,5 +1,5 @@
|
| #!/usr/bin/python2.5
|
| -# Copyright (c) 2010 The Chromium Authors. All rights reserved.
|
| +# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| # Use of this source code is governed by a BSD-style license that can be
|
| # found in the LICENSE file.
|
|
|
| @@ -7,21 +7,47 @@
|
|
|
| This implements a simple cloud policy test server that can be used to test
|
| chrome's device management service client. The policy information is read from
|
| -from files in a directory. The files should contain policy definitions in JSON
|
| -format, using the top-level dictionary as a key/value store. The format is
|
| -identical to what the Linux implementation reads from /etc. Here is an example:
|
| +the file named device_management in the server's data directory. It contains
|
| +enforced and recommended policies for the device and user scope, and a list
|
| +of managed users.
|
| +
|
| +The format of the file is JSON. The root dictionary contains a list under the
|
| +key "managed_users". It contains auth tokens for which the server will claim
|
| +that the user is managed. The token string "*" indicates that all users are
|
| +claimed to be managed. Other keys in the root dictionary identify request
|
| +scopes. Each request scope is described by a dictionary that holds two
|
| +sub-dictionaries: "mandatory" and "recommended". Both these hold the policy
|
| +definitions as key/value stores, their format is identical to what the Linux
|
| +implementation reads from /etc.
|
| +
|
| +Example:
|
|
|
| {
|
| - "HomepageLocation" : "http://www.chromium.org"
|
| + "chromeos/device": {
|
| + "mandatory": {
|
| + "HomepageLocation" : "http://www.chromium.org"
|
| + },
|
| + "recommended": {
|
| + "JavascriptEnabled": false,
|
| + },
|
| + },
|
| + "managed_users": [
|
| + "secret123456"
|
| + ]
|
| }
|
|
|
| +
|
| """
|
|
|
| import cgi
|
| import logging
|
| +import os
|
| import random
|
| import re
|
| import sys
|
| +import time
|
| +import tlslite
|
| +import tlslite.api
|
|
|
| # The name and availability of the json module varies in python versions.
|
| try:
|
| @@ -33,6 +59,8 @@ except ImportError:
|
| json = None
|
|
|
| import device_management_backend_pb2 as dm
|
| +import cloud_policy_pb2 as cp
|
| +
|
|
|
| class RequestHandler(object):
|
| """Decodes and handles device management requests from clients.
|
| @@ -95,9 +123,29 @@ class RequestHandler(object):
|
| return self.ProcessUnregister(rmsg.unregister_request)
|
| elif request_type == 'policy':
|
| return self.ProcessPolicy(rmsg.policy_request)
|
| + elif request_type == 'cloud_policy':
|
| + return self.ProcessCloudPolicyRequest(rmsg.cloud_policy_request)
|
| + elif request_type == 'managed_check':
|
| + return self.ProcessManagedCheck(rmsg.managed_check_request)
|
| else:
|
| return (400, 'Invalid request parameter')
|
|
|
| + def CheckGoogleLogin(self):
|
| + """Extracts the GoogleLogin auth token from the HTTP request, and
|
| + returns it. Returns None if the token is not present.
|
| + """
|
| + match = re.match('GoogleLogin auth=(\\w+)',
|
| + self._headers.getheader('Authorization', ''))
|
| + if not match:
|
| + return None
|
| + return match.group(1)
|
| +
|
| + def GetDeviceName(self):
|
| + """Returns the name for the currently authenticated device based on its
|
| + device id.
|
| + """
|
| + return 'chromeos-' + self.GetUniqueParam('deviceid')
|
| +
|
| def ProcessRegister(self, msg):
|
| """Handles a register request.
|
|
|
| @@ -111,11 +159,8 @@ class RequestHandler(object):
|
| A tuple of HTTP status code and response data to send to the client.
|
| """
|
| # Check the auth token and device ID.
|
| - match = re.match('GoogleLogin auth=(\\w+)',
|
| - self._headers.getheader('Authorization', ''))
|
| - if not match:
|
| + if not self.CheckGoogleLogin():
|
| return (403, 'No authorization')
|
| - auth_token = match.group(1)
|
|
|
| device_id = self.GetUniqueParam('deviceid')
|
| if not device_id:
|
| @@ -128,6 +173,7 @@ class RequestHandler(object):
|
| response = dm.DeviceManagementResponse()
|
| response.error = dm.DeviceManagementResponse.SUCCESS
|
| response.register_response.device_management_token = dmtoken
|
| + response.register_response.device_name = self.GetDeviceName()
|
|
|
| self.DumpMessage('Response', response)
|
|
|
| @@ -162,11 +208,44 @@ class RequestHandler(object):
|
|
|
| return (200, response.SerializeToString())
|
|
|
| + def ProcessManagedCheck(self, msg):
|
| + """Handles a 'managed check' request.
|
| +
|
| + Queries the list of managed users and responds the client if their user
|
| + is managed or not.
|
| +
|
| + Args:
|
| + msg: The ManagedCheckRequest message received from the client.
|
| +
|
| + Returns:
|
| + A tuple of HTTP status code and response data to send to the client.
|
| + """
|
| + # Check the management token.
|
| + auth = self.CheckGoogleLogin()
|
| + if not auth:
|
| + return (403, 'No authorization')
|
| +
|
| + managed_check_response = dm.ManagedCheckResponse()
|
| + if ('*' in self._server.policy['managed_users'] or
|
| + auth in self._server.policy['managed_users']):
|
| + managed_check_response.mode = dm.ManagedCheckResponse.MANAGED;
|
| + else:
|
| + managed_check_response.mode = dm.ManagedCheckResponse.UNMANAGED;
|
| +
|
| + # Prepare and send the response.
|
| + response = dm.DeviceManagementResponse()
|
| + response.error = dm.DeviceManagementResponse.SUCCESS
|
| + response.managed_check_response.CopyFrom(managed_check_response)
|
| +
|
| + self.DumpMessage('Response', response)
|
| +
|
| + return (200, response.SerializeToString())
|
| +
|
| def ProcessPolicy(self, msg):
|
| """Handles a policy request.
|
|
|
| Checks for authorization, encodes the policy into protobuf representation
|
| - and constructs the repsonse.
|
| + and constructs the response.
|
|
|
| Args:
|
| msg: The DevicePolicyRequest message received from the client.
|
| @@ -214,6 +293,113 @@ class RequestHandler(object):
|
|
|
| return (200, response.SerializeToString())
|
|
|
| + def SetProtobufMessageField(self, group_message, field, field_value):
|
| + '''Sets a field in a protobuf message.
|
| +
|
| + Args:
|
| + group_message: The protobuf message.
|
| + field: The field of the message to set, it shuold be a member of
|
| + group_message.DESCRIPTOR.fields.
|
| + field_value: The value to set.
|
| + '''
|
| + if field.label == field.LABEL_REPEATED:
|
| + assert type(field_value) == list
|
| + assert field.type == field.TYPE_STRING
|
| + list_field = group_message.__getattribute__(field.name)
|
| + for list_item in field_value:
|
| + list_field.append(list_item)
|
| + else:
|
| + # Simple cases:
|
| + if field.type == field.TYPE_BOOL:
|
| + assert type(field_value) == bool
|
| + elif field.type == field.TYPE_STRING:
|
| + assert type(field_value) == str
|
| + elif field.type == field.TYPE_INT64:
|
| + assert type(field_value) == int
|
| + else:
|
| + raise Exception('Unknown field type %s' % field.type_name)
|
| + group_message.__setattr__(field.name, field_value)
|
| +
|
| + def GatherPolicySettings(self, settings, policies):
|
| + '''Copies all the policies from a dictionary into a protobuf of type
|
| + CloudPolicySettings.
|
| +
|
| + Args:
|
| + settings: The destination: a CloudPolicySettings protobuf.
|
| + policies: The source: a dictionary containing policies under keys
|
| + 'recommended' and 'mandatory'.
|
| + '''
|
| + for group in settings.DESCRIPTOR.fields:
|
| + # Create protobuf message for group.
|
| + group_message = eval('cp.' + group.message_type.name + '()')
|
| + # We assume that this policy group will be recommended, and only switch
|
| + # it to mandatory if at least one of its members is mandatory.
|
| + group_message.policy_options.mode = cp.PolicyOptions.RECOMMENDED
|
| + # Indicates if at least one field was set in |group_message|.
|
| + got_fields = False
|
| + # Iterate over fields of the message and feed them from the
|
| + # policy config file.
|
| + for field in group_message.DESCRIPTOR.fields:
|
| + field_value = None
|
| + if field.name in policies['mandatory']:
|
| + group_message.policy_options.mode = cp.PolicyOptions.MANDATORY
|
| + field_value = policies['mandatory'][field.name]
|
| + elif field.name in policies['recommended']:
|
| + field_value = policies['recommended'][field.name]
|
| + if field_value != None:
|
| + got_fields = True
|
| + self.SetProtobufMessageField(group_message, field, field_value)
|
| + if got_fields:
|
| + settings.__getattribute__(group.name).CopyFrom(group_message)
|
| +
|
| + def ProcessCloudPolicyRequest(self, msg):
|
| + """Handles a cloud policy request. (New protocol for policy requests.)
|
| +
|
| + Checks for authorization, encodes the policy into protobuf representation,
|
| + signs it and constructs the repsonse.
|
| +
|
| + Args:
|
| + msg: The CloudPolicyRequest message received from the client.
|
| +
|
| + Returns:
|
| + A tuple of HTTP status code and response data to send to the client.
|
| + """
|
| + token, response = self.CheckToken()
|
| + if not token:
|
| + return response
|
| +
|
| + settings = cp.CloudPolicySettings()
|
| +
|
| + if msg.policy_scope in self._server.policy:
|
| + # Respond is only given if the scope is specified in the config file.
|
| + # Normally 'chromeos/device' and 'chromeos/user' should be accepted.
|
| + self.GatherPolicySettings(settings,
|
| + self._server.policy[msg.policy_scope])
|
| +
|
| + # Construct response
|
| + signed_response = dm.SignedCloudPolicyResponse()
|
| + signed_response.settings.CopyFrom(settings)
|
| + signed_response.timestamp = int(time.time())
|
| + signed_response.request_token = token;
|
| + signed_response.device_name = self.GetDeviceName()
|
| +
|
| + cloud_response = dm.CloudPolicyResponse()
|
| + cloud_response.signed_response = signed_response.SerializeToString()
|
| + signed_data = cloud_response.signed_response
|
| + cloud_response.signature = (
|
| + self._server.private_key.hashAndSign(signed_data).tostring())
|
| + for certificate in self._server.cert_chain:
|
| + cloud_response.certificate_chain.append(
|
| + certificate.writeBytes().tostring())
|
| +
|
| + response = dm.DeviceManagementResponse()
|
| + response.error = dm.DeviceManagementResponse.SUCCESS
|
| + response.cloud_policy_response.CopyFrom(cloud_response)
|
| +
|
| + self.DumpMessage('Response', response)
|
| +
|
| + return (200, response.SerializeToString())
|
| +
|
| def CheckToken(self):
|
| """Helper for checking whether the client supplied a valid DM token.
|
|
|
| @@ -254,11 +440,13 @@ class RequestHandler(object):
|
| class TestServer(object):
|
| """Handles requests and keeps global service state."""
|
|
|
| - def __init__(self, policy_path):
|
| + def __init__(self, policy_path, policy_cert_chain):
|
| """Initializes the server.
|
|
|
| Args:
|
| policy_path: Names the file to read JSON-formatted policy from.
|
| + policy_cert_chain: List of paths to X.509 certificate files of the
|
| + certificate chain used for signing responses.
|
| """
|
| self._registered_devices = {}
|
| self.policy = {}
|
| @@ -270,6 +458,20 @@ class TestServer(object):
|
| except IOError:
|
| print 'Failed to load policy from %s' % policy_path
|
|
|
| + self.private_key = None
|
| + self.cert_chain = []
|
| + for cert_path in policy_cert_chain:
|
| + try:
|
| + cert_text = open(cert_path).read()
|
| + except IOError:
|
| + print 'Failed to load certificate from %s' % cert_path
|
| + certificate = tlslite.api.X509()
|
| + certificate.parse(cert_text)
|
| + self.cert_chain.append(certificate)
|
| + if self.private_key is None:
|
| + self.private_key = tlslite.api.parsePEMKey(cert_text, private=True)
|
| + assert self.private_key != None
|
| +
|
| def HandleRequest(self, path, headers, request):
|
| """Handles a request.
|
|
|
|
|