~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/python -S
#
# Copyright 2009 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Remove personal details of a user from the database, leaving a stub."""

__metaclass__ = type
__all__ = []

import _pythonpath

from optparse import OptionParser
import sys

from canonical.database.sqlbase import connect, sqlvalues
from canonical.launchpad.scripts import db_options, logger_options, logger
from canonical.launchpad.interfaces import (
    PersonCreationRationale, QuestionStatus)
from canonical.launchpad.interfaces.account import AccountStatus

def close_account(con, log, username):
    """Close a person's account.

    Return True on success, or log an error message and return False
    """
    cur = con.cursor()
    cur.execute("""
        SELECT Person.id, Person.account, name, teamowner
        FROM Person LEFT OUTER JOIN EmailAddress
            ON Person.id = EmailAddress.person
        WHERE name=%(username)s or lower(email)=lower(%(username)s)
        """, vars())
    try:
        person_id, account_id, username, teamowner = cur.fetchone()
    except TypeError:
        log.fatal("User %s does not exist" % username)
        return False

    # We don't do teams
    if teamowner is not None:
        log.fatal("%s is a team" % username)
        return False

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

    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

    # 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')
    cur.execute("""
        DELETE FROM EmailAddress WHERE person = %s
        """ % sqlvalues(person_id))

    # Clean out personal details from the Person table
    table_notification('Person')
    unknown_rationale = PersonCreationRationale.UNKNOWN.value
    cur.execute("""
        UPDATE Person
        SET displayname='Removed by request',
            name=%(new_name)s, language=NULL, account=NULL,
            homepage_content=NULL, icon=NULL, mugshot=NULL,
            hide_email_addresses=TRUE, registrant=NULL, logo=NULL,
            creation_rationale=%(unknown_rationale)s, creation_comment=NULL
        WHERE id=%(person_id)s
        """, vars())

    # Remove the Account. We don't set the status to deactivated,
    # as this script is used to satisfy people who insist on us removing
    # all their personal details from our systems. This includes any
    # identification tokens like email addresses or openid identifiers.
    # So the Account record would be unusable, and contain no useful
    # information.
    table_notification('Account')
    if account_id is not None:
        cur.execute("""
            DELETE FROM Account WHERE id = %s
            """ % sqlvalues(account_id))

    # Reassign their bugs
    table_notification('BugTask')
    cur.execute("""
        UPDATE BugTask SET assignee=NULL WHERE assignee=%(person_id)s
        """, vars())

    # Reassign questions assigned to the user, and close all their questions
    # since nobody else can
    table_notification('Question')
    cur.execute("""
        UPDATE Question SET assignee=NULL WHERE assignee=%(person_id)s
        """, vars())
    closed_question_status = QuestionStatus.SOLVED.value
    cur.execute("""
        UPDATE Question
        SET status=%(closed_question_status)s, whiteboard=
            'Closed by Launchpad due to owner requesting account removal'
        WHERE owner=%(person_id)s
        """, vars())

    # Remove rows from tables in simple cases in the given order
    removals = [
        # Trash their email addresses. Unsociable privacy nut jobs who request
        # account removal would be pissed 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 where
        # most concerned with being removed from our systems.
        ('EmailAddress', 'person'),

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

        # Subscriptions
        ('BranchSubscription', 'person'),
        ('BugSubscription', 'person'),
        ('QuestionSubscription', 'person'),
        ('POSubscription', 'person'),
        ('SpecificationSubscription', 'person'),

        # 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
        ('PackageBugSupervisor', 'bug_supervisor'),
        ('AnswerContact', 'person'),

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

        # Access lists
        ('PushMirrorAccess', 'person'),
        ('DistroComponentUploader', 'uploader'),
        ]
    for table, person_id_column in removals:
        table_notification(table)
        cur.execute("""
                DELETE FROM %(table)s WHERE %(person_id_column)s=%(person_id)d
                """ % vars())

    # Trash Sprint Attendance records in the future.
    table_notification('SprintAttendance')
    cur.execute("""
        DELETE FROM SprintAttendance
        USING Sprint
        WHERE Sprint.id = SprintAttendance.sprint
            AND attendee=%(person_id)s
            AND Sprint.time_starts > CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
        """, vars())

    return True

def main():
    parser = OptionParser(
            '%prog [options] (username|email) [...]'
            )
    db_options(parser)
    logger_options(parser)

    (options, args) = parser.parse_args()

    if len(args) == 0:
        parser.error("Must specify username (Person.name)")

    log = logger(options)

    con = None
    try:
        log.debug("Connecting to database")
        con = connect(options.dbuser)
        for username in args:
            if not close_account(con, log, username):
                log.debug("Rolling back")
                con.rollback()
                return 1
        log.debug("Committing changes")
        con.commit()
        return 0
    except:
        log.exception("Unhandled exception")
        log.debug("Rolling back")
        if con is not None:
            con.rollback()
        return 1

if __name__ == '__main__':
    sys.exit(main())