blob: edcbe9ed0d2b1e72ae93f83d07fe8f70bd712660 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (C) 2019 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of Apple Inc. ("Apple") nor the names of
# its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
import atexit
import re
import requests
import subprocess
import sys
from dateutil.parser import parse
bugzilla_self_user_id = None
bugzilla_token = None
def get_bugzilla_token():
security = subprocess.Popen(['/usr/bin/security', 'find-internet-password', '-gs', 'bugs.webkit.org'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
security.wait()
if security.returncode:
raise Exception('Could not get bugs.webkit.org credentials from Keychain, exit code ' + str(security.returncode))
for line in security.stdout:
m = re.search('^ "acct"<blob>="(.+)"$', line)
if m:
account = m.group(1)
continue
m = re.search('^password: "(.+)"$', line)
if m:
password = m.group(1)
if not account or not password:
raise Exception('Could not parse security tool output to get bugs.webkit.org credentials')
response = requests.get('https://bugs.webkit.org/rest/login', params={'login': account, 'password': password}).json()
global bugzilla_token
global bugzilla_self_user_id
bugzilla_token = response['token']
bugzilla_self_user_id = response['id']
atexit.register(invalidate_bugzilla_token)
def invalidate_bugzilla_token():
requests.get('https://bugs.webkit.org/rest/logout?' + bugzilla_token)
def get_user_info(id):
response = requests.get('https://bugs.webkit.org/rest/user/' + id,
params={'include_disabled': True, 'token': bugzilla_token}).json()
if len(response['users']) != 1:
raise Exception('Unexpected number of accounts found for ' + id + ': ' + str(len(response['users'])))
return response['users'][0]
def get_user_info_self():
global bugzilla_self_user_id
return get_user_info(str(bugzilla_self_user_id))
def can_use_this_tool():
my_groups = [x['name'] for x in get_user_info_self()['groups']]
return 'admin' in my_groups or ('editbugs' in my_groups and 'editusers' in my_groups and 'Spam-Masters' in my_groups and 'Security-Sensitive' in my_groups)
def disable_user(user_id, reason):
response = requests.put('https://bugs.webkit.org/rest/user/' + str(user_id),
params = {'email_enabled': 0, 'login_denied_text': reason, 'token': bugzilla_token})
response.raise_for_status()
def get_bugs_created_by_user(id):
response = requests.get('https://bugs.webkit.org/rest/bug',
params={'product': ['WebKit', 'Security'], 'email1': id, 'emailreporter1': '1', 'emailtype1': 'equals',
'include_fields': 'id,summary,product', 'token': bugzilla_token})
return response.json()['bugs']
def get_bugs_commented_on_by_user(id):
response = requests.get('https://bugs.webkit.org/rest/bug',
params={'product': ['WebKit', 'Security'], 'email1': id, 'emaillongdesc1': '1', 'emailtype1': 'equals',
'include_fields': 'id,summary,product', 'token': bugzilla_token})
return response.json()['bugs']
def get_comments(bug_id):
response = requests.get('https://bugs.webkit.org/rest/bug/' + str(bug_id) + '/comment', params={'token': bugzilla_token})
return response.json()['bugs'][str(bug_id)]["comments"]
def hide_bug(bug_id):
response = requests.put('https://bugs.webkit.org/rest/bug/' + str(bug_id),
json={'product': 'Spam', 'component': 'Spam', 'version': 'unspecified', 'is_creator_accessible': False, 'is_cc_accessible': False},
params = {'token': bugzilla_token})
if not response:
print '!!! Failed to move bug ' + str(bug_id) + ': ' + response.text
def hide_comments(bug_id, comment_ids):
response = requests.put('https://bugs.webkit.org/rest/bug/' + str(bug_id),
json={'comment_is_private': {str(id): True for id in comment_ids}},
params={'token': bugzilla_token})
if not response:
print '!!! Failed to hide comments for bug' + str(bug_id) + ': ' + response.text
for comment_id in comment_ids:
response = requests.put('https://bugs.webkit.org/rest/bug/comment/' + str(comment_id) + '/tags',
json={'comment_id': comment_id, 'add': ['spam']},
params={'token': bugzilla_token})
if not response:
print '!!! Failed to mark comment with spam tag: ' + response.text
def ask_yes_no(question, default='yes'):
if default is None:
prompt_string = ' [y/n] '
elif default == 'yes':
prompt_string = ' [Y/n] '
elif default == 'no':
prompt_string = ' [y/N] '
answers= {'yes': True, 'y': True, 'ye': True, 'no': False, 'n': False}
while True:
sys.stdout.write(question + prompt_string)
response = raw_input().lower()
if default is not None and response == '':
return answers[default]
elif response in answers:
return answers[response]
def sanitized_string(string):
return string.encode('utf-8').translate(None, '\x1B')
def main():
parser = argparse.ArgumentParser(description='Block Bugzilla spammers, and hide their comments.')
parser.add_argument('accounts', nargs='+', help='accounts to block')
args = parser.parse_args()
get_bugzilla_token()
if not can_use_this_tool():
print 'You need to be a Bugzilla admin to use this tool.'
exit(1)
print 'Fetching account activity...'
users_to_disable = []
for account_id in args.accounts:
user_info = get_user_info(account_id)
users_to_disable.append(user_info)
user_info['bugs_created'] = get_bugs_created_by_user(account_id)
created_bug_ids = [y['id'] for y in user_info['bugs_created']]
user_info['bugs_commented'] = [x for x in get_bugs_commented_on_by_user(account_id) if x['id'] not in created_bug_ids]
for bug in user_info['bugs_commented']:
bug['comments'] = get_comments(bug['id'])
for user in users_to_disable:
print sanitized_string(user['real_name']) + ' <' + user['name'] + '>'
if not user['can_login']:
print '*** already disabled ***'
if user['bugs_created']:
print 'Created ' + str(len(user['bugs_created'])) + ' bug(s):'
for bug in user['bugs_created']:
print 'https://bugs.webkit.org/show_bug.cgi?id=' + str(bug['id']) + ' ' + sanitized_string(bug['summary'])
if user['bugs_commented']:
print 'Commented on ' + str(len(user['bugs_commented'])) + ' bug(s):'
for bug in user['bugs_commented']:
print 'https://bugs.webkit.org/show_bug.cgi?id=' + str(bug['id']) + ' ' + sanitized_string(bug['summary'])
for comment in bug['comments']:
if comment['creator'] == user['name']:
print 'Comment ' + str(comment['count']) + ', ' + str(parse(comment['creation_time'])) + ': ' + sanitized_string(comment['text'])
print
if not ask_yes_no("Block all these accounts, and hide their bugs and comments?"):
print "*** Exiting, no work performed ***"
exit(0)
# Block all of the users first, so that they don't receive e-mail notifications about further actions.
for user in users_to_disable:
if not user['can_login']:
print 'User ' + str(user['name']) + ' is already disabled, skipping'
continue
print 'Disabling user ' + str(user['name'])
disable_user(user['id'], 'spam')
for user in users_to_disable:
for bug in user['bugs_created']:
if bug['product'] == 'Spam':
print 'Bug ' + str(bug['id']) + ' is already in the Spam product, skipping'
continue
print 'Moving bug ' + str(bug['id']) + ' to the Spam product'
hide_bug(bug['id'])
for bug in user['bugs_commented']:
comments_to_hide = []
for comment in bug['comments']:
if comment['creator'] != user['name']:
continue
if comment['is_private']:
print 'Comment ' + str(comment['count']) + ' on bug ' + str(bug['id']) + ' is already private, skipping'
continue
assert(comment['bug_id'] == bug['id'])
comments_to_hide.append(comment['id'])
if comments_to_hide:
print 'Hiding comment(s) from user ' + str(user['name']) + ' on bug ' + str(bug['id'])
hide_comments(bug['id'], comments_to_hide)
if __name__ == "__main__":
main()