') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); rest/meta: org-scoped metadata READ routes (/history, /diff, single-item dashboard overlay) read env-wide under isolated multi-org — the session org is not forwarded · Issue #13406 · objectstack-ai/objectstack · GitHub
Skip to content

rest/meta: org-scoped metadata READ routes (/history, /diff, single-item dashboard overlay) read env-wide under isolated multi-org — the session org is not forwarded #13406

Description

@baozhoutao

On a single-DB multi-org deployment (OS_TENANCY_POSTURE=isolated), overlay-type metadata authored org-scoped is under-served by several REST read doors because those routes do not forward the caller's session org — organizationId defaults to null, so the read resolves at env scope and misses the org row. Sibling doors (/audit, single-item view overlay read, rollback) ARE org-aware, so this is an inconsistency, not a global posture gap. No cross-org leak — the caller's OWN org data is under-served.

Confirmed by two independent agents (original run + a dedicated rule-7 verify-pass), each authoring its own org-scoped overlays and confirming the pg rows exist before hitting the read door.

Symptoms (all reproduced ×2, active org = a real org, rows pg-verified)

1. GET /api/v1/meta/:type/:name/history{events:[]} while sys_metadata_history holds the org-scoped rows.

  • Repro: author a dashboard org-scoped, publish it twice → pg sys_metadata_history has 4 rows (create + publish ×2), all organization_id=<org>. GET …/history returns {events:[]}. Sibling GET …/audit returns the 4 org-scoped events. Exact asymmetry.
  • Root (confirmed in source): rest-server.ts:5755 calls historyMetaItem({type,name,…environmentId…}) with noorganizationId; protocol.ts:14601 does const orgId = request.organizationId ?? null → reads env scope (14606), finding nothing. The /audit route (rest-server.ts:5876) passes organizationId: ctx?.tenantId ?? null.

2. GET /api/v1/meta/:type/:name/diff cannot resolve org-scoped versions.

  • Repro: diff?from=1&to=2 between two org-scoped revisions (rev1 Users w3 vs rev2 Users v2 w6) → {fromVersion:1,toVersion:2,added:[],removed:[],changed:[]}; no-params → {fromVersion:null,toVersion:null,…}.
  • Root: diffMetaItem (protocol.ts:18484) carries the identical orgId = request.organizationId ?? null; the route (rest-server.ts:6202) omits org.

3. Single-item GET /api/v1/meta/dashboard/<name> ignores an org-scoped overlay — kind asymmetry vs view.

  • Repro: author an org-scoped overlay on packaged dashboard system_overview (widget0 title → sentinel): PUT 200, pg shows the org-scoped active row with the new title — but GET /meta/dashboard/system_overview (and the list read) serve the base title. The same shape on a packaged view (GET /meta/view/<name>) DOES serve the overlay. Same org, both rows in pg; the dashboard single-item read does not merge the org overlay, the view read does.

Fix direction

Thread the session org into the read path for history, diff, and the single-item overlay-merge read (dashboard and any other overlay kind that resolves like it), mirroring how /audit, single-item view, and rollback already resolve ctx.tenantId. Prior art in the same family (metadata route org-scoping, all closed): #10340, #10503, #6780.

Discovered on a live multi-node EE deployment; getViewsByObject's object-scoped omission looked related at first but is a distinct, org-independent container-expansion gap — filed separately.

QA-source: #13404 · studio-authoring.draft-publish-lifecycle · history,diff
QA-source: #13404 · studio-authoring.packaged-display-class-direct-edit · dashboard-overlay

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions