Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implements OAuth association API resource #286

Merged
merged 9 commits into from
Sep 12, 2024
Merged
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