Skip to content

Commit 3832b43

Browse files
authored
feat: Implements OAuth association API resource (#286)
Adds support for requesting and updating oauth associations through the Connect API.
1 parent 5c6eed3 commit 3832b43

File tree

11 files changed

+423
-6
lines changed

11 files changed

+423
-6
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
from packaging import version
5+
6+
from posit import connect
7+
8+
from .. import CONNECT_VERSION
9+
10+
11+
@pytest.mark.skipif(
12+
CONNECT_VERSION <= version.parse("2024.06.0"),
13+
reason="OAuth Integrations not supported.",
14+
)
15+
class TestAssociations:
16+
@classmethod
17+
def setup_class(cls):
18+
cls.client = connect.Client()
19+
cls.integration = cls.client.oauth.integrations.create(
20+
name="example integration",
21+
description="integration description",
22+
template="custom",
23+
config={
24+
"auth_mode": "Confidential",
25+
"authorization_uri": "https://example.com/__tenand_id__/oauth2/v2.0/authorize",
26+
"client_id": "client_id",
27+
"client_secret": "client_secret",
28+
"scopes": "a b c",
29+
"token_endpoint_auth_method": "client_secret_post",
30+
"token_uri": "https://example.com/__tenant_id__/oauth2/v2.0/token",
31+
},
32+
)
33+
34+
cls.another_integration = cls.client.oauth.integrations.create(
35+
name="another example integration",
36+
description="another integration description",
37+
template="custom",
38+
config={
39+
"auth_mode": "Confidential",
40+
"authorization_uri": "https://example.com/__tenand_id__/oauth2/v2.0/authorize",
41+
"client_id": "client_id",
42+
"client_secret": "client_secret",
43+
"scopes": "a b c",
44+
"token_endpoint_auth_method": "client_secret_post",
45+
"token_uri": "https://example.com/__tenant_id__/oauth2/v2.0/token",
46+
},
47+
)
48+
49+
# create content
50+
# requires full bundle deployment to produce an interactive content type
51+
cls.content = cls.client.content.create(name="example-flask-minimal")
52+
# create bundle
53+
path = Path(
54+
"../../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz"
55+
)
56+
path = (Path(__file__).parent / path).resolve()
57+
bundle = cls.content.bundles.create(str(path))
58+
# deploy bundle
59+
task = bundle.deploy()
60+
task.wait_for()
61+
62+
cls.content.oauth.associations.update(cls.integration["guid"])
63+
64+
@classmethod
65+
def teardown_class(cls):
66+
cls.integration.delete()
67+
cls.another_integration.delete()
68+
assert len(cls.client.oauth.integrations.find()) == 0
69+
70+
cls.content.delete()
71+
assert cls.client.content.count() == 0
72+
73+
def test_find_by_integration(self):
74+
associations = self.integration.associations.find()
75+
assert len(associations) == 1
76+
assert (
77+
associations[0]["oauth_integration_guid"]
78+
== self.integration["guid"]
79+
)
80+
81+
no_associations = self.another_integration.associations.find()
82+
assert len(no_associations) == 0
83+
84+
def test_find_update_by_content(self):
85+
associations = self.content.oauth.associations.find()
86+
assert len(associations) == 1
87+
assert associations[0]["app_guid"] == self.content["guid"]
88+
assert (
89+
associations[0]["oauth_integration_guid"]
90+
== self.integration["guid"]
91+
)
92+
93+
# update content association to another_integration
94+
self.content.oauth.associations.update(
95+
self.another_integration["guid"]
96+
)
97+
updated_associations = self.content.oauth.associations.find()
98+
assert len(updated_associations) == 1
99+
assert updated_associations[0]["app_guid"] == self.content["guid"]
100+
assert (
101+
updated_associations[0]["oauth_integration_guid"]
102+
== self.another_integration.guid
103+
)
104+
105+
# unset content association
106+
self.content.oauth.associations.delete()
107+
no_associations = self.content.oauth.associations.find()
108+
assert len(no_associations) == 0

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def teardown_class(cls):
5151
assert len(cls.client.oauth.integrations.find()) == 0
5252

5353
def test_get(self):
54-
result = self.client.oauth.integrations.get(self.integration.guid)
54+
result = self.client.oauth.integrations.get(self.integration["guid"])
5555
assert result == self.integration
5656

5757
def test_find(self):
@@ -78,7 +78,7 @@ def test_create_update_delete(self):
7878
},
7979
)
8080

81-
created = self.client.oauth.integrations.get(integration.guid)
81+
created = self.client.oauth.integrations.get(integration["guid"])
8282
assert created == integration
8383

8484
all_integrations = self.client.oauth.integrations.find()
@@ -87,7 +87,7 @@ def test_create_update_delete(self):
8787
# update the new integration
8888

8989
created.update(name="updated integration name")
90-
updated = self.client.oauth.integrations.get(integration.guid)
90+
updated = self.client.oauth.integrations.get(integration["guid"])
9191
assert updated.name == "updated integration name"
9292

9393
# delete the new integration

src/posit/connect/client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
from requests import Response, Session
88

9-
from posit.connect.resources import ResourceParameters
10-
119
from . import hooks, me
1210
from .auth import Auth
1311
from .config import Config
1412
from .content import Content
1513
from .groups import Groups
1614
from .metrics import Metrics
1715
from .oauth import OAuth
16+
from .resources import ResourceParameters
1817
from .tasks import Tasks
1918
from .users import User, Users
2019

src/posit/connect/content.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from posixpath import dirname
88
from typing import Any, List, Optional, overload
99

10+
from posit.connect.oauth.associations import ContentItemAssociations
11+
1012
from . import tasks
1113
from .bundles import Bundles
1214
from .env import EnvVars
@@ -16,6 +18,18 @@
1618
from .variants import Variants
1719

1820

21+
class ContentItemOAuth(Resource):
22+
def __init__(self, params: ResourceParameters, content_guid: str) -> None:
23+
super().__init__(params)
24+
self.content_guid = content_guid
25+
26+
@property
27+
def associations(self) -> ContentItemAssociations:
28+
return ContentItemAssociations(
29+
self.params, content_guid=self.content_guid
30+
)
31+
32+
1933
class ContentItemOwner(Resource):
2034
pass
2135

@@ -27,6 +41,10 @@ def __getitem__(self, key: Any) -> Any:
2741
return ContentItemOwner(params=self.params, **v)
2842
return v
2943

44+
@property
45+
def oauth(self) -> ContentItemOAuth:
46+
return ContentItemOAuth(self.params, content_guid=self["guid"])
47+
3048
def delete(self) -> None:
3149
"""Delete the content item."""
3250
path = f"v1/content/{self.guid}"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""OAuth association resources."""
2+
3+
from typing import List
4+
5+
from ..resources import Resource, ResourceParameters, Resources
6+
7+
8+
class Association(Resource):
9+
pass
10+
11+
12+
class IntegrationAssociations(Resources):
13+
"""IntegrationAssociations resource."""
14+
15+
def __init__(
16+
self, params: ResourceParameters, integration_guid: str
17+
) -> None:
18+
super().__init__(params)
19+
self.integration_guid = integration_guid
20+
21+
def find(self) -> List[Association]:
22+
"""Find OAuth associations.
23+
24+
Returns
25+
-------
26+
List[Association]
27+
"""
28+
path = f"v1/oauth/integrations/{self.integration_guid}/associations"
29+
url = self.params.url + path
30+
31+
response = self.params.session.get(url)
32+
return [
33+
Association(
34+
self.params,
35+
**result,
36+
)
37+
for result in response.json()
38+
]
39+
40+
41+
class ContentItemAssociations(Resources):
42+
"""ContentItemAssociations resource."""
43+
44+
def __init__(self, params: ResourceParameters, content_guid: str) -> None:
45+
super().__init__(params)
46+
self.content_guid = content_guid
47+
48+
def find(self) -> List[Association]:
49+
"""Find OAuth associations.
50+
51+
Returns
52+
-------
53+
List[Association]
54+
"""
55+
path = (
56+
f"v1/content/{self.content_guid}/oauth/integrations/associations"
57+
)
58+
url = self.params.url + path
59+
response = self.params.session.get(url)
60+
return [
61+
Association(
62+
self.params,
63+
**result,
64+
)
65+
for result in response.json()
66+
]
67+
68+
def delete(self) -> None:
69+
"""Delete integration associations."""
70+
data = []
71+
72+
path = (
73+
f"v1/content/{self.content_guid}/oauth/integrations/associations"
74+
)
75+
url = self.params.url + path
76+
self.params.session.put(url, json=data)
77+
78+
def update(self, integration_guid: str) -> None:
79+
"""Set integration associations."""
80+
data = [{"oauth_integration_guid": integration_guid}]
81+
82+
path = (
83+
f"v1/content/{self.content_guid}/oauth/integrations/associations"
84+
)
85+
url = self.params.url + path
86+
self.params.session.put(url, json=data)

src/posit/connect/oauth/integrations.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22

33
from typing import List, Optional, overload
44

5+
from posit.connect.oauth.associations import IntegrationAssociations
6+
57
from ..resources import Resource, Resources
68

79

810
class Integration(Resource):
911
"""OAuth integration resource."""
1012

13+
@property
14+
def associations(self) -> IntegrationAssociations:
15+
return IntegrationAssociations(
16+
self.params, integration_guid=self["guid"]
17+
)
18+
1119
def delete(self) -> None:
1220
"""Delete the OAuth integration."""
1321
path = f"v1/oauth/integrations/{self['guid']}"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"app_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
4+
"oauth_integration_guid": "22644575-a27b-4118-ad06-e24459b05126",
5+
"oauth_integration_name": "keycloak integration",
6+
"oauth_integration_description": "integration description",
7+
"oauth_integration_template": "custom",
8+
"created_time": "2024-10-01T18:16:09Z"
9+
}
10+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"app_guid": "f2f37341-e21d-3d80-c698-a935ad614066",
4+
"oauth_integration_guid": "22644575-a27b-4118-ad06-e24459b05126",
5+
"oauth_integration_name": "keycloak integration",
6+
"oauth_integration_description": "integration description",
7+
"oauth_integration_template": "custom",
8+
"created_time": "2024-10-01T18:16:09Z"
9+
}
10+
]

0 commit comments

Comments
 (0)