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

Adds an OAuthIntegration #52

Merged
merged 14 commits into from
Feb 27, 2024
Merged

Adds an OAuthIntegration #52

merged 14 commits into from
Feb 27, 2024

Conversation

dbkegley
Copy link
Collaborator

Adds a client.oauth resource for interacting the the v1/oauth/integrations/credentials endpoint to perform a token exchange with Connect.

Copy link

github-actions bot commented Feb 23, 2024

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
544 508 93% 80% 🟢

New Files

File Coverage Status
src/posit/connect/external/init.py 100% 🟢
src/posit/connect/external/databricks.py 0% 🟢
src/posit/connect/external/databricks_test.py 100% 🟢
src/posit/connect/oauth.py 100% 🟢
src/posit/connect/oauth_test.py 100% 🟢
TOTAL 80% 🟢

Modified Files

File Coverage Status
src/posit/connect/client.py 100% 🟢
src/posit/connect/client_test.py 100% 🟢
TOTAL 100% 🟢

updated for commit: ec35b17 by action🐍

@dbkegley dbkegley force-pushed the kegs/databricks-oauth-2 branch from 93bf580 to ad3dba8 Compare February 23, 2024 22:48
Copy link
Collaborator

@nealrichardson nealrichardson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few notes. LMK if you want to talk through using responses to set up some tests that stub out the server side.

Comment on lines 59 to 61
if self._oauth is None:
self._oauth = OAuthIntegration(config=self.config, session=self.session)
return self._oauth
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT there's nothing in OAuthIntegration that needs to be cached, so we just need to be able to access it here, as we do (now) for Users below.

Suggested change
if self._oauth is None:
self._oauth = OAuthIntegration(config=self.config, session=self.session)
return self._oauth
return OAuthIntegration(config=self.config, session=self.session)

self.session = session


def get_credentials(self, user_identity: Optional[str]) -> Response:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to return Response or can it return the response body or something? I don't see this being used anywhere yet, so I can't tell.

I think we're trying to avoid having the request/response details leaking outside of the get/put/post/etc. methods.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for reference, here's the place it gets used:

access_token = self.posit_oauth.get_credentials(self.user_identity).json()['access_token']

So we could definitely make the return type more specific and not expose the response object.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I was using github's symbol lookup and it didn't find it, should have stuck with good ol' cmd-F.

Yeah in that case, I'd have this method return response.json()

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A defined type would be even better so that the consumer gets type hints.

@dataclass
class OAuthResponse:
    access_token: str

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the expectation that this endpoint might eventually return something other than an access_token, I like the idea of returning a defined type.

from .users import Users


# Connect sets the value of the environment variable RSTUDIO_PRODUCT = CONNECT
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😭 it's 2024, we should start setting POSIT_PRODUCT in the env too, yeah?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the presence of CONNECT_SERVER instead and sidestep branding issues?


@abc.abstractmethod
def auth_type(self) -> str:
...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these abstract method's raise NotImplementedError()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 18 to 19
def is_local() -> bool:
return not os.getenv("RSTUDIO_PRODUCT") == "CONNECT"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this is only being used src/posit/connect/external/databricks.py.

Should it be moved there?

This could also go in Config if there is reuse. But I'd rename it to something like run_mode, runtime_environment, etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently yes it's only used by the databricks module. I could see this being useful elsewhere though. No preference from me on where this lives

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 Place it where it's used for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does usage of this file look like? I think the implementation can be reduced, but I'm not convinced since it seems dependent on the consuming code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's our example from last weeks demo.

with Client() as connect_client:
credentials_provider = viewer_credentials_provider(
connect_client.oauth, content_identity=CONTENT_IDENTITY)
cfg = Config(host=DB_HOST_URL, credentials_provider=credentials_provider)

This usage has changed a little bit since then. In this PR we accept a client as an optional parameter so that the Content doesn't require a Connect server when running locally. The SDK client is initialized only when the content is running on Connect by default.

# https://github.com/databricks/databricks-sdk-py/blob/v0.20.0/databricks/sdk/credentials_provider.py
# https://github.com/databricks/databricks-sql-python/blob/v3.1.0/src/databricks/sql/auth/authenticators.py
# In order to keep compatibility with the Databricks SDK
class CredentialsProvider(abc.ABC):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid adding their SDK as a dependency. I'll test and see if we can remove this abstract definition altogether.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave this for now. I think we will have a similar problem for our content types. It would be nice to provide helpers for each framework when obtaining the identity token from the headers but it will vary by content type.

return inner


def viewer_credentials_provider(client: Optional[Client], user_identity: Optional[str]) -> Optional[CredentialsProvider]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the value of user_identity ever something other than headers.get('Posit-Connect-Content-Identity')?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value is always contained in a header called Posit-Connect-Content-Identity. But the method for obtaining the header value varies by content type:

https://docs.posit.co/connect/user/shiny/#user-meta-data
https://docs.posit.co/connect/user/flask/#user-meta-data

@dbkegley dbkegley force-pushed the kegs/databricks-oauth-2 branch from 40a3830 to 57508a6 Compare February 26, 2024 21:33
# Use these environment varariables to determine if the
# client SDK was initialized from a piece of content running on a Connect server.
def is_local() -> bool:
return not os.getenv("CONNECT_SERVER") and not os.getenv("CONNECT_CONTENT_GUID")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think CONNECT_SERVER is right here: that's what we recommend folks set locally in all of our API examples (e.g. https://docs.posit.co/connect/cookbook/).

I'd rather use the accurate variable even if it says RSTUDIO; we can fix that on the server going forward and (eventually) purge it here.

@dbkegley dbkegley force-pushed the kegs/databricks-oauth-2 branch from 109a55b to 8e3a865 Compare February 27, 2024 17:27
@dbkegley dbkegley merged commit 19b6cab into main Feb 27, 2024
7 checks passed
@dbkegley dbkegley deleted the kegs/databricks-oauth-2 branch February 27, 2024 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants