Skip to content

Commit f1f6061

Browse files
committed
Add ability to present Managed Identity derived bearer token
1 parent b06d93b commit f1f6061

2 files changed

Lines changed: 40 additions & 12 deletions

File tree

src/services/dicom/dicom_uploader.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
import os
1010
from typing import Optional
1111

12+
1213
import requests
14+
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
1315

1416
logger = logging.getLogger(__name__)
1517

@@ -20,11 +22,6 @@ def __init__(self, api_endpoint: str | None = None, timeout: int = 30, verify_ss
2022
self.timeout = timeout
2123
self.verify_ssl = verify_ssl
2224

23-
def headers(self) -> dict:
24-
return {
25-
"Authorization": f"Bearer {os.getenv('CLOUD_API_TOKEN', '')}",
26-
}
27-
2825
def upload_dicom(self, sop_instance_uid: str, dicom_stream: io.BufferedReader, action_id: Optional[str]) -> bool:
2926
if not action_id:
3027
logger.error(f"No action_id for {sop_instance_uid}, upload will be rejected by server")
@@ -42,7 +39,7 @@ def upload_dicom(self, sop_instance_uid: str, dicom_stream: io.BufferedReader, a
4239
files=files,
4340
timeout=self.timeout,
4441
verify=self.verify_ssl,
45-
headers=self.headers(),
42+
headers=self.headers,
4643
)
4744

4845
if response.status_code == 201:
@@ -60,3 +57,17 @@ def upload_dicom(self, sop_instance_uid: str, dicom_stream: io.BufferedReader, a
6057
except requests.exceptions.RequestException as e:
6158
logger.error(f"Upload error for {sop_instance_uid}: {e}", exc_info=True)
6259
return False
60+
61+
@property
62+
def headers(self) -> dict:
63+
return {
64+
"Authorization": f"Bearer {self.access_token}",
65+
}
66+
67+
@property
68+
def access_token(self) -> str:
69+
resource = os.getenv("CLOUD_API_RESOURCE")
70+
if resource:
71+
return ManagedIdentityCredential().get_token(resource).token
72+
else:
73+
return os.getenv("CLOUD_API_TOKEN", "")

tests/services/dicom/test_dicom_uploader.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from services.dicom.dicom_uploader import DICOMUploader
1010

1111

12+
@patch("services.dicom.dicom_uploader.requests.put")
1213
class TestDICOMUploader:
1314
@pytest.fixture
1415
def dicom_file(self):
@@ -17,7 +18,6 @@ def dicom_file(self):
1718
tf.close()
1819
yield tf.name
1920

20-
@patch("services.dicom.dicom_uploader.requests.put")
2121
def test_upload_success(self, mock_put, dicom_file):
2222
mock_response = Mock()
2323
mock_response.status_code = 201
@@ -39,7 +39,7 @@ def test_upload_success(self, mock_put, dicom_file):
3939
files=mock_put.call_args[1]["files"],
4040
timeout=30,
4141
verify=True,
42-
headers=uploader.headers(),
42+
headers=uploader.headers,
4343
)
4444

4545
call_kwargs = mock_put.call_args[1]
@@ -49,14 +49,13 @@ def test_upload_success(self, mock_put, dicom_file):
4949
assert isinstance(file_tuple[1], io.BufferedReader)
5050
assert file_tuple[1].read() == open(dicom_file, "rb").read()
5151

52-
def test_upload_without_action_id(self, dicom_file):
52+
def test_upload_without_action_id(self, _, dicom_file):
5353
"""Upload without action_id does not make request."""
5454
uploader = DICOMUploader()
5555
result = uploader.upload_dicom(sop_instance_uid="1.2.3", dicom_stream=open(dicom_file, "rb"), action_id=None)
5656

5757
assert result is False
5858

59-
@patch("services.dicom.dicom_uploader.requests.put")
6059
def test_upload_failure_status_code(self, mock_put, dicom_file):
6160
mock_response = Mock()
6261
mock_response.status_code = 500
@@ -68,7 +67,6 @@ def test_upload_failure_status_code(self, mock_put, dicom_file):
6867

6968
assert result is False
7069

71-
@patch("services.dicom.dicom_uploader.requests.put")
7270
def test_upload_timeout(self, mock_put, dicom_file):
7371
mock_put.side_effect = requests.exceptions.Timeout()
7472

@@ -77,11 +75,30 @@ def test_upload_timeout(self, mock_put, dicom_file):
7775

7876
assert result is False
7977

80-
@patch("services.dicom.dicom_uploader.requests.put")
8178
def test_upload_network_error(self, mock_put, dicom_file):
8279
mock_put.side_effect = requests.exceptions.ConnectionError()
8380

8481
uploader = DICOMUploader()
8582
result = uploader.upload_dicom("1.2.3", open(dicom_file, "rb"), None)
8683

8784
assert result is False
85+
86+
def test_upload_headers_with_managed_identity_access_token(self, _, monkeypatch):
87+
"""Test that headers include access token from ManagedIdentityCredential."""
88+
monkeypatch.setenv("CLOUD_API_RESOURCE", "https://example.com/.default")
89+
with patch("services.dicom.dicom_uploader.ManagedIdentityCredential") as mock_credential:
90+
mock_credential_instance = Mock()
91+
mock_credential_instance.get_token.return_value.token = "fake_access_token"
92+
mock_credential.return_value = mock_credential_instance
93+
94+
assert DICOMUploader().headers == {"Authorization": "Bearer fake_access_token"}
95+
96+
def test_upload_headers_without_managed_identity_resource(self, _, monkeypatch):
97+
"""Test that headers include CLOUD_API_TOKEN if CLOUD_API_RESOURCE is not set."""
98+
monkeypatch.setenv("CLOUD_API_TOKEN", "env_access_token")
99+
100+
assert DICOMUploader().headers == {"Authorization": "Bearer env_access_token"}
101+
102+
def test_upload_headers_without_any_token(self, _):
103+
"""Test that headers include empty token if neither CLOUD_API_RESOURCE nor CLOUD_API_TOKEN is set."""
104+
assert DICOMUploader().headers == {"Authorization": "Bearer "}

0 commit comments

Comments
 (0)