-
Notifications
You must be signed in to change notification settings - Fork 4
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
Conversation
f82a67f
to
93bf580
Compare
☂️ Python Coverage
Overall Coverage
New Files
Modified Files
|
93bf580
to
ad3dba8
Compare
There was a problem hiding this 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.
src/posit/connect/client.py
Outdated
if self._oauth is None: | ||
self._oauth = OAuthIntegration(config=self.config, session=self.session) | ||
return self._oauth |
There was a problem hiding this comment.
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.
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) |
src/posit/connect/oauth.py
Outdated
self.session = session | ||
|
||
|
||
def get_credentials(self, user_identity: Optional[str]) -> Response: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/posit/connect/client.py
Outdated
from .users import Users | ||
|
||
|
||
# Connect sets the value of the environment variable RSTUDIO_PRODUCT = CONNECT |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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: | ||
... |
There was a problem hiding this comment.
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()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are directly copied from the databricks client(s) so it's unclear if they are intentionally not raising an error
https://github.com/databricks/databricks-sdk-py/blob/525576073da90de0b98ff652ad5b6f2d2bb77871/databricks/sdk/credentials_provider.py#L30-L40
https://github.com/databricks/databricks-sql-python/blob/50489346440cdf9e547b597254643ee4ceb28c19/src/databricks/sql/auth/authenticators.py#L22-L33
src/posit/connect/client.py
Outdated
def is_local() -> bool: | ||
return not os.getenv("RSTUDIO_PRODUCT") == "CONNECT" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
posit-sdk-py/connect-content/sample-content.py
Lines 27 to 30 in 62fcc8e
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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. See if you can utilize optional dependencies: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#dependencies-optional-dependencies
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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')
?
There was a problem hiding this comment.
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
40a3830
to
57508a6
Compare
# 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") |
There was a problem hiding this comment.
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.
109a55b
to
8e3a865
Compare
Adds a
client.oauth
resource for interacting the thev1/oauth/integrations/credentials
endpoint to perform a token exchange with Connect.