Skip to content

Commit 9bbfa7b

Browse files
authored
Merge pull request #88 from plooploops/fix/add-bearer-token-header
fix(add-bearer-token-header) add authorization header for Bearer token
2 parents e33a137 + 375304e commit 9bbfa7b

File tree

7 files changed

+245
-13
lines changed

7 files changed

+245
-13
lines changed

docs/howto/devTest.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
## Dev-Test
22

3+
### Set up Python Virtual Environment
4+
5+
You can set up a Python development environment with a virtual environment:
6+
7+
```bash
8+
python3 -m venv py3
9+
```
10+
11+
Make sure that you have the virtual environment activated:
12+
13+
```bash
14+
. py3/bin/activate
15+
```
16+
317
### Install poetry
418

519
To use the latest code in this repo (or to develop new features) you can clone this repo, install `poetry`:
@@ -22,11 +36,31 @@ Local development like this:
2236
```
2337
poetry shell
2438
poetry install -vv
25-
python -m pytest
39+
python3 -m pytest
2640
```
2741

2842
There are various ways to select a subset of python unit-tests - see: https://stackoverflow.com/questions/36456920/is-there-a-way-to-specify-which-pytest-tests-to-run-from-a-file
2943

44+
### Manual Testing
45+
46+
You can also set up credentials to submit data to the graph in your data commons. This assumes that you can get API access by downloading your [credentials.json](https://gen3.org/resources/user/using-api/#credentials-to-send-api-requests).
47+
48+
> Make sure that your python virtual environment and dependencies are updated. Also, check that your credentials have appropriate permissions to make the service calls too.
49+
```python
50+
COMMONS_URL = "https://mycommons.azurefd.net"
51+
PROGRAM_NAME = "MyProgram"
52+
PROJECT_NAME = "MyProject"
53+
CREDENTIALS_FILE_PATH = "credentials.json"
54+
gen3_node_json = {
55+
"projects": {"code": PROJECT_NAME},
56+
"type": "core_metadata_collection",
57+
"submitter_id": "core_metadata_collection_myid123456",
58+
}
59+
auth = Gen3Auth(endpoint=COMMONS_URL, refresh_file=CREDENTIALS_FILE_PATH)
60+
sheepdog_client = Gen3Submission(COMMONS_URL, auth)
61+
json_result = sheepdog_client.submit_record(PROGRAM_NAME, PROJECT_NAME, gen3_node_json)
62+
```
63+
3064
### Smoke test
3165

3266
Most of the SDK functionality requires a backend Gen3 environment

gen3/auth.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import time
99
import logging
1010
from urllib.parse import urlparse
11+
import backoff
1112

12-
from gen3.utils import raise_for_status
13+
from gen3.utils import DEFAULT_BACKOFF_SETTINGS, raise_for_status
1314

1415

1516
class Gen3AuthError(Exception):
@@ -256,7 +257,7 @@ def _handle_401(self, response, **kwargs):
256257
return _response
257258

258259
def refresh_access_token(self):
259-
""" Get a new access token """
260+
"""Get a new access token"""
260261
if self._use_wts:
261262
self._access_token = get_access_token_from_wts(
262263
self._wts_namespace, self._wts_idp
@@ -267,21 +268,37 @@ def refresh_access_token(self):
267268
cache_file = token_cache_file(
268269
self._refresh_token and self._refresh_token["api_key"] or self._wts_idp
269270
)
271+
272+
try:
273+
self._write_to_file(cache_file, self._access_token)
274+
except Exception as e:
275+
logging.warning(
276+
f"Exceeded number of retries, unable to write to cache file."
277+
)
278+
279+
return self._access_token
280+
281+
@backoff.on_exception(
282+
wait_gen=backoff.expo, exception=Exception, **DEFAULT_BACKOFF_SETTINGS
283+
)
284+
def _write_to_file(self, cache_file, content):
270285
# write a temp file, then rename - to avoid
271286
# simultaneous writes to same file race condition
272287
temp = cache_file + (
273288
".tmp_eraseme_%d_%d" % (random.randrange(100000), time.time())
274289
)
275290
try:
276291
with open(temp, "w") as f:
277-
f.write(self._access_token)
292+
f.write(content)
278293
os.rename(temp, cache_file)
279-
except:
294+
return True
295+
except Exception as e:
280296
logging.warning("failed to write token cache file: " + cache_file)
281-
return self._access_token
297+
logging.warning(str(e))
298+
raise e
282299

283300
def get_access_token(self):
284-
""" Get the access token - auto refresh if within 5 minutes of expiration """
301+
"""Get the access token - auto refresh if within 5 minutes of expiration"""
285302
if not self._access_token:
286303
cache_file = token_cache_file(
287304
self._refresh_token and self._refresh_token["api_key"] or self._wts_idp
@@ -291,10 +308,12 @@ def get_access_token(self):
291308
with open(cache_file) as f:
292309
self._access_token = f.read()
293310
self._access_token_info = decode_token(self._access_token)
294-
except:
311+
except Exception as e:
295312
logging.warning("ignoring invalid token cache: " + cache_file)
296313
self._access_token = None
297314
self._access_token_info = None
315+
logging.warning(str(e))
316+
298317
need_new_token = (
299318
not self._access_token
300319
or not self._access_token_info

gen3/submission.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import requests
44
import pandas as pd
55
import os
6+
import logging
67

78
from gen3.utils import raise_for_status
89

@@ -198,8 +199,10 @@ def submit_record(self, program, project, json):
198199
199200
"""
200201
api_url = "{}/api/v0/submission/{}/{}".format(self._endpoint, program, project)
202+
logging.info("\nUsing the Sheepdog API URL {}\n".format(api_url))
203+
201204
output = requests.put(api_url, auth=self._auth_provider, json=json)
202-
raise_for_status(output)
205+
output.raise_for_status()
203206
return output.json()
204207

205208
def delete_record(self, program, project, uuid):

gen3/tools/indexing/index_manifest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ def index_object_manifest(
494494
auth(Gen3Auth): Gen3 auth or tuple with basic auth name and password
495495
replace_urls(bool): flag to indicate if replace urls or not
496496
manifest_file_delimiter(str): manifest's delimiter
497+
output_filename(str): output file name for manifest
497498
498499
Returns:
499500
files(list(dict)): list of file info
@@ -520,6 +521,8 @@ def index_object_manifest(
520521
if not commons_url.endswith(service_location):
521522
commons_url += "/" + service_location
522523

524+
logging.info("\nUsing URL {}\n".format(commons_url))
525+
523526
indexclient = client.IndexClient(commons_url, "v0", auth=auth)
524527

525528
# if delimter not specified, try to get based on file ext

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
from gen3.index import Gen3Index
88
from gen3.submission import Gen3Submission
99
from gen3.query import Gen3Query
10+
from gen3.auth import Gen3Auth
1011
import pytest
1112
from drsclient.client import DrsClient
13+
from unittest.mock import call, MagicMock, patch
1214

1315

1416
class MockAuth:
1517
def __init__(self):
1618
self.endpoint = "https://example.commons.com"
19+
self.refresh_token = {"api_key": "123"}
1720

1821

1922
@pytest.fixture
@@ -26,6 +29,17 @@ def gen3_auth():
2629
return MockAuth()
2730

2831

32+
@pytest.fixture
33+
def mock_gen3_auth():
34+
mock_auth = MockAuth()
35+
# patch as __init__ has method call
36+
with patch("gen3.auth.endpoint_from_token") as mock_endpoint_from_token:
37+
mock_endpoint_from_token().return_value = mock_auth.endpoint
38+
return Gen3Auth(
39+
endpoint=mock_auth.endpoint, refresh_token=mock_auth.refresh_token
40+
)
41+
42+
2943
# for unittest with mock server
3044
@pytest.fixture
3145
def index_client(indexd_server):

tests/test_auth.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,103 @@ def test_token_cache():
3838
assert cache_file == expected
3939

4040

41+
def test_refresh_access_token(mock_gen3_auth):
42+
"""
43+
Make sure that access token ends up in header when refresh is called
44+
"""
45+
with patch("gen3.auth.get_access_token_with_key") as mock_access_token:
46+
mock_access_token.return_value = "new_access_token"
47+
with patch("gen3.auth.decode_token") as mock_decode_token:
48+
mock_decode_token().return_value = {"aud": "123"}
49+
with patch("gen3.auth.Gen3Auth._write_to_file") as mock_write_to_file:
50+
mock_write_to_file().return_value = True
51+
with patch(
52+
"gen3.auth.Gen3Auth.__call__",
53+
return_value=MagicMock(
54+
headers={"Authorization": "Bearer new_access_token"}
55+
),
56+
) as mock_call:
57+
access_token = mock_gen3_auth.refresh_access_token()
58+
assert (
59+
"Bearer " + access_token == mock_call().headers["Authorization"]
60+
)
61+
62+
63+
def test_refresh_access_token_no_cache_file(mock_gen3_auth):
64+
"""
65+
Make sure that access token ends up in header when refresh is called after failing to write to cache file
66+
"""
67+
with patch("gen3.auth.get_access_token_with_key") as mock_access_token:
68+
mock_access_token.return_value = "new_access_token"
69+
with patch("gen3.auth.decode_token") as mock_decode_token:
70+
mock_decode_token().return_value = {"aud": "123"}
71+
with patch("gen3.auth.Gen3Auth._write_to_file") as mock_write_to_file:
72+
mock_write_to_file().return_value = False
73+
with patch(
74+
"gen3.auth.Gen3Auth.__call__",
75+
return_value=MagicMock(
76+
headers={"Authorization": "Bearer new_access_token"}
77+
),
78+
) as mock_call:
79+
access_token = mock_gen3_auth.refresh_access_token()
80+
assert (
81+
"Bearer " + access_token == mock_call().headers["Authorization"]
82+
)
83+
84+
85+
def test_write_to_file_success(mock_gen3_auth):
86+
"""
87+
Make sure that you can write content to a file
88+
"""
89+
with patch("builtins.open", create=True) as mock_open_file:
90+
mock_open_file.return_value = MagicMock()
91+
with patch("builtins.open.write") as mock_file_write:
92+
mock_file_write.return_value = True
93+
with patch("os.rename") as mock_os_rename:
94+
mock_os_rename.return_value = True
95+
result = mock_gen3_auth._write_to_file("some_file", "content")
96+
assert result == True
97+
98+
99+
def test_write_to_file_permission_error(mock_gen3_auth):
100+
"""
101+
Check that the file isn't written when there's a PermissionError
102+
"""
103+
with patch("builtins.open", create=True) as mock_open_file:
104+
mock_open_file.return_value = MagicMock()
105+
with patch(
106+
"builtins.open.write", side_effect=PermissionError
107+
) as mock_file_write:
108+
with pytest.raises(FileNotFoundError):
109+
result = mock_gen3_auth._write_to_file("some_file", "content")
110+
111+
112+
def test_write_to_file_rename_permission_error(mock_gen3_auth):
113+
"""
114+
Check that the file isn't written when there's a PermissionError for renaming
115+
"""
116+
with patch("builtins.open", create=True) as mock_open_file:
117+
mock_open_file.return_value = MagicMock()
118+
with patch("builtins.open.write") as mock_file_write:
119+
mock_file_write.return_value = True
120+
with patch("os.rename", side_effect=PermissionError) as mock_os_rename:
121+
with pytest.raises(PermissionError):
122+
result = mock_gen3_auth._write_to_file("some_file", "content")
123+
124+
125+
def test_write_to_file_rename_file_not_found_error(mock_gen3_auth):
126+
"""
127+
Check that the file isn't renamed when there's a FileNotFoundError
128+
"""
129+
with patch("builtins.open", create=True) as mock_open_file:
130+
mock_open_file.return_value = MagicMock()
131+
with patch("builtins.open.write") as mock_file_write:
132+
mock_file_write.return_value = True
133+
with patch("os.rename", side_effect=FileNotFoundError) as mock_os_rename:
134+
with pytest.raises(FileNotFoundError):
135+
result = mock_gen3_auth._write_to_file("some_file", "content")
136+
137+
41138
def test_auth_init_outside_workspace():
42139
"""
43140
Test that a Gen3Auth instance can be initialized when the

tests/test_submission.py

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,12 @@ def test_open_project(sub):
100100

101101

102102
def test_submit_record(sub):
103-
with patch("gen3.submission.requests") as mock_request:
104-
mock_request.status_code = 200
105-
mock_request.json.return_value = '{ "key": "value" }'
103+
"""
104+
Make sure that you can submit a record
105+
"""
106+
with patch("gen3.submission.requests.put") as mock_request:
107+
mock_request().status_code = 200
108+
mock_request().json.return_value = '{ "key": "value" }'
106109
rec = sub.submit_record(
107110
"prog1",
108111
"proj1",
@@ -112,7 +115,66 @@ def test_submit_record(sub):
112115
"type": "experiment",
113116
},
114117
)
115-
assert rec
118+
assert rec == mock_request().json.return_value
119+
120+
121+
def test_submit_record_include_refresh_token(sub):
122+
"""
123+
Make sure that you can submit a record and include a refresh token
124+
"""
125+
sub._auth_provider._refresh_token = {"api_key": "123"}
126+
127+
with patch("gen3.submission.requests.put") as mock_request:
128+
mock_request().status_code = 200
129+
mock_request().json.return_value = '{ "key": "value" }'
130+
rec = sub.submit_record(
131+
"prog1",
132+
"proj1",
133+
{
134+
"projects": [{"code": "proj1"}],
135+
"submitter_id": "mjmartinson",
136+
"type": "experiment",
137+
},
138+
)
139+
assert rec == mock_request().json.return_value
140+
141+
142+
def test_submit_record_include_refresh_token_missing_api_key(sub):
143+
"""
144+
Check that there's a KeyError when submitting a record while missing an api key
145+
"""
146+
sub._auth_provider._refresh_token = {"missing_api_key": "123"}
147+
with patch("gen3.submission.requests.put", side_effect=KeyError) as mock_request:
148+
with pytest.raises(KeyError):
149+
rec = sub.submit_record(
150+
"prog1",
151+
"proj1",
152+
{
153+
"projects": [{"code": "proj1"}],
154+
"submitter_id": "mjmartinson",
155+
"type": "experiment",
156+
},
157+
)
158+
159+
160+
def test_submit_record_include_refresh_token_wrong_api_key(sub):
161+
"""
162+
Check that there's an Exception when submitting a record with the wrong api key
163+
"""
164+
sub._auth_provider._refresh_token = {"api_key": "wrong_api_key"}
165+
with patch(
166+
"gen3.submission.requests.put", side_effect=Exception("invalid jwt token")
167+
) as mock_request:
168+
with pytest.raises(Exception):
169+
rec = sub.submit_record(
170+
"prog1",
171+
"proj1",
172+
{
173+
"projects": [{"code": "proj1"}],
174+
"submitter_id": "mjmartinson",
175+
"type": "experiment",
176+
},
177+
)
116178

117179

118180
def test_export_record(sub):

0 commit comments

Comments
 (0)