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 toReferer. 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
nameused 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.userTypeisUserTypeEnum.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.tsparses and comparesOrigin,Referer, request hosts, and a tenant code, but does not authenticate header provenance. - Legacy
resolver.tscompares the sourceslugwith 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.nameis unique and mutable through administration; it is the only current exact generic lookup key. - Current
EducationTenantControlleraccepts arbitrary publichostnameortenantName, preserves ports, returns status and Education-configuredloginMethods, and exposes distinct unknown/disabled/expired errors. EducationProperties.hostnameTenantMapdocuments lowercase host-only keys without ports, while current implementation and tests preserve ports.system_tenant.websitesis an exact string-list lookup and existing target tests demonstrate values containing a scheme. No normalization seam currently makes those values host-only.EducationContextControllerderives IDs from framework contexts but reads only the user ID and therefore does not reject an authenticated ADMIN principal.TenantSecurityWebFilteralready 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.TenantCommonApiexposes generic System-owned tenant lookup methods;TenantApiImplimplements them, but the interface currently hides missing adapters behindUnsupportedOperationExceptiondefaults and has no focused owning-module contract test.- No verified Member public interface advertises enabled login methods.
MemberConfigApicurrently 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
-
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
hostnamemay only confirm that evidence. A mismatch is a public locator conflict. OriginandRefererare 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.
- Derive browser-context evidence from a valid HTTP(S)
-
Explicit headless handle and legacy query compatibility
- A headless client may submit
tenantHandle, defined above as the existing System tenant uniquenameunder a constrained public contract. - The legacy public
tenantNamequery 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
tenantHandlemay be supplied together only when both resolve to the same tenant; disagreement is a public locator conflict.
- A headless client may submit
-
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 itfalse. - When enabled, a configured local request host (
localhost,*.localhost, loopback IPv4,0.0.0.0, or::1) may resolve without a handle. IftenantHandleis 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.
- The sole authority is
-
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:48080normalizes tolocalhost.- Canonical
system_tenant.websitesentries 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.
-
Authenticated Education context
/education/contextobtains the fullLoginUser, rejects missing authentication, and rejectsuserType != UserTypeEnum.MEMBER.- User and tenant IDs continue to come only from security and tenant contexts.
- EDU-004 preserves and does not duplicate or bypass
TenantSecurityWebFiltermismatch and availability checks.
-
Login-method metadata ownership
- Login-method metadata belongs to Member authentication, not System tenant metadata and not Education.
- EDU-004 removes
loginMethodsfrom 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.
-
Exact external wire contract
- The target framework represents business failures as HTTP
200 OKwith aCommonResultenvelope. 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 onlytenantIdanddisplayName.displayNamecurrently comes from the System tenantname; because that same field is the current Public Tenant Handle, an exact handle lookup necessarily returns the submitted handle asdisplayName. 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, orloginMethods.
- The target framework represents business failures as HTTP
-
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.
-
System seam
- Retain
TenantCommonApias the generic System-owned seam; do not add Education-specific locator, branding, redaction, or login-method concepts. - Replace
UnsupportedOperationExceptionlookup defaults with required abstract methods and add focusedTenantApiImplcontract tests. EducationTenantControllerremains the public adapter applying claim consistency, canonical website policy, availability coarsening, exact errors, and redaction.
- Retain
Rejected alternatives
- Treating
OriginorRefereras 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
nameto the distinct Tenant Code domain term. - Continuing the legacy
tenantNamepublic 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
CommonResultcode, message, data shape, and redaction rules. - EDU-004 has exact production/test seams and legacy
tenantNamecompatibility 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.