Skip to content

Add TokenProvider#principal accessor with memoised lookups - #23

Merged
alejandrohdezma merged 6 commits into
mainfrom
feat/principal
Jun 1, 2026
Merged

Add TokenProvider#principal accessor with memoised lookups#23
alejandrohdezma merged 6 commits into
mainfrom
feat/principal

Conversation

@alejandrohdezma

Copy link
Copy Markdown
Collaborator

💻 How to review this PR?

This PR was created with the idea of being reviewed commit by commit. Each commit contains an incremental change that makes it easier to review. Also some of the commits contain additional information in their description to help understand why the change was made.

I also recommend checking "Hide whitespace" when reviewing this PR!

🚀 What's included in this PR?

Adds a principal: F[Option[String]] accessor on TokenProvider that returns the subject identifier each provider is authenticated as (service-account email, user-account email from userinfo, or the JWT email claim for identity tokens). The lookup is memoised at construction time, so each provider instance makes at most one underlying HTTP call regardless of how many times principal is read.

To make room for principal, this PR also:

  • Refactors Parser's ADC handling into a CredentialsFile AST (ServiceAccount / AuthorizedUser) returned by Parser.defaultCredentialsFile[F], replacing the old onDefaultCredentialsFile + credentialsType CPS-style dispatch.
  • Adds a googleExpect helper on Client[F] to encapsulate the trailing-slash + Metadata-Flavor: Google boilerplate.
  • Extracts a private Impl class for TokenProvider.create so principal and its withPrincipal overrides have a place to live.
  • F-wraps the factories that need to perform HTTP fetches at construction time (identity, userIdentity, serviceAccount(client), userAccount(triple)).
  • Relaxes versionPolicyIntention to Compatibility.None to acknowledge the breaking changes to the TokenProvider trait.

Introduce a sealed `Parser.CredentialsFile` AST (with `ServiceAccount` and
`AuthorizedUser` cases) returned by a single `Parser.defaultCredentialsFile[F]:
F[(Path, Option[CredentialsFile])]` entry point. The dispatch on the file's
`type` field is encoded in the `Decoder[CredentialsFile]` instance, so the
caller only sees a parsed AST or `None`.

`Parser.googleServiceAccount` now returns the `ServiceAccount` AST case
directly via `parser.decode[ServiceAccount]`. The PEM-to-RSAPrivateKey
transformation is exposed as `Decoder[RSAPrivateKey]` (pure — no `Sync.blocking`
wrap, the work is CPU-bound).

Split the old `applicationDefaultCredentials` failure modes into a missing-file
case (`DefaultCredentialsFileNotFound`) and a parse-failure case
(`UnableToGetDefaultCredentials`). PEM-decode failures now fold into
`UnableToGetDefaultCredentials` via the decoder's catch-all.

Update `TokenProvider.userAccount(httpClient)` and `userIdentity(httpClient)`
to pattern-match on the AST, raising `UnsupportedCredentialsType` when handed
a service-account JSON (where an authorized-user is required).
Surface the subject identifier each provider is authenticated as
(service-account email, workload metadata `/email`, JWT `email`/`sub`
claim) through a new abstract `principal: F[Option[String]]` on the
trait. Internally route every provider through a single `Impl` class
that can be rewired via `withPrincipal`, so factories assemble
providers as `TokenProvider.create(fetchToken).withPrincipal(...)`.

Factories that need an HTTP call to determine the principal (`identity`,
`userIdentity`, `serviceAccount(client)`, and the direct
`userAccount(id, secret, refresh, client)`) memoise the lookup at
construction time via `Concurrent.memoize`, so each provider instance
makes at most one `/email`, `userinfo`, or JWT-decode call regardless
of how many times `principal` is read.

`cached.build` delegates `principal` to the wrapped provider so
caching the token doesn't drop the principal.
@alejandrohdezma
alejandrohdezma requested a review from a team as a code owner May 25, 2026 18:24
@github-actions github-actions Bot added 🚀 feature New feature or improvement ⚖️ L Size of PR between 500 and 1000 lines labels May 25, 2026

@Oliver-Taylor Oliver-Taylor left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@alejandrohdezma
alejandrohdezma merged commit a9d7e2e into main Jun 1, 2026
6 checks passed
@alejandrohdezma
alejandrohdezma deleted the feat/principal branch June 1, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚖️ L Size of PR between 500 and 1000 lines 🚀 feature New feature or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants