Skip to content

fix(self-hosted): tighten tenant creation and config publication - #1309

Merged
feruzm merged 4 commits into
developfrom
bugfix/self-hosted-community-ownership
Aug 2, 2026
Merged

fix(self-hosted): tighten tenant creation and config publication#1309
feruzm merged 4 commits into
developfrom
bugfix/self-hosted-community-ownership

Conversation

@feruzm

@feruzm feruzm commented Aug 1, 2026

Copy link
Copy Markdown
Member

Two authorization gaps in the hosting API tenant lifecycle. Grouped because they touch the same routes and both concern which tenant state a caller can bring about.

Community instance creation

Creating a community tenant verified that the community existed, that its id matched the subdomain, and that a separate Hive account was named as owner. It never checked that the named owner had any standing in the community.

verifyCommunity is replaced by verifyCommunityControlledBy, which reads the community team from the same bridge.get_community call and requires the claimed owner to hold the owner or admin role. That matches who can change the community’s own settings on chain. Mods are excluded: they moderate content but do not control the community’s identity.

The check fails closed, so a node error refuses the claim rather than granting it. This matters because ownership is not recoverable after the fact: every later mutation authorizes against tenants.owner, and the create upsert only re-enters an abandoned row or a same-owner inactive one.

Note team carries [account, role, title] tuples, not objects, and the tests cover the malformed shapes.

Config publication

POST /v1/tenants deliberately does not write the served config file, with a comment explaining why: nginx serves any file that exists with no subscription check. PATCH /v1/tenants/:username broke that invariant by regenerating the file unconditionally, so a tenant that had never been activated could be served, and the periodic sweep that removes such files could be undone by saving again.

Publication is now gated on the same rule syncAllConfigs already applies (status active), extracted as isPublishableTenant so the two paths cannot drift.

The config is still persisted for any status, so an edit is never lost. The response reports whether it went live, which also gives the config editor something to say when a saved change is not yet visible.

Verification

  • 12 new tests covering roles, case handling, malformed team data, node failure, and each subscription status.
  • 132 hosting API tests pass, typecheck clean.
  • No client changes: apps/web’s signup already sends owner, and a community owner who administers the community is unaffected.

Summary by CodeRabbit

  • New Features

    • Community tenant validation now confirms the requesting account is an owner or administrator of the specified community.
    • Configuration files are published only for tenants with active subscriptions.
    • Tenant updates now indicate whether configuration was published, including a distinct response for inactive subscriptions.
  • Bug Fixes

    • Invalid, unauthorized, missing, or malformed community data is handled safely without granting access.

feruzm added 2 commits August 1, 2026 22:02
…ance

Creating a community tenant verified that the community existed, that its
id matched the subdomain, and that a separate Hive account was named as
owner. It never checked that the named owner had any standing in the
community, so the instance for any community could be claimed by an
account with no part in it. Ownership is not recoverable afterwards:
every later mutation authorises against tenants.owner, and the create
upsert only re-enters an abandoned row or a same-owner inactive one.

verifyCommunity is replaced by verifyCommunityControlledBy, which reads
the community's team from the same bridge.get_community call and requires
the claimed owner to hold the owner or admin role, matching who can
change the community's own settings on chain. Mods are excluded: they
moderate content but do not control the community's identity.

The check fails closed, so a node error refuses the claim rather than
granting it.
POST /v1/tenants deliberately does not write the served config file, with
a comment explaining why: nginx serves any file that exists with no
subscription check, so writing one for a tenant that has not paid puts a
blog live. PATCH /v1/tenants/:username broke that invariant by
regenerating the file unconditionally, so reserving a name and saving a
config once was enough to be served until the periodic sweep removed the
file, and re-saving restored it.

Publication is now gated on the same rule syncAllConfigs already applies
(status 'active'), shared as isPublishableTenant so the two cannot drift.
The config is still persisted for any status, so an edit is never lost;
the response reports whether it went live, which also gives the editor
something to say when a saved change is not visible yet.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f9ecdfba-971e-492b-bd23-a43020d9a31c

📥 Commits

Reviewing files that changed from the base of the PR and between e30c94d and 3aadfe4.

📒 Files selected for processing (5)
  • apps/self-hosted/hosting/api/src/payment-listener.ts
  • apps/self-hosted/hosting/api/src/routes/tenant-validation.test.ts
  • apps/self-hosted/hosting/api/src/routes/tenants.ts
  • apps/self-hosted/hosting/api/src/services/community-name.test.ts
  • apps/self-hosted/hosting/api/src/services/tenant-service.ts
📝 Walkthrough

Walkthrough

Tenant creation now verifies that the owner controls the community. Tenant configuration updates persist for inactive tenants without generating served config files. Publication is limited to active subscriptions and is reported in the PATCH response.

Changes

Tenant validation and publication

Layer / File(s) Summary
Community control verification
apps/self-hosted/hosting/api/src/services/tenant-service.ts, apps/self-hosted/hosting/api/src/routes/tenants.ts, apps/self-hosted/hosting/api/src/services/tenant-service-community-role.test.ts
Community validation accepts only case-insensitive owner or admin team roles. Missing, malformed, or failed RPC responses return false.
Active-subscription publication
apps/self-hosted/hosting/api/src/services/config-service.ts, apps/self-hosted/hosting/api/src/routes/tenants.ts, apps/self-hosted/hosting/api/src/services/config-publication.test.ts
Config publication uses the exported isPublishableTenant predicate. Only active subscriptions generate served config files. PATCH responses include publication status and subscription-specific messages.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TenantsRoute as tenants.ts route
  participant TenantService
  participant HiveRPC as Hive RPC
  TenantsRoute->>TenantService: verifyCommunityControlledBy(communityId, owner)
  TenantService->>HiveRPC: fetch community and team data
  HiveRPC-->>TenantService: return community and team
  TenantService-->>TenantsRoute: return authorization result
Loading

Possibly related PRs

Suggested labels: patch

Poem

A rabbit checks the community gate,
Where owners and admins validate.
Active tenants publish bright,
Inactive changes wait overnight.
Configs now hop to the proper state.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's two main changes: tighter tenant creation authorization and restricted config publication.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/self-hosted-community-ownership

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e30c94dd70

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
if (!(await TenantService.verifyCommunity(communityId))) {
return { ok: false, status: 400, error: 'Community not found' };
if (!(await TenantService.verifyCommunityControlledBy(communityId, owner))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive community validation from the target username

This ownership check is selected by the caller-controlled config.type, so an attacker can submit { "username": "hive-125125" } without declaring type: "community", pass the personal-blog branch with owner === username, and then fund blog:hive-125125; the payment listener activates that reservation while preserving the unmanageable community account as owner. The listener can also create the same owner=username row directly for a payment when no reservation exists (payment-listener.ts lines 341-351). Thus an arbitrary payer can still permanently reserve a community subdomain despite this check; hive-* community identifiers should require a prevalidated admin-owned reservation and must not be auto-created as personal tenants.

Useful? React with 👍 / 👎.

… body

The ownership check added in the previous commit was selected by
body.config.type, which the caller controls. Omitting it routed a request
for a hive-NNNN subdomain through the personal blog branch, which only
requires owner === username, so the community's subdomain could still be
captured with no ownership check. A community account is a real Hive
account, so the account existence check does not stop it, and funding the
reservation activates a tenant the community's team can never reclaim.

isCommunity is now derived from the username being claimed, with the
declared type kept only as an additional trigger. communityId defaults to
that username instead of being required in the body.

resolveAndValidateTenant is exported and covered directly: the two tests
for an undeclared and a blog-declared community name both fail against
the previous logic.
@feruzm

feruzm commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

Good find, and it defeated the whole point of the first commit. Fixed in 5c6de8c.

The check was selected by body.config.type, which the caller controls. Omitting it routed a request for a hive-NNNN subdomain through the personal-blog branch, which only requires owner === username, so the ownership check never ran. Confirmed the premise too: a community account is a real Hive account (condenser_api.get_accounts returns hive-125125), so the existence check does not stop it.

isCommunity is now derived from the username being claimed, with the declared type kept only as an additional trigger, and communityId defaults to that username rather than being required in the body.

resolveAndValidateTenant is now exported and covered directly. The two tests that matter, a community name claimed with no type and one claimed as type: "blog", both fail against the previous logic and pass with this change, so the regression is pinned rather than assumed.

140 hosting API tests pass, typecheck clean.

The ownership check added here covers POST /v1/tenants, but the payment
listener creates a tenant of its own when a payment arrives for a name
that has no row, with owner = username. A blog:hive-NNNN transfer
therefore reached that path and took the community's subdomain without
any community-control verification at all: the same squat, through the
other door.

A community account is a real Hive account, so the existence check that
follows does not stop it, and the row is not recoverable afterwards. The
create upsert only revives an abandoned row or a same-owner inactive one,
so the real team could never claim it back.

The listener now refuses to auto-create a community-shaped name and
requires it to have been reserved through the validated route first. The
name pattern moved to tenant-service so the route and the listener cannot
drift on what counts as a community.
@feruzm

feruzm commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Correct, and it is the same squat through the other door. Fixed in 3aadfe4.

I gated POST /v1/tenants and did not check the other path that can bring a tenant into existence. The listener creates a row itself when a payment arrives for a name with no reservation, with owner = username, so blog:hive-NNNN reached it with no community-control verification at all. parseMemo accepts that memo form, and a community account is a real Hive account, so the accountExistsStrict check immediately below does not stop it either.

Not recoverable afterwards, which is what makes it a squat rather than a nuisance: the create upsert only revives an abandoned row or a same-owner inactive one, so the real team can never take it back.

The listener now refuses to auto-create a community-shaped name and requires the reservation to have gone through the validated route first, rejected as permanent so the payment fails rather than retrying forever. The pattern moved to tenant-service so the route and the listener cannot drift on what counts as a community.

This is the second time this fix has been bypassed by a path I did not enumerate — first body.config.type, now the payment rail. The generalisation I should have applied after the first one, and have now: when a check protects a resource, enumerate every route that can create or mutate that resource, not just the one the bug report came in through. I checked the remaining writers while here: the internal activation rail resolves an existing tenant and does not auto-create, and TenantService.create is only reached through the validated route.

143 tests pass, typecheck clean, and I confirmed this merges cleanly with #1306, which also touches payment-listener.ts.

@feruzm
feruzm merged commit d26e1ac into develop Aug 2, 2026
8 checks passed
@feruzm
feruzm deleted the bugfix/self-hosted-community-ownership branch August 2, 2026 07:44
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.

1 participant