Skip to content

Commit 7f3bf8c

Browse files
refactor OAuth integration types and move to new types module; remove creds helper and just rely on usage of content item associations find_by
1 parent 35e6826 commit 7f3bf8c

File tree

5 files changed

+82
-112
lines changed

5 files changed

+82
-112
lines changed

src/posit/connect/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from .context import Context, ContextManager, requires
1212
from .groups import Groups
1313
from .metrics.metrics import Metrics
14-
from .oauth.oauth import OAuth, OAuthTokenType
14+
from .oauth.oauth import OAuth
15+
from .oauth.types import OAuthTokenType
1516
from .resources import _PaginatedResourceSequence, _ResourceSequence
1617
from .sessions import Session
1718
from .system import System

src/posit/connect/external/aws.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from typing_extensions import TYPE_CHECKING, Optional, TypedDict
88

9-
from ..oauth.oauth import OAuthTokenType
9+
from ..oauth.types import OAuthTokenType
1010

1111
if TYPE_CHECKING:
1212
from ..client import Client

src/posit/connect/oauth/associations.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
from __future__ import annotations
44

5-
from typing_extensions import TYPE_CHECKING, List
5+
import re
66

7+
from typing_extensions import TYPE_CHECKING, List, Optional
8+
9+
# from ..context import requires
710
from ..resources import BaseResource, Resources
811

912
if TYPE_CHECKING:
1013
from ..context import Context
14+
from ..oauth import types
1115

1216

1317
class Association(BaseResource):
@@ -63,6 +67,43 @@ def find(self) -> List[Association]:
6367
for result in response.json()
6468
]
6569

70+
# TODO turn this on before merging
71+
# @requires("2025.07.0")
72+
def find_by(
73+
self,
74+
integration_type: Optional[types.OAuthIntegrationType] = None,
75+
auth_type: Optional[types.OAuthIntegrationAuthType] = None,
76+
name: Optional[str] = None,
77+
guid: Optional[str] = None,
78+
) -> Association | None:
79+
for association in self.find():
80+
match = True
81+
82+
if (
83+
integration_type is not None
84+
and association.get("oauth_integration_template") != integration_type
85+
):
86+
match = False
87+
88+
if (
89+
auth_type is not None
90+
and association.get("oauth_integration_auth_type") != auth_type
91+
):
92+
match = False
93+
94+
if name is not None:
95+
integration_name = association.get("oauth_integration_name", "")
96+
if not re.search(name, integration_name):
97+
match = False
98+
99+
if guid is not None and association.get("oauth_integration_guid") != guid:
100+
match = False
101+
102+
if match:
103+
return association
104+
else:
105+
return None
106+
66107
def delete(self) -> None:
67108
"""Delete integration associations."""
68109
data = []

src/posit/connect/oauth/oauth.py

Lines changed: 7 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,17 @@
11
from __future__ import annotations
22

33
import os
4-
import re
5-
from enum import Enum
64

75
from typing_extensions import TYPE_CHECKING, Optional, TypedDict
86

9-
from posit.connect.oauth.associations import ContentItemAssociations
10-
7+
from ..oauth import types
118
from ..resources import Resources
129
from .integrations import Integrations
1310
from .sessions import Sessions
1411

1512
if TYPE_CHECKING:
16-
from posit.connect.external import aws
17-
1813
from ..context import Context
1914

20-
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
21-
22-
23-
class OAuthTokenType(str, Enum):
24-
ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"
25-
AWS_CREDENTIALS = "urn:ietf:params:aws:token-type:credentials"
26-
API_KEY = "urn:posit:connect:api-key"
27-
CONTENT_SESSION_TOKEN = "urn:posit:connect:content-session-token"
28-
USER_SESSION_TOKEN = "urn:posit:connect:user-session-token"
29-
30-
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-
4915

5016
def _get_content_session_token() -> str:
5117
"""Return the content session token.
@@ -85,14 +51,14 @@ def sessions(self):
8551
def get_credentials(
8652
self,
8753
user_session_token: Optional[str] = None,
88-
requested_token_type: Optional[str | OAuthTokenType] = None,
54+
requested_token_type: Optional[str | types.OAuthTokenType] = None,
8955
audience: Optional[str] = None,
9056
) -> Credentials:
9157
"""Perform an oauth credential exchange with a user-session-token."""
9258
# craft a credential exchange request
9359
data = {}
94-
data["grant_type"] = GRANT_TYPE
95-
data["subject_token_type"] = OAuthTokenType.USER_SESSION_TOKEN
60+
data["grant_type"] = types.GRANT_TYPE
61+
data["subject_token_type"] = types.OAuthTokenType.USER_SESSION_TOKEN
9662
if user_session_token:
9763
data["subject_token"] = user_session_token
9864
if requested_token_type:
@@ -106,14 +72,14 @@ def get_credentials(
10672
def get_content_credentials(
10773
self,
10874
content_session_token: Optional[str] = None,
109-
requested_token_type: Optional[str | OAuthTokenType] = None,
75+
requested_token_type: Optional[str | types.OAuthTokenType] = None,
11076
audience: Optional[str] = None,
11177
) -> Credentials:
11278
"""Perform an oauth credential exchange with a content-session-token."""
11379
# craft a credential exchange request
11480
data = {}
115-
data["grant_type"] = GRANT_TYPE
116-
data["subject_token_type"] = OAuthTokenType.CONTENT_SESSION_TOKEN
81+
data["grant_type"] = types.GRANT_TYPE
82+
data["subject_token_type"] = types.OAuthTokenType.CONTENT_SESSION_TOKEN
11783
data["subject_token"] = content_session_token or _get_content_session_token()
11884
if requested_token_type:
11985
data["requested_token_type"] = requested_token_type
@@ -123,74 +89,6 @@ def get_content_credentials(
12389
response = self._ctx.client.post(self._path, data=data)
12490
return Credentials(**response.json())
12591

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-
19492

19593
class Credentials(TypedDict, total=False):
19694
access_token: str

src/posit/connect/oauth/types.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from enum import Enum
2+
3+
GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange"
4+
5+
6+
class OAuthTokenType(str, Enum):
7+
ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token"
8+
AWS_CREDENTIALS = "urn:ietf:params:aws:token-type:credentials"
9+
API_KEY = "urn:posit:connect:api-key"
10+
CONTENT_SESSION_TOKEN = "urn:posit:connect:content-session-token"
11+
USER_SESSION_TOKEN = "urn:posit:connect:user-session-token"
12+
13+
14+
class OAuthIntegrationAuthType(str, Enum):
15+
"""OAuth integration authentication type."""
16+
17+
VIEWER = "Viewer"
18+
SERVICE_ACCOUNT = "Service Account"
19+
VISITOR_API_KEY = "Visitor API Key"
20+
21+
22+
class OAuthIntegrationType(str, Enum):
23+
"""OAuth integration type."""
24+
25+
AWS = "aws"
26+
AZURE = "azure"
27+
CONNECT = "connect"
28+
SNOWFLAKE = "snowflake"
29+
CUSTOM = "custom"
30+
# TODO add the rest

0 commit comments

Comments
 (0)