The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/app/api/server/v1/roles.ts

332 lines
8.7 KiB

import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import { Users } from '../../../models/server';
import { API } from '../api';
import { getUsersInRole, hasRole } from '../../../authorization/server';
import { settings } from '../../../settings/server/index';
import { api } from '../../../../server/sdk/api';
import { Roles } from '../../../models/server/raw';
import { hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole';
import {
isRoleAddUserToRoleProps,
isRoleCreateProps,
isRoleDeleteProps,
isRoleRemoveUserFromRoleProps,
isRoleUpdateProps,
} from '../../../../definition/rest/v1/roles';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
API.v1.addRoute(
'roles.list',
{ authRequired: true },
{
async get() {
const roles = await Roles.find({}, { projection: { _updatedAt: 0 } }).toArray();
return API.v1.success({ roles });
},
},
);
API.v1.addRoute(
'roles.sync',
{ authRequired: true },
{
async get() {
check(
this.queryParams,
Match.ObjectIncluding({
updatedSince: Match.Where((value: unknown): value is string => typeof value === 'string' && !Number.isNaN(Date.parse(value))),
}),
);
const { updatedSince } = this.queryParams;
return API.v1.success({
roles: {
update: await Roles.findByUpdatedDate(new Date(updatedSince)).toArray(),
remove: await Roles.trashFindDeletedAfter(new Date(updatedSince)).toArray(),
},
});
},
},
);
API.v1.addRoute(
'roles.create',
{ authRequired: true },
{
async post() {
if (!isRoleCreateProps(this.bodyParams)) {
throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.');
}
const { name, scope, description, mandatory2fa } = this.bodyParams;
if (!(await hasPermissionAsync(Meteor.userId(), 'access-permissions'))) {
throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed');
}
if (await Roles.findOneByIdOrName(name)) {
throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists');
}
const roleId = (
await Roles.createWithRandomId(
name,
scope && ['Users', 'Subscriptions'].includes(scope) ? scope : 'Users',
description,
false,
mandatory2fa,
)
).insertedId;
if (settings.get('UI_DisplayRoles')) {
api.broadcast('user.roleUpdate', {
type: 'changed',
_id: roleId,
});
}
const role = await Roles.findOneByIdOrName(roleId);
if (!role) {
return API.v1.failure('error-role-not-found', 'Role not found');
}
return API.v1.success({
role,
});
},
},
);
API.v1.addRoute(
'roles.addUserToRole',
{ authRequired: true },
{
async post() {
if (!isRoleAddUserToRoleProps(this.bodyParams)) {
throw new Meteor.Error('error-invalid-role-properties', isRoleAddUserToRoleProps.errors?.map((error) => error.message).join('\n'));
}
const user = this.getUserFromParams();
const { roleName, roomId } = this.bodyParams;
if (hasRole(user._id, roleName, roomId)) {
throw new Meteor.Error('error-user-already-in-role', 'User already in role');
}
await Meteor.call('authorization:addUserToRole', roleName, user.username, roomId);
const role = await Roles.findOneByIdOrName(roleName);
if (!role) {
return API.v1.failure('error-role-not-found', 'Role not found');
}
return API.v1.success({
role,
});
},
},
);
API.v1.addRoute(
'roles.getUsersInRole',
{ authRequired: true },
{
async get() {
const { roomId, role } = this.queryParams;
const { offset, count = 50 } = this.getPaginationItems();
const projection = {
name: 1,
username: 1,
emails: 1,
avatarETag: 1,
};
if (!role) {
throw new Meteor.Error('error-param-not-provided', 'Query param "role" is required');
}
if (!(await hasPermissionAsync(this.userId, 'access-permissions'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}
if (roomId && !(await hasPermissionAsync(this.userId, 'view-other-user-channels'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}
const users = await getUsersInRole(role, roomId, {
limit: count as number,
sort: { username: 1 },
skip: offset as number,
projection,
});
return API.v1.success({ users: await users.toArray(), total: await users.count() });
},
},
);
API.v1.addRoute(
'roles.update',
{ authRequired: true },
{
async post() {
const { bodyParams } = this;
if (!isRoleUpdateProps(bodyParams)) {
throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.');
}
if (!(await hasPermissionAsync(this.userId, 'access-permissions'))) {
throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed');
}
const roleData = {
roleId: bodyParams.roleId,
name: bodyParams.name,
scope: bodyParams.scope || 'Users',
description: bodyParams.description,
mandatory2fa: bodyParams.mandatory2fa,
};
const role = await Roles.findOneByIdOrName(roleData.roleId);
if (!role) {
throw new Meteor.Error('error-invalid-roleId', 'This role does not exist');
}
if (role.protected && ((roleData.name && roleData.name !== role.name) || (roleData.scope && roleData.scope !== role.scope))) {
throw new Meteor.Error('error-role-protected', 'Role is protected');
}
if (roleData.name) {
const otherRole = await Roles.findOneByIdOrName(roleData.name);
if (otherRole && otherRole._id !== role._id) {
throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists');
}
}
if (['Users', 'Subscriptions'].includes(roleData.scope) === false) {
throw new Meteor.Error('error-invalid-scope', 'Invalid scope');
}
await Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa);
if (settings.get('UI_DisplayRoles')) {
api.broadcast('user.roleUpdate', {
type: 'changed',
_id: roleData.roleId,
});
}
const updatedRole = await Roles.findOneByIdOrName(roleData.roleId);
if (!updatedRole) {
return API.v1.failure();
}
return API.v1.success({
role: updatedRole,
});
},
},
);
API.v1.addRoute(
'roles.delete',
{ authRequired: true },
{
async post() {
const { bodyParams } = this;
if (!isRoleDeleteProps(bodyParams)) {
throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.');
}
if (!(await hasPermissionAsync(this.userId, 'access-permissions'))) {
throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed');
}
const role = await Roles.findOneByIdOrName(bodyParams.roleId);
if (!role) {
throw new Meteor.Error('error-invalid-roleId', 'This role does not exist');
}
if (role.protected) {
throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role');
}
const existingUsers = await Roles.findUsersInRole(role.name, role.scope);
if (existingUsers && (await existingUsers.count()) > 0) {
throw new Meteor.Error('error-role-in-use', "Cannot delete role because it's in use");
}
await Roles.removeById(role._id);
return API.v1.success();
},
},
);
API.v1.addRoute(
'roles.removeUserFromRole',
{ authRequired: true },
{
async post() {
const { bodyParams } = this;
if (!isRoleRemoveUserFromRoleProps(bodyParams)) {
throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.');
}
const { roleName, username, scope } = bodyParams;
if (!(await hasPermissionAsync(this.userId, 'access-permissions'))) {
throw new Meteor.Error('error-not-allowed', 'Accessing permissions is not allowed');
}
const user = Users.findOneByUsername(username);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'There is no user with this username');
}
const role = await Roles.findOneByIdOrName(roleName);
if (!role) {
throw new Meteor.Error('error-invalid-roleId', 'This role does not exist');
}
if (!(await hasAnyRoleAsync(user._id, [role.name], scope))) {
throw new Meteor.Error('error-user-not-in-role', 'User is not in this role');
}
if (role._id === 'admin') {
const adminCount = await (await Roles.findUsersInRole('admin')).count();
if (adminCount === 1) {
throw new Meteor.Error('error-admin-required', 'You need to have at least one admin');
}
}
await Roles.removeUserRoles(user._id, [role.name], scope);
if (settings.get('UI_DisplayRoles')) {
api.broadcast('user.roleUpdate', {
type: 'removed',
_id: role._id,
u: {
_id: user._id,
username: user.username,
},
scope,
});
}
return API.v1.success({
role,
});
},
},
);