-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implements OAuth association API resource (#286)
Adds support for requesting and updating oauth associations through the Connect API.
- Loading branch information
1 parent
5c6eed3
commit 3832b43
Showing
11 changed files
with
423 additions
and
6 deletions.
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
integration/tests/posit/connect/oauth/test_associations.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
from pathlib import Path | ||
|
||
import pytest | ||
from packaging import version | ||
|
||
from posit import connect | ||
|
||
from .. import CONNECT_VERSION | ||
|
||
|
||
@pytest.mark.skipif( | ||
CONNECT_VERSION <= version.parse("2024.06.0"), | ||
reason="OAuth Integrations not supported.", | ||
) | ||
class TestAssociations: | ||
@classmethod | ||
def setup_class(cls): | ||
cls.client = connect.Client() | ||
cls.integration = cls.client.oauth.integrations.create( | ||
name="example integration", | ||
description="integration description", | ||
template="custom", | ||
config={ | ||
"auth_mode": "Confidential", | ||
"authorization_uri": "https://example.com/__tenand_id__/oauth2/v2.0/authorize", | ||
"client_id": "client_id", | ||
"client_secret": "client_secret", | ||
"scopes": "a b c", | ||
"token_endpoint_auth_method": "client_secret_post", | ||
"token_uri": "https://example.com/__tenant_id__/oauth2/v2.0/token", | ||
}, | ||
) | ||
|
||
cls.another_integration = cls.client.oauth.integrations.create( | ||
name="another example integration", | ||
description="another integration description", | ||
template="custom", | ||
config={ | ||
"auth_mode": "Confidential", | ||
"authorization_uri": "https://example.com/__tenand_id__/oauth2/v2.0/authorize", | ||
"client_id": "client_id", | ||
"client_secret": "client_secret", | ||
"scopes": "a b c", | ||
"token_endpoint_auth_method": "client_secret_post", | ||
"token_uri": "https://example.com/__tenant_id__/oauth2/v2.0/token", | ||
}, | ||
) | ||
|
||
# create content | ||
# requires full bundle deployment to produce an interactive content type | ||
cls.content = cls.client.content.create(name="example-flask-minimal") | ||
# create bundle | ||
path = Path( | ||
"../../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz" | ||
) | ||
path = (Path(__file__).parent / path).resolve() | ||
bundle = cls.content.bundles.create(str(path)) | ||
# deploy bundle | ||
task = bundle.deploy() | ||
task.wait_for() | ||
|
||
cls.content.oauth.associations.update(cls.integration["guid"]) | ||
|
||
@classmethod | ||
def teardown_class(cls): | ||
cls.integration.delete() | ||
cls.another_integration.delete() | ||
assert len(cls.client.oauth.integrations.find()) == 0 | ||
|
||
cls.content.delete() | ||
assert cls.client.content.count() == 0 | ||
|
||
def test_find_by_integration(self): | ||
associations = self.integration.associations.find() | ||
assert len(associations) == 1 | ||
assert ( | ||
associations[0]["oauth_integration_guid"] | ||
== self.integration["guid"] | ||
) | ||
|
||
no_associations = self.another_integration.associations.find() | ||
assert len(no_associations) == 0 | ||
|
||
def test_find_update_by_content(self): | ||
associations = self.content.oauth.associations.find() | ||
assert len(associations) == 1 | ||
assert associations[0]["app_guid"] == self.content["guid"] | ||
assert ( | ||
associations[0]["oauth_integration_guid"] | ||
== self.integration["guid"] | ||
) | ||
|
||
# update content association to another_integration | ||
self.content.oauth.associations.update( | ||
self.another_integration["guid"] | ||
) | ||
updated_associations = self.content.oauth.associations.find() | ||
assert len(updated_associations) == 1 | ||
assert updated_associations[0]["app_guid"] == self.content["guid"] | ||
assert ( | ||
updated_associations[0]["oauth_integration_guid"] | ||
== self.another_integration.guid | ||
) | ||
|
||
# unset content association | ||
self.content.oauth.associations.delete() | ||
no_associations = self.content.oauth.associations.find() | ||
assert len(no_associations) == 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"""OAuth association resources.""" | ||
|
||
from typing import List | ||
|
||
from ..resources import Resource, ResourceParameters, Resources | ||
|
||
|
||
class Association(Resource): | ||
pass | ||
|
||
|
||
class IntegrationAssociations(Resources): | ||
"""IntegrationAssociations resource.""" | ||
|
||
def __init__( | ||
self, params: ResourceParameters, integration_guid: str | ||
) -> None: | ||
super().__init__(params) | ||
self.integration_guid = integration_guid | ||
|
||
def find(self) -> List[Association]: | ||
"""Find OAuth associations. | ||
Returns | ||
------- | ||
List[Association] | ||
""" | ||
path = f"v1/oauth/integrations/{self.integration_guid}/associations" | ||
url = self.params.url + path | ||
|
||
response = self.params.session.get(url) | ||
return [ | ||
Association( | ||
self.params, | ||
**result, | ||
) | ||
for result in response.json() | ||
] | ||
|
||
|
||
class ContentItemAssociations(Resources): | ||
"""ContentItemAssociations resource.""" | ||
|
||
def __init__(self, params: ResourceParameters, content_guid: str) -> None: | ||
super().__init__(params) | ||
self.content_guid = content_guid | ||
|
||
def find(self) -> List[Association]: | ||
"""Find OAuth associations. | ||
Returns | ||
------- | ||
List[Association] | ||
""" | ||
path = ( | ||
f"v1/content/{self.content_guid}/oauth/integrations/associations" | ||
) | ||
url = self.params.url + path | ||
response = self.params.session.get(url) | ||
return [ | ||
Association( | ||
self.params, | ||
**result, | ||
) | ||
for result in response.json() | ||
] | ||
|
||
def delete(self) -> None: | ||
"""Delete integration associations.""" | ||
data = [] | ||
|
||
path = ( | ||
f"v1/content/{self.content_guid}/oauth/integrations/associations" | ||
) | ||
url = self.params.url + path | ||
self.params.session.put(url, json=data) | ||
|
||
def update(self, integration_guid: str) -> None: | ||
"""Set integration associations.""" | ||
data = [{"oauth_integration_guid": integration_guid}] | ||
|
||
path = ( | ||
f"v1/content/{self.content_guid}/oauth/integrations/associations" | ||
) | ||
url = self.params.url + path | ||
self.params.session.put(url, json=data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
...pi__/v1/content/f2f37341-e21d-3d80-c698-a935ad614066/oauth/integrations/associations.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[ | ||
{ | ||
"app_guid": "f2f37341-e21d-3d80-c698-a935ad614066", | ||
"oauth_integration_guid": "22644575-a27b-4118-ad06-e24459b05126", | ||
"oauth_integration_name": "keycloak integration", | ||
"oauth_integration_description": "integration description", | ||
"oauth_integration_template": "custom", | ||
"created_time": "2024-10-01T18:16:09Z" | ||
} | ||
] |
10 changes: 10 additions & 0 deletions
10
...nect/__api__/v1/oauth/integrations/22644575-a27b-4118-ad06-e24459b05126/associations.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[ | ||
{ | ||
"app_guid": "f2f37341-e21d-3d80-c698-a935ad614066", | ||
"oauth_integration_guid": "22644575-a27b-4118-ad06-e24459b05126", | ||
"oauth_integration_name": "keycloak integration", | ||
"oauth_integration_description": "integration description", | ||
"oauth_integration_template": "custom", | ||
"created_time": "2024-10-01T18:16:09Z" | ||
} | ||
] |
Oops, something went wrong.