fix(channels): restrict private-channel invitations (#4612)

This change requires an active owner or administrator for third-party
additions to private channels. The relay validator and transactional
database authority enforce the same rule, including removed-member
reactivation and role-change paths.

Idempotent self-target behavior remains available, while ordinary
members can no longer extend private-channel access to another identity.

## Testing

- `git diff --check
origin/main...codex/security-private-channel-invite-authority`
- Rebased onto `origin/main` at `5c98932`
- Full CI pending

Originating Buzz thread:
`buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1`

---------

Signed-off-by: Jordan Mecom <jm@squareup.com>
Signed-off-by: Eli Foster <efoster@squareup.com>
Co-authored-by: Eli Foster <efoster@squareup.com>
This commit is contained in:
Jordan Mecom
2026-08-05 12:47:00 -05:00
committed by GitHub
parent ad538bfb1e
commit efe1893dd3
19 changed files with 858 additions and 190 deletions

View File

@@ -371,7 +371,9 @@ async fn acquire_channel_membership_lock(
/// Role enforcement:
/// - Open channels: `invited_by` is optional; role is forced to `Member` regardless of
/// what the caller passes — callers cannot self-assign elevated roles.
/// - Private channels: requires an `invited_by` who is an active owner/admin.
/// - Private channels: requires an `invited_by` who is an active owner/admin, the channel
/// creator bootstrapping their own first membership, or the target adding themselves
/// (idempotent re-add — an active member's *role* still cannot change this way).
/// - Elevated roles (`Owner`, `Admin`) may only be granted by an existing owner/admin,
/// even on open channels.
///
@@ -419,10 +421,14 @@ pub async fn add_member(
DbError::InvalidData(format!("invalid role in database: {inviter_role_str}"))
})?;
// Any member can invite others, but only owners/admins may grant elevated roles.
if role.is_elevated() && !inviter_role.is_elevated() {
// Only owners/admins may extend private-channel access to another
// identity. `inviter == pubkey` keeps a member's own idempotent
// re-add working; it is not a role-escalation hole, because the
// active-role-change guard below still rejects a self-targeted
// promotion from any non-elevated caller.
if !inviter_role.is_elevated() && inviter != pubkey {
return Err(DbError::AccessDenied(
"only owners/admins may grant elevated roles".to_string(),
"only owners/admins may add private-channel members".to_string(),
));
}
}