Skip to content

Commit 741353e

Browse files
committed
Check for upload permission based on google group
1 parent 13fb6f2 commit 741353e

File tree

2 files changed

+153
-70
lines changed

2 files changed

+153
-70
lines changed

src/clusterfuzz/_internal/cron/external_testcase_reader.py

+29-5
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

+124-65
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)