Chromium Code Reviews| 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..050381fe0c7edd63002efcff329d237c561f08cb 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,39 @@ |
| 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 should |
| +contain a dictionary with three top-level keys. Keys "enforced" and |
| +"recommended" hold sub-dictionaries with policy definitions in JSON |
| +format as key/value stores. Their format is identical to what the Linux |
| +implementation reads from /etc. "managed_users" holds a list of 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. Here is an example: |
| { |
| - "HomepageLocation" : "http://www.chromium.org" |
| + "enforced": { |
| + "HomepageLocation" : "http://www.chromium.org" |
| + }, |
| + "recommended": { |
| + "JavascriptEnabled": false, |
| + }, |
| + "managed_users": [ |
| + "secret123456" |
| + ] |
| } |
| + |
| """ |
| +import calendar |
| 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 +51,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 +115,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. |
| @@ -113,9 +153,8 @@ class RequestHandler(object): |
| # Check the auth token and device ID. |
| match = re.match('GoogleLogin auth=(\\w+)', |
| 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.
|
| - 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 +167,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,6 +202,39 @@ 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. |
| @@ -214,6 +287,104 @@ 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.type == field.TYPE_STRING and field.label == field.LABEL_REPEATED: |
| + assert type(field_value) == list |
| + 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) |
|
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
|
| + |
| + 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. |
|
Jakob Kummerow
2011/01/28 10:45:52
nit: response
gfeher
2011/01/28 13:42:10
Done.
|
| + |
| + 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 == 'chromeos/device': |
| + 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
|
| + |
| + for group in settings.DESCRIPTOR.fields: |
| + # Create protobuf message for group. |
| + group_message = eval('cp.' + group.message_type.name + '()') |
| + # 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.
|
| + got_fields = False |
| + # Indicates if the current group is recommended (and not enforced.) |
| + # A group will be considered recommended if all of its present members |
| + # are set to recommended in the policy config file. |
| + 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.
|
| + # 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 self._server.policy['enforced']: |
| + got_fields = True |
| + recommended = False |
| + field_value = self._server.policy['enforced'][field.name] |
| + elif field.name in self._server.policy['recommended']: |
| + got_fields = True |
| + field_value = self._server.policy['recommended'][field.name] |
| + if field_value != None: |
| + self.SetProtobufMessageField(group_message, field, field_value) |
| + if got_fields: |
| + if recommended: |
| + group_message.policy_mode = cp.RECOMMENDED |
| + settings.__getattribute__(group.name).CopyFrom(group_message) |
| + |
| + # Construct response |
| + signed_response = dm.SignedCloudPolicyResponse() |
| + signed_response.settings.CopyFrom(settings) |
| + 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.
|
| + 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 +425,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 +443,17 @@ class TestServer(object): |
| except IOError: |
| print 'Failed to load policy from %s' % policy_path |
| + self.cert_chain = [] |
| + for cert_path in policy_cert_chain: |
| + try: |
| + last_cert = open(cert_path).read() |
| + except IOError: |
| + print 'Failed to load certificate from %s' % cert_path |
| + certificate = tlslite.api.X509() |
| + certificate.parse(last_cert) |
| + self.cert_chain.append(certificate) |
| + self.private_key = tlslite.api.parsePEMKey(last_cert, private=True) |
| + |
| def HandleRequest(self, path, headers, request): |
| """Handles a request. |