RocketChat/Rocket.Chat · error · Error

user-not-on-private-team

Error message

user-not-on-private-team

What it means

Thrown by TeamService.listRooms (apps/meteor/server/services/team/service.ts:528) when three conditions align: the team's type is PRIVATE, the caller lacks the allowPrivateTeam escape hatch, and TeamMember.findOneByUserIdAndTeamId finds no membership. allowPrivateTeam is computed by the REST layer from the 'view-all-teams' permission, so for ordinary users this is the private-team membership gate for listing channels.

Source

Thrown at apps/meteor/server/services/team/service.ts:528

		teamId: string,
		filter: IListRoomsFilter,
		{ offset: skip, count: limit }: IPaginationOptions = { offset: 0, count: 50 },
	): Promise<IRecordsWithTotal<IRoom>> {
		if (!teamId) {
			throw new Error('missing-teamId');
		}
		const team = await Team.findOneById<Pick<ITeam, '_id' | 'type'>>(teamId, {
			projection: { _id: 1, type: 1 },
		});
		if (!team) {
			throw new Error('invalid-team');
		}

		const { getAllRooms, allowPrivateTeam, name, isDefault } = filter;

		const isMember = await TeamMember.findOneByUserIdAndTeamId(uid, teamId);
		if (team.type === TeamType.PRIVATE && !allowPrivateTeam && !isMember) {
			throw new Error('user-not-on-private-team');
		}

		if (getAllRooms) {
			const { cursor, totalCount } = Rooms.findPaginatedByTeamIdContainingNameAndDefault(teamId, name, isDefault, undefined, {
				skip,
				limit,
			});
			const [records, total] = await Promise.all([cursor.toArray(), totalCount]);
			return {
				total,
				records,
			};
		}

		const user = await Users.findOneById<{ __rooms: string[] }>(uid, {
			projection: { __rooms: 1 },
		});
		const userRooms = user?.__rooms;

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Only call listRooms for private teams when the caller is a member (or holds 'view-all-teams')
  2. Grant 'view-all-teams' to roles that legitimately need to browse private teams (admins, auditors)
  3. Pre-check membership: await TeamMember.findOneByUserIdAndTeamId(uid, teamId) and skip/403 before calling

Example fix

// before
await Team.listRooms(uid, teamId, { allowPrivateTeam: false, ... }); // private team, non-member → throws

// after
const isMember = !!(await TeamMember.findOneByUserIdAndTeamId(uid, teamId));
const allowPrivateTeam = await Authorization.hasPermission(uid, 'view-all-teams', team.roomId);
if (!isMember && !allowPrivateTeam) {
	throw new Error('user-not-on-private-team'); // fail fast with context
}
await Team.listRooms(uid, teamId, { allowPrivateTeam, ... });
Defensive patterns

Strategy: validation

Validate before calling

const isMember = !!(await TeamMember.findOneByUserIdAndTeamId(uid, teamId));
const allowPrivateTeam = await Authorization.hasPermission(uid, 'view-all-teams', team.roomId);
if (!isMember && !allowPrivateTeam) {
	throw new Error('user-not-on-private-team');
}

Try / catch

try {
	await Team.listRooms(uid, teamId, filter);
} catch (e) {
	if (e instanceof Error && e.message === 'user-not-on-private-team') {
		// map to 403 with 'join the team or ask an admin' guidance
	}
	throw e;
}

Prevention

When it happens

Trigger: GET /api/v1/teams.listRooms?teamId=... for a private team by a non-member without 'view-all-teams'; direct service calls with allowPrivateTeam=false (its usual value) for a private team the uid does not belong to; users removed from the team while listing its rooms.

Common situations: Building directory/search UIs that enumerate teams without filtering by visibility; former members holding stale client state; admins assumed to see everything but lacking the view-all-teams permission.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/0ea355faa35a4707. Report an issue: GitHub.