Skip to content

tenants: a tenant's seat limit - provisioner-owned tenant properties, and Settings → Users refuses an invitation beyond the seats (#8593) - #8595

Merged
iliyan-velichkov merged 3 commits into
masterfrom
issue-8593-tenant-seat-limit
Nov 13, 2026
Merged

iliyan-velichkov merged 3 commits into
masterfrom
issue-8593-tenant-seat-limit

Conversation

@iliyan-velichkov

@iliyan-velichkov iliyan-velichkov commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Tenant properties (core-tenants): DIRIGIBLE_TENANT_PROPERTIES in SystemDB, one row per tenant, one typed nullable column per property - the shape of DIRIGIBLE_TENANT_INITIALIZATIONS, not key/value rows (the platform keeps those for open-ended keys only). The first column is TENANTPROP_SEAT_LIMIT.
    • TenantProperties, TenantPropertiesRepository, TenantPropertiesService (find / save, which checks every field), a changeset in dirigible-system.json with its MARK_RAN precondition.
    • TenantPropertiesTenantTeardown, order 410: after the user replica (300), whose screen reads it, beside the initialization record (400).
    • Written only by the provisioning system, through the API that carries each property; no generic write route.
  • The users snapshot (PUT …/tenants/{tenantId}/users) gains a top-level seatLimit: a whole number of at least 1, checked by value so a fraction is refused rather than truncated; absent or null clears it. TenantUserReplicaService.apply stores it as the tenant's property whatever its users' revisions (the latest body wins; a seat change draws no revision).
  • The refusal: TenantUserCommands.invite answers 409 SEAT_LIMIT - Every seat of this tenant is taken. Remove someone to free a seat, or ask your administrator for more seats. - when the users PENDING, INVITED or ASSIGNED, the invitee left out, are at or above the limit - after ALREADY_MEMBER/REQUEST_PENDING, before anything is published. A role change or a removal is never refused for seats, and a lowered limit removes nobody.
  • The screen: the context carries seatLimit; Settings → Users shows "Seats: N of M" (over the limit too) and the refusal's text in the invite dialog. A user's lastError SEAT_LIMIT has its text. en-US and bg-BG.
  • No link to buy seats (second commit): a tenant's Owner may not be the one who can buy them, and the platform does not know who sells them, so there is no seats URL; the refusal asks the administrator instead.
  • .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:

  • Schema: SystemDB is created from the Liquibase changelog (core-liquibase), so the table comes with a changeset, and a later property needs an appended addColumn changeset; hbm2ddl=update only fills gaps.
  • Transaction: apply runs 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:format on the changed modules, then mvn -T 1C formatter:validate with the formatter cache wiped: BUILD SUCCESS.
  • Unit suites: core-tenants-users 73 tests (new: SEAT_LIMIT counting, 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-tenants 37 (new: TenantPropertiesServiceTest, TenantPropertiesTenantTeardownTest), core-liquibase (SystemChangelogIdempotencyTest), commons-config.
  • Integration tests after a quick-build install in the worktree: TenantUserSnapshotApiIT (new case: the limit stored, kept by an ignored body, refused for 0/2.5/"many", cleared, and the row gone after DELETE …/tenants/{id}), TenantUsersIT (new case: the context's seatLimit, an invitation at the limit 409 SEAT_LIMIT with 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.
  • The release-profile javadoc build of core-tenants and commons-config: BUILD SUCCESS, no warning on the new code.
  • Documentation: docs: a tenant's seat limit - Settings -> Users seats and refusal, seatLimit in the users push dirigible-io/dirigible-io.github.io#290 (the Seats section of Settings → Users, seatLimit in the users push).

Screenshots

pr8595-1-seats-line pr8595-2-invite-refused

Fixes #8593

🤖 Generated with Claude Code

… 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>
@iliyan-velichkov iliyan-velichkov self-assigned this Nov 13, 2026
@iliyan-velichkov
iliyan-velichkov merged commit 7eca937 into master Nov 13, 2026
26 of 28 checks passed
@iliyan-velichkov
iliyan-velichkov deleted the issue-8593-tenant-seat-limit branch November 13, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tenant users: a tenant's seat limit - the Users screen shows the seats and refuses an invitation beyond them (409 SEAT_LIMIT)

2 participants