feat(education): complete Flyway migration and atomic submit

This commit is contained in:
2026-07-30 12:06:55 +08:00
parent ce02f8acb4
commit 79a5799502
228 changed files with 32551 additions and 1376 deletions

View File

@@ -0,0 +1,160 @@
# 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.