Skip to content

Commit 97fabdd

Browse files
cemililikclaude
andcommitted
docs: correct three statements the code contradicts
TenancyDbContext said `tenants` gets no query filter. It gets one of a different shape — self-keyed, comparing `Id` rather than a `TenantId` column — which the self-keyed branch in TenantQueryFilters has applied since Packet 7. One of the eight entity types genuinely gets none, not two. permissions.md named `tenancy.tenant.admin` as the key that will govern the host mapping, while the matrix directly above gives `HostMapping` its own resource with no `write`. The prose now names `tenancy.hostmapping.admin` and says why the resource is separate: pointing a hostname at a tenant is an admin-scope act, not something the everyday tenant-admin role should carry. ADR-0036's Decision still prints the normalization order Amendment 1 measured as wrong — rejecting IPv4 literals before stripping a port, which lets 1.2.3.4:8080 through because TryParse fails on the port-bearing string and never runs again. The statement was false when it entered the record, so it takes the default instrument: an inline erratum beside it, pointing at the amendment that corrects it. The Decision is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent cd15f22 commit 97fabdd

3 files changed

Lines changed: 22 additions & 6 deletions

File tree

‎backend/src/Modules/Tenancy/LearnStack.Modules.Tenancy.Infrastructure/Persistence/TenancyDbContext.cs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ namespace LearnStack.Modules.Tenancy.Infrastructure.Persistence;
2626
/// <b>The global query filters come from the base.</b>
2727
/// <c>TenantScopedDbContext</c> owns the two members they close over and applies
2828
/// one to every entity implementing <c>ITenantOwned</c>; this context adds none
29-
/// of its own. Two of its eight entity types deliberately get no filter:
30-
/// <see cref="Tenants"/>, which is tenant-owned <b>self-keyed</b> — its <c>id</c>
31-
/// is the tenant id, and its policy says so — and
29+
/// of its own. <see cref="Tenants"/> gets a filter of a different SHAPE, not none:
30+
/// it is tenant-owned <b>self-keyed</b> — its <c>id</c> is the tenant id, and its
31+
/// policy says so — so the predicate compares <c>Id</c> rather than a
32+
/// <c>TenantId</c> column. An earlier version of this paragraph said it got no
33+
/// filter at all, which the self-keyed branch in <c>TenantQueryFilters</c>
34+
/// contradicts. One of the eight entity types genuinely gets none:
3235
/// <see cref="PlatformHostMappings"/>, which is <b>platform-scoped</b> and read
3336
/// in order to determine the tenant, so a tenant-keyed predicate on it would make
3437
/// host resolution return zero rows forever. Row Level Security remains the

‎docs/decisions/0036-tenant-resolution-trusted-inputs.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,16 @@ Load-bearing details:
174174
token and does not carry bare `key`, so existing Serilog and error-tracker redaction
175175
covers the header for free.
176176

177+
> **Erratum — 2026-09-04.** The order in the paragraph below rejects IPv4 literals
178+
> *before* stripping a port, and that is the order Packet 4 measured as wrong: it
179+
> lets `1.2.3.4:8080` through, because `IPAddress.TryParse` fails on the
180+
> port-bearing string and the check never runs again after the port is stripped.
181+
> The corrected order strips the port first and rejects the literal after; shown by
182+
> § Amendments, Amendment 1, and narrowed further by its own 2026-09-02 erratum.
183+
> The Decision is unchanged. Current authority:
184+
> [Amendment 1](#2026-08-20--amendment-1-the-normalization-order-corrected-by-measurement)
185+
> and `EffectiveHost.Normalize`.
186+
177187
**Normalization is a total pure function.** `EffectiveHost.Normalize(string) → string?`
178188
is the sole producer of both the lookup key and the `app.resolving_host` value. Every
179189
failure returns `null` (⇒ unresolved ⇒ 404); nothing throws. Do **not** use

‎docs/modules/tenancy/permissions.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ refuses the write.
3131

3232
**`MapHostToTenantCommand` is the one to gate first.** It writes the row that
3333
decides whose data an anonymous request sees, which makes it the highest-value
34-
write in the module and the reason `tenancy.tenant.admin` — not
35-
`tenancy.tenant.write` — is the key that will govern it. The three keys are
36-
registered with the rest in Phase 03.
34+
write in the module — and the reason the matrix above gives `HostMapping` its own
35+
resource with **no `write`**: pointing a hostname at a tenant is an admin-scope
36+
act, so `tenancy.hostmapping.admin` is the key that will govern it rather than any
37+
grant inside the everyday tenant-admin role. `tenancy.tenant.admin` governs
38+
provisioning and `tenancy.organization.write` the second organization. All three
39+
are registered with the rest in Phase 03.
3740

3841
| Resource | read | write | delete | admin | Default role grants |
3942
|----------|:----:|:-----:|:------:|:-----:|---------------------|

0 commit comments

Comments
 (0)