Home | History | Annotate | Download | only in fake_device_server
      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