Files
ruoyi-vue-pro/docs/education/migration/issues/EDU-003-tenant-resolution-decision.md

17 KiB

EDU-003 — Decide tenant resolution and student-principal policy

  • Status: done — corrected policy and EDU-004 test seams are implementation-ready
  • Type: decision
  • Phase: 1
  • Blockers: EDU-000

Decision outcome

Public tenant resolution accepts caller-supplied locator claims and is not an authentication boundary. Browser Origin/Referer evidence improves browser-context consistency but is forgeable by non-browser clients. The accepted contract therefore documents tenant-existence disclosure, uses one redacted unavailable response for unknown/disabled/expired tenants, requires abuse controls, and reserves authenticated/signed locators for deployments that require spoof resistance. Authenticated Education context is Member-only. This ticket records policy and tests-to-write only; it changes no production behavior.

Domain language

  • A Tenant Locator Claim is an unauthenticated pre-login value used to request tenant selection: either a browser-context hostname claim or an explicit Public Tenant Handle. It is not proof of caller identity or tenant authorization.
  • Browser-context evidence is a normalized host derived from Origin, falling back to Referer. It can bind browser UX inputs consistently, but any HTTP client can forge it.
  • A Public Tenant Handle is the current System tenant's unique name used as an exact public lookup key because the target has no separate stable tenant-code capability. It is not called a Tenant Code. It is case-sensitive, must match ^[A-Za-z0-9._-]{2,64}$, and administrators must treat it as immutable after publication. A future mutable display label must be a separate field.
  • A Student Principal is an authenticated identity whose LoginUser.userType is UserTypeEnum.MEMBER. A generic authenticated account is not necessarily a Student Principal.
  • Public Tenant Resolution maps a Tenant Locator Claim to minimal login-routing fields. It intentionally discloses existence when a claim succeeds; it does not disclose whether a failed tenant is unknown, disabled, or expired.

Evidence reviewed

  • Legacy apps/api/src/features/tenant/locator.ts parses and compares Origin, Referer, request hosts, and a tenant code, but does not authenticate header provenance.
  • Legacy resolver.ts compares the source slug with the explicit code, proving the source Tenant Code was distinct from its display name.
  • The target has no System-owned stable tenant-code field or API. system_tenant.name is unique and mutable through administration; it is the only current exact generic lookup key.
  • Current EducationTenantController accepts arbitrary public hostname or tenantName, preserves ports, returns status and Education-configured loginMethods, and exposes distinct unknown/disabled/expired errors.
  • EducationProperties.hostnameTenantMap documents lowercase host-only keys without ports, while current implementation and tests preserve ports.
  • system_tenant.websites is an exact string-list lookup and existing target tests demonstrate values containing a scheme. No normalization seam currently makes those values host-only.
  • EducationContextController derives IDs from framework contexts but reads only the user ID and therefore does not reject an authenticated ADMIN principal.
  • TenantSecurityWebFilter already fills a missing request tenant from the authenticated principal, rejects authenticated principal/request-tenant mismatch, requires a tenant for non-ignored URLs, and validates tenant availability.
  • TenantCommonApi exposes generic System-owned tenant lookup methods; TenantApiImpl implements them, but the interface currently hides missing adapters behind UnsupportedOperationException defaults and has no focused owning-module contract test.
  • No verified Member public interface advertises enabled login methods. MemberConfigApi currently exposes points configuration only.

ADR: public tenant-resolution and student-principal policy

Status

Accepted for EDU-004.

Context and trade-off

The resolver is public and cannot authenticate Origin, Referer, or ordinary query/header values. Browser headers are useful for consistent browser routing, not identity. A successful lookup necessarily distinguishes an available tenant from a failed candidate when it returns routing fields. The contract can hide lifecycle state among failures, but cannot honestly promise general non-enumeration without an unguessable or signed locator.

The target also lacks the source system's distinct stable tenant code. Adding one would require a separately designed System-owned capability and likely data work. For the current slice, the existing unique System tenant name is explicitly exposed as a constrained Public Tenant Handle; it is no longer mislabeled as a Tenant Code.

