Mount a plugin's own surface behind the portal - #955
Open
importantitemzl-tech wants to merge 1 commit into
Open
Conversation
A deployment that runs a plugin with an HTTP surface of its own has no way to put it behind the portal. The upstream map is a fixed record of web-ui and admin, and any other first path segment falls through to web-ui, so the only options are publishing a second address with a second sign-in, or reimplementing session sealing, PKCE and CSRF inside the plugin. A plugin entry may now declare `portalPath`. The target derives the plugin's own internal address — a network alias on docker, `<appPrefix>-<name>.internal` on fly — and sets `PORTAL_PLUGIN_UPSTREAMS` on the portal and `PORTAL_BASE_PATH` on the plugin, so an operator never writes a container or app URL by hand. The surface is then proxied exactly as admin is: same session requirement, same prefix stripping, same signed `x-portal-identity`. `proxyToSurface` already took an arbitrary upstream, so this extends a record rather than adding routing. Three things are refused rather than documented, because all three fail quietly: - A path the portal answers itself. A plugin mounted on `admin` would take that route over. - An upstream that is not on the deployment's own network. The portal mints an identity header for whatever it is pointed at, so a public URL here would hand a person's verified identity to a third party. A bare label with no dot is admitted because that is how every container already addresses every other one on the docker target. - A path claimed twice. The CLI rejects all three when the config loads; the portal rejects them again at boot and refuses to start, because a dropped entry is silent and the request would then reach web-ui instead of 404ing. The portal's copy is the one that has to hold — it does not trust its own environment. Anonymous playground sessions are refused a plugin surface, on the same reasoning as `/connect` and `/drop`: the portal cannot know what a deployment's own surface does with an identity it is handed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A deployment can run a plugin with an HTTP surface of its own, but there is no
way to put that surface behind the portal.
UPSTREAMSis a fixed record ofweb-uiandadmin, and any other first path segment falls through toweb-ui, so a deployment has two options and both are bad: publish a secondaddress with a second sign-in, or reimplement session sealing, PKCE and CSRF
inside the plugin — which is the portal's job and an authorization surface no
plugin should own.
The change
A plugin entry may declare
portalPath:https://<portal>/reports/...is then proxied to thereportsplugin exactlyas
/adminis proxied to admin: same session requirement, same prefixstripping, same signed
x-portal-identity. The plugin verifies that header andpasses it to core, which stays the only thing deciding what a person may read.
The deployment target derives the plugin's own internal address — a network
alias on docker,
<appPrefix>-<name>.internalon fly — and setsPORTAL_PLUGIN_UPSTREAMSon the portal andPORTAL_BASE_PATHon the plugin, soan operator never writes a container or app URL by hand and the surface gets
back the prefix the portal strips.
proxyToSurfacealready took an arbitrary upstream base, so this extends onerecord by configuration rather than adding routing.
What is refused, and why it is refused rather than documented
All three of these fail quietly, which is what makes them worth code:
admin,auth,v1,web-ui,healthz,connect,drop,d,deployments, and the broker prefix. Aplugin mounted on one of these would take that route over.
an identity header for whatever it is pointed at, so a public URL here would
hand a person's verified identity to a third party. A bare label with no dot
is admitted, because that is how every container already addresses every
other one on the docker target.
The CLI rejects all three when the config loads. The portal rejects them again
at boot and refuses to start rather than dropping the entry — a dropped entry is
silent, and the request would then reach
web-uiinstead of 404ing. Theportal's copy is the one that has to hold: it does not trust its own
environment.
Anonymous playground sessions are refused a plugin surface, on the same
reasoning as
/connectand/drop: the portal cannot know what a deployment'sown surface does with an identity it is handed.
Compatibility
Additive. A deployment that declares no
portalPathsets no new variable androutes identically —
PORTAL_PLUGIN_UPSTREAMSis absent, the upstream map isthe same two entries, and the fall-through to
web-uiis unchanged.Tests
plugins/portal/test/plugin-surface.test.ts— the proxy path end to end(prefix stripped, identity header present, root reachable, an adjacent segment
still falling through to
web-ui), the anon refusal, the 401 with no session,the parser's whole refusal table, and two boot refusals driven by spawning the
portal and asserting it exits non-zero.
cli/test/portal-plugin-surface.test.ts— the config schema, both targets'derived environment, the reserved-path and duplicate-path refusals, and
PORTAL_BASE_PATHbeing the target's to set.Suites run: root 4295 passed / 0 failed (159 skipped — Postgres), CLI 534,
portal plugin 124.
typecheck,lint,lint:ox,lint:knipandformat:checkclean.No new rendered surface: the page a visitor sees under
/reportsis theplugin's own, unchanged, and the only HTML this adds a path to is the existing
playground-restricted 403. Happy to put a mock plugin behind a booted portal and
attach the result if a reviewer wants to see it rendered.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.