[finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

Description

@hotlong

Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

Summary

SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

Measurement

Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

crm_contact is itself controlled_by_parent under crm_account.

crm_contract field orderresolved masterrep reads crm_contract
crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

Why the second row is org-wide rather than merely "wrong master"

On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

Prior art checked (not duplicates)

  • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
  • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
  • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

Suggested directions (not deciding)

  1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
  2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
  3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

    Description

    @hotlong

    Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

    Summary

    SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

    constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

    pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

    The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

    Measurement

    Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

    crm_contact is itself controlled_by_parent under crm_account.

    crm_contract field orderresolved masterrep reads crm_contract
    crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
    crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

    contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

    Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

    Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

    Why the second row is org-wide rather than merely "wrong master"

    On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

    The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

    Prior art checked (not duplicates)

    • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
    • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
    • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

    Suggested directions (not deciding)

    1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
    2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
    3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

    Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

      Description

      @hotlong

      Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

      Summary

      SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

      constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

      pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

      The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

      Measurement

      Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

      crm_contact is itself controlled_by_parent under crm_account.

      crm_contract field orderresolved masterrep reads crm_contract
      crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
      crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

      contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

      Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

      Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

      Why the second row is org-wide rather than merely "wrong master"

      On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

      The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

      Prior art checked (not duplicates)

      • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
      • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
      • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

      Suggested directions (not deciding)

      1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
      2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
      3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

      Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

        Description

        @hotlong

        Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

        Summary

        SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

        constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

        pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

        The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

        Measurement

        Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

        crm_contact is itself controlled_by_parent under crm_account.

        crm_contract field orderresolved masterrep reads crm_contract
        crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
        crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

        contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

        Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

        Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

        Why the second row is org-wide rather than merely "wrong master"

        On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

        The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

        Prior art checked (not duplicates)

        • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
        • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
        • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

        Suggested directions (not deciding)

        1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
        2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
        3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

        Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

        Activity

        Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

        Metadata

        Metadata

        Assignees

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

          Description

          @hotlong

          Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

          Summary

          SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

          constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

          pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

          The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

          Measurement

          Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

          crm_contact is itself controlled_by_parent under crm_account.

          crm_contract field orderresolved masterrep reads crm_contract
          crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
          crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

          contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

          Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

          Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

          Why the second row is org-wide rather than merely "wrong master"

          On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

          The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

          Prior art checked (not duplicates)

          • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
          • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
          • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

          Suggested directions (not deciding)

          1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
          2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
          3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

          Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

            Description

            @hotlong

            Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

            Summary

            SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

            constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

            pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

            The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

            Measurement

            Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

            crm_contact is itself controlled_by_parent under crm_account.

            crm_contract field orderresolved masterrep reads crm_contract
            crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
            crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

            contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

            Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

            Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

            Why the second row is org-wide rather than merely "wrong master"

            On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

            The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

            Prior art checked (not duplicates)

            • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
            • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
            • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

            Suggested directions (not deciding)

            1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
            2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
            3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

            Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

            Activity

            Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

            Metadata

            Metadata

            Assignees

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

              Description

              @hotlong

              Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

              Summary

              SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

              constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

              pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

              The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

              Measurement

              Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

              crm_contact is itself controlled_by_parent under crm_account.

              crm_contract field orderresolved masterrep reads crm_contract
              crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
              crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

              contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

              Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

              Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

              Why the second row is org-wide rather than merely "wrong master"

              On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

              The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

              Prior art checked (not duplicates)

              • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
              • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
              • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

              Suggested directions (not deciding)

              1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
              2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
              3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

              Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [finding] controlled_by_parent master selection is silently decided by FIELD DECLARATION ORDER when an object declares several required lookups — reordering fields moves a security boundary with no diagnostic #14747

                Description

                @hotlong

                Measured on @objectstack/* 17.2.0 against a real booted kernel (ObjectQL + plugin-security + plugin-sharing) driving the HotCRM app metadata. No decision requested — filing the measurement.

                Summary

                SecurityPlugin.resolveCbpRelation picks the master relation with this precedence:

                constfound=pick((f)=>f?.type==="master_detail"&&f?.required)??pick((f)=>f?.type==="master_detail")??pick((f)=>f?.type==="lookup"&&f?.required);

                pick is entries.find(...), so within each tier the winner is whichever candidate appears first in the field map — declaration order. When an object declares two or more required lookups (and no master_detail), that order is the only thing deciding which object the record-level security derives from. Nothing reports the ambiguity: not objectstack validate, not objectstack lint, not a boot warning.

                The failure direction is the dangerous one. Moving a field up or down in a schema file is a review-invisible edit that reads as cosmetic, and it can silently repoint the security master.

                Measurement

                Fixture: two accounts (acct_US, acct_JP) owned by another user; a sales_rep who owns none of them and receives acct_US only through a territory sharing rule (sys_record_share, access_level: edit). Subject: HotCRM's crm_contract, which declares two required lookups — crm_account (declared first) and crm_contact — and no master_detail. crm_contract was set to controlled_by_parent for the measurement (an in-test stack override; nothing shipped).

                crm_contact is itself controlled_by_parent under crm_account.

                crm_contract field orderresolved masterrep reads crm_contract
                crm_account first (as authored)crm_accountcontract_US, contract_mixed — correct, narrowed to the shared account
                crm_contact first (same fields, order swapped, nothing else changed)crm_contactcontract_JP, contract_US, contract_mixedorg-wide

                contract_mixed is the discriminator row: crm_account: acct_US (readable) with crm_contact: contact_JP (not readable). contract_JP appearing in the second row is the whole finding — one reordering, and a record on an account the caller cannot see becomes readable.

                Control, live in the same runs: crm_opportunity_line_item (controlled_by_parent under crm_opportunity, which stays private) reads [oli_own] in both configurations, and oli_JP reads 0 rows and refuses the write. So the flip tracks the resolved master, not the harness.

                Same-run sanity: the sibling object crm_quote declares only ONE required lookup (crm_account; its crm_contact is optional and conditionally required), and its reach was identical in both runs — the order swap moved only the object that actually had two candidates.

                Why the second row is org-wide rather than merely "wrong master"

                On 17.2.0 the derivation does not compose across a chain, so a controlled_by_parent master resolves to no restriction and the child goes org-wide. That half is objectstack#11082, fixed by merged PR objectstack#11183 — the fix is present in this repo's main source and absent from every published version (17.2.0, the latest published, went out 2026-08-23T07:01:00Z, about seven hours before #11082 closed).

                The ambiguity survives that fix. Once the chain composes, picking crm_contact instead of crm_account no longer leaks org-wide — it silently derives access from a different object, so contract_mixed would follow the primary contact's account rather than the contract's own account. Narrower, still not what the author declared, and still unreported. The two are independent defects; this one is about which master gets chosen, not what happens after it is chosen.

                Prior art checked (not duplicates)

                • objectstack#7503 (closed) — publish-time lint for controlled_by_parent with no relation to derive from. This is the opposite case: too many candidates, not zero.
                • objectstack#9138 (closed) / objectstack#9139 (open) — force / lint required: true on a master_detail under controlled_by_parent. Adjacent, and lint: promote relationship/master-detail-required from warning to error, scoped to controlled_by_parent — ruled for the v18 boundary (Direction 1 of #8772) #9139's v18 direction would push authors toward an explicit master_detail, which would cover the lookup-tier case by construction. It does not address ambiguity within a tier (two master_detail fields pick by order the same way), and it is not landed.
                • objectstack#11082 (closed) / objectstack#5386 (closed) — the composition and share-folding defects. Different question.

                Suggested directions (not deciding)

                1. Report the ambiguity at publish time — if more than one candidate sits in the winning tier, name them and refuse, in the spirit of Publish-time lint: sharingModel: controlled_by_parent with no master_detail relation is statically detectable and unreported #7503. Silence is the part that makes this expensive.
                2. Let the author name the master explicitly — an authored key beats any precedence chain, and makes the security boundary greppable instead of positional.
                3. At minimum, document that the tie-break is declaration order, so an author reordering fields knows what they are moving.

                Reported by a HotCRM consumer: hotcrm#549's ruled conversion turns crm_contract into exactly this shape, so the app would inherit an order-dependent security master the day it lands.

                Activity

                Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                Metadata

                Metadata

                Assignees

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions