# 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 - [x] Browser headers are described as forgeable consistency evidence, not trusted identity. - [x] Public existence disclosure and the narrower lifecycle-indistinguishability guarantee are explicit. - [x] Public Tenant Handle is distinguished from the source Tenant Code and has exact mutability/case/format semantics. - [x] Local behavior has one named, secure-default configuration seam and unambiguous precedence. - [x] Canonical stored website compatibility policy is selected without claiming data migration. - [x] Every error category has exact HTTP status, stable `CommonResult` code, message, data shape, and redaction rules. - [x] 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.