Refuse suspended tenants at resolution (#7) - #34
Conversation
|
Thanks for this, and for the comments — the reasoning in them is the part I'd have had to ask about otherwise. Two things I checked rather than took on trust. The tests are real. I mutation-tested them instead of reading them: removing the suspension check fails five, and making it also refuse tenants the registry has never heard of fails another — because you wrote a test asserting exactly that case is still served. That is a defended decision rather than an accident, and it is the right one. Turning every deployment with an unpopulated registry into one that rejects all traffic would be a bad surprise. Also worth knowing: this now composes correctly with the One thing needs changing before this can merge. The comment says:
That property disables the whole of The cost is not nothing, either. Two changes and I am happy to merge it:
Nothing here is a redesign, and the shape of the change is right. Happy to talk through either if you would rather do it differently. |
|
Thanks for the careful review, and for mutation-testing rather than reading. The unknown-tenant case was You're right about tenantlayer.registry.enabled=false. When I wrote that comment the registry did much less, and
One question on the cache: I plan to keep it on the enforcement path only, leaving JdbcTenantRegistry.find() |
|
Enforcement path only, and for a reason beyond keeping the PR focused.
The two paths genuinely want different things. Enforcement is per-request, hot, and perfectly happy with an answer that is a few seconds stale. Provisioning and Thirty seconds sounds right for the default. Worth a line in the docs saying suspension takes effect within the TTL rather than instantly, so nobody is surprised by a suspended tenant being served for a moment after the switch is flipped. |
dad20a1 to
eed698b
Compare
Signed-off-by: rajdeephere <rajdeep.workx@gmail.com>
Signed-off-by: rajdeephere <rajdeep.workx@gmail.com>
…io#7) Signed-off-by: rajdeephere <rajdeep.workx@gmail.com>
eed698b to
4173373
Compare
What this changes
Closes #7. The registry's
statuscolumn has existed since 0.1.0 and, by its own javadoc,was "carried and reported, not yet used to reject requests". This makes it mean something:
when a
TenantRegistrybean exists,TenantFilterlooks the resolved tenant up — aftermembership verification, before the tenant is bound — and refuses any status other than
ACTIVEwith a 403.That is the same rule
forEachTenanthas always applied, so a suspended tenant is off forits users and for the nightly job at the same moment. A tenant is never off for one and on
for the other; there is a test that asserts exactly that.
Two boundaries drawn deliberately:
control from status, and enforcing it here would turn every deployment with an
unpopulated registry into one that rejects all traffic.
insecure value is the default is a rejection; the way to say "I don't want this" is
tenantlayer.registry.enabled=false, which is a decision someone has to type.Mutation test
Three mutations, each reverted after:
if (false)in place of the status check — never refusesuspendedTenantIsRefused,refusedRequestBindsNoTenant,filterAndForEachTenantAgreeOnWhoIsServedACTIVE, serveSUSPENDEDactiveTenantIsServedTenantContext.enter()above the status checkrefusedRequestBindsNoTenant,registryFailureIsNotFailOpenThe second one is the one I care about. A filter that rejects everything satisfies
"a suspended tenant is refused" trivially, so
activeTenantIsServedexists to catch it andits assertion message says so. The third confirms the "before any connection is bound"
ordering is load-bearing rather than merely asserted in a comment — with the binding moved
one line earlier, a refused request leaves the tenant on the thread.
— the analogue here is
suspendedTenantIsRefusedpaired withactiveTenantIsServedin-memory registry and
MockHttpServletRequest, no databasePublic API
TenantResolver,TenantContextStorage,TenantScope,TenantMembershipVerifier,TenantRegistry, or anything inio.tenantlayer.testTenantFiltergained a fifth constructor argument, the registry. It is not in the listabove, and both existing constructors still compile and behave exactly as before — they
delegate with a null registry, which skips the check.
Behaviour change worth flagging
The registry is autoconfigured whenever a
DataSourceexists, so with this change everyscoped request performs one registry lookup. Two consequences, both in the changelog:
DataSourcebut notenantlayer_tenantstable will now failrequests with a registry error rather than serve them. Loud on purpose, but it is a
behaviour change. The DDL is in
TenantRegistrySchema.DDL; the opt-out istenantlayer.registry.enabled=false.JdbcTenantRegistry.find()is an uncachedSELECTper scoped request. I have notadded caching, because a cached suspension that takes effect "eventually" is a worse
thing to ship than a query — and
reactivationTakesEffectImmediatelypins that. But itdoes soften the "no runtime dependency on the registry for request handling" line in
architecture.md, which this PR rewrites accordingly. Happy to take direction here: ashort-TTL cache with a documented staleness window is the obvious alternative, and it is
your call whether that trade is worth making.
Checks
git commit -s)mvn testpasses — 159 tests, 0 failures, 0 errorsmvn test -Phibernate7passes — 159 tests, 0 failures. Nothing here touchespersistence, but I ran it anyway and confirmed the profile really does put
Hibernate 7.0.2 and Jakarta Persistence 3.2 on the classpath (6.6.18 / 3.1.0 by
default), rather than trusting a green build
is on the classpath
architecture.md,configuration.md,recipes.md,securing-resolution.md,tenant-registry.md, and a changelog entryThe recipe in
recipes.mdandsecuring-resolution.mdthat told people to check status byhand inside a
TenantMembershipVerifierhas been removed rather than left to rot, sincethe library now does it.