Decision

  1. Production browser-context binding, not trusted identity

    • Derive browser-context evidence from a valid HTTP(S) Origin; if absent, use a valid HTTP(S) Referer.
    • A supplied hostname may only confirm that evidence. A mismatch is a public locator conflict.
    • Origin and Referer are untrusted caller claims. Proxy preservation and forwarding-header controls do not make them authentic and are not cited as spoofing protection.
    • A non-browser/headless caller can forge either header and probe hostnames. This accepted threat is handled through the public disclosure policy and abuse controls below.
    • A deployment requiring spoof resistance must replace this public mode with an authenticated/signed locator or a host value supplied through a separately designed trusted-proxy boundary. That stronger mode is not implemented by EDU-004.
  2. Explicit headless handle and legacy query compatibility

    • A headless client may submit tenantHandle, defined above as the existing System tenant unique name under a constrained public contract.
    • The legacy public tenantName query is unsupported and must be rejected, not silently aliased. EDU-004 adds a compatibility test for its rejection/removal.
    • A browser domain claim and explicit tenantHandle may be supplied together only when both resolve to the same tenant; disagreement is a public locator conflict.
  3. Local-development activation seam

    • The sole authority is yudao.education.tenant-resolution.local-development-enabled.
    • Its secure default is false; absence means production-safe behavior. Spring profile names and environment names do not implicitly enable it.
    • Only developer workstations and automated tests may set it to true; shared, staging, and production deployments must keep it false.
    • When enabled, a configured local request host (localhost, *.localhost, loopback IPv4, 0.0.0.0, or ::1) may resolve without a handle. If tenantHandle is also present, the explicit handle takes precedence.
    • EDU-004 tests code-less local host, local host plus handle, and rejection of local/request-host fallback when the flag is absent or false.
  4. Hostname identity and normalization

    • Tenant hostname identity is host-only: trim whitespace, lowercase, remove one trailing dot, remove IPv6 brackets, and discard default or non-default ports.
    • Accept valid DNS hosts, IPv4, and IPv6; reject credentials, paths, comma-separated/multi-value input, malformed authorities, and unsupported schemes.
    • localhost:48080 normalizes to localhost.
    • Canonical system_tenant.websites entries used for this resolver are host-only values in the same normalized form. Entries containing a scheme, path, credentials, comma-separated values, or a port are legacy/non-canonical configuration and are not matched by Public Tenant Resolution.
    • EDU-004 implements canonical exact lookup and focused tests; it does not silently normalize legacy stored candidates at read time. Tenant administrators must correct non-canonical website configuration before enabling domain resolution. If later inventory requires automated data correction, that becomes a separately scoped Flyway/data ticket using flyway-postgresql; EDU-004 must not claim such correction.
  5. Authenticated Education context

    • /education/context obtains the full LoginUser, rejects missing authentication, and rejects userType != UserTypeEnum.MEMBER.
    • User and tenant IDs continue to come only from security and tenant contexts.
    • EDU-004 preserves and does not duplicate or bypass TenantSecurityWebFilter mismatch and availability checks.
  6. Login-method metadata ownership

    • Login-method metadata belongs to Member authentication, not System tenant metadata and not Education.
    • EDU-004 removes loginMethods from Education resolution and deprecates Education configuration/documentation that presents it as authoritative.
    • If later routing proves it necessary, introduce only a minimal Member-owned public interface with focused Member tests; do not create tenant-specific auth configuration in Education.
  7. Exact external wire contract

    • The target framework represents business failures as HTTP 200 OK with a CommonResult envelope. EDU-004 keeps that convention; tests assert both transport status and envelope.
    • Malformed, missing, locally forbidden, or otherwise unsupported locator claim: HTTP 200; CommonResult.code = 1005001003; msg = "租户识别请求无效"; data = null.
    • Domain/handle or browser-evidence/requested-host conflict: HTTP 200; CommonResult.code = 1005001008 (new stable Education business code); msg = "租户识别信息冲突"; data = null.
    • Unknown, disabled, or expired tenant: HTTP 200; CommonResult.code = 1005001004; msg = "当前租户不可用"; data = null.
    • Messages contain no rejected host/handle, lifecycle status, System exception text, or lookup detail. Logs may record a reason category and correlation metadata but must not log secrets or echo unsanitized header values.
    • Unknown, disabled, and expired paths must have identical status, code, message, JSON field set, null-data shape, and no intentional timing distinction. System errors remain internal.
    • Success is HTTP 200, code = 0, msg = "", and data contains only tenantId and displayName. displayName currently comes from the System tenant name; because that same field is the current Public Tenant Handle, an exact handle lookup necessarily returns the submitted handle as displayName. A future non-echoing mutable label requires a separate System-owned public display field. The response contains no separate handle field, raw status, websites, expiry, package, private configuration, internal lifecycle detail, or loginMethods.
  8. Disclosure and abuse threat model

    • The resolver is not generally non-enumerating: a valid Public Tenant Handle or domain claim yields success with tenant ID/display name, while an unavailable candidate yields the generic failure.
    • The accepted guarantee is only unknown/disabled/expired indistinguishability.
    • EDU-004 must attach the public resolver to the repository's existing public API rate-limiting/ingress mechanism where available, emit structured success/failure-category security metrics, and document alerting for sustained candidate probing. If no reusable limiter seam exists, EDU-004 records that operational blocker rather than inventing an Education-only limiter.
  9. System seam

    • Retain TenantCommonApi as the generic System-owned seam; do not add Education-specific locator, branding, redaction, or login-method concepts.
    • Replace UnsupportedOperationException lookup defaults with required abstract methods and add focused TenantApiImpl contract tests.
    • EducationTenantController remains the public adapter applying claim consistency, canonical website policy, availability coarsening, exact errors, and redaction.

Rejected alternatives

  • Treating Origin or Referer as authenticated tenant identity.
  • Claiming forwarding-header ingress controls authenticate browser headers.
  • Claiming general non-enumeration while successful lookup returns identifying fields.
  • Silently aliasing System tenant name to the distinct Tenant Code domain term.
  • Continuing the legacy tenantName public query.
  • Silently normalizing scheme/path/port-bearing stored website values during lookup.
  • Port-sensitive tenant identity.
  • ADMIN accepted as Student Principal.
  • Education-owned login methods or an Education-specific System API.

Consequences

  • EDU-004 intentionally changes current query, response, error, local-mode, website, and port behavior.
  • Published Public Tenant Handles use the System tenant unique name; renaming one is a breaking login-routing change until a genuine stable System-owned code exists.
  • Non-canonical website entries require configuration correction before domain resolution is enabled; no database change is authorized here.
  • Public existence disclosure is accepted and must be monitored and throttled. Strong spoof resistance requires a future signed/authenticated locator design.

EDU-004 exact test matrix

Scenario Exact expected behavior Owning test seam
Valid production Origin Resolve normalized domain claim; success HTTP 200/code 0 Education controller HTTP test
Missing Origin, valid Referer Resolve normalized Referer host; success HTTP 200/code 0 Education controller HTTP test
Forged but syntactically valid browser header Documented as accepted untrusted claim; no authenticity assertion Education controller test name/documentation
Malformed Origin HTTP 200/code 1005001003/generic message/null data; no fallback Education controller HTTP test
Origin/requested-host mismatch HTTP 200/code 1005001008/generic conflict/null data Education controller HTTP test
Arbitrary production hostname without browser evidence HTTP 200/code 1005001003 Education controller HTTP test
Explicit tenantHandle Exact case-sensitive constrained System-name lookup Education controller HTTP test
Legacy tenantName query Rejected/unsupported; HTTP 200/code 1005001003 Education controller compatibility HTTP test
Unknown, disabled, expired Identical HTTP 200/code 1005001004/message/JSON/null data Education controller HTTP parameterized test
Host case/trailing dot/IPv4/IPv6/ports Canonical host-only identity; all ports discarded Education normalization/HTTP tests
Non-canonical stored website candidate Not matched; generic unavailable response System adapter fixture plus Education HTTP test
Local flag absent/false Local/request-host fallback rejected with code 1005001003 Education controller HTTP test
Local flag true, code-less local host Configured local host may resolve Education controller HTTP test
Local flag true, local host plus handle Explicit handle takes precedence Education controller HTTP test
Domain/handle agreement Resolve one tenant Education controller HTTP test
Domain/handle conflict HTTP 200/code 1005001008 Education controller HTTP test
Anonymous /education/context Existing unauthorized contract Education context HTTP test
Missing tenant or authenticated mismatch Existing filter behavior remains active Framework TenantSecurityWebFilter tests
ADMIN/MEMBER principal ADMIN rejected; MEMBER accepted with context-derived IDs Education context HTTP tests
Public field redaction Success has only tenantId/displayName; failure has code/msg/data only Education controller HTTP test
TenantCommonApi adapters Required methods delegate/map; no unsupported defaults System TenantApiImpl contract test
Abuse controls Reused limiter/ingress attachment and structured category metric proven, or blocker recorded Configuration/integration test where seam exists

Acceptance criteria

  • Browser headers are described as forgeable consistency evidence, not trusted identity.
  • Public existence disclosure and the narrower lifecycle-indistinguishability guarantee are explicit.
  • Public Tenant Handle is distinguished from the source Tenant Code and has exact mutability/case/format semantics.
  • Local behavior has one named, secure-default configuration seam and unambiguous precedence.
  • Canonical stored website compatibility policy is selected without claiming data migration.
  • Every error category has exact HTTP status, stable CommonResult code, message, data shape, and redaction rules.
  • EDU-004 has exact production/test seams and legacy tenantName compatibility coverage.

Verification

Static design review only. Reviewed legacy locator.ts/resolver.ts, current Education controller/properties/error codes, CommonResult and global error handling, TenantSecurityWebFilter, TenantCommonApi/TenantApiImpl, System tenant name/website storage and tests, Member public interfaces, the completed EDU-016 ticket, tracker, and dirty working tree. No production implementation, build, database connection, Flyway execution, or migration was performed by EDU-003.