feat: Trusted Publishing support#1980
Conversation
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.
gnugomez
left a comment
There was a problem hiding this comment.
@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.
|
Latest changes:
|
How the finished flow works
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.iss, validates the JWT (signature, issuer, audience, forbidden headers), and matches claims against registrations.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
JwtDecoders.fromIssuerLocation()does network discovery, which previously ran in the constructor and would have made app startup depend on external issuers.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.