# EDU-004 — Enforce tenant resolution and student identity boundaries - **Status:** done — accepted tenant-locator and Member-principal policy implemented and verified; ingress/IP-only probing throttle remains an operational blocker - **Type:** implementation - **Phase:** 1 - **Blockers:** EDU-003 ## Selected policy from EDU-003 - `Origin` then `Referer` supplies forgeable browser-context evidence, not trusted identity. It binds browser UX inputs but does not prevent non-browser spoofing. - Headless clients use `tenantHandle`, the existing System tenant unique `name` under an exact constrained public contract; do not call it a Tenant Code. - Reject/remove the legacy public `tenantName` query as a separate compatibility behavior. - Production domain/handle agreement is checked; disagreement is a locator conflict. - Host identity removes case, whitespace, trailing dot, IPv6 brackets, and all ports. - Local fallback is controlled only by `yudao.education.tenant-resolution.local-development-enabled`, default `false`; profiles do not implicitly enable it. - Canonical System website values for this resolver are normalized host-only strings. Scheme/path/port-bearing stored candidates are not silently normalized and require configuration correction or a separate future Flyway/data ticket. - `/education/context` accepts only an authenticated `UserTypeEnum.MEMBER` Student Principal. - Unknown/disabled/expired failures are identical, but successful resolution still discloses tenant existence. Reuse rate limiting/ingress controls and emit abuse-monitoring metrics. - Login-method metadata is Member-owned; remove/deprecate Education `loginMethods` unless a minimal Member public interface is first proven necessary. - Retain generic `TenantCommonApi`, make its lookup methods required, and add System-owned `TenantApiImpl` contract tests. See [`EDU-003-tenant-resolution-decision.md`](EDU-003-tenant-resolution-decision.md) for rationale and exact threat model. ## Implementation result Implemented the accepted public HTTP contracts for tenant resolution and Member-only Education context. The resolver treats `Origin`, `Referer`, `hostname`, and `tenantHandle` only as forgeable locator claims; successful responses expose only `tenantId` and `displayName`, and unknown/disabled/expired tenants share the exact unavailable envelope. Local fallback is controlled exclusively by the secure-default property, including browser-derived loopback hosts, and only configured local hosts use the Education mapping. Production domains always use the canonical System website lookup. `TenantCommonApi` lookup methods are now required and System-owned adapter tests prove exact delegation and DTO mapping. Existing `TenantSecurityWebFilter` production behavior was unchanged and is covered by focused filter-boundary regression tests. No adequate reusable candidate-probing rate-limit attachment was found. The existing `ClientIpRateLimiterKeyResolver` includes attacker-controlled method arguments in its key, so attaching it would create per-candidate limits rather than an IP-only probing limit. No Education-only limiter was introduced; ingress/IP-only throttling and alerting remain an operational blocker. The Education module also has no direct generic Micrometer dependency seam, so structured resolver metrics remain part of the same operational blocker rather than adding an Education-only dependency or abstraction. System currently has no separate public tenant display field: its unique `name` is both the Public Tenant Handle and the only public label available through `TenantCommonApi`. Therefore handle-based success returns that same value as `displayName`; tests now model this real exact-name adapter behavior. Suppressing that value requires a future generic System-owned display-field capability, not an Education workaround. No database or Flyway change was made or executed. ## Student outcome A student receives minimal login-routing data for an available tenant, while authenticated Education endpoints reject wrong tenants and non-Member principals. The public resolver does not claim that caller-supplied locator headers authenticate tenant identity. ## Scope - browser-context domain claim selection from `Origin`, then `Referer`; - requested-host confirmation and domain/handle conflict handling; - explicit `tenantHandle` exact lookup and legacy `tenantName` rejection/removal; - host-only normalization and canonical stored-website behavior; - secure-default local-development configuration seam and precedence; - Member/student principal enforcement for `/education/context`; - exact public `CommonResult` contract and safe response fields; - minimal generic `TenantCommonApi` adjustment with System-owned tests; - reuse of public resolver rate limiting/ingress controls and structured abuse metrics where an existing seam is available. ## Exact external contract All business outcomes use the framework convention of HTTP `200 OK` with `CommonResult`: | Category | HTTP | `CommonResult.code` | `msg` | `data` | |---|---:|---:|---|---| | Malformed/missing/untrusted/locally forbidden locator | 200 | `1005001003` | `租户识别请求无效` | `null` | | Requested-host/domain/handle conflict | 200 | `1005001008` | `租户识别信息冲突` | `null` | | Unknown, disabled, or expired tenant | 200 | `1005001004` | `当前租户不可用` | `null` | | Success | 200 | `0` | empty string | object containing only `tenantId`, `displayName`; current `displayName` is System tenant `name` and therefore equals a successful handle claim | Failure messages and JSON shape never contain the rejected host/handle, lifecycle state, System exception detail, or lookup reason. Unknown, disabled, and expired paths must be byte-shape equivalent after normal serialization and have no intentional timing distinction. ## Reuse boundaries - Reuse `TenantSecurityWebFilter`, `TenantContextHolder`, System Tenant public APIs, Member/System security context, `UserTypeEnum`, and an existing public rate-limit/ingress seam if present. - Do not duplicate tenant tables, token logic, login methods, RBAC, or a generic rate-limiter in Education. - Education must not depend on System internal Services, Mappers, or DOs. - A non-Education change must be generic, minimal, backward-compatible, and tested in its owning module. - If no reusable abuse-control seam exists, record the operational blocker; do not invent an Education-only infrastructure abstraction. ## Required TDD tests ### Education tenant controller HTTP tests - valid production `Origin`; valid `Referer` fallback; - syntactically valid forged header is treated only as an untrusted claim, with no authenticity assertion; - malformed Origin and no attacker-selected fallback: exact HTTP/code/msg/data; - Origin/requested-host mismatch: exact conflict contract; - arbitrary production hostname without browser evidence: exact invalid contract; - explicit case-sensitive `tenantHandle` with `^[A-Za-z0-9._-]{2,64}$` validation; - legacy `tenantName` query rejected/removed independently; - domain/handle agreement and conflict; - unknown/disabled/expired exact identical wire shape; - case, trailing dot, DNS, IPv4, bracketed IPv6, default and non-default port normalization; - local flag absent/false rejects local/request-host fallback; - local flag true permits code-less configured local host; - local flag true plus handle gives the handle precedence; - non-canonical stored website candidate does not match; - success exposes only `tenantId` and System tenant `name` as `displayName`; for handle lookup this necessarily equals the submitted handle until System owns a separate public display field; no separate handle field, status, websites, expiry, package, private config, or `loginMethods`. ### Education context HTTP tests - unauthenticated context rejected; - ADMIN principal rejected; - MEMBER principal accepted and IDs derived only from security/tenant contexts. ### System contract tests - `TenantCommonApi` lookup methods are required, not optional unsupported defaults; - `TenantApiImpl` exact name and canonical website lookups delegate and map DTOs; - non-canonical website candidates are not silently normalized by the Public Tenant Resolution path. ### Framework/configuration tests - existing missing-tenant and authenticated tenant/header mismatch filter behavior remains active; - local-development flag defaults false and is not inferred from a Spring profile; - reusable limiter/ingress attachment and structured success/failure-category metric are verified where an existing seam is identified. ## Acceptance criteria - [x] Client `tenantId` and `userId` are never authoritative. - [x] Browser headers are documented and implemented as forgeable context claims, not authentication. - [x] Public existence disclosure is accepted; only unknown/disabled/expired status is indistinguishable. - [x] `tenantHandle` is not mislabeled as Tenant Code; mutability, case, and format match EDU-003. - [x] Legacy `tenantName` is rejected/removed and covered by a compatibility test. - [x] Local fallback uses the named secure-default property and unambiguous precedence. - [x] Canonical website compatibility policy is implemented without silent legacy normalization. - [x] Exact HTTP/CommonResult code/message/data contracts are asserted. - [x] Authenticated Student context enforces Member principal type. - [x] Existing framework mismatch checks remain active and are not bypassed. - [x] Public abuse-control gap is truthfully recorded; no inadequate or Education-only limiter was introduced. - [x] Every non-Education modification has a necessity explanation and focused owning-module tests. - [x] API documentation matches implementation and tests. ## Verification Run focused Education, System, framework, and configuration tests as applicable, then: ```bash git diff --check mvn -pl yudao-server -am -DskipTests clean compile ``` No database change is in scope. If investigation proves automated website data correction is required, stop that part, create a separate blocked data/Flyway ticket, and invoke `flyway-postgresql` before any schema/data work. ## Risk and rollback - **Risk:** High security, disclosure, and login-routing impact. - **Rollback:** Application/configuration rollback to the previous resolver adapter; keep local mode disabled by default and do not weaken authenticated tenant checks. No destructive tenant-data operation.