Skip to content

Implement a way to change channel permissions for each user.#1280

Open
2Pacalypse- wants to merge 3 commits intochannel-settings-dialog-2from
user-channel-permissions
Open

Implement a way to change channel permissions for each user.#1280
2Pacalypse- wants to merge 3 commits intochannel-settings-dialog-2from
user-channel-permissions

Conversation

@2Pacalypse-
Copy link
Copy Markdown
Member

No description provided.

@2Pacalypse-
Copy link
Copy Markdown
Member Author

Depends on #1279

@2Pacalypse- 2Pacalypse- force-pushed the user-channel-permissions branch from e846737 to 9d094cb Compare March 17, 2026 15:27
@2Pacalypse- 2Pacalypse- force-pushed the channel-settings-dialog-2 branch from a36e0a3 to d8c66ef Compare March 17, 2026 16:08
@2Pacalypse- 2Pacalypse- force-pushed the user-channel-permissions branch from 9d094cb to 2f06a50 Compare March 17, 2026 16:09
@2Pacalypse- 2Pacalypse- force-pushed the channel-settings-dialog-2 branch 2 times, most recently from a8c697a to de99ba0 Compare March 26, 2026 18:13
@ShieldBattery ShieldBattery deleted a comment from claude Bot Mar 26, 2026
@2Pacalypse- 2Pacalypse- force-pushed the user-channel-permissions branch from 37aa829 to 426c420 Compare March 26, 2026 18:14
@2Pacalypse- 2Pacalypse- force-pushed the channel-settings-dialog-2 branch 2 times, most recently from 8b2a6d0 to 1354049 Compare March 27, 2026 12:45
@2Pacalypse- 2Pacalypse- force-pushed the user-channel-permissions branch from 426c420 to ef06878 Compare March 27, 2026 13:00
Comment on lines +192 to 217
"permissions": {
"ban": "Can ban users",
"banShort": "Ban",
"changeTopic": "Can change topic",
"changeTopicShort": "Change topic",
"editPermissions": "Can edit permissions",
"editPermissionsShort": "Edit permissions",
"editTitle": "Edit permissions for {{name}}",
"joinedDate": "Joined {{date}}",
"kick": "Can kick users",
"kickShort": "Kick",
"loadError": "Failed to load users.",
"noSearchResults": "No users match your search",
"noUsers": "This channel has no other members",
"owner": "Owner",
"saveError": "Failed to save permissions",
"togglePrivate": "Can toggle private status",
"togglePrivateShort": "Toggle private"
},
"tabs": {
"general": "General"
"general": "General",
"permissions": "Permissions"
},
"users": {
"title": "Users"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CLAUDE.md violation: translation file manually edited

CLAUDE.md states:

Don't edit translation files (global.json) manually - run pnpm run gen-translations

Translation keys should be extracted from source code by the gen-translations command rather than added directly to global.json. Manual edits may be overwritten or cause conflicts on the next generation run.

Comment on lines +1041 to +1094
async listUserChannelEntries({
channelId,
userId,
isAdmin,
limit,
offset,
searchStr,
}: {
channelId: SbChannelId
userId: SbUserId
isAdmin: boolean
limit: number
offset: number
searchStr?: string
}): Promise<ListUserChannelEntriesResponse> {
const [channelInfo, userChannelEntry] = await Promise.all([
getChannelInfo(channelId),
getUserChannelEntryForUser(userId, channelId),
])

if (!channelInfo) {
throw new ChatServiceError(ChatServiceErrorCode.ChannelNotFound, 'Channel not found')
}
if (!userChannelEntry) {
throw new ChatServiceError(
ChatServiceErrorCode.NotInChannel,
'Must be in channel to view user channel entries',
)
}

const isUserChannelOwner = channelInfo.ownerId === userId
if (!isAdmin && !isUserChannelOwner && !userChannelEntry.channelPermissions.editPermissions) {
throw new ChatServiceError(
ChatServiceErrorCode.NotEnoughPermissions,
"You don't have enough permissions to view user channel entries",
)
}

const userChannelEntries = await getUserChannelEntriesForChannel({
channelId,
limit,
offset,
searchStr,
})
const userIds = userChannelEntries.map(u => u.userId)
const users = await findUsersById(userIds)

return {
channelId,
userChannelEntries: userChannelEntries.map(u => toUserChannelEntryJson(u)),
hasMoreUsers: userChannelEntries.length >= limit,
users,
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CLAUDE.md violation: class method doesn't use this

CLAUDE.md states:

If a class method doesn't use this, extract it as a module-level function instead.

listUserChannelEntries only calls module-level functions (getChannelInfo, getUserChannelEntryForUser, getUserChannelEntriesForChannel, findUsersById, toUserChannelEntryJson) and never references this. It should be extracted as a standalone exported function at module level, similar to the pattern used by other functions in this file.

Comment on lines 1152 to 1156
const isUserChannelOwner = channelInfo.ownerId === userId
if (!isUserChannelOwner && !userChannelEntry.channelPermissions.editPermissions) {
if (!isAdmin && !isUserChannelOwner && !userChannelEntry?.channelPermissions.editPermissions) {
throw new ChatServiceError(
ChatServiceErrorCode.NotEnoughPermissions,
"You don't have enough permissions to update other user's permissions",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Security: missing target restrictions allow privilege escalation

The authorization check here only validates that the caller has sufficient permissions, but does not restrict who can be targeted. This creates three distinct privilege escalation paths:

  1. Self-escalation: A user with editPermissions can pass their own userId as targetId and grant themselves ban, kick, togglePrivate, or changeTopic. The only guard is client-side (the UI hides this option), making the endpoint directly exploitable.

  2. Targeting the channel owner: A non-owner moderator with editPermissions can target channelInfo.ownerId as targetId and modify the owner's stored channelPermissions. Again, only the client prevents this.

  3. editPermissions cascade: A user with editPermissions (not the owner or server admin) can grant editPermissions to any other channel member, since there is no restriction on which specific permissions a delegated moderator can set. This allows unbounded privilege promotion chains.

Suggested fixes:

  • Add if (!isAdmin && !isUserChannelOwner && targetId === userId) throw new ChatServiceError(...) to prevent self-targeting.
  • Add a guard preventing non-owners/non-admins from targeting the channel owner.
  • Restrict which permissions a delegated moderator can grant — at minimum, prevent non-owners/non-admins from granting editPermissions to others.

@claude
Copy link
Copy Markdown

claude Bot commented Mar 27, 2026

Code review

Found 3 issues (2 CLAUDE.md violations, 1 security concern):

  • server/public/locales/en/global.json — Translation keys added manually; CLAUDE.md requires running pnpm run gen-translations instead.
  • server/lib/chat/chat-service.ts (listUserChannelEntries) — Class method never uses this and should be extracted as a module-level function per CLAUDE.md.
  • server/lib/chat/chat-service.ts (updateUserPermissions) — Authorization only checks the caller, not the target, enabling three privilege escalation paths: self-promotion, modifying the channel owner's permissions, and cascading editPermissions grants to arbitrary users.

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