Skip to content

Commit 67c7af3

Browse files
committed
initial proposal for client credentials support in python sdk
1 parent 4051bfb commit 67c7af3

File tree

2 files changed

+75
-2
lines changed

2 files changed

+75
-2
lines changed

src/posit/connect/external/databricks.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,46 @@ def auth_type(self) -> str:
2828
def __call__(self, *args, **kwargs) -> CredentialsProvider:
2929
raise NotImplementedError
3030

31+
# TODO: Refactor common behavior across different cred providers.
32+
33+
class PositContentCredentialsProvider:
34+
def __init__(self, client: Client):
35+
self._client = client
36+
37+
def __call__(self) -> Dict[str, str]:
38+
credentials = self._client.oauth.get_content_credentials()
39+
access_token = credentials.get("access_token")
40+
if access_token is None:
41+
raise ValueError("Missing value for field 'access_token' in credentials.")
42+
return {"Authorization": f"Bearer {access_token}"}
43+
44+
class PositContentCredentialsStrategy:
45+
def __init__(
46+
self,
47+
local_strategy: CredentialsStrategy,
48+
client: Optional[Client] = None,
49+
):
50+
self._local_strategy = local_strategy
51+
self._client = client
52+
53+
def sql_credentials_provider(self, *args, **kwargs):
54+
return lambda: self.__call__(*args, **kwargs)
55+
56+
def auth_type(self) -> str:
57+
if is_local():
58+
return self._local_strategy.auth_type()
59+
else:
60+
return "posit-oauth-integration"
61+
62+
def __call__(self, *args, **kwargs) -> CredentialsProvider:
63+
if is_local():
64+
return self._local_strategy(*args, **kwargs)
65+
66+
if self._client is None:
67+
self._client = Client()
68+
69+
return PositContentCredentialsProvider(self._client)
70+
3171

3272
class PositCredentialsProvider:
3373
def __init__(self, client: Client, user_session_token: str):

src/posit/connect/oauth/oauth.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from typing import Optional
45

56
from typing_extensions import TypedDict
@@ -8,6 +9,27 @@
89
from .integrations import Integrations
910
from .sessions import Sessions
1011

12+
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
13+
USER_SESSION_TOKEN_TYPE = "urn:posit:connect:user-session-token"
14+
CONTENT_SESSION_TOKEN_TYPE = "urn:posit:connect:content-session-token"
15+
16+
def _get_content_session_token() -> str:
17+
"""Return the content session token.
18+
19+
Reads the environment variable 'CONNECT_CONTENT_SESSION_TOKEN'.
20+
21+
Raises
22+
------
23+
ValueError: If CONNECT_CONTENT_SESSION_TOKEN is not set or invalid
24+
25+
Returns
26+
-------
27+
str
28+
"""
29+
value = os.environ.get("CONNECT_CONTENT_SESSION_TOKEN")
30+
if not value:
31+
raise ValueError("Invalid value for 'CONNECT_CONTENT_SESSION_TOKEN': Must be a non-empty string.")
32+
return value
1133

1234
class OAuth(Resources):
1335
def __init__(self, params: ResourceParameters, api_key: str) -> None:
@@ -27,14 +49,25 @@ def get_credentials(self, user_session_token: Optional[str] = None) -> Credentia
2749

2850
# craft a credential exchange request
2951
data = {}
30-
data["grant_type"] = "urn:ietf:params:oauth:grant-type:token-exchange"
31-
data["subject_token_type"] = "urn:posit:connect:user-session-token"
52+
data["grant_type"] = GRANT_TYPE
53+
data["subject_token_type"] = USER_SESSION_TOKEN_TYPE
3254
if user_session_token:
3355
data["subject_token"] = user_session_token
3456

3557
response = self.params.session.post(url, data=data)
3658
return Credentials(**response.json())
3759

60+
def get_content_credentials(self, content_session_token: Optional[str] = None) -> Credentials:
61+
url = self.params.url + "v1/oauth/integrations/credentials"
62+
63+
# craft a credential exchange request
64+
data = {}
65+
data["grant_type"] = GRANT_TYPE
66+
data["subject_token_type"] = CONTENT_SESSION_TOKEN_TYPE
67+
data["subject_token"] = content_session_token or _get_content_session_token()
68+
69+
response = self.params.session.post(url, data=data)
70+
return Credentials(**response.json())
3871

3972
class Credentials(TypedDict, total=False):
4073
access_token: str

0 commit comments

Comments
 (0)