Пример #1
0
    def query_users(self, query, request):
        if not query:
            return

        try:
            bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        except BugzillaError as e:
            raise UserQueryError('Bugzilla error: %s' % e.msg)

        try:
            # We don't want to auto populate just any user because Bugzilla has
            # over 300,000 users and most of them aren't relevant to MozReview.
            #
            # So, we only auto import users if they have IRC nick syntax or
            # if the search matches them exactly.
            def user_relevant(u):
                if BZ_IRCNICK_RE.search(u['real_name']):
                    return True
                if u['email'] == query:
                    return True

                # This might allow too many users through. Let's not get too
                # attached to this.
                if u['real_name'] == query:
                    return True

                return False

            users = bugzilla.query_users(query)
            users['users'] = [u for u in users['users'] if user_relevant(u)]
            get_or_create_bugzilla_users(users)
        except BugzillaError as e:
            raise UserQueryError('Bugzilla error: %s' % e.msg)
Пример #2
0
def on_reply_publishing(user, reply, **kwargs):
    review_request = reply.review_request

    # skip review requests that were not pushed
    if not is_review_request_pushed(review_request):
        return

    bug_id = int(review_request.get_bug_list()[0])
    b = Bugzilla(get_bugzilla_api_key(user))

    url = get_reply_url(reply)
    comment = build_plaintext_review(reply, url, {"user": user})
    b.post_comment(bug_id, comment)
Пример #3
0
def on_reply_publishing(user, reply, **kwargs):
    review_request = reply.review_request
    logger.info('Posting bugzilla reply for review request %s' %
                (review_request.id))

    # skip review requests that were not pushed
    if not is_pushed(review_request):
        return

    bug_id = int(review_request.get_bug_list()[0])
    b = Bugzilla(get_bugzilla_api_key(user))

    url = get_reply_url(reply)
    comment = build_plaintext_review(reply, url, {"user": user})
    b.post_comment(bug_id, comment, get_diff_url(review_request), True)
Пример #4
0
def on_review_request_closed_discarded(user, review_request, type, **kwargs):
    if type != ReviewRequest.DISCARDED:
        return

    commit_data = fetch_commit_data(review_request)

    if is_parent(review_request, commit_data):
        # close_child_review_requests will call save on this review request, so
        # we don't have to worry about it.
        review_request.commit = None

        _close_child_review_requests(user,
                                     review_request,
                                     ReviewRequest.DISCARDED,
                                     AUTO_CLOSE_DESCRIPTION,
                                     commit_data=commit_data)
    else:
        # TODO: Remove this once we properly prevent users from closing
        # commit review requests.
        b = Bugzilla(get_bugzilla_api_key(user))
        bug = int(review_request.get_bug_list()[0])
        attachment_updates = BugzillaAttachmentUpdates(b, bug)
        attachment_updates.obsolete_review_attachments(
            get_diff_url(review_request))
        attachment_updates.do_updates()
Пример #5
0
def on_review_request_closed_discarded(user, review_request, type, **kwargs):
    if type != ReviewRequest.DISCARDED:
        return

    if is_review_request_squashed(review_request):
        # close_child_review_requests will call save on this review request, so
        # we don't have to worry about it.
        review_request.commit = None

        close_child_review_requests(user, review_request,
                                    ReviewRequest.DISCARDED,
                                    AUTO_CLOSE_DESCRIPTION)
    else:
        # TODO: Remove this once we properly prevent users from closing
        # commit review requests.
        b = Bugzilla(get_bugzilla_api_key(user))
        bug = int(review_request.get_bug_list()[0])
        url = get_obj_url(review_request)
        b.obsolete_review_attachments(bug, url)
def on_review_request_closed_discarded(user, review_request, type, **kwargs):
    if type != ReviewRequest.DISCARDED:
        return

    commit_data = fetch_commit_data(review_request)

    if is_parent(review_request, commit_data):
        # close_child_review_requests will call save on this review request, so
        # we don't have to worry about it.
        review_request.commit = None

        _close_child_review_requests(user, review_request,
                                     ReviewRequest.DISCARDED,
                                     AUTO_CLOSE_DESCRIPTION,
                                     commit_data=commit_data)
    else:
        # TODO: Remove this once we properly prevent users from closing
        # commit review requests.
        b = Bugzilla(get_bugzilla_api_key(user))
        bug = int(review_request.get_bug_list()[0])
        diff_url = '%sdiff/#index_header' % get_obj_url(review_request)
        b.obsolete_review_attachments(bug, diff_url)
Пример #7
0
    def get_or_create_user(self, username, request):
        """Always check Bugzilla for updates."""
        username = username.strip()

        try:
            bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        except BugzillaUrlError:
            return None
        except BugzillaError:
            raise PermissionDenied

        user_data = bugzilla.get_user(username)

        if not user_data:
            raise self.bz_error_response(request)

        # Just store the results.
        get_or_create_bugzilla_users(user_data)

        try:
            return User.objects.get(username=username)
        except User.DoesNotExist:
            return None
Пример #8
0
    def get_or_create_user(self, username, request):
        """Always check Bugzilla for updates."""
        username = username.strip()

        try:
            bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        except BugzillaUrlError:
            return None
        except BugzillaError:
            raise PermissionDenied

        user_data = bugzilla.get_user(username)

        if not user_data:
            raise self.bz_error_response(request)

        # Just store the results.
        get_or_create_bugzilla_users(user_data)

        try:
            return User.objects.get(username=username)
        except User.DoesNotExist:
            return None
    def create(self, request, reviewers, *args, **kwargs):
        bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        new_reviewers = [u.strip() for u in reviewers.split(',') if u.strip()]
        invalid_reviewers = []
        for reviewer in new_reviewers:
            try:
                bugzilla.get_user_from_irc_nick(reviewer)
            except User.DoesNotExist:
                invalid_reviewers.append(reviewer)

        if invalid_reviewers:
            # Because this isn't called through Review Board's built-in
            # backbone system, it's dramatically simpler to return just the
            # intended error message instead of categorising the errors by
            # field.
            if len(invalid_reviewers) == 1:
                return INVALID_FORM_DATA.with_message(
                    "The reviewer '%s' was not found" % invalid_reviewers[0])
            else:
                return INVALID_FORM_DATA.with_message(
                    "The reviewers '%s' were not found"
                    % "', '".join(invalid_reviewers))

        return 200, {}
Пример #10
0
    def query_users(self, query, request):
        if not query:
            return

        try:
            bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        except BugzillaError as e:
            raise UserQueryError('Bugzilla error: %s' % e.msg)

        try:
            # We don't want to auto populate just any user because Bugzilla has
            # over 300,000 users and most of them aren't relevant to MozReview.
            #
            # So, we only auto import users if they have IRC nick syntax or
            # if the search matches them exactly.
            def user_relevant(u):
                if BMO_IRC_NICK_RE.search(u['real_name']):
                    return True
                if u['email'] == query:
                    return True

                # This might allow too many users through. Let's not get too
                # attached to this.
                if u['real_name'] == query:
                    return True

                return False

            users = bugzilla.query_users(query)
            users['users'] = [u for u in users['users'] if user_relevant(u)]
            logger.info(
                'importing Bugzilla users from query "%s": %s' %
                (query, ', '.join(sorted(u['email'] for u in users['users']))))
            get_or_create_bugzilla_users(users)
        except BugzillaError as e:
            raise UserQueryError('Bugzilla error: %s' % e.msg)
def on_review_publishing(user, review, **kwargs):
    """Comment in the bug and potentially r+ or clear a review flag.

    Note that a reviewer *must* have editbugs to set an attachment flag on
    someone else's attachment (i.e. the standard BMO review process).

    TODO: Report lack-of-editbugs properly; see bug 1119065.
    """
    review_request = review.review_request
    logger.info('Publishing review for user: %s review id: %s '
                'review request id: %s' % (user, review.id,
                                            review_request.id))

    # skip review requests that were not pushed
    if not is_pushed(review_request):
        logger.info('Did not publish review: %s: for user: %d: review not '
                    'pushed.' % (user, review.id))
        return

    site = Site.objects.get_current()
    siteconfig = SiteConfiguration.objects.get_current()
    comment = build_plaintext_review(review,
                                     get_obj_url(review, site,
                                                 siteconfig),
                                     {"user": user})
    b = Bugzilla(get_bugzilla_api_key(user))

    # TODO: Update all attachments in one call.  This is not possible right
    # now because we have to potentially mix changing and creating flags.

    if is_parent(review_request):
        # Mirror the comment to the bug, unless it's a ship-it, in which
        # case throw an error.  Ship-its are allowed only on child commits.
        if review.ship_it:
            raise ParentShipItError

        [b.post_comment(int(bug_id), comment) for bug_id in
         review_request.get_bug_list()]
    else:
        diff_url = '%sdiff/#index_header' % get_obj_url(review_request)
        bug_id = int(review_request.get_bug_list()[0])

        if review.ship_it:
            commented = b.r_plus_attachment(bug_id, review.user.email,
                                            diff_url, comment)
        else:
            commented = b.cancel_review_request(bug_id, review.user.email,
                                                diff_url, comment)

        if comment and not commented:
            b.post_comment(bug_id, comment)
def on_review_request_publishing(user, review_request_draft, **kwargs):
    # There have been strange cases (all local, and during development), where
    # when attempting to publish a review request, this handler will fail
    # because the draft does not exist. This is a really strange case, and not
    # one we expect to happen in production. However, since we've seen it
    # locally, we handle it here, and log.
    if not review_request_draft:
        logger.error('Strangely, there was no review request draft on the '
                     'review request we were attempting to publish.')
        return

    # If the review request draft has a new DiffSet we will only allow
    # publishing if that DiffSet has been verified. It is important to
    # do this for every review request, not just pushed ones, because
    # we can't trust the storage mechanism which indicates it was pushed.
    # TODO: This will be fixed when we transition away from extra_data.
    if review_request_draft.diffset:
        try:
            DiffSetVerification.objects.get(
                diffset=review_request_draft.diffset)
        except DiffSetVerification.DoesNotExist:
            logger.error(
                'An attempt was made by User %s to publish an unverified '
                'DiffSet with id %s',
                user.id,
                review_request_draft.diffset.id)

            raise PublishError(
                'This review request draft contained a manually uploaded '
                'diff, which is prohibited. Please push to the review server '
                'to create review requests. If you believe you received this '
                'message in error, please file a bug.')

    review_request = review_request_draft.get_review_request()
    commit_data = fetch_commit_data(review_request)

    # skip review requests that were not pushed
    if not is_pushed(review_request, commit_data=commit_data):
        return

    if not is_parent(review_request, commit_data):
        # Send a signal asking for approval to publish this review request.
        # We only want to publish this commit request if we are in the middle
        # of publishing the parent. If the parent is publishing it will be
        # listening for this signal to approve it.
        approvals = commit_request_publishing.send_robust(
            sender=review_request,
            user=user,
            review_request_draft=review_request_draft)

        for receiver, approved in approvals:
            if approved:
                break
        else:
            # This publish is not approved by the parent review request.
            raise CommitPublishProhibited()

    # The reviewid passed through p2rb is, for Mozilla's instance anyway,
    # bz://<bug id>/<irc nick>.
    reviewid = commit_data.draft_extra_data.get(IDENTIFIER_KEY, None)
    m = REVIEWID_RE.match(reviewid)

    if not m:
        raise InvalidBugIdError('<unknown>')

    bug_id = m.group(1)

    try:
        bug_id = int(bug_id)
    except (TypeError, ValueError):
        raise InvalidBugIdError(bug_id)

    siteconfig = SiteConfiguration.objects.get_current()
    using_bugzilla = (
        siteconfig.settings.get("auth_backend", "builtin") == "bugzilla")

    if using_bugzilla:
        b = Bugzilla(get_bugzilla_api_key(user))

        try:
            if b.is_bug_confidential(bug_id):
                raise ConfidentialBugError
        except BugzillaError as e:
            # Special cases:
            #   100: Invalid Bug Alias
            #   101: Bug does not exist
            if e.fault_code and (e.fault_code == 100 or e.fault_code == 101):
                raise InvalidBugIdError(bug_id)
            raise

    # Note that the bug ID has already been set when the review was created.

    # If this is a squashed/parent review request, automatically publish all
    # relevant children.
    if is_parent(review_request, commit_data):
        unpublished_rids = map(int, json.loads(
            commit_data.extra_data[UNPUBLISHED_KEY]))
        discard_on_publish_rids = map(int, json.loads(
            commit_data.extra_data[DISCARD_ON_PUBLISH_KEY]))
        child_rrs = list(gen_child_rrs(review_request_draft))

        # Create or update Bugzilla attachments for each draft commit.  This
        # is done before the children are published to ensure that MozReview
        # doesn't get into a strange state if communication with Bugzilla is
        # broken or attachment creation otherwise fails.  The Bugzilla
        # attachments will then, of course, be in a weird state, but that
        # should be fixed by the next successful publish.
        if using_bugzilla:
            for child in child_rrs:
                child_draft = child.get_draft(user=user)

                if child_draft:
                    if child.id in discard_on_publish_rids:
                        b.obsolete_review_attachments(
                            bug_id, get_obj_url(child))
                    post_bugzilla_attachment(b, bug_id, child_draft, child)

        # Publish draft commits. This will already include items that are in
        # unpublished_rids, so we'll remove anything we publish out of
        # unpublished_rids.
        for child in child_rrs:
            if child.get_draft(user=user) or not child.public:
                def approve_publish(sender, user, review_request_draft,
                                    **kwargs):
                    return child is sender

                # Setup the parent signal handler to approve the publish
                # and then publish the child.
                commit_request_publishing.connect(approve_publish,
                                                  sender=child,
                                                  weak=False)
                try:
                    child.publish(user=user)
                finally:
                    commit_request_publishing.disconnect(
                        receiver=approve_publish,
                        sender=child,
                        weak=False)

                if child.id in unpublished_rids:
                    unpublished_rids.remove(child.id)

        # The remaining unpubished_rids need to be closed as discarded because
        # they have never been published, and they will appear in the user's
        # dashboard unless closed.
        for child in gen_rrs_by_rids(unpublished_rids):
            child.close(ReviewRequest.DISCARDED,
                        user=user,
                        description=NEVER_USED_DESCRIPTION)

        # We also close the discard_on_publish review requests because, well,
        # we don't need them anymore. We use a slightly different message
        # though.
        for child in gen_rrs_by_rids(discard_on_publish_rids):
            child.close(ReviewRequest.DISCARDED,
                        user=user,
                        description=OBSOLETE_DESCRIPTION)

        commit_data.extra_data[UNPUBLISHED_KEY] = '[]'
        commit_data.extra_data[DISCARD_ON_PUBLISH_KEY] = '[]'

    # Copy any drafted CommitData from draft_extra_data to extra_data.
    for key in DRAFTED_COMMIT_DATA_KEYS:
        if key in commit_data.draft_extra_data:
            commit_data.extra_data[key] = commit_data.draft_extra_data[key]

    commit_data.save(update_fields=['extra_data'])

    review_request.save()
Пример #13
0
def on_review_request_publishing(user, review_request_draft, **kwargs):
    # There have been strange cases (all local, and during development), where
    # when attempting to publish a review request, this handler will fail
    # because the draft does not exist. This is a really strange case, and not
    # one we expect to happen in production. However, since we've seen it
    # locally, we handle it here, and log.
    if not review_request_draft:
        logging.error('Strangely, there was no review request draft on the '
                      'review request we were attempting to publish.')
        return

    review_request = review_request_draft.get_review_request()

    # skip review requests that were not pushed
    if not is_review_request_pushed(review_request):
        return

    if not is_parent(review_request):
        # Send a signal asking for approval to publish this review request.
        # We only want to publish this commit request if we are in the middle
        # of publishing the parent. If the parent is publishing it will be
        # listening for this signal to approve it.
        approvals = commit_request_publishing.send_robust(
            sender=review_request,
            user=user,
            review_request_draft=review_request_draft)

        for receiver, approved in approvals:
            if approved:
                break
        else:
            # This publish is not approved by the parent review request.
            raise CommitPublishProhibited()

    # The reviewid passed through p2rb is, for Mozilla's instance anyway,
    # bz://<bug id>/<irc nick>.
    reviewid = review_request_draft.extra_data.get('p2rb.identifier', None)
    m = REVIEWID_RE.match(reviewid)

    if not m:
        raise InvalidBugIdError('<unknown>')

    bug_id = m.group(1)
    using_bugzilla = we_are_using_bugzilla()
    try:
        bug_id = int(bug_id)
    except (TypeError, ValueError):
        raise InvalidBugIdError(bug_id)

    if using_bugzilla:
        b = Bugzilla(get_bugzilla_api_key(user))

        try:
            if b.is_bug_confidential(bug_id):
                raise ConfidentialBugError
        except BugzillaError as e:
            # Special cases:
            #   100: Invalid Bug Alias
            #   101: Bug does not exist
            if e.fault_code and (e.fault_code == 100 or e.fault_code == 101):
                raise InvalidBugIdError(bug_id)
            raise

    # Note that the bug ID has already been set when the review was created.

    # If this is a squashed/parent review request, automatically publish all
    # relevant children.
    if is_review_request_squashed(review_request):
        unpublished_rids = map(int, json.loads(
            review_request.extra_data['p2rb.unpublished_rids']))
        discard_on_publish_rids = map(int, json.loads(
            review_request.extra_data['p2rb.discard_on_publish_rids']))
        child_rrs = list(gen_child_rrs(review_request_draft))

        # Create or update Bugzilla attachments for each draft commit.  This
        # is done before the children are published to ensure that MozReview
        # doesn't get into a strange state if communication with Bugzilla is
        # broken or attachment creation otherwise fails.  The Bugzilla
        # attachments will then, of course, be in a weird state, but that
        # should be fixed by the next successful publish.
        if using_bugzilla:
            for child in child_rrs:
                child_draft = child.get_draft(user=user)

                if child_draft:
                    if child.id in discard_on_publish_rids:
                        b.obsolete_review_attachments(
                            bug_id, get_obj_url(child))
                    post_bugzilla_attachment(b, bug_id, child_draft, child)

        # Publish draft commits. This will already include items that are in
        # unpublished_rids, so we'll remove anything we publish out of
        # unpublished_rids.
        for child in child_rrs:
            if child.get_draft(user=user) or not child.public:
                def approve_publish(sender, user, review_request_draft,
                                    **kwargs):
                    return child is sender

                # Setup the parent signal handler to approve the publish
                # and then publish the child.
                commit_request_publishing.connect(approve_publish, sender=child,
                                                  weak=False)
                try:
                    child.publish(user=user)
                finally:
                    commit_request_publishing.disconnect(
                        receiver=approve_publish,
                        sender=child,
                        weak=False)

                if child.id in unpublished_rids:
                    unpublished_rids.remove(child.id)

        # The remaining unpubished_rids need to be closed as discarded because
        # they have never been published, and they will appear in the user's
        # dashboard unless closed.
        for child in gen_rrs_by_rids(unpublished_rids):
            child.close(ReviewRequest.DISCARDED,
                        user=user,
                        description=NEVER_USED_DESCRIPTION)

        # We also close the discard_on_publish review requests because, well,
        # we don't need them anymore. We use a slightly different message
        # though.
        for child in gen_rrs_by_rids(discard_on_publish_rids):
            child.close(ReviewRequest.DISCARDED,
                        user=user,
                        description=OBSOLETE_DESCRIPTION)

        review_request.extra_data['p2rb.unpublished_rids'] = '[]'
        review_request.extra_data['p2rb.discard_on_publish_rids'] = '[]'

    # Copy p2rb extra data from the draft, overwriting the current
    # values on the review request.
    draft_extra_data = review_request_draft.extra_data

    for key in DRAFTED_EXTRA_DATA_KEYS:
        if key in draft_extra_data:
            review_request.extra_data[key] = draft_extra_data[key]

    review_request.save()
Пример #14
0
    def authenticate_api_key(self, username, api_key):
        """Authenticate a user from a username and API key.

        This is intended to be used by the Web API and not a user-facing
        login form. We enforce that the user already exists and has an
        API key - not necessarily the same API key - on file. The API key
        passed in is only used for authentication: all subsequent communication
        with Bugzilla should be performed using the API key on file.

        We require the user already exist in the database because having
        the user go through the browser-facing login flow is the most sane
        (and secure) way to obtain an API key. We don't want to store the
        API key provided to us from the client because API keys obtained by
        the browser login may have special permissions not granted to normal
        API keys.
        """
        username = username.strip()

        logger.info('Login attempt (apikey) for user %s: ' % username)

        try:
            bugzilla = Bugzilla()
        except BugzillaUrlError:
            logger.warn('Login failure (apikey) for user %s: Bugzilla URL '
                        'not set.' % username)

        try:
            valid = bugzilla.valid_api_key(username, api_key)
        except BugzillaError as e:
            logger.error('Login failure (apikey) for user %s: %s' %
                         (username, e))
            return None

        if not valid:
            logger.error('Login failure for user %s: invalid API key' %
                         username)
            assert bugzilla.base_url.endswith('/')
            raise BugzillaAPIKeyNeededError(bugzilla.base_url +
                                            'userprefs.cgi?tab=apikey')

        # Assign the API key to the Bugzilla connection so the user info
        # lookup uses it.
        # TODO can we skip valid_api_key() and just get user info straight up?
        bugzilla.api_key = api_key

        try:
            user_data = bugzilla.get_user(username)
        except BugzillaError as e:
            logger.error('Login failure (apikey) for user %s: unable to '
                         'retrieve Bugzilla user info: %s' % (username, e))
            return None

        if not user_data:
            logger.warning('Could not retrieve user info for %s after '
                           'validating API key' % username)
            return None

        bz_user = user_data['users'][0]

        try:
            bum = BugzillaUserMap.objects.get(bugzilla_user_id=bz_user['id'])
            user = bum.user
        except BugzillaUserMap.DoesNotExist:
            logger.warning('Login failure for user %s: API key valid but '
                           'user missing from database' % username)
            raise WebLoginNeededError()

        if not user.is_active:
            logger.error('Login failure (apikey) for user %s: user not '
                         'active' % username)
            return None

        # We require a local API key to be on file, as it will be used for
        # subsequent requests.
        if not get_bugzilla_api_key(user):
            logger.warning('Login failure for user %s: no API key in '
                           'database' % username)
            raise WebLoginNeededError()

        logger.info('Login successful (apikey) for user %s: ' % username)

        return user
