Add private Sites and owner access - #247
Conversation
Use the established UI URL, authentication setting and Sites configuration instead of private- site-specific settings.
vigneshrajsb
left a comment
There was a problem hiding this comment.
Inline walkthrough of the production changes for V1 Sites access control.
| email: identity.email, | ||
| preferredUsername: identity.preferredUsername, | ||
| displayName: identity.displayName, | ||
| issuer: identity.issuer ?? null, |
There was a problem hiding this comment.
New personal keys retain the verified issuer with the subject. Site ownership must not depend on a mutable email or username.
| const input = body as Record<string, unknown>; | ||
| const visibility = readSiteVisibility(input.visibility); | ||
| const revision = readSiteRevision(input.expectedAccessRevision, true)!; | ||
| const site = await new SitesService().setVisibility((await params).siteId, visibility, principal, revision); |
There was a problem hiding this comment.
Visibility has its own owner-authorized mutation. Requiring the access revision prevents an old page from overwriting a newer publish or privacy decision.
| try { | ||
| const upload = await readUploadFile(req); | ||
| const service = new SitesService(); | ||
| const existing = await service.getSite(routeParams.siteId, principal); |
There was a problem hiding this comment.
Check access before reading the upload body, then let the service recheck ownership and revisions when it commits the replacement.
| try { | ||
| const service = new SitesService(); | ||
| const site = await service.extendSite(routeParams.siteId); | ||
| const site = await service.extendSite( |
There was a problem hiding this comment.
Extension now uses the caller identity and optional access revision, so only the owner can extend the current Site state.
| try { | ||
| const service = new SitesService(); | ||
| const site = await service.getSite(routeParams.siteId); | ||
| const site = await service.getSite(routeParams.siteId, principal); |
There was a problem hiding this comment.
The single-Site endpoint now applies the same private metadata boundary as listing: only the owner can retrieve a private Site.
| } | ||
|
|
||
| /** RFC 7662, authenticated as this confidential client; no Admin API role expansion. */ | ||
| async introspectAccessToken(token: string): Promise<boolean> { |
There was a problem hiding this comment.
The existing confidential Keycloak client checks whether a user access token is still active; no new bridge credential is introduced.
| } | ||
| } | ||
|
|
||
| async getOAuthTokenStatus(token: string): Promise<'active' | 'revoked' | 'unknown'> { |
There was a problem hiding this comment.
An unknown identity-provider response fails Site access closed rather than treating an unverified token as active.
| this.assertEnabled(config); | ||
| const principal = input.principal; | ||
| const machine = principal.kind === 'service_key'; | ||
| const visibility = input.visibility ?? (machine ? 'public' : 'private'); |
There was a problem hiding this comment.
User and personal-key uploads default to private; service-key uploads create public Sites for automation workflows.
| }, | ||
| }, | ||
| schemas: { | ||
| SitesBrowserMintRequest: { |
There was a problem hiding this comment.
The published API contract includes the browser ticket request and the new Site visibility, revision, and capability fields consumed by clients.
| res.end(statusCode === 404 ? 'not found' : 'internal server error'); | ||
| return true; | ||
| } | ||
| await handleSitesRequest(req, res, service); |
There was a problem hiding this comment.
The gateway delegates Site traffic to the authorization-aware handler before any content is streamed.
Some deployments can't put Sites content on a separate registrable domain from the UI. Add SITES_ALLOW_SHARED_APEX to bypass that check per-deployment, paired with a tighter gateway CSP (connect-src/form-action 'self') so hosted content can't reach elsewhere on the shared apex.
Denied-owner responses now return a distinct 403 with a clear message instead of collapsing into the same 404 used for missing/deleted Sites. This is an internal tool, so anti-enumeration protection is traded for clarity: a denied teammate now knows to ask the owner for access rather than assuming the link is broken.
…hange Missed in the earlier 404->403 pass: a service-key-owned site's nonowner delete, and the test that asserted missing/private deletion errors were identical (now intentionally distinct: 404 for missing, 403 for denied).
Replacing content or toggling visibility no longer bumps a site to the top of the list; the list now reflects when a site was first created.
Adds private-by-default Sites for users, with owner checks across the API, gateway, and MCP. Existing Sites and service-key uploads stay public. A Site keeps its ID and content URL when visibility changes.
Migration 035 preserves existing Site rows and supports an intentional full downgrade that makes active Sites public. Stop old writers and gateways before starting the new core image.
Validation: 309 focused tests, server typecheck, lint, and an isolated PostgreSQL migration up/down check passed. Live private-open retest remains pending.
Companion PRs: UI · CLI · Helm · Docs