1 # Copyright 2014 The Chromium OS Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Module contains a simple implementation of the registrationTickets RPC.""" 6 7 import logging 8 from cherrypy import tools 9 import time 10 import uuid 11 12 import common 13 from fake_device_server import common_util 14 from fake_device_server import server_errors 15 16 REGISTRATION_PATH = 'registrationTickets' 17 18 19 class RegistrationTickets(object): 20 """A simple implementation of the registrationTickets interface. 21 22 A common workflow of using this API is: 23 24 client: POST .../ # Creates a new ticket with id <id> claims the ticket. 25 device: PATCH .../<id> with json blob # Populate ticket with device info 26 device: POST .../<id>/finalize # Finalize the device registration. 27 """ 28 # OAUTH2 Bearer Access Token 29 TEST_ACCESS_TOKEN = '1/TEST-ME' 30 31 # Needed for cherrypy to expose this to requests. 32 exposed = True 33 34 35 def __init__(self, resource, devices_instance, fail_control_handler): 36 """Initializes a registration ticket. 37 38 @param resource: A resource delegate. 39 @param devices_instance: Instance of Devices class. 40 @param fail_control_handler: Instance of FailControl. 41 """ 42 self.resource = resource 43 self.devices_instance = devices_instance 44 self._fail_control_handler = fail_control_handler 45 46 47 def _default_registration_ticket(self): 48 """Creates and returns a new registration ticket.""" 49 current_time_ms = time.time() * 1000 50 ticket = {'kind': 'clouddevices#registrationTicket', 51 'creationTimeMs': current_time_ms, 52 'expirationTimeMs': current_time_ms + (10 * 1000)} 53 return ticket 54 55 56 def _finalize(self, id, api_key, ticket): 57 """Finalizes the ticket causing the server to add robot account info.""" 58 if 'userEmail' not in ticket: 59 raise server_errors.HTTPError(400, 'Unclaimed ticket') 60 61 robot_account_email = 'robot (at] test.org' 62 robot_auth = uuid.uuid4().hex 63 new_data = {'robotAccountEmail': robot_account_email, 64 'robotAccountAuthorizationCode':robot_auth} 65 updated_data_val = self.resource.update_data_val(id, api_key, new_data) 66 updated_data_val['deviceDraft'] = self.devices_instance.create_device( 67 api_key, updated_data_val.get('deviceDraft')) 68 return updated_data_val 69 70 71 def _add_claim_data(self, data): 72 """Adds userEmail to |data| to claim ticket. 73 74 Raises: 75 server_errors.HTTPError if there is an authorization error. 76 """ 77 access_token = common_util.grab_header_field('Authorization') 78 if not access_token: 79 raise server_errors.HTTPError(401, 'Missing Authorization.') 80 81 # Authorization should contain "<type> <token>" 82 access_token_list = access_token.split() 83 if len(access_token_list) != 2: 84 raise server_errors.HTTPError(400, 'Malformed Authorization field') 85 86 [type, code] = access_token_list 87 # TODO(sosa): Consider adding HTTP WWW-Authenticate response header 88 # field 89 if type != 'Bearer': 90 raise server_errors.HTTPError(403, 'Authorization requires ' 91 'bearer token.') 92 elif code != RegistrationTickets.TEST_ACCESS_TOKEN: 93 raise server_errors.HTTPError(403, 'Wrong access token.') 94 else: 95 logging.info('Ticket is being claimed.') 96 data['userEmail'] = 'test_account (at] chromium.org' 97 98 99 @tools.json_out() 100 def GET(self, *args, **kwargs): 101 """GET .../ticket_number returns info about the ticket. 102 103 Raises: 104 server_errors.HTTPError if the ticket doesn't exist. 105 """ 106 self._fail_control_handler.ensure_not_in_failure_mode() 107 id, api_key, _ = common_util.parse_common_args(args, kwargs) 108 return self.resource.get_data_val(id, api_key) 109 110 111 @tools.json_out() 112 def POST(self, *args, **kwargs): 113 """Either creates a ticket OR claim/finalizes a ticket. 114 115 This method implements the majority of the registration workflow. 116 More specifically: 117 POST ... creates a new ticket 118 POST .../ticket_number/claim claims a given ticket with a fake email. 119 POST .../ticket_number/finalize finalizes a ticket with a robot account. 120 121 Raises: 122 server_errors.HTTPError if the ticket should exist but doesn't 123 (claim/finalize) or if we can't parse all the args. 124 """ 125 self._fail_control_handler.ensure_not_in_failure_mode() 126 id, api_key, operation = common_util.parse_common_args( 127 args, kwargs, supported_operations=set(['finalize'])) 128 if operation: 129 ticket = self.resource.get_data_val(id, api_key) 130 if operation == 'finalize': 131 return self._finalize(id, api_key, ticket) 132 else: 133 raise server_errors.HTTPError( 134 400, 'Unsupported method call %s' % operation) 135 136 else: 137 data = common_util.parse_serialized_json() 138 if data is None or data.get('userEmail', None) != 'me': 139 raise server_errors.HTTPError( 140 400, 141 'Require userEmail=me to create ticket %s' % operation) 142 if [key for key in data.iterkeys() if key != 'userEmail']: 143 raise server_errors.HTTPError( 144 400, 'Extra data for ticket creation: %r.' % data) 145 if id: 146 raise server_errors.HTTPError( 147 400, 'Should not specify ticket ID.') 148 149 self._add_claim_data(data) 150 # We have an insert operation so make sure we have all required 151 # fields. 152 data.update(self._default_registration_ticket()) 153 154 logging.info('Ticket is being created.') 155 return self.resource.update_data_val(id, api_key, data_in=data) 156 157 158 @tools.json_out() 159 def PATCH(self, *args, **kwargs): 160 """Updates the given ticket with the incoming json blob. 161 162 Format of this call is: 163 PATCH .../ticket_number 164 165 Caller must define a json blob to patch the ticket with. 166 167 Raises: 168 server_errors.HTTPError if the ticket doesn't exist. 169 """ 170 self._fail_control_handler.ensure_not_in_failure_mode() 171 id, api_key, _ = common_util.parse_common_args(args, kwargs) 172 if not id: 173 server_errors.HTTPError(400, 'Missing id for operation') 174 175 data = common_util.parse_serialized_json() 176 177 return self.resource.update_data_val( 178 id, api_key, data_in=data) 179 180 181 @tools.json_out() 182 def PUT(self, *args, **kwargs): 183 """Replaces the given ticket with the incoming json blob. 184 185 Format of this call is: 186 PUT .../ticket_number 187 188 Caller must define a json blob to patch the ticket with. 189 190 Raises: 191 """ 192 self._fail_control_handler.ensure_not_in_failure_mode() 193 id, api_key, _ = common_util.parse_common_args(args, kwargs) 194 if not id: 195 server_errors.HTTPError(400, 'Missing id for operation') 196 197 data = common_util.parse_serialized_json() 198 199 # Handle claiming a ticket with an authorized request. 200 if data and data.get('userEmail') == 'me': 201 self._add_claim_data(data) 202 203 return self.resource.update_data_val( 204 id, api_key, data_in=data, update=False) 205