[NEW][ENTERPRISE] Push Notification Data Privacy (#18254)

Co-authored-by: Rodrigo Nascimento <rodrigoknascimento@gmail.com>
Co-authored-by: Diego Sampaio <chinello@gmail.com>
pull/18316/head^2
pierre-lehnen-rc 6 years ago committed by GitHub
parent 13636d8bd9
commit c4f2dd8e67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      app/api/server/v1/push.js
  2. 27
      app/api/server/v1/settings.js
  3. 10
      app/lib/server/startup/settings.js
  4. 8
      app/models/server/models/Settings.js
  5. 78
      app/push-notifications/server/lib/PushNotification.js
  6. 20
      app/settings/client/lib/settings.ts
  7. 77
      app/settings/server/functions/settings.ts
  8. 3
      app/settings/server/index.ts
  9. 5
      ee/app/canned-responses/server/settings.js
  10. 30
      ee/app/ldap-enterprise/server/settings.js
  11. 1
      ee/app/license/server/bundles.ts
  12. 5
      ee/app/license/server/license.ts
  13. 38
      ee/app/livechat-enterprise/server/settings.js
  14. 1
      ee/app/settings/server/index.js
  15. 70
      ee/app/settings/server/settings.ts
  16. 1
      ee/server/index.js
  17. 1
      packages/rocketchat-i18n/i18n/en.i18n.json
  18. 12
      server/publications/settings/index.js

@ -1,8 +1,12 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { Match, check } from 'meteor/check';
import { appTokensCollection } from '../../../push/server';
import { API } from '../api';
import PushNotification from '../../../push-notifications/server/lib/PushNotification';
import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom';
import { Users, Messages, Rooms } from '../../../models/server';
API.v1.addRoute('push.token', { authRequired: true }, {
post() {
@ -63,3 +67,35 @@ API.v1.addRoute('push.token', { authRequired: true }, {
return API.v1.success();
},
});
API.v1.addRoute('push.get', { authRequired: true }, {
get() {
const params = this.requestParams();
check(params, Match.ObjectIncluding({
id: String,
}));
const receiver = Users.findOneById(this.userId);
if (!receiver) {
throw new Error('error-user-not-found');
}
const message = Messages.findOneById(params.id);
if (!message) {
throw new Error('error-message-not-found');
}
const room = Rooms.findOneById(message.rid);
if (!room) {
throw new Error('error-room-not-found');
}
if (!canAccessRoom(room, receiver)) {
throw new Error('error-not-allowed');
}
const data = PushNotification.getNotificationForMessageId({ receiver, room, message });
return API.v1.success({ data });
},
});

@ -6,6 +6,19 @@ import _ from 'underscore';
import { Settings } from '../../../models/server';
import { hasPermission } from '../../../authorization';
import { API } from '../api';
import { SettingsEvents } from '../../../settings/server';
const fetchSettings = (query, sort, offset, count, fields) => {
const settings = Settings.find(query, {
sort: sort || { _id: 1 },
skip: offset,
limit: count,
fields: Object.assign({ _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1 }, fields),
}).fetch();
SettingsEvents.emit('fetch-settings', settings);
return settings;
};
// settings endpoints
API.v1.addRoute('settings.public', { authRequired: false }, {
@ -20,12 +33,7 @@ API.v1.addRoute('settings.public', { authRequired: false }, {
ourQuery = Object.assign({}, query, ourQuery);
const settings = Settings.find(ourQuery, {
sort: sort || { _id: 1 },
skip: offset,
limit: count,
fields: Object.assign({ _id: 1, value: 1 }, fields),
}).fetch();
const settings = fetchSettings(ourQuery, sort, offset, count, fields);
return API.v1.success({
settings,
@ -94,12 +102,7 @@ API.v1.addRoute('settings', { authRequired: true }, {
ourQuery = Object.assign({}, query, ourQuery);
const settings = Settings.find(ourQuery, {
sort: sort || { _id: 1 },
skip: offset,
limit: count,
fields: Object.assign({ _id: 1, value: 1 }, fields),
}).fetch();
const settings = fetchSettings(ourQuery, sort, offset, count, fields);
return API.v1.success({
settings,

@ -1283,10 +1283,18 @@ settings.addGroup('Push', function() {
type: 'boolean',
public: true,
});
return this.add('Push_show_message', true, {
this.add('Push_show_message', true, {
type: 'boolean',
public: true,
});
this.add('Push_request_content_from_server', true, {
type: 'boolean',
enterprise: true,
invalidValue: false,
modules: [
'push-privacy',
],
});
});
});

@ -58,7 +58,7 @@ export class Settings extends Base {
filter._id = { $in: ids };
}
return this.find(filter, { fields: { _id: 1, value: 1, editor: 1 } });
return this.find(filter, { fields: { _id: 1, value: 1, editor: 1, enterprise: 1, invalidValue: 1, modules: 1 } });
}
findNotHiddenPublicUpdatedAfter(updatedAt) {
@ -70,7 +70,7 @@ export class Settings extends Base {
},
};
return this.find(filter, { fields: { _id: 1, value: 1, editor: 1 } });
return this.find(filter, { fields: { _id: 1, value: 1, editor: 1, enterprise: 1, invalidValue: 1, modules: 1 } });
}
findNotHiddenPrivate() {
@ -105,6 +105,10 @@ export class Settings extends Base {
return this.find({ wizard: { $exists: true, $ne: null } });
}
findEnterpriseSettings() {
return this.find({ enterprise: true });
}
// UPDATE
updateValueById(_id, value) {
const query = {

@ -3,7 +3,11 @@ import { Meteor } from 'meteor/meteor';
import { Push } from '../../../push/server';
import { settings } from '../../../settings/server';
import { metrics } from '../../../metrics/server';
import { Users } from '../../../models/server';
import { RocketChatAssets } from '../../../assets/server';
import { replaceMentionedUsernamesWithFullNames, parseMessageTextPerUser } from '../../../lib/server/functions/notifications';
import { callbacks } from '../../../callbacks/server';
import { getPushData } from '../../../lib/server/functions/notifications/mobile';
export class PushNotification {
getNotificationId(roomId) {
@ -11,18 +15,7 @@ export class PushNotification {
return this.hash(`${ serverId }|${ roomId }`); // hash
}
hash(str) {
let hash = 0;
let i = str.length;
while (i) {
hash = ((hash << 5) - hash) + str.charCodeAt(--i);
hash &= hash; // Convert to 32bit integer
}
return hash;
}
send({ rid, uid: userId, mid: messageId, roomName, username, message, payload, badge = 1, category }) {
getNotificationConfig({ rid, uid: userId, mid: messageId, roomName, username, message, payload, badge = 1, category, idOnly = false }) {
let title;
if (roomName && roomName !== '') {
title = `${ roomName }`;
@ -36,13 +29,14 @@ export class PushNotification {
badge,
sound: 'default',
priority: 10,
title,
text: message,
title: idOnly ? '' : title,
text: idOnly ? '' : message,
payload: {
host: Meteor.absoluteUrl(),
rid,
...idOnly || { rid },
messageId,
...payload,
notificationType: idOnly ? 'message-id-only' : 'message',
...idOnly || payload,
},
userId,
notId: this.getNotificationId(rid),
@ -58,9 +52,61 @@ export class PushNotification {
};
}
return config;
}
hash(str) {
let hash = 0;
let i = str.length;
while (i) {
hash = ((hash << 5) - hash) + str.charCodeAt(--i);
hash &= hash; // Convert to 32bit integer
}
return hash;
}
send({ rid, uid, mid, roomName, username, message, payload, badge = 1, category }) {
const idOnly = settings.get('Push_request_content_from_server');
const config = this.getNotificationConfig({ rid, uid, mid, roomName, username, message, payload, badge, category, idOnly });
metrics.notificationsSent.inc({ notification_type: 'mobile' });
return Push.send(config);
}
getNotificationForMessageId({ receiver, message, room }) {
const sender = Users.findOne(message.u._id, { fields: { username: 1, name: 1 } });
if (!sender) {
throw new Error('Message sender not found');
}
let notificationMessage = callbacks.run('beforeSendMessageNotifications', message.msg);
if (message.mentions?.length > 0 && settings.get('UI_Use_Real_Name')) {
notificationMessage = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions);
}
notificationMessage = parseMessageTextPerUser(notificationMessage, message, receiver);
const pushData = Promise.await(getPushData({
room,
message,
userId: receiver._id,
receiverUsername: receiver.username,
senderUsername: sender.username,
senderName: sender.name,
notificationMessage,
}));
return {
message,
notification: this.getNotificationConfig({
...pushData,
rid: message.rid,
uid: message.u._id,
mid: message._id,
idOnly: false,
}),
};
}
}
export default new PushNotification();

@ -15,20 +15,18 @@ class Settings extends SettingsBase {
return this.dict.get(_id);
}
private _storeSettingValue(record: { _id: string; value: SettingValue }, initialLoad: boolean): void {
Meteor.settings[record._id] = record.value;
this.dict.set(record._id, record.value);
this.load(record._id, record.value, initialLoad);
}
init(): void {
let initialLoad = true;
this.collection.find().observe({
added: (record: {_id: string; value: SettingValue}) => {
Meteor.settings[record._id] = record.value;
this.dict.set(record._id, record.value);
this.load(record._id, record.value, initialLoad);
},
changed: (record: {_id: string; value: SettingValue}) => {
Meteor.settings[record._id] = record.value;
this.dict.set(record._id, record.value);
this.load(record._id, record.value, initialLoad);
},
removed: (record: {_id: string}) => {
added: (record: { _id: string; value: SettingValue }) => this._storeSettingValue(record, initialLoad),
changed: (record: { _id: string; value: SettingValue }) => this._storeSettingValue(record, initialLoad),
removed: (record: { _id: string }) => {
delete Meteor.settings[record._id];
this.dict.set(record._id, null);
this.load(record._id, undefined, initialLoad);

@ -1,3 +1,5 @@
import { EventEmitter } from 'events';
import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
@ -15,6 +17,8 @@ if (process.env.SETTINGS_HIDDEN) {
process.env.SETTINGS_HIDDEN.split(',').forEach((settingId) => hiddenSettings.add(settingId.trim()));
}
export const SettingsEvents = new EventEmitter();
const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddOptions): SettingValue => {
const envValue = process.env[_id];
if (envValue) {
@ -79,12 +83,19 @@ export interface ISettingAddOptions {
multiline?: boolean;
values?: Array<ISettingSelectOption>;
public?: boolean;
enterprise?: boolean;
modules?: Array<string>;
invalidValue?: SettingValue;
}
export interface ISettingSelectOption {
key: string;
i18nLabel: string;
}
export interface ISettingRecord extends ISettingAddOptions {
_id: string;
env: boolean;
value: SettingValue;
}
export interface ISettingAddGroupOptions {
hidden?: boolean;
@ -94,6 +105,7 @@ export interface ISettingAddGroupOptions {
i18nDescription?: string;
}
interface IUpdateOperator {
$set: ISettingAddOptions;
$setOnInsert: ISettingAddOptions & {
@ -143,6 +155,13 @@ class Settings extends SettingsBase {
options.hidden = options.hidden || false;
options.blocked = options.blocked || false;
options.secret = options.secret || false;
options.enterprise = options.enterprise || false;
if (options.enterprise && !('invalidValue' in options)) {
console.error(`Enterprise setting ${ _id } is missing the invalidValue option`);
throw new Error(`Enterprise setting ${ _id } is missing the invalidValue option`);
}
if (options.group && options.sorter == null) {
options.sorter = this._sorter[options.group]++;
}
@ -329,33 +348,47 @@ class Settings extends SettingsBase {
return SettingsModel.updateValueById(_id, undefined);
}
/*
* Change a setting value on the Meteor.settings object
*/
storeSettingValue(record: ISettingRecord, initialLoad: boolean): void {
const newData = {
value: record.value,
};
SettingsEvents.emit('store-setting-value', record, newData);
const { value } = newData;
Meteor.settings[record._id] = value;
if (record.env === true) {
process.env[record._id] = String(value);
}
this.load(record._id, value, initialLoad);
}
/*
* Remove a setting value on the Meteor.settings object
*/
removeSettingValue(record: ISettingRecord, initialLoad: boolean): void {
SettingsEvents.emit('remove-setting-value', record);
delete Meteor.settings[record._id];
if (record.env === true) {
delete process.env[record._id];
}
this.load(record._id, undefined, initialLoad);
}
/*
* Update a setting by id
*/
init(): void {
this.initialLoad = true;
SettingsModel.find().observe({
added: (record: {_id: string; env: boolean; value: SettingValue}) => {
Meteor.settings[record._id] = record.value;
if (record.env === true) {
process.env[record._id] = String(record.value);
}
return this.load(record._id, record.value, this.initialLoad);
},
changed: (record: {_id: string; env: boolean; value: SettingValue}) => {
Meteor.settings[record._id] = record.value;
if (record.env === true) {
process.env[record._id] = String(record.value);
}
return this.load(record._id, record.value, this.initialLoad);
},
removed: (record: {_id: string; env: boolean}) => {
delete Meteor.settings[record._id];
if (record.env === true) {
delete process.env[record._id];
}
return this.load(record._id, undefined, this.initialLoad);
},
added: (record: ISettingRecord) => this.storeSettingValue(record, this.initialLoad),
changed: (record: ISettingRecord) => this.storeSettingValue(record, this.initialLoad),
removed: (record: ISettingRecord) => this.removeSettingValue(record, this.initialLoad),
});
this.initialLoad = false;
this.afterInitialLoad.forEach((fn) => fn(Meteor.settings));

@ -1,6 +1,7 @@
import { settings } from './functions/settings';
import { settings, SettingsEvents } from './functions/settings';
import './observer';
export {
settings,
SettingsEvents,
};

@ -6,6 +6,11 @@ export const createSettings = () => {
this.add('Canned_Responses_Enable', false, {
type: 'boolean',
public: true,
enterprise: true,
invalidValue: false,
modules: [
'canned-responses',
],
});
});
});

@ -7,23 +7,48 @@ export const createSettings = () => {
this.add('LDAP_Enable_LDAP_Roles_To_RC_Roles', false, {
type: 'boolean',
enableQuery: { _id: 'LDAP_Enable', value: true },
enterprise: true,
invalidValue: false,
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Roles_To_Rocket_Chat_Roles', '{}', {
type: 'code',
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true },
enterprise: true,
invalidValue: '{}',
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Validate_Roles_For_Each_Login', false, {
type: 'boolean',
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true },
enterprise: true,
invalidValue: false,
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Default_Role_To_User', 'user', {
type: 'select',
values: Roles.find({ scope: 'Users' }).fetch().map((role) => ({ key: role._id, i18nLabel: role._id })),
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true },
enterprise: true,
invalidValue: 'user',
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Query_To_Get_User_Groups', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', {
type: 'string',
enableQuery: { _id: 'LDAP_Enable_LDAP_Roles_To_RC_Roles', value: true },
enterprise: true,
invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))',
modules: [
'ldap-enterprise',
],
});
});
@ -37,6 +62,11 @@ export const createSettings = () => {
],
i18nDescription: 'LDAP_Sync_User_Active_State_Description',
enableQuery: { _id: 'LDAP_Enable', value: true },
enterprise: true,
invalidValue: 'none',
modules: [
'ldap-enterprise',
],
});
});
});

@ -9,6 +9,7 @@ const bundles: IBundle = {
'ldap-enterprise',
'livechat-enterprise',
'engagement-dashboard',
'push-privacy',
],
pro: [
],

@ -183,6 +183,7 @@ class LicenseClass {
return item;
});
EnterpriseLicenses.emit('validate');
this.showLicenses();
}
@ -271,6 +272,10 @@ export function onLicense(feature: string, cb: (...args: any[]) => void): void {
EnterpriseLicenses.once(`valid:${ feature }`, cb);
}
export function onValidateLicenses(cb: (...args: any[]) => void): void {
EnterpriseLicenses.on('validate', cb);
}
export interface IOverrideClassProperties {
[key: string]: (...args: any[]) => any;
}

@ -7,6 +7,8 @@ export const createSettings = () => {
group: 'Omnichannel',
section: 'Routing',
i18nLabel: 'Waiting_queue',
enterprise: true,
invalidValue: false,
});
settings.add('Livechat_waiting_queue_message', '', {
@ -16,6 +18,11 @@ export const createSettings = () => {
i18nLabel: 'Waiting_queue_message',
i18nDescription: 'Waiting_queue_message_description',
enableQuery: { _id: 'Livechat_waiting_queue', value: true },
enterprise: true,
invalidValue: '',
modules: [
'livechat-enterprise',
],
});
settings.add('Livechat_maximum_chats_per_agent', 0, {
@ -25,6 +32,11 @@ export const createSettings = () => {
i18nLabel: 'Max_number_of_chats_per_agent',
i18nDescription: 'Max_number_of_chats_per_agent_description',
enableQuery: { _id: 'Livechat_waiting_queue', value: true },
enterprise: true,
invalidValue: 0,
modules: [
'livechat-enterprise',
],
});
settings.add('Livechat_number_most_recent_chats_estimate_wait_time', 100, {
@ -34,6 +46,11 @@ export const createSettings = () => {
i18nLabel: 'Number_of_most_recent_chats_estimate_wait_time',
i18nDescription: 'Number_of_most_recent_chats_estimate_wait_time_description',
enableQuery: { _id: 'Livechat_waiting_queue', value: true },
enterprise: true,
invalidValue: 100,
modules: [
'livechat-enterprise',
],
});
settings.add('Livechat_auto_close_abandoned_rooms', false, {
@ -41,6 +58,11 @@ export const createSettings = () => {
group: 'Omnichannel',
section: 'Sessions',
i18nLabel: 'Enable_omnichannel_auto_close_abandoned_rooms',
enterprise: true,
invalidValue: false,
modules: [
'livechat-enterprise',
],
});
settings.add('Livechat_abandoned_rooms_closed_custom_message', '', {
@ -49,6 +71,11 @@ export const createSettings = () => {
section: 'Sessions',
i18nLabel: 'Livechat_abandoned_rooms_closed_custom_message',
enableQuery: { _id: 'Livechat_auto_close_abandoned_rooms', value: true },
enterprise: true,
invalidValue: '',
modules: [
'livechat-enterprise',
],
});
settings.add('Livechat_last_chatted_agent_routing', false, {
@ -56,6 +83,11 @@ export const createSettings = () => {
group: 'Omnichannel',
section: 'Routing',
enableQuery: { _id: 'Livechat_Routing_Method', value: { $ne: 'Manual_Selection' } },
enterprise: true,
invalidValue: false,
modules: [
'livechat-enterprise',
],
});
settings.addGroup('Omnichannel', function() {
@ -71,10 +103,14 @@ export const createSettings = () => {
}],
public: true,
i18nLabel: 'Livechat_business_hour_type',
enterprise: true,
invalidValue: 'Single',
modules: [
'livechat-enterprise',
],
});
});
});
Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Balancing', i18nLabel: 'Load_Balancing' });
};

@ -0,0 +1 @@
import './settings';

@ -0,0 +1,70 @@
import { Meteor } from 'meteor/meteor';
import { SettingsEvents, settings, ISettingRecord } from '../../../../app/settings/server/functions/settings';
import { SettingValue } from '../../../../app/settings/lib/settings';
import { isEnterprise, hasLicense, onValidateLicenses } from '../../license/server/license';
import SettingsModel from '../../../../app/models/server/models/Settings';
function changeSettingValue(record: ISettingRecord): undefined | { value: SettingValue } {
if (!record.enterprise) {
return;
}
if (!isEnterprise()) {
return { value: record.invalidValue };
}
if (!record.modules?.length) {
return;
}
for (const moduleName of record.modules) {
if (!hasLicense(moduleName)) {
return { value: record.invalidValue };
}
}
}
SettingsEvents.on('store-setting-value', (record: ISettingRecord, newRecord: { value: SettingValue }) => {
const changedValue = changeSettingValue(record);
if (changedValue) {
newRecord.value = changedValue.value;
}
});
SettingsEvents.on('fetch-settings', (settings: Array<ISettingRecord>): void => {
for (const setting of settings) {
const changedValue = changeSettingValue(setting);
if (changedValue) {
setting.value = changedValue.value;
}
}
});
type ISettingNotificationValue = {
_id: string;
value: SettingValue;
editor: string;
properties: string;
enterprise: boolean;
};
SettingsEvents.on('change-setting', (record: ISettingRecord, value: ISettingNotificationValue): void => {
const changedValue = changeSettingValue(record);
if (changedValue) {
value.value = changedValue.value;
}
});
function updateSettings(): void {
const enterpriseSettings = SettingsModel.findEnterpriseSettings();
enterpriseSettings.forEach((record: ISettingRecord) => settings.storeSettingValue(record, false));
}
Meteor.startup(() => {
updateSettings();
onValidateLicenses(updateSettings);
});

@ -6,3 +6,4 @@ import '../app/canned-responses/server/index';
import '../app/engagement-dashboard/server/index';
import '../app/ldap-enterprise/server/index';
import '../app/livechat-enterprise/server/index';
import '../app/settings/server/index';

@ -2870,6 +2870,7 @@
"Push_gcm_api_key": "GCM API Key",
"Push_gcm_project_number": "GCM Project Number",
"Push_production": "Production",
"Push_request_content_from_server": "Fetch full message content from the server on receipt",
"Push_show_message": "Show Message in Notification",
"Push_show_username_room": "Show Channel/Group/Username in Notification",
"Push_test_push": "Test",

@ -4,11 +4,14 @@ import { Settings } from '../../../app/models/server';
import { Notifications } from '../../../app/notifications/server';
import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/server';
import { getSettingPermissionId } from '../../../app/authorization/lib';
import { SettingsEvents } from '../../../app/settings/server/functions/settings';
Meteor.methods({
'public-settings/get'(updatedAt) {
if (updatedAt instanceof Date) {
const records = Settings.findNotHiddenPublicUpdatedAfter(updatedAt).fetch();
SettingsEvents.emit('fetch-settings', records);
return {
update: records,
remove: Settings.trashFindDeletedAfter(updatedAt, {
@ -24,7 +27,11 @@ Meteor.methods({
}).fetch(),
};
}
return Settings.findNotHiddenPublic().fetch();
const publicSettings = Settings.findNotHiddenPublic().fetch();
SettingsEvents.emit('fetch-settings', publicSettings);
return publicSettings;
},
'private-settings/get'(updatedAfter) {
const uid = Meteor.userId();
@ -84,8 +91,11 @@ Settings.on('change', ({ clientAction, id, data, diff }) => {
value: setting.value,
editor: setting.editor,
properties: setting.properties,
enterprise: setting.enterprise,
};
SettingsEvents.emit('change-setting', setting, value);
if (setting.public === true) {
Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value);
}

Loading…
Cancel
Save