Skip to content

feat: Trusted Publishing support#1980

Draft
cstamas wants to merge 4 commits into
eclipse-openvsx:mainfrom
cstamas:trusted-publishing
Draft

feat: Trusted Publishing support#1980
cstamas wants to merge 4 commits into
eclipse-openvsx:mainfrom
cstamas:trusted-publishing

Conversation

@cstamas

@cstamas cstamas commented Jul 21, 2026

Copy link
Copy Markdown
Member

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

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.
@cstamas cstamas changed the title Trusted Publishing support feat: Trusted Publishing support Jul 21, 2026

@gnugomez gnugomez left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@cstamas I was about to open a PR to add some webui for setting up TP on user-owned namespaces, but I think we're just missing one more thing.

If we want to be consistent, the app currently is not exposing the user role on the namespace call, instead it exposes detailsUrl, membersUrl or roleUrl, these fields outline if a user is able to view/edit those namespace fields.

I believe that since we provide a /user/namespace/{namespace}/trusted-publishing endpoint, we should be doing the same here on the user namespace return. Adding a trustedPublishingUrl so that from the UI if this field is present we can assume that the user has enough privileges to view/edit TPs.

I'm not a great fan of this, but this makes it pretty flexible frontend-wise, so if we introduce other roles, that allow, different actions, the UI will need minimal changes.

@cstamas

cstamas commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Latest changes:

  • adds trustedPublishingUrl (admin endpoint missing)
  • input fields changed, now each provider will hand out a list of "inputs" (key, description, optional).

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