-
Notifications
You must be signed in to change notification settings - Fork 95
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(auth): implement iap auth token #637
Conversation
b6b8a8e
to
e44bc43
Compare
e44bc43
to
9ef32ac
Compare
@TheKevJames Thoughts on building out a thin IAP-secured service on |
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.
Please update the usage docs to include the IapToken workflow.
auth/gcloud/aio/auth/token.py
Outdated
@dataclass | ||
class TokenResponse: | ||
value: str | ||
expires_in: int = GCLOUD_TOKEN_DURATION |
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.
Looks like there are fairly limited places where this default gets used, all within the new IAP flow. It might be a bit easier to reason about if this value is passed in explicitly in that case, since (if I'm reading this correctly!) it looks like there might be two values at play here: previously, we passed this value in and Google created tokens of the designated duration. With IAP, though, it seems like we're finding the tokens have this duration even though we aren't passing this value in (for gce metadata and authorized users)?
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 some reason the responses from IAP token requests are all over the place and they don't provide you with an expiry. Maybe there's some way to force them to, but in practice the tokens do expire within 1 hour.
I've moved this default TTL to the class-level and removed the default from this TokenResponse
dataclass. LMK what you think!
auth/gcloud/aio/auth/token.py
Outdated
|
||
https://cloud.google.com/iap/docs/authentication-howto#obtaining_an_oidc_token_in_all_other_cases | ||
""" | ||
async def get_access_token(timeout: int) -> 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.
nit: consider de-nesting this method to allow us to more easily test it on its own.
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 am with Kevin here, unless there are compelling reasons not to.
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'll go with the majority on this one and de-nest this one, but I personally find inline functions to be great for readability when a chunk of code doesn't deserve its own method but represents a block of functionality which serves a specific purpose. I.e. it can be represented as an input/output contract via a function.
I like the idea in principle, but this repo has a problem with integration tests that we've not yet solved: external contributors can't auth correctly, so tests fail for their PRs. It'd be nice if we could avoid introducing more "failures" like that until we can solve the underlying problem, I think... The long term solution is probably to solve #578 and use workload identity via CircleCI to authenticate so it isn't tied to org secrets, but that's a much bigger task here. I wonder if mocking out the integration pieces would be a good enough test in the meantime? That said, I guess e2e tests which only work for us are still better than no e2e tests at all... |
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 have added my comments inline.
timeout: float, params: Optional[Mapping[str, Union[int, str]]], | ||
allow_redirects: bool, | ||
) -> Response: | ||
pass |
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.
That could definitely work, although I would like to limit the sprawl of my changes and this is an extension of existing form. Since this is an abstract base class and this is an abstract method, you can't instantiate an object of this class so raising is redundant:
>>> import abc
>>> class Base(abc.ABC):
... def fn(self):
... return 42
... @abc.abstractmethod
... def abstract(self):
... pass
...
>>> b = Base()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Base with abstract method abstract
timeout: Timeout = 10, | ||
params: Optional[Mapping[str, Union[int, str]]] = None, | ||
allow_redirects: bool = False, | ||
) -> aiohttp.ClientResponse: |
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.
This is imported as from aiohttp import ClientResponse as Response
. Perhaps change?
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.
Same as the previous comment about wanting to avoid changing unrelated parts of the code.
@TheKevJames might be able to tell you more about this choice.
# pylint: disable=too-many-instance-attributes | ||
__metaclass__ = ABCMeta |
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.
Do you have any good references on metaclass and working with metaclasses in Python? Interest only.
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 only place that has served me well on this has been the official Python docs unfortunately! I'm happy to field questions if you have specific ones :)
async def _refresh_authorized_user(self, timeout: int) -> Response: | ||
@abstractmethod | ||
async def refresh(self, *, timeout: int) -> TokenResponse: | ||
pass |
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.
Ditto for my comment above.
#637 (comment)
auth/gcloud/aio/auth/token.py
Outdated
|
||
https://cloud.google.com/iap/docs/authentication-howto#obtaining_an_oidc_token_in_all_other_cases | ||
""" | ||
async def get_access_token(timeout: int) -> 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.
I am with Kevin here, unless there are compelling reasons not to.
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.
Looks pretty good to me! More tests would be phenomenal but I saw Kevin's comment re: integration tests and I see we don't have much at the moment. As long as this had been tested again for the various auth modes for both Token and IapToken then I'm happy with this.
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.
doc updates LGTM, but you'll need to md->rst them and move the README updates into the init.py file as well once you rebase. Had to switch that as part of the pdoc->sphinx changeover.
Summary
Implement an IAP auth token class
IapToken
which acts in a similar way to our OAuth 2.0 access token classToken
. I've refactored out the basic components of the two asBaseToken
, which includes:get
Session
, ex. context manager dunder methods,close
, etc.In terms of the
IapToken
class, its general behaviour is:Other changes include:
HEAD
method onSession
and piping throughallow_redirects
, for this specific application it aids in explicitly stopping at the3xx
response to parse out thelocation
headerTokenResponse
dataclass to handle differences between the HTTP response formats of various requests (ex. GCE metadata server for IAP vs. GCE metadata server for OAuth) so that they can all be handled similarly by theBaseToken
class' token managementChecklist:
IapToken
against GCE metadata server; however I verified that the URL is correct through manual testing)