forked from wangziqi/ruoyi-vue-pro
feat(education): complete Flyway migration and atomic submit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user