Skip to content

importObject's opts.name override bypasses the namespace prefix and persists an unprefixed federated object #11061

Closed

Description

@os-warren

Found while working #10712 (PR #11059), which gave generateObjectDraft a
${namespace}_ prefix derived from the datasource's owning package. Filed rather than
fixed there: #10712 is scoped to the draft output, and this is a different path with
a different question behind it. Unassigned, awaiting first-touch grading.

What

ExternalDatasourceService.importObject reuses the draft pipeline, so it inherits the
prefix — but only when the caller does not name the object itself:

const name = opts.name ?? draft.name;

opts.name is taken verbatim. A caller passing { "name": "customers" } gets an object
persisted as customers, with no ${namespace}_ prefix, via
persistObject → metadata.register('object', name, definition).

Reachable today

Not hypothetical, and not dependent on #10712's change. ImportObjectOpts extends GenerateDraftOpts with name?: string ("Override the auto-derived object name"), and
the import route forwards the request body as opts unfiltered —
packages/rest/src/external-datasource-routes.ts:462:

const result = await svc.importObject(
  req.params.name,
  /* remote */ …,
  (req.body as Record<string, unknown>) ?? {},
);

So POST /api/v1/datasources/:name/external/tables/:remote/import with
{"name": "customers"} reaches it.

Why this is not simply "the caller owns the name"

That reading is defensible on its own, and may well be the right ruling — but the
runtime write path this lands on does not apply the check that would catch it.
validateObjectNamespacePrefix runs in two places: defineStack() (compile time) and
MetadataManager.publishPackage / the protocol's publish pre-flight (the Studio
"publish" path, NAMESPACE_PREFIX). metadata.register('object', …) is neither. So an
imported object with an explicitly unprefixed name is registered and served without any
gate having looked at the name.

Note the in-tree callers do not currently exercise this: objectui's
ImportObjectDialog posts no name, and os datasource introspect does not import at
all. The exposure is the route body.

The question a first-touch should answer

Three shapes, none of which a dev should pick unilaterally:

  • A. Apply the prefix to opts.name too (same validateObjectNamespacePrefix
    treatment as the derived name). Consistent, but silently rewrites a name the caller
    explicitly asked for.
  • B. Refuse an opts.name that fails the prefix rule, with the rule's own actionable
    message. Contract-first, and matches what the publish gate does with the same
    violation — but it turns a currently-accepted request into a 4xx.
  • C. Leave it: an explicit override is the caller's decision. Then the gap worth
    closing is arguably the broader one — that metadata.register('object', …) applies no
    namespace check at all — which is a much wider question than this seam.

Related: #10712 / PR #11059 (the draft half, where the prefix derivation lives),
#11000 (the unauthorable primaryKey key in the same generator).

Activity

  1. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 22, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 23, 2026
  3. added theissue type on Aug 23, 2026
  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue, domain:services, Bug. importObject's opts.name override bypasses the namespace prefix and persists an unprefixed federated object — invariant restore (the prefix rule is the declared behavior); loud refusal or forced prefixing, implementer argues which in the PR.


    Generated by Claude Code

  5. claude commented on Aug 23, 2026

    @claude
    Contributor

    Contract review — PASS(fable 席审核,session 5213b871-5164-5bc3-8874-28b336bbcd40)

    授权来源:maintainer 2026-08-23「要不还是你挂个定时处理审核吧」——常设合同审核职责。

    降档保险丝读数(本席自读 get_session → external_metadata.last_served_model):claude-fable-5,符合 CONTRACT_REVIEW_TIER(scripts/pm/dispatch-gates.mjs:2816)。

    审核对象:PR #11241(external-datasource import 命名前缀拒绝)。

    判定依据:

    • importNameRefusedError 走 The direct-mount REST package door answers 500 INTERNAL_ERROR for coded 4xx errors the dispatcher twin maps correctly #8016 拒绝信封:code: 'EXTERNAL_IMPORT_ERROR'、status: 400,refusal 在 draft 之前抛出,无部分持久化。
    • 前置检查仅在 opts.name !== undefined 时触发,normaliseNamespace(await this.config.getNamespace?.(datasource)) 兼容无 namespace 配置的数据源(此时不拒绝,与现约定一致)。
    • 测试钉住合同三要素:status).toBe(400)、code).toBe('EXTERNAL_IMPORT_ERROR')、message 含修复指引("Rename it to 'wh_orders'"),并断言 nothing persisted。

    结论:拒绝面收窄且 fail-closed,信封形状与 #8016 一致,无既有导入路径行为变化。PASS,清除 needs:contract-review(卡 + PR 双载体)。


    Generated by Claude Code

  6. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions