Skip to content

Commit 43354f4

Browse files
Access token and auto add api key paths (#283)
* Added support for using an access token for authorization when making requests. -Giovanni and George * MNT: Automatically add /api-key or /authorized as necessary We can detect when different routes should be hit, so we can automatically handle this for the user. This makes it so that a user doesn't need to set the URL differently based on whether they want to hit an authorized endpoint or not. --------- Co-authored-by: Harrison <pleasant@menloinnovations.com>
1 parent 496caec commit 43354f4

7 files changed

Lines changed: 110 additions & 18 deletions

File tree

README.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,31 @@ To access some unreleased data products and quicklooks, you may
139139
need elevated permissions. To programmatically get that, you need
140140
an API Key, which can be requested from the SDC team.
141141

142-
To use the API Key you can set environment variables and then use
143-
the tool as usual. Note that the api endpoints are prefixed with `/api-key`
144-
to request unreleased data. This will also require an update to the
145-
data access url. So the following should be used when programatically
146-
accessing the data.
142+
To use the API Key you can set the `IMAP_API_KEY` environment variable and then use
143+
the tool as usual.
147144

148145
```bash
149-
IMAP_API_KEY=<your-api-key> IMAP_DATA_ACCESS_URL=https://api.dev.imap-mission.com/api-key imap-data-access ...
146+
IMAP_API_KEY=<your-api-key> imap-data-access ...
150147
```
151148

152149
or with CLI flags
153150

154151
```bash
155-
imap-data-access --api-key <your-api-key> --url https://api.dev.imap-mission.com/api-key ...
152+
imap-data-access --api-key <your-api-key> ...
156153
```
157154

155+
### Automated use with Access token
156+
157+
An alternative to using an API key to access protected data is using an access token provided by LASP's authentication server. LASP's authentication uses [keycloak authentication](https://www.keycloak.org/documentation).
158+
159+
To use an access token with imap-data-access you can set the following environment variable:
160+
161+
```dotenv
162+
IMAP_ACCESS_TOKEN={{Access token from LASP auth server}}
163+
```
164+
165+
Any queries or downloads made with imap-data-access will now use these credentials.
166+
158167
## Troubleshooting
159168

160169
### Network issues

imap_data_access/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
or "https://api.imap-mission.com",
5959
"DATA_DIR": Path(os.getenv("IMAP_DATA_DIR") or Path.cwd() / "data"),
6060
"API_KEY": os.getenv("IMAP_API_KEY"),
61+
"ACCESS_TOKEN": os.getenv("IMAP_ACCESS_TOKEN"),
6162
# Create a base64 encoded string for the username and password
6263
# echo -n 'username:password' | base64
6364
"WEBPODA_TOKEN": os.getenv("IMAP_WEBPODA_TOKEN"),

imap_data_access/io.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,16 @@ def _make_request(request: requests.PreparedRequest):
3232
when making HTTP requests and yield the response body.
3333
"""
3434
logger.debug("Making request: %s", request)
35+
3536
if imap_data_access.config["API_KEY"]:
3637
# Add the API key to the request headers if it exists
3738
request.headers["x-api-key"] = imap_data_access.config["API_KEY"]
38-
39+
elif imap_data_access.config["ACCESS_TOKEN"]:
40+
# Add the access token to the request headers if it exists
41+
# and API key does not exist
42+
request.headers["Authorization"] = (
43+
f"Bearer {imap_data_access.config['ACCESS_TOKEN']}"
44+
)
3945
try:
4046
with requests.Session() as session:
4147
response = session.send(request)
@@ -50,6 +56,23 @@ def _make_request(request: requests.PreparedRequest):
5056
raise IMAPDataAccessError(error_msg) from e
5157

5258

59+
def _get_base_url() -> str:
60+
"""Get the base URL of the data access API.
61+
62+
Adds in the /api-key and /authorized to direct the url
63+
to the proper authorized endpoints as needed.
64+
"""
65+
url = imap_data_access.config["DATA_ACCESS_URL"]
66+
67+
# Only add these if someone hasn't already added the /api-key themselves.
68+
if imap_data_access.config["API_KEY"] and not url.endswith("/api-key"):
69+
url = f"{url}/api-key"
70+
elif imap_data_access.config["ACCESS_TOKEN"] and not url.endswith("/authorized"):
71+
url = f"{url}/authorized"
72+
73+
return url
74+
75+
5376
def download(file_path: Union[Path, str]) -> Path:
5477
"""Download a file from the data archive.
5578
@@ -78,7 +101,7 @@ def download(file_path: Union[Path, str]) -> Path:
78101
logger.info("The file %s already exists, skipping download", destination)
79102
return destination
80103

81-
url = f"{imap_data_access.config['DATA_ACCESS_URL']}/download/{file_path}"
104+
url = f"{_get_base_url()}/download/{file_path}"
82105
logger.info("Downloading file %s from %s to %s", file_path, url, destination)
83106

84107
# Create a request with the provided URL
@@ -279,7 +302,7 @@ def query(
279302
else:
280303
query_params["repointing"] = int(repointing)
281304

282-
url = f"{imap_data_access.config['DATA_ACCESS_URL']}/query"
305+
url = f"{_get_base_url()}/query"
283306
request = requests.Request(method="GET", url=url, params=query_params).prepare()
284307

285308
logger.info("Querying data archive for %s with url %s", query_params, request.url)
@@ -383,7 +406,7 @@ def reprocess(
383406
):
384407
raise ValueError("Not a valid end date, use format 'YYYYMMDD'.")
385408
reprocess_params["reprocessing"] = "True"
386-
url = f"{imap_data_access.config['DATA_ACCESS_URL']}/reprocess"
409+
url = f"{_get_base_url()}/reprocess"
387410
request = requests.Request(
388411
method="POST", url=url, params=reprocess_params
389412
).prepare()
@@ -410,7 +433,7 @@ def upload(file_path: Union[Path, str]) -> None:
410433
raise FileNotFoundError(file_path)
411434

412435
# The upload name needs to be given as a path parameter
413-
url = f"{imap_data_access.config['DATA_ACCESS_URL']}/upload/{file_path.name}"
436+
url = f"{_get_base_url()}/upload/{file_path.name}"
414437
logger.info("Uploading file %s to %s", file_path, url)
415438

416439
# We send a GET request with the filename and the server

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ lint.ignore = ["D104", "D203", "D213", "D413", "PLR2004"]
6161

6262
[tool.ruff.lint.per-file-ignores]
6363
# S101: Use of assert detected
64-
"tests/*" = ["S101", "D"]
64+
"tests/*" = ["D", "PLR0913", "S101"]
6565
"*.ipynb" = ["E501"]
6666
"imap_data_access/io.py" = ["PLR0913", "S310"]
6767
"imap_data_access/webpoda.py" = ["S310"]

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def _set_global_config(monkeypatch: pytest.fixture, tmp_path: pytest.fixture):
1515
imap_data_access.config, "DATA_ACCESS_URL", "https://api.test.com"
1616
)
1717
# Make sure we don't leak any of this content if a user has set them locally
18-
monkeypatch.setitem(imap_data_access.config, "API_KEY", "test_key")
18+
monkeypatch.setitem(imap_data_access.config, "API_KEY", None)
1919
monkeypatch.setitem(imap_data_access.config, "WEBPODA_TOKEN", "test_token")
2020

2121

tests/test_config_options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
("DATA_DIR", Path.cwd() / "data", str(Path("/test/path"))),
1717
("DATA_ACCESS_URL", "https://api.imap-mission.com", "https://test.url"),
1818
("API_KEY", None, "test-api-key"),
19+
("ACCESS_TOKEN", None, "test-access-token"),
1920
],
2021
)
2122
def test_configuration_updates(config_var, default, expected):

tests/test_io.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,52 @@
1010
import requests
1111

1212
import imap_data_access
13-
from imap_data_access.io import _make_request
13+
from imap_data_access.io import _get_base_url, _make_request
1414

1515
test_science_filename = "imap_swe_l1_test-description_20100101_v000.cdf"
1616
test_science_path = "imap/swe/l1/2010/01/" + test_science_filename
1717

1818

19+
@pytest.mark.parametrize(
20+
("url", "api_key", "access_token", "expected"),
21+
[
22+
# Default return base url
23+
("https://api.test.com", None, None, "https://api.test.com"),
24+
# API Key appends /api-key
25+
("https://api.test.com", "test_key", None, "https://api.test.com/api-key"),
26+
# API Key with already specified /api-key doesn't double add it
27+
(
28+
"https://api.test.com/api-key",
29+
"test_key",
30+
None,
31+
"https://api.test.com/api-key",
32+
),
33+
# Access token appends /authorized
34+
("https://api.test.com", None, "test_token", "https://api.test.com/authorized"),
35+
# Access token with already specified /authorized doesn't double add it
36+
(
37+
"https://api.test.com/authorized",
38+
None,
39+
"test_token",
40+
"https://api.test.com/authorized",
41+
),
42+
# API Key takes precedence over access token
43+
(
44+
"https://api.test.com",
45+
"test_key",
46+
"test_token",
47+
"https://api.test.com/api-key",
48+
),
49+
],
50+
)
51+
def test_base_url(url, api_key, access_token, expected, monkeypatch):
52+
"""Test that the base URL is set correctly based on the config."""
53+
monkeypatch.setitem(imap_data_access.config, "DATA_ACCESS_URL", url)
54+
monkeypatch.setitem(imap_data_access.config, "API_KEY", api_key)
55+
monkeypatch.setitem(imap_data_access.config, "ACCESS_TOKEN", access_token)
56+
assert _get_base_url() == expected
57+
58+
1959
def test_redirect(mock_send_request):
2060
"""Verify that we follow a 307 redirect from newly created s3 buckets.
2161
@@ -327,13 +367,23 @@ def test_upload_no_file(mock_send_request):
327367
"upload_file_path", ["a/b/test-file.txt", Path("a/b/test-file.txt")]
328368
)
329369
@pytest.mark.parametrize(
330-
("api_key", "expected_header"),
331-
[(None, {}), ("test-api-key", {"x-api-key": "test-api-key"})],
370+
("api_key", "access_token", "expected_header"),
371+
[
372+
(None, None, {}),
373+
("test-api-key", None, {"x-api-key": "test-api-key"}),
374+
(None, "test-access-token", {"Authorization": "Bearer test-access-token"}),
375+
(
376+
"test-api-key-default",
377+
"test-access-token",
378+
{"x-api-key": "test-api-key-default"},
379+
),
380+
],
332381
)
333382
def test_upload(
334383
mock_send_request,
335384
upload_file_path: str | Path,
336385
api_key: str | None,
386+
access_token: str | None,
337387
expected_header: dict,
338388
monkeypatch,
339389
):
@@ -346,10 +396,13 @@ def test_upload(
346396
The upload file path to test with
347397
api_key : str or None
348398
The API key to use for the upload
399+
access_token : str or None
400+
The access token to use for the upload
349401
expected_header : dict
350402
The expected header to be sent with the request
351403
"""
352404
monkeypatch.setitem(imap_data_access.config, "API_KEY", api_key)
405+
monkeypatch.setitem(imap_data_access.config, "ACCESS_TOKEN", access_token)
353406
mock_send_request.return_value.json.return_value = "https://s3-test-bucket.com"
354407
# Call the upload function
355408
file_to_upload = imap_data_access.config["DATA_DIR"] / upload_file_path
@@ -373,9 +426,14 @@ def test_upload(
373426
]
374427

375428
# First urlopen call should be to get the s3 upload url
429+
auth_path = ""
430+
if api_key:
431+
auth_path = "/api-key"
432+
elif access_token:
433+
auth_path = "/authorized"
376434
request_sent = mock_calls[0].args[0]
377435
called_url = request_sent.url
378-
expected_url_encoded = "https://api.test.com/upload/test-file.txt"
436+
expected_url_encoded = f"https://api.test.com{auth_path}/upload/test-file.txt"
379437
assert called_url == expected_url_encoded
380438
assert request_sent.method == "GET"
381439
# An API key needs to be added to the header for uploads

0 commit comments

Comments
 (0)