# Copyright 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Module contains a simple implementation of the registrationTickets RPC.""" import logging from cherrypy import tools import time import uuid import common from fake_device_server import common_util from fake_device_server import server_errors REGISTRATION_PATH = 'registrationTickets' class RegistrationTickets(object): """A simple implementation of the registrationTickets interface. A common workflow of using this API is: client: POST .../ # Creates a new ticket with id claims the ticket. device: PATCH .../ with json blob # Populate ticket with device info device: POST ...//finalize # Finalize the device registration. """ # OAUTH2 Bearer Access Token TEST_ACCESS_TOKEN = '1/TEST-ME' # Needed for cherrypy to expose this to requests. exposed = True def __init__(self, resource, devices_instance, fail_control_handler): """Initializes a registration ticket. @param resource: A resource delegate. @param devices_instance: Instance of Devices class. @param fail_control_handler: Instance of FailControl. """ self.resource = resource self.devices_instance = devices_instance self._fail_control_handler = fail_control_handler def _default_registration_ticket(self): """Creates and returns a new registration ticket.""" current_time_ms = time.time() * 1000 ticket = {'kind': 'clouddevices#registrationTicket', 'creationTimeMs': current_time_ms, 'expirationTimeMs': current_time_ms + (10 * 1000)} return ticket def _finalize(self, id, api_key, ticket): """Finalizes the ticket causing the server to add robot account info.""" if 'userEmail' not in ticket: raise server_errors.HTTPError(400, 'Unclaimed ticket') robot_account_email = 'robot@test.org' robot_auth = uuid.uuid4().hex new_data = {'robotAccountEmail': robot_account_email, 'robotAccountAuthorizationCode':robot_auth} updated_data_val = self.resource.update_data_val(id, api_key, new_data) updated_data_val['deviceDraft'] = self.devices_instance.create_device( api_key, updated_data_val.get('deviceDraft')) return updated_data_val def _add_claim_data(self, data): """Adds userEmail to |data| to claim ticket. Raises: server_errors.HTTPError if there is an authorization error. """ access_token = common_util.grab_header_field('Authorization') if not access_token: raise server_errors.HTTPError(401, 'Missing Authorization.') # Authorization should contain " " access_token_list = access_token.split() if len(access_token_list) != 2: raise server_errors.HTTPError(400, 'Malformed Authorization field') [type, code] = access_token_list # TODO(sosa): Consider adding HTTP WWW-Authenticate response header # field if type != 'Bearer': raise server_errors.HTTPError(403, 'Authorization requires ' 'bearer token.') elif code != RegistrationTickets.TEST_ACCESS_TOKEN: raise server_errors.HTTPError(403, 'Wrong access token.') else: logging.info('Ticket is being claimed.') data['userEmail'] = 'test_account@chromium.org' @tools.json_out() def GET(self, *args, **kwargs): """GET .../ticket_number returns info about the ticket. Raises: server_errors.HTTPError if the ticket doesn't exist. """ self._fail_control_handler.ensure_not_in_failure_mode() id, api_key, _ = common_util.parse_common_args(args, kwargs) return self.resource.get_data_val(id, api_key) @tools.json_out() def POST(self, *args, **kwargs): """Either creates a ticket OR claim/finalizes a ticket. This method implements the majority of the registration workflow. More specifically: POST ... creates a new ticket POST .../ticket_number/claim claims a given ticket with a fake email. POST .../ticket_number/finalize finalizes a ticket with a robot account. Raises: server_errors.HTTPError if the ticket should exist but doesn't (claim/finalize) or if we can't parse all the args. """ self._fail_control_handler.ensure_not_in_failure_mode() id, api_key, operation = common_util.parse_common_args( args, kwargs, supported_operations=set(['finalize'])) if operation: ticket = self.resource.get_data_val(id, api_key) if operation == 'finalize': return self._finalize(id, api_key, ticket) else: raise server_errors.HTTPError( 400, 'Unsupported method call %s' % operation) else: data = common_util.parse_serialized_json() if data is None or data.get('userEmail', None) != 'me': raise server_errors.HTTPError( 400, 'Require userEmail=me to create ticket %s' % operation) if [key for key in iter(data) if key != 'userEmail']: raise server_errors.HTTPError( 400, 'Extra data for ticket creation: %r.' % data) if id: raise server_errors.HTTPError( 400, 'Should not specify ticket ID.') self._add_claim_data(data) # We have an insert operation so make sure we have all required # fields. data.update(self._default_registration_ticket()) logging.info('Ticket is being created.') return self.resource.update_data_val(id, api_key, data_in=data) @tools.json_out() def PATCH(self, *args, **kwargs): """Updates the given ticket with the incoming json blob. Format of this call is: PATCH .../ticket_number Caller must define a json blob to patch the ticket with. Raises: server_errors.HTTPError if the ticket doesn't exist. """ self._fail_control_handler.ensure_not_in_failure_mode() id, api_key, _ = common_util.parse_common_args(args, kwargs) if not id: server_errors.HTTPError(400, 'Missing id for operation') data = common_util.parse_serialized_json() return self.resource.update_data_val( id, api_key, data_in=data) @tools.json_out() def PUT(self, *args, **kwargs): """Replaces the given ticket with the incoming json blob. Format of this call is: PUT .../ticket_number Caller must define a json blob to patch the ticket with. Raises: """ self._fail_control_handler.ensure_not_in_failure_mode() id, api_key, _ = common_util.parse_common_args(args, kwargs) if not id: server_errors.HTTPError(400, 'Missing id for operation') data = common_util.parse_serialized_json() # Handle claiming a ticket with an authorized request. if data and data.get('userEmail') == 'me': self._add_claim_data(data) return self.resource.update_data_val( id, api_key, data_in=data, update=False)