Repository navigation
tenants: a tenant's seat limit - provisioner-owned tenant properties, and Settings → Users refuses an invitation beyond the seats (#8593) - #8595
Merged
Conversation
… and Settings → Users refuses an invitation beyond the seats (#8593) Cause: a tenant may be sold a fixed number of seats, and the provisioning system refuses an invitation beyond them only after the Owner's request was published and processed - the Users screen did not know the limit, and the platform had no place for a per-tenant value the provisioner decides and the tenant must not change. Change: - Tenant properties (core-tenants): DIRIGIBLE_TENANT_PROPERTIES in SystemDB, one row per tenant and one typed nullable column per property - the shape of DIRIGIBLE_TENANT_INITIALIZATIONS -, starting with TENANTPROP_SEAT_LIMIT. TenantProperties, TenantPropertiesService (find / save, which checks every field), a changeset, and TenantPropertiesTenantTeardown (order 410) for deprovisioning. - The users snapshot body gains a top-level seatLimit (a whole number of at least 1, checked by value; absent or null clears it), stored as the tenant's property whatever its users' revisions. - TenantUserCommands.invite refuses 409 SEAT_LIMIT when the users PENDING, INVITED or ASSIGNED, the invitee left out, are at the limit - before publishing; role changes and removals never are. - The context carries seatLimit and seatsUrl (DIRIGIBLE_TENANT_USERS_SEATS_URL, optional); the screen shows "Seats: N of M" and links "Get more seats" there and under a SEAT_LIMIT refusal (en-US, bg-BG). Verified: formatter:validate (cache wiped); the unit suites of core-tenants, core-tenants-users, core-liquibase (SystemChangelogIdempotencyTest) and commons-config; TenantUserSnapshotApiIT, TenantUsersIT and TenantUsersSettingsIT (19 tests, the browser one against Keycloak); the release javadoc build of core-tenants and commons-config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nistrator for more (#8593) A tenant's Owner may not be the one who can buy seats, and the platform does not know who sells them, so DIRIGIBLE_TENANT_USERS_SEATS_URL, the context's seatsUrl and the three "Get more seats" links are gone. The SEAT_LIMIT refusal now reads "Every seat of this tenant is taken. Remove someone to free a seat, or ask your administrator for more seats." (en-US, bg-BG), in the endpoint's message as on the screen. Verified: formatter:validate (cache wiped); the unit suites of core-tenants, core-tenants-users and commons-config; TenantUserSnapshotApiIT, TenantUsersIT and TenantUsersSettingsIT (19 tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, not the deprecated asText (#8593) CodeQL java/deprecated-call on the seat-limit case added by this PR. TenantUsersIT: 10/10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cause
A tenant may be sold a fixed number of seats. The provisioning system refuses an invitation beyond them, but only after the Owner's request was published and processed: Settings → Users accepted the invitation, showed it in progress, and a moment later showed it refused. The screen could not do better, because it did not know the limit - and the platform had no place to keep a per-tenant value that the provisioner decides and the tenant must not change.
Change
core-tenants):DIRIGIBLE_TENANT_PROPERTIESin SystemDB, one row per tenant, one typed nullable column per property - the shape ofDIRIGIBLE_TENANT_INITIALIZATIONS, not key/value rows (the platform keeps those for open-ended keys only). The first column isTENANTPROP_SEAT_LIMIT.TenantProperties,TenantPropertiesRepository,TenantPropertiesService(find/save, which checks every field), a changeset indirigible-system.jsonwith itsMARK_RANprecondition.TenantPropertiesTenantTeardown, order 410: after the user replica (300), whose screen reads it, beside the initialization record (400).PUT …/tenants/{tenantId}/users) gains a top-levelseatLimit: a whole number of at least 1, checked by value so a fraction is refused rather than truncated; absent or null clears it.TenantUserReplicaService.applystores it as the tenant's property whatever its users' revisions (the latest body wins; a seat change draws no revision).TenantUserCommands.inviteanswers409 SEAT_LIMIT- Every seat of this tenant is taken. Remove someone to free a seat, or ask your administrator for more seats. - when the usersPENDING,INVITEDorASSIGNED, the invitee left out, are at or above the limit - afterALREADY_MEMBER/REQUEST_PENDING, before anything is published. A role change or a removal is never refused for seats, and a lowered limit removes nobody.seatLimit; Settings → Users shows "Seats: N of M" (over the limit too) and the refusal's text in the invite dialog. A user'slastErrorSEAT_LIMIThas its text. en-US and bg-BG..claude/docs/tenants.md: the seats rules, a Tenant properties section, the 410 teardown.Two places where this follows the code rather than the issue's wording:
core-liquibase), so the table comes with a changeset, and a later property needs an appendedaddColumnchangeset;hbm2ddl=updateonly fills gaps.applyruns one transaction per user, so the limit is stored in a transaction of its own, before the users, rather than "in the same transaction as the users".Verification
mvn formatter:formaton the changed modules, thenmvn -T 1C formatter:validatewith the formatter cache wiped: BUILD SUCCESS.core-tenants-users73 tests (new:SEAT_LIMITcounting, pending/failed/removed, the invitee's own row, a lowered limit vs role change and removal, no limit; the limit stored even when every user is ignored, cleared, not rewritten, refused before writing; validation),core-tenants37 (new:TenantPropertiesServiceTest,TenantPropertiesTenantTeardownTest),core-liquibase(SystemChangelogIdempotencyTest),commons-config.TenantUserSnapshotApiIT(new case: the limit stored, kept by an ignored body, refused for0/2.5/"many", cleared, and the row gone afterDELETE …/tenants/{id}),TenantUsersIT(new case: the context'sseatLimit, an invitation at the limit409 SEAT_LIMITwith nothing on the queue, then cleared and published),TenantUsersSettingsIT(new browser case against Keycloak: "Seats: 1 of 1", the refusal's full text in the invite dialog, nothing published) - 19 tests, all green, on both commits.core-tenantsandcommons-config: BUILD SUCCESS, no warning on the new code.seatLimitin the users push).Screenshots
Fixes #8593
🤖 Generated with Claude Code