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(auth): implement iap auth token #637

Merged
merged 10 commits into from
Sep 22, 2023
Merged

Conversation

jonathan-johnston
Copy link
Contributor

@jonathan-johnston jonathan-johnston commented Sep 14, 2023

Summary

Implement an IAP auth token class IapToken which acts in a similar way to our OAuth 2.0 access token class Token. I've refactored out the basic components of the two as BaseToken, which includes:

  • Top-level methods like get
  • Reading in and interpreting service files
  • Refreshing the token before the stated expiry
  • General token management
  • Handling the Session, ex. context manager dunder methods, close, etc.

In terms of the IapToken class, its general behaviour is:

  • Manage tokens for a single IAP-secured service, since IAP is secured on a per-service level
  • When running in the Google infrastructure, directly fetch the identity token
  • When running with a SA file, fetch the IAP audience (client ID) then fetch the identity token for that IAP audience
  • When running as a user, similarly fetch the IAP audience (client ID) before impersonating a service account to generate the identity token

Other changes include:

  • Implementing a HEAD method on Session and piping through allow_redirects, for this specific application it aids in explicitly stopping at the 3xx response to parse out the location header
  • Implementing a TokenResponse 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 the BaseToken class' token management

Checklist:

  • Included references to GCP docs in comments and docstrings
  • Tested this manually via alpha version (except for IapToken against GCE metadata server; however I verified that the URL is correct through manual testing)

@jonathan-johnston jonathan-johnston changed the title checkpoint feat(auth): implement iap auth token Sep 14, 2023
@jonathan-johnston jonathan-johnston force-pushed the jonjon/auth-iap branch 4 times, most recently from b6b8a8e to e44bc43 Compare September 14, 2023 18:40
@jonathan-johnston
Copy link
Contributor Author

@TheKevJames Thoughts on building out a thin IAP-secured service on dialpad-oss to allow us to write an integration test for IapToken?

@jonathan-johnston jonathan-johnston marked this pull request as ready for review September 15, 2023 13:48
@jonathan-johnston jonathan-johnston requested review from aherrada-dialpad and removed request for a team September 15, 2023 13:48
Copy link
Member

@TheKevJames TheKevJames left a 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/session.py Outdated Show resolved Hide resolved
@dataclass
class TokenResponse:
value: str
expires_in: int = GCLOUD_TOKEN_DURATION
Copy link
Member

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)?

Copy link
Contributor Author

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!


https://cloud.google.com/iap/docs/authentication-howto#obtaining_an_oidc_token_in_all_other_cases
"""
async def get_access_token(timeout: int) -> str:
Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

auth/gcloud/aio/auth/token.py Show resolved Hide resolved
@TheKevJames
Copy link
Member

Thoughts on building out a thin IAP-secured service on dialpad-oss to allow us to write an integration test for IapToken?

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...

Copy link
Contributor

@caseydialpad caseydialpad left a 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
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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:
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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 :)

https://docs.python.org/3/library/abc.html

async def _refresh_authorized_user(self, timeout: int) -> Response:
@abstractmethod
async def refresh(self, *, timeout: int) -> TokenResponse:
pass
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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)


https://cloud.google.com/iap/docs/authentication-howto#obtaining_an_oidc_token_in_all_other_cases
"""
async def get_access_token(timeout: int) -> str:
Copy link
Contributor

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.

Copy link
Contributor

@shaundialpad shaundialpad left a 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.

Copy link
Member

@TheKevJames TheKevJames left a 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.

auth/gcloud/aio/auth/token.py Outdated Show resolved Hide resolved
@jonathan-johnston jonathan-johnston merged commit 70f2e9d into master Sep 22, 2023
@jonathan-johnston jonathan-johnston deleted the jonjon/auth-iap branch September 22, 2023 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants