Skip to content

Commit

Permalink
feat: Implements OAuth association API resource (#286)
Browse files Browse the repository at this point in the history
Adds support for requesting and updating oauth associations through the
Connect API.
  • Loading branch information
zackverham authored Sep 12, 2024
1 parent 5c6eed3 commit 3832b43
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 6 deletions.
108 changes: 108 additions & 0 deletions integration/tests/posit/connect/oauth/test_associations.py
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
6 changes: 3 additions & 3 deletions integration/tests/posit/connect/oauth/test_integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def teardown_class(cls):
assert len(cls.client.oauth.integrations.find()) == 0

def test_get(self):
result = self.client.oauth.integrations.get(self.integration.guid)
result = self.client.oauth.integrations.get(self.integration["guid"])
assert result == self.integration

def test_find(self):
Expand All @@ -78,7 +78,7 @@ def test_create_update_delete(self):
},
)

created = self.client.oauth.integrations.get(integration.guid)
created = self.client.oauth.integrations.get(integration["guid"])
assert created == integration

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

created.update(name="updated integration name")
updated = self.client.oauth.integrations.get(integration.guid)
updated = self.client.oauth.integrations.get(integration["guid"])
assert updated.name == "updated integration name"

# delete the new integration
Expand Down
3 changes: 1 addition & 2 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@

from requests import Response, Session

from posit.connect.resources import ResourceParameters

from . import hooks, me
from .auth import Auth
from .config import Config
from .content import Content
from .groups import Groups
from .metrics import Metrics
from .oauth import OAuth
from .resources import ResourceParameters
from .tasks import Tasks
from .users import User, Users

Expand Down
18 changes: 18 additions & 0 deletions src/posit/connect/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from posixpath import dirname
from typing import Any, List, Optional, overload

from posit.connect.oauth.associations import ContentItemAssociations

from . import tasks
from .bundles import Bundles
from .env import EnvVars
Expand All @@ -16,6 +18,18 @@
from .variants import Variants


class ContentItemOAuth(Resource):
def __init__(self, params: ResourceParameters, content_guid: str) -> None:
super().__init__(params)
self.content_guid = content_guid

@property
def associations(self) -> ContentItemAssociations:
return ContentItemAssociations(
self.params, content_guid=self.content_guid
)


class ContentItemOwner(Resource):
pass

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

@property
def oauth(self) -> ContentItemOAuth:
return ContentItemOAuth(self.params, content_guid=self["guid"])

def delete(self) -> None:
"""Delete the content item."""
path = f"v1/content/{self.guid}"
Expand Down
86 changes: 86 additions & 0 deletions src/posit/connect/oauth/associations.py
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)
8 changes: 8 additions & 0 deletions src/posit/connect/oauth/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

from typing import List, Optional, overload

from posit.connect.oauth.associations import IntegrationAssociations

from ..resources import Resource, Resources


class Integration(Resource):
"""OAuth integration resource."""

@property
def associations(self) -> IntegrationAssociations:
return IntegrationAssociations(
self.params, integration_guid=self["guid"]
)

def delete(self) -> None:
"""Delete the OAuth integration."""
path = f"v1/oauth/integrations/{self['guid']}"
Expand Down
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"
}
]
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"
}
]
Loading

0 comments on commit 3832b43

Please sign in to comment.