Example #1
0
    def create(self,
               request,
               username,
               service_id,
               password=None,
               hosting_url=None,
               local_site_name=None,
               *args,
               **kwargs):
        """Creates a hosting service account.

        The ``service_id`` is a registered HostingService ID. This must be
        known beforehand, and can be looked up in the Review Board
        administration UI.
        """
        local_site = self._get_local_site(local_site_name)

        if not HostingServiceAccount.objects.can_create(
                request.user, local_site):
            return self._no_access_error(request.user)

        # Validate the service.
        service = get_hosting_service(service_id)

        if not service:
            return INVALID_FORM_DATA, {
                'fields': {
                    'service': ['This is not a valid service name'],
                }
            }

        if service.self_hosted and not hosting_url:
            return INVALID_FORM_DATA, {
                'fields': {
                    'hosting_url': ['This field is required'],
                }
            }

        account = HostingServiceAccount(service_name=service_id,
                                        username=username,
                                        hosting_url=hosting_url,
                                        local_site=local_site)
        service = account.service

        if service.needs_authorization:
            try:
                service.authorize(request, username, password, hosting_url,
                                  local_site_name)
            except AuthorizationError as e:
                return HOSTINGSVC_AUTH_ERROR, {
                    'reason': six.text_type(e),
                }

        account.save()

        return 201, {
            self.item_result_key: account,
        }
    def create(self, request, username, service_id, password=None,
               hosting_url=None, local_site_name=None, *args, **kwargs):
        """Creates a hosting service account.

        The ``service_id`` is a registered HostingService ID. This must be
        known beforehand, and can be looked up in the Review Board
        administration UI.
        """
        local_site = self._get_local_site(local_site_name)

        if not HostingServiceAccount.objects.can_create(request.user,
                                                        local_site):
            return self._no_access_error(request.user)

        # Validate the service.
        service = get_hosting_service(service_id)

        if not service:
            return INVALID_FORM_DATA, {
                'fields': {
                    'service': ['This is not a valid service name'],
                }
            }

        if service.self_hosted and not hosting_url:
            return INVALID_FORM_DATA, {
                'fields': {
                    'hosting_url': ['This field is required'],
                }
            }

        account = HostingServiceAccount(service_name=service_id,
                                        username=username,
                                        hosting_url=hosting_url,
                                        local_site=local_site)
        service = account.service

        if service.needs_authorization:
            try:
                service.authorize(request, username, password, hosting_url,
                                  local_site_name)
            except AuthorizationError as e:
                return HOSTINGSVC_AUTH_ERROR, {
                    'reason': six.text_type(e),
                }

        account.save()

        return 201, {
            self.item_result_key: account,
        }
    def test_authorization(self):
        """Testing that ReviewBoardGateway authorization sends expected data"""
        http_post_data = {}

        def _http_post(self, *args, **kwargs):
            http_post_data['args'] = args
            http_post_data['kwargs'] = kwargs

            return json.dumps({'private_token': 'abc123'}), {}

        self.service_class._http_post = _http_post

        account = HostingServiceAccount(service_name=self.service_name,
                                        username='******')
        service = account.service

        self.spy_on(service.client.http_post, call_fake=_http_post)

        self.assertFalse(account.is_authorized)

        service.authorize('myuser',
                          'mypass',
                          hosting_url='https://example.com')
        self.assertTrue(account.is_authorized)

        self.assertEqual(http_post_data['kwargs']['url'],
                         'https://example.com/session')
        self.assertIn('username', http_post_data['kwargs'])
        self.assertIn('password', http_post_data['kwargs'])
Example #4
0
    def test_authorization(self):
        """Testing that GitLab account authorization sends expected data"""
        http_post_data = {}

        def _http_post(self, *args, **kwargs):
            http_post_data['args'] = args
            http_post_data['kwargs'] = kwargs

            return json.dumps({
                'id': 1,
                'private_token': 'abc123',
            }), {}

        account = HostingServiceAccount(service_name=self.service_name,
                                        username='******')
        service = account.service

        self.spy_on(service.client.http_post, call_fake=_http_post)

        self.assertFalse(account.is_authorized)

        service.authorize('myuser',
                          'mypass',
                          hosting_url='https://example.com')
        self.assertTrue(account.is_authorized)

        self.assertEqual(http_post_data['kwargs']['url'],
                         'https://example.com/api/v3/session')
        self.assertIn('fields', http_post_data['kwargs'])

        fields = http_post_data['kwargs']['fields']
        self.assertEqual(fields['login'], 'myuser')
        self.assertEqual(fields['password'], 'mypass')
Example #5
0
    def test_authorization(self):
        """Testing that GitHub account authorization sends expected data"""
        http_post_data = {}

        def _http_post(self, *args, **kwargs):
            http_post_data['args'] = args
            http_post_data['kwargs'] = kwargs

            return json.dumps({
                'id': 1,
                'url': 'https://api.github.com/authorizations/1',
                'scopes': ['user', 'repo'],
                'token': 'abc123',
                'note': '',
                'note_url': '',
                'updated_at': '2012-05-04T03:30:00Z',
                'created_at': '2012-05-04T03:30:00Z',
            }), {}

        account = HostingServiceAccount(service_name=self.service_name,
                                        username='******')
        service = account.service

        self.spy_on(service.client.http_post, call_fake=_http_post)

        self.assertFalse(account.is_authorized)

        service.authorize('myuser', 'mypass', None)
        self.assertTrue(account.is_authorized)

        self.assertEqual(http_post_data['kwargs']['url'],
                         'https://api.github.com/authorizations')
        self.assertEqual(http_post_data['kwargs']['username'], 'myuser')
        self.assertEqual(http_post_data['kwargs']['password'], 'mypass')
    def setUp(self):
        super(HostingServiceClientTests, self).setUp()

        account = HostingServiceAccount()
        service = HostingService(account)

        self.client = HostingServiceClient(service)
        self.client.http_request_cls = DummyHTTPRequest
Example #7
0
    def _get_hosting_account(self, use_url=False):
        if use_url:
            hosting_url = 'https://example.com'
        else:
            hosting_url = None

        return HostingServiceAccount(service_name=self.service_name,
                                     username='******',
                                     hosting_url=hosting_url)
Example #8
0
    def create_hosting_account(self, use_url=None, local_site=None,
                               data=None):
        """Create a hosting account to test with.

        Args:
            use_url (unicode, optional):
                Whether the account should be attached to a given hosting URL,
                for self-hosted services. If set, this will use
                ``https://example.com``.

            local_site (reviewboard.site.models.LocalSite, optional):
                A Local Site to attach the account to.

            data (dict, optional):
                Optional data to set for the account. If this is ``None``,
                :py:attr:`default_account_data` will be used.

        Returns:
            reviewboard.hostingsvcs.models.HostingServiceAccount:
            The new hosting service account.
        """
        if use_url is None:
            use_url = self.default_use_hosting_url

        if use_url:
            hosting_url = self.default_hosting_url
        else:
            hosting_url = None

        account = HostingServiceAccount(service_name=self.service_name,
                                        username=self.default_username,
                                        hosting_url=hosting_url,
                                        local_site=local_site)

        if data is not None:
            account.data = data
        else:
            account.data = self.default_account_data

        account.save()

        return account
Example #9
0
    def bug_tracker_service(self):
        """Returns selected bug tracker service if one exists."""
        if self.extra_data.get('bug_tracker_use_hosting'):
            return self.hosting_service
        else:
            bug_tracker_type = self.extra_data.get('bug_tracker_type')
            if bug_tracker_type:
                bug_tracker_cls = get_hosting_service(bug_tracker_type)

                # TODO: we need to figure out some way of storing a second
                # hosting service account for bug trackers.
                return bug_tracker_cls(HostingServiceAccount())

        return None
Example #10
0
    def _create_repository(self, github=True, with_local_site=False):
        """Create and return a repository for testing.

        Args:
            github (bool, optional):
                Whether the repository should use the GitHub hosting service.

        Returns:
            reviewboard.scmtools.models.Repository:
            A repository for use in unit tests.
        """
        if github:
            account = HostingServiceAccount(service_name='github',
                                            username='******')

            def _http_post_authorize(self, *args, **kwargs):
                return json.dumps({
                    'id': 1,
                    'url': 'https://api.github.com/authorizations/1',
                    'scopes': ['user', 'repo'],
                    'token': 'abc123',
                    'note': '',
                    'note_url': '',
                    'updated_at': '2012-05-04T03:30:00Z',
                    'created_at': '2012-05-04T03:30:00Z',
                }), {}

            service = account.service
            self.spy_on(service.client.http_post,
                        call_fake=_http_post_authorize)

            service.authorize('myuser', 'mypass', None)
            self.assertTrue(account.is_authorized)

            service.client.http_post.unspy()

            repository = self.create_repository(
                with_local_site=with_local_site)
            repository.hosting_account = account
            repository.extra_data['repository_plan'] = 'public-org'
            repository.extra_data['github_public_org_name'] = 'myorg'
            repository.extra_data['github_public_org_repo_name'] = 'myrepo'
            repository.save()
            return repository
        else:
            return self.create_repository()
Example #11
0
    def test_init_with_body_not_bytes(self):
        """Testing HostingServiceHTTPRequest construction with non-bytes body
        """
        account = HostingServiceAccount()
        service = HostingService(account)

        expected_message = (
            'Received non-bytes body for the HTTP request for %r. This is '
            'likely an implementation problem. Please make sure only byte '
            'strings are sent for the request body.' % HostingService)

        with self.assertRaisesMessage(TypeError, expected_message):
            HostingServiceHTTPRequest(
                url='http://example.com?z=1&z=2&baz=true',
                method='POST',
                body=123,
                hosting_service=service)
Example #12
0
    def test_init_with_header_value_not_unicode(self):
        """Testing HostingServiceHTTPRequest construction with non-Unicode
        header value
        """
        account = HostingServiceAccount()
        service = HostingService(account)

        expected_message = (
            'Received non-Unicode header %r (value=%r) for the HTTP request '
            'for %r. This is likely an implementation problem. Please make '
            'sure only Unicode strings are sent in request headers.' %
            ('My-Header', b'abc', HostingService))

        with self.assertRaisesMessage(TypeError, expected_message):
            HostingServiceHTTPRequest(
                url='http://example.com?z=1&z=2&baz=true',
                method='POST',
                headers={
                    'My-Header': b'abc',
                },
                hosting_service=service)
Example #13
0
    def test_authorization_with_client_info(self):
        """Testing that GitHub account authorization with registered client
        info
        """
        http_post_data = {}
        client_id = '<my client id>'
        client_secret = '<my client secret>'

        def _http_post(self, *args, **kwargs):
            http_post_data['args'] = args
            http_post_data['kwargs'] = kwargs

            return json.dumps({
                'id': 1,
                'url': 'https://api.github.com/authorizations/1',
                'scopes': ['user', 'repo'],
                'token': 'abc123',
                'note': '',
                'note_url': '',
                'updated_at': '2012-05-04T03:30:00Z',
                'created_at': '2012-05-04T03:30:00Z',
            }), {}

        account = HostingServiceAccount(service_name=self.service_name,
                                        username='******')
        service = account.service

        self.spy_on(service.client.http_post, call_fake=_http_post)

        self.assertFalse(account.is_authorized)

        with self.settings(GITHUB_CLIENT_ID=client_id,
                           GITHUB_CLIENT_SECRET=client_secret):
            service.authorize('myuser', 'mypass', None)

        self.assertTrue(account.is_authorized)

        body = json.loads(http_post_data['kwargs']['body'])
        self.assertEqual(body['client_id'], client_id)
        self.assertEqual(body['client_secret'], client_secret)
    def create(self, request, username, service_id, password=None,
               hosting_url=None, local_site_name=None, *args, **kwargs):
        local_site = self._get_local_site(local_site_name)

        if not HostingServiceAccount.objects.can_create(request.user,
                                                        local_site):
            return self._no_access_error(request.user)

        # Validate the service.
        service = get_hosting_service(service_id)

        if not service:
            return INVALID_FORM_DATA, {
                'fields': {
                    'service': ['This is not a valid service name'],
                }
            }

        if service.self_hosted and not hosting_url:
            return INVALID_FORM_DATA, {
                'fields': {
                    'hosting_url': ['This field is required'],
                }
            }

        account = HostingServiceAccount(service_name=service_id,
                                        username=username,
                                        hosting_url=hosting_url,
                                        local_site=local_site)
        service = account.service

        if service.needs_authorization:
            try:
                service.authorize(request, username, password, hosting_url,
                                  local_site_name)
            except AuthorizationError, e:
                return HOSTINGSVC_AUTH_ERROR, {
                    'reason': str(e),
                }
Example #15
0
    def bug_tracker_service(self):
        """The selected bug tracker service for the repository.

        This will be ``None`` if this repository is not associated with a bug
        tracker.

        Type:
            reviewboard.hostingsvcs.service.HostingService
        """
        if self.extra_data.get('bug_tracker_use_hosting'):
            return self.hosting_service

        bug_tracker_type = self.extra_data.get('bug_tracker_type')

        if bug_tracker_type:
            bug_tracker_cls = get_hosting_service(bug_tracker_type)

            # TODO: we need to figure out some way of storing a second
            # hosting service account for bug trackers.
            return bug_tracker_cls(HostingServiceAccount())

        return None
Example #16
0
    def create_hosting_account(self, use_url=None, local_site=None,
                               data=None):
        """Create a hosting account to test with.

        Args:
            use_url (unicode, optional):
                Whether the account should be attached to a given hosting URL,
                for self-hosted services. If set, this will use
                ``https://example.com``.

            local_site (reviewboard.site.models.LocalSite, optional):
                A Local Site to attach the account to.

            data (dict, optional):
                Optional data to set for the account. If this is ``None``,
                :py:attr:`default_account_data` will be used.

        Returns:
            reviewboard.hostingsvcs.models.HostingServiceAccount:
            The new hosting service account.
        """
        if use_url is None:
            use_url = self.default_use_hosting_url

        if use_url:
            hosting_url = self.default_hosting_url
        else:
            hosting_url = None

        account = HostingServiceAccount(service_name=self.service_name,
                                        username=self.default_username,
                                        hosting_url=hosting_url,
                                        local_site=local_site)

        if data is not None:
            account.data = data
        else:
            account.data = self.default_account_data

        account.save()

        return account
Example #17
0
    def save(self, allow_authorize=True, force_authorize=False,
             extra_authorize_kwargs=None, trust_host=False, save=True):
        """Save the hosting account and authorize against the service.

        This will create or update a hosting account, based on the information
        provided in the form and to this method.

        :py:meth:`is_valid` must be called prior to saving.

        Args:
            allow_authorize (bool, optional):
                If ``True`` (the default), the account will be authorized
                against the hosting service. If ``False``, only the database
                entry for the account will be affected.

            force_authorize (bool, optional):
                Force the account to be re-authorized, if already authorized.

            extra_authorize_kwargs (dict, optional):
                Additional keyword arguments to provide for the
                :py:meth:`HostingService.authorize()
                <reviewboard.hostingsvcs.models.HostingService.authorize>`
                call.

            trust_host (bool, optional):
                Whether to trust the given host, even if the linked certificate
                is invalid or self-signed.

            save (bool, optional):
                Whether or not the created account should be saved.

                This is intended to be used by subclasses who want to add
                additional data to the resulting hosting account before saving.

                If this is ``False``, the caller must ensure the resulting
                hosting account is saved.

        Returns:
            reviewboard.hostingsvcs.models.HostingServiceAccount:
            The updated or created hosting service account.

        Raises:
            reviewboard.hostingsvcs.errors.AuthorizationError:
                Information needed to authorize was missing, or authorization
                failed.

            reviewboard.hostingsvcs.errors.TwoFactorAuthCodeRequiredError:
                A two-factor authentication code is required to authorize the
                account. A code will need to be provided to the form.
        """
        if extra_authorize_kwargs is None:
            extra_authorize_kwargs = {}

        credentials = self.get_credentials()

        # Grab the username from the credentials, sanity-checking that it's
        # been provided as part of the get_credentials() result.
        try:
            username = credentials['username']
        except KeyError:
            logging.exception('%s.get_credentials() must return a "username" '
                              'key.',
                              self.__class__.__name__)

            raise AuthorizationError(
                ugettext('Hosting service implementation error: '
                         '%s.get_credentials() must return a "username" key.')
                % self.__class__.__name__)

        hosting_account = self.hosting_account
        hosting_service_id = self.hosting_service_cls.hosting_service_id
        hosting_url = self.cleaned_data.get('hosting_url')

        if not self.hosting_service_cls.self_hosted:
            assert hosting_url is None

        if hosting_account:
            # Update the username and hosting URL, if they've changed.
            hosting_account.username = username
            hosting_account.hosting_url = hosting_url
        else:
            # Fetch an existing hosting account based on the credentials and
            # parameters, if there is one. If not, we're going to create one,
            # but we won't save it until we've authorized.
            hosting_account_attrs = {
                'service_name': hosting_service_id,
                'username': username,
                'hosting_url': hosting_url,
                'local_site': self.local_site,
            }

            try:
                hosting_account = \
                    HostingServiceAccount.objects.get(**hosting_account_attrs)
            except HostingServiceAccount.DoesNotExist:
                # Create a new one, but don't save it yet.
                hosting_account = \
                    HostingServiceAccount(**hosting_account_attrs)

        if (allow_authorize and
            self.hosting_service_cls.needs_authorization and
            (not hosting_account.is_authorized or force_authorize)):
            # Attempt to authorize the account.
            if self.local_site:
                local_site_name = self.local_site.name
            else:
                local_site_name = None

            password = credentials.get('password')
            two_factor_auth_code = credentials.get('two_factor_auth_code')

            authorize_kwargs = dict({
                'username': username,
                'password': password,
                'hosting_url': hosting_url,
                'two_factor_auth_code': two_factor_auth_code,
                'local_site_name': local_site_name,
                'credentials': credentials,
            }, **extra_authorize_kwargs)

            try:
                self.authorize(hosting_account, hosting_service_id,
                               **authorize_kwargs)
            except UnverifiedCertificateError as e:
                if trust_host:
                    hosting_account.accept_certificate(e.certificate)
                    self.authorize(hosting_account, hosting_service_id,
                                   **authorize_kwargs)
                else:
                    raise

        if save:
            hosting_account.save()

        return hosting_account
Example #18
0
 def _get_hosting_account(self):
     return HostingServiceAccount(service_name=self.service_name,
                                  username='******')
Example #19
0
    def _clean_hosting_info(self):
        """Clean the hosting service information.

        If using a hosting service, this will validate that the data
        provided is valid on that hosting service. Then it will create an
        account and link it, if necessary, with the hosting service.
        """
        hosting_type = self.cleaned_data['hosting_type']

        if hosting_type == self.NO_HOSTING_SERVICE_ID:
            self.data['hosting_account'] = None
            self.cleaned_data['hosting_account'] = None
            return

        # This should have been caught during validation, so we can assume
        # it's fine.
        hosting_service_cls = get_hosting_service(hosting_type)
        assert hosting_service_cls

        # Validate that the provided tool is valid for the hosting service.
        tool_name = self.cleaned_data['tool'].name

        if tool_name not in hosting_service_cls.supported_scmtools:
            self.errors['tool'] = self.error_class([
                _('This tool is not supported on the given hosting service')
            ])
            return

        # Now make sure all the account info is correct.
        hosting_account = self.cleaned_data['hosting_account']
        username = self.cleaned_data['hosting_account_username']
        password = self.cleaned_data['hosting_account_password']

        if hosting_service_cls.self_hosted:
            hosting_url = self.cleaned_data['hosting_url'] or None
        else:
            hosting_url = None

        if hosting_account and hosting_account.hosting_url != hosting_url:
            self.errors['hosting_account'] = self.error_class([
                _('This account is not compatible with this hosting service '
                  'configuration'),
            ])
            return
        elif hosting_account and not username:
            username = hosting_account.username
        elif not hosting_account and not username:
            self.errors['hosting_account'] = self.error_class([
                _('An account must be linked in order to use this hosting '
                  'service'),
            ])
            return

        if not hosting_account:
            # See if this account with the supplied credentials already
            # exists. If it does, we don't want to create a new entry.
            try:
                hosting_account = HostingServiceAccount.objects.get(
                    service_name=hosting_type,
                    username=username,
                    hosting_url=hosting_url,
                    local_site=self.local_site)
            except HostingServiceAccount.DoesNotExist:
                # That's fine. We're just going to create it later.
                pass

        plan = self.cleaned_data['repository_plan'] or self.DEFAULT_PLAN_ID

        # Set the main repository fields (Path, Mirror Path, etc.) based on
        # the field definitions in the hosting service.
        #
        # This will take into account the hosting service's form data for
        # the given repository plan, the main form data, and the hosting
        # account information.
        #
        # It's expected that the required fields will have validated by now.
        repository_form = self.repository_forms[hosting_type][plan]
        field_vars = repository_form.cleaned_data.copy()
        field_vars.update(self.cleaned_data)

        # If the hosting account needs to authorize and link with an external
        # service, attempt to do so and watch for any errors.
        #
        # If it doesn't need to link with it, we'll just create an entry
        # with the username and save it.
        if not hosting_account:
            hosting_account = HostingServiceAccount(
                service_name=hosting_type,
                username=username,
                hosting_url=hosting_url,
                local_site=self.local_site)

        if (hosting_service_cls.needs_authorization and
            not hosting_account.is_authorized):
            try:
                hosting_account.service.authorize(
                    username, password,
                    hosting_url,
                    local_site_name=self.local_site_name)
            except AuthorizationError as e:
                self.errors['hosting_account'] = self.error_class([
                    _('Unable to link the account: %s') % e,
                ])
                return
            except Exception as e:
                self.errors['hosting_account'] = self.error_class([
                    _('Unknown error when linking the account: %s') % e,
                ])
                return

            # Flag that we've linked the account. If there are any
            # validation errors, and this flag is set, we tell the user
            # that we successfully linked and they don't have to do it
            # again.
            self.hosting_account_linked = True
            hosting_account.save()

        self.data['hosting_account'] = hosting_account
        self.cleaned_data['hosting_account'] = hosting_account

        try:
            self.cleaned_data.update(hosting_service_cls.get_repository_fields(
                hosting_account.username, hosting_account.hosting_url, plan,
                tool_name, field_vars))
        except KeyError as e:
            raise ValidationError([unicode(e)])
Example #20
0
    def _clean_hosting_info(self):
        """Clean the hosting service information.

        If using a hosting service, this will validate that the data
        provided is valid on that hosting service. Then it will create an
        account and link it, if necessary, with the hosting service.
        """
        hosting_type = self.cleaned_data['hosting_type']

        if hosting_type == self.NO_HOSTING_SERVICE_ID:
            return

        # This should have been caught during validation, so we can assume
        # it's fine.
        hosting_service_cls = get_hosting_service(hosting_type)
        assert hosting_service_cls

        # Validate that the provided tool is valid for the hosting service.
        tool_name = self.cleaned_data['tool'].name

        if tool_name not in hosting_service_cls.supported_scmtools:
            self.errors['tool'] = self.error_class(
                [_('This tool is not supported on the given hosting service')])
            return

        # Now make sure all the account info is correct.
        hosting_account = self.cleaned_data['hosting_account']
        username = self.cleaned_data['hosting_account_username']
        password = self.cleaned_data['hosting_account_password']

        if hosting_account and not username:
            username = hosting_account.username
        elif not hosting_account and not username:
            self.errors['hosting_account'] = self.error_class([
                _('An account must be linked in order to use this hosting '
                  'service'),
            ])
            return

        if not hosting_account:
            # See if this account with the supplied credentials already
            # exists. If it does, we don't want to create a new entry.
            try:
                hosting_account = HostingServiceAccount.objects.get(
                    service_name=hosting_type,
                    username=username,
                    local_site=self.local_site)
            except HostingServiceAccount.DoesNotExist:
                # That's fine. We're just going to create it later.
                pass

        # If the hosting account needs to authorize and link with an external
        # service, attempt to do so and watch for any errors.
        #
        # If it doesn't need to link with it, we'll just create an entry
        # with the username and save it.
        if not hosting_account:
            hosting_account = HostingServiceAccount(service_name=hosting_type,
                                                    username=username,
                                                    local_site=self.local_site)

        if (hosting_service_cls.needs_authorization
                and not hosting_account.is_authorized):
            try:
                hosting_account.service.authorize(
                    username, password, local_site_name=self.local_site_name)
            except AuthorizationError, e:
                self.errors['hosting_account'] = self.error_class([
                    _('Unable to link the account: %s') % e,
                ])
                return
            except Exception, e:
                self.errors['hosting_account'] = self.error_class([
                    _('Unknown error when linking the account: %s') % e,
                ])
                return
Example #21
0
    def save(self,
             allow_authorize=True,
             force_authorize=False,
             extra_authorize_kwargs=None,
             trust_host=False,
             save=True):
        """Save the hosting account and authorize against the service.

        This will create or update a hosting account, based on the information
        provided in the form and to this method.

        :py:meth:`is_valid` must be called prior to saving.

        Args:
            allow_authorize (bool, optional):
                If ``True`` (the default), the account will be authorized
                against the hosting service. If ``False``, only the database
                entry for the account will be affected.

            force_authorize (bool, optional):
                Force the account to be re-authorized, if already authorized.

            extra_authorize_kwargs (dict, optional):
                Additional keyword arguments to provide for the
                :py:meth:`HostingService.authorize()
                <reviewboard.hostingsvcs.models.HostingService.authorize>`
                call.

            trust_host (bool, optional):
                Whether to trust the given host, even if the linked certificate
                is invalid or self-signed.

            save (bool, optional):
                Whether or not the created account should be saved.

                This is intended to be used by subclasses who want to add
                additional data to the resulting hosting account before saving.

                If this is ``False``, the caller must ensure the resulting
                hosting account is saved.

        Returns:
            reviewboard.hostingsvcs.models.HostingServiceAccount:
            The updated or created hosting service account.

        Raises:
            reviewboard.hostingsvcs.errors.AuthorizationError:
                Information needed to authorize was missing, or authorization
                failed.

            reviewboard.hostingsvcs.errors.TwoFactorAuthCodeRequiredError:
                A two-factor authentication code is required to authorize the
                account. A code will need to be provided to the form.
        """
        if extra_authorize_kwargs is None:
            extra_authorize_kwargs = {}

        credentials = self.get_credentials()

        # Grab the username from the credentials, sanity-checking that it's
        # been provided as part of the get_credentials() result.
        try:
            username = credentials['username']
        except KeyError:
            logging.exception(
                '%s.get_credentials() must return a "username" '
                'key.', self.__class__.__name__)

            raise AuthorizationError(
                ugettext('Hosting service implementation error: '
                         '%s.get_credentials() must return a "username" key.')
                % self.__class__.__name__)

        hosting_account = self.hosting_account
        hosting_service_id = self.hosting_service_cls.hosting_service_id
        hosting_url = self.cleaned_data.get('hosting_url')

        if not self.hosting_service_cls.self_hosted:
            assert hosting_url is None

        if hosting_account:
            # Update the username and hosting URL, if they've changed.
            hosting_account.username = username
            hosting_account.hosting_url = hosting_url
        else:
            # Fetch an existing hosting account based on the credentials and
            # parameters, if there is one. If not, we're going to create one,
            # but we won't save it until we've authorized.
            hosting_account_attrs = {
                'service_name': hosting_service_id,
                'username': username,
                'hosting_url': hosting_url,
                'local_site': self.local_site,
            }

            try:
                hosting_account = \
                    HostingServiceAccount.objects.get(**hosting_account_attrs)
            except HostingServiceAccount.DoesNotExist:
                # Create a new one, but don't save it yet.
                hosting_account = \
                    HostingServiceAccount(**hosting_account_attrs)

        if (allow_authorize and self.hosting_service_cls.needs_authorization
                and (not hosting_account.is_authorized or force_authorize)):
            # Attempt to authorize the account.
            if self.local_site:
                local_site_name = self.local_site.name
            else:
                local_site_name = None

            password = credentials.get('password')
            two_factor_auth_code = credentials.get('two_factor_auth_code')

            authorize_kwargs = dict(
                {
                    'username': username,
                    'password': password,
                    'hosting_url': hosting_url,
                    'two_factor_auth_code': two_factor_auth_code,
                    'local_site_name': local_site_name,
                    'credentials': credentials,
                }, **extra_authorize_kwargs)

            try:
                self.authorize(hosting_account, hosting_service_id,
                               **authorize_kwargs)
            except UnverifiedCertificateError as e:
                if trust_host:
                    hosting_account.accept_certificate(e.certificate)
                    self.authorize(hosting_account, hosting_service_id,
                                   **authorize_kwargs)
                else:
                    raise

        if save:
            hosting_account.save()

        return hosting_account
Example #22
0
    def save(self,
             allow_authorize=True,
             force_authorize=False,
             extra_authorize_kwargs={}):
        """Save the hosting account and authorize against the service.

        This will create or update a hosting account, based on the information
        provided in the form and to this method.

        :py:meth:`is_valid` must be called prior to saving.

        Args:
            allow_authorize (bool, optional):
                If ``True`` (the default), the account will be authorized
                against the hosting service. If ``False``, only the database
                entry for the account will be affected.

            force_authorize (bool, optional):
                Force the account to be re-authorized, if already authorized.

            extra_authorize_kwargs (dict, optional):
                Additional keyword arguments to provide for the
                :py:meth:`HostingService.authorize()
                <reviewboard.hostingsvcs.models.HostingService.authorize>`
                call.

        Returns:
            reviewboard.hostingsvcs.models.HostingServiceAccount:
            The updated or created hosting service account.

        Raises:
            reviewboard.hostingsvcs.errors.AuthorizationError:
                Information needed to authorize was missing, or authorziation
                failed.

            reviewboard.hostingsvcs.errors.TwoFactorAuthCodeRequiredError:
                A two-factor authentication code is required to authorize the
                account. A code will need to be provided to the form.
        """
        credentials = self.get_credentials()

        # Grab the username from the credentials, sanity-checking that it's
        # been provided as part of the get_credentials() result.
        try:
            username = credentials['username']
        except KeyError:
            logging.exception(
                '%s.get_credentials() must return a "username" '
                'key.', self.__class__.__name__)

            raise AuthorizationError(
                ugettext('Hosting service implementation error: '
                         '%s.get_credentials() must return a "username" key.')
                % self.__class__.__name__)

        hosting_account = self.hosting_account
        hosting_service_id = self.hosting_service_cls.hosting_service_id
        hosting_url = self.cleaned_data.get('hosting_url')

        if not self.hosting_service_cls.self_hosted:
            assert hosting_url is None

        if hosting_account:
            # Update the username and hosting URL, if they've changed.
            hosting_account.username = username
            hosting_account.hosting_url = hosting_url
        else:
            # Fetch an existing hosting account based on the credentials and
            # parameters, if there is one. If not, we're going to create one,
            # but we won't save it until we've authorized.
            hosting_account_attrs = {
                'service_name': hosting_service_id,
                'username': username,
                'hosting_url': hosting_url,
                'local_site': self.local_site,
            }

            try:
                hosting_account = \
                    HostingServiceAccount.objects.get(**hosting_account_attrs)
            except HostingServiceAccount.DoesNotExist:
                # Create a new one, but don't save it yet.
                hosting_account = \
                    HostingServiceAccount(**hosting_account_attrs)

        if (allow_authorize and self.hosting_service_cls.needs_authorization
                and (not hosting_account.is_authorized or force_authorize)):
            # Attempt to authorize the account.
            if self.local_site:
                local_site_name = self.local_site.name
            else:
                local_site_name = None

            password = credentials.get('password')
            two_factor_auth_code = credentials.get('two_factor_auth_code')

            try:
                hosting_account.service.authorize(
                    username=username,
                    password=password,
                    hosting_url=hosting_url,
                    two_factor_auth_code=two_factor_auth_code,
                    local_site_name=local_site_name,
                    credentials=credentials,
                    **extra_authorize_kwargs)
            except TwoFactorAuthCodeRequiredError:
                # Mark this asrequired for the next form render.
                self.fields['hosting_account_two_factor_auth_code']\
                    .required = True

                # Re-raise the error.
                raise
            except AuthorizationError:
                logging.exception(
                    'Authorization error linking hosting '
                    'account ID=%r for hosting service=%r, '
                    'username=%r, LocalSite=%r', hosting_account.pk,
                    hosting_service_id, username, local_site_name)

                # Re-raise the error.
                raise
            except Exception:
                logging.exception(
                    'Unknown error linking hosting account '
                    'ID=%r for hosting service=%r, '
                    'username=%r, LocalSite=%r', hosting_account.pk,
                    hosting_service_id, username, local_site_name)

                # Re-raise the error.
                raise

        hosting_account.save()

        return hosting_account
Example #23
0
    def _clean_hosting_info(self):
        """Clean the hosting service information.

        If using a hosting service, this will validate that the data
        provided is valid on that hosting service. Then it will create an
        account and link it, if necessary, with the hosting service.
        """
        hosting_type = self.cleaned_data['hosting_type']

        if hosting_type == self.NO_HOSTING_SERVICE_ID:
            self.data['hosting_account'] = None
            self.cleaned_data['hosting_account'] = None
            return

        # This should have been caught during validation, so we can assume
        # it's fine.
        hosting_service_cls = get_hosting_service(hosting_type)
        assert hosting_service_cls

        # Validate that the provided tool is valid for the hosting service.
        tool_name = self.cleaned_data['tool'].name

        if tool_name not in hosting_service_cls.supported_scmtools:
            self.errors['tool'] = self.error_class([
                _('This tool is not supported on the given hosting service')
            ])
            return

        # Now make sure all the account info is correct.
        hosting_account = self.cleaned_data['hosting_account']
        username = self.cleaned_data['hosting_account_username']
        password = self.cleaned_data['hosting_account_password']

        if hosting_service_cls.self_hosted:
            hosting_url = self.cleaned_data['hosting_url'] or None
        else:
            hosting_url = None

        if hosting_service_cls.supports_two_factor_auth:
            two_factor_auth_code = \
                self.cleaned_data['hosting_account_two_factor_auth_code']
        else:
            two_factor_auth_code = None

        if hosting_account and hosting_account.hosting_url != hosting_url:
            self.errors['hosting_account'] = self.error_class([
                _('This account is not compatible with this hosting service '
                  'configuration'),
            ])
            return
        elif hosting_account and not username:
            username = hosting_account.username
        elif not hosting_account and not username:
            self.errors['hosting_account'] = self.error_class([
                _('An account must be linked in order to use this hosting '
                  'service'),
            ])
            return

        if not hosting_account:
            # See if this account with the supplied credentials already
            # exists. If it does, we don't want to create a new entry.
            try:
                hosting_account = HostingServiceAccount.objects.get(
                    service_name=hosting_type,
                    username=username,
                    hosting_url=hosting_url,
                    local_site=self.local_site)
            except HostingServiceAccount.DoesNotExist:
                # That's fine. We're just going to create it later.
                pass

        plan = self.cleaned_data['repository_plan'] or self.DEFAULT_PLAN_ID

        # Set the main repository fields (Path, Mirror Path, etc.) based on
        # the field definitions in the hosting service.
        #
        # This will take into account the hosting service's form data for
        # the given repository plan, the main form data, and the hosting
        # account information.
        #
        # It's expected that the required fields will have validated by now.
        repository_form = self.repository_forms[hosting_type][plan]
        field_vars = repository_form.cleaned_data.copy()
        field_vars.update(self.cleaned_data)

        # If the hosting account needs to authorize and link with an external
        # service, attempt to do so and watch for any errors.
        #
        # If it doesn't need to link with it, we'll just create an entry
        # with the username and save it.
        if not hosting_account:
            hosting_account = HostingServiceAccount(
                service_name=hosting_type,
                username=username,
                hosting_url=hosting_url,
                local_site=self.local_site)

        if (hosting_service_cls.needs_authorization and
            not hosting_account.is_authorized):
            # Attempt to authorize the account.
            hosting_service = None
            plan = None

            if hosting_service_cls:
                hosting_service = hosting_service_cls(hosting_account)

                if hosting_service:
                    plan = (self.cleaned_data['repository_plan'] or
                            self.DEFAULT_PLAN_ID)

            repository_extra_data = self._build_repository_extra_data(
                hosting_service, hosting_type, plan)

            try:
                hosting_account.service.authorize(
                    username, password,
                    hosting_url=hosting_url,
                    two_factor_auth_code=two_factor_auth_code,
                    tool_name=tool_name,
                    local_site_name=self.local_site_name,
                    **repository_extra_data)
            except TwoFactorAuthCodeRequiredError as e:
                self.errors['hosting_account'] = \
                    self.error_class([six.text_type(e)])
                hosting_info = self.hosting_service_info[hosting_type]
                hosting_info['needs_two_factor_auth_code'] = True
                return
            except AuthorizationError as e:
                self.errors['hosting_account'] = self.error_class([
                    _('Unable to link the account: %s') % e,
                ])
                return
            except Exception as e:
                self.errors['hosting_account'] = self.error_class([
                    _('Unknown error when linking the account: %s') % e,
                ])
                return

            # Flag that we've linked the account. If there are any
            # validation errors, and this flag is set, we tell the user
            # that we successfully linked and they don't have to do it
            # again.
            self.hosting_account_linked = True
            hosting_account.save()

        self.data['hosting_account'] = hosting_account
        self.cleaned_data['hosting_account'] = hosting_account

        try:
            self.cleaned_data.update(hosting_service_cls.get_repository_fields(
                hosting_account.username, hosting_account.hosting_url, plan,
                tool_name, field_vars))
        except KeyError as e:
            raise ValidationError([six.text_type(e)])
Example #24
0
    def save(self, allow_authorize=True, extra_authorize_kwargs={}):
        """Save the hosting account and authorize against the service.

        This will create or update a hosting account, based on the information
        provided in the form and to this method.

        :py:meth:`is_valid` must be called prior to saving.

        Args:
            allow_authorize (bool, optional):
                If ``True`` (the default), the account will be authorized
                against the hosting service. If ``False``, only the database
                entry for the account will be affected.

            extra_authorize_kwargs (dict, optional):
                Additional keyword arguments to provide for the
                :py:meth:`HostingService.authorize()
                <reviewboard.hostingsvcs.models.HostingService.authorize>`
                call.

        Returns:
            reviewboard.hostingsvcs.models.HostingServiceAccount:
            The updated or created hosting service account.

        Raises:
            reviewboard.hostingsvcs.errors.AuthorizationError:
                Information needed to authorize was missing, or authorziation
                failed.

            reviewboard.hostingsvcs.errors.TwoFactorAuthCodeRequiredError:
                A two-factor authentication code is required to authorize the
                account. A code will need to be provided to the form.
        """
        credentials = self.get_credentials()

        # Grab the username from the credentials, sanity-checking that it's
        # been provided as part of the get_credentials() result.
        try:
            username = credentials['username']
        except KeyError:
            logging.exception('%s.get_credentials() must return a "username" '
                              'key.',
                              self.__class__.__name__)

            raise AuthorizationError(
                ugettext('Hosting service implementation error: '
                         '%s.get_credentials() must return a "username" key.')
                % self.__class__.__name__)

        hosting_account = self.hosting_account
        hosting_service_id = self.hosting_service_cls.hosting_service_id
        hosting_url = self.cleaned_data.get('hosting_url')

        if not self.hosting_service_cls.self_hosted:
            assert hosting_url is None

        if hosting_account:
            # Update the username and hosting URL, if they've changed.
            hosting_account.username = username
            hosting_account.hosting_url = hosting_url
        else:
            # Fetch an existing hosting account based on the credentials and
            # parameters, if there is one. If not, we're going to create one,
            # but we won't save it until we've authorized.
            hosting_account_attrs = {
                'service_name': hosting_service_id,
                'username': username,
                'hosting_url': hosting_url,
                'local_site': self.local_site,
            }

            try:
                hosting_account = \
                    HostingServiceAccount.objects.get(**hosting_account_attrs)
            except HostingServiceAccount.DoesNotExist:
                # Create a new one, but don't save it yet.
                hosting_account = \
                    HostingServiceAccount(**hosting_account_attrs)

        if (allow_authorize and
            self.hosting_service_cls.needs_authorization and
            not hosting_account.is_authorized):
            # Attempt to authorize the account.
            if self.local_site:
                local_site_name = self.local_site.name
            else:
                local_site_name = None

            password = credentials.get('password')
            two_factor_auth_code = credentials.get('two_factor_auth_code')

            try:
                hosting_account.service.authorize(
                    username=username,
                    password=password,
                    hosting_url=hosting_url,
                    two_factor_auth_code=two_factor_auth_code,
                    local_site_name=local_site_name,
                    credentials=credentials,
                    **extra_authorize_kwargs)
            except TwoFactorAuthCodeRequiredError:
                # Mark this asrequired for the next form render.
                self.fields['hosting_account_two_factor_auth_code']\
                    .required = True

                # Re-raise the error.
                raise
            except AuthorizationError:
                logging.exception('Authorization error linking hosting '
                                  'account ID=%r for hosting service=%r, '
                                  'username=%r, LocalSite=%r',
                                  hosting_account.pk, hosting_service_id,
                                  username, local_site_name)

                # Re-raise the error.
                raise
            except Exception:
                logging.exception('Unknown error linking hosting account '
                                  'ID=%r for hosting service=%r, '
                                  'username=%r, LocalSite=%r',
                                  hosting_account.pk, hosting_service_id,
                                  username, local_site_name)

                # Re-raise the error.
                raise

        hosting_account.save()

        return hosting_account