Пример #15
0
    def create(self, request, *args, **kwargs):
        ext = get_extension_manager().get_enabled_extension(
            'mozreview.extension.MozReviewExtension')

        testing = ext.get_settings('autoland_testing', False)

        if not testing and not request.user.has_perm(
                'mozreview.add_autolandeventlogentry'):
            return PERMISSION_DENIED

        try:
            fields = json.loads(request.body)

            for field_name in self.fields:
                assert (type(
                    fields[field_name]) == self.fields[field_name]['type'])
        except (ValueError, IndexError, KeyError, AssertionError) as e:
            return INVALID_FORM_DATA, {
                'error': '%s' % e,
            }

        try:
            autoland_request = AutolandRequest.objects.get(
                pk=fields['request_id'])
        except AutolandRequest.DoesNotExist:
            return DOES_NOT_EXIST

        rr = ReviewRequest.objects.get(pk=autoland_request.review_request_id)
        bz_comment = None

        if fields['landed']:
            autoland_request.repository_revision = fields['result']
            autoland_request.save()

            # If we've landed to the "inbound" repository, we'll close the
            # review request automatically.
            landing_repo = rr.repository.extra_data.get(
                'landing_repository_url')

            if autoland_request.repository_url == landing_repo:
                rr.close(ReviewRequest.SUBMITTED)

            AutolandEventLogEntry.objects.create(
                autoland_request_id=fields['request_id'],
                status=AutolandEventLogEntry.SERVED,
                details=fields['result'])
        elif not fields.get('error_msg') and fields.get('result'):
            AutolandEventLogEntry.objects.create(
                autoland_request_id=fields['request_id'],
                status=AutolandEventLogEntry.REQUESTED,
                details=fields['result'])
        else:
            AutolandEventLogEntry.objects.create(
                autoland_request_id=fields['request_id'],
                status=AutolandEventLogEntry.PROBLEM,
                error_msg=fields['error_msg'])

            # The error message contains context explaining that Autoland
            # failed, so no leading text is necessary.
            bz_comment = fields['error_msg']

        lock_id = get_autoland_lock_id(rr.id, autoland_request.repository_url,
                                       autoland_request.push_revision)
        release_lock(lock_id)

        if bz_comment:
            bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
            bug_id = int(rr.get_bug_list()[0])

            # Catch and log Bugzilla errors rather than bubbling them up,
            # since we don't want the Autoland server to continously
            # retry the update.
            try:
                bugzilla.post_comment(bug_id, bz_comment)
            except BugzillaError as e:
                logger.error('Failed to post comment to Bugzilla: %s' % e)

        return 200, {}
Пример #16
0
def get_bmo_auth_callback(request):
    """Handler for the third part of the Bugzilla auth-delegation process.

    After the above POST call is executed, Bugzilla then redirects back to
    this view, passing the return value of the POST handler, as
    `callback_result`, and optionally a redirect, passed from the original
    redirect to Bugzilla (from the MozReview login view).

    This handler then verifies the API key with Bugzilla and attempts to
    create or update the user in MozReview.  If everything succeeds, the
    user is again redirected back to the original page (or the root page if
    there was no redirect passed in, e.g., in tests).  Otherwise the user is
    shown an error page.
    """
    bmo_username = request.GET.get('client_api_login', None)
    callback_result = request.GET.get('callback_result', None)
    redirect = request.GET.get('redirect', None)
    secret = request.GET.get('secret', None)

    if not (bmo_username and callback_result):
        logger.error('Bugzilla auth callback called without required '
                     'parameters.')
        return render_login_error(request)

    # Delete expired unverified keys (5 minute lifetime).
    UnverifiedBugzillaApiKey.objects.filter(timestamp__lte=timezone.now() -
                                            timedelta(minutes=5)).delete()

    parsed = None if not redirect else urlparse(redirect)

    # Enforce relative redirects; we don't want people crafting auth links
    # that redirect to other sites.  We check the scheme as well as the netloc
    # to catch data, file, and other such server-less URIs.

    if not parsed or parsed.scheme or parsed.netloc:
        redirect = '/'

    unverified_keys = UnverifiedBugzillaApiKey.objects.filter(
        bmo_username=bmo_username).order_by('timestamp')

    if not unverified_keys:
        logger.error('No unverified keys found for BMO user %s.' %
                     bmo_username)
        return render_login_error(request)

    unverified_key = unverified_keys.last()

    if len(unverified_keys) > 1:
        logger.warning('Multiple unverified keys on file for BMO user %s. '
                       'Using most recent, from %s.' %
                       (bmo_username, unverified_key.timestamp))

    if callback_result != unverified_key.callback_result:
        logger.error('Callback result does not match for BMO user %s.' %
                     bmo_username)
        return render_login_error(request)

    if secret is None or request.COOKIES['bmo_auth_secret'] != secret:
        logger.error('Callback secret does not match cookie for user %s.' %
                     bmo_username)
        return render_login_error(request)

    bmo_api_key = unverified_key.api_key
    unverified_key.delete()

    b = Bugzilla()

    try:
        if not b.valid_api_key(bmo_username, bmo_api_key):
            logger.error('Invalid API key for %s.' % bmo_username)
            return render_login_error(request)
    except BugzillaError as e:
        logger.error('Error validating API key: %s' % e.msg)
        return render_login_error(request)

    b.api_key = bmo_api_key

    try:
        user_data = b.get_user(bmo_username)
    except BugzillaError as e:
        logger.error('Error getting user data: %s' % e.msg)
        return render_login_error(request)

    if not user_data:
        logger.warning('Could not retrieve user info for %s after '
                       'validating API key.' % bmo_username)
        return render_login_error(request)

    users = get_or_create_bugzilla_users(user_data)

    if not users:
        logger.warning('Failed to create user %s after validating API key.' %
                       bmo_username)
        return render_login_error(request)

    user = users[0]
    assert user.email == bmo_username

    if not user.is_active:
        logger.warning('Validated API key but user %s is inactive.' %
                       bmo_username)
        return render_login_error(request)

    set_bugzilla_api_key(user, bmo_api_key)

    try:
        associate_employee_ldap(user)
    except LDAPAssociationException as e:
        logger.info('LDAP association failed: %s' % str(e))
    except Exception:
        logger.exception('Error while performing LDAP association')

    user.backend = 'mozreview.bugzilla.auth.BugzillaBackend'
    logger.info('BMO Auth callback succeeded for user: %s' % bmo_username)
    login(request, user)
    response = HttpResponseRedirect(redirect)
    response.delete_cookie('bmo_auth_secret')
    return response
Пример #17
0
def on_review_publishing(user, review, **kwargs):
    """Comment in the bug and potentially r+ or clear a review flag.

    Note that a reviewer *must* have editbugs to set an attachment flag on
    someone else's attachment (i.e. the standard BMO review process).

    TODO: Report lack-of-editbugs properly; see bug 1119065.
    """
    review_request = review.review_request
    logger.info('Publishing review for user: %s review id: %s '
                'review request id: %s' % (user, review.id, review_request.id))

    # skip review requests that were not pushed
    if not is_pushed(review_request):
        logger.info('Did not publish review: %s: for user: %d: review not '
                    'pushed.' % (user, review.id))
        return

    site = Site.objects.get_current()
    siteconfig = SiteConfiguration.objects.get_current()
    comment = build_plaintext_review(review,
                                     get_obj_url(review, site, siteconfig),
                                     {"user": user})
    b = Bugzilla(get_bugzilla_api_key(user))

    if is_parent(review_request):
        # We only support raw comments on parent review requests to prevent
        # confusion.  If the ship-it flag or the review flag was set, throw
        # an error.
        # Otherwise, mirror the comment over, associating it with the first
        # commit.
        if review.ship_it or review.extra_data.get(REVIEW_FLAG_KEY):
            raise ParentShipItError

        # TODO: If we ever allow multiple bugs in a single series, and we want
        # to continue to allow comments on parents, we'll have to pick one
        # child for each unique bug.
        first_child = list(gen_child_rrs(review_request))[0]
        b.post_comment(int(first_child.get_bug_list()[0]), comment,
                       get_diff_url(first_child), False)
    else:
        diff_url = get_diff_url(review_request)
        bug_id = int(review_request.get_bug_list()[0])

        commented = False
        flag = review.extra_data.get(REVIEW_FLAG_KEY)

        if flag is not None:
            commented = b.set_review_flag(bug_id, flag, review.user.email,
                                          diff_url, comment)
        else:
            # If for some reasons we don't have the flag set in extra_data,
            # fall back to ship_it
            logger.warning('Review flag not set on review %s, '
                           'updating attachment based on ship_it' % review.id)
            if review.ship_it:
                commented = b.r_plus_attachment(bug_id, review.user.email,
                                                diff_url, comment)
            else:
                commented = b.cancel_review_request(bug_id, review.user.email,
                                                    diff_url, comment)

        if comment and not commented:
            b.post_comment(bug_id, comment, diff_url, False)
Пример #18
0
    def authenticate(self, username, password, cookie=False):
        username = username.strip()

        logger.info('Login attempt (password) for user %s: ' % username)

        # If the user provides an email address when authenticating,
        # it is checked against Review Board's email field in the User
        # Model.  If a match is found, the email will be translated into
        # the username field before being passed into this method's
        # 'username' argument.
        #
        # If a match is not found, 'username' will contain whatever was
        # entered, which may be the Bugzilla login (email address) for a
        # user who does not yet have an entry in the Review Board
        # database.

        if not cookie:
            try:
                username = User.objects.get(username=username).email
            except User.DoesNotExist:
                pass

        # There is a *tiny* probability that this will not work, but only if
        # user A changes their email address, then user B changes their email
        # address to user A's old email, and Review Board doesn't pick up
        # user A's change because they aren't currently involved in any
        # Review Board reviews.  In this case 'username' would have resolved
        # to user A's address.  There's no easy way to detect this without
        # a search on Bugzilla before every log in, and I (mcote) don't think
        # that's worth it for such an improbable event.
        #
        # This also applies to changes to the user's username, since it has
        # to be unique (see get_or_create_bugzilla_users()).

        try:
            bugzilla = Bugzilla()
        except BugzillaUrlError:
            logger.warn('Login failure (password) for user %s: Bugzilla URL '
                        ' not set.' % username)
            return None

        try:
            user_data = bugzilla.log_in(username, password, cookie)
        except BugzillaError as e:
            logger.error('Login failure (password) for user %s: %s' %
                         (username, e))
            return None

        if not user_data:
            return None

        users = get_or_create_bugzilla_users(user_data)

        if not users:
            logger.error('Login failure (password) for user %s: failed to '
                         'create user.' % username)
            return None

        user = users[0]

        if not user.is_active:
            logger.error('Login failure (password) for user %s: user is not '
                         'active.' % username)
            return None

        logger.info('Login successful (password) for user %s: ' % username)
        return user
Пример #19
0
def on_review_request_publishing(user, review_request_draft, **kwargs):
    # There have been strange cases (all local, and during development), where
    # when attempting to publish a review request, this handler will fail
    # because the draft does not exist. This is a really strange case, and not
    # one we expect to happen in production. However, since we've seen it
    # locally, we handle it here, and log.
    if not review_request_draft:
        logger.error('Strangely, there was no review request draft on the '
                     'review request we were attempting to publish.')
        return

    # If the review request draft has a new DiffSet we will only allow
    # publishing if that DiffSet has been verified. It is important to
    # do this for every review request, not just pushed ones, because
    # we can't trust the storage mechanism which indicates it was pushed.
    # TODO: This will be fixed when we transition away from extra_data.
    if review_request_draft.diffset:
        try:
            DiffSetVerification.objects.get(
                diffset=review_request_draft.diffset)
        except DiffSetVerification.DoesNotExist:
            logger.error(
                'An attempt was made by User %s to publish an unverified '
                'DiffSet with id %s', user.id, review_request_draft.diffset.id)

            raise PublishError(
                'This review request draft contained a manually uploaded '
                'diff, which is prohibited. Please push to the review server '
                'to create review requests. If you believe you received this '
                'message in error, please file a bug.')

    review_request = review_request_draft.get_review_request()
    commit_data = fetch_commit_data(review_request)

    # skip review requests that were not pushed
    if not is_pushed(review_request, commit_data=commit_data):
        return

    if not is_parent(review_request, commit_data):
        # Send a signal asking for approval to publish this review request.
        # We only want to publish this commit request if we are in the middle
        # of publishing the parent. If the parent is publishing it will be
        # listening for this signal to approve it.
        approvals = commit_request_publishing.send_robust(
            sender=review_request,
            user=user,
            review_request_draft=review_request_draft)

        for receiver, approved in approvals:
            if approved:
                break
        else:
            # This publish is not approved by the parent review request.
            raise CommitPublishProhibited()

    # The reviewid passed through p2rb is, for Mozilla's instance anyway,
    # bz://<bug id>/<irc nick>.
    reviewid = commit_data.draft_extra_data.get(IDENTIFIER_KEY, None)
    m = REVIEWID_RE.match(reviewid)

    if not m:
        raise InvalidBugIdError('<unknown>')

    bug_id = m.group(1)

    try:
        bug_id = int(bug_id)
    except (TypeError, ValueError):
        raise InvalidBugIdError(bug_id)

    siteconfig = SiteConfiguration.objects.get_current()
    using_bugzilla = (siteconfig.settings.get("auth_backend",
                                              "builtin") == "bugzilla")

    if using_bugzilla:
        commit_data = fetch_commit_data(review_request_draft)
        publish_as_id = commit_data.draft_extra_data.get(PUBLISH_AS_KEY)
        if publish_as_id:
            u = User.objects.get(id=publish_as_id)
            b = Bugzilla(get_bugzilla_api_key(u))
        else:
            b = Bugzilla(get_bugzilla_api_key(user))

        try:
            if b.is_bug_confidential(bug_id):
                raise ConfidentialBugError
        except BugzillaError as e:
            # Special cases:
            #   100: Invalid Bug Alias
            #   101: Bug does not exist
            if e.fault_code and (e.fault_code == 100 or e.fault_code == 101):
                raise InvalidBugIdError(bug_id)
            raise

    # Note that the bug ID has already been set when the review was created.

    # If this is a squashed/parent review request, automatically publish all
    # relevant children.
    if is_parent(review_request, commit_data):
        unpublished_rids = map(
            int, json.loads(commit_data.extra_data[UNPUBLISHED_KEY]))
        discard_on_publish_rids = map(
            int, json.loads(commit_data.extra_data[DISCARD_ON_PUBLISH_KEY]))
        child_rrs = list(gen_child_rrs(review_request_draft))

        # Create or update Bugzilla attachments for each draft commit.  This
        # is done before the children are published to ensure that MozReview
        # doesn't get into a strange state if communication with Bugzilla is
        # broken or attachment creation otherwise fails.  The Bugzilla
        # attachments will then, of course, be in a weird state, but that
        # should be fixed by the next successful publish.
        if using_bugzilla:
            children_to_post = []
            children_to_obsolete = []

            for child in child_rrs:
                child_draft = child.get_draft(user=user)

                if child_draft:
                    if child.id in discard_on_publish_rids:
                        children_to_obsolete.append(child)

                    children_to_post.append((child_draft, child))

            if children_to_post or children_to_obsolete:
                update_bugzilla_attachments(b, bug_id, children_to_post,
                                            children_to_obsolete)

        # Publish draft commits. This will already include items that are in
        # unpublished_rids, so we'll remove anything we publish out of
        # unpublished_rids.
        for child in child_rrs:
            if child.get_draft(user=user) or not child.public:

                def approve_publish(sender, user, review_request_draft,
                                    **kwargs):
                    return child is sender

                # Setup the parent signal handler to approve the publish
                # and then publish the child.
                commit_request_publishing.connect(approve_publish,
                                                  sender=child,
                                                  weak=False)
                try:
                    child.publish(user=user)
                except NotModifiedError:
                    # As we create empty drafts as part of allowing reviewer
                    # delegation, delete these empty drafts instead of
                    # throwing an error.
                    child.get_draft(user=user).delete()
                finally:
                    commit_request_publishing.disconnect(
                        receiver=approve_publish, sender=child, weak=False)

                if child.id in unpublished_rids:
                    unpublished_rids.remove(child.id)

        # The remaining unpubished_rids need to be closed as discarded because
        # they have never been published, and they will appear in the user's
        # dashboard unless closed.
        for child in gen_rrs_by_rids(unpublished_rids):
            child.close(ReviewRequest.DISCARDED,
                        user=user,
                        description=NEVER_USED_DESCRIPTION)

        # We also close the discard_on_publish review requests because, well,
        # we don't need them anymore. We use a slightly different message
        # though.
        for child in gen_rrs_by_rids(discard_on_publish_rids):
            child.close(ReviewRequest.DISCARDED,
                        user=user,
                        description=OBSOLETE_DESCRIPTION)

        commit_data.extra_data[UNPUBLISHED_KEY] = '[]'
        commit_data.extra_data[DISCARD_ON_PUBLISH_KEY] = '[]'

    # Copy any drafted CommitData from draft_extra_data to extra_data.
    for key in DRAFTED_COMMIT_DATA_KEYS:
        if key in commit_data.draft_extra_data:
            commit_data.extra_data[key] = commit_data.draft_extra_data[key]

    commit_data.save(update_fields=['extra_data'])

    review_request.save()
    def create(self, request, parent_request_id, reviewers, *args, **kwargs):
        try:
            parent_rr = ReviewRequest.objects.get(pk=parent_request_id)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if not (parent_rr.is_accessible_by(request.user)
                or parent_rr.is_mutable_by(request.user)):
            return PERMISSION_DENIED

        if not is_parent(parent_rr):
            return NOT_PARENT

        # Validate and expand the new reviewer list.

        bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        child_reviewers = json.loads(reviewers)
        invalid_reviewers = []
        for child_rrid in child_reviewers:
            users = []
            for username in child_reviewers[child_rrid]:
                try:
                    users.append(bugzilla.get_user_from_irc_nick(username))
                except User.DoesNotExist:
                    invalid_reviewers.append(username)
            child_reviewers[child_rrid] = users

        if invalid_reviewers:
            # Because this isn't called through Review Board's built-in
            # backbone system, it's dramatically simpler to return just the
            # intended error message instead of categorising the errors by
            # field.
            if len(invalid_reviewers) == 1:
                return INVALID_FORM_DATA.with_message(
                    "The reviewer '%s' was not found" % invalid_reviewers[0])
            else:
                return INVALID_FORM_DATA.with_message(
                    "The reviewers '%s' were not found" %
                    "', '".join(invalid_reviewers))

        # Review Board only supports the submitter updating a review
        # request.  In order for this to work, we publish these changes
        # in Review Board under the review submitter's account, and
        # set an extra_data field which instructs our bugzilla
        # connector to use this request's user when adjusting flags.
        #
        # Updating the review request requires creating a draft and
        # publishing it, so we have to be careful to not overwrite
        # existing drafts.

        try:
            with transaction.atomic():
                for rr in itertools.chain([parent_rr],
                                          gen_child_rrs(parent_rr)):
                    if rr.get_draft() is not None:
                        return REVIEW_REQUEST_UPDATE_NOT_ALLOWED.with_message(
                            "Unable to update reviewers as the review "
                            "request has pending changes (the patch author "
                            "has a draft)")

                try:
                    for child_rr in gen_child_rrs(parent_rr):
                        if str(child_rr.id) in child_reviewers:
                            if not child_rr.is_accessible_by(request.user):
                                return PERMISSION_DENIED.with_message(
                                    "You do not have permission to update "
                                    "reviewers on review request %s" %
                                    child_rr.id)

                            draft = ReviewRequestDraft.create(child_rr)
                            draft.target_people.clear()
                            for user in child_reviewers[str(child_rr.id)]:
                                draft.target_people.add(user)

                    set_publish_as(parent_rr, request.user)
                    parent_rr_draft = ReviewRequestDraft.create(parent_rr)
                    update_parent_rr_reviewers(parent_rr_draft)
                    parent_rr.publish(user=parent_rr.submitter)
                finally:
                    clear_publish_as(parent_rr)

        except PublishError as e:
            logging.error("failed to update reviewers on %s: %s" %
                          (parent_rr.id, str(e)))
            return PUBLISH_ERROR.with_message(str(e))

        return 200, {}
