示例#1
0
    def test_is_authenticated(self, get_mock):
        """
        Tests how QBO client's on-demand auth check.
        """
        org = Org(id='test', provider_config=self.test_provider_config).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'refresh_token': 'refresh'
                       }).put()

        # getting CompanyInfo means authenticated
        get_mock.return_value = {'CompanyInfo': {'CompanyName': 'jaja'}}
        qbo_session = QboApiSession('test')

        self.assertTrue(qbo_session.is_authenticated())

        # no CompanyInfo means not authenticated
        get_mock.return_value = {'NotCompanyInfo': {}}
        self.assertFalse(qbo_session.is_authenticated())

        # an exception means not authenticated
        get_mock.side_effect = UnauthorizedApiCallException()
        self.assertFalse(qbo_session.is_authenticated())
示例#2
0
    def test_non_ascii_response(self, request_mock):
        """
        Ensures that the client can handle non-ascii response body (can break due to logging for example).

        Args:
            request_mock(Mock): a mock of the response
        """
        # setup an org
        org = Org(id='test', provider_config=self.test_provider_config).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'refresh_token': 'refresh'
                       }).put()

        # non-200 and non-ascii response
        request_mock.return_value = Mock(status_code=400, text=u'te\xa0st')

        # there should be no exception
        session = QboApiSession('test')
        with self.assertRaises(ValueError):
            session.request('GET', 'http://testurl.com')

        # 200 and non-ascii response
        request_mock.return_value = Mock(status_code=200,
                                         text=u'{"value": "te\xa0st"}')

        # there should be no exception
        session = QboApiSession('test')
        self.assertDictEqual(session.request('GET', 'http://testurl.com'),
                             {"value": u"te\xa0st"})
示例#3
0
def _get_session_cookie(user_creds):
    """ Method to fetch a session cookie from the Zuora API. A session cookies duration time can
    be set by the user. Since there is no way to find out what it has been set to,
    the duration has been set to the minimum (15 minutes). Any cookies which have a duration less
    than this, will be refreshed in the reconnect loop

    Args:
        user_creds(UserCredentials): The users credentials
    """
    session_cookie_url = '{}/connections'.format(BASE_API_URI)
    session_cookie_response = post(session_cookie_url,
                                   headers={
                                       'apiAccessKeyId': user_creds.username,
                                       'apiSecretAccessKey':
                                       user_creds.password,
                                       'Accept': 'application/json',
                                       'content-type': 'application/json'
                                   })

    if session_cookie_response.status_code == 401:
        raise UnauthorizedApiCallException()

    session_cookie = session_cookie_response.headers.get('set-cookie')
    cookie_expiry = datetime.utcnow() + timedelta(minutes=14)
    cookie_expiry = calendar.timegm(cookie_expiry.utctimetuple())
    token = {'expires_at': cookie_expiry, 'access_token': session_cookie}
    parent = ndb.Key('Org', user_creds.key.id())
    OrgCredentials(parent=parent, id=user_creds.key.id(), token=token).put()
    def test_mismatching_xero_file(self, fetch_token_mock, connected_mock,
                                   disconnected_mock):
        """
        Verifies that a XeroOrg cannot be reconnected to a different file than its own. (i.e via mismatching ShortCodes)

        Args:
            fetch_token_mock (MagicMock): fetch_access_token mock
        """

        token = {
            'expires_at': 117,
            'oauth_token': 'blah',
            'oauth_token_secret': 'secret'
        }
        fetch_token_mock.return_value = token

        org = Org(id='test',
                  redirect_url="http://app?app_state=blah",
                  entity_id='vlg_pls_stop',
                  provider_config=self.provider_configs['xerov2']).put()
        OrgCredentials(id='test', parent=org, token=token).put()

        response = self.app.get(
            '/linker/test/oauth?oauth_verifier=123&oauth_token=blah')

        # user is redirected to the app with an error message
        self.assertEqual(response.status_code, 302)
        connected_mock.assert_not_called()
        disconnected_mock.assert_called_once()
        self.assertEqual(
            response.location,
            "http://app?app_state=blah&error_code=source_mismatch")
    def test_oauth1(self, init_update_mock, save_token_mock, connected_mock,
                    publish_mock):
        """
        Tests oauth1 token session flow
        """

        org = Org(id='test',
                  redirect_url="http://app",
                  provider_config=self.provider_configs['xerov2']).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'oauth_token': 'blah',
                           'oauth_token_secret': 'doggo'
                       }).put()
        response = self.app.get(
            '/linker/test/oauth?oauth_verifier=123&oauth_token=blah')
        self.assertEqual(response.status_code, 302)

        # the user is redirected to the provider
        self.assertEqual(response.location,
                         "http://app?data_source_name=ACUIT")

        # token is saved
        save_token_mock.assert_called_once()

        # and then org is connected (this publishes status as connected as well)
        connected_mock.assert_called_once()

        # and the initial sync has been kicked off
        init_update_mock.assert_called_once()
示例#6
0
    def __init__(self, org_uid):
        """
        Prepares access token for API calls (gets it from datastore and refreshes as needed).

        Args:
            org_uid(str): org identifier
        """
        parent = ndb.Key('Org', org_uid)
        self.org_uid = org_uid
        org = parent.get_async()
        self.creds = OrgCredentials.get_by_id(org_uid, parent=parent)
        expires_at = datetime.utcfromtimestamp(self.creds.token['expires_at'])

        # TODO: this call and refresh_token function might not be needed as OAuth2Session can take auto_refresh_url
        # as a parameter and do this automatically
        provider_config_key = org.get_result().provider_config
        if provider_config_key is None:
            logging.warn("org `{}` does not have a provider config.".format(parent.id()))
            raise MissingProviderConfigException()
        else:
            self.provider_config = provider_config_key.get()
        if (expires_at - datetime.utcnow()).total_seconds() < 60:
            logging.info("access token for {} about to expire, refreshing".format(self.org_uid))
            self.refresh_token()

        super(QboApiSession, self).__init__(self.provider_config.client_id, token=self.creds.token)
示例#7
0
    def test_request(self, request_mock):
        """
        Tests Xero API request error handling.

        Args:
            request_mock(Mock): mock of the xero api call response
        """
        org = Org(id='test', provider_config=self.test_provider_config).put()
        OrgCredentials(id='test', parent=org, token={'expires_at': 0, 'oauth_token': 'token', 'oauth_token_secret': 'secret'}).put()
        session = XeroApiSession('test')

        # successful response data comes through
        request_mock.return_value = Mock(status_code=200, text='{"key": "value"}')
        data = session.get("https://xero")
        self.assertEqual(data, {"key": "value"})

        # 401 response raises a custom exception
        request_mock.return_value = Mock(status_code=401)
        with self.assertRaises(UnauthorizedApiCallException):
            session.get("https://xero")

        request_mock.return_value = Mock(status_code=403)
        with self.assertRaises(ForbiddenApiCallException):
            session.get('https://xero')

        # non 200 and non 401 response raises an exception
        request_mock.return_value = Mock(status_code=500)
        with self.assertRaises(ValueError):
            session.get("https://xero")
    def refresh_token(self, rsa_key):
        """
        Refreshes the access token. This can only be done with Partner tokens,
        not public ones.

        Args:
            rsa_key (str): The RSA key
        """

        oauth = OAuth1Session(
            self.provider_config.client_id,
            client_secret=self.provider_config.client_secret,
            resource_owner_key=self.current_token['oauth_token'],
            resource_owner_secret=self.current_token.get('oauth_token_secret'),
            rsa_key=rsa_key,
            signature_method='RSA-SHA1'
        )

        try:
            resp = oauth.post(
                ACCESS_URL,
                params={'oauth_session_handle': self.current_token['oauth_session_handle']}
            )
        except Exception as e:
            logging.error("failed to refresh token", e)
            raise DisconnectException()

        self.current_token = _process_token(json_module.loads(resp.text))
        parent = ndb.Key('Org', self.org_uid)
        OrgCredentials(parent=parent, id=self.org_uid, token=self.current_token).put()
    def __init__(self, org_uid, callback_args):
        """
        Third step of the Oauth1 flow. Processing the callback from Xero and
        using the callback params for fetching the access token.

        Args:
            org_uid(str): org identifier
            callback_args(dict): request parameters send by Xero
        """

        self.org_uid = org_uid
        self.callback_args = callback_args
        self.org = Org.get_by_id(org_uid)
        self.provider = self.org.provider_config.get()
        rsa_key, rsa_method = _get_partner_session_attrs(self.provider)
        request_token = OrgCredentials.get_by_id(self.org_uid, parent=self.org.key).token

        super(XeroTokenSession, self).__init__(
            self.provider.client_id,
            client_secret=self.provider.client_secret,
            resource_owner_key=callback_args['oauth_token'],
            resource_owner_secret=request_token.get('oauth_token_secret'),
            verifier=callback_args['oauth_verifier'],
            rsa_key=rsa_key,
            signature_method=rsa_method
        )
示例#10
0
    def __init__(self, org_uid):
        """
        Prepares access token for API calls (gets it from datastore and refreshes as needed).

        Args:
            org_uid(str): org identifier
        """
        parent = ndb.Key('Org', org_uid)
        self.org_uid = org_uid
        org = parent.get_async()
        self.creds = OrgCredentials.get_by_id(org_uid, parent=parent)
        expires_at = datetime.utcfromtimestamp(self.creds.token['expires_at'])

        provider_config_key = org.get_result().provider_config
        if provider_config_key is None:
            logging.warn("org `{}` does not have a provider config.".format(
                parent.id()))
            raise MissingProviderConfigException()
        else:
            self.provider_config = provider_config_key.get()

        if (expires_at - datetime.utcnow()).total_seconds() < 60:
            logging.info(
                "access token for {} about to expire, refreshing".format(
                    self.org_uid))
            self.refresh_token()
            self.access_token = self.creds.token['access_token']

        super(ZuoraApiSession, self).__init__()
示例#11
0
    def refresh_token(self):
        """
        Refreshes the session cookie for the org
        """
        parent = ndb.Key('Org', self.org_uid)
        user_creds = UserCredentials.get_by_id(self.org_uid, parent=parent)

        _get_session_cookie(user_creds)
        self.creds = OrgCredentials.get_by_id(self.org_uid, parent=parent)
    def get_and_save_token(self):
        token = _process_token(self.fetch_access_token(ACCESS_URL))
        parent = ndb.Key('Org', self.org_uid)
        OrgCredentials(parent=parent, id=self.org_uid, token=token).put()

        # If the entity_id exists, this is a reconnect. Confirm the file connected is the same one.
        if self.org.entity_id:
            xero_session = XeroApiSession(self.org_uid)
            if xero_session.get_short_code() != self.org.entity_id:
                raise MismatchingFileConnectionAttempt(self.org)
    def create_org(self, status=CONNECTED, changeset=-1):
        """
        Utility method to create a dummy org.

        Args:
            status(int): connection of the dummy org
        """
        org = Org(id='test',
                  status=status,
                  changeset=changeset,
                  provider_config=self.test_provider_config).put()
        OrgCredentials(id='test', parent=org, token={'expires_at': 0}).put()
    def get_authorization_url(self):
        """
        Returns the url to which the user should be redirected to in order to complete the auth flow.

        Returns:
            str: url to which the user should be redirected to in order to complete the auth flow
        """

        request_token = self.fetch_request_token(TOKEN_URL)
        authorization_url = self.authorization_url(AUTH_HOST)
        parent = ndb.Key('Org', self.org_uid)
        OrgCredentials(parent=parent, id=self.org_uid, token=request_token).put()
        return authorization_url
示例#15
0
    def refresh_token(self):
        """
        Refreshes the access token for the org.
        """
        try:
            token = OAuth2Session().refresh_token(
                token_url=TOKEN_URL,
                refresh_token=self.creds.token['refresh_token'],
                headers={
                    'Authorization': "Basic " + base64.b64encode(
                        self.provider_config.client_id + ":" + self.provider_config.client_secret
                    ),
                    'Accept': 'application/json',
                    'content-type': 'application/x-www-form-urlencoded'
                }
            )
        except InvalidGrantError:
            logging.warn("got InvalidGrantError exception on token refresh")
            raise InvalidGrantException()

        parent = ndb.Key('Org', self.org_uid)
        OrgCredentials(parent=parent, id=self.org_uid, token=token).put()
        self.creds = OrgCredentials.get_by_id(self.org_uid, parent=parent)
    def test_basic_auth_creds_provided_by_apigee(self, init_update_mock,
                                                 save_token_mock,
                                                 connected_mock, publish_mock):
        """
        Verifies that linking works correctly when user credentials are supplied from apigee via
        the connect endpoint
        """
        org = Org(provider='zuora',
                  id='test',
                  redirect_url="http://app",
                  provider_config=self.provider_configs['zuora']).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'access_token': 'blah'
                       }).put()

        response = self.app.post(
            '/linker/zuora/test/connect?redirect_url=http://app&app_family=local_host_family',
            json={
                'username': '******',
                'password': '******'
            },
            content_type='application/json')
        self.assertEqual(response.status_code, 302)

        # the user is redirected to the provider
        self.assertEqual(response.location,
                         'http://app?data_source_name=ACUIT')

        # org is linking and status is published on pubsub
        self.assertEqual(Org.get_by_id('test').status, LINKING)

        # app redirect url is saved
        self.assertEqual(Org.get_by_id('test').redirect_url, "http://app")

        # token is saved
        save_token_mock.assert_called_once()

        # and then org is connected (this publishes status as connected as well)
        connected_mock.assert_called_once()

        # and the initial sync has been kicked off
        init_update_mock.assert_called_once()
    def __init__(self, org_uid):
        """
        Prepares access token for API calls (gets it from datastore and refreshes as needed).

        Args:
            org_uid(str): org identifier
        """
        parent = ndb.Key('Org', org_uid)
        self.org_uid = org_uid
        org = parent.get_async()
        self.creds = OrgCredentials.get_by_id(org_uid, parent=parent)

        self.current_token = self.creds.token
        expires_at = datetime.utcfromtimestamp((self.creds.token['expires_at']))

        self.org = org.get_result()
        provider_config_key = self.org.provider_config

        if provider_config_key is None:
            logging.warn("org `{}` does not have a provider config.".format(parent.id()))
            raise MissingProviderConfigException
        else:
            self.provider_config = provider_config_key.get()

        auth_attrs = json_module.loads(self.provider_config.additional_auth_attributes)

        if (expires_at - datetime.utcnow()).total_seconds() < 60:
            logging.info("access token for {} about to expire, refreshing".format(self.org_uid))

            if auth_attrs['application_type'] == PARTNER:
                self.refresh_token(auth_attrs['rsa_key'])
            else:
                logging.info("application type is `public`. Skipping refresh.")

        rsa_key, sig_method = _get_partner_session_attrs(self.provider_config)

        super(XeroApiSession, self).__init__(
            self.provider_config.client_id,
            client_secret=self.provider_config.client_secret,
            resource_owner_key=self.current_token['oauth_token'],
            resource_owner_secret=self.current_token.get('oauth_token_secret'),
            rsa_key=rsa_key,
            signature_method=sig_method
        )
示例#18
0
 def get_and_save_token(self):
     """
     Fetches the access token from QBO with the given auth code. Also kicks off the sync of the org as it can be
     considered connected once the access token is obtained.
     """
     provider_config = self.org.provider_config.get()
     token = self.fetch_token(
         TOKEN_URL,
         code=self.callback_args.get('code'),
         headers={
             'Authorization': "Basic " + base64.b64encode(
                 provider_config.client_id + ":" + provider_config.client_secret
             ),
             'Accept': 'application/json',
             'content-type': 'application/x-www-form-urlencoded'
         }
     )
     parent = ndb.Key('Org', self.org_uid)
     OrgCredentials(parent=parent, id=self.org_uid, token=token).put()
示例#19
0
    def test_is_authenticated(self, get_mock):
        """
        Tests how Xero client's on-demand auth check.
        """
        org = Org(id='test', provider_config=self.test_provider_config, entity_id='ShortCode').put()
        OrgCredentials(id='test', parent=org, token={'expires_at': 0, 'oauth_token': 'token', 'oauth_token_secret': 'secret'}).put()

        # getting CompanyInfo means authenticated
        get_mock.return_value = {'Organisations': [{'Name': 'test'}]}
        xero_session = XeroApiSession('test')

        self.assertTrue(xero_session.is_authenticated())

        # no CompanyInfo means not authenticated
        get_mock.return_value = {'NotCompanyInfo': {}}
        self.assertFalse(xero_session.is_authenticated())

        # an exception means not authenticated
        get_mock.side_effect = UnauthorizedApiCallException()
        self.assertFalse(xero_session.is_authenticated())
    def test_basic_auth(self, init_update_mock, save_token_mock,
                        connected_mock, publish_mock):
        """
        Tests username/password auth flow
        """

        org = Org(id='test',
                  redirect_url="http://app",
                  provider_config=self.provider_configs['zuora']).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'access_token': 'blah'
                       }).put()
        response = self.app.post(
            '/linker/handle_login',
            data={
                'username': '******',
                'password': '******',
                'provider': 'zuora',
                'org_uid': 'test'
            },
            content_type='application/x-www-form-urlencoded')

        self.assertEqual(response.status_code, 302)

        # the user is redirected to the provider
        self.assertEqual(response.location,
                         "http://app?data_source_name=ACUIT")

        # token is saved
        save_token_mock.assert_called_once()

        # and then org is connected (this publishes status as connected as well)
        connected_mock.assert_called_once()

        # and the initial sync has been kicked off
        init_update_mock.assert_called_once()
示例#21
0
    def test_is_authenticated_invalid_grant(self, disconnected_mock):
        """
        Verifies that the InvalidGrantException in the long term reconnect loop does not throw an exception, but is
        interpreted as a disconnection.
        """
        org = Org(id='test',
                  provider='qbo',
                  provider_config=self.test_provider_config).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'refresh_token': 'refresh'
                       }).put()

        response = self.app.post(
            '/adapter/test/reconnect',
            headers={'X-AppEngine-TaskExecutionCount': 10})

        # InvalidGrantException should be treated as a disconnection, and no exception should be thrown
        self.assertEqual(response.status_code, 423)
        disconnected_mock.assert_not_called()
示例#22
0
    def test_request(self, request_mock):
        """
        Tests QBO API request error handling.

        Args:
            request_mock(Mock): mock of the qbo api call response
        """
        org = Org(id='test', provider_config=self.test_provider_config).put()
        OrgCredentials(id='test',
                       parent=org,
                       token={
                           'expires_at': 0,
                           'refresh_token': 'refresh'
                       }).put()
        session = QboApiSession('test')

        # successful response data comes through
        request_mock.return_value = Mock(status_code=200,
                                         text='{"key": "value"}')
        data = session.get("https://qbo")
        self.assertEqual(data, {"key": "value"})

        # 401 response raises a custom exception
        request_mock.return_value = Mock(status_code=401)
        with self.assertRaises(UnauthorizedApiCallException):
            session.get("https://qbo")

        # non 200 and non 401 response raises an exception
        request_mock.return_value = Mock(status_code=500)
        with self.assertRaises(ValueError):
            session.get("https://qbo")

        # qbo notifies of some errors with 200 and Fault key in response
        request_mock.return_value = Mock(status_code=200,
                                         text='{"Fault": "wrong"}')
        with self.assertRaises(ValueError):
            session.get("https://qbo")
    def create_org(self,
                   provider='qbo',
                   status=CONNECTED,
                   set_provider_config=True):
        """
        Utility method to create a dummy org.

        Args:
            provider(str): The provider of the org
            status(int): connection of the dummy org
            set_provider_config (bool): Whether to set the provider config
        """

        if set_provider_config:
            provider_config = self.provider_configs[provider]
        else:
            provider_config = None

        org = Org(provider=provider,
                  id='test',
                  status=status,
                  provider_config=provider_config).put()

        OrgCredentials(id='test', parent=org, token={'expires_at': 0}).put()
    def test_oauth2(self, init_update_mock, save_token_mock, connected_mock,
                    publish_mock):
        """
        Tests the first step of the oauth flow authorisation.
        """
        org = Org(id='test',
                  redirect_url="http://app",
                  provider_config=self.provider_configs['qbo']).put()
        OrgCredentials(id='test', parent=org, token={'expires_at': 0}).put()
        response = self.app.get('/linker/oauth?state=test')
        self.assertEqual(response.status_code, 302)

        # the user is redirected to the provider
        self.assertEqual(response.location,
                         "http://app?data_source_name=ACUIT")

        # token is saved
        save_token_mock.assert_called_once()

        # and then org is connected (this publishes status as connected as well)
        connected_mock.assert_called_once()

        # and the initial sync has been kicked off
        init_update_mock.assert_called_once()