• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7import logging
8from cherrypy import tools
9import time
10import uuid
11
12import common
13from fake_device_server import common_util
14from fake_device_server import server_errors
15
16REGISTRATION_PATH = 'registrationTickets'
17
18
19class 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@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@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