Exemplo n.º 1
0
    def create_category(self,
                        name,
                        color,
                        text_color='FFFFFF',
                        permissions=None,
                        parent=None,
                        **kwargs):
        """ permissions - dict of 'everyone', 'admins', 'moderators', 'staff' with values of
        """

        kwargs['name'] = name
        kwargs['color'] = color
        kwargs['text_color'] = text_color

        if permissions is None and 'permissions' not in kwargs:
            permissions = {'everyone': '1'}

        for key, value in permissions.items():
            kwargs['permissions[{0}]'.format(key)] = value

        if parent:
            parent_id = None
            for category in self.categories():
                if category['name'] == parent:
                    parent_id = category['id']
                    continue

            if not parent_id:
                raise DiscourseClientError(u'{0} not found'.format(parent))
            kwargs['parent_category_id'] = parent_id

        return self._post('/categories', **kwargs)
Exemplo n.º 2
0
    def _request(self, verb, path, params):
        params['api_key'] = self.api_key
        if 'api_username' not in params:
            params['api_username'] = self.api_username
        url = self.host + path

        response = requests.request(verb,
                                    url,
                                    allow_redirects=False,
                                    params=params,
                                    timeout=self.timeout)

        log.debug('response %s: %s', response.status_code, repr(response.text))
        if not response.ok:
            try:
                msg = u','.join(response.json()['errors'])
            except (ValueError, TypeError, KeyError):
                if response.reason:
                    msg = response.reason
                else:
                    msg = u'{0}: {1}'.format(response.status_code,
                                             response.text)

            if 400 <= response.status_code < 500:
                raise DiscourseClientError(msg, response=response)

            raise DiscourseServerError(msg, response=response)

        if response.status_code == 302:
            raise DiscourseError(
                'Unexpected Redirect, invalid api key or host?',
                response=response)

        json_content = 'application/json; charset=utf-8'
        content_type = response.headers['content-type']
        if content_type != json_content:
            # some calls return empty html documents
            if response.content == ' ':
                return None

            raise DiscourseError(
                'Invalid Response, expecting "{0}" got "{1}"'.format(
                    json_content, content_type),
                response=response)

        try:
            decoded = response.json()
        except ValueError:
            raise DiscourseError('failed to decode response',
                                 response=response)

        if 'errors' in decoded:
            message = decoded.get('message')
            if not message:
                message = u','.join(decoded['errors'])
            raise DiscourseError(message, response=response)

        return decoded
Exemplo n.º 3
0
    def create_category(self,
                        name,
                        color,
                        text_color="FFFFFF",
                        permissions=None,
                        parent=None,
                        **kwargs):
        """

        Args:
            name:
            color:
            text_color: hex color without number symbol
            permissions: dict of 'everyone', 'admins', 'moderators', 'staff' with values of ???
            parent: name of the category
            parent_category_id:
            **kwargs:

        Returns:

        """
        kwargs["name"] = name
        kwargs["color"] = color
        kwargs["text_color"] = text_color

        if permissions is None and "permissions" not in kwargs:
            permissions = {"everyone": "1"}

        for key, value in permissions.items():
            kwargs["permissions[{0}]".format(key)] = value

        if parent:
            parent_id = None
            for category in self.categories():
                if category["name"] == parent:
                    parent_id = category["id"]
                    continue

            if not parent_id:
                raise DiscourseClientError(u"{0} not found".format(parent))

            kwargs["parent_category_id"] = parent_id

        return self._post("/categories", **kwargs)
