Repository navigation
/static serves build assets only; all mesh content moves to /api/content (#587) - #911
Conversation
Fixes #587. Supersedes the gate in #907, which was rejected: protected content must be UNREACHABLE under /static, not reachable-then-denied. /static now has no access control at all, because nothing needing protection is served there. Two surfaces, opposite contracts: - /static/{mount}/{file} — build assets only, backed by a new StaticAssetMount read straight from the assembly manifest. No IContentService, no IPathResolver, no hub post, no identity, no permission check anywhere. public/immutable. UserContextMiddleware re-excludes /static/ — it must not even HAVE a caller to check. - /api/content/{node}[/{collection}]/{file} — all mesh content. Resolves the owner via IPathResolver, then posts GetDataRequest (which carries RequiresPermission(Read)) to that node's hub with the caller's AccessContext. private/no-store. The collectionCache short-circuit is DELETED: the config lookup was the only permission-bearing hop, and a cache hit skipped it entirely — a standing bypass. Staying on /static: NodeTypeIcons (42 SVGs EmbeddedResource-compiled into MeshWeaver.Graph) and DocContent (images shipped in the nuget package). Both are build output, not mesh content, and serving them from the manifest rather than a collection is what makes /static access-control-free BY CONSTRUCTION rather than by policy. Neither declares IsStatic, so neither is fetchable on the content route either. Unmounted: the mesh-level storage collection — every partition's content at content/{nodePath}, the #587 headline hole — and Memex.LocalMesh's own /static, which resolved any collection with no identity resolution at all. IsStatic (default-closed) is now actually READ, as the content route's mount check. Generators redirected across the repo, including MarkdownExtensions .ToStaticHref to ToContentHref, which rewrites every image in every markdown document and which #907 never touched; plus 200 authored literals across 190 files, marketing links, stale doc pages, and the agent guidance that told models to write /static logos. Readers still accept the legacy prefix persisted on nodes. Tests: StaticContentUnmountedTest (15, real HTTP over TestServer) proves the backing store is 404 for anonymous AND for a fully-entitled admin — unreachable, not denied — while the same file IS served to an authorized caller via /api/content; ContentUrlGenerationTest (25) pins every generator; ContentReferenceIntegrityTest is the durable repo-scan guard behind "static should not be referenced by anyone" and found 9 real offenders on its first run. Fail-without: with probes re-mounting the collections, 5 of 15 fail including BackingStoreUrl_IsNotReachableUnderStatic_ForAnonymous — "Expected NotFound but found OK", the vulnerability reproduced. Probes reverted: 243/243. Suites green across Blazor 243, Content 246, Markdown 137, Portal.Shared 238 and six more; independently re-verified 40/40 on the three new classes after merging main. Release -warnaserror clean on all 16 touched projects. Closes #587 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR hardens content serving by making /static serve build assets only (no identity, no ACL), and moving all mesh content file serving to the access-controlled /api/content/... route. It updates URL generators, docs, samples, and adds/adjusts tests to ensure protected content is unreachable under /static.
Changes:
- Introduces build-asset mounts (e.g.,
NodeTypeIcons,DocContent) for/static, and ensures mesh content is published only via/api/content. - Redirects content URL generation across markdown, layout areas, file browser, branding/export readers, and sample data from
/static/...to/api/content/.... - Adds/updates integration + guard tests to enforce “unreachable under
/static” and “no/staticreferences” invariants.
Reviewed changes
Copilot reviewed 242 out of 243 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.PluginCatalog.Test/NodeRepoPluginListingTest.cs | Updates plugin fixture poster URL to /api/content/... and asserts new value. |
| test/MeshWeaver.Hosting.Monolith.Test/TestData/MeshWeaver/Documentation/DataMesh/UnifiedPath.md | Updates test doc front-matter Icon URL to /api/content/.... |
| test/MeshWeaver.Hosting.Blazor.Test/UserContextMiddlewareExclusionTest.cs | Updates middleware exclusion expectations: /static excluded again; /api/content must resolve user. |
| test/MeshWeaver.Hosting.Blazor.Test/MeshWeaver.Hosting.Blazor.Test.csproj | Adds monolith test base reference for real HTTP endpoint tests. |
| test/MeshWeaver.Content.Test/MarkdownFileParserTest.cs | Updates expectations for resolved icon URLs and serialization behavior under /api/content. |
| test/MeshWeaver.Autocomplete.Test/AutocompleteIconTests.cs | Updates icon expectations to /api/content/... in autocomplete results. |
| src/MeshWeaver.Markdown/MarkdownExtensions.cs | Rewrites markdown image hrefs to use api/content/... (relative to <base href>). |
| src/MeshWeaver.Markdown.Export/Branding/ExportTemplateResolver.cs | Switches template path parsing to ContentPathReference. |
| src/MeshWeaver.Markdown.Export/Branding/CorporateIdentity.cs | Updates docs to reference /api/content/... path style. |
| src/MeshWeaver.Markdown.Export/Branding/BrandingResolver.cs | Uses ContentPathReference to accept content: + legacy + /api/content shapes. |
| src/MeshWeaver.Layout/DocumentSourceControl.cs | Updates docs/examples to indicate source file is served via /api/content/.... |
| src/MeshWeaver.Hosting/Persistence/Parsers/MarkdownFileParser.cs | Resolves relative icon paths via content-route helpers and avoids re-serializing resolved URLs. |
| src/MeshWeaver.Graph/SettingsLayoutArea.cs | Updates UI placeholders/docs to stop suggesting /static content paths. |
| src/MeshWeaver.Graph/NodeTypeLayoutAreas.cs | Updates UI placeholders/docs to stop suggesting /static content paths. |
| src/MeshWeaver.Graph/NodeIconPickerDialog.cs | Updates docs to reflect /api/content rewrite behavior. |
| src/MeshWeaver.Graph/MeshNodeThumbnailControl.cs | Uses content-route helper for thumbnails (no /static for mesh content). |
| src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs | Updates image rendering to use /api/content route. |
| src/MeshWeaver.Graph/MeshNodeImageHelper.cs | Uses content-route helper for content: icon references. |
| src/MeshWeaver.Graph/CreateLayoutArea.cs | Updates docs to reflect /api/content icon URL support. |
| src/MeshWeaver.Graph/Configuration/GraphConfigurationExtensions.cs | Registers NodeTypeIcons as a build-asset mount for /static. |
| src/MeshWeaver.GitSync/ContentAssetMapper.cs | Updates docs to reflect new content URL scheme. |
| src/MeshWeaver.Documentation/DocumentationExtensions.cs | Registers DocContent as a build-asset mount; publishes page assets via content route. |
| src/MeshWeaver.Documentation/Data/index.md | Updates documentation space icon URL to /api/content/.... |
| src/MeshWeaver.Documentation/Data/DataMesh/UnifiedPath.md | Updates docs to describe /api/content/... as the HTTP surface for files. |
| src/MeshWeaver.Documentation/Data/DataMesh/CreatingNodeTypes.md | Updates example icon URL to /api/content/.... |
| src/MeshWeaver.Documentation/Data/Architecture/StaticRepoImport.md | Updates docs to reference /api/content/... serving of imported assets. |
| src/MeshWeaver.Documentation/Data/Architecture/OnDeviceVoice.md | Updates catalog URL to /api/content/... for model downloads. |
| src/MeshWeaver.Documentation/Data/Architecture/OnboardingNewEnvironment.md | Updates rollout verification probes from /static to /api/content. |
| src/MeshWeaver.Documentation/Data/Architecture/AuthoringDocumentation.md | Updates authoring guidance to avoid /static for content. |
| src/MeshWeaver.ContentCollections/ContentService.cs | Ensures IsStatic publication is taken from the mapping config, not inherited from backing store. |
| src/MeshWeaver.ContentCollections/ContentPathReference.cs | Adds helper to normalize content: + legacy /static/... + /api/content/... references. |
| src/MeshWeaver.ContentCollections/ContentLayoutArea.cs | Renders PDFs via /api/content URL helper instead of /static. |
| src/MeshWeaver.ContentCollections/ContentCollectionConfig.cs | Clarifies IsStatic now controls publication on the access-controlled content route (not /static). |
| src/MeshWeaver.ContentCollections.Indexing.Graph/DocumentLayoutAreas.cs | Builds source-file URLs using ContentFileRoutePrefix (/api/content). |
| src/MeshWeaver.Blazor/Infrastructure/UserContextMiddleware.cs | Re-excludes /static/ from identity resolution; clarifies /api/content is the gated route. |
| src/MeshWeaver.Blazor/FileExplorer/FileBrowser.razor.cs | Redirects download link generation from /static/... to /api/content/.... |
| src/MeshWeaver.Blazor/Components/NamedAreaView.razor.cs | Updates docs to reflect /api/content/... site-relative meta URL resolution. |
| samples/Todo/MeshWeaver.Todo/TodoApplicationExtensions.cs | Publishes embedded resources on /api/content and updates thumbnails path. |
| samples/Northwind/MeshWeaver.Northwind.Application/NorthwindViewModels.cs | Updates thumbnails path from /static to /api/content. |
| samples/Northwind/MeshWeaver.Northwind.Application/NorthwindApplicationExtensions.cs | Publishes embedded resources on /api/content. |
| samples/Insurance/MeshWeaver.Insurance.Domain/InsuranceApplicationExtensions.cs | Publishes embedded resources and mapped submission files on /api/content. |
| samples/Graph/Data/Systemorph/Marketing/*.json | Updates many sample icon/imageLink URLs from /static to /api/content. |
| samples/Graph/Data/Systemorph.json | Updates sample space icon/logo URLs to /api/content. |
| samples/Graph/Data/Northwind/ReportsCatalog/Source/ReportsCatalogLayoutAreas.cs | Updates thumbnail image URLs to /api/content. |
| samples/Graph/Data/Northwind/Article/Source/ArticleLayoutAreas.cs | Updates thumbnail image URLs to /api/content. |
| samples/Graph/Data/Northwind/*.md / *.json | Updates space/catalog icons from /static to /api/content. |
| samples/Graph/Data/MeshWeaver*.md / *.json | Updates sample space icons/logos to /api/content. |
| samples/Graph/Data/FutuRe/** | Sweeps sample content to update icon URLs from /static to /api/content. |
| samples/Graph/Data/Cornerstone/** | Updates sample icon URLs from /static to /api/content. |
| samples/Graph/Data/Brand/Systemorph.json | Updates corporate identity logo/icon paths to /api/content. |
| samples/Graph/Data/ACME/** | Updates sample icon URLs and docs to /api/content. |
| memex/Memex.Portal.Shared/MemexConfiguration.cs | Publishes per-node content + attachments + static assets collections on /api/content. |
| memex/Memex.Portal.Shared/Authentication/OnboardingMiddleware.cs | Excludes /api/content from onboarding redirects to avoid broken asset fetches. |
| memex/Memex.Portal.Shared/Api/MeshApiEndpoints.cs | Updates docs/comments to reference /api/content instead of /static. |
| memex/Memex.Portal.Monolith/Program.cs | Ensures mesh-level storage backing store is not published (IsStatic stays false). |
| memex/Memex.LocalMesh/Program.cs | Changes /static endpoint to serve only build-asset mounts (no content collections). |
| memex/aspire/Memex.Portal.Distributed/Program.cs | Ensures distributed storage backing store is not published (IsStatic stays false). |
| deploy/aks/DEPLOY-RUNBOOK.md | Updates rollout verification probe URLs from /static to /api/content. |
| content/ai/Skill/space.md | Updates agent guidance to prefer content: → /api/content for logos; forbids /static for content. |
| content/ai/Agent/ToolsReference.md | Updates guidance to forbid file-path icons (now including /api/content/...). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // /api/content/{collection}/content/{path} — fold the default-collection segment out so the | ||
| // result is the same {collection}/{path} the other two shapes produce. | ||
| var rest = path[routePrefix.Length..]; | ||
| var marker = $"/{ContentCollectionsExtensions.DefaultCollectionName}/"; | ||
| var at = rest.IndexOf(marker, StringComparison.OrdinalIgnoreCase); | ||
| return at < 0 ? rest : rest[..at] + "/" + rest[(at + marker.Length)..]; |
| var address = host.Hub.Address.ToString(); | ||
| var staticUrl = $"/static/{address}/{ContentCollectionsExtensions.DefaultCollectionName}/{contentPath}"; | ||
| var staticUrl = ContentCollectionsExtensions.GetContentFileUrl( | ||
| address, ContentCollectionsExtensions.DefaultCollectionName, contentPath); | ||
|
|
||
| return Observable.Return<UiControl?>( | ||
| Controls.Html($"<img src='{staticUrl}' alt='{Path.GetFileName(contentPath)}' style='max-width: 100%;' />")); |
| /// <summary> | ||
| /// Resolves a node's icon for rendering, handling content: references relative to the node path. | ||
| /// E.g., "content:icon.svg" on node "Org/Project" → "/static/storage/content/Org/Project/icon.svg". | ||
| /// E.g., "content:icon.svg" on node "Org/Project" → "/api/content/Org/Project/content/icon.svg". | ||
| /// When the node carries no icon of its own, falls back to a default icon for its | ||
| /// <see cref="MeshNode.NodeType"/> (<see cref="DefaultIconForNodeType"/>) so every node reads as |
Test Results (shard 3) 11 files 11 suites 5m 23s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)1 076 tests 1 071 ✅ 6m 24s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)672 tests 672 ✅ 5m 4s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 225 tests 1 225 ✅ 7m 4s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 671 tests 1 480 ✅ 5m 46s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 726 tests 1 724 ✅ 6m 52s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
Test Results 65 files 65 suites 36m 36s ⏱️ Results for commit aeef09d. ♻️ This comment has been updated with latest results. |
|
CI red on a single test, and it is not this change: #899 now blocks essentially every PR in the repo — it has redded #905, #907, #910's own run and this one. This PR should merge once #910 lands, not before; re-running shard 5 here would only re-sample the same deadlock. 🤖 Generated with Claude Code |
Summary
Fixes #587. Supersedes the gate in #907, which was rejected: protected content must be unreachable under
/static, not reachable-then-denied./staticnow has no access control at all — because nothing that needs protecting is served there.Two surfaces with opposite contracts:
/static/{mount}/{file}— build assets only. Backed by a newStaticAssetMount(Segment, Assembly, ResourcePrefix)read straight from the assembly manifest. NoIContentService, noIPathResolver, no hub post, no identity resolution, no permission check anywhere.public, max-age=30d, immutable.UserContextMiddlewarere-excludes/static/— it must not have a caller to check./api/content/{node}[/{collection}]/{file}— all mesh content. Resolves the owning node withIPathResolver, then postsGetDataRequest(which carries[RequiresPermission(Read)]) to that node's hub with the caller'sAccessContext.private, no-store.The
collectionCacheshort-circuit is deleted — it was a standing bypass: the config lookup was the only permission-bearing hop, and a cache hit skipped it entirely.What stays on
/static, and why that is safe by constructionNodeTypeIconsEmbeddedResource-compiled intoMeshWeaver.Graph. No user data, identical in every deployment, needed before any identity exists (login page, nav, anonymous cards). ~457 existing references + DB rows (migration V41) keep working unchanged.DocContentMeshWeaver.Documentation, shipped on nuget.org. Same class; ~50 references unchanged.Both are build output, not mesh content. Serving them from the assembly manifest rather than a collection is what lets
/staticbe access-control-free by construction instead of by policy. Both remain registered as in-mesh collections for listings/reads, but neither declaresIsStatic, so neither is fetchable on the content route either.Unmounted
storage—IsStaticremoved inMemex.Portal.Monolithandaspire/Memex.Portal.Distributed. This is the collection holding every partition's content atcontent/{nodePath}/…— the Hardening: /static/ endpoint serves content collections without authentication or access control #587 headline hole. Now unreachable over HTTP.Memex.LocalMesh's own/static— was a reduced copy that resolved any collection with no identity resolution at all. Build-asset mounts only now.IsStatic(default-closed) is now actually read, as the content route's mount check.URL generators redirected
MeshNodeImageHelper·MeshNodeThumbnailControl·MeshNodeLayoutAreas.RenderImageAsync·MarkdownFileParser(resolve + serialize strip) ·ContentLayoutArea.RenderPdf·DocumentLayoutAreas·FileBrowser(both hand-duplicated download links) ·ContentCollectionsExtensions.AdaptResourceUrl·MarkdownExtensions.ToStaticHref→ToContentHref— the biggest miss, and #907 never touched it: it rewrites every image in every markdown document ·BrandingResolver/ExportTemplateResolver(readers, now viaContentPathReference, still accepting the legacy prefix persisted on nodes) · 5 sample layout areas ·WithThumbnailsPathin Northwind/Todo · 200 authored literals across 190 sample/doc/test-data files · 3 marketingimageLinks · 6 stale doc pages ·content/ai/Skill/space.md+Agent/ToolsReference.md(agent guidance that told models to write/staticlogos) · plugin-catalog fixture · deploy runbook.Post-sweep,
/staticsurvives only as: route/prefix declarations,NodeTypeIcons/DocContent, prose describing the contract, and two deliberate legacy-read constants.Verification
StaticContentUnmountedTest(15, real HTTP over TestServer against a real mesh): the backing store is 404 for anonymous AND for a fully-entitled admin — unreachable, not denied; icons serve identically to both withpublic/immutable; the same file is served to an authorized caller via/api/content; an unpublished collection 404s even for an admin; a gated response is neverpublic.ContentUrlGenerationTest(25) pins every generator + the traversal rule.ContentReferenceIntegrityTest.MeshContentUrls_AreNeverAddressedThroughTheStaticRoute— the durable repo-scan guard behind "static should not be referenced by anyone". It found 9 real offenders on first run (3 marketing links + 6 stale doc pages), all fixed.Fail-without: with three probes applied (re-mount collections on
/static, drop theIsStaticcheck, drop the traversal guard) → 5 of 15 fail, includingBackingStoreUrl_IsNotReachableUnderStatic_ForAnonymous→ "Expected NotFound … but found OK" — i.e. the vulnerability reproduced. Probes reverted, 243/243 on the identical tree.Suites: Hosting.Blazor 243/0 · Content 246 (1 pre-existing skip) · ContentCollections 28 · Markdown 137 · Markdown.Export 19 · Documentation 19 · Autocomplete 150 · Memex.Portal.Shared 238 · ContentCollections.Indexing.Graph 31. Independently re-verified 40/40 across the three new classes after merging current main.
-c Release -warnaserrorclean on all 16 touched projects.🚨 Review before merge — broad blast radius (243 files)
/playwrightpass over images, PDFs, the file browser and doc diagrams.Icon/CorporateIdentity.LogoPathrows holding/static/storage/content/…still resolve through the readers, but a stored value rendered directly into an<img src>will 404 until re-saved. No migration written — flagged rather than guessed./static/...URLs (e.g. an AgenticEngineeringposter) now 404; those need a content-side edit./api/content/{node}/{file}: if the file path's first segment matches a collection name on that node, the explicit-collection branch wins. Documented; no collision in the current tree.AdaptResourceUrlwith a null address now returns the relative URL unchanged — zero callers, contract-only.Closes #587
🤖 Generated with Claude Code