Skip to content

Commit 77c729e

Browse files
feat: auto discover audience for connect api integrations (#423)
checks the current content's assoications and if there is a single connect api integration when there are multiple associations it will grab that guid automatically.
1 parent e4db4fa commit 77c729e

File tree

8 files changed

+387
-12
lines changed

8 files changed

+387
-12
lines changed

integration/tests/posit/connect/oauth/test_associations.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ def test_find_by_integration(self):
9595
no_associations = self.another_integration.associations.find()
9696
assert len(no_associations) == 0
9797

98+
def test_find_by_content(self):
99+
association = self.content.oauth.associations.find_by(integration_type="custom")
100+
assert association is not None
101+
assert association["oauth_integration_guid"] == self.integration["guid"]
102+
103+
no_association = self.content.oauth.associations.find_by(integration_type="connect")
104+
assert no_association is None
105+
98106
def test_find_update_by_content(self):
99107
associations = self.content.oauth.associations.find()
100108
assert len(associations) == 1
@@ -114,3 +122,37 @@ def test_find_update_by_content(self):
114122
self.content.oauth.associations.delete()
115123
no_associations = self.content.oauth.associations.find()
116124
assert len(no_associations) == 0
125+
126+
@pytest.mark.skipif(
127+
CONNECT_VERSION < version.parse("2025.07.0"),
128+
reason="Multi associations not supported.",
129+
)
130+
def test_find_update_by_content_multiple(self):
131+
self.content.oauth.associations.update(
132+
[
133+
self.integration["guid"],
134+
self.another_integration["guid"],
135+
]
136+
)
137+
updated_associations = self.content.oauth.associations.find()
138+
assert len(updated_associations) == 2
139+
for assoc in updated_associations:
140+
assert assoc["app_guid"] == self.content["guid"]
141+
assert assoc["oauth_integration_guid"] in [
142+
self.integration["guid"],
143+
self.another_integration["guid"],
144+
]
145+
146+
associated_connect_integration = self.content.oauth.associations.find_by(
147+
name=".*another.*"
148+
)
149+
assert associated_connect_integration is not None
150+
assert (
151+
associated_connect_integration["oauth_integration_guid"]
152+
== self.another_integration["guid"]
153+
)
154+
155+
# unset content association
156+
self.content.oauth.associations.delete()
157+
no_associations = self.content.oauth.associations.find()
158+
assert len(no_associations) == 0

integration/tests/posit/connect/oauth/test_integrations.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ def test_find(self):
6060
assert results[0] == self.integration
6161
assert results[1] == self.another_integration
6262

63+
def test_find_by(self):
64+
result = self.client.oauth.integrations.find_by(
65+
integration_type="custom",
66+
config={"auth_mode": "Confidential"},
67+
name="example integration",
68+
)
69+
assert result is not None
70+
assert result["guid"] == self.integration["guid"]
71+
72+
result = self.client.oauth.integrations.find_by(
73+
integration_type="custom",
74+
config={"auth_mode": "Confidential"},
75+
name="nonexistent integration",
76+
)
77+
assert result is None
78+
6379
def test_create_update_delete(self):
6480
# create a new integration
6581

src/posit/connect/client.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .groups import Groups
1313
from .metrics.metrics import Metrics
1414
from .oauth.oauth import OAuth
15-
from .oauth.types import OAuthTokenType
15+
from .oauth.types import OAuthIntegrationType, OAuthTokenType
1616
from .resources import _PaginatedResourceSequence, _ResourceSequence
1717
from .sessions import Session
1818
from .system import System
@@ -198,6 +198,10 @@ def with_user_session_token(
198198
----------
199199
token : str
200200
The user session token.
201+
audience : str, optional
202+
The audience for the token exchange. This is the integration GUID of the Connect API integration
203+
that is associate with the content. If not provided when there are multiple integrations, the
204+
function will attempt to determine the audience from the current content associations.
201205
202206
Returns
203207
-------
@@ -260,6 +264,18 @@ def user_profile():
260264
if token is None or token == "":
261265
raise ValueError("token must be set to non-empty string.")
262266

267+
# If the audience is not provided and there are multiple associations,
268+
# we will try to find the Connect API integration GUID from the content resource.
269+
current_content_associations = self.content.get().oauth.associations.find()
270+
if audience is None and len(current_content_associations) > 1:
271+
connect_api_integration_guids = [
272+
a["oauth_integration_guid"]
273+
for a in current_content_associations
274+
if a.get("oauth_integration_template") == OAuthIntegrationType.CONNECT
275+
]
276+
if len(connect_api_integration_guids) == 1:
277+
audience = connect_api_integration_guids[0]
278+
263279
visitor_credentials = self.oauth.get_credentials(
264280
token,
265281
requested_token_type=OAuthTokenType.API_KEY,

src/posit/connect/oauth/associations.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from typing_extensions import TYPE_CHECKING, List, Optional
88

9-
from ..context import requires
109
from ..resources import BaseResource, Resources, _matches_exact, _matches_pattern
1110

1211
if TYPE_CHECKING:
@@ -67,7 +66,6 @@ def find(self) -> List[Association]:
6766
for result in response.json()
6867
]
6968

70-
@requires("2025.07.0-dev")
7169
def find_by(
7270
self,
7371
integration_type: Optional[types.OAuthIntegrationType | str] = None,

src/posit/connect/oauth/integrations.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from typing_extensions import TYPE_CHECKING, List, Optional, overload
88

9-
from ..context import requires
109
from ..resources import (
1110
BaseResource,
1211
Resources,
@@ -132,7 +131,6 @@ def find(self) -> List[Integration]:
132131
for result in response.json()
133132
]
134133

135-
@requires("2025.07.0-dev")
136134
def find_by(
137135
self,
138136
integration_type: Optional[types.OAuthIntegrationType | str] = None,

tests/posit/connect/__api__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/oauth/integrations/associations.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"oauth_integration_guid": "00000000-a27b-4118-ad06-e24459b05126",
1313
"oauth_integration_name": "another integration",
1414
"oauth_integration_description": "another description",
15-
"oauth_integration_template": "custom",
15+
"oauth_integration_template": "connect",
1616
"created_time": "2024-10-02T18:16:09Z"
1717
}
1818
]

tests/posit/connect/oauth/test_associations.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,9 @@ def test(self):
220220
assert found["oauth_integration_name"] == "keycloak integration" # first one
221221

222222
# by multiple criteria
223-
found = associations.find_by(integration_type="custom", name="another integration")
223+
found = associations.find_by(integration_type="connect", name="another integration")
224224
assert found is not None
225-
assert found["oauth_integration_template"] == "custom"
225+
assert found["oauth_integration_template"] == "connect"
226226
assert found["oauth_integration_name"] == "another integration"
227227

228228
# no match

0 commit comments

Comments
 (0)