Skip to content

feat: PAT improvements (UUIDv7 for random + hashing) - #2048

Merged
netomi merged 8 commits into
eclipse-openvsx:trusted-publishingfrom
cstamas:trusted-publishing-tokens
Aug 7, 2026
Merged

netomi merged 8 commits into
eclipse-openvsx:trusted-publishingfrom
cstamas:trusted-publishing-tokens

Conversation

@cstamas

@cstamas cstamas commented Aug 6, 2026 •

Copy link
Copy Markdown
Member

Changes:

  • use UUIDv7 for random tokens
  • implement token value hashing

cstamas added 2 commits August 6, 2026 14:24
Introduce faster `java-uuid-generator` library from Tatu.
For now just as "drop in replacement", but we should also
do cluster-wide-sync using Redis maybe?
@cstamas
cstamas requested a review from netomi August 6, 2026 12:36
@cstamas

This comment was marked as outdated.

@netomi

netomi commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Let me verify the current JUG API since I want to give you the right generator methods.The short version: the answer depends entirely on which UUID version you pick, and the good news is the modern choice makes cross-pod coordination a non-issue.

If you use v4 (random) or v7, you don't need to coordinate anything between pods. Both draw the bulk of their bits from a CSPRNG, so uniqueness comes from entropy rather than from any per-node identifier. For v7 the layout is a 48-bit millisecond timestamp plus ~74 bits of randomness. Two pods generating in the very same millisecond would both have to draw the same 74-bit value to collide — the birthday bound puts that at roughly 2³⁷ UUIDs within a single millisecond before collision probability becomes meaningful. That's not a scenario you'll hit, and it holds regardless of how many pods you run or how you schedule them. No downward API, no node assignment, nothing.

Given your stack (Spring Boot, JPA, TimescaleDB), I'd default to v7 specifically. It's time-sortable (lexicographic order matches creation order), which keeps B-tree index inserts near the right edge instead of scattering them like v4 does — much friendlier as a primary key, and it plays nicely with time-partitioned data. In JUG that's:

// v7 — recommended default, no per-pod setup needed
private static final NoArgGenerator UUIDS = Generators.timeBasedEpochGenerator();
UUID id = UUIDS.generate();

There's also Generators.timeBasedEpochRandomGenerator() (added in 5.0), which uses fresh random for the trailing bits on every call rather than a monotonic counter within a millisecond. The plain timeBasedEpochGenerator() gives you monotonicity within a pod, which is usually what you want for ordering; the random variant trades a bit of intra-millisecond ordering for slightly simpler entropy behavior. Either is fine across pods.

The only case where multi-pod coordination actually matters is v1 or v6 (time-based with a node identifier). These derive part of the value from the host's MAC address / node ID plus timestamp plus clock sequence. In containers the MAC is often randomly assigned or, worse, could repeat across pods depending on your CNI, and JUG falls back to a random "dummy" address if it can't read a real one. Collisions there require same node ID + same 100ns tick + same clock sequence — unlikely, but the whole design assumes the node ID is genuinely unique, so you'd want to guarantee it rather than hope. If you have a reason to use v6 (say you want time-ordering and the v1-style structure), inject a stable per-pod identifier via the downward API and hand it to JUG as the ethernet address:

env:
  - name: POD_UID
    valueFrom:
      fieldRef:
        fieldPath: metadata.uid
// derive a 48-bit node id from the pod UID, set the multicast bit
// so it can never collide with a real hardware MAC
byte[] node = deriveFrom(System.getenv("POD_UID")); // hash -> 6 bytes
node[0] |= 0x01;
EthernetAddress addr = new EthernetAddress(node);
UUID id = Generators.timeBasedReorderedGenerator(addr).generate(); // v6

Pod UID is better than pod name or IP here — names collide across namespaces and restarts, and pod IPs get recycled, whereas the UID is unique for the lifetime of that object. Setting the multicast bit (the 0x01 on the first byte) marks it as locally-administered so it's guaranteed disjoint from any real NIC address JUG might otherwise pick up.

So my recommendation: go v7 with timeBasedEpochGenerator() and skip the coordination problem entirely. Only reach for the downward-API node-ID wiring if you specifically need v1/v6 semantics.

One aside — for v4 you don't even need JUG, since java.util.UUID.randomUUID() is built in; JUG earns its place mainly for v1/v6/v7 and for its faster generators when you're producing them at high volume. Current JUG is 5.1.0.

@cstamas

This comment was marked as outdated.

@cstamas cstamas changed the title feat: UUID use cleanup feat: UUIDv7 for random Aug 6, 2026
@cstamas
cstamas marked this pull request as ready for review August 6, 2026 13:30
@cstamas
cstamas requested a review from netomi August 6, 2026 15:17
Hash token values, also PAT table got version column that is 0 for existing tokens, and current version is 1.
"On the fly" upgrade added but also a job.

Changes:
* token config got hashAlg + salt
* token table got version smallint field (initialized to 0 on migration)
* new job upgrades token (replaces value with hash(value + salt) and sets version = 1)
* use token upgrade on the fly if (needed)
* tests reworked to use less mocks
@cstamas cstamas changed the title feat: UUIDv7 for random feat: PAT improvements (UUIDv7 for random + hashing) Aug 7, 2026
@netomi
netomi merged commit 063e431 into eclipse-openvsx:trusted-publishing Aug 7, 2026
1 check passed
@cstamas
cstamas deleted the trusted-publishing-tokens branch August 7, 2026 12:22
netomi added a commit that referenced this pull request Aug 21, 2026
* feat: Trusted Publishing support (#1980)

* Trusted Publishing support

How the finished flow works

1. A namespace owner registers a trusted publisher via `POST /user/namespace/{ns}/trusted-publishing/create` (body: provider, owner, repo, workflow, optional extension/environment). The provider resolves names to stable IDs (GitHub repos API, GitLab projects API) and the claims are stored in a new trusted_publisher table (JSONB column, migration V1_70). List and delete endpoints exist alongside it, all owner-only.
2. CI exchanges its OIDC ID token via POST /api/-/trusted-publishing/token (CSRF-exempted like /api/-/publish). The manager picks the provider by `iss`, validates the JWT (signature, issuer, audience, forbidden headers), and matches claims against registrations.
3. On match, it mints a short-lived `PersonalAccessToken` (default 15 min, `ovsx.trusted-publishing.token-expiry`) owned by the registering user, so the existing publish pipeline — permissions, auditing, expiry — is reused unchanged.

Key decisions baked in

- Matching is anchored on stable numeric IDs (repository_id/repository_owner_id, project_id/namespace_id) to block resurrection attacks, plus the workflow path with the @<ref> suffix stripped (any branch/tag can publish), plus environment only if pinned.
- The OIDC decoder is built lazily — `JwtDecoders.fromIssuerLocation()` does network discovery, which previously ran in the constructor and would have made app startup depend on external issuers.
- The tests for both providers added, also got offline matches() tests covering ref-independence, ID mismatch, and pinned environments.

Note: namespace-scoped tokens (the minted token has the registering user's full publish rights).
This is server-side work only; no CLI or WebUI changes added.

* Regenerate jOOQ classes with TP

* Fix response: it is 403 not 400

* Add trustedPublishingUrl and sort out inputs

* Add TODO and remove it for now

* feat: adding trusted publishers widget

* feat: adds promo banner on user tokens page

* feat: conditionally render namespace trusted publishers

* feat: scope TP widget to the namespace trustedPublishingUrl

---------

Signed-off-by: Thomas Neidhart <thomas.neidhart@eclipse-foundation.org>
Co-authored-by: Jordi Gómez <jordi.gomez@eclipse-foundation.org>
Co-authored-by: Thomas Neidhart <thomas.neidhart@eclipse-foundation.org>

* fix import due to react-router bump

* increment migration number after rebase

* feat: adding TP enabled flag (#2002)

* feat: adding TP enabled flag in version response

* feat(webui): using TP enabled check to show/hide entrypoints

* Fix: the build fails due flyway migration conflict (#2003)

Fix the flyway migration conflict.

* Do not use properties duplicated across (#2004)

* Do not use properties duplicated across

Instead, use the proper config bean as source of truth.

* reformat

* feat: add support for trusted publishing to cli (#2001)

* Multiple TP fixes (#2010)

* Enforce eclipse agreement and new TP provider endpoint

Enforces signed agreement on TP creation.
Also, introduces new endpoint to query supported TP providers
without NS.

* Expose status for TP

* Swap out version TP enabled for audience (if enabled)

Drop superfluous call.

* Drop unused

* Audience does not belong here (is exposed via version API)

* Fix TP duplication search

* Rename, document

* Mark it as nullable

* feat: adapting webui to new TP status endpoint (#2017)

* Tokens cleanup (#2015)

* Tokens cleanup

Just some cleanup and added token TYPE (PAT and OTT) for now.

* Token handling and notif

* Set or not the delete URL

* Tidy up

* Remove unused method

* Expose token type

* Cleanup enum

* We do not consume here

* enforce OTT tokens expire; quickly

* Fix migration: set LLT and OTT based on description

* Add dedicated flag for TP instead

* No need to reveal type at all

As it is either sent to use to UI (then it is LLT) or
sent to client/app for TP publisher (then is OTT).

* Collapse all back to original place

* Reformat

* Make verify not "use"; unsure why would anyone verify OTT but let it be

* Set the access TS as before

* Align DDL with expectation (#2019)

The extension name is not optional.

* Allow TP registration only for existing active extensions (#2020)

* Reconnect TP to Extension (#2021)

It was Namespace+extensionName, now reworked DDL to make
TP has a FK on extension instead.

* adapt frontend to backend changes (#2022)

* increment migration number after rebase

* fix test after rebase: findAccessTokens was renamed to findPersonalAccessTokens

* remove empty file after rebase

* Introduce TPT token type + token scoping (#2034)

Improving existing TP and generally access tokens. Now system supports three types of tokens, and tokens can be scoped (by Ns or Ext). TP uses dedicated TPT tokens and is always scoped to Ext).

Changes:
* added `AccessTokenAction` that callers has to set (accepted by `PersonalAccessToken useAccessToken(String tokenValue, AccessTokenAction accessTokenAction)` method).
* added `AccessTokenScope` that is able to "scope" (limit usability by action) the tokens, handled by `AccessTokenService`
* extended config for all three supported token types
* refactored `ExtensionService` to make sure extension file is processed only once, and also to be able to extract before actual install the NS+name (for token validation)
* DDL changes for `personal_access_token` table, added new FKs to extension, namespace and trusted publisher (latter can be deleted, in which case link is nullified).
* UI receives now only `LLT` tokens (rest are hidden)

* Reformat (#2035)

* feat: Configure active providers (#2045)

For now keep only "github" as active, but it is now matter
of configuration (providerIds as comma separated list).

* feat: PAT improvements (UUIDv7 for random + hashing) (#2048)

* feat: UUID use cleanup

Introduce faster `java-uuid-generator` library from Tatu.
For now just as "drop in replacement", but we should also
do cluster-wide-sync using Redis maybe?

* More fixes

* Tidy up

* Tidy up

* Javadoc

* Merge pull request #2 from cstamas/trusted-publishing-tokens-hash

Hash token values, also PAT table got version column that is 0 for existing tokens, and current version is 1.
"On the fly" upgrade added but also a job.

Changes:
* token config got hashAlg + salt
* token table got version smallint field (initialized to 0 on migration)
* new job upgrades token (replaces value with hash(value + salt) and sets version = 1)
* use token upgrade on the fly if (needed)
* tests reworked to use less mocks

* One more test fix

* Streamline update

* Apply fixes

As DDL on main drifted from TP

* Update pull request branches in workflow

Removed 'trusted-publishing' branch from pull request triggers.

Signed-off-by: Thomas Neidhart <thomas.neidhart@eclipse-foundation.org>

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Thomas Neidhart <thomas.neidhart@gmail.com>

* fix: stop leaking a notched DOM attribute from the publisher role filter

Select injects notched:true into its custom `input` whenever displayEmpty
is set and variant defaults to 'outlined', regardless of what that input
is. The custom InputBase here doesn't consume that prop, so it fell through
to the DOM as an invalid boolean attribute. This Select never rendered the
outlined chrome anyway (the border comes from the wrapping Paper), so
variant='standard' is both the fix and the semantically correct setting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix formatting

* suppress error log in test for expected error

* fix javadoc

---------

Signed-off-by: Thomas Neidhart <thomas.neidhart@eclipse-foundation.org>
Signed-off-by: Thomas Neidhart <thomas.neidhart@gmail.com>
Co-authored-by: Tamas Cservenak <tamas@cservenak.net>
Co-authored-by: Jordi Gómez <jordi.gomez@eclipse-foundation.org>
Co-authored-by: Jordi Gómez Hidalgo <31970428+gnugomez@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

2 participants