Skip to content

Commit 8c3f0f2

Browse files
Check for upload permission based on google group (#4562)
Although the IssueTracker component create controls are locked down to this group, its possible for folks to accidentally migrate bugs into this component. To prevent that from happening, check for the reporter's membership before proceeding with the upload. It is a bit hacky i admit.
1 parent 98f7d7e commit 8c3f0f2

File tree

2 files changed

+153
-70
lines changed

2 files changed

+153
-70
lines changed

src/clusterfuzz/_internal/cron/external_testcase_reader.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import datetime
1717
import re
1818

19+
from google.cloud import storage
1920
import requests
2021

2122
from appengine.libs import form
@@ -31,7 +32,18 @@
3132
ISSUETRACKER_WONTFIX_STATE = 'NOT_REPRODUCIBLE'
3233

3334

34-
def close_issue_if_invalid(upload_request, attachment_info, description):
35+
def get_vrp_uploaders():
36+
"""Checks whether the given reporter has permission to upload."""
37+
# TODO(pgrace) Add this to a YAML file.
38+
storage_client = storage.Client()
39+
bucket = storage_client.bucket('clusterfuzz-vrp-uploaders')
40+
blob = bucket.blob('vrp-uploaders')
41+
members = blob.download_as_string().decode('utf-8').splitlines()[0].split(',')
42+
return members
43+
44+
45+
def close_issue_if_invalid(upload_request, attachment_info, description,
46+
vrp_uploaders):
3547
"""Closes any invalid upload requests with a helpful message."""
3648
comment_message = (
3749
'Hello, this issue is automatically closed. Please file a new bug after'
@@ -42,7 +54,12 @@ def close_issue_if_invalid(upload_request, attachment_info, description):
4254
if upload_request.id == 373893311:
4355
return False
4456

45-
# TODO(pgrace) Add secondary check for authorized reporters.
57+
if not upload_request.reporter in vrp_uploaders:
58+
comment_message += (
59+
'You are not authorized to submit testcases to Clusterfuzz.'
60+
' If you believe you should be, please reach out to'
61+
' [email protected] for assistance.\n')
62+
invalid = True
4663

4764
# Issue must have exactly one attachment.
4865
if len(attachment_info) != 1:
@@ -63,7 +80,7 @@ def close_issue_if_invalid(upload_request, attachment_info, description):
6380

6481
# Issue must have valid flags as the description.
6582
flag_format = re.compile(r'^([ ]?\-\-[A-Za-z\-\_]*){50}$')
66-
if flag_format.match(description):
83+
if description and flag_format.match(description):
6784
comment_message += (
6885
'Please provide flags in the format: "--test_flag_one --testflagtwo",\n'
6986
)
@@ -72,7 +89,7 @@ def close_issue_if_invalid(upload_request, attachment_info, description):
7289
if invalid:
7390
comment_message += (
7491
'\nPlease see the new bug template for more information on how to use'
75-
'Clusterfuzz direct uploads.')
92+
' Clusterfuzz direct uploads.')
7693
upload_request.status = ISSUETRACKER_WONTFIX_STATE
7794
upload_request.save(new_comment=comment_message, notify=True)
7895

@@ -142,6 +159,12 @@ def handle_testcases(tracker):
142159
query_filters=['componentid:1600865', 'id:373893311'],
143160
only_open=True)
144161

162+
if len(issues) == 0:
163+
return
164+
165+
# TODO(pgrace) Cache in redis.
166+
vrp_uploaders = get_vrp_uploaders()
167+
145168
# TODO(pgrace) Implement rudimentary rate limiting.
146169

147170
for issue in issues:
@@ -154,7 +177,8 @@ def handle_testcases(tracker):
154177
# Close out invalid bugs.
155178
attachment_metadata = tracker.get_attachment_metadata(issue.id)
156179
commandline_flags = tracker.get_description(issue.id)
157-
if close_issue_if_invalid(issue, attachment_metadata, commandline_flags):
180+
if close_issue_if_invalid(issue, attachment_metadata, commandline_flags,
181+
vrp_uploaders):
158182
helpers.log('Closing issue {issue_id} as it is invalid', issue.id)
159183
continue
160184

src/clusterfuzz/_internal/tests/appengine/handlers/cron/external_testcase_reader_test.py

Lines changed: 124 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -32,108 +32,131 @@
3232
}
3333

3434

35+
@mock.patch.object(
36+
external_testcase_reader,
37+
'get_vrp_uploaders',
38+
return_value=['[email protected]'],
39+
autospec=True)
40+
@mock.patch.object(external_testcase_reader, 'submit_testcase', autospec=True)
41+
@mock.patch.object(
42+
external_testcase_reader, 'close_issue_if_invalid', autospec=True)
3543
class ExternalTestcaseReaderTest(unittest.TestCase):
36-
"""external_testcase_reader tests."""
44+
"""external_testcase_reader handle main function tests."""
3745

38-
def setUp(self):
39-
self.mock_basic_issue = mock.MagicMock()
40-
self.mock_basic_issue.created_time = '2024-06-25T01:29:30.021Z'
41-
self.mock_basic_issue.status = 'NEW'
42-
external_testcase_reader.submit_testcase = mock.MagicMock()
43-
44-
def test_handle_testcases(self):
46+
def test_handle_testcases(self, mock_close_issue_if_invalid,
47+
mock_submit_testcase, _):
4548
"""Test a basic handle_testcases where issue is fit for submission."""
49+
mock_close_issue_if_invalid.return_value = False
4650
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
47-
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
48-
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
49-
external_testcase_reader.close_issue_if_invalid.return_value = False
51+
basic_issue = mock.MagicMock()
52+
basic_issue.reporter.return_value = '[email protected]'
53+
mock_it.find_issues_with_filters.return_value = [basic_issue]
5054

5155
external_testcase_reader.handle_testcases(mock_it)
52-
external_testcase_reader.close_issue_if_invalid.assert_called_once()
56+
57+
mock_close_issue_if_invalid.assert_called_once()
5358
mock_it.get_attachment.assert_called_once()
54-
external_testcase_reader.submit_testcase.assert_called_once()
59+
mock_submit_testcase.assert_called_once()
5560

56-
def test_handle_testcases_invalid(self):
61+
def test_handle_testcases_invalid(self, mock_close_issue_if_invalid,
62+
mock_submit_testcase, _):
5763
"""Test a basic handle_testcases where issue is invalid."""
64+
mock_close_issue_if_invalid.return_value = True
5865
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
59-
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
60-
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
61-
external_testcase_reader.close_issue_if_invalid.return_value = True
66+
basic_issue = mock.MagicMock()
67+
basic_issue.reporter.return_value = '[email protected]'
68+
mock_it.find_issues_with_filters.return_value = [basic_issue]
6269

6370
external_testcase_reader.handle_testcases(mock_it)
64-
external_testcase_reader.close_issue_if_invalid.assert_called_once()
65-
mock_it.get_attachment.assert_not_called()
66-
external_testcase_reader.submit_testcase.assert_not_called()
6771

68-
def test_handle_testcases_not_reproducible(self):
69-
"""Test a basic handle_testcases where issue is not reprodiclbe."""
72+
mock_close_issue_if_invalid.assert_called_once()
73+
mock_it.get_attachment.assert_not_called()
74+
mock_submit_testcase.assert_not_called()
75+
76+
@mock.patch.object(
77+
external_testcase_reader,
78+
'close_issue_if_not_reproducible',
79+
autospec=True)
80+
def test_handle_testcases_not_reproducible(
81+
self, mock_repro, mock_close_issue_if_invalid, mock_submit_testcase, _):
82+
"""Test a basic handle_testcases where issue is not reproducible."""
83+
mock_repro.return_value = True
7084
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
71-
mock_it.find_issues_with_filters.return_value = [self.mock_basic_issue]
72-
external_testcase_reader.close_issue_if_not_reproducible = mock.MagicMock()
73-
external_testcase_reader.close_issue_if_not_reproducible.return_value = True
74-
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
85+
basic_issue = mock.MagicMock()
86+
basic_issue.reporter.return_value = '[email protected]'
87+
mock_it.find_issues_with_filters.return_value = [basic_issue]
7588

7689
external_testcase_reader.handle_testcases(mock_it)
77-
external_testcase_reader.close_issue_if_invalid.assert_not_called()
90+
91+
mock_close_issue_if_invalid.assert_not_called()
7892
mock_it.get_attachment.assert_not_called()
79-
external_testcase_reader.submit_testcase.assert_not_called()
93+
mock_submit_testcase.assert_not_called()
8094

81-
def test_handle_testcases_no_issues(self):
95+
def test_handle_testcases_no_issues(self, mock_close_issue_if_invalid,
96+
mock_submit_testcase, _):
8297
"""Test a basic handle_testcases that returns no issues."""
8398
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
8499
mock_it.find_issues_with_filters.return_value = []
85-
external_testcase_reader.close_issue_if_invalid = mock.MagicMock()
86100

87101
external_testcase_reader.handle_testcases(mock_it)
88-
external_testcase_reader.close_issue_if_invalid.assert_not_called()
102+
103+
mock_close_issue_if_invalid.assert_not_called()
89104
mock_it.get_attachment.assert_not_called()
90-
external_testcase_reader.submit_testcase.assert_not_called()
105+
mock_submit_testcase.assert_not_called()
91106

92-
def test_close_issue_if_not_reproducible_true(self):
93-
"""Test a basic close_issue_if_invalid with valid flags."""
94-
external_testcase_reader.filed_one_day_ago = mock.MagicMock()
95-
external_testcase_reader.filed_one_day_ago.return_value = True
96-
self.mock_basic_issue.status = 'ACCEPTED'
97-
self.assertEqual(
98-
True,
99-
external_testcase_reader.close_issue_if_not_reproducible(
100-
self.mock_basic_issue))
107+
108+
class ExternalTestcaseReaderInvalidIssueTest(unittest.TestCase):
109+
"""external_testcase_reader close_issue_if_invalid tests."""
110+
111+
def setUp(self):
112+
self.mock_basic_issue = mock.MagicMock()
113+
self.mock_basic_issue.created_time = '2024-06-25T01:29:30.021Z'
114+
self.mock_basic_issue.status = 'NEW'
115+
self.mock_basic_issue.reporter = '[email protected]'
101116

102117
def test_close_issue_if_invalid_basic(self):
103118
"""Test a basic close_issue_if_invalid with valid flags."""
104119
attachment_info = [BASIC_ATTACHMENT]
105120
description = '--flag-one --flag_two'
106-
self.assertEqual(
107-
False,
108-
external_testcase_reader.close_issue_if_invalid(
109-
self.mock_basic_issue, attachment_info, description))
121+
122+
actual = external_testcase_reader.close_issue_if_invalid(
123+
self.mock_basic_issue, attachment_info, description,
124+
125+
126+
self.assertEqual(False, actual)
110127

111128
def test_close_issue_if_invalid_no_flag(self):
112129
"""Test a basic close_issue_if_invalid with no flags."""
113130
attachment_info = [BASIC_ATTACHMENT]
114131
description = ''
115-
self.assertEqual(
116-
False,
117-
external_testcase_reader.close_issue_if_invalid(
118-
self.mock_basic_issue, attachment_info, description))
132+
133+
actual = external_testcase_reader.close_issue_if_invalid(
134+
self.mock_basic_issue, attachment_info, description,
135+
136+
137+
self.assertEqual(False, actual)
119138

120139
def test_close_issue_if_invalid_too_many_attachments(self):
121140
"""Test close_issue_if_invalid with too many attachments."""
122141
attachment_info = [BASIC_ATTACHMENT, BASIC_ATTACHMENT]
123142
description = ''
124-
self.assertEqual(
125-
True,
126-
external_testcase_reader.close_issue_if_invalid(
127-
self.mock_basic_issue, attachment_info, description))
143+
144+
actual = external_testcase_reader.close_issue_if_invalid(
145+
self.mock_basic_issue, attachment_info, description,
146+
147+
148+
self.assertEqual(True, actual)
128149

129150
def test_close_issue_if_invalid_no_attachments(self):
130151
"""Test close_issue_if_invalid with no attachments."""
131152
attachment_info = []
132153
description = ''
133-
self.assertEqual(
134-
True,
135-
external_testcase_reader.close_issue_if_invalid(
136-
self.mock_basic_issue, attachment_info, description))
154+
155+
actual = external_testcase_reader.close_issue_if_invalid(
156+
self.mock_basic_issue, attachment_info, description,
157+
158+
159+
self.assertEqual(True, actual)
137160

138161
def test_close_issue_if_invalid_invalid_upload(self):
139162
"""Test close_issue_if_invalid with an invalid upload."""
@@ -146,10 +169,12 @@ def test_close_issue_if_invalid_invalid_upload(self):
146169
'etag': 'TXpjek9Ea3pNekV4TFRZd01USTNOalk0TFRjNE9URTROVFl4TlE9PQ=='
147170
}]
148171
description = ''
149-
self.assertEqual(
150-
True,
151-
external_testcase_reader.close_issue_if_invalid(
152-
self.mock_basic_issue, attachment_info, description))
172+
173+
actual = external_testcase_reader.close_issue_if_invalid(
174+
self.mock_basic_issue, attachment_info, description,
175+
176+
177+
self.assertEqual(True, actual)
153178

154179
def test_close_issue_if_invalid_invalid_content_type(self):
155180
"""Test close_issue_if_invalid with an invalid content type."""
@@ -164,7 +189,41 @@ def test_close_issue_if_invalid_invalid_content_type(self):
164189
'etag': 'TXpjek9Ea3pNekV4TFRZd01USTNOalk0TFRjNE9URTROVFl4TlE9PQ=='
165190
}]
166191
description = ''
167-
self.assertEqual(
168-
True,
169-
external_testcase_reader.close_issue_if_invalid(
170-
self.mock_basic_issue, attachment_info, description))
192+
actual = external_testcase_reader.close_issue_if_invalid(
193+
self.mock_basic_issue, attachment_info, description,
194+
195+
196+
self.assertEqual(True, actual)
197+
198+
def test_close_issue_if_invalid_no_permission(self):
199+
"""Test close_issue_if_invalid with an no upload permission."""
200+
attachment_info = [BASIC_ATTACHMENT]
201+
description = ''
202+
actual = external_testcase_reader.close_issue_if_invalid(
203+
self.mock_basic_issue, attachment_info, description, [])
204+
205+
self.assertEqual(True, actual)
206+
207+
208+
class ExternalTestcaseReaderPermissionTest(unittest.TestCase):
209+
"""external_testcase_reader get_vrp_uploaders tests."""
210+
211+
def test_get_vrp_uploaders(self):
212+
"""Test get_vrp_uploaders."""
213+
with mock.patch(
214+
'src.clusterfuzz._internal.cron.external_testcase_reader.storage.Client'
215+
) as mock_storage:
216+
mock_storage.return_value = mock.MagicMock()
217+
mock_bucket = mock.MagicMock()
218+
mock_storage.return_value.bucket.return_value = mock_bucket
219+
mock_blob = mock.MagicMock()
220+
mock_bucket.blob.return_value = mock_blob
221+
mock_blob.download_as_string.return_value = "[email protected],[email protected]".encode(
222+
'utf-8')
223+
224+
actual = external_testcase_reader.get_vrp_uploaders()
225+
mock_storage.return_value.bucket.assert_called_once_with(
226+
'clusterfuzz-vrp-uploaders')
227+
mock_bucket.blob.assert_called_once_with('vrp-uploaders')
228+
self.assertEqual(actual,
229+

0 commit comments

Comments
 (0)