Пример #21
0
def get_bmo_auth_callback(request):
    """Handler for the third part of the Bugzilla auth-delegation process.

    After the above POST call is executed, Bugzilla then redirects back to
    this view, passing the return value of the POST handler, as
    `callback_result`, and optionally a redirect, passed from the original
    redirect to Bugzilla (from the MozReview login view).

    This handler then verifies the API key with Bugzilla and attempts to
    create or update the user in MozReview.  If everything succeeds, the
    user is again redirected back to the original page (or the root page if
    there was no redirect passed in, e.g., in tests).  Otherwise the user is
    shown an error page.
    """
    bmo_username = request.GET.get('client_api_login', None)
    callback_result = request.GET.get('callback_result', None)
    redirect = request.GET.get('redirect', None)
    secret = request.GET.get('secret', None)

    if not (bmo_username and callback_result):
        logger.error('Bugzilla auth callback called without required '
                     'parameters.')
        return show_error_page(request)

    # Delete expired unverified keys (5 minute lifetime).
    UnverifiedBugzillaApiKey.objects.filter(
        timestamp__lte=timezone.now() - timedelta(minutes=5)).delete()

    parsed = None if not redirect else urlparse(redirect)

    # Enforce relative redirects; we don't want people crafting auth links
    # that redirect to other sites.  We check the scheme as well as the netloc
    # to catch data, file, and other such server-less URIs.

    if not parsed or parsed.scheme or parsed.netloc:
        redirect = '/'

    unverified_keys = UnverifiedBugzillaApiKey.objects.filter(
        bmo_username=bmo_username).order_by('timestamp')

    if not unverified_keys:
        logger.error('No unverified keys found for BMO user %s.' %
                     bmo_username)
        return show_error_page(request)

    unverified_key = unverified_keys.last()

    if len(unverified_keys) > 1:
        logger.warning('Multiple unverified keys on file for BMO user %s. '
                       'Using most recent, from %s.' %
                       (bmo_username, unverified_key.timestamp))

    if callback_result != unverified_key.callback_result:
        logger.error('Callback result does not match for BMO user %s.' %
                     bmo_username)
        return show_error_page(request)

    if secret is None or request.COOKIES['bmo_auth_secret'] != secret:
        logger.error('Callback secret does not match cookie for user %s.' %
                     bmo_username)
        return show_error_page(request)

    bmo_api_key = unverified_key.api_key
    unverified_key.delete()

    b = Bugzilla()

    try:
        if not b.valid_api_key(bmo_username, bmo_api_key):
            logger.error('Invalid API key for %s.' % bmo_username)
            return show_error_page(request)
    except BugzillaError as e:
        logger.error('Error validating API key: %s' % e.msg)
        return show_error_page(request)

    b.api_key = bmo_api_key

    try:
        user_data = b.get_user(bmo_username)
    except BugzillaError as e:
        logger.error('Error getting user data: %s' % e.msg)
        return show_error_page(request)

    if not user_data:
        logger.warning('Could not retrieve user info for %s after '
                       'validating API key.' % bmo_username)
        return show_error_page(request)

    users = get_or_create_bugzilla_users(user_data)

    if not users:
        logger.warning('Failed to create user %s after validating API key.' %
                       bmo_username)
        return show_error_page(request)

    user = users[0]
    assert user.email == bmo_username

    if not user.is_active:
        logger.warning('Validated API key but user %s is inactive.' %
                       bmo_username)
        return show_error_page(request)

    set_bugzilla_api_key(user, bmo_api_key)

    try:
        associate_employee_ldap(user)
    except LDAPAssociationException as e:
        logger.info('LDAP association failed: %s' % str(e))
    except Exception:
        logger.exception('Error while performing LDAP association')

    user.backend = 'rbbz.auth.BugzillaBackend'
    logger.info('BMO Auth callback succeeded for user: %s' % bmo_username)
    login(request, user)
    response = HttpResponseRedirect(redirect)
    response.delete_cookie('bmo_auth_secret')
    return response
Пример #22
0
def get_bmo_auth_callback(request):
    """Handler for the second part of the Bugzilla auth-delegation process.

    After the above POST call is executed, Bugzilla then redirects back to
    this view, passing the return value of the POST handler, as
    `callback_result`, and optionally a redirect, passed from the original
    redirect to Bugzilla (from the MozReview login view).

    This handler then verifies the API key with Bugzilla and attempts to
    create or update the user in MozReview.  If everything succeeds, the
    user is again redirected back to the original page (or the root page if
    there was no redirect passed in, e.g., in tests).  Otherwise the user is
    shown an error page.
    """
    bmo_username = request.GET.get('client_api_login', None)
    callback_result = request.GET.get('callback_result', None)
    redirect = request.GET.get('redirect', None)

    if not (bmo_username and callback_result):
        logging.error('Bugzilla auth callback called without required '
                      'parameters.')
        return show_error_page(request)

    if not redirect:
        redirect = '/'

    unverified_keys = UnverifiedBugzillaApiKey.objects.filter(
        bmo_username=bmo_username).order_by('timestamp')

    if not unverified_keys:
        logging.error('No unverified keys found for BMO user %s.' %
                      bmo_username)
        return show_error_page(request)

    unverified_key = unverified_keys.last()

    if len(unverified_keys) > 1:
        logging.warning('Multiple unverified keys on file for BMO user %s. '
                        'Using most recent, from %s.' %
                        (bmo_username, unverified_key.timestamp))

    if callback_result != unverified_key.callback_result:
        logging.error('Callback result does not match for BMO user %s.' %
                      bmo_username)
        return show_error_page(request)

    bmo_api_key = unverified_key.api_key
    unverified_key.delete()

    b = Bugzilla()

    try:
        if not b.valid_api_key(bmo_username, bmo_api_key):
            logging.error('Invalid API key for %s.' % bmo_username)
            return show_error_page(request)
    except BugzillaError as e:
        logging.error('Error validating API key: %s' % e.msg)
        return show_error_page(request)

    b.api_key = bmo_api_key

    try:
        user_data = b.get_user(bmo_username)
    except BugzillaError as e:
        logging.error('Error getting user data: %s' % e.msg)
        return show_error_page(request)

    if not user_data:
        logging.warning('Could not retrieve user info for %s after '
                        'validating API key.' % bmo_username)
        return show_error_page(request)

    users = get_or_create_bugzilla_users(user_data)

    if not users:
        logging.warning('Failed to create user %s after validating API key.' %
                        bmo_username)
        return show_error_page(request)

    user = users[0]
    assert user.email == bmo_username

    if not user.is_active:
        logging.warning('Validated API key but user %s is inactive.' %
                        bmo_username)
        return show_error_page(request)

    set_bugzilla_api_key(user, bmo_api_key)
    user.backend = 'rbbz.auth.BugzillaBackend'
    login(request, user)
    return HttpResponseRedirect(redirect)
