示例#1
0
 def findUnfinishedJobs(branch, since=None):
     """See `IRosettaUploadJobSource`."""
     store = IMasterStore(BranchJob)
     match = And(Job.id == BranchJob.jobID, BranchJob.branch == branch,
                 BranchJob.job_type == BranchJobType.ROSETTA_UPLOAD,
                 Job._status != JobStatus.COMPLETED,
                 Job._status != JobStatus.FAILED)
     if since is not None:
         match = And(match, Job.date_created > since)
     jobs = store.using(BranchJob, Job).find((BranchJob), match)
     return jobs
示例#2
0
 def findUnfinishedJobs(branch, since=None):
     """See `IRosettaUploadJobSource`."""
     store = IMasterStore(BranchJob)
     match = And(
         Job.id == BranchJob.jobID,
         BranchJob.branch == branch,
         BranchJob.job_type == BranchJobType.ROSETTA_UPLOAD,
         Job._status != JobStatus.COMPLETED,
         Job._status != JobStatus.FAILED)
     if since is not None:
         match = And(match, Job.date_created > since)
     jobs = store.using(BranchJob, Job).find((BranchJob), match)
     return jobs
示例#3
0
def close_account(username, log):
    """Close a person's account.

    Return True on success, or log an error message and return False
    """
    store = IMasterStore(Person)
    janitor = getUtility(ILaunchpadCelebrities).janitor

    cur = cursor()
    references = list(postgresql.listReferences(cur, 'person', 'id'))
    postgresql.check_indirect_references(references)

    person = store.using(
        Person,
        LeftJoin(EmailAddress, Person.id == EmailAddress.personID)).find(
            Person,
            Or(Person.name == username,
               Lower(EmailAddress.email) == Lower(username))).one()
    if person is None:
        raise LaunchpadScriptFailure("User %s does not exist" % username)
    person_name = person.name

    # We don't do teams
    if person.is_team:
        raise LaunchpadScriptFailure("%s is a team" % person_name)

    log.info("Closing %s's account" % person_name)

    def table_notification(table):
        log.debug("Handling the %s table" % table)

    # All names starting with 'removed' are blacklisted, so this will always
    # succeed.
    new_name = 'removed%d' % person.id

    # Some references can safely remain in place and link to the cleaned-out
    # Person row.
    skip = {
        # These references express some kind of audit trail.  The actions in
        # question still happened, and in some cases the rows may still have
        # functional significance (e.g. subscriptions or access grants), but
        # we no longer identify the actor.
        ('accessartifactgrant', 'grantor'),
        ('accesspolicygrant', 'grantor'),
        ('binarypackagepublishinghistory', 'removed_by'),
        ('branch', 'registrant'),
        ('branchmergeproposal', 'merge_reporter'),
        ('branchmergeproposal', 'merger'),
        ('branchmergeproposal', 'queuer'),
        ('branchmergeproposal', 'registrant'),
        ('branchmergeproposal', 'reviewer'),
        ('branchsubscription', 'subscribed_by'),
        ('bug', 'owner'),
        ('bug', 'who_made_private'),
        ('bugactivity', 'person'),
        ('bugnomination', 'decider'),
        ('bugnomination', 'owner'),
        ('bugtask', 'owner'),
        ('bugsubscription', 'subscribed_by'),
        ('codeimport', 'owner'),
        ('codeimport', 'registrant'),
        ('codeimportevent', 'person'),
        ('faq', 'last_updated_by'),
        ('featureflagchangelogentry', 'person'),
        ('gitactivity', 'changee'),
        ('gitactivity', 'changer'),
        ('gitrepository', 'registrant'),
        ('gitrule', 'creator'),
        ('gitrulegrant', 'grantor'),
        ('gitsubscription', 'subscribed_by'),
        ('message', 'owner'),
        ('messageapproval', 'disposed_by'),
        ('messageapproval', 'posted_by'),
        ('packagecopyrequest', 'requester'),
        ('packagediff', 'requester'),
        ('packageupload', 'signing_key_owner'),
        ('personlocation', 'last_modified_by'),
        ('persontransferjob', 'major_person'),
        ('persontransferjob', 'minor_person'),
        ('poexportrequest', 'person'),
        ('pofile', 'lasttranslator'),
        ('pofiletranslator', 'person'),
        ('product', 'registrant'),
        ('question', 'answerer'),
        ('questionreopening', 'answerer'),
        ('questionreopening', 'reopener'),
        ('snapbuild', 'requester'),
        ('sourcepackagepublishinghistory', 'creator'),
        ('sourcepackagepublishinghistory', 'removed_by'),
        ('sourcepackagepublishinghistory', 'sponsor'),
        ('sourcepackagerecipebuild', 'requester'),
        ('sourcepackagerelease', 'creator'),
        ('sourcepackagerelease', 'maintainer'),
        ('sourcepackagerelease', 'signing_key_owner'),
        ('specification', 'approver'),
        ('specification', 'completer'),
        ('specification', 'drafter'),
        ('specification', 'goal_decider'),
        ('specification', 'goal_proposer'),
        ('specification', 'last_changed_by'),
        ('specification', 'starter'),
        ('structuralsubscription', 'subscribed_by'),
        ('teammembership', 'acknowledged_by'),
        ('teammembership', 'proposed_by'),
        ('teammembership', 'reviewed_by'),
        ('translationimportqueueentry', 'importer'),
        ('translationmessage', 'reviewer'),
        ('translationmessage', 'submitter'),
        ('translationrelicensingagreement', 'person'),
        ('usertouseremail', 'recipient'),
        ('usertouseremail', 'sender'),
        ('xref', 'creator'),

        # This is maintained by trigger functions and a garbo job.  It
        # doesn't need to be updated immediately.
        ('bugsummary', 'viewed_by'),

        # XXX cjwatson 2019-05-02 bug=1827399: This is suboptimal because it
        # does retain some personal information, but it's currently hard to
        # deal with due to the size and complexity of references to it.  We
        # can hopefully provide a garbo job for this eventually.
        ('revisionauthor', 'person'),
    }
    reference_names = {(src_tab, src_col)
                       for src_tab, src_col, _, _, _, _ in references}
    for src_tab, src_col in skip:
        if (src_tab, src_col) not in reference_names:
            raise AssertionError(
                "%s.%s is not a Person reference; possible typo?" %
                (src_tab, src_col))

    # XXX cjwatson 2018-11-29: Registrants could possibly be left as-is, but
    # perhaps we should pretend that the registrant was ~registry in that
    # case instead?

    # Remove the EmailAddress. This is the most important step, as
    # people requesting account removal seem to primarily be interested
    # in ensuring we no longer store this information.
    table_notification('EmailAddress')
    store.find(EmailAddress, EmailAddress.personID == person.id).remove()

    # Clean out personal details from the Person table
    table_notification('Person')
    person.display_name = 'Removed by request'
    person.name = new_name
    person.homepage_content = None
    person.icon = None
    person.mugshot = None
    person.hide_email_addresses = False
    person.registrant = None
    person.logo = None
    person.creation_rationale = PersonCreationRationale.UNKNOWN
    person.creation_comment = None

    # Keep the corresponding PersonSettings row, but reset everything to the
    # defaults.
    table_notification('PersonSettings')
    store.find(PersonSettings, PersonSettings.personID == person.id).set(
        selfgenerated_bugnotifications=DEFAULT,
        # XXX cjwatson 2018-11-29: These two columns have NULL defaults, but
        # perhaps shouldn't?
        expanded_notification_footers=False,
        require_strong_email_authentication=False)
    skip.add(('personsettings', 'person'))

    # Remove almost everything from the Account row and the corresponding
    # OpenIdIdentifier rows, preserving only a minimal audit trail.
    if person.account is not None:
        table_notification('Account')
        account = removeSecurityProxy(person.account)
        account.displayname = 'Removed by request'
        account.creation_rationale = AccountCreationRationale.UNKNOWN
        person.setAccountStatus(AccountStatus.CLOSED, janitor,
                                "Closed using close-account.")

        table_notification('OpenIdIdentifier')
        store.find(OpenIdIdentifier,
                   OpenIdIdentifier.account_id == account.id).remove()

    # Reassign their bugs
    table_notification('BugTask')
    store.find(BugTask, BugTask.assigneeID == person.id).set(assigneeID=None)

    # Reassign questions assigned to the user, and close all their questions
    # in non-final states since nobody else can.
    table_notification('Question')
    store.find(Question, Question.assigneeID == person.id).set(assigneeID=None)
    owned_non_final_questions = store.find(
        Question, Question.ownerID == person.id,
        Question.status.is_in([
            QuestionStatus.OPEN,
            QuestionStatus.NEEDSINFO,
            QuestionStatus.ANSWERED,
        ]))
    owned_non_final_questions.set(
        status=QuestionStatus.SOLVED,
        whiteboard=(
            'Closed by Launchpad due to owner requesting account removal'))
    skip.add(('question', 'owner'))

    # Remove rows from tables in simple cases in the given order
    removals = [
        # Trash their email addresses. People who request complete account
        # removal would be unhappy if they reregistered with their old email
        # address and this resurrected their deleted account, as the email
        # address is probably the piece of data we store that they were most
        # concerned with being removed from our systems.
        ('EmailAddress', 'person'),

        # Trash their codes of conduct and GPG keys
        ('SignedCodeOfConduct', 'owner'),
        ('GpgKey', 'owner'),

        # Subscriptions and notifications
        ('BranchSubscription', 'person'),
        ('BugMute', 'person'),
        ('BugNotificationRecipient', 'person'),
        ('BugSubscription', 'person'),
        ('BugSubscriptionFilterMute', 'person'),
        ('GitSubscription', 'person'),
        ('MailingListSubscription', 'person'),
        ('QuestionSubscription', 'person'),
        ('SpecificationSubscription', 'person'),
        ('StructuralSubscription', 'subscriber'),

        # Personal stuff, freeing up the namespace for others who want to play
        # or just to remove any fingerprints identifying the user.
        ('IrcId', 'person'),
        ('JabberId', 'person'),
        ('WikiName', 'person'),
        ('PersonLanguage', 'person'),
        ('PersonLocation', 'person'),
        ('SshKey', 'person'),

        # Karma
        ('Karma', 'person'),
        ('KarmaCache', 'person'),
        ('KarmaTotalCache', 'person'),

        # Team memberships
        ('TeamMembership', 'person'),
        ('TeamParticipation', 'person'),

        # Contacts
        ('AnswerContact', 'person'),

        # Pending items in queues
        ('POExportRequest', 'person'),

        # Access grants
        ('AccessArtifactGrant', 'grantee'),
        ('AccessPolicyGrant', 'grantee'),
        ('ArchivePermission', 'person'),
        ('GitRuleGrant', 'grantee'),
        ('SharingJob', 'grantee'),

        # Soyuz reporting
        ('LatestPersonSourcePackageReleaseCache', 'creator'),
        ('LatestPersonSourcePackageReleaseCache', 'maintainer'),

        # "Affects me too" information
        ('BugAffectsPerson', 'person'),
    ]
    for table, person_id_column in removals:
        table_notification(table)
        store.execute(
            """
            DELETE FROM %(table)s WHERE %(person_id_column)s = ?
            """ % {
                'table': table,
                'person_id_column': person_id_column,
            }, (person.id, ))

    # Trash Sprint Attendance records in the future.
    table_notification('SprintAttendance')
    store.execute(
        """
        DELETE FROM SprintAttendance
        USING Sprint
        WHERE Sprint.id = SprintAttendance.sprint
            AND attendee = ?
            AND Sprint.time_starts > CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
        """, (person.id, ))
    # Any remaining past sprint attendance records can harmlessly refer to
    # the placeholder person row.
    skip.add(('sprintattendance', 'attendee'))

    # generate_ppa_htaccess currently relies on seeing active
    # ArchiveAuthToken rows so that it knows which ones to remove from
    # .htpasswd files on disk in response to the cancellation of the
    # corresponding ArchiveSubscriber rows; but even once PPA authorisation
    # is handled dynamically, we probably still want to have the per-person
    # audit trail here.
    archive_subscriber_ids = set(
        store.find(
            ArchiveSubscriber.id, ArchiveSubscriber.subscriber_id == person.id,
            ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT))
    if archive_subscriber_ids:
        getUtility(IArchiveSubscriberSet).cancel(archive_subscriber_ids,
                                                 janitor)
    skip.add(('archivesubscriber', 'subscriber'))
    skip.add(('archiveauthtoken', 'person'))

    # Remove hardware submissions.
    table_notification('HWSubmissionDevice')
    store.execute(
        """
        DELETE FROM HWSubmissionDevice
        USING HWSubmission
        WHERE HWSubmission.id = HWSubmissionDevice.submission
            AND owner = ?
        """, (person.id, ))
    table_notification('HWSubmission')
    store.find(HWSubmission, HWSubmission.ownerID == person.id).remove()

    has_references = False

    # Check for active related projects, and skip inactive ones.
    for col in 'bug_supervisor', 'driver', 'owner':
        # Raw SQL because otherwise using Product._owner while displaying it
        # as Product.owner is too fiddly.
        result = store.execute(
            """
            SELECT COUNT(*) FROM product WHERE active AND %(col)s = ?
            """ % {'col': col}, (person.id, ))
        count = result.get_one()[0]
        if count:
            log.error("User %s is still referenced by %d product.%s values" %
                      (person_name, count, col))
            has_references = True
        skip.add(('product', col))
    for col in 'driver', 'owner':
        count = store.find(ProductSeries, ProductSeries.product == Product.id,
                           Product.active,
                           getattr(ProductSeries, col) == person).count()
        if count:
            log.error(
                "User %s is still referenced by %d productseries.%s values" %
                (person_name, count, col))
            has_references = True
        skip.add(('productseries', col))

    # Closing the account will only work if all references have been handled
    # by this point.  If not, it's safer to bail out.  It's OK if this
    # doesn't work in all conceivable situations, since some of them may
    # require careful thought and decisions by a human administrator.
    for src_tab, src_col, ref_tab, ref_col, updact, delact in references:
        if (src_tab, src_col) in skip:
            continue
        result = store.execute(
            """
            SELECT COUNT(*) FROM %(src_tab)s WHERE %(src_col)s = ?
            """ % {
                'src_tab': src_tab,
                'src_col': src_col,
            }, (person.id, ))
        count = result.get_one()[0]
        if count:
            log.error("User %s is still referenced by %d %s.%s values" %
                      (person_name, count, src_tab, src_col))
            has_references = True
    if has_references:
        raise LaunchpadScriptFailure("User %s is still referenced" %
                                     person_name)

    return True