~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/bugs/model/bug.py

[r=sinzui][bug=475775] Ensure that when a bug is marked as private or
        security_related, the correct direct subscriptions are in place.

Show diffs side-by-side

added added

removed removed

Lines of Context:
811
811
        self.updateHeat()
812
812
        return sub
813
813
 
814
 
    def unsubscribe(self, person, unsubscribed_by):
 
814
    def unsubscribe(self, person, unsubscribed_by, **kwargs):
815
815
        """See `IBug`."""
816
816
        # Drop cached subscription info.
817
817
        clear_property_cache(self)
821
821
        if person is None:
822
822
            person = unsubscribed_by
823
823
 
 
824
        ignore_permissions = kwargs.get('ignore_permissions', False)
824
825
        for sub in self.subscriptions:
825
826
            if sub.person.id == person.id:
826
 
                if not sub.canBeUnsubscribedByUser(unsubscribed_by):
 
827
                if (not ignore_permissions
 
828
                        and not sub.canBeUnsubscribedByUser(unsubscribed_by)):
827
829
                    raise UserCannotUnsubscribePerson(
828
830
                        '%s does not have permission to unsubscribe %s.' % (
829
831
                            unsubscribed_by.displayname,
1644
1646
 
1645
1647
        return bugtask
1646
1648
 
1647
 
    def setPrivate(self, private, who):
1648
 
        """See `IBug`.
1649
 
 
1650
 
        We also record who made the change and when the change took
1651
 
        place.
1652
 
        """
 
1649
    def setPrivacyAndSecurityRelated(self, private, security_related, who):
 
1650
        """ See `IBug`."""
 
1651
        private_changed = False
 
1652
        security_related_changed = False
 
1653
 
 
1654
        # Before we update the privacy or security_related status, we need to
 
1655
        # reconcile the subscribers to avoid leaking private information.
 
1656
        if (self.private != private
 
1657
                or self.security_related != security_related):
 
1658
            self.reconcileSubscribers(private, security_related, who)
 
1659
 
1653
1660
        if self.private != private:
1654
 
            if private:
1655
 
                # Change indirect subscribers into direct subscribers
1656
 
                # *before* setting private because
1657
 
                # getIndirectSubscribers() behaves differently when
1658
 
                # the bug is private.
1659
 
                for person in self.getIndirectSubscribers():
1660
 
                    self.subscribe(person, who)
1661
 
                subscribers_for_who = self.getSubscribersForPerson(who)
1662
 
                if subscribers_for_who.is_empty():
1663
 
                    # We also add `who` as a subscriber, if they're not
1664
 
                    # already directly subscribed or part of a team
1665
 
                    # that's directly subscribed, so that they can
1666
 
                    # see the bug they've just marked private.
1667
 
                    self.subscribe(who, who)
1668
 
 
 
1661
            private_changed = True
1669
1662
            self.private = private
1670
1663
 
1671
1664
            if private:
1680
1673
            for attachment in self.attachments_unpopulated:
1681
1674
                attachment.libraryfile.restricted = private
1682
1675
 
1683
 
            # Correct the heat for the bug immediately, so that we don't have
1684
 
            # to wait for the next calculation job for the adjusted heat.
1685
 
            self.updateHeat()
1686
 
            return True  # Changed.
1687
 
        else:
1688
 
            return False  # Not changed.
1689
 
 
1690
 
    def setSecurityRelated(self, security_related):
1691
 
        """Setter for the `security_related` property."""
1692
1676
        if self.security_related != security_related:
 
1677
            security_related_changed = True
1693
1678
            self.security_related = security_related
1694
1679
 
 
1680
        if private_changed or security_related_changed:
1695
1681
            # Correct the heat for the bug immediately, so that we don't have
1696
1682
            # to wait for the next calculation job for the adjusted heat.
1697
1683
            self.updateHeat()
1698
1684
 
1699
 
            return True  # Changed
1700
 
        else:
1701
 
            return False  # Unchanged
 
1685
        return private_changed, security_related_changed
 
1686
 
 
1687
    def setPrivate(self, private, who):
 
1688
        """See `IBug`.
 
1689
 
 
1690
        We also record who made the change and when the change took
 
1691
        place.
 
1692
        """
 
1693
        return self.setPrivacyAndSecurityRelated(
 
1694
            private, self.security_related, who)[0]
 
1695
 
 
1696
    def setSecurityRelated(self, security_related, who):
 
1697
        """Setter for the `security_related` property."""
 
1698
        return self.setPrivacyAndSecurityRelated(
 
1699
            self.private, security_related, who)[1]
 
1700
 
 
1701
    def getRequiredSubscribers(self, for_private, for_security_related, who):
 
1702
        """Return the mandatory subscribers for a bug with given attributes.
 
1703
 
 
1704
        When a bug is marked as private or security related, it is required
 
1705
        that certain people be subscribed so they can access details about the
 
1706
        bug. The rules are:
 
1707
            security=true, private=true/false ->
 
1708
                subscribers = the reporter + security contact for each task
 
1709
            security=false, private=true ->
 
1710
                subscribers = the reporter + bug supervisor for each task
 
1711
            security=false, private=false ->
 
1712
                subscribers = ()
 
1713
 
 
1714
        If bug supervisor or security contact is unset, fallback to bugtask
 
1715
        reporter/owner.
 
1716
        """
 
1717
        if not for_private and not for_security_related:
 
1718
            return set()
 
1719
        result = set()
 
1720
        result.add(self.owner)
 
1721
        for bugtask in self.bugtasks:
 
1722
            maintainer = bugtask.pillar.owner
 
1723
            if for_security_related:
 
1724
                result.add(bugtask.pillar.security_contact or maintainer)
 
1725
            if for_private:
 
1726
                result.add(bugtask.pillar.bug_supervisor or maintainer)
 
1727
        if for_private:
 
1728
            subscribers_for_who = self.getSubscribersForPerson(who)
 
1729
            if subscribers_for_who.is_empty():
 
1730
                result.add(who)
 
1731
        return result
 
1732
 
 
1733
    def getAutoRemovedSubscribers(self, for_private, for_security_related):
 
1734
        """Return the to be removed subscribers for bug with given attributes.
 
1735
 
 
1736
        When a bug's privacy or security related attributes change, some
 
1737
        existing subscribers may need to be automatically removed.
 
1738
        The rules are:
 
1739
            security=false ->
 
1740
                auto removed subscribers = (bugtask security contacts)
 
1741
            privacy=false ->
 
1742
                auto removed subscribers = (bugtask bug supervisors)
 
1743
 
 
1744
        """
 
1745
        result = set()
 
1746
 
 
1747
        # There has been some discussion as to whether we really want to
 
1748
        # automatically unsubscribe the security contact if a bug is marked
 
1749
        # as no longer security related or the bug supervisor if a bug is no
 
1750
        # longer private.
 
1751
        # We want this behaviour for Launchpad but perhaps not Ubuntu.
 
1752
        # Until this can be clarified, we will not automatically unsubscribe
 
1753
        # anyone.
 
1754
 
 
1755
#        for bugtask in self.bugtasks:
 
1756
#            if not for_security_related and bugtask.pillar.security_contact:
 
1757
#                result.add(bugtask.pillar.security_contact)
 
1758
#            if not for_private and bugtask.pillar.bug_supervisor:
 
1759
#                result.add(bugtask.pillar.bug_supervisor)
 
1760
        return result
 
1761
 
 
1762
    def reconcileSubscribers(self, for_private, for_security_related, who):
 
1763
        """ Ensure only appropriate people are subscribed to private bugs.
 
1764
 
 
1765
        When a bug is marked as either private = True or security_related =
 
1766
        True, we need to ensure that only people who are authorised to know
 
1767
        about the privileged contents of the bug remain directly subscribed
 
1768
        to it. So we:
 
1769
          1. Get the required subscribers depending on the bug status.
 
1770
          2. Get the auto removed subscribers depending on the bug status.
 
1771
             eg security contacts when a bug is updated to security related =
 
1772
             false.
 
1773
          3. Get the allowed subscribers = required subscribers
 
1774
                                            + bugtask owners
 
1775
          4. Remove any current direct subscribers who are not allowed or are
 
1776
             to be auto removed.
 
1777
          5. Add any subscribers who are required.
 
1778
        """
 
1779
        current_direct_subscribers = (
 
1780
            self.getSubscriptionInfo().direct_subscribers)
 
1781
        required_subscribers = self.getRequiredSubscribers(
 
1782
            for_private, for_security_related, who)
 
1783
        auto_removed_subscribers = self.getAutoRemovedSubscribers(
 
1784
            for_private, for_security_related)
 
1785
        allowed_subscribers = set()
 
1786
        allowed_subscribers.add(self.owner)
 
1787
        for bugtask in self.bugtasks:
 
1788
            allowed_subscribers.add(bugtask.owner)
 
1789
            allowed_subscribers.add(bugtask.pillar.owner)
 
1790
            allowed_subscribers.update(set(bugtask.pillar.drivers))
 
1791
        allowed_subscribers = required_subscribers.union(allowed_subscribers)
 
1792
 
 
1793
        subscribers_to_remove = auto_removed_subscribers
 
1794
        if for_private or for_security_related:
 
1795
            subscribers_to_remove = auto_removed_subscribers.union(
 
1796
                current_direct_subscribers.difference(allowed_subscribers))
 
1797
        for subscriber in subscribers_to_remove:
 
1798
            self.unsubscribe(subscriber, who, ignore_permissions=True)
 
1799
 
 
1800
        subscribers_to_add = (
 
1801
            required_subscribers.difference(current_direct_subscribers))
 
1802
        for subscriber in subscribers_to_add:
 
1803
            self.subscribe(subscriber, who)
1702
1804
 
1703
1805
    def getBugTask(self, target):
1704
1806
        """See `IBug`."""