Exemplo n.º 4
0
    def _request(
        self, verb, path, params=None, files=None, data=None, json=None, override_request_kwargs=None
    ):
        """
        Executes HTTP request to API and handles response

        Args:
            verb: HTTP verb as string: GET, DELETE, PUT, POST
            path: the path on the Discourse API
            params: dictionary of parameters to include to the API
            override_request_kwargs: dictionary of requests.request keyword arguments to override defaults

        Returns:
            dictionary of response body data or None

        """
        override_request_kwargs = override_request_kwargs or {}

        url = self.host + path

        headers = {
            "Accept": "application/json; charset=utf-8",
            "Api-Key": self.api_key,
            "Api-Username": self.api_username,
        }

        # How many times should we retry if rate limited
        retry_count = 4
        # Extra time (on top of that required by API) to wait on a retry.
        retry_backoff = 1

        while retry_count > 0:
            request_kwargs = dict(
                allow_redirects=False,
                params=params,
                files=files,
                data=data,
                json=json,
                headers=headers,
                timeout=self.timeout,
            )

            request_kwargs.update(override_request_kwargs)

            response = requests.request(verb, url, **request_kwargs)

            log.debug("response %s: %s", response.status_code, repr(response.text))
            if response.ok:
                break
            if not response.ok:
                try:
                    msg = u",".join(response.json()["errors"])
                except (ValueError, TypeError, KeyError):
                    if response.reason:
                        msg = response.reason
                    else:
                        msg = u"{0}: {1}".format(response.status_code, response.text)

                if 400 <= response.status_code < 500:
                    if 429 == response.status_code:
                        # This codepath relies on wait_seconds from Discourse v2.0.0.beta3 / v1.9.3 or higher.
                        rj = response.json()
                        wait_delay = (
                            retry_backoff + rj["extras"]["wait_seconds"]
                        )  # how long to back off for.

                        if retry_count > 1:
                            time.sleep(wait_delay)
                        retry_count -= 1
                        log.info(
                            "We have been rate limited and waited {0} seconds ({1} retries left)".format(
                                wait_delay, retry_count
                            )
                        )
                        log.debug("API returned {0}".format(rj))
                        continue
                    else:
                        raise DiscourseClientError(msg, response=response)

                # Any other response.ok resulting in False
                raise DiscourseServerError(msg, response=response)

        if retry_count == 0:
            raise DiscourseRateLimitedError(
                "Number of rate limit retries exceeded. Increase retry_backoff or retry_count",
                response=response,
            )

        if response.status_code == 302:
            raise DiscourseError(
                "Unexpected Redirect, invalid api key or host?", response=response
            )

        json_content = "application/json; charset=utf-8"
        content_type = response.headers["content-type"]
        if content_type != json_content:
            # some calls return empty html documents
            if not response.content.strip():
                return None

            raise DiscourseError(
                'Invalid Response, expecting "{0}" got "{1}"'.format(
                    json_content, content_type
                ),
                response=response,
            )

        try:
            decoded = response.json()
        except ValueError:
            raise DiscourseError("failed to decode response", response=response)

        # Checking "errors" length because
        # data-explorer (e.g. POST /admin/plugins/explorer/queries/{}/run)
        # sends an empty errors array
        if "errors" in decoded and len(decoded["errors"]) > 0:
            message = decoded.get("message")
            if not message:
                message = u",".join(decoded["errors"])
            raise DiscourseError(message, response=response)

        return decoded
Exemplo n.º 5
0
class TestSsoSyncRest(BaseTestRest):
    def setUp(self):
        super().setUp()
        self._url = "/sso_sync"

        self.contributor = self.session.query(User). \
            filter(User.username == 'contributor'). \
            one()

        self.session.add(SsoKey(
            domain=sso_domain,
            key=sso_key,
        ))
        self.contributor_external_id = SsoExternalId(
            domain=sso_domain,
            external_id='1',
            user=self.contributor,
        )
        self.session.add(self.contributor_external_id)
        self.session.flush()

        set_discourse_client(None)

    def test_no_sso_key(self):
        request_body = {
            'external_id': '999',
            'email': '*****@*****.**'
        }
        body = self.app_post_json(self._url, request_body, status=400).json
        errors = body.get('errors')
        self.assertEqual('sso_key', errors[0].get('name'))
        self.assertEqual('Required', errors[0].get('description'))

    def test_bad_sso_key(self):
        request_body = {
            'sso_key': 'bad_sso_key',
            'external_id': '999',
            'email': '*****@*****.**'
        }
        body = self.app_post_json(self._url, request_body, status=403).json
        errors = body.get('errors')
        self.assertEqual('sso_key', errors[0].get('name'))
        self.assertEqual('Invalid', errors[0].get('description'))

    def test_no_external_id(self):
        request_body = {'sso_key': sso_key}
        body = self.app_post_json(self._url, request_body, status=400).json
        errors = body.get('errors')
        self.assertEqual('external_id', errors[0].get('name'))
        self.assertEqual('Required', errors[0].get('description'))

    def test_new_user_no_email(self):
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
        }
        body = self.app_post_json(self._url, request_body, status=400).json
        errors = body.get('errors')
        self.assertEqual('email', errors[0].get('name'))
        self.assertEqual('Required', errors[0].get('description'))

    def test_new_user_existing_username(self):
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
            'email': '*****@*****.**',
            'username': self.contributor.username,
            'lang': 'fr',
            'groups': 'group1,group2'
        }
        body = self.app_post_json(self._url, request_body, status=400).json
        errors = body.get('errors')
        self.assertEqual('already used username', errors[0].get('description'))

    def test_new_user_existing_forum_username(self):
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
            'email': '*****@*****.**',
            'username': '******',
            'forum_username': self.contributor.forum_username,
            'lang': 'fr',
            'groups': 'group1,group2'
        }
        body = self.app_post_json(self._url, request_body, status=400).json
        errors = body.get('errors')
        self.assertEqual('already used forum_username',
                         errors[0].get('description'))

    def test_new_user_forum_username_too_long(self):
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
            'email': '*****@*****.**',
            'username': '******',
            'forum_username': '******',
            'lang': 'fr',
            'groups': 'group1,group2'
        }
        body = self.app_post_json(self._url, request_body, status=400).json
        errors = body.get('errors')
        self.assertEqual('forum_username', errors[0].get('name'))
        self.assertEqual('Longer than maximum length 25',
                         errors[0].get('description'))

    @patch('c2corg_api.security.discourse_client.DiscourseClient',
           return_value=Mock(
               by_external_id=Mock(side_effect=DiscourseClientError(
                   response=Mock(status_code=404))),
               sync_sso=Mock(return_value={'id': 555})))
    def test_new_user_success(self, discourse_mock):
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
            'email': '*****@*****.**',
            'username': '******',
            'name': 'New User',
            'forum_username': '******',
            'lang': 'fr',
        }
        body = self.app_post_json(self._url, request_body, status=200).json

        sso_external_id = self.session.query(SsoExternalId). \
            filter(SsoExternalId.domain == sso_domain). \
            filter(SsoExternalId.external_id == '999'). \
            one_or_none()
        self.assertIsNotNone(sso_external_id)
        sso_user = sso_external_id.user
        self.assertEqual('newuser', sso_user.username)
        self.assertEqual('New User', sso_user.name)
        self.assertEqual('NewUser', sso_user.forum_username)

        self.assertEqual(sso_external_id.token,
                         self.token_from_url(body.get('url')))

        client = discourse_mock.return_value
        client.by_external_id.assert_called_with(sso_external_id.user.id)
        client.sync_sso.assert_called_once_with(
            sso_secret=self.settings.get('discourse.sso_secret'),
            name='New User',
            username='******',
            email='*****@*****.**',
            external_id=sso_user.id)

    def token_from_url(self, url):
        qs = parse_qs(urlparse(url).query)
        return qs.get('token')[0]

    @patch('c2corg_api.security.discourse_client.DiscourseClient',
           return_value=Mock(by_external_id=Mock(return_value={
               'id': 1,
               'username': '******'
           })))
    def test_success_by_email(self, discourse_mock):
        request_body = {
            'sso_key': sso_key,
            'external_id': '1',
            'email': self.contributor.email
        }
        body = self.app_post_json(self._url, request_body, status=200).json

        sso_external_id = self.session.query(SsoExternalId). \
            filter(SsoExternalId.domain == sso_domain). \
            filter(SsoExternalId.external_id == '1'). \
            one_or_none()
        self.assertIsNotNone(sso_external_id)
        self.assertEqual('contributor', sso_external_id.user.username)

        self.assertEqual(sso_external_id.token,
                         self.token_from_url(body.get('url')))

    @patch('c2corg_api.security.discourse_client.DiscourseClient',
           return_value=Mock(by_external_id=Mock(return_value={
               'id': 1,
               'username': '******'
           })))
    def test_success_by_external_id(self, discourse_mock):
        request_body = {
            'sso_key': sso_key,
            'external_id': '1',
            'email': '*****@*****.**',
            'username': self.contributor.username,
            'name': self.contributor.name,
            'forum_username': self.contributor.forum_username,
            'lang': 'fr',
        }
        body = self.app_post_json(self._url, request_body, status=200).json

        self.session.expire(self.contributor_external_id)
        self.assertEqual(self.contributor_external_id.token,
                         self.token_from_url(body.get('url')))

        client = discourse_mock.return_value
        client.by_external_id.assert_called_once_with(self.contributor.id)

    @patch(
        'c2corg_api.security.discourse_client.DiscourseClient',
        return_value=Mock(by_external_id=Mock(side_effect=ConnectionError())))
    def test_discourse_down(self, discourse_mock):
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
            'email': '*****@*****.**',
            'username': '******',
            'name': 'New User',
            'forum_username': '******',
            'lang': 'fr',
        }
        body = self.app_post_json(self._url, request_body, status=500).json
        errors = body.get('errors')
        self.assertEqual('Error with Discourse', errors[0].get('description'))

    @patch('c2corg_api.security.discourse_client.DiscourseClient',
           return_value=Mock(
               by_external_id=Mock(side_effect=DiscourseClientError(
                   response=Mock(status_code=404))),
               sync_sso=Mock(return_value={'id': 555})))
    def test_new_user_no_name(self, discourse_mock):
        """name and forum_username should default to username"""
        request_body = {
            'sso_key': sso_key,
            'external_id': '999',
            'email': '*****@*****.**',
            'username': '******',
            'lang': 'fr',
        }
        self.app_post_json(self._url, request_body, status=200).json

        sso_external_id = self.session.query(SsoExternalId). \
            filter(SsoExternalId.domain == sso_domain). \
            filter(SsoExternalId.external_id == '999'). \
            one_or_none()
        self.assertIsNotNone(sso_external_id)
        sso_user = sso_external_id.user
        self.assertEqual('newuser', sso_user.username)
        self.assertEqual('newuser', sso_user.name)
        self.assertEqual('newuser', sso_user.forum_username)

    @patch('c2corg_api.security.discourse_client.DiscourseClient',
           return_value=Mock(
               by_external_id=Mock(return_value={
                   'id': 1,
                   'username': '******'
               }),
               sync_sso=Mock(return_value={'id': 1}),
               groups=Mock(return_value=[]),
           ))
    def test_not_found_group(self, discourse_mock):
        request_body = {
            'sso_key': sso_key,
            'external_id': '1',
            'group': 'group_1',
        }
        self.app_post_json(self._url, request_body, status=200).json

    @patch('c2corg_api.security.discourse_client.DiscourseClient',
           return_value=Mock(by_external_id=Mock(return_value={
               'id': 1,
               'username': '******'
           }),
                             groups=Mock(return_value=[{
                                 'id': 222,
                                 'name': 'group_1'
                             }]),
                             add_user_to_group=Mock()))
    def test_existing_group(self, discourse_mock):
        request_body = {
            'sso_key': sso_key,
            'external_id': '1',
            'groups': 'group_1'
        }
        self.app_post_json(self._url, request_body, status=200).json

        client = discourse_mock.return_value
        client.add_user_to_group.assert_called_once_with(222, 1)
Exemplo n.º 6
0
    def _request(self, verb, path, params={}, data={}):
        """
        Executes HTTP request to API and handles response

        Args:
            verb: HTTP verb as string: GET, DELETE, PUT, POST
            path: the path on the Discourse API
            params: dictionary of parameters to include to the API

        Returns:

        """
        params['api_key'] = self.api_key
        if 'api_username' not in params:
            params['api_username'] = self.api_username
        url = self.host + path

        headers = {'Accept': 'application/json; charset=utf-8'}

        response = requests.request(verb,
                                    url,
                                    allow_redirects=False,
                                    params=params,
                                    data=data,
                                    headers=headers,
                                    timeout=self.timeout)

        log.debug('response %s: %s', response.status_code, repr(response.text))
        if not response.ok:
            try:
                msg = u','.join(response.json()['errors'])
            except (ValueError, TypeError, KeyError):
                if response.reason:
                    msg = response.reason
                else:
                    msg = u'{0}: {1}'.format(response.status_code,
                                             response.text)

            if 400 <= response.status_code < 500:
                raise DiscourseClientError(msg, response=response)

            raise DiscourseServerError(msg, response=response)

        if response.status_code == 302:
            raise DiscourseError(
                'Unexpected Redirect, invalid api key or host?',
                response=response)

        json_content = 'application/json; charset=utf-8'
        content_type = response.headers['content-type']
        if content_type != json_content:
            # some calls return empty html documents
            if not response.content.strip():
                return None

            raise DiscourseError(
                'Invalid Response, expecting "{0}" got "{1}"'.format(
                    json_content, content_type),
                response=response)

        try:
            decoded = response.json()
        except ValueError:
            raise DiscourseError('failed to decode response',
                                 response=response)

        if 'errors' in decoded:
            message = decoded.get('message')
            if not message:
                message = u','.join(decoded['errors'])
            raise DiscourseError(message, response=response)

        return decoded
Exemplo n.º 7
0
 def authenticate_user(self, login, password):
     resp = self._post('/session', login=login, password=password)
     if 'error' in resp:
         raise DiscourseClientError(resp['error'])
     return resp['user']