Skip to content

Commit 35e6826

Browse files
feat: add get_credentials_by method for OAuth credential exchange with integration filtering
1 parent d32a31d commit 35e6826

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

src/posit/connect/oauth/oauth.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
from __future__ import annotations
22

33
import os
4+
import re
45
from enum import Enum
56

67
from typing_extensions import TYPE_CHECKING, Optional, TypedDict
78

9+
from posit.connect.oauth.associations import ContentItemAssociations
10+
811
from ..resources import Resources
912
from .integrations import Integrations
1013
from .sessions import Sessions
1114

1215
if TYPE_CHECKING:
16+
from posit.connect.external import aws
17+
1318
from ..context import Context
1419

1520
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
@@ -23,6 +28,25 @@ class OAuthTokenType(str, Enum):
2328
USER_SESSION_TOKEN = "urn:posit:connect:user-session-token"
2429

2530

31+
class OAuthIntegrationAuthType(str, Enum):
32+
"""OAuth integration authentication type."""
33+
34+
VIEWER = "Viewer"
35+
SERVICE_ACCOUNT = "Service Account"
36+
VISITOR_API_KEY = "Visitor API Key"
37+
38+
39+
class OAuthIntegrationType(str, Enum):
40+
"""OAuth integration type."""
41+
42+
AWS = "aws"
43+
AZURE = "azure"
44+
CONNECT = "connect"
45+
SNOWFLAKE = "snowflake"
46+
CUSTOM = "custom"
47+
# TODO add the rest
48+
49+
2650
def _get_content_session_token() -> str:
2751
"""Return the content session token.
2852
@@ -99,6 +123,74 @@ def get_content_credentials(
99123
response = self._ctx.client.post(self._path, data=data)
100124
return Credentials(**response.json())
101125

126+
def get_credentials_by(
127+
self,
128+
user_session_token: str,
129+
content_session_token: Optional[str] = None,
130+
integration_type: Optional[OAuthIntegrationType] = None,
131+
auth_type: Optional[OAuthIntegrationAuthType] = None,
132+
name: Optional[str | re.Pattern] = None,
133+
guid: Optional[str] = None,
134+
) -> Credentials | aws.Credentials:
135+
"""Perform an oauth credential exchange for all integrations associated with the current content item."""
136+
content_guid = os.getenv("CONNECT_CONTENT_GUID")
137+
if not content_guid:
138+
raise ValueError("CONNECT_CONTENT_GUID environment variable is required.")
139+
content_associations = ContentItemAssociations(self._ctx, content_guid=content_guid).find()
140+
# associations format: [{
141+
# content_guid: uuid
142+
# app_guid: uuid
143+
# oauth_integration_guid: uuid
144+
# oauth_integration_name: string
145+
# oauth_integration_description: string┃null
146+
# oauth_integration_template: string
147+
# oauth_integration_auth_type: string
148+
# created_time: date-time
149+
# }]
150+
for association in content_associations:
151+
match = True
152+
153+
if (
154+
integration_type is not None
155+
and association.get("oauth_integration_template") != integration_type
156+
):
157+
match = False
158+
159+
if (
160+
auth_type is not None
161+
and association.get("oauth_integration_auth_type") != auth_type
162+
):
163+
match = False
164+
165+
if name is not None:
166+
integration_name = association.get("oauth_integration_name", "")
167+
if isinstance(name, re.Pattern):
168+
if not name.search(integration_name):
169+
match = False
170+
else:
171+
if integration_name != name:
172+
match = False
173+
174+
if guid is not None and association.get("oauth_integration_guid") != guid:
175+
match = False
176+
177+
if match:
178+
# Use the first matching association to get credentials
179+
if association.get("oauth_integration_auth_type") in [
180+
OAuthIntegrationAuthType.VIEWER,
181+
OAuthIntegrationAuthType.VISITOR_API_KEY,
182+
]:
183+
return self.get_credentials(
184+
user_session_token=user_session_token,
185+
audience=association.get("oauth_integration_guid"),
186+
)
187+
return self.get_content_credentials(
188+
content_session_token=content_session_token,
189+
audience=association.get("oauth_integration_guid"),
190+
)
191+
192+
raise ValueError("No matching OAuth integration found for the specified criteria.")
193+
102194

103195
class Credentials(TypedDict, total=False):
104196
access_token: str

0 commit comments

Comments
 (0)