Skip to content

Commit 021cfdd

Browse files
hpatel41KAllan357
andauthored
TDL-13115: Added code change for checking minimal permissions (#123)
* TDL-13115: Added code change for checking minimal permissions * Enhanced some code * Removed nested try-catch for error code handling * Updated some error message Co-authored-by: Kyle Allan <[email protected]>
1 parent 2d9579e commit 021cfdd

File tree

6 files changed

+64
-94
lines changed

6 files changed

+64
-94
lines changed

tap_github/__init__.py

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class RateLimitExceeded(GithubException):
9797
},
9898
404: {
9999
"raise_exception": NotFoundException,
100-
"message": "The resource you have specified cannot be found."
100+
"message": "The resource you have specified cannot be found"
101101
},
102102
409: {
103103
"raise_exception": ConflictError,
@@ -164,25 +164,32 @@ def get_bookmark(state, repo, stream_name, bookmark_key, start_date):
164164
return start_date
165165
return None
166166

167-
def raise_for_error(resp):
167+
def raise_for_error(resp, source):
168+
169+
content_length = len(resp.content)
170+
if content_length == 0:
171+
# There is nothing we can do here since Github has neither sent
172+
# us a 2xx response nor a response content.
173+
return
174+
175+
error_code = resp.status_code
168176
try:
169-
resp.raise_for_status()
170-
except (requests.HTTPError, requests.ConnectionError) as error:
171-
try:
172-
error_code = resp.status_code
173-
try:
174-
response_json = resp.json()
175-
except Exception:
176-
response_json = {}
177-
178-
message = "HTTP-error-code: {}, Error: {}".format(
179-
error_code, ERROR_CODE_EXCEPTION_MAPPING.get(error_code, {}).get("message", "Unknown Error") if response_json == {} else response_json)
180-
181-
exc = ERROR_CODE_EXCEPTION_MAPPING.get(error_code, {}).get("raise_exception", GithubException)
182-
raise exc(message) from None
183-
184-
except (ValueError, TypeError):
185-
raise GithubException(error) from None
177+
response_json = resp.json()
178+
except Exception:
179+
response_json = {}
180+
181+
if error_code == 404:
182+
details = ERROR_CODE_EXCEPTION_MAPPING.get(error_code).get("message")
183+
if source == "teams":
184+
details += ' or it is a personal account repository'
185+
message = "HTTP-error-code: 404, Error: {}. Please refer \'{}\' for more details.".format(details, response_json.get("documentation_url"))
186+
else:
187+
message = "HTTP-error-code: {}, Error: {}".format(
188+
error_code, ERROR_CODE_EXCEPTION_MAPPING.get(error_code, {}).get("message", "Unknown Error") if response_json == {} else response_json)
189+
190+
exc = ERROR_CODE_EXCEPTION_MAPPING.get(error_code, {}).get("raise_exception", GithubException)
191+
raise exc(message) from None
192+
186193
def calculate_seconds(epoch):
187194
current = time.time()
188195
return int(round((epoch - current), 0))
@@ -204,7 +211,7 @@ def authed_get(source, url, headers={}):
204211
session.headers.update(headers)
205212
resp = session.request(method='get', url=url)
206213
if resp.status_code != 200:
207-
raise_for_error(resp)
214+
raise_for_error(resp, source)
208215
return None
209216
else:
210217
timer.tags[metrics.Tag.http_status_code] = resp.status_code
@@ -318,15 +325,7 @@ def verify_repo_access(url_for_repo, repo):
318325
message = "HTTP-error-code: 404, Error: Please check the repository name \'{}\' or you do not have sufficient permissions to access this repository.".format(repo)
319326
raise NotFoundException(message) from None
320327

321-
def verify_org_access(url_for_org, org):
322-
try:
323-
authed_get("verifying organization access", url_for_org)
324-
except NotFoundException:
325-
# throwing user-friendly error message as it shows "Not Found" message
326-
message = "HTTP-error-code: 404, Error: \'{}\' is not an organization.".format(org)
327-
raise NotFoundException(message) from None
328-
329-
def verify_access_for_repo_org(config):
328+
def verify_access_for_repo(config):
330329

331330
access_token = config['access_token']
332331
session.headers.update({'authorization': 'token ' + access_token, 'per_page': '1', 'page': '1'})
@@ -335,18 +334,14 @@ def verify_access_for_repo_org(config):
335334

336335
for repo in repositories:
337336
logger.info("Verifying access of repository: %s", repo)
338-
org = repo.split('/')[0]
339337

340-
url_for_repo = "https://api.github.com/repos/{}/collaborators".format(repo)
341-
url_for_org = "https://api.github.com/orgs/{}/teams".format(org)
338+
url_for_repo = "https://api.github.com/repos/{}/commits".format(repo)
342339

343340
# Verifying for Repo access
344341
verify_repo_access(url_for_repo, repo)
345-
# Verifying for Org access
346-
verify_org_access(url_for_org, org)
347342

348343
def do_discover(config):
349-
verify_access_for_repo_org(config)
344+
verify_access_for_repo(config)
350345
catalog = get_catalog()
351346
# dump catalog
352347
print(json.dumps(catalog, indent=2))

tests/unittests/test_exception_handling.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def __init__(self, status_code, json, raise_error, headers={'X-RateLimit-Remaini
99
self.raise_error = raise_error
1010
self.text = json
1111
self.headers = headers
12+
self.content = "github"
1213

1314
def raise_for_status(self):
1415
if not self.raise_error:
@@ -49,12 +50,21 @@ def test_403_error(self, mocked_request):
4950
self.assertEquals(str(e), "HTTP-error-code: 403, Error: User doesn't have permission to access the resource.")
5051

5152
def test_404_error(self, mocked_request):
52-
mocked_request.return_value = get_response(404, raise_error = True)
53+
json = {"message": "Not Found", "documentation_url": "https:/docs.github.com/"}
54+
mocked_request.return_value = get_response(404, json = json, raise_error = True)
5355

5456
try:
5557
tap_github.authed_get("", "")
5658
except tap_github.NotFoundException as e:
57-
self.assertEquals(str(e), "HTTP-error-code: 404, Error: The resource you have specified cannot be found.")
59+
self.assertEquals(str(e), "HTTP-error-code: 404, Error: The resource you have specified cannot be found. Please refer '{}' for more details.".format(json.get("documentation_url")))
60+
61+
def test_404_error_for_teams(self, mocked_request):
62+
json = {"message": "Not Found", "documentation_url": "https:/docs.github.com/"}
63+
64+
try:
65+
tap_github.raise_for_error(get_response(404, json = json, raise_error = True), "teams")
66+
except tap_github.NotFoundException as e:
67+
self.assertEquals(str(e), "HTTP-error-code: 404, Error: The resource you have specified cannot be found or it is a personal account repository. Please refer '{}' for more details.".format(json.get("documentation_url")))
5868

5969
def test_500_error(self, mocked_request):
6070
mocked_request.return_value = get_response(500, raise_error = True)

tests/unittests/test_formatting_dates.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Mockresponse:
77
def __init__(self, resp, not_list=False):
88
self.not_list = not_list
99
self.json_data = resp
10+
self.content = "github"
1011

1112
def json(self):
1213
if self.not_list:
@@ -34,7 +35,7 @@ def test_due_on_none_without_state(self, mocked_request):
3435
init_state = {}
3536
repo_path = "singer-io/tap-github"
3637

37-
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {})
38+
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {}, "")
3839
# as we will get 1 record and initial bookmark is empty, checking that if bookmark exists in state file returned
3940
self.assertTrue(final_state["bookmarks"][repo_path]["issue_milestones"]["since"])
4041

@@ -51,7 +52,7 @@ def test_due_on_none_with_state(self, mocked_request):
5152
init_state = {'bookmarks': {'singer-io/tap-github': {'issue_milestones': {'since': '2021-05-05T07:20:36.887412Z'}}}}
5253
init_bookmark = singer.utils.strptime_to_utc(init_state["bookmarks"][repo_path]["issue_milestones"]["since"])
5354

54-
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {})
55+
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {}, "")
5556
last_bookmark = singer.utils.strptime_to_utc(final_state["bookmarks"][repo_path]["issue_milestones"]["since"])
5657
# as we will get 1 record, final bookmark will be greater than initial bookmark
5758
self.assertGreater(last_bookmark, init_bookmark)
@@ -70,7 +71,7 @@ def test_due_on_not_none_1(self, mocked_request):
7071
init_state = {'bookmarks': {'singer-io/tap-github': {'issue_milestones': {'since': '2021-05-05T07:20:36.887412Z'}}}}
7172
init_bookmark = singer.utils.strptime_to_utc(init_state["bookmarks"][repo_path]["issue_milestones"]["since"])
7273

73-
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {})
74+
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {}, "")
7475
last_bookmark = singer.utils.strptime_to_utc(final_state["bookmarks"][repo_path]["issue_milestones"]["since"])
7576
# as we will get 1 record, final bookmark will be greater than initial bookmark
7677
self.assertGreater(last_bookmark, init_bookmark)
@@ -88,7 +89,7 @@ def test_due_on_not_none_2(self, mocked_request):
8889
init_state = {'bookmarks': {'singer-io/tap-github': {'issue_milestones': {'since': '2021-05-08T07:20:36.887412Z'}}}}
8990
init_bookmark = init_state["bookmarks"][repo_path]["issue_milestones"]["since"]
9091

91-
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {})
92+
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {}, "")
9293
# as we will get 0 records, initial and final bookmark will be same
9394
self.assertEquals(init_bookmark, final_state["bookmarks"][repo_path]["issue_milestones"]["since"])
9495

@@ -111,7 +112,7 @@ def test_data_containing_both_values(self, mocked_write_record, mocked_request):
111112
init_state = {'bookmarks': {'singer-io/tap-github': {'issue_milestones': {'since': '2021-05-08T07:20:36.887412Z'}}}}
112113
init_bookmark = singer.utils.strptime_to_utc(init_state["bookmarks"][repo_path]["issue_milestones"]["since"])
113114

114-
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {})
115+
final_state = tap_github.get_all_issue_milestones({}, repo_path, init_state, {}, "")
115116
last_bookmark = singer.utils.strptime_to_utc(final_state["bookmarks"][repo_path]["issue_milestones"]["since"])
116117
# as we will get 2 record, final bookmark will be greater than initial bookmark
117118
self.assertGreater(last_bookmark, init_bookmark)

tests/unittests/test_key_error.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
class Mockresponse:
66
def __init__(self, resp):
77
self.json_data = resp
8+
self.content = "github"
89

910
def json(self):
1011
return [(self.json_data)]
@@ -35,7 +36,7 @@ def test_slug_sub_stream_selected_slug_selected(self, mocked_team_members, mocke
3536
"breadcrumb": [ "properties", "name"],
3637
"metadata": {"inclusion": "available"}
3738
}]
38-
tap_github.get_all_teams(schemas, "tap-github", {}, mdata)
39+
tap_github.get_all_teams(schemas, "tap-github", {}, mdata, "")
3940
self.assertEquals(mocked_team_members.call_count, 1)
4041

4142
@mock.patch("tap_github.__init__.get_all_team_members")
@@ -58,7 +59,7 @@ def test_slug_sub_stream_not_selected_slug_selected(self, mocked_team_members, m
5859
"breadcrumb": [ "properties", "name"],
5960
"metadata": {"inclusion": "available"}
6061
}]
61-
tap_github.get_all_teams(schemas, "tap-github", {}, mdata)
62+
tap_github.get_all_teams(schemas, "tap-github", {}, mdata, "")
6263
self.assertEquals(mocked_team_members.call_count, 0)
6364

6465
@mock.patch("tap_github.__init__.get_all_team_members")
@@ -81,7 +82,7 @@ def test_slug_sub_stream_selected_slug_not_selected(self, mocked_team_members, m
8182
"breadcrumb": [ "properties", "name"],
8283
"metadata": {"inclusion": "available"}
8384
}]
84-
tap_github.get_all_teams(schemas, "tap-github", {}, mdata)
85+
tap_github.get_all_teams(schemas, "tap-github", {}, mdata, "")
8586
self.assertEquals(mocked_team_members.call_count, 1)
8687

8788
@mock.patch("tap_github.__init__.get_all_team_members")
@@ -104,7 +105,7 @@ def test_slug_sub_stream_not_selected_slug_not_selected(self, mocked_team_member
104105
"breadcrumb": [ "properties", "name"],
105106
"metadata": {"inclusion": "available"}
106107
}]
107-
tap_github.get_all_teams(schemas, "tap-github", {}, mdata)
108+
tap_github.get_all_teams(schemas, "tap-github", {}, mdata, "")
108109
self.assertEquals(mocked_team_members.call_count, 0)
109110

110111
@mock.patch("tap_github.__init__.authed_get_all_pages")
@@ -130,7 +131,7 @@ def test_user_not_selected_in_stargazers(self, mocked_write_records, mocked_requ
130131
"breadcrumb": ["properties", "starred_at"],
131132
"metadata": {"inclusion": "available"}
132133
}]
133-
tap_github.get_all_stargazers(schemas, "tap-github", {}, mdata)
134+
tap_github.get_all_stargazers(schemas, "tap-github", {}, mdata, "")
134135
self.assertEquals(mocked_write_records.call_count, 1)
135136

136137
@mock.patch("singer.write_record")
@@ -153,5 +154,5 @@ def test_user_selected_in_stargazers(self, mocked_write_records, mocked_request)
153154
"breadcrumb": ["properties", "starred_at"],
154155
"metadata": {"inclusion": "available"}
155156
}]
156-
tap_github.get_all_stargazers(schemas, "tap-github", {}, mdata)
157+
tap_github.get_all_stargazers(schemas, "tap-github", {}, mdata, "")
157158
self.assertEquals(mocked_write_records.call_count, 1)

tests/unittests/test_stargazers_full_table.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ def test_stargazers_without_query_params(self, mocked_request):
99

1010
schemas = {"stargazers": "None"}
1111

12-
tap_github.get_all_stargazers(schemas, "tap-github", {}, {})
12+
tap_github.get_all_stargazers(schemas, "tap-github", {}, {}, "")
1313

1414
mocked_request.assert_called_with(mock.ANY, "https://api.github.com/repos/tap-github/stargazers", mock.ANY)

tests/unittests/test_verify_access.py

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def __init__(self, status_code, json, raise_error, headers={'X-RateLimit-Remaini
99
self.raise_error = raise_error
1010
self.text = json
1111
self.headers = headers
12+
self.content = "github"
1213

1314
def raise_for_status(self):
1415
if not self.raise_error:
@@ -26,7 +27,7 @@ def get_response(status_code, json={}, raise_error=False):
2627
class TestCredentials(unittest.TestCase):
2728

2829
def test_repo_not_found(self, mocked_request):
29-
json = {"message": "Not Found", "documentation_url": "https:/"}
30+
json = {"message": "Not Found", "documentation_url": "https:/docs.github.com/"}
3031
mocked_request.return_value = get_response(404, json, True)
3132

3233
try:
@@ -43,49 +44,14 @@ def test_repo_bad_request(self, mocked_request):
4344
self.assertEquals(str(e), "HTTP-error-code: 400, Error: The request is missing or has a bad parameter.")
4445

4546
def test_repo_bad_creds(self, mocked_request):
46-
json = {"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}
47+
json = {"message": "Bad credentials", "documentation_url": "https://docs.github.com/"}
4748
mocked_request.return_value = get_response(401, json, True)
4849

4950
try:
5051
tap_github.verify_repo_access("", "repo")
5152
except tap_github.BadCredentialsException as e:
5253
self.assertEquals(str(e), "HTTP-error-code: 401, Error: {}".format(json))
5354

54-
def test_org_not_found(self, mocked_request):
55-
json = {"message": "Not Found", "documentation_url": "https:/"}
56-
mocked_request.return_value = get_response(404, json, True)
57-
58-
try:
59-
tap_github.verify_org_access("", "personal-repo")
60-
except tap_github.NotFoundException as e:
61-
self.assertEquals(str(e), "HTTP-error-code: 404, Error: 'personal-repo' is not an organization.")
62-
63-
def test_org_bad_request(self, mocked_request):
64-
mocked_request.return_value = get_response(400, raise_error = True)
65-
66-
try:
67-
tap_github.verify_org_access("", "personal-repo")
68-
except tap_github.BadRequestException as e:
69-
self.assertEquals(str(e), "HTTP-error-code: 400, Error: The request is missing or has a bad parameter.")
70-
71-
def test_org_forbidden(self, mocked_request):
72-
json = {'message': 'Must have admin rights to Repository.', 'documentation_url': 'https://docs.github.com/rest/reference/'}
73-
mocked_request.return_value = get_response(403, json, True)
74-
75-
try:
76-
tap_github.verify_org_access("", "personal-repo")
77-
except tap_github.AuthException as e:
78-
self.assertEquals(str(e), "HTTP-error-code: 403, Error: {}".format(json))
79-
80-
def test_org_bad_creds(self, mocked_request):
81-
json = {"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}
82-
mocked_request.return_value = get_response(401, json, True)
83-
84-
try:
85-
tap_github.verify_org_access("", "personal-repo")
86-
except tap_github.BadCredentialsException as e:
87-
self.assertEquals(str(e), "HTTP-error-code: 401, Error: {}".format(json))
88-
8955
@mock.patch("tap_github.get_catalog")
9056
def test_discover_valid_creds(self, mocked_get_catalog, mocked_request):
9157
mocked_request.return_value = get_response(200)
@@ -97,7 +63,7 @@ def test_discover_valid_creds(self, mocked_get_catalog, mocked_request):
9763

9864
@mock.patch("tap_github.get_catalog")
9965
def test_discover_not_found(self, mocked_get_catalog, mocked_request):
100-
json = {"message": "Not Found", "documentation_url": "https:/"}
66+
json = {"message": "Not Found", "documentation_url": "https:/docs.github.com/"}
10167
mocked_request.return_value = get_response(404, json, True)
10268
mocked_get_catalog.return_value = {}
10369

@@ -120,7 +86,7 @@ def test_discover_bad_request(self, mocked_get_catalog, mocked_request):
12086

12187
@mock.patch("tap_github.get_catalog")
12288
def test_discover_bad_creds(self, mocked_get_catalog, mocked_request):
123-
json = {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}
89+
json = {"message":"Bad credentials","documentation_url":"https://docs.github.com/"}
12490
mocked_request.return_value = get_response(401, json, True)
12591
mocked_get_catalog.return_value = {}
12692

@@ -132,7 +98,7 @@ def test_discover_bad_creds(self, mocked_get_catalog, mocked_request):
13298

13399
@mock.patch("tap_github.get_catalog")
134100
def test_discover_forbidden(self, mocked_get_catalog, mocked_request):
135-
json = {'message': 'Must have admin rights to Repository.', 'documentation_url': 'https://docs.github.com/rest/reference/'}
101+
json = {'message': 'Must have admin rights to Repository.', 'documentation_url': 'https://docs.github.com/'}
136102
mocked_request.return_value = get_response(403, json, True)
137103
mocked_get_catalog.return_value = {}
138104

@@ -145,19 +111,16 @@ def test_discover_forbidden(self, mocked_get_catalog, mocked_request):
145111

146112
@mock.patch("tap_github.logger.info")
147113
@mock.patch("tap_github.verify_repo_access")
148-
@mock.patch("tap_github.verify_org_access")
149114
class TestRepoCallCount(unittest.TestCase):
150-
def test_repo_call_count(self, mocked_org, mocked_repo, mocked_logger_info):
115+
def test_repo_call_count(self, mocked_repo, mocked_logger_info):
151116
"""
152117
Here 3 repos are given,
153118
so tap will check creds for all 3 repos
154119
"""
155-
mocked_org.return_value = None
156120
mocked_repo.return_value = None
157121

158122
config = {"access_token": "access_token", "repository": "org1/repo1 org1/repo2 org2/repo1"}
159-
tap_github.verify_access_for_repo_org(config)
123+
tap_github.verify_access_for_repo(config)
160124

161125
self.assertEquals(mocked_logger_info.call_count, 3)
162-
self.assertEquals(mocked_org.call_count, 3)
163126
self.assertEquals(mocked_repo.call_count, 3)

0 commit comments

Comments
 (0)