Skip to content

Commit 9d3bf23

Browse files
slight refactor after PR feedback; simplified provider
1 parent 994373d commit 9d3bf23

File tree

2 files changed

+89
-35
lines changed

2 files changed

+89
-35
lines changed

src/posit/connect/external/connect_api.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,19 @@
1414
from .external import is_local
1515

1616

17-
class ConnectAPIKeyProvider:
18-
"""Viewer API key provider for Connect API integration.
17+
class ViewerConnectClientProvider:
18+
"""Viewer client provider for Connect API integration.
1919
20-
Provider for exchanging Connect User Session Token for Connect API Key that acts on behalf of the user.
21-
This is an ephemeral API key that adheres to typical ephemeral API key clean up processes.
20+
Provider handles exchanging Connect User Session Token for Connect API Key that acts on behalf of the user.
21+
This is an ephemeral API key that adheres to typical ephemeral API key clean up processes. The provider
22+
returns a `Client` that is scoped to the viewer's API key.
2223
2324
Examples
2425
--------
2526
```python
2627
from shiny import App, ui
2728
from posit.connect import Client
28-
from posit.connect.external.connect_api import ConnectAPIKeyProvider
29+
from posit.connect.external.connect_api import ViewerConnectClientProvider
2930
3031
app_ui = ui.page_fixed(
3132
ui.h1("My Shiny App"),
@@ -36,8 +37,7 @@ class ConnectAPIKeyProvider:
3637
def server(input, output, session):
3738
client = Client()
3839
user_session_token = session.http_conn.headers.get("Posit-Connect-User-Session-Token")
39-
provider = ConnectAPIKeyProvider(client, user_session_token)
40-
viewer_client = Client(api_key=provider.viewer)
40+
viewer_client = ViewerConnectClientProvider(user_session_token).get_client()
4141
4242
assert client.me() != viewer_client.me()
4343
@@ -50,35 +50,31 @@ def server(input, output, session):
5050

5151
def __init__(
5252
self,
53-
client: Optional[Client] = None,
54-
user_session_token: Optional[str] = None,
53+
user_session_token: str,
54+
client_override: Optional[Client] = None,
55+
url_override: Optional[str] = None,
5556
):
56-
self._client = client
57+
if user_session_token == "":
58+
raise ValueError("Must provide valid user session token")
59+
5760
self._user_session_token = user_session_token
61+
self._client = client_override if client_override else Client()
62+
self._url = url_override
5863

59-
@property
60-
def viewer(self) -> Optional[str]:
61-
"""Retrieve the viewer api key.
64+
def get_client(self) -> Client:
65+
"""A new Connect client that can act on behalf of the viewer based on the user session token.
6266
6367
The viewer key is retrieved through an OAuth exchange process using the user session token.
6468
The issued API key is associated with the viewer of your app and can be used on their behalf
65-
to interact with the Connect API.
69+
to interact with the Connect API using this client.
6670
"""
71+
# If running this locally, the viewer will be assumed to be the same as the publisher.
6772
if is_local():
68-
return None
69-
70-
# If the user-session-token wasn't provided and we're running on Connect then we raise an exception.
71-
# user_session_token is required to impersonate the viewer.
72-
if self._user_session_token is None:
73-
raise ValueError(
74-
"The user-session-token is required for viewer API key authorization."
75-
)
76-
77-
if self._client is None:
78-
self._client = Client()
73+
return self._client
7974

8075
credentials = self._client.oauth.get_credentials(
8176
user_session_token=self._user_session_token,
8277
requested_token_type=API_KEY_TOKEN_TYPE,
8378
)
84-
return credentials.get("access_token")
79+
80+
return Client(url=self._url, api_key=credentials.get("access_token"))

tests/posit/connect/external/test_connect_api.py

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import responses
44

55
from posit.connect import Client
6-
from posit.connect.external.connect_api import ConnectAPIKeyProvider
6+
from posit.connect.external.connect_api import ViewerConnectClientProvider
77

88

99
def register_mocks():
@@ -29,24 +29,82 @@ def register_mocks():
2929

3030
class TestConnectAPIKeyProvider:
3131
@responses.activate
32-
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
32+
@patch.dict(
33+
"os.environ",
34+
{
35+
"RSTUDIO_PRODUCT": "CONNECT",
36+
"CONNECT_SERVER": "http://connect.example/",
37+
"CONNECT_API_KEY": "12345",
38+
},
39+
)
3340
def test_provider(self):
3441
register_mocks()
3542

43+
provider = ViewerConnectClientProvider(
44+
user_session_token="cit",
45+
)
46+
viewer_client = provider.get_client()
47+
assert viewer_client is not None
48+
assert viewer_client.cfg.url == "http://connect.example/"
49+
assert viewer_client.cfg.api_key == "viewer-api-key"
50+
51+
@responses.activate
52+
@patch.dict(
53+
"os.environ",
54+
{
55+
"RSTUDIO_PRODUCT": "CONNECT",
56+
"CONNECT_SERVER": "http://connect.example/",
57+
"CONNECT_API_KEY": "12345",
58+
},
59+
)
60+
def test_provider_with_url_override(self):
61+
register_mocks()
62+
63+
provider = ViewerConnectClientProvider(
64+
user_session_token="cit",
65+
url_override="http://connect2.example/",
66+
)
67+
viewer_client = provider.get_client()
68+
assert viewer_client is not None
69+
assert viewer_client.cfg.url == "http://connect2.example/"
70+
assert viewer_client.cfg.api_key == "viewer-api-key"
71+
72+
@responses.activate
73+
@patch.dict("os.environ", {"RSTUDIO_PRODUCT": "CONNECT"})
74+
def test_provider_with_client_override(self):
75+
register_mocks()
76+
3677
client = Client(api_key="12345", url="https://connect.example/")
3778
client._ctx.version = None
38-
auth = ConnectAPIKeyProvider(
39-
client=client,
79+
provider = ViewerConnectClientProvider(
4080
user_session_token="cit",
81+
client_override=client,
4182
)
42-
assert auth.viewer == "viewer-api-key"
83+
viewer_client = provider.get_client()
84+
assert viewer_client is not None
85+
assert viewer_client.cfg.url == "http://connect.example/"
86+
assert viewer_client.cfg.api_key == "viewer-api-key"
4387

88+
@patch.dict(
89+
"os.environ", {"CONNECT_SERVER": "http://connect.example/", "CONNECT_API_KEY": "12345"}
90+
)
4491
def test_provider_fallback(self):
45-
# local_authenticator is used when the content is running locally
92+
# default client is used when the content is running locally
93+
provider = ViewerConnectClientProvider(
94+
user_session_token="cit",
95+
)
96+
viewer_client = provider.get_client()
97+
assert viewer_client.cfg.url == "https://connect.example/"
98+
assert viewer_client.cfg.api_key == "12345"
99+
100+
def test_provider_fallback_with_client_override(self):
101+
# provided client is used when the content is running locally
46102
client = Client(api_key="12345", url="https://connect.example/")
47103
client._ctx.version = None
48-
auth = ConnectAPIKeyProvider(
49-
client=client,
104+
provider = ViewerConnectClientProvider(
50105
user_session_token="cit",
106+
client_override=client,
51107
)
52-
assert auth.viewer is None
108+
viewer_client = provider.get_client()
109+
assert viewer_client.cfg.url == "https://connect.example/"
110+
assert viewer_client.cfg.api_key == "12345"

0 commit comments

Comments
 (0)