Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* Tighten controls on channel metadata DB operations, move private channels to separate metadata DB [#3329](https://github.com/TryQuiet/quiet/issues/3329)
* Use chain permission checks to gate channel creation, deletion and membership [#3344](https://github.com/TryQuiet/quiet/issues/3344)
* Use randomly generated role names for private channels [#3354](https://github.com/TryQuiet/quiet/issues/3354)

## [8.0.0]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ class UserService extends ChainServiceBase {
return this.sigChain.team!.members(memberIds, options)
}

public getUserById(memberId: string, options: typeof DEFAULT_SEARCH_OPTIONS): Member
public getUserById(memberId: string, options?: MemberSearchOptions): Member
public getUserById(memberId: string, options: MemberSearchOptions = DEFAULT_SEARCH_OPTIONS): Member | undefined {
const users = this.getUsersById([memberId], options)
if (users.length === 0) {
Expand All @@ -93,6 +95,12 @@ class UserService extends ChainServiceBase {
return users[0]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This can return undefined as long as throwOnMissing is not true, so the type definition for the returned value is incorrect. Same with getMyUser.

}

public getMyUser(options: typeof DEFAULT_SEARCH_OPTIONS): Member
public getMyUser(options?: MemberSearchOptions): Member
public getMyUser(options: MemberSearchOptions = DEFAULT_SEARCH_OPTIONS): Member | undefined {
return this.getUserById(this.sigChain.user.userId, options)
}

public static redactUser(user: UserWithSecrets): User {
return SigChain.lfa.redactUser(user)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SigChain } from '../../sigchain'
import { createLogger } from '../../../common/logger'
import { RoleName } from './roles'
import { DEFAULT_CHANNEL_ROLE_NAME_LENGTH, RoleName } from './roles'
import { base58, hash, randomBytes } from '@localfirst/crypto'
import * as uint8arrays from 'uint8arrays'
import { generateProof, InviteResult, MemberContext, redactKeys, Team } from '@localfirst/auth'
Expand All @@ -19,7 +19,7 @@ describe('channels', () => {
let seed: string
let salt: string
let generatedKeys: InviteLockboxMetadata
const channelId = 'foobar'
let channelRoleName: string

it('should initialize a new sigchain and be admin', () => {
adminSigChain = SigChain.create()
Expand All @@ -37,13 +37,13 @@ describe('channels', () => {
expect(adminSigChain.channels.canIDeletePublicChannel()).toBe(true)
})
it('should create channel and admin should be added as member', () => {
const channel = adminSigChain.channels.create(channelId)
expect(channel).toBeDefined()
expect(channel).toBe(adminSigChain.channels.generateChannelRoleName(channelId))
expect(adminSigChain.channels.amIMemberOfChannel(channelId)).toBe(true)
expect(adminSigChain.channels.canIAddMembersToPrivateChannel(channelId)).toBe(true)
expect(adminSigChain.channels.canIRemoveMembersFromPrivateChannel(channelId)).toBe(true)
expect(adminSigChain.channels.canIDeletePrivateChannel(channelId)).toBe(true)
channelRoleName = adminSigChain.channels.create()
expect(channelRoleName).toBeDefined()
expect(channelRoleName).toHaveLength(DEFAULT_CHANNEL_ROLE_NAME_LENGTH)
expect(adminSigChain.channels.amIMemberOfChannel(channelRoleName)).toBe(true)
expect(adminSigChain.channels.canIAddMembersToPrivateChannel(channelRoleName)).toBe(true)
expect(adminSigChain.channels.canIRemoveMembersFromPrivateChannel(channelRoleName)).toBe(true)
expect(adminSigChain.channels.canIDeletePrivateChannel(channelRoleName)).toBe(true)
})
it('should create an invite', () => {
invite = adminSigChain.invites.createUserInvite()
Expand Down Expand Up @@ -105,30 +105,32 @@ describe('channels', () => {
expect(secondSigChain.roles.amIMemberOfRole(RoleName.MEMBER)).toBe(true)
})
it('should fail to self-assign channel role on second user', () => {
const failedSelfAssign = () =>
secondSigChain.roles.addSelf(secondSigChain.channels.generateChannelRoleName(channelId), seed, salt)
const failedSelfAssign = () => secondSigChain.roles.addSelf(channelRoleName, seed, salt)
expect(failedSelfAssign).toThrow()
})
it('should add second user to channel', () => {
adminSigChain.channels.addMember(secondSigChain.context.user.userId, channelId)
expect(adminSigChain.channels.memberInChannel(secondSigChain.context.user.userId, channelId)).toBe(true)
adminSigChain.channels.addMember(secondSigChain.context.user.userId, channelRoleName)
expect(adminSigChain.channels.memberInChannel(secondSigChain.context.user.userId, channelRoleName)).toBe(true)
expect(
adminSigChain.channels.canMemberAddMembersToPrivateChannel(secondSigChain.context.user.userId, channelId)
adminSigChain.channels.canMemberAddMembersToPrivateChannel(secondSigChain.context.user.userId, channelRoleName)
).toBe(false)
expect(
adminSigChain.channels.canMemberRemoveMembersFromPrivateChannel(secondSigChain.context.user.userId, channelId)
adminSigChain.channels.canMemberRemoveMembersFromPrivateChannel(
secondSigChain.context.user.userId,
channelRoleName
)
).toBe(false)
expect(
adminSigChain.channels.canMemberDeletePrivateChannel(secondSigChain.context.user.userId, channelRoleName)
).toBe(false)
expect(adminSigChain.channels.canMemberDeletePrivateChannel(secondSigChain.context.user.userId, channelId)).toBe(
false
)
})
it('should fail to create channel on second user', () => {
expect(secondSigChain.roles.amIAdmin()).toBe(false)
expect(secondSigChain.channels.canICreatePublicChannel()).toBe(false)
expect(secondSigChain.channels.canICreatePrivateChannel()).toBe(false)
expect(secondSigChain.channels.canIDeletePublicChannel()).toBe(false)

const failedToCreateChannel = () => secondSigChain.channels.create(randomUUID())
const failedToCreateChannel = () => secondSigChain.channels.create()
expect(failedToCreateChannel).toThrow()
})
})
81 changes: 36 additions & 45 deletions packages/backend/src/nest/auth/services/roles/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { SigChain } from '../../sigchain'
import { ChainServiceBase } from '../chainServiceBase'
import { Member } from '@localfirst/auth'
import { createLogger } from '../../../common/logger'
import { hash } from '@localfirst/crypto'
import { PUBLIC_CHANNEL_MODIFICATION_ROLES } from './roles'
import { hash, randomKey } from '@localfirst/crypto'
import { DEFAULT_CHANNEL_ROLE_NAME_LENGTH, PUBLIC_CHANNEL_MODIFICATION_ROLES } from './roles'

const logger = createLogger('auth:channelService')

Expand All @@ -16,52 +16,46 @@ class ChannelService extends ChainServiceBase {
super(sigChain)
}

public create(channelId: string): string {
const roleName = this.generateChannelRoleName(channelId)
public create(): string {
const roleName = this.generateChannelRoleName()
logger.info(`Adding new channel role with name ${roleName}`)
this.sigChain.roles.create(roleName)
return roleName
}

public createWithMembers(channelId: string, memberIdsForChannel: string[]): string {
const roleName = this.create(channelId)
public createWithMembers(memberIdsForChannel: string[]): string {
const roleName = this.create()
for (const memberId of memberIdsForChannel) {
this.addMember(memberId, channelId)
this.addMember(memberId, roleName)
}
return roleName
}

public addMember(memberId: string, channelId: string) {
logger.info(`Adding member with ID ${memberId} to channel ${channelId}`)
const roleName = this.generateChannelRoleName(channelId)
this.sigChain.roles!.addMember(memberId, roleName)
public addMember(memberId: string, channelRoleName: string) {
logger.info(`Adding member with ID ${memberId} to channel ${channelRoleName}`)
this.sigChain.roles!.addMember(memberId, channelRoleName)
}

public memberInChannel(memberId: string, channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.memberHasRole(memberId, roleName)
public memberInChannel(memberId: string, channelRoleName: string): boolean {
return this.sigChain.roles.memberHasRole(memberId, channelRoleName)
}

public amIMemberOfChannel(channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.amIMemberOfRole(roleName)
public amIMemberOfChannel(channelRoleName: string): boolean {
return this.sigChain.roles.amIMemberOfRole(channelRoleName)
}

public getMembersInChannel(channelId: string): Member[] {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.getMembersForRole(roleName)
public getMembersInChannel(channelRoleName: string): Member[] {
return this.sigChain.roles.getMembersForRole(channelRoleName)
}

public revokeMembership(memberId: string, channelId: string) {
logger.info(`Revoking membership of channel ${channelId} for member with ID ${memberId}`)
const roleName = this.generateChannelRoleName(channelId)
this.sigChain.roles.revokeMembership(memberId, roleName)
public revokeMembership(memberId: string, channelRoleName: string) {
logger.info(`Revoking membership of channel for member with ID ${memberId}`)
this.sigChain.roles.revokeMembership(memberId, channelRoleName)
}

public delete(channelId: string) {
logger.info(`Removing role for channel ${channelId}`)
const roleName = this.generateChannelRoleName(channelId)
this.sigChain.roles.delete(roleName)
public delete(channelRoleName: string) {
logger.info(`Removing role for channel`)
this.sigChain.roles.delete(channelRoleName)
}

public canMemberCreatePublicChannel(memberId: string): boolean {
Expand Down Expand Up @@ -98,35 +92,32 @@ class ChannelService extends ChainServiceBase {
return this.canMemberCreatePrivateChannel(this.sigChain.user.userId)
}

public canMemberAddMembersToPrivateChannel(memberId: string, channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.canMemberAddMembersRole(memberId, roleName)
public canMemberAddMembersToPrivateChannel(memberId: string, channelRoleName: string): boolean {
return this.sigChain.roles.canMemberAddMembersRole(memberId, channelRoleName)
}

public canIAddMembersToPrivateChannel(channelId: string): boolean {
return this.canMemberAddMembersToPrivateChannel(this.sigChain.user.userId, channelId)
public canIAddMembersToPrivateChannel(channelRoleName: string): boolean {
return this.canMemberAddMembersToPrivateChannel(this.sigChain.user.userId, channelRoleName)
}

public canMemberRemoveMembersFromPrivateChannel(memberId: string, channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.canMemberRemoveMembersFromRole(memberId, roleName)
public canMemberRemoveMembersFromPrivateChannel(memberId: string, channelRoleName: string): boolean {
return this.sigChain.roles.canMemberRemoveMembersFromRole(memberId, channelRoleName)
}

public canIRemoveMembersFromPrivateChannel(channelId: string): boolean {
return this.canMemberRemoveMembersFromPrivateChannel(this.sigChain.user.userId, channelId)
public canIRemoveMembersFromPrivateChannel(channelRoleName: string): boolean {
return this.canMemberRemoveMembersFromPrivateChannel(this.sigChain.user.userId, channelRoleName)
}

public canMemberDeletePrivateChannel(memberId: string, channelId: string): boolean {
const roleName = this.generateChannelRoleName(channelId)
return this.sigChain.roles.canMemberDeleteRole(memberId, roleName)
public canMemberDeletePrivateChannel(memberId: string, channelRoleName: string): boolean {
return this.sigChain.roles.canMemberDeleteRole(memberId, channelRoleName)
}

public canIDeletePrivateChannel(channelId: string): boolean {
return this.canMemberDeletePrivateChannel(this.sigChain.user.userId, channelId)
public canIDeletePrivateChannel(channelRoleName: string): boolean {
return this.canMemberDeletePrivateChannel(this.sigChain.user.userId, channelRoleName)
}

public generateChannelRoleName(channelId: string): string {
return hash(this.sigChain.team!.id, `private_channel_${channelId}`)
public generateChannelRoleName(roleNameLength: number = DEFAULT_CHANNEL_ROLE_NAME_LENGTH): string {
return randomKey(roleNameLength)
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/nest/auth/services/roles/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ export class NotAdminError extends Error {
super('User is not an admin on this community')
}
}

export const DEFAULT_CHANNEL_ROLE_NAME_LENGTH = 64
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import { QPSService } from '../qps/qps.service'
import { CaptchaService } from '../captcha/captcha.service'
import { SigChain } from '../auth/sigchain'
import { Member } from '@localfirst/auth'
import type { PrivateChannelMappings } from '../storage/channels/channels.types'

/**
* A monolith service that handles lots of events received from the state-manager.
Expand Down Expand Up @@ -975,26 +976,33 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
}

// handle chain updates
let channelMapping: Record<string, PublicChannel> = {}
const sigChain = this.sigChainService.getChain(teamId)
let channelMapping: PrivateChannelMappings = {
roleNameToChannel: {},
idToRoleName: {},
}
if (!this.storageService || !this.storageService.initialized || !this.storageService.channels.initialized) {
this.logger.warn(`StorageService hasn't been initialized, skipping channel mappings...`)
} else {
channelMapping = await this.storageService.channels.getPrivateChannelsByRolename()
}

const _handleUser = (member: Member, sigChain: SigChain): User => {
const privateChannelIds =
const privateChannelIds: string[] =
channelMapping != null
? member.roles.filter(roleName => roleName in channelMapping).map(roleName => channelMapping[roleName].id)
? member.roles
.filter(roleName => roleName in channelMapping.roleNameToChannel)
.map(roleName => channelMapping.roleNameToChannel[roleName].id)
: []
if (member.userId === sigChain.user.userId) {
const channelSpecificPermissions: PrivateChannelPermissions[] = []
for (const channelId of privateChannelIds) {
const roleName = channelMapping.idToRoleName[channelId]
channelSpecificPermissions.push({
channelId,
addMembers: sigChain.channels.canMemberAddMembersToPrivateChannel(member.userId, channelId),
removeMembers: sigChain.channels.canMemberRemoveMembersFromPrivateChannel(member.userId, channelId),
delete: sigChain.channels.canMemberDeletePrivateChannel(member.userId, channelId),
addMembers: sigChain.channels.canMemberAddMembersToPrivateChannel(member.userId, roleName),
removeMembers: sigChain.channels.canMemberRemoveMembersFromPrivateChannel(member.userId, roleName),
delete: sigChain.channels.canMemberDeletePrivateChannel(member.userId, roleName),
})
}
const payload: SetChannelPermissionsPayload = {
Expand Down Expand Up @@ -1025,7 +1033,6 @@ export class ConnectionsManagerService extends EventEmitter implements OnModuleI
*
* (Can we base these updates on the graph itself vs pulling directly from the Team object?)
*/
const sigChain = this.sigChainService.getChain(teamId)
const users = sigChain.team?.members().map((member): User => _handleUser(member, sigChain))
this.serverIoProvider.io.emit(SocketEvents.USERS_UPDATED, { users })
}
Expand Down
17 changes: 11 additions & 6 deletions packages/backend/src/nest/qss/qss.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
QSSEvents,
} from './qss.types'
import { createLogger } from '../common/logger'
import { Community, Identity, ChannelMessage, SocketActions, SocketEvents } from '@quiet/types'
import { Community, Identity, ChannelMessage, SocketActions, SocketEvents, PublicChannel } from '@quiet/types'
import { getReduxStoreFactory, getBaseTypesFactory, prepareStore, Store } from '@quiet/state-manager'
import { FactoryGirl } from 'factory-girl'
import { DateTime } from 'luxon'
Expand Down Expand Up @@ -1078,8 +1078,9 @@ describe('QSSService', () => {
AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }),
sync: true,
})
const channel = await baseFactory.create<PublicChannel>('PublicChannel')
const channelMessage = await baseFactory.create<ChannelMessage>('ChannelMessage')
const hash = await db.add(await publicMessagesService.onSend(channelMessage))
const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel))
const entry = await db.log.get(hash)
expect(hash).toBeDefined()
expect(entry).toBeDefined()
Expand Down Expand Up @@ -1323,8 +1324,9 @@ describe('QSSService', () => {
AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }),
sync: true,
})
const channel = await baseFactory.create<PublicChannel>('PublicChannel')
const channelMessage = await baseFactory.create<ChannelMessage>('ChannelMessage')
const hash = await db.add(await publicMessagesService.onSend(channelMessage))
const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel))
expect(hash).toBeDefined()
const entry = await db.log.get(hash)
expect(entry).toBeDefined()
Expand Down Expand Up @@ -1849,8 +1851,9 @@ describe('QSSService', () => {
AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }),
sync: true,
})
const channel = await baseFactory.create<PublicChannel>('PublicChannel')
const channelMessage = await baseFactory.create<ChannelMessage>('ChannelMessage')
const hash = await db.add(await publicMessagesService.onSend(channelMessage))
const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel))
const entry = await db.log.get(hash)
expect(hash).toBeDefined()
expect(entry).toBeDefined()
Expand Down Expand Up @@ -1879,8 +1882,9 @@ describe('QSSService', () => {
AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }),
sync: true,
})
const channel = await baseFactory.create<PublicChannel>('PublicChannel')
const channelMessage = await baseFactory.create<ChannelMessage>('ChannelMessage')
const hash = await db.add(await publicMessagesService.onSend(channelMessage))
const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel))
const entry = await db.log.get(hash)
expect(hash).toBeDefined()
expect(entry).toBeDefined()
Expand Down Expand Up @@ -1909,8 +1913,9 @@ describe('QSSService', () => {
AccessController: messagesAccessController.createAccessControllerFunc({ write: ['*'], sigchainService }),
sync: true,
})
const channel = await baseFactory.create<PublicChannel>('PublicChannel')
const channelMessage = await baseFactory.create<ChannelMessage>('ChannelMessage')
const hash = await db.add(await publicMessagesService.onSend(channelMessage))
const hash = await db.add(await publicMessagesService.onSend(channelMessage, channel))
const entry = await db.log.get(hash)
expect(hash).toBeDefined()
expect(entry).toBeDefined()
Expand Down
Loading
Loading