blob: 15f1a6a6dbe81da29870d4fbfda6312674ff954b [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (C) 2019-2020 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 logging
import requests
import os
from dateutil.parser import parse
from webkitpy.common.net.credentials import Credentials
_log = logging.getLogger(os.path.basename(__file__))
logging.basicConfig(level=logging.INFO, format='%(message)s')
bugzilla_self_user_id = None
bugzilla_token = None
def get_bugzilla_token():
credentials = Credentials('bugs.webkit.org', git_prefix='bugzilla')
account, password = credentials.read_credentials(use_stored_credentials=True)
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 get_bugs_with_attachments_created_by_user(id):
response = requests.get('https://bugs.webkit.org/rest/bug',
params={'product': ['WebKit', 'Security'], 'f1': 'attachments.submitter', 'o1': 'equals', 'v1': id,
'include_fields': 'id,summary,product', 'token': bugzilla_token})
return response.json()['bugs']
def get_attachments(bug_id):
response = requests.get('https://bugs.webkit.org/rest/bug/' + str(bug_id) + '/attachment',
params={'include_fields': 'id,bug_id,creator,creation_time,summary,is_private', 'token': bugzilla_token})
return response.json()['bugs'][str(bug_id)]
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 hide_attachments(bug_id, attachment_ids):
response = requests.put('https://bugs.webkit.org/rest/bug/attachment/1',
json={'ids': attachment_ids, 'is_private': True},
params={'token': bugzilla_token})
if not response:
print('!!! Failed to hide attachments for bug ' + str(bug_id) + ': ' + 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:
response = input(question + prompt_string).lower()
if default is not None and response == '':
return answers[default]
elif response in answers:
return answers[response]
def sanitized_string(string):
return string.translate(str.maketrans('', '', '\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'])
user_info['bugs_with_attachments_added'] = [x for x in get_bugs_with_attachments_created_by_user(account_id) if x['id'] not in created_bug_ids]
for bug in user_info['bugs_with_attachments_added']:
bug['attachments'] = get_attachments(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']))
if user['bugs_with_attachments_added']:
print('Added attachments to ' + str(len(user['bugs_with_attachments_added'])) + ' bug(s):')
for bug in user['bugs_with_attachments_added']:
print('https://bugs.webkit.org/show_bug.cgi?id=' + str(bug['id']) + ' ' + sanitized_string(bug['summary']))
for attachment in bug['attachments']:
if attachment['creator'] == user['name']:
print('Attachment ' + str(attachment['id']) + ', ' + str(parse(attachment['creation_time'])) + ': ' + sanitized_string(attachment['summary']))
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)
for bug in user['bugs_with_attachments_added']:
attachments_to_hide = []
for attachment in bug['attachments']:
if attachment['creator'] != user['name']:
continue
if attachment['is_private']:
print('Attachment ' + str(attachment['id']) + ' on bug ' + str(bug['id']) + ' is already private, skipping')
continue
assert(attachment['bug_id'] == bug['id'])
attachments_to_hide.append(attachment['id'])
if attachments_to_hide:
print('Deleting attachment(s) from user ' + str(user['name']) + ' on bug ' + str(bug['id']))
hide_attachments(bug['id'], attachments_to_hide)
if __name__ == "__main__":
main()