Skip to content

Commit 19025a6

Browse files
kt474cpkurotori
andauthored
Use IBM Cloud access token to call runtime API (#2102)
* wip use cloud access token * Support staging & add reno * Fallback to apikey * Add integration test * refresh token after expiration * update docstring * address comments * cameron's diff + lint & test fixes Co-authored-by: Cameron Kurotori <[email protected]> * lint * lint again --------- Co-authored-by: Cameron Kurotori <[email protected]>
1 parent 0a9867f commit 19025a6

File tree

6 files changed

+54
-6
lines changed

6 files changed

+54
-6
lines changed

qiskit_ibm_runtime/accounts/account.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def __init__(
272272

273273
def get_auth_handler(self) -> AuthBase:
274274
"""Returns the Cloud authentication handler."""
275-
return CloudAuth(api_key=self.token, crn=self.instance)
275+
return CloudAuth(api_key=self.token, crn=self.instance, private=self.private_endpoint)
276276

277277
def resolve_crn(self) -> None:
278278
"""Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service

qiskit_ibm_runtime/api/auth.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,28 @@
1414

1515
from typing import Dict
1616

17-
17+
import warnings
1818
from requests import PreparedRequest
1919
from requests.auth import AuthBase
2020

21+
from ibm_cloud_sdk_core import IAMTokenManager
22+
from ..utils.utils import cname_from_crn
23+
24+
CLOUD_IAM_URL = "iam.cloud.ibm.com"
25+
STAGING_CLOUD_IAM_URL = "iam.test.cloud.ibm.com"
26+
2127

2228
class CloudAuth(AuthBase):
2329
"""Attaches IBM Cloud Authentication to the given Request object."""
2430

25-
def __init__(self, api_key: str, crn: str):
26-
self.api_key = api_key
31+
def __init__(self, api_key: str, crn: str, private: bool = False):
2732
self.crn = crn
33+
self.api_key = api_key
34+
iam_url = (
35+
f"https://{'private.' if private else ''}"
36+
f"{STAGING_CLOUD_IAM_URL if cname_from_crn(crn) == 'staging' else CLOUD_IAM_URL}"
37+
)
38+
self.tm = IAMTokenManager(api_key, url=iam_url)
2839

2940
def __eq__(self, other: object) -> bool:
3041
if isinstance(other, CloudAuth):
@@ -42,7 +53,14 @@ def __call__(self, r: PreparedRequest) -> PreparedRequest:
4253

4354
def get_headers(self) -> Dict:
4455
"""Return authorization information to be stored in header."""
45-
return {"Service-CRN": self.crn, "Authorization": f"apikey {self.api_key}"}
56+
try:
57+
access_token = self.tm.get_token()
58+
return {"Service-CRN": self.crn, "Authorization": f"Bearer {access_token}"}
59+
except Exception as ex: # pylint: disable=broad-except
60+
warnings.warn(
61+
f"Unable to retrieve IBM Cloud access token. API Key will be used instead. {ex}"
62+
)
63+
return {"Service-CRN": self.crn, "Authorization": f"apikey {self.api_key}"}
4664

4765

4866
class QuantumAuth(AuthBase):

qiskit_ibm_runtime/api/client_parameters.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def __init__(
6262
def get_auth_handler(self) -> Union[CloudAuth, QuantumAuth]:
6363
"""Returns the respective authentication handler."""
6464
if self.channel == "ibm_cloud":
65-
return CloudAuth(api_key=self.token, crn=self.instance)
65+
return CloudAuth(api_key=self.token, crn=self.instance, private=self.private_endpoint)
6666

6767
return QuantumAuth(access_token=self.token)
6868

qiskit_ibm_runtime/utils/utils.py

+15
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,21 @@ def _location_from_crn(crn: str) -> str:
356356
return re.search(pattern, crn).group(6)
357357

358358

359+
def cname_from_crn(crn: str) -> str:
360+
"""Computes the CNAME ('bluemix' or 'staging') from a given CRN.
361+
362+
Args:
363+
crn: A CRN (format: https://cloud.ibm.com/docs/account?topic=account-crn#format-crn)
364+
365+
Returns:
366+
The location.
367+
"""
368+
if is_crn(crn):
369+
pattern = "(.*?):(.*?):(.*?):(.*?):(.*?):(.*?):.*"
370+
return re.search(pattern, crn).group(3)
371+
return None
372+
373+
359374
def to_python_identifier(name: str) -> str:
360375
"""Convert a name to a valid Python identifier.
361376
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
IBM Cloud accounts will now use an access token to call the Qiskit Runtime API instead of the
2+
token provided by the user.

test/integration/test_auth_client.py

+13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ def test_valid_login(self, dependencies: IntegrationTestDependencies) -> None:
3131
client = self._init_auth_client(dependencies.token, dependencies.url)
3232
self.assertTrue(client.access_token)
3333

34+
@integration_test_setup(supported_channel=["ibm_cloud"], init_service=False)
35+
def test_cloud_access_token(self, dependencies: IntegrationTestDependencies) -> None:
36+
"""Test valid cloud authentication."""
37+
params = ClientParameters(
38+
channel="ibm_cloud",
39+
token=dependencies.token,
40+
url=dependencies.url,
41+
instance=dependencies.instance,
42+
)
43+
cloud_auth = params.get_auth_handler()
44+
self.assertTrue(cloud_auth.tm)
45+
self.assertTrue(cloud_auth.tm.access_token())
46+
3447
@integration_test_setup(supported_channel=["ibm_quantum"], init_service=False)
3548
def test_url_404(self, dependencies: IntegrationTestDependencies) -> None:
3649
"""Test login against a 404 URL"""

0 commit comments

Comments
 (0)