Пример #23
0
    def authenticate_api_key(self, username, api_key):
        """Authenticate a user from a username and API key.

        This is intended to be used by the Web API and not a user-facing
        login form. We enforce that the user already exists and has an
        API key - not necessarily the same API key - on file. The API key
        passed in is only used for authentication: all subsequent communication
        with Bugzilla should be performed using the API key on file.

        We require the user already exist in the database because having
        the user go through the browser-facing login flow is the most sane
        (and secure) way to obtain an API key. We don't want to store the
        API key provided to us from the client because API keys obtained by
        the browser login may have special permissions not granted to normal
        API keys.
        """
        username = username.strip()

        try:
            bugzilla = Bugzilla()
        except BugzillaUrlError:
            logging.warn('Login failure for user %s: Bugzilla URL not set.' %
                         username)

        try:
            valid = bugzilla.valid_api_key(username, api_key)
        except BugzillaError as e:
            logging.error('Login failure for user %s: %s' % (username, e))
            return None

        if not valid:
            logging.error('Login failure for user %s: invalid API key' %
                          username)
            assert bugzilla.base_url.endswith('/')
            raise BugzillaAPIKeyNeededError(
                    bugzilla.base_url + 'userprefs.cgi?tab=apikey')

        # Assign the API key to the Bugzilla connection so the user info
        # lookup uses it.
        # TODO can we skip valid_api_key() and just get user info straight up?
        bugzilla.api_key = api_key

        try:
            user_data = bugzilla.get_user(username)
        except BugzillaError as e:
            logging.error('Login failure for user %s: unable to retrieve '
                          'Bugzilla user info: %s' % (username, e))
            return None

        if not user_data:
            logging.warning('Could not retrieve user info for %s after '
                            'validating API key' % username)
            return None

        bz_user = user_data['users'][0]

        try:
            bum = BugzillaUserMap.objects.get(bugzilla_user_id=bz_user['id'])
            user = bum.user
        except BugzillaUserMap.DoesNotExist:
            logging.warning('Login failure for user %s: API key valid but '
                            'user missing from database' % username)
            raise WebLoginNeededError()

        if not user.is_active:
            logging.error('Login failure for user %s: user not active' %
                          username)
            return None

        # We require a local API key to be on file, as it will be used for
        # subsequent requests.
        if not get_bugzilla_api_key(user):
            logging.warning('Login failure for user %s: no API key in '
                            'database' % username)
            raise WebLoginNeededError()

        return user
    def create(self, request, parent_request_id, reviewers, *args, **kwargs):
        try:
            parent_rr = ReviewRequest.objects.get(pk=parent_request_id)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if not (parent_rr.is_accessible_by(request.user)
                or parent_rr.is_mutable_by(request.user)):
            return PERMISSION_DENIED

        if not is_parent(parent_rr):
            return NOT_PARENT

        # Validate and expand the new reviewer list.

        bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        child_reviewers = json.loads(reviewers)
        invalid_reviewers = []
        for child_rrid in child_reviewers:
            users = []
            for username in child_reviewers[child_rrid]:
                try:
                    users.append(bugzilla.get_user_from_irc_nick(username))
                except User.DoesNotExist:
                    invalid_reviewers.append(username)
            child_reviewers[child_rrid] = users

        if invalid_reviewers:
            # Because this isn't called through Review Board's built-in
            # backbone system, it's dramatically simpler to return just the
            # intended error message instead of categorising the errors by
            # field.
            if len(invalid_reviewers) == 1:
                return INVALID_FORM_DATA.with_message(
                    "The reviewer '%s' was not found" % invalid_reviewers[0])
            else:
                return INVALID_FORM_DATA.with_message(
                    "The reviewers '%s' were not found"
                    % "', '".join(invalid_reviewers))

        # Review Board only supports the submitter updating a review
        # request.  In order for this to work, we publish these changes
        # in Review Board under the review submitter's account, and
        # set an extra_data field which instructs our bugzilla
        # connector to use this request's user when adjusting flags.
        #
        # Updating the review request requires creating a draft and
        # publishing it, so we have to be careful to not overwrite
        # existing drafts.

        try:
            with transaction.atomic():
                for rr in itertools.chain([parent_rr],
                                          gen_child_rrs(parent_rr)):
                    if rr.get_draft() is not None:
                        return REVIEW_REQUEST_UPDATE_NOT_ALLOWED.with_message(
                            "Unable to update reviewers as the review "
                            "request has pending changes (the patch author "
                            "has a draft)")

                try:
                    for child_rr in gen_child_rrs(parent_rr):
                        if str(child_rr.id) in child_reviewers:
                            if not child_rr.is_accessible_by(request.user):
                                return PERMISSION_DENIED.with_message(
                                    "You do not have permission to update "
                                    "reviewers on review request %s"
                                    % child_rr.id)

                            draft = ReviewRequestDraft.create(child_rr)
                            draft.target_people.clear()
                            for user in child_reviewers[str(child_rr.id)]:
                                draft.target_people.add(user)

                    set_publish_as(parent_rr, request.user)
                    parent_rr_draft = ReviewRequestDraft.create(parent_rr)
                    update_parent_rr_reviewers(parent_rr_draft)
                    parent_rr.publish(user=parent_rr.submitter)
                finally:
                    clear_publish_as(parent_rr)

        except PublishError as e:
                logging.error("failed to update reviewers on %s: %s"
                              % (parent_rr.id, str(e)))
                return PUBLISH_ERROR.with_message(str(e))

        return 200, {}
Пример #25
0
    def authenticate(self, username, password, cookie=False):
        username = username.strip()

        # If the user provides an email address when authenticating,
        # it is checked against Review Board's email field in the User
        # Model.  If a match is found, the email will be translated into
        # the username field before being passed into this method's
        # 'username' argument.
        #
        # If a match is not found, 'username' will contain whatever was
        # entered, which may be the Bugzilla login (email address) for a
        # user who does not yet have an entry in the Review Board
        # database.

        if not cookie:
            try:
                username = User.objects.get(username=username).email
            except User.DoesNotExist:
                pass

        # There is a *tiny* probability that this will not work, but only if
        # user A changes their email address, then user B changes their email
        # address to user A's old email, and Review Board doesn't pick up
        # user A's change because they aren't currently involved in any
        # Review Board reviews.  In this case 'username' would have resolved
        # to user A's address.  There's no easy way to detect this without
        # a search on Bugzilla before every log in, and I (mcote) don't think
        # that's worth it for such an improbable event.
        #
        # This also applies to changes to the user's username, since it has
        # to be unique (see get_or_create_bugzilla_users()).

        try:
            bugzilla = Bugzilla()
        except BugzillaUrlError:
            logging.warn('Login failure for user %s: Bugzilla URL not set.'
                         % username)
            return None

        try:
            user_data = bugzilla.log_in(username, password, cookie)
        except BugzillaError as e:
            logging.error('Login failure for user %s: %s' % (username, e))
            return None

        if not user_data:
            return None

        users = get_or_create_bugzilla_users(user_data)

        if not users:
            logging.error('Login failure for user %s: failed to create user.'
                          % username)
            return None

        user = users[0]

        if not user.is_active:
            logging.error('Login failure for user %s: user is not active.'
                          % username)
            return None

        return user