Merge branch 'develop' into new/omnichannel-source-fields

pull/23090/head
Kevin Aleman 4 years ago
commit bdce4e0c1c
  1. 2
      app/2fa/client/TOTPLDAP.js
  2. 1
      app/api/server/index.js
  3. 33
      app/api/server/v1/ldap.ts
  4. 3
      app/importer-slack/server/importer.js
  5. 210
      app/importer/server/classes/ImportDataConverter.ts
  6. 2
      app/importer/server/classes/ImporterBase.js
  7. 149
      app/importer/server/classes/VirtualDataConverter.ts
  8. 11
      app/importer/server/definitions/IConversionCallbacks.ts
  9. 2
      app/importer/server/index.js
  10. 1
      app/ldap/client/index.js
  11. 34
      app/ldap/client/loginHelper.js
  12. 5
      app/ldap/server/index.js
  13. 520
      app/ldap/server/ldap.js
  14. 184
      app/ldap/server/loginHandler.js
  15. 134
      app/ldap/server/settings.js
  16. 629
      app/ldap/server/sync.js
  17. 31
      app/ldap/server/syncUsers.js
  18. 43
      app/ldap/server/testConnection.js
  19. 2
      app/models/server/index.js
  20. 6
      app/models/server/models/ImportData.ts
  21. 6
      app/models/server/models/Users.js
  22. 18
      app/models/server/raw/ImportData.ts
  23. 18
      app/models/server/raw/Users.js
  24. 3
      app/models/server/raw/index.ts
  25. 20
      app/settings/lib/settings.ts
  26. 12
      app/settings/server/functions/settings.mocks.ts
  27. 85
      app/settings/server/functions/settings.ts
  28. 10
      app/statistics/server/lib/getServicesStatistics.ts
  29. 13
      app/utils/server/lib/cron/Cronjobs.ts
  30. 11
      client/UIKit/hooks/useUIKitHandleAction.tsx
  31. 21
      client/contexts/EditableSettingsContext.ts
  32. 12
      client/contexts/ModalContext.ts
  33. 2
      client/contexts/ServerContext/endpoints.ts
  34. 14
      client/contexts/ServerContext/endpoints/v1/ldap.ts
  35. 3
      client/contexts/SettingsContext.ts
  36. 1
      client/importPackages.ts
  37. 129
      client/providers/EditableSettingsProvider.tsx
  38. 1
      client/startup/index.ts
  39. 20
      client/startup/ldap.ts
  40. 8
      client/views/account/AccountProfilePage.js
  41. 2
      client/views/admin/info/LicenseCard.js
  42. 11
      client/views/admin/settings/GroupPage.js
  43. 9
      client/views/admin/settings/GroupSelector.tsx
  44. 5
      client/views/admin/settings/MemoizedSetting.js
  45. 13
      client/views/admin/settings/Section.js
  46. 2
      client/views/admin/settings/Setting.js
  47. 103
      client/views/admin/settings/groups/LDAPGroupPage.tsx
  48. 66
      client/views/admin/settings/groups/TabbedGroupPage.tsx
  49. 12
      client/views/omnichannel/departments/EditDepartment.js
  50. 2
      client/views/omnichannel/webhooks/WebhooksPage.js
  51. 0
      definition/IImportChannel.ts
  52. 0
      definition/IImportMessage.ts
  53. 12
      definition/IImportRecord.ts
  54. 4
      definition/IImportUser.ts
  55. 4
      definition/IRoom.ts
  56. 3
      definition/ISetting.ts
  57. 12
      definition/IUser.ts
  58. 4
      definition/ldap-escape.d.ts
  59. 12
      definition/ldap/ILDAPCallback.ts
  60. 3
      definition/ldap/ILDAPEntry.ts
  61. 6
      definition/ldap/ILDAPLoginRequest.ts
  62. 5
      definition/ldap/ILDAPLoginResult.ts
  63. 27
      definition/ldap/ILDAPOptions.ts
  64. 4
      definition/ldap/ILDAPUniqueIdentifierField.ts
  65. 3
      definition/meteor-sha.d.ts
  66. 22
      ee/app/canned-responses/server/settings.js
  67. 22
      ee/app/ldap-enterprise/README.md
  68. 11
      ee/app/ldap-enterprise/server/hooks/beforeSearchAll.js
  69. 19
      ee/app/ldap-enterprise/server/hooks/syncExistentUser.js
  70. 69
      ee/app/ldap-enterprise/server/index.js
  71. 144
      ee/app/ldap-enterprise/server/ldapEnterprise.js
  72. 30
      ee/app/ldap-enterprise/server/listener.js
  73. 112
      ee/app/ldap-enterprise/server/settings.js
  74. 54
      ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts
  75. 5
      ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts
  76. 2
      ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx
  77. 5
      ee/definition/ldap/ILDAPEEOptions.ts
  78. 1
      ee/server/api/index.ts
  79. 31
      ee/server/api/ldap.ts
  80. 57
      ee/server/configuration/ldap.ts
  81. 3
      ee/server/index.js
  82. 65
      ee/server/lib/ldap/Connection.ts
  83. 501
      ee/server/lib/ldap/Manager.ts
  84. 16
      ee/server/local-services/ldap/service.ts
  85. 4
      ee/server/sdk/index.ts
  86. 3
      ee/server/sdk/types/ILDAPEEService.ts
  87. 154
      package-lock.json
  88. 6
      package.json
  89. 6
      packages/rocketchat-i18n/i18n/af.i18n.json
  90. 6
      packages/rocketchat-i18n/i18n/ar.i18n.json
  91. 6
      packages/rocketchat-i18n/i18n/az.i18n.json
  92. 6
      packages/rocketchat-i18n/i18n/be-BY.i18n.json
  93. 6
      packages/rocketchat-i18n/i18n/bg.i18n.json
  94. 6
      packages/rocketchat-i18n/i18n/bs.i18n.json
  95. 55
      packages/rocketchat-i18n/i18n/ca.i18n.json
  96. 53
      packages/rocketchat-i18n/i18n/cs.i18n.json
  97. 6
      packages/rocketchat-i18n/i18n/cy.i18n.json
  98. 55
      packages/rocketchat-i18n/i18n/da.i18n.json
  99. 6
      packages/rocketchat-i18n/i18n/de-AT.i18n.json
  100. 6
      packages/rocketchat-i18n/i18n/de-IN.i18n.json
  101. Some files were not shown because too many files have changed in this diff Show More

@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Utils2fa } from './lib/2fa';
import '../../ldap/client/loginHelper';
import '../../../client/startup/ldap';
Meteor.loginWithLDAPAndTOTP = function(...args) {
// Pull username and password

@ -24,6 +24,7 @@ import './v1/im';
import './v1/integrations';
import './v1/invites';
import './v1/import';
import './v1/ldap';
import './v1/misc';
import './v1/permissions';
import './v1/push';

@ -0,0 +1,33 @@
import { hasRole } from '../../../authorization/server';
import { settings } from '../../../settings/server';
import { API } from '../api';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { LDAP } from '../../../../server/sdk';
API.v1.addRoute('ldap.testConnection', { authRequired: true }, {
post() {
if (!this.userId) {
throw new Error('error-invalid-user');
}
if (!hasRole(this.userId, 'admin')) {
throw new Error('error-not-authorized');
}
if (settings.get('LDAP_Enable') !== true) {
throw new Error('LDAP_disabled');
}
try {
Promise.await(LDAP.testConnection());
} catch (error) {
SystemLogger.error(error);
throw new Error('Connection_failed');
}
return API.v1.success({
message: 'Connection_success',
});
},
});

@ -3,10 +3,9 @@ import _ from 'underscore';
import {
Base,
ProgressStep,
ImportData,
ImporterWebsocket,
} from '../../importer/server';
import { Messages } from '../../models';
import { Messages, ImportData } from '../../models/server';
import { settings } from '../../settings/server';
import { MentionsParser } from '../../mentions/lib/MentionsParser';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';

@ -2,18 +2,17 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import _ from 'underscore';
import { ImportData } from '../models/ImportData';
import { IImportUser } from '../definitions/IImportUser';
import { IImportMessage, IImportMessageReaction } from '../definitions/IImportMessage';
import { IImportChannel } from '../definitions/IImportChannel';
import { IImportUserRecord, IImportChannelRecord, IImportMessageRecord } from '../definitions/IImportRecord';
import { Users, Rooms, Subscriptions } from '../../../models/server';
import { ImportData as ImportDataRaw } from '../../../models/server/raw';
import { IImportUser } from '../../../../definition/IImportUser';
import { IImportMessage, IImportMessageReaction } from '../../../../definition/IImportMessage';
import { IImportChannel } from '../../../../definition/IImportChannel';
import { IConversionCallbacks } from '../definitions/IConversionCallbacks';
import { IImportUserRecord, IImportChannelRecord, IImportMessageRecord } from '../../../../definition/IImportRecord';
import { Users, Rooms, Subscriptions, ImportData } from '../../../models/server';
import { generateUsernameSuggestion, insertMessage } from '../../../lib/server';
import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus';
import { IUser } from '../../../../definition/IUser';
// @ts-ignore //@ToDo: Add the Logger class definitions.
type FakeLogger = Logger;
import type { Logger } from '../../../../server/lib/logger/Logger';
type IRoom = Record<string, any>;
type IMessage = Record<string, any>;
@ -38,14 +37,9 @@ type IMessageReaction = {
type IMessageReactions = Record<string, IMessageReaction>;
interface IConversionCallbacks {
beforeImportFn?: {
(data: IImportUser | IImportChannel | IImportMessage, type: string): boolean;
};
afterImportFn?: {
(data: IImportUser | IImportChannel | IImportMessage, type: string): void;
};
}
export type IConverterOptions = {
flagEmailsAsVerified?: boolean;
};
const guessNameFromUsername = (username: string): string =>
username
@ -64,16 +58,25 @@ export class ImportDataConverter {
private _roomNameCache: Map<string, string>;
private _logger: FakeLogger;
private _logger: Logger;
private _options: IConverterOptions;
public get options(): IConverterOptions {
return this._options;
}
constructor() {
constructor(options?: IConverterOptions) {
this._options = options || {
flagEmailsAsVerified: false,
};
this._userCache = new Map();
this._userDisplayNameCache = new Map();
this._roomCache = new Map();
this._roomNameCache = new Map();
}
setLogger(logger: FakeLogger): void {
setLogger(logger: Logger): void {
this._logger = logger;
}
@ -113,7 +116,7 @@ export class ImportDataConverter {
this.addUserToCache(userData.importIds[0], userData._id, userData.username);
}
addObject(type: string, data: Record<string, any>, options: Record<string, any> = {}): void {
protected addObject(type: string, data: Record<string, any>, options: Record<string, any> = {}): void {
ImportData.model.rawCollection().insert({
data,
dataType: type,
@ -135,17 +138,7 @@ export class ImportDataConverter {
});
}
updateUserId(_id: string, userData: IImportUser): void {
const updateData: Record<string, any> = {
$set: {
statusText: userData.statusText || undefined,
roles: userData.roles || ['user'],
type: userData.type || 'user',
bio: userData.bio || undefined,
name: userData.name || undefined,
},
};
addUserImportId(updateData: Record<string, any>, userData: IImportUser): void {
if (userData.importIds?.length) {
updateData.$addToSet = {
importIds: {
@ -153,7 +146,79 @@ export class ImportDataConverter {
},
};
}
}
addUserServices(updateData: Record<string, any>, userData: IImportUser): void {
if (!userData.services) {
return;
}
for (const serviceKey in userData.services) {
if (!userData.services[serviceKey]) {
continue;
}
const service = userData.services[serviceKey];
for (const key in service) {
if (!service[key]) {
continue;
}
updateData.$set[`services.${ serviceKey }.${ key }`] = service[key];
}
}
}
flagEmailsAsVerified(updateData: Record<string, any>, userData: IImportUser): void {
if (!this.options.flagEmailsAsVerified || !userData.emails.length) {
return;
}
updateData.$set['emails.$[].verified'] = true;
}
addCustomFields(updateData: Record<string, any>, userData: IImportUser): void {
if (!userData.customFields) {
return;
}
const subset = (source: Record<string, any>, currentPath: string): void => {
for (const key in source) {
if (!source.hasOwnProperty(key)) {
continue;
}
const keyPath = `${ currentPath }.${ key }`;
if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
subset(source[key], keyPath);
continue;
}
updateData.$set[keyPath] = source[key];
}
};
subset(userData.customFields, 'customFields');
}
updateUserId(_id: string, userData: IImportUser): void {
// #ToDo: #TODO: Move this to the model class
const updateData: Record<string, any> = {
$set: {
roles: userData.roles || ['user'],
type: userData.type || 'user',
...userData.statusText && { statusText: userData.statusText },
...userData.bio && { bio: userData.bio },
...userData.name && { name: userData.name },
...userData.services?.ldap && { ldap: true },
},
};
this.addCustomFields(updateData, userData);
this.addUserServices(updateData, userData);
this.addUserImportId(updateData, userData);
this.flagEmailsAsVerified(updateData, userData);
Users.update({ _id }, updateData);
}
@ -220,9 +285,28 @@ export class ImportDataConverter {
return user;
}
convertUsers({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const users = ImportData.find({ dataType: 'user' });
users.forEach(({ data, _id }: IImportUserRecord) => {
protected async getUsersToImport(): Promise<Array<IImportUserRecord>> {
return ImportDataRaw.getAllUsers().toArray();
}
findExistingUser(data: IImportUser): IUser | undefined {
if (data.emails.length) {
const emailUser = Users.findOneByEmailAddress(data.emails[0], {});
if (emailUser) {
return emailUser;
}
}
// If we couldn't find one by their email address, try to find an existing user by their username
if (data.username) {
return Users.findOneByUsernameIgnoringCase(data.username, {});
}
}
public convertUsers({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const users = Promise.await(this.getUsersToImport());
users.forEach(({ data, _id }) => {
try {
if (beforeImportFn && !beforeImportFn(data, 'user')) {
this.skipRecord(_id);
@ -236,23 +320,16 @@ export class ImportDataConverter {
throw new Error('importer-user-missing-email-and-username');
}
let existingUser;
if (data.emails.length) {
existingUser = Users.findOneByEmailAddress(data.emails[0], {});
}
if (data.username) {
// If we couldn't find one by their email address, try to find an existing user by their username
if (!existingUser) {
existingUser = Users.findOneByUsernameIgnoringCase(data.username, {});
}
} else {
let existingUser = this.findExistingUser(data);
if (!data.username) {
data.username = generateUsernameSuggestion({
name: data.name,
emails: data.emails,
});
}
const isNewUser = !existingUser;
if (existingUser) {
this.updateUser(existingUser, data);
} else {
@ -266,28 +343,21 @@ export class ImportDataConverter {
// Deleted users are 'inactive' users in Rocket.Chat
if (data.deleted && existingUser?.active) {
setUserActiveStatus(data._id, false, true);
} else if (data.deleted === false && existingUser?.active === false) {
setUserActiveStatus(data._id, true);
}
if (afterImportFn) {
afterImportFn(data, 'user');
afterImportFn(data, 'user', isNewUser);
}
} catch (e) {
this._logger.error(e);
this.saveError(_id, e);
}
});
}
saveNewId(importId: string, newId: string): void {
ImportData.update({
_id: importId,
}, {
$set: {
id: newId,
},
});
}
saveError(importId: string, error: Error): void {
protected saveError(importId: string, error: Error): void {
this._logger.error(error);
ImportData.update({
_id: importId,
@ -301,7 +371,7 @@ export class ImportDataConverter {
});
}
skipRecord(_id: string): void {
protected skipRecord(_id: string): void {
ImportData.update({
_id,
}, {
@ -424,9 +494,13 @@ export class ImportDataConverter {
return result;
}
protected async getMessagesToImport(): Promise<Array<IImportMessageRecord>> {
return ImportDataRaw.getAllMessages().toArray();
}
convertMessages({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const rids: Array<string> = [];
const messages = ImportData.find({ dataType: 'message' });
const messages = Promise.await(this.getMessagesToImport());
messages.forEach(({ data: m, _id }: IImportMessageRecord) => {
try {
if (beforeImportFn && !beforeImportFn(m, 'message')) {
@ -498,7 +572,7 @@ export class ImportDataConverter {
}
if (afterImportFn) {
afterImportFn(m, 'message');
afterImportFn(m, 'message', true);
}
} catch (e) {
this.saveError(_id, e);
@ -528,7 +602,7 @@ export class ImportDataConverter {
this.updateRoomId(room._id, roomData);
}
findDMForImportedUsers(...users: Array<string>): IImportChannel | undefined {
public findDMForImportedUsers(...users: Array<string>): IImportChannel | undefined {
const record = ImportData.findDMForImportedUsers(...users);
if (record) {
return record.data;
@ -709,7 +783,7 @@ export class ImportDataConverter {
roomData._id = roomInfo.rid;
});
} catch (e) {
this._logger.warn(roomData.name, members);
this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members });
this._logger.error(e);
throw e;
}
@ -768,8 +842,12 @@ export class ImportDataConverter {
return Rooms.findOneByNonValidatedName(data.name, {});
}
protected async getChannelsToImport(): Promise<Array<IImportChannelRecord>> {
return ImportDataRaw.getAllChannels().toArray();
}
convertChannels(startedByUserId: string, { beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const channels = ImportData.find({ dataType: 'channel' });
const channels = Promise.await(this.getChannelsToImport());
channels.forEach(({ data: c, _id }: IImportChannelRecord) => {
try {
if (beforeImportFn && !beforeImportFn(c, 'channel')) {
@ -801,7 +879,7 @@ export class ImportDataConverter {
}
if (afterImportFn) {
afterImportFn(c, 'channel');
afterImportFn(c, 'channel', !existingRoom);
}
} catch (e) {
this.saveError(_id, e);
@ -824,7 +902,7 @@ export class ImportDataConverter {
});
}
clearImportData(): void {
public clearImportData(): void {
// Using raw collection since its faster
Promise.await(ImportData.model.rawCollection().remove({}));
}

@ -14,7 +14,7 @@ import { RawImports } from '../models/RawImports';
import { Settings, Imports } from '../../../models';
import { Logger } from '../../../logger';
import { ImportDataConverter } from './ImportDataConverter';
import { ImportData } from '../models/ImportData';
import { ImportData } from '../../../models/server';
import { t } from '../../../utils/server';
import {
Selection,

@ -0,0 +1,149 @@
import { Random } from 'meteor/random';
import type { IImportUserRecord, IImportChannelRecord, IImportMessageRecord, IImportRecord, IImportRecordType, IImportData } from '../../../../definition/IImportRecord';
import { IImportChannel } from '../../../../definition/IImportChannel';
import { ImportDataConverter } from './ImportDataConverter';
import type { IConverterOptions } from './ImportDataConverter';
export class VirtualDataConverter extends ImportDataConverter {
protected _userRecords: Array<IImportUserRecord>;
protected _channelRecords: Array<IImportChannelRecord>;
protected _messageRecords: Array<IImportMessageRecord>;
protected useVirtual: boolean;
constructor(virtual = true, options?: IConverterOptions) {
super(options);
this.useVirtual = virtual;
if (virtual) {
this.clearVirtualData();
}
}
public clearImportData(): void {
if (!this.useVirtual) {
return super.clearImportData();
}
this.clearVirtualData();
}
public clearSuccessfullyImportedData(): void {
if (!this.useVirtual) {
return super.clearSuccessfullyImportedData();
}
this.clearVirtualData();
}
public findDMForImportedUsers(...users: Array<string>): IImportChannel | undefined {
if (!this.useVirtual) {
return super.findDMForImportedUsers(...users);
}
// The original method is only used by the hipchat importer so we probably don't need to implement this on the virtual converter.
return undefined;
}
protected addObject(type: IImportRecordType, data: IImportData, options: Record<string, any> = {}): void {
if (!this.useVirtual) {
return super.addObject(type, data, options);
}
const list = this.getObjectList(type);
list.push({
_id: Random.id(),
data,
dataType: type,
...options,
});
}
protected async getUsersToImport(): Promise<Array<IImportUserRecord>> {
if (!this.useVirtual) {
return super.getUsersToImport();
}
return this._userRecords;
}
protected saveError(importId: string, error: Error): void {
if (!this.useVirtual) {
return super.saveError(importId, error);
}
const record = this.getVirtualRecordById(importId);
if (!record) {
return;
}
if (!record.errors) {
record.errors = [];
}
record.errors.push({
message: error.message,
stack: error.stack,
});
}
protected skipRecord(_id: string): void {
if (!this.useVirtual) {
return super.skipRecord(_id);
}
const record = this.getVirtualRecordById(_id);
if (record) {
record.skipped = true;
}
}
protected async getMessagesToImport(): Promise<IImportMessageRecord[]> {
if (!this.useVirtual) {
return super.getMessagesToImport();
}
return this._messageRecords;
}
protected async getChannelsToImport(): Promise<IImportChannelRecord[]> {
if (!this.useVirtual) {
return super.getChannelsToImport();
}
return this._channelRecords;
}
private clearVirtualData(): void {
this._userRecords = [];
this._channelRecords = [];
this._messageRecords = [];
}
private getObjectList(type: IImportRecordType): Array<IImportRecord> {
switch (type) {
case 'user':
return this._userRecords;
case 'channel':
return this._channelRecords;
case 'message':
return this._messageRecords;
}
}
private getVirtualRecordById(id: string): IImportRecord | undefined {
for (const store of [this._userRecords, this._channelRecords, this._messageRecords]) {
for (const record of store) {
if (record._id === id) {
return record;
}
}
}
}
}

@ -0,0 +1,11 @@
import { IImportUser } from '../../../../definition/IImportUser';
import { IImportMessage } from '../../../../definition/IImportMessage';
import { IImportChannel } from '../../../../definition/IImportChannel';
export type ImporterBeforeImportCallback = {(data: IImportUser | IImportChannel | IImportMessage, type: string): boolean}
export type ImporterAfterImportCallback = {(data: IImportUser | IImportChannel | IImportMessage, type: string, isNewRecord: boolean): void};
export interface IConversionCallbacks {
beforeImportFn?: ImporterBeforeImportCallback;
afterImportFn?: ImporterAfterImportCallback;
}

@ -2,7 +2,6 @@ import { Base } from './classes/ImporterBase';
import { ImporterWebsocket } from './classes/ImporterWebsocket';
import { Progress } from './classes/ImporterProgress';
import { RawImports } from './models/RawImports';
import { ImportData } from './models/ImportData';
import { Selection } from './classes/ImporterSelection';
import { SelectionChannel } from './classes/ImporterSelectionChannel';
import { SelectionUser } from './classes/ImporterSelectionUser';
@ -26,7 +25,6 @@ export {
Progress,
ProgressStep,
RawImports,
ImportData,
Selection,
SelectionChannel,
SelectionUser,

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

@ -1,34 +0,0 @@
// Pass in username, password as normal
// customLdapOptions should be passed in if you want to override LDAP_DEFAULTS
// on any particular call (if you have multiple ldap servers you'd like to connect to)
// You'll likely want to set the dn value here {dn: "..."}
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
Meteor.loginWithLDAP = function(...args) {
// Pull username and password
const username = args.shift();
const password = args.shift();
// Check if last argument is a function
// if it is, pop it off and set callback to it
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null;
// if args still holds options item, grab it
const customLdapOptions = args.length > 0 ? args.shift() : {};
// Set up loginRequest object
const loginRequest = {
ldap: true,
username,
ldapPass: password,
ldapOptions: customLdapOptions,
};
Accounts.callLoginMethod({
// Call login method with ldap = true
// This will hook into our login handler for ldap
methodArguments: [loginRequest],
userCallback: callback,
});
};

@ -1,5 +0,0 @@
import './loginHandler';
import './settings';
import './testConnection';
import './syncUsers';
import './sync';

@ -1,520 +0,0 @@
import { Meteor } from 'meteor/meteor';
import ldapjs from 'ldapjs';
import Bunyan from 'bunyan';
import { callbacks } from '../../callbacks/server';
import { settings } from '../../settings';
import { Logger } from '../../logger';
const logger = new Logger('LDAP');
export const connLogger = logger.section('Connection');
export const bindLogger = logger.section('Bind');
export const searchLogger = logger.section('Search');
export const authLogger = logger.section('Auth');
export default class LDAP {
constructor() {
this.ldapjs = ldapjs;
this.connected = false;
this.options = {
host: settings.get('LDAP_Host'),
port: settings.get('LDAP_Port'),
Reconnect: settings.get('LDAP_Reconnect'),
Internal_Log_Level: settings.get('LDAP_Internal_Log_Level'),
timeout: settings.get('LDAP_Timeout'),
connect_timeout: settings.get('LDAP_Connect_Timeout'),
idle_timeout: settings.get('LDAP_Idle_Timeout'),
encryption: settings.get('LDAP_Encryption'),
ca_cert: settings.get('LDAP_CA_Cert'),
reject_unauthorized: settings.get('LDAP_Reject_Unauthorized') || false,
Authentication: settings.get('LDAP_Authentication'),
Authentication_UserDN: settings.get('LDAP_Authentication_UserDN'),
Authentication_Password: settings.get('LDAP_Authentication_Password'),
BaseDN: settings.get('LDAP_BaseDN'),
User_Search_Filter: settings.get('LDAP_User_Search_Filter'),
User_Search_Scope: settings.get('LDAP_User_Search_Scope'),
User_Search_Field: settings.get('LDAP_User_Search_Field'),
Search_Page_Size: settings.get('LDAP_Search_Page_Size'),
Search_Size_Limit: settings.get('LDAP_Search_Size_Limit'),
group_filter_enabled: settings.get('LDAP_Group_Filter_Enable'),
group_filter_object_class: settings.get('LDAP_Group_Filter_ObjectClass'),
group_filter_group_id_attribute: settings.get('LDAP_Group_Filter_Group_Id_Attribute'),
group_filter_group_member_attribute: settings.get('LDAP_Group_Filter_Group_Member_Attribute'),
group_filter_group_member_format: settings.get('LDAP_Group_Filter_Group_Member_Format'),
group_filter_group_name: settings.get('LDAP_Group_Filter_Group_Name'),
find_user_after_login: settings.get('LDAP_Find_User_After_Login'),
};
}
connectSync(...args) {
if (!this._connectSync) {
this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
}
return this._connectSync(...args);
}
searchAllSync(...args) {
if (!this._searchAllSync) {
this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
}
return this._searchAllSync(...args);
}
connectAsync(callback) {
connLogger.info('Init setup');
let replied = false;
const connectionOptions = {
url: `${ this.options.host }:${ this.options.port }`,
timeout: this.options.timeout,
connectTimeout: this.options.connect_timeout,
idleTimeout: this.options.idle_timeout,
reconnect: this.options.Reconnect,
};
if (this.options.Internal_Log_Level !== 'disabled') {
connectionOptions.log = new Bunyan({
name: 'ldapjs',
component: 'client',
stream: process.stderr,
level: this.options.Internal_Log_Level,
});
}
const tlsOptions = {
rejectUnauthorized: this.options.reject_unauthorized,
};
if (this.options.ca_cert && this.options.ca_cert !== '') {
// Split CA cert into array of strings
const chainLines = settings.get('LDAP_CA_Cert').split('\n');
let cert = [];
const ca = [];
chainLines.forEach((line) => {
cert.push(line);
if (line.match(/-END CERTIFICATE-/)) {
ca.push(cert.join('\n'));
cert = [];
}
});
tlsOptions.ca = ca;
}
if (this.options.encryption === 'ssl') {
connectionOptions.url = `ldaps://${ connectionOptions.url }`;
connectionOptions.tlsOptions = tlsOptions;
} else {
connectionOptions.url = `ldap://${ connectionOptions.url }`;
}
connLogger.info({ msg: 'Connecting', url: connectionOptions.url });
connLogger.debug({ msg: 'connectionOptions', connectionOptions });
this.client = ldapjs.createClient(connectionOptions);
this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
this.client.on('error', (error) => {
connLogger.error({ msg: 'connection', err: error });
if (replied === false) {
replied = true;
callback(error, null);
}
});
this.client.on('idle', () => {
searchLogger.info('Idle');
this.disconnect();
});
this.client.on('close', () => {
searchLogger.info('Closed');
});
if (this.options.encryption === 'tls') {
// Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
// https://github.com/RocketChat/Rocket.Chat/issues/2035
// https://github.com/mcavage/node-ldapjs/issues/349
tlsOptions.host = this.options.host;
connLogger.info('Starting TLS');
connLogger.debug({ tlsOptions });
this.client.starttls(tlsOptions, null, (error, response) => {
if (error) {
connLogger.error({ msg: 'TLS connection', err: error });
if (replied === false) {
replied = true;
callback(error, null);
}
return;
}
connLogger.info('TLS connected');
this.connected = true;
if (replied === false) {
replied = true;
callback(null, response);
}
});
} else {
this.client.on('connect', (response) => {
connLogger.info('LDAP connected');
this.connected = true;
if (replied === false) {
replied = true;
callback(null, response);
}
});
}
setTimeout(() => {
if (replied === false) {
connLogger.error({ msg: 'connection time out', connectTimeout: connectionOptions.connectTimeout });
replied = true;
callback(new Error('Timeout'));
}
}, connectionOptions.connectTimeout);
}
getUserFilter(username) {
const filter = [];
if (this.options.User_Search_Filter !== '') {
if (this.options.User_Search_Filter[0] === '(') {
filter.push(`${ this.options.User_Search_Filter }`);
} else {
filter.push(`(${ this.options.User_Search_Filter })`);
}
}
const usernameFilter = this.options.User_Search_Field.split(',').map((item) => `(${ item }=${ username })`);
if (usernameFilter.length === 0) {
logger.error('LDAP_LDAP_User_Search_Field not defined');
} else if (usernameFilter.length === 1) {
filter.push(`${ usernameFilter[0] }`);
} else {
filter.push(`(|${ usernameFilter.join('') })`);
}
return `(&${ filter.join('') })`;
}
bindIfNecessary() {
if (this.domainBinded === true) {
return;
}
if (this.options.Authentication !== true) {
return;
}
bindLogger.info({ msg: 'Binding UserDN', userDN: this.options.Authentication_UserDN });
this.bindSync(this.options.Authentication_UserDN, this.options.Authentication_Password);
this.domainBinded = true;
}
searchUsersSync(username, page) {
this.bindIfNecessary();
const searchOptions = {
filter: this.getUserFilter(username),
scope: this.options.User_Search_Scope || 'sub',
sizeLimit: this.options.Search_Size_Limit,
};
if (this.options.Search_Page_Size > 0) {
searchOptions.paged = {
pageSize: this.options.Search_Page_Size,
pagePause: !!page,
};
}
searchLogger.info({ msg: 'Searching user', username });
searchLogger.debug({ searchOptions, BaseDN: this.options.BaseDN });
if (page) {
return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
}
return this.searchAllSync(this.options.BaseDN, searchOptions);
}
getUserByIdSync(id, attribute) {
this.bindIfNecessary();
const Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field').split(',');
let filter;
if (attribute) {
filter = new this.ldapjs.filters.EqualityFilter({
attribute,
value: Buffer.from(id, 'hex'),
});
} else {
const filters = [];
Unique_Identifier_Field.forEach((item) => {
filters.push(new this.ldapjs.filters.EqualityFilter({
attribute: item,
value: Buffer.from(id, 'hex'),
}));
});
filter = new this.ldapjs.filters.OrFilter({ filters });
}
const searchOptions = {
filter,
scope: 'sub',
attributes: ['*', '+'],
};
searchLogger.info({ msg: 'Searching by id', id });
searchLogger.debug({ msg: 'search filter', filter: searchOptions.filter, BaseDN: this.options.BaseDN });
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
searchLogger.error(`Search by id ${ id } returned ${ result.length } records`);
}
return result[0];
}
getUserByUsernameSync(username) {
this.bindIfNecessary();
const searchOptions = {
filter: this.getUserFilter(username),
scope: this.options.User_Search_Scope || 'sub',
};
searchLogger.info({ msg: 'Searching user', username });
searchLogger.debug({ searchOptions, BaseDN: this.options.BaseDN });
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
searchLogger.error(`Search by username ${ username } returned ${ result.length } records`);
}
return result[0];
}
isUserInGroup(username, userdn) {
if (!this.options.group_filter_enabled) {
return true;
}
const filter = ['(&'];
if (this.options.group_filter_object_class !== '') {
filter.push(`(objectclass=${ this.options.group_filter_object_class })`);
}
if (this.options.group_filter_group_member_attribute !== '') {
filter.push(`(${ this.options.group_filter_group_member_attribute }=${ this.options.group_filter_group_member_format })`);
}
if (this.options.group_filter_group_id_attribute !== '') {
filter.push(`(${ this.options.group_filter_group_id_attribute }=${ this.options.group_filter_group_name })`);
}
filter.push(')');
const searchOptions = {
filter: filter.join('').replace(/#{username}/g, username).replace(/#{userdn}/g, userdn),
scope: 'sub',
};
searchLogger.debug({ msg: 'Group filter LDAP:', filter: searchOptions.filter });
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return false;
}
return true;
}
extractLdapEntryData(entry) {
const values = {
_raw: entry.raw,
};
Object.keys(values._raw).forEach((key) => {
const value = values._raw[key];
if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
if (value instanceof Buffer) {
values[key] = value.toString();
} else {
values[key] = value;
}
}
if (key === 'ou' && Array.isArray(value)) {
value.forEach((item, index) => {
if (item instanceof Buffer) {
value[index] = item.toString();
}
});
}
});
return values;
}
searchAllPaged(BaseDN, options, page) {
this.bindIfNecessary();
({ BaseDN, options } = callbacks.run('ldap.beforeSearchAll', { BaseDN, options }));
const processPage = ({ entries, title, end, next }) => {
searchLogger.info(title);
// Force LDAP idle to wait the record processing
this.client._updateIdle(true);
page(null, entries, { end,
next: () => {
// Reset idle timer
this.client._updateIdle();
next && next();
} });
};
this.client.search(BaseDN, options, (error, res) => {
if (error) {
searchLogger.error(error);
page(error);
return;
}
res.on('error', (error) => {
searchLogger.error(error);
page(error);
});
let entries = [];
const internalPageSize = options.paged && options.paged.pageSize > 0 ? options.paged.pageSize * 2 : 500;
res.on('searchEntry', (entry) => {
entries.push(this.extractLdapEntryData(entry));
if (entries.length >= internalPageSize) {
processPage({
entries,
title: 'Internal Page',
end: false,
});
entries = [];
}
});
res.on('page', (result, next) => {
if (!next) {
this.client._updateIdle(true);
processPage({
entries,
title: 'Final Page',
end: true,
});
entries = [];
} else if (entries.length) {
processPage({
entries,
title: 'Page',
end: false,
next,
});
entries = [];
}
});
res.on('end', () => {
if (entries.length) {
processPage({
entries,
title: 'Final Page',
end: true,
});
entries = [];
}
});
});
}
searchAllAsync(BaseDN, options, callback) {
this.bindIfNecessary();
({ BaseDN, options } = callbacks.run('ldap.beforeSearchAll', { BaseDN, options }));
this.client.search(BaseDN, options, (error, res) => {
if (error) {
searchLogger.error(error);
callback(error);
return;
}
res.on('error', (error) => {
searchLogger.error(error);
callback(error);
});
const entries = [];
res.on('searchEntry', (entry) => {
entries.push(this.extractLdapEntryData(entry));
});
res.on('end', () => {
searchLogger.info(`Search result count ${ entries.length }`);
callback(null, entries);
});
});
}
authSync(dn, password) {
authLogger.info({ msg: 'Authenticating', dn });
try {
this.bindSync(dn, password);
if (this.options.find_user_after_login) {
const searchOptions = {
scope: this.options.User_Search_Scope || 'sub',
};
const result = this.searchAllSync(dn, searchOptions);
if (result.length === 0) {
authLogger.info({ msg: 'Bind successful but user was not found via search', dn, searchOptions });
return false;
}
}
authLogger.info({ msg: 'Authenticated', dn });
return true;
} catch (error) {
authLogger.info({ msg: 'Not authenticated', dn });
authLogger.debug(error);
return false;
}
}
disconnect() {
this.connected = false;
this.domainBinded = false;
connLogger.info('Disconecting');
this.client.unbind();
}
}

@ -1,184 +0,0 @@
import { SHA256 } from 'meteor/sha';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import ldapEscape from 'ldap-escape';
import { slug, getLdapUsername, getLdapUserUniqueID, syncUserData, addLdapUser } from './sync';
import LDAP from './ldap';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { Logger } from '../../logger';
const logger = new Logger('LDAPHandler');
function fallbackDefaultAccountSystem(bind, username, password) {
if (typeof username === 'string') {
if (username.indexOf('@') === -1) {
username = { username };
} else {
username = { email: username };
}
}
logger.info('Fallback to default account system', username);
const loginRequest = {
user: username,
password: {
digest: SHA256(password),
algorithm: 'sha-256',
},
};
return Accounts._runLoginHandlers(bind, loginRequest);
}
Accounts.registerLoginHandler('ldap', function(loginRequest) {
if (!loginRequest.ldap || !loginRequest.ldapOptions) {
return undefined;
}
logger.info('Init LDAP login', loginRequest.username);
if (settings.get('LDAP_Enable') !== true) {
return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.ldapPass);
}
const self = this;
const ldap = new LDAP();
let ldapUser;
const escapedUsername = ldapEscape.filter`${ loginRequest.username }`;
try {
ldap.connectSync();
const users = ldap.searchUsersSync(escapedUsername);
if (users.length !== 1) {
logger.info('Search returned', users.length, 'record(s) for', escapedUsername);
throw new Error('User not Found');
}
if (ldap.authSync(users[0].dn, loginRequest.ldapPass) === true) {
if (ldap.isUserInGroup(escapedUsername, users[0].dn)) {
ldapUser = users[0];
} else {
throw new Error('User not in a valid group');
}
} else {
logger.info('Wrong password for', escapedUsername);
}
} catch (error) {
logger.error(error);
}
if (ldapUser === undefined) {
return fallbackDefaultAccountSystem(self, loginRequest.username, loginRequest.ldapPass);
}
// Look to see if user already exists
let userQuery;
const Unique_Identifier_Field = getLdapUserUniqueID(ldapUser);
let user;
if (Unique_Identifier_Field) {
userQuery = {
'services.ldap.id': Unique_Identifier_Field.value,
};
logger.info('Querying user');
logger.debug('userQuery', userQuery);
user = Meteor.users.findOne(userQuery);
}
let username;
if (settings.get('LDAP_Username_Field') !== '') {
username = slug(getLdapUsername(ldapUser));
} else {
username = slug(loginRequest.username);
}
if (!user) {
userQuery = {
username,
};
logger.debug('userQuery', userQuery);
user = Meteor.users.findOne(userQuery);
}
// Login user if they exist
if (user) {
if (user.ldap !== true && settings.get('LDAP_Merge_Existing_Users') !== true) {
logger.info('User exists without "ldap: true"');
throw new Meteor.Error('LDAP-login-error', `LDAP Authentication succeeded, but there's already an existing user with provided username [${ username }] in Mongo.`);
}
logger.info('Logging user');
syncUserData(user, ldapUser, ldap);
if (settings.get('LDAP_Login_Fallback') === true && typeof loginRequest.ldapPass === 'string' && loginRequest.ldapPass.trim() !== '') {
Accounts.setPassword(user._id, loginRequest.ldapPass, { logout: false });
}
logger.info('running afterLDAPLogin');
callbacks.run('afterLDAPLogin', { user, ldapUser, ldap });
return {
userId: user._id,
};
}
logger.info('User does not exist, creating', username);
if (settings.get('LDAP_Username_Field') === '') {
username = undefined;
}
if (settings.get('LDAP_Login_Fallback') !== true) {
loginRequest.ldapPass = undefined;
}
// Create new user
const result = addLdapUser(ldapUser, username, loginRequest.ldapPass, ldap);
if (result instanceof Error) {
throw result;
}
callbacks.run('afterLDAPLogin', { user: result, ldapUser, ldap });
return result;
});
let LDAP_Enable;
settings.get('LDAP_Enable', (key, value) => {
if (LDAP_Enable === value) {
return;
}
LDAP_Enable = value;
if (!value) {
return callbacks.remove('beforeValidateLogin', 'validateLdapLoginFallback');
}
callbacks.add('beforeValidateLogin', (login) => {
if (!login.allowed) {
return login;
}
// The fallback setting should only block password logins, so users that have other login services can continue using them
if (login.type !== 'password') {
return login;
}
if (login.user.services && login.user.services.ldap && login.user.services.ldap.id) {
login.allowed = !!settings.get('LDAP_Login_Fallback');
}
return login;
}, callbacks.priority.MEDIUM, 'validateLdapLoginFallback');
});

@ -1,134 +0,0 @@
import { settings } from '../../settings';
settings.addGroup('LDAP', function() {
const enableQuery = { _id: 'LDAP_Enable', value: true };
const enableAuthentication = [
enableQuery,
{ _id: 'LDAP_Authentication', value: true },
];
const enableTLSQuery = [
enableQuery,
{ _id: 'LDAP_Encryption', value: { $in: ['tls', 'ssl'] } },
];
const syncDataQuery = [
enableQuery,
{ _id: 'LDAP_Sync_User_Data', value: true },
];
const syncGroupsQuery = [
enableQuery,
{ _id: 'LDAP_Sync_User_Data_Groups', value: true },
];
const syncGroupsChannelsQuery = [
enableQuery,
{ _id: 'LDAP_Sync_User_Data_Groups', value: true },
{ _id: 'LDAP_Sync_User_Data_Groups_AutoChannels', value: true },
];
const groupFilterQuery = [
enableQuery,
{ _id: 'LDAP_Group_Filter_Enable', value: true },
];
const backgroundSyncQuery = [
enableQuery,
{ _id: 'LDAP_Background_Sync', value: true },
];
this.add('LDAP_Enable', false, { type: 'boolean', public: true });
this.add('LDAP_Login_Fallback', false, { type: 'boolean', enableQuery: null });
this.add('LDAP_Find_User_After_Login', true, { type: 'boolean', enableQuery });
this.add('LDAP_Host', '', { type: 'string', enableQuery });
this.add('LDAP_Port', '389', { type: 'int', enableQuery });
this.add('LDAP_Reconnect', false, { type: 'boolean', enableQuery });
this.add('LDAP_Encryption', 'plain', { type: 'select', values: [{ key: 'plain', i18nLabel: 'No_Encryption' }, { key: 'tls', i18nLabel: 'StartTLS' }, { key: 'ssl', i18nLabel: 'SSL/LDAPS' }], enableQuery });
this.add('LDAP_CA_Cert', '', { type: 'string', multiline: true, enableQuery: enableTLSQuery, secret: true });
this.add('LDAP_Reject_Unauthorized', true, { type: 'boolean', enableQuery: enableTLSQuery });
this.add('LDAP_BaseDN', '', { type: 'string', enableQuery });
this.add('LDAP_Internal_Log_Level', 'disabled', {
type: 'select',
values: [
{ key: 'disabled', i18nLabel: 'Disabled' },
{ key: 'error', i18nLabel: 'Error' },
{ key: 'warn', i18nLabel: 'Warn' },
{ key: 'info', i18nLabel: 'Info' },
{ key: 'debug', i18nLabel: 'Debug' },
{ key: 'trace', i18nLabel: 'Trace' },
],
enableQuery,
});
this.add('LDAP_Test_Connection', 'ldap_test_connection', { type: 'action', actionText: 'Test_Connection' });
this.section('Authentication', function() {
this.add('LDAP_Authentication', false, { type: 'boolean', enableQuery });
this.add('LDAP_Authentication_UserDN', '', { type: 'string', enableQuery: enableAuthentication, secret: true });
this.add('LDAP_Authentication_Password', '', { type: 'password', enableQuery: enableAuthentication, secret: true });
});
this.section('Timeouts', function() {
this.add('LDAP_Timeout', 60000, { type: 'int', enableQuery });
this.add('LDAP_Connect_Timeout', 1000, { type: 'int', enableQuery });
this.add('LDAP_Idle_Timeout', 1000, { type: 'int', enableQuery });
});
this.section('User Search', function() {
this.add('LDAP_User_Search_Filter', '(objectclass=*)', { type: 'string', enableQuery });
this.add('LDAP_User_Search_Scope', 'sub', { type: 'string', enableQuery });
this.add('LDAP_User_Search_Field', 'sAMAccountName', { type: 'string', enableQuery });
this.add('LDAP_Search_Page_Size', 250, { type: 'int', enableQuery });
this.add('LDAP_Search_Size_Limit', 1000, { type: 'int', enableQuery });
});
this.section('User Search (Group Validation)', function() {
this.add('LDAP_Group_Filter_Enable', false, { type: 'boolean', enableQuery });
this.add('LDAP_Group_Filter_ObjectClass', 'groupOfUniqueNames', { type: 'string', enableQuery: groupFilterQuery });
this.add('LDAP_Group_Filter_Group_Id_Attribute', 'cn', { type: 'string', enableQuery: groupFilterQuery });
this.add('LDAP_Group_Filter_Group_Member_Attribute', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery });
this.add('LDAP_Group_Filter_Group_Member_Format', 'uniqueMember', { type: 'string', enableQuery: groupFilterQuery });
this.add('LDAP_Group_Filter_Group_Name', 'ROCKET_CHAT', { type: 'string', enableQuery: groupFilterQuery });
});
this.section('Sync / Import', function() {
this.add('LDAP_Username_Field', 'sAMAccountName', {
type: 'string',
enableQuery,
// public so that it's visible to AccountProfilePage:
public: true,
});
this.add('LDAP_Unique_Identifier_Field', 'objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber', { type: 'string', enableQuery });
this.add('LDAP_Default_Domain', '', { type: 'string', enableQuery });
this.add('LDAP_Merge_Existing_Users', false, { type: 'boolean', enableQuery });
this.add('LDAP_Sync_User_Data', false, { type: 'boolean', enableQuery });
this.add('LDAP_Sync_User_Data_FieldMap', '{"cn":"name", "mail":"email"}', { type: 'string', enableQuery: syncDataQuery });
this.add('LDAP_Sync_User_Data_Groups', false, { type: 'boolean', enableQuery });
this.add('LDAP_Sync_User_Data_Groups_AutoRemove', false, { type: 'boolean', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_Groups_Filter', '(&(cn=#{groupName})(memberUid=#{username}))', { type: 'string', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_Groups_BaseDN', '', { type: 'string', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_GroupsMap', '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}', {
type: 'code',
multiline: true,
public: false,
code: 'application/json',
enableQuery: syncGroupsQuery,
});
this.add('LDAP_Sync_User_Data_Groups_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsQuery });
this.add('LDAP_Sync_User_Data_Groups_AutoChannels_Admin', 'rocket.cat', { type: 'string', enableQuery: syncGroupsChannelsQuery });
this.add('LDAP_Sync_User_Data_Groups_AutoChannelsMap', '{\n\t"employee": "general",\n\t"techsupport": [\n\t\t"helpdesk",\n\t\t"support"\n\t]\n}', {
type: 'code',
multiline: true,
public: false,
code: 'application/json',
enableQuery: syncGroupsChannelsQuery,
});
this.add('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels', false, { type: 'boolean', enableQuery: syncGroupsChannelsQuery });
this.add('LDAP_Sync_User_Avatar', true, { type: 'boolean', enableQuery });
this.add('LDAP_Avatar_Field', '', { type: 'string', enableQuery });
this.add('LDAP_Background_Sync', false, { type: 'boolean', enableQuery });
this.add('LDAP_Background_Sync_Interval', 'Every 24 hours', { type: 'string', enableQuery: backgroundSyncQuery });
this.add('LDAP_Background_Sync_Import_New_Users', true, { type: 'boolean', enableQuery: backgroundSyncQuery });
this.add('LDAP_Background_Sync_Keep_Existant_Users_Updated', true, { type: 'boolean', enableQuery: backgroundSyncQuery });
this.add('LDAP_Sync_Now', 'ldap_sync_now', { type: 'action', actionText: 'Execute_Synchronization_Now' });
});
});

@ -1,629 +0,0 @@
import limax from 'limax';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { SyncedCron } from 'meteor/littledata:synced-cron';
import _ from 'underscore';
import LDAP from './ldap';
import { callbacks } from '../../callbacks/server';
import { RocketChatFile } from '../../file';
import { settings } from '../../settings';
import { Users, Roles, Rooms, Subscriptions } from '../../models';
import { Logger } from '../../logger';
import { _setRealName } from '../../lib';
import { templateVarHandler } from '../../utils';
import { FileUpload } from '../../file-upload';
import { addUserToRoom, removeUserFromRoom, createRoom, saveUserIdentity } from '../../lib/server/functions';
import { api } from '../../../server/sdk/api';
export const logger = new Logger('LDAPSync');
export function isUserInLDAPGroup(ldap, ldapUser, user, ldapGroup) {
const syncUserRolesFilter = settings.get('LDAP_Sync_User_Data_Groups_Filter').trim();
const syncUserRolesBaseDN = settings.get('LDAP_Sync_User_Data_Groups_BaseDN').trim();
if (!syncUserRolesFilter || !syncUserRolesBaseDN) {
logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.');
return false;
}
const searchOptions = {
filter: syncUserRolesFilter.replace(/#{username}/g, user.username).replace(/#{groupName}/g, ldapGroup).replace(/#{userdn}/g, ldapUser.dn),
scope: 'sub',
};
const result = ldap.searchAllSync(syncUserRolesBaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
logger.debug(`${ user.username } is not in ${ ldapGroup } group!!!`);
} else {
logger.debug(`${ user.username } is in ${ ldapGroup } group.`);
return true;
}
return false;
}
export function slug(text) {
if (settings.get('UTF8_Names_Slugify') !== true) {
return text;
}
text = limax(text, { replacement: '.' });
return text.replace(/[^0-9a-z-_.]/g, '');
}
export function getPropertyValue(obj, key) {
try {
return _.reduce(key.split('.'), (acc, el) => acc[el], obj);
} catch (err) {
return undefined;
}
}
export function getLdapUsername(ldapUser) {
const usernameField = settings.get('LDAP_Username_Field');
if (usernameField.indexOf('#{') > -1) {
return usernameField.replace(/#{(.+?)}/g, function(match, field) {
return ldapUser[field];
});
}
return ldapUser[usernameField];
}
export function getLdapUserUniqueID(ldapUser) {
let Unique_Identifier_Field = settings.get('LDAP_Unique_Identifier_Field');
if (Unique_Identifier_Field !== '') {
Unique_Identifier_Field = Unique_Identifier_Field.replace(/\s/g, '').split(',');
} else {
Unique_Identifier_Field = [];
}
let User_Search_Field = settings.get('LDAP_User_Search_Field');
if (User_Search_Field !== '') {
User_Search_Field = User_Search_Field.replace(/\s/g, '').split(',');
} else {
User_Search_Field = [];
}
Unique_Identifier_Field = Unique_Identifier_Field.concat(User_Search_Field);
if (Unique_Identifier_Field.length > 0) {
Unique_Identifier_Field = Unique_Identifier_Field.find((field) => !_.isEmpty(ldapUser._raw[field]));
if (Unique_Identifier_Field) {
Unique_Identifier_Field = {
attribute: Unique_Identifier_Field,
value: ldapUser._raw[Unique_Identifier_Field].toString('hex'),
};
}
return Unique_Identifier_Field;
}
}
export function getDataToSyncUserData(ldapUser, user) {
const syncUserData = settings.get('LDAP_Sync_User_Data');
const syncUserDataFieldMap = settings.get('LDAP_Sync_User_Data_FieldMap').trim();
const userData = {};
if (syncUserData && syncUserDataFieldMap) {
const whitelistedUserFields = ['email', 'name', 'customFields'];
const fieldMap = JSON.parse(syncUserDataFieldMap);
const emailList = [];
_.map(fieldMap, function(userField, ldapField) {
switch (userField) {
case 'email':
if (!ldapUser.hasOwnProperty(ldapField)) {
logger.debug(`user does not have attribute: ${ ldapField }`);
return;
}
const verified = settings.get('Accounts_Verify_Email_For_External_Accounts');
if (_.isObject(ldapUser[ldapField])) {
_.map(ldapUser[ldapField], function(item) {
emailList.push({ address: item, verified });
});
} else {
emailList.push({ address: ldapUser[ldapField], verified });
}
break;
default:
const [outerKey, innerKeys] = userField.split(/\.(.+)/);
if (!_.find(whitelistedUserFields, (el) => el === outerKey)) {
logger.debug(`user attribute not whitelisted: ${ userField }`);
return;
}
if (outerKey === 'customFields') {
let customFieldsMeta;
try {
customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields'));
} catch (e) {
logger.debug('Invalid JSON for Custom Fields');
return;
}
if (!getPropertyValue(customFieldsMeta, innerKeys)) {
logger.debug(`user attribute does not exist: ${ userField }`);
return;
}
}
const tmpUserField = getPropertyValue(user, userField);
const tmpLdapField = templateVarHandler(ldapField, ldapUser);
if (tmpLdapField && tmpUserField !== tmpLdapField) {
// creates the object structure instead of just assigning 'tmpLdapField' to
// 'userData[userField]' in order to avoid the "cannot use the part (...)
// to traverse the element" (MongoDB) error that can happen. Do not handle
// arrays.
// TODO: Find a better solution.
const dKeys = userField.split('.');
const lastKey = _.last(dKeys);
_.reduce(dKeys, (obj, currKey) => {
if (currKey === lastKey) {
obj[currKey] = tmpLdapField;
} else {
obj[currKey] = obj[currKey] || {};
}
return obj[currKey];
}, userData);
logger.debug(`user.${ userField } changed to: ${ tmpLdapField }`);
}
}
});
if (emailList.length > 0) {
if (JSON.stringify(user.emails) !== JSON.stringify(emailList)) {
userData.emails = emailList;
}
}
}
const uniqueId = getLdapUserUniqueID(ldapUser);
if (uniqueId && (!user.services || !user.services.ldap || user.services.ldap.id !== uniqueId.value || user.services.ldap.idAttribute !== uniqueId.attribute)) {
userData['services.ldap.id'] = uniqueId.value;
userData['services.ldap.idAttribute'] = uniqueId.attribute;
}
if (user.ldap !== true) {
userData.ldap = true;
}
if (_.size(userData)) {
return userData;
}
}
export function mapLdapGroupsToUserRoles(ldap, ldapUser, user) {
const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups');
const syncUserRolesAutoRemove = settings.get('LDAP_Sync_User_Data_Groups_AutoRemove');
const syncUserRolesFieldMap = settings.get('LDAP_Sync_User_Data_GroupsMap').trim();
if (!syncUserRoles || !syncUserRolesFieldMap) {
logger.debug('not syncing user roles');
return [];
}
const roles = Roles.find({}, {
fields: {
_updatedAt: 0,
},
}).fetch();
if (!roles) {
return [];
}
let fieldMap;
try {
fieldMap = JSON.parse(syncUserRolesFieldMap);
} catch (err) {
logger.error(`Unexpected error : ${ err.message }`);
return [];
}
if (!fieldMap) {
return [];
}
const userRoles = [];
for (const ldapField in fieldMap) {
if (!fieldMap.hasOwnProperty(ldapField)) {
continue;
}
const userField = fieldMap[ldapField];
const [roleName] = userField.split(/\.(.+)/);
if (!_.find(roles, (el) => el._id === roleName)) {
logger.debug(`User Role doesn't exist: ${ roleName }`);
continue;
}
logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`);
if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) {
userRoles.push(roleName);
continue;
}
if (!syncUserRolesAutoRemove) {
continue;
}
const del = Roles.removeUserRoles(user._id, roleName);
if (settings.get('UI_DisplayRoles') && del) {
api.broadcast('user.roleUpdate', {
type: 'removed',
_id: roleName,
u: {
_id: user._id,
username: user.username,
},
});
}
}
return userRoles;
}
export function createRoomForSync(channel) {
logger.info(`Channel '${ channel }' doesn't exist, creating it.`);
const room = createRoom('c', channel, settings.get('LDAP_Sync_User_Data_Groups_AutoChannels_Admin'), [], false, { customFields: { ldap: true } });
if (!room || !room.rid) {
logger.error(`Unable to auto-create channel '${ channel }' during ldap sync.`);
return;
}
room._id = room.rid;
return room;
}
export function mapLDAPGroupsToChannels(ldap, ldapUser, user) {
const syncUserRoles = settings.get('LDAP_Sync_User_Data_Groups');
const syncUserRolesAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_AutoChannels');
const syncUserRolesEnforceAutoChannels = settings.get('LDAP_Sync_User_Data_Groups_Enforce_AutoChannels');
const syncUserRolesChannelFieldMap = settings.get('LDAP_Sync_User_Data_Groups_AutoChannelsMap').trim();
const userChannels = [];
if (!syncUserRoles || !syncUserRolesAutoChannels || !syncUserRolesChannelFieldMap) {
logger.debug('not syncing groups to channels');
return [];
}
let fieldMap;
try {
fieldMap = JSON.parse(syncUserRolesChannelFieldMap);
} catch (err) {
logger.error(`Unexpected error : ${ err.message }`);
return [];
}
if (!fieldMap) {
return [];
}
_.map(fieldMap, function(channels, ldapField) {
if (!Array.isArray(channels)) {
channels = [channels];
}
for (const channel of channels) {
let room = Rooms.findOneByNonValidatedName(channel);
if (!room) {
room = createRoomForSync(channel);
}
if (isUserInLDAPGroup(ldap, ldapUser, user, ldapField)) {
if (room.teamMain) {
logger.error(`Can't add user to channel ${ channel } because it is a team.`);
} else {
userChannels.push(room._id);
}
} else if (syncUserRolesEnforceAutoChannels && !room.teamMain) {
const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id);
if (subscription) {
removeUserFromRoom(room._id, user);
}
}
}
});
return userChannels;
}
function syncUserAvatar(user, ldapUser) {
if (!user?._id || settings.get('LDAP_Sync_User_Avatar') !== true) {
return;
}
const avatarField = (settings.get('LDAP_Avatar_Field') || 'thumbnailPhoto').trim();
const avatar = ldapUser._raw[avatarField] || ldapUser._raw.thumbnailPhoto || ldapUser._raw.jpegPhoto;
if (!avatar) {
return;
}
logger.info('Syncing user avatar');
Meteor.defer(() => {
const rs = RocketChatFile.bufferToStream(avatar);
const fileStore = FileUpload.getStore('Avatars');
fileStore.deleteByName(user.username);
const file = {
userId: user._id,
type: 'image/jpeg',
size: avatar.length,
};
Meteor.runAsUser(user._id, () => {
fileStore.insert(file, rs, (err, result) => {
Meteor.setTimeout(function() {
Users.setAvatarData(user._id, 'ldap', result.etag);
api.broadcast('user.avatarUpdate', { username: user.username, avatarETag: result.etag });
}, 500);
});
});
});
}
export function syncUserData(user, ldapUser, ldap) {
logger.info('Syncing user data');
logger.debug('user', { email: user.email, _id: user._id });
logger.debug('ldapUser', ldapUser.object);
const userData = getDataToSyncUserData(ldapUser, user);
// Returns a list of Rocket.Chat Groups a user should belong
// to if their LDAP group matches the LDAP_Sync_User_Data_GroupsMap
const userRoles = mapLdapGroupsToUserRoles(ldap, ldapUser, user);
// Returns a list of Rocket.Chat Channels a user should belong
// to if their LDAP group matches the LDAP_Sync_User_Data_Groups_AutoChannelsMap
const userChannels = mapLDAPGroupsToChannels(ldap, ldapUser, user);
if (user && user._id && userData) {
logger.debug({ msg: 'setting', userData });
if (userData.name) {
_setRealName(user._id, userData.name);
delete userData.name;
}
userData.customFields = {
...user.customFields, ...userData.customFields,
};
Meteor.users.update(user._id, { $set: userData });
user = Meteor.users.findOne({ _id: user._id });
}
if (settings.get('LDAP_Username_Field') !== '') {
const username = slug(getLdapUsername(ldapUser));
if (user && user._id && username !== user.username) {
logger.info('Syncing user username', user.username, '->', username);
saveUserIdentity({ _id: user._id, username });
}
}
if (settings.get('LDAP_Sync_User_Data_Groups') === true) {
for (const roleName of userRoles) {
const add = Roles.addUserRoles(user._id, roleName);
if (settings.get('UI_DisplayRoles') && add) {
api.broadcast('user.roleUpdate', {
type: 'added',
_id: roleName,
u: {
_id: user._id,
username: user.username,
},
});
}
logger.info('Synced user group', roleName, 'from LDAP for', user.username);
}
}
if (settings.get('LDAP_Sync_User_Data_Groups_AutoChannels') === true) {
for (const userChannel of userChannels) {
addUserToRoom(userChannel, user);
logger.info('Synced user channel', userChannel, 'from LDAP for', user.username);
}
}
syncUserAvatar(user, ldapUser);
}
export function addLdapUser(ldapUser, username, password, ldap) {
const uniqueId = getLdapUserUniqueID(ldapUser);
const userObject = {};
if (username) {
userObject.username = username;
}
const userData = getDataToSyncUserData(ldapUser, {});
if (userData && userData.emails && userData.emails[0] && userData.emails[0].address) {
if (Array.isArray(userData.emails[0].address)) {
userObject.email = userData.emails[0].address[0];
} else {
userObject.email = userData.emails[0].address;
}
} else if (ldapUser.mail && ldapUser.mail.indexOf('@') > -1) {
userObject.email = ldapUser.mail;
} else if (settings.get('LDAP_Default_Domain') !== '') {
userObject.email = `${ username || uniqueId.value }@${ settings.get('LDAP_Default_Domain') }`;
} else {
const error = new Meteor.Error('LDAP-login-error', 'LDAP Authentication succeeded, there is no email to create an account. Have you tried setting your Default Domain in LDAP Settings?');
logger.error(error);
throw error;
}
logger.debug('New user data', userObject);
if (password) {
userObject.password = password;
}
try {
userObject._id = Accounts.createUser(userObject);
} catch (error) {
logger.error('Error creating user', error);
return error;
}
syncUserData(userObject, ldapUser, ldap);
return {
userId: userObject._id,
};
}
export function importNewUsers(ldap) {
if (settings.get('LDAP_Enable') !== true) {
logger.error('Can\'t run LDAP Import, LDAP is disabled');
return;
}
if (!ldap) {
ldap = new LDAP();
}
if (!ldap.connected) {
ldap.connectSync();
}
let count = 0;
ldap.searchUsersSync('*', Meteor.bindEnvironment((error, ldapUsers, { next, end } = {}) => {
if (error) {
throw error;
}
ldapUsers.forEach((ldapUser) => {
count++;
const uniqueId = getLdapUserUniqueID(ldapUser);
// Look to see if user already exists
const userQuery = {
'services.ldap.id': uniqueId.value,
};
logger.debug('userQuery', userQuery);
let username;
if (settings.get('LDAP_Username_Field') !== '') {
username = slug(getLdapUsername(ldapUser));
}
// Add user if it was not added before
let user = Meteor.users.findOne(userQuery);
if (!user && username && settings.get('LDAP_Merge_Existing_Users') === true) {
const userQuery = {
username,
};
logger.debug('userQuery merge', userQuery);
user = Meteor.users.findOne(userQuery);
if (user) {
syncUserData(user, ldapUser, ldap);
}
}
if (!user) {
addLdapUser(ldapUser, username, undefined, ldap);
}
if (count % 100 === 0) {
logger.info('Import running. Users imported until now:', count);
}
});
if (end) {
logger.info('Import finished. Users imported:', count);
}
next(count);
}));
}
export function sync() {
if (settings.get('LDAP_Enable') !== true) {
return;
}
const ldap = new LDAP();
try {
ldap.connectSync();
let users;
if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) {
users = Users.findLDAPUsers();
}
if (settings.get('LDAP_Background_Sync_Import_New_Users') === true) {
importNewUsers(ldap);
}
if (settings.get('LDAP_Background_Sync_Keep_Existant_Users_Updated') === true) {
users.forEach(function(user) {
let ldapUser;
if (user.services && user.services.ldap && user.services.ldap.id) {
ldapUser = ldap.getUserByIdSync(user.services.ldap.id, user.services.ldap.idAttribute);
} else {
ldapUser = ldap.getUserByUsernameSync(user.username);
}
if (ldapUser) {
syncUserData(user, ldapUser, ldap);
}
callbacks.run('ldap.afterSyncExistentUser', { ldapUser, user });
});
}
} catch (error) {
logger.error(error);
return error;
}
return true;
}
const jobName = 'LDAP_Sync';
const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() {
if (settings.get('LDAP_Background_Sync') !== true) {
logger.info('Disabling LDAP Background Sync');
if (SyncedCron.nextScheduledAtDate(jobName)) {
SyncedCron.remove(jobName);
}
return;
}
if (settings.get('LDAP_Background_Sync_Interval')) {
logger.info('Enabling LDAP Background Sync');
SyncedCron.add({
name: jobName,
schedule: (parser) => parser.text(settings.get('LDAP_Background_Sync_Interval')),
job() {
sync();
},
});
}
}), 500);
Meteor.startup(() => {
Meteor.defer(() => {
settings.get('LDAP_Background_Sync', addCronJob);
settings.get('LDAP_Background_Sync_Interval', addCronJob);
});
});

@ -1,31 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { sync } from './sync';
import { hasRole } from '../../authorization';
import { settings } from '../../settings';
Meteor.methods({
ldap_sync_now() {
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_sync_users' });
}
if (!hasRole(user._id, 'admin')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_sync_users' });
}
if (settings.get('LDAP_Enable') !== true) {
throw new Meteor.Error('LDAP_disabled');
}
Meteor.defer(() => {
sync();
});
return {
message: 'Sync_in_progress',
params: [],
};
},
});

@ -1,43 +0,0 @@
import { Meteor } from 'meteor/meteor';
import LDAP from './ldap';
import { hasRole } from '../../authorization/server';
import { settings } from '../../settings/server';
import { SystemLogger } from '../../../server/lib/logger/system';
Meteor.methods({
ldap_test_connection() {
const user = Meteor.user();
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'ldap_test_connection' });
}
if (!hasRole(user._id, 'admin')) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'ldap_test_connection' });
}
if (settings.get('LDAP_Enable') !== true) {
throw new Meteor.Error('LDAP_disabled');
}
let ldap;
try {
ldap = new LDAP();
ldap.connectSync();
} catch (error) {
SystemLogger.error(error);
throw new Meteor.Error(error.message);
}
try {
ldap.bindIfNecessary();
} catch (error) {
throw new Meteor.Error(error.name || error.message);
}
return {
message: 'Connection_success',
params: [],
};
},
});

@ -40,6 +40,7 @@ import LivechatExternalMessage from './models/LivechatExternalMessages';
import OmnichannelQueue from './models/OmnichannelQueue';
import Analytics from './models/Analytics';
import EmailInbox from './models/EmailInbox';
import ImportData from './models/ImportData';
export { AppsLogsModel } from './models/apps-logs-model';
export { AppsPersistenceModel } from './models/apps-persistence-model';
@ -92,4 +93,5 @@ export {
Analytics,
OmnichannelQueue,
EmailInbox,
ImportData,
};

@ -1,5 +1,5 @@
import { Base } from '../../../models/server';
import { IImportUserRecord, IImportChannelRecord } from '../definitions/IImportRecord';
import { Base } from './_Base';
import { IImportUserRecord, IImportChannelRecord } from '../../../../definition/IImportRecord';
class ImportDataModel extends Base {
constructor() {
@ -85,4 +85,4 @@ class ImportDataModel extends Base {
}
}
export const ImportData = new ImportDataModel();
export default new ImportDataModel();

@ -875,12 +875,6 @@ export class Users extends Base {
return this.find(query, options);
}
findLDAPUsers(options) {
const query = { ldap: true };
return this.find(query, options);
}
findCrowdUsers(options) {
const query = { crowd: true };

@ -0,0 +1,18 @@
import { Cursor } from 'mongodb';
import { BaseRaw } from './BaseRaw';
import { IImportRecord, IImportUserRecord, IImportMessageRecord, IImportChannelRecord } from '../../../../definition/IImportRecord';
export class ImportDataRaw extends BaseRaw<IImportRecord> {
getAllUsers(): Cursor<IImportUserRecord> {
return this.find({ dataType: 'user' }) as Cursor<IImportUserRecord>;
}
getAllMessages(): Cursor<IImportMessageRecord> {
return this.find({ dataType: 'message' }) as Cursor<IImportMessageRecord>;
}
getAllChannels(): Cursor<IImportChannelRecord> {
return this.find({ dataType: 'channel' }) as Cursor<IImportChannelRecord>;
}
}

@ -138,6 +138,24 @@ export class UsersRaw extends BaseRaw {
return this.findOne(query, options);
}
async findOneByLDAPId(id, attribute = undefined) {
const query = {
'services.ldap.id': id,
};
if (attribute) {
query['services.ldap.idAttribute'] = attribute;
}
return this.findOne(query);
}
async findLDAPUsers(options) {
const query = { ldap: true };
return this.find(query, options);
}
isUserInRole(userId, roleName) {
const query = {
_id: userId,

@ -69,6 +69,8 @@ import EmailMessageHistoryModel from '../models/EmailMessageHistory';
import { EmailMessageHistoryRaw } from './EmailMessageHistory';
import { api } from '../../../../server/sdk/api';
import { initWatchers } from '../../../../server/modules/watchers/watchers.module';
import ImportDataModel from '../models/ImportData';
import { ImportDataRaw } from './ImportData';
const trashCollection = trash.rawCollection();
@ -106,6 +108,7 @@ export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), tra
export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection);
export const EmailInbox = new EmailInboxRaw(EmailInboxModel.model.rawCollection(), trashCollection);
export const EmailMessageHistory = new EmailMessageHistoryRaw(EmailMessageHistoryModel.model.rawCollection(), trashCollection);
export const ImportData = new ImportDataRaw(ImportDataModel.model.rawCollection(), trashCollection);
const map = {
[Messages.col.collectionName]: MessagesModel,

@ -3,7 +3,7 @@ import _ from 'underscore';
import { SettingValue } from '../../../definition/ISetting';
export type SettingComposedValue = {key: string; value: SettingValue};
export type SettingComposedValue<T extends SettingValue = SettingValue> = {key: string; value: T};
export type SettingCallback = (key: string, value: SettingValue, initialLoad?: boolean) => void;
interface ISettingRegexCallbacks {
@ -17,11 +17,15 @@ export class SettingsBase {
private regexCallbacks = new Map<string, ISettingRegexCallbacks>();
// private ts = new Date()
public get(_id: RegExp, callback?: SettingCallback): SettingComposedValue[];
public get<T extends SettingValue = SettingValue>(_id: RegExp, callback: SettingCallback): void;
public get(_id: string, callback?: SettingCallback): SettingValue | void;
public get<T extends SettingValue = SettingValue>(_id: string, callback: SettingCallback): void;
public get(_id: string | RegExp, callback?: SettingCallback): SettingValue | SettingComposedValue[] | void {
public get<T extends SettingValue = SettingValue>(_id: RegExp): SettingComposedValue<T>[];
public get<T extends SettingValue = SettingValue>(_id: string): T | undefined;
public get<T extends SettingValue = SettingValue>(_id: string | RegExp, callback?: SettingCallback): T | undefined | SettingComposedValue<T>[] | void {
if (callback != null) {
this.onload(_id, callback);
if (!Meteor.settings) {
@ -44,7 +48,11 @@ export class SettingsBase {
}
if (typeof _id === 'string') {
return Meteor.settings[_id] != null && callback(_id, Meteor.settings[_id]);
const value = Meteor.settings[_id];
if (value != null) {
callback(_id, Meteor.settings[_id]);
}
return;
}
}
@ -53,7 +61,7 @@ export class SettingsBase {
}
if (_.isRegExp(_id)) {
return Object.keys(Meteor.settings).reduce((items: SettingComposedValue[], key) => {
return Object.keys(Meteor.settings).reduce((items: SettingComposedValue<T>[], key) => {
const value = Meteor.settings[key];
if (_id.test(key)) {
items.push({

@ -10,8 +10,18 @@ class SettingsClass {
public upsertCalls = 0;
private checkQueryMatch(key: string, data: Dictionary, queryValue: any): boolean {
if (typeof queryValue === 'object') {
if (queryValue.$exists !== undefined) {
return (data.hasOwnProperty(key) && data[key] !== undefined) === queryValue.$exists;
}
}
return queryValue === data[key];
}
findOne(query: Dictionary): any {
return [...this.data.values()].find((data) => Object.entries(query).every(([key, value]) => data[key] === value));
return [...this.data.values()].find((data) => Object.entries(query).every(([key, value]) => this.checkQueryMatch(key, data, value)));
}
upsert(query: any, update: any): void {

@ -68,6 +68,8 @@ const overrideSetting = (_id: string, value: SettingValue, options: ISettingAddO
export interface ISettingAddOptions extends Partial<ISetting> {
force?: boolean;
actionText?: string;
code?: 'application/json';
}
export interface ISettingAddGroupOptions {
@ -86,6 +88,7 @@ interface IUpdateOperator {
};
$unset?: {
section?: 1;
tab?: 1;
};
}
@ -99,11 +102,13 @@ type Query<T> = {
type addSectionCallback = (this: {
add(id: string, value: SettingValue, options: ISettingAddOptions): void;
set(options: ISettingAddOptions, cb: addSectionCallback): void;
}) => void;
type addGroupCallback = (this: {
add(id: string, value: SettingValue, options: ISettingAddOptions): void;
section(section: string, cb: addSectionCallback): void;
set(options: ISettingAddOptions, cb: addGroupCallback): void;
}) => void;
class Settings extends SettingsBase {
@ -113,20 +118,13 @@ class Settings extends SettingsBase {
private initialLoad = false;
/*
* Add a setting
*/
add(_id: string, value: SettingValue, { editor, ...options }: ISettingAddOptions = {}): boolean {
if (!_id || value == null) {
return false;
}
private validateOptions(_id: string, value: SettingValue, options: ISettingAddOptions): void {
if (options.group && this._sorter[options.group] == null) {
this._sorter[options.group] = 0;
}
options.packageValue = value;
options.valueSource = 'packageValue';
options.hidden = options.hidden || false;
options.blocked = options.blocked || false;
options.requiredOnWizard = options.requiredOnWizard || false;
options.secret = options.secret || false;
options.enterprise = options.enterprise || false;
@ -142,8 +140,8 @@ class Settings extends SettingsBase {
if (options.enableQuery != null) {
options.enableQuery = JSON.stringify(options.enableQuery);
}
if (options.i18nLabel == null) {
options.i18nLabel = _id;
if (options.displayQuery != null) {
options.displayQuery = JSON.stringify(options.displayQuery);
}
if (options.i18nDescription == null) {
options.i18nDescription = `${ _id }_Description`;
@ -160,6 +158,21 @@ class Settings extends SettingsBase {
if (options.autocomplete == null) {
options.autocomplete = true;
}
}
/*
* Add a setting
*/
add(_id: string, value: SettingValue, { editor, ...options }: ISettingAddOptions = {}): boolean {
if (!_id || value == null) {
return false;
}
this.validateOptions(_id, value, options);
options.blocked = options.blocked || false;
if (options.i18nLabel == null) {
options.i18nLabel = _id;
}
value = overrideSetting(_id, value, options);
@ -196,6 +209,15 @@ class Settings extends SettingsBase {
};
}
if (!options.tab) {
updateOperations.$unset = {
tab: 1,
};
query.tab = {
$exists: false,
};
}
const existentSetting = SettingsModel.findOne(query);
if (existentSetting) {
if (existentSetting.editor || !updateOperations.$setOnInsert.editor) {
@ -279,19 +301,36 @@ class Settings extends SettingsBase {
}
if (cb != null) {
cb.call({
add: (id: string, value: SettingValue, options: ISettingAddOptions = {}) => {
options.group = _id;
return this.add(id, value, options);
},
section: (section: string, cb: addSectionCallback) => cb.call({
add: (id: string, value: SettingValue, options: ISettingAddOptions = {}) => {
options.group = _id;
options.section = section;
return this.add(id, value, options);
},
}),
});
const addWith = (preset: ISettingAddOptions) => (id: string, value: SettingValue, options: ISettingAddOptions = {}): void => {
const mergedOptions = Object.assign({}, preset, options);
this.add(id, value, mergedOptions);
};
const sectionSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addSectionCallback): void => {
const mergedOptions = Object.assign({}, preset, options);
cb.call({
add: addWith(mergedOptions),
set: sectionSetWith(mergedOptions),
});
};
const sectionWith = (preset: ISettingAddOptions) => (section: string, cb: addSectionCallback): void => {
const mergedOptions = Object.assign({}, preset, { section });
cb.call({
add: addWith(mergedOptions),
set: sectionSetWith(mergedOptions),
});
};
const groupSetWith = (preset: ISettingAddOptions) => (options: ISettingAddOptions, cb: addGroupCallback): void => {
const mergedOptions = Object.assign({}, preset, options);
cb.call({
add: addWith(mergedOptions),
section: sectionWith(mergedOptions),
set: groupSetWith(mergedOptions),
});
};
groupSetWith({ group: _id })({}, cb);
}
return true;
}

@ -25,10 +25,10 @@ export function getServicesStatistics(): Record<string, unknown> {
loginFallback: settings.get('LDAP_Login_Fallback'),
encryption: settings.get('LDAP_Encryption'),
mergeUsers: settings.get('LDAP_Merge_Existing_Users'),
syncRoles: settings.get('LDAP_Sync_User_Data_Groups'),
syncRolesAutoRemove: settings.get('LDAP_Sync_User_Data_Groups_AutoRemove'),
syncData: settings.get('LDAP_Sync_User_Data'),
syncChannels: settings.get('LDAP_Sync_User_Data_Groups_AutoChannels'),
syncRoles: settings.get('LDAP_Sync_User_Data_Roles'),
syncRolesAutoRemove: settings.get('LDAP_Sync_User_Data_Roles_AutoRemove'),
syncData: settings.get('LDAP_Sync_Custom_Fields'),
syncChannels: settings.get('LDAP_Sync_User_Data_Channels'),
syncAvatar: settings.get('LDAP_Sync_User_Avatar'),
groupFilter: settings.get('LDAP_Group_Filter_Enable'),
backgroundSync: {
@ -40,7 +40,7 @@ export function getServicesStatistics(): Record<string, unknown> {
ee: {
syncActiveState: settings.get('LDAP_Sync_User_Active_State'),
syncTeams: settings.get('LDAP_Enable_LDAP_Groups_To_RC_Teams'),
syncRoles: settings.get('LDAP_Enable_LDAP_Roles_To_RC_Roles'),
syncRoles: settings.get('LDAP_Sync_User_Data_Roles'),
},
},
saml: {

@ -1,15 +1,18 @@
import { SyncedCron } from 'meteor/littledata:synced-cron';
type ScheduleType = 'cron' | 'text';
export interface ICronJobs {
add(name: string, schedule: string, callback: Function): void;
add(name: string, schedule: string, callback: Function, scheduleType?: ScheduleType): void;
remove(name: string): void;
nextScheduledAtDate(name: string): Date | number | undefined;
}
class SyncedCronJobs implements ICronJobs {
add(name: string, schedule: string, callback: Function): void {
add(name: string, schedule: string, callback: Function, scheduleType: ScheduleType = 'cron'): void {
SyncedCron.add({
name,
schedule: (parser: any) => parser.cron(schedule),
schedule: (parser: any) => parser[scheduleType](schedule),
job() {
const [day, hour] = this.name.split('/');
callback(day, hour);
@ -20,6 +23,10 @@ class SyncedCronJobs implements ICronJobs {
remove(name: string): void {
SyncedCron.remove(name);
}
nextScheduledAtDate(name: string): Date | number | undefined {
return SyncedCron.nextScheduledAtDate(name);
}
}
export const cronJobs: ICronJobs = new SyncedCronJobs();

@ -13,8 +13,11 @@ import { UiKitPayload, UIKitActionEvent } from '../../../definition/UIKit';
const useUIKitHandleAction = <S extends UiKitPayload>(
state: S,
): ((event: UIKitActionEvent) => Promise<void>) =>
useMutableCallback(async ({ blockId, value, appId, actionId }) =>
ActionManager.triggerBlockAction({
useMutableCallback(async ({ blockId, value, appId, actionId }) => {
if (!appId) {
throw new Error('useUIKitHandleAction - invalid appId');
}
return ActionManager.triggerBlockAction({
container: {
type: UIKitIncomingInteractionContainerType.VIEW,
id: state.viewId || state.appId,
@ -23,7 +26,7 @@ const useUIKitHandleAction = <S extends UiKitPayload>(
appId,
value,
blockId,
}),
);
});
});
export { useUIKitHandleAction };

@ -1,12 +1,13 @@
import { createContext, useContext, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
import { ISetting, SectionName, SettingId, GroupId } from '../../definition/ISetting';
import { ISetting, SectionName, SettingId, GroupId, TabId } from '../../definition/ISetting';
import { SettingsContextQuery } from './SettingsContext';
export interface IEditableSetting extends ISetting {
disabled: boolean;
changed: boolean;
invisible: boolean;
}
export type EditableSettingsContextQuery = SettingsContextQuery & {
@ -18,7 +19,8 @@ export type EditableSettingsContextValue = {
readonly queryEditableSettings: (
query: EditableSettingsContextQuery,
) => Subscription<IEditableSetting[]>;
readonly queryGroupSections: (_id: GroupId) => Subscription<SectionName[]>;
readonly queryGroupSections: (_id: GroupId, tab?: TabId) => Subscription<SectionName[]>;
readonly queryGroupTabs: (_id: GroupId) => Subscription<TabId[]>;
readonly dispatch: (changes: Partial<IEditableSetting>[]) => void;
};
@ -35,6 +37,10 @@ export const EditableSettingsContext = createContext<EditableSettingsContextValu
getCurrentValue: (): SectionName[] => [],
subscribe: (): Unsubscribe => (): void => undefined,
}),
queryGroupTabs: () => ({
getCurrentValue: (): TabId[] => [],
subscribe: (): Unsubscribe => (): void => undefined,
}),
dispatch: () => undefined,
});
@ -54,10 +60,17 @@ export const useEditableSettings = (query?: EditableSettingsContextQuery): IEdit
return useSubscription(subscription);
};
export const useEditableSettingsGroupSections = (_id: SettingId): SectionName[] => {
export const useEditableSettingsGroupSections = (_id: SettingId, tab?: TabId): SectionName[] => {
const { queryGroupSections } = useContext(EditableSettingsContext);
const subscription = useMemo(() => queryGroupSections(_id), [queryGroupSections, _id]);
const subscription = useMemo(() => queryGroupSections(_id, tab), [queryGroupSections, _id, tab]);
return useSubscription(subscription);
};
export const useEditableSettingsGroupTabs = (_id: SettingId): TabId[] => {
const { queryGroupTabs } = useContext(EditableSettingsContext);
const subscription = useMemo(() => queryGroupTabs(_id), [queryGroupTabs, _id]);
return useSubscription(subscription);
};

@ -1,12 +1,16 @@
import { createContext, useContext, ReactNode } from 'react';
type ModalContextValue = unknown & {
import { modal } from '../../app/ui-utils/client';
type ModalContextValue = typeof modal & {
setModal: (modal: ReactNode) => void;
};
export const ModalContext = createContext<ModalContextValue>({
setModal: () => undefined,
});
export const ModalContext = createContext<ModalContextValue>(
Object.assign(modal, {
setModal: () => undefined,
}),
);
export const useModal = (): ModalContextValue => useContext(ModalContext);

@ -10,6 +10,7 @@ import type { DnsEndpoints } from './endpoints/v1/dns';
import type { EmojiCustomEndpoints } from './endpoints/v1/emojiCustom';
import type { GroupsEndpoints } from './endpoints/v1/groups';
import type { ImEndpoints } from './endpoints/v1/im';
import type { LDAPEndpoints } from './endpoints/v1/ldap';
import type { MiscEndpoints } from './endpoints/v1/misc';
import type { OmnichannelEndpoints } from './endpoints/v1/omnichannel';
import type { RoomsEndpoints } from './endpoints/v1/rooms';
@ -25,6 +26,7 @@ type Endpoints = ChatEndpoints &
EmojiCustomEndpoints &
GroupsEndpoints &
ImEndpoints &
LDAPEndpoints &
RoomsEndpoints &
TeamsEndpoints &
UsersEndpoints &

@ -0,0 +1,14 @@
import type { TranslationKey } from '../../../TranslationContext';
export type LDAPEndpoints = {
'ldap.testConnection': {
POST: () => {
message: TranslationKey;
};
};
'ldap.syncNow': {
POST: () => {
message: TranslationKey;
};
};
};

@ -1,12 +1,13 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
import { useSubscription, Subscription, Unsubscribe } from 'use-subscription';
import { SettingId, ISetting, GroupId, SectionName } from '../../definition/ISetting';
import { SettingId, ISetting, GroupId, SectionName, TabId } from '../../definition/ISetting';
export type SettingsContextQuery = {
readonly _id?: SettingId[];
readonly group?: GroupId;
readonly section?: SectionName;
readonly tab?: TabId;
};
export type SettingsContextValue = {

@ -24,7 +24,6 @@ import '../app/importer-hipchat-enterprise/client';
import '../app/importer-slack/client';
import '../app/importer-slack-users/client';
import '../app/integrations/client/startup';
import '../app/ldap/client';
import '../app/lib/client';
import '../app/livestream/client';
import '../app/logger/client';

@ -1,9 +1,10 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Mongo } from 'meteor/mongo';
import { Tracker } from 'meteor/tracker';
import { FilterQuery } from 'mongodb';
import React, { useEffect, useMemo, FunctionComponent, useRef, MutableRefObject } from 'react';
import { SettingId, GroupId } from '../../definition/ISetting';
import { SettingId, GroupId, ISetting, TabId } from '../../definition/ISetting';
import {
EditableSettingsContext,
IEditableSetting,
@ -44,37 +45,36 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
}
}, [getSettingsCollection, persistedSettings]);
const queryEditableSetting = useMemo(
() =>
createReactiveSubscriptionFactory((_id: SettingId): IEditableSetting | undefined => {
const settingsCollection = getSettingsCollection();
const editableSetting = settingsCollection.findOne(_id);
if (!editableSetting) {
return undefined;
}
if (editableSetting.blocked) {
return { ...editableSetting, disabled: true };
}
if (!editableSetting.enableQuery) {
return { ...editableSetting, disabled: false };
}
const queries = [].concat(
typeof editableSetting.enableQuery === 'string'
? JSON.parse(editableSetting.enableQuery)
: editableSetting.enableQuery,
);
return {
...editableSetting,
disabled: !queries.every((query) => settingsCollection.find(query).count() > 0),
};
}),
[getSettingsCollection],
);
const queryEditableSetting = useMemo(() => {
const validateSettingQueries = (
query: undefined | string | FilterQuery<ISetting> | FilterQuery<ISetting>[],
settingsCollection: Mongo.Collection<IEditableSetting>,
): boolean => {
if (!query) {
return true;
}
const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query);
return queries.every((query) => settingsCollection.find(query).count() > 0);
};
return createReactiveSubscriptionFactory((_id: SettingId): IEditableSetting | undefined => {
const settingsCollection = getSettingsCollection();
const editableSetting = settingsCollection.findOne(_id);
if (!editableSetting) {
return undefined;
}
return {
...editableSetting,
disabled:
editableSetting.blocked ||
!validateSettingQueries(editableSetting.enableQuery, settingsCollection),
invisible: !validateSettingQueries(editableSetting.displayQuery, settingsCollection),
};
});
}, [getSettingsCollection]);
const queryEditableSettings = useMemo(
() =>
@ -84,13 +84,25 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
{
...('_id' in query && { _id: { $in: query._id } }),
...('group' in query && { group: query.group }),
...('section' in query &&
(query.section
? { section: query.section }
: {
$or: [{ section: { $exists: false } }, { section: '' }],
})),
...('changed' in query && { changed: query.changed }),
$and: [
{
...('section' in query &&
(query.section
? { section: query.section }
: {
$or: [{ section: { $exists: false } }, { section: '' }],
})),
},
{
...('tab' in query &&
(query.tab
? { tab: query.tab }
: {
$or: [{ tab: { $exists: false } }, { tab: '' }],
})),
},
],
},
{
sort: {
@ -107,21 +119,26 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
const queryGroupSections = useMemo(
() =>
createReactiveSubscriptionFactory((_id: GroupId) =>
createReactiveSubscriptionFactory((_id: GroupId, tab?: TabId) =>
Array.from(
new Set(
getSettingsCollection()
.find(
{
group: _id,
...(tab !== undefined
? { tab }
: {
$or: [{ tab: { $exists: false } }, { tab: '' }],
}),
},
{
fields: {
section: 1,
},
sort: {
section: 1,
sorter: 1,
section: 1,
i18nLabel: 1,
},
},
@ -134,6 +151,35 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
[getSettingsCollection],
);
const queryGroupTabs = useMemo(
() =>
createReactiveSubscriptionFactory((_id: GroupId) =>
Array.from(
new Set(
getSettingsCollection()
.find(
{
group: _id,
},
{
fields: {
tab: 1,
},
sort: {
sorter: 1,
tab: 1,
i18nLabel: 1,
},
},
)
.fetch()
.map(({ tab }) => tab || ''),
),
),
),
[getSettingsCollection],
);
const dispatch = useMutableCallback((changes: Partial<IEditableSetting>[]): void => {
for (const { _id, ...data } of changes) {
if (!_id) {
@ -150,9 +196,10 @@ const EditableSettingsProvider: FunctionComponent<EditableSettingsProviderProps>
queryEditableSetting,
queryEditableSettings,
queryGroupSections,
queryGroupTabs,
dispatch,
}),
[queryEditableSetting, queryEditableSettings, queryGroupSections, dispatch],
[queryEditableSetting, queryEditableSettings, queryGroupSections, queryGroupTabs, dispatch],
);
return <EditableSettingsContext.Provider children={children} value={contextValue} />;

@ -7,6 +7,7 @@ import './e2e';
import './emailVerification';
import './i18n';
import './listenActiveUsers';
import './ldap';
import './loginViaQuery';
import './messageTypes';
import './notifications';

@ -0,0 +1,20 @@
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
(Meteor as any).loginWithLDAP = function (
username: string,
password: string,
callback?: (err?: any) => void,
): void {
Accounts.callLoginMethod({
methodArguments: [
{
ldap: true,
username,
ldapPass: password,
ldapOptions: {},
},
],
userCallback: callback,
});
};

@ -54,16 +54,12 @@ const AccountProfilePage = () => {
const erasureType = useSetting('Message_ErasureType');
const allowRealNameChange = useSetting('Accounts_AllowRealNameChange');
const allowUserStatusMessageChange = useSetting('Accounts_AllowUserStatusMessageChange');
const allowUsernameChange = useSetting('Accounts_AllowUsernameChange');
const canChangeUsername = useSetting('Accounts_AllowUsernameChange');
const allowEmailChange = useSetting('Accounts_AllowEmailChange');
let allowPasswordChange = useSetting('Accounts_AllowPasswordChange');
const allowOAuthPasswordChange = useSetting('Accounts_AllowPasswordChangeForOAuthUsers');
const allowUserAvatarChange = useSetting('Accounts_AllowUserAvatarChange');
const allowDeleteOwnAccount = useSetting('Accounts_AllowDeleteOwnAccount');
const ldapEnabled = useSetting('LDAP_Enable');
const ldapUsernameField = useSetting('LDAP_Username_Field');
// whether the username is forced to match LDAP:
const ldapUsernameLinked = ldapEnabled && ldapUsernameField;
const requireName = useSetting('Accounts_RequireNameForSignUp');
const namesRegexSetting = useSetting('UTF8_User_Names_Validation');
@ -73,8 +69,6 @@ const AccountProfilePage = () => {
const namesRegex = useMemo(() => new RegExp(`^${namesRegexSetting}$`), [namesRegexSetting]);
const canChangeUsername = allowUsernameChange && !ldapUsernameLinked;
const settings = useMemo(
() => ({
allowRealNameChange,

@ -68,7 +68,7 @@ const LicenseCard = ({ statistics, isLoading }) => {
<>
<Feature label={t('Omnichannel')} enabled={hasOmnichannel} />
<Feature label={t('Auditing')} enabled={hasAuditing} />
<Feature label={t('Canned_responses')} enabled={hasCannedResponses} />
<Feature label={t('Canned_Responses')} enabled={hasCannedResponses} />
<Feature label={t('Engagement_Dashboard')} enabled={hasEngagement} />
</>
)}

@ -13,7 +13,14 @@ import { useTranslation, useLoadLanguage } from '../../../contexts/TranslationCo
import { useUser } from '../../../contexts/UserContext';
import GroupPageSkeleton from './GroupPageSkeleton';
function GroupPage({ children, headerButtons, _id, i18nLabel, i18nDescription }) {
function GroupPage({
children = undefined,
headerButtons = undefined,
_id,
i18nLabel,
i18nDescription = undefined,
tabs = undefined,
}) {
const changedEditableSettings = useEditableSettings(
useMemo(
() => ({
@ -136,6 +143,8 @@ function GroupPage({ children, headerButtons, _id, i18nLabel, i18nDescription })
</ButtonGroup>
</Page.Header>
{tabs}
<Page.ScrollableContentWithShadow>
<Box marginBlock='none' marginInline='auto' width='full' maxWidth='x580'>
{t.has(i18nDescription) && (

@ -4,8 +4,9 @@ import { GroupId } from '../../../../definition/ISetting';
import { useSettingStructure } from '../../../contexts/SettingsContext';
import GroupPage from './GroupPage';
import AssetsGroupPage from './groups/AssetsGroupPage';
import GenericGroupPage from './groups/GenericGroupPage';
import LDAPGroupPage from './groups/LDAPGroupPage';
import OAuthGroupPage from './groups/OAuthGroupPage';
import TabbedGroupPage from './groups/TabbedGroupPage';
type GroupSelectorProps = {
groupId: GroupId;
@ -26,7 +27,11 @@ const GroupSelector: FunctionComponent<GroupSelectorProps> = ({ groupId }) => {
return <OAuthGroupPage {...group} />;
}
return <GenericGroupPage {...group} />;
if (groupId === 'LDAP') {
return <LDAPGroupPage {...group} />;
}
return <TabbedGroupPage {...group} />;
};
export default GroupSelector;

@ -27,8 +27,13 @@ const MemoizedSetting = ({
onChangeValue = () => {},
onChangeEditor = () => {},
className,
invisible,
...inputProps
}) => {
if (invisible) {
return null;
}
const InputComponent =
{
boolean: BooleanSettingInput,

@ -10,14 +10,23 @@ import { useTranslation } from '../../../contexts/TranslationContext';
import SectionSkeleton from './SectionSkeleton';
import Setting from './Setting';
function Section({ children, groupId, hasReset = true, help, sectionName, solo }) {
function Section({
children = undefined,
groupId,
hasReset = true,
help = undefined,
sectionName,
tabName,
solo,
}) {
const editableSettings = useEditableSettings(
useMemo(
() => ({
group: groupId,
section: sectionName,
tab: tabName,
}),
[groupId, sectionName],
[groupId, sectionName, tabName],
),
);

@ -87,6 +87,7 @@ function Setting({ className, settingId, sectionChanged }) {
i18nLabel,
i18nDescription,
alert,
invisible,
} = setting;
const label = (i18nLabel && t(i18nLabel)) || _id || t(_id);
@ -122,6 +123,7 @@ function Setting({ className, settingId, sectionChanged }) {
onChangeValue={onChangeValue}
onChangeEditor={onChangeEditor}
onResetButtonClick={onResetButtonClick}
invisible={invisible}
/>
);
}

@ -0,0 +1,103 @@
import { Button } from '@rocket.chat/fuselage';
import React, { memo, useMemo } from 'react';
import type { ISetting } from '../../../../../definition/ISetting';
import { useEditableSettings } from '../../../../contexts/EditableSettingsContext';
import { useModal } from '../../../../contexts/ModalContext';
import { useEndpoint } from '../../../../contexts/ServerContext';
import { useSetting } from '../../../../contexts/SettingsContext';
import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../../contexts/TranslationContext';
import TabbedGroupPage from './TabbedGroupPage';
function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const testConnection = useEndpoint('POST', 'ldap.testConnection');
const syncNow = useEndpoint('POST', 'ldap.syncNow');
const ldapEnabled = useSetting('LDAP_Enable');
const ldapSyncEnabled = useSetting('LDAP_Background_Sync') && ldapEnabled;
const modal = useModal();
const editableSettings = useEditableSettings(
useMemo(
() => ({
group: _id,
}),
[_id],
),
);
const changed = useMemo(
() => editableSettings.some(({ changed }) => changed),
[editableSettings],
);
const handleTestConnectionButtonClick = async (): Promise<void> => {
try {
const { message } = await testConnection(undefined);
dispatchToastMessage({ type: 'success', message: t(message) });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
const handleSyncNowButtonClick = async (): Promise<void> => {
try {
await testConnection(undefined);
// #ToDo: Switch to modal.setModal
modal.open(
{
title: t('Execute_Synchronization_Now'),
text: t('LDAP_Sync_Now_Description'),
confirmButtonText: t('Sync'),
showCancelButton: true,
closeOnConfirm: true,
closeOnCancel: true,
},
async (isConfirm: boolean): Promise<void> => {
if (!isConfirm) {
return;
}
try {
const { message } = await syncNow(undefined);
dispatchToastMessage({ type: 'success', message: t(message) });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
},
);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
return (
<TabbedGroupPage
_id={_id}
{...group}
headerButtons={
<>
<Button
children={t('Test_Connection')}
disabled={!ldapEnabled || changed}
onClick={handleTestConnectionButtonClick}
/>
{ldapSyncEnabled && (
<Button
children={t('LDAP_Sync_Now')}
disabled={!ldapSyncEnabled || changed}
onClick={handleSyncNowButtonClick}
/>
)}
<Button is='a' href='https://go.rocket.chat/i/ldap-doc' target='_blank'>
{t('LDAP_Documentation')}
</Button>
</>
}
/>
);
}
export default memo(LDAPGroupPage);

@ -0,0 +1,66 @@
import { Tabs } from '@rocket.chat/fuselage';
import React, { ReactNode, memo, useState, useMemo } from 'react';
import {
useEditableSettingsGroupSections,
useEditableSettingsGroupTabs,
} from '../../../../contexts/EditableSettingsContext';
import { useTranslation, TranslationKey } from '../../../../contexts/TranslationContext';
import GroupPage from '../GroupPage';
import Section from '../Section';
import GenericGroupPage from './GenericGroupPage';
function TabbedGroupPage({
_id,
...group
}: {
children?: ReactNode;
headerButtons?: ReactNode;
_id: string;
i18nLabel: string;
i18nDescription?: string;
tabs?: ReactNode;
}): JSX.Element {
const t = useTranslation();
const tabs = useEditableSettingsGroupTabs(_id);
const [tab, setTab] = useState(tabs[0]);
const handleTabClick = useMemo(() => (tab: string) => (): void => setTab(tab), [setTab]);
const sections = useEditableSettingsGroupSections(_id, tab);
const solo = sections.length === 1;
if (!tabs.length || (tabs.length === 1 && !tabs[0])) {
return <GenericGroupPage _id={_id} {...group} />;
}
if (!tab && tabs[0]) {
setTab(tabs[0]);
}
const tabsComponent = (
<Tabs>
{tabs.map((tabName) => (
<Tabs.Item key={tabName || ''} selected={tab === tabName} onClick={handleTabClick(tabName)}>
{tabName ? t(tabName as TranslationKey) : t(_id as TranslationKey)}
</Tabs.Item>
))}
</Tabs>
);
return (
<GroupPage _id={_id} {...group} tabs={tabsComponent}>
{sections.map((sectionName) => (
<Section
key={sectionName || ''}
groupId={_id}
sectionName={sectionName}
tabName={tab}
solo={solo}
/>
))}
</GroupPage>
);
}
export default memo(TabbedGroupPage);

@ -21,12 +21,11 @@ import { isEmail } from '../../../../app/utils/client';
import Page from '../../../components/Page';
import { useRoomsList } from '../../../components/RoomAutoComplete/hooks/useRoomsList';
import { useRoute } from '../../../contexts/RouterContext';
import { useMethod } from '../../../contexts/ServerContext';
import { useMethod, useEndpoint } from '../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useRecordList } from '../../../hooks/lists/useRecordList';
import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
import { useForm } from '../../../hooks/useForm';
import { AsyncStatePhase } from '../../../lib/asyncState';
import { formsSubscription } from '../additionalForms';
@ -135,10 +134,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) {
});
const saveDepartmentInfo = useMethod('livechat:saveDepartment');
const saveDepartmentAgentsInfoOnEdit = useEndpointAction(
'POST',
`livechat/department/${id}/agents`,
);
const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `livechat/department/${id}/agents`);
const dispatchToastMessage = useToastMessageDispatch();
@ -223,7 +219,9 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) {
try {
if (id) {
await saveDepartmentInfo(id, payload, []);
await saveDepartmentAgentsInfoOnEdit(agentListPayload);
if (agentListPayload.upsert.length > 0 || agentListPayload.remove.length > 0) {
await saveDepartmentAgentsInfoOnEdit(agentListPayload);
}
} else {
await saveDepartmentInfo(id, payload, agentList);
}

@ -25,7 +25,7 @@ const reduceSendOptions = (options) =>
return acc;
}, []);
const integrationsUrl = 'https://rocket.chat/docs/administrator-guides/livechat/#integrations';
const integrationsUrl = 'https://docs.rocket.chat/guides/omnichannel/webhooks-managers-guide';
const getInitialValues = ({
Livechat_webhookUrl,

@ -2,11 +2,19 @@ import { IImportUser } from './IImportUser';
import { IImportChannel } from './IImportChannel';
import { IImportMessage } from './IImportMessage';
export type IImportRecordType = 'user' | 'channel' | 'message';
export type IImportData = IImportUser | IImportChannel | IImportMessage;
export interface IImportRecord {
data: IImportUser | IImportChannel | IImportMessage;
dataType: 'user' | 'channel' | 'message';
data: IImportData;
dataType: IImportRecordType;
_id: string;
options?: {};
errors?: Array<{
message: string;
stack?: string;
}>;
skipped?: boolean;
}
export interface IImportUserRecord extends IImportRecord {

@ -7,11 +7,13 @@ export interface IImportUser {
importIds: Array<string>;
name?: string;
utcOffset?: number;
active?: boolean;
avatarUrl?: string;
deleted?: boolean;
statusText?: string;
roles?: Array<string>;
type: 'user' | 'bot';
bio?: string;
services?: Record<string, Record<string, any>>;
customFields?: Record<string, any>;
}

@ -63,6 +63,10 @@ export interface IRoom extends IRocketChatRecord {
muted?: string[];
}
export interface ICreatedRoom extends IRoom {
rid: string;
}
export interface IDirectMessageRoom extends Omit<IRoom, 'default' | 'featured' | 'u' | 'name'> {
t: 'd';
uids: Array<string>;

@ -2,6 +2,7 @@ import { FilterQuery } from 'mongodb';
export type SettingId = string;
export type GroupId = SettingId;
export type TabId = SettingId;
export type SectionName = string;
export enum SettingEditor {
@ -25,6 +26,7 @@ export interface ISetting {
env: boolean;
group?: GroupId;
section?: SectionName;
tab?: TabId;
i18nLabel: string;
value: SettingValue;
packageValue: SettingValue;
@ -32,6 +34,7 @@ export interface ISetting {
packageEditor?: SettingEditor;
blocked: boolean;
enableQuery?: string | FilterQuery<ISetting> | FilterQuery<ISetting>[];
displayQuery?: string | FilterQuery<ISetting> | FilterQuery<ISetting>[];
sorter?: number;
properties?: unknown;
enterprise?: boolean;

@ -33,6 +33,13 @@ export interface IUserEmailCode {
type LoginToken = IMeteorLoginToken & IPersonalAccessToken;
export type Username = string;
export type ILoginUsername = {
username: string;
} | {
email: string;
}
export type LoginUsername = string | ILoginUsername;
export interface IUserServices {
password?: {
bcrypt: string;
@ -64,6 +71,10 @@ export interface IUserServices {
idpSession?: string;
nameID?: string;
};
ldap?: {
id: string;
idAttribute?: string;
};
}
export interface IUserEmail {
@ -129,6 +140,7 @@ export interface IUser extends IRocketChatRecord {
};
settings?: IUserSettings;
defaultRoom?: string;
ldap?: boolean;
}
export type IUserDataEvent = {

@ -0,0 +1,4 @@
declare module 'ldap-escape' {
export function filter(strings: TemplateStringsArray, ...values: string[]): string;
export function dn(strings: TemplateStringsArray, ...values: string[]): string;
}

@ -0,0 +1,12 @@
export interface ILDAPCallback {
(error?: Error | null, result?: any): void;
}
export interface ILDAPPageData {
end: boolean;
next: Function | undefined;
}
export interface ILDAPPageCallback {
(error?: Error | null, result?: any, page?: ILDAPPageData): void;
}

@ -0,0 +1,3 @@
export interface ILDAPEntry extends Record<string, any> {
_raw: Record<string, any>;
}

@ -0,0 +1,6 @@
export interface ILDAPLoginRequest {
ldap?: boolean;
ldapOptions?: Record<string, any>;
username: string;
ldapPass: string;
}

@ -0,0 +1,5 @@
export interface ILDAPLoginResult extends Record<string, any> {
userId?: string;
}
export type LDAPLoginResult = ILDAPLoginResult | undefined;

@ -0,0 +1,27 @@
export type LDAPEncryptionType = 'plain' | 'tls' | 'ssl';
export type LDAPSearchScope = 'base' | 'one' | 'sub';
export interface ILDAPConnectionOptions {
host?: string;
port: number;
reconnect: boolean;
timeout: number;
connectionTimeout: number;
idleTimeout: number;
encryption: LDAPEncryptionType;
caCert?: string;
rejectUnauthorized: boolean;
baseDN: string;
userSearchFilter: string;
userSearchScope: LDAPSearchScope;
userSearchField: string;
searchPageSize: number;
searchSizeLimit: number;
uniqueIdentifierField?: string;
groupFilterEnabled: boolean;
groupFilterObjectClass?: string;
groupFilterGroupIdAttribute?: string;
groupFilterGroupMemberAttribute?: string;
groupFilterGroupMemberFormat?: string;
groupFilterGroupName?: string;
}

@ -0,0 +1,4 @@
export type ILDAPUniqueIdentifierField = {
attribute: string;
value: string;
}

@ -0,0 +1,3 @@
declare module 'meteor/sha' {
function SHA256(input: string): string;
}

@ -1,17 +1,15 @@
import { settings } from '../../../../app/settings';
export const createSettings = () => {
settings.addGroup('Canned_Responses', function() {
this.section('Canned_Responses', function() {
this.add('Canned_Responses_Enable', false, {
type: 'boolean',
public: true,
enterprise: true,
invalidValue: false,
modules: [
'canned-responses',
],
});
});
settings.add('Canned_Responses_Enable', true, {
group: 'Omnichannel',
section: 'Canned_Responses',
type: 'boolean',
public: true,
enterprise: true,
invalidValue: false,
modules: [
'canned-responses',
],
});
};

@ -1,22 +0,0 @@
# LDAP-Enterprise
This package enables the administrator option to map the roles used on your LDAP server with the Rocket.Chat server roles.
With the correspondent license for this product, a new "Roles" section will be enabled in the admin panel in the LDAP group,
where the administrator can map LDAP roles to Rocket.Chat roles`(Admin panel -> LDAP -> Roles)`, following the pattern described below:
```
{
"ldapRole": "["admin", "guest"], //must be an array of valid Rocket.Chat Users Roles
"anotherLdapRole": "["anonymous"]
}
```
**Note:** If some mapping error occurs, be aware of the server log, that the error will be shown.
<br/>
<br/>
In addition to the options described above, still in the same section, there are other options such as:
* `LDAP_Enable_LDAP_Roles_To_RC_Roles`: Enable or disable this feature;
* `LDAP_Validate_Roles_For_Each_Login`: If the validation should occurs for each login (**Be careful with this setting because it will overwrite
the user roles in each login, otherwise this will be validated only at the moment of user creation**);
* `LDAP_Default_Role_To_User`: The default Rocket.Chat role to be defined, if any LDAP role that the user has, is not mapped;
* `LDAP_Query_To_Get_User_Groups`: The LDAP query to get the LDAP groups that the user is part of;

@ -1,11 +0,0 @@
export const beforeSearchAll = (searchParams) => {
const { options } = searchParams;
if (!Array.isArray(options.attributes)) {
options.attributes = options.attributes ? [options.attributes] : ['*'];
}
options.attributes.push('pwdAccountLockedTime');
return searchParams;
};

@ -1,19 +0,0 @@
import { logger } from '../../../../../app/ldap/server/sync';
import { setUserActiveStatus } from '../../../../../app/lib/server/functions/setUserActiveStatus';
import { settings } from '../../../../../app/settings';
export const syncExistentUser = ({ ldapUser, user }) => {
const activate = !!ldapUser && !ldapUser.pwdAccountLockedTime;
if (activate === user.active) {
return;
}
const syncUserState = settings.get('LDAP_Sync_User_Active_State');
if (syncUserState === 'none' || (syncUserState === 'disable' && activate)) {
return;
}
setUserActiveStatus(user._id, activate);
logger.info(`${ activate ? 'Activating' : 'Deactivating' } user ${ user.name } (${ user._id })`);
};

@ -1,69 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { syncExistentUser } from './hooks/syncExistentUser';
import { beforeSearchAll } from './hooks/beforeSearchAll';
import { callbacks } from '../../../../app/callbacks/server';
import { settings } from '../../../../app/settings';
import { onLicense } from '../../license/server';
onLicense('ldap-enterprise', () => {
const { createSettings } = require('./settings');
const { validateLDAPRolesMappingChanges, validateLDAPTeamsMappingChanges } = require('./ldapEnterprise');
const { onLdapLogin } = require('./listener');
Meteor.startup(function() {
createSettings();
validateLDAPRolesMappingChanges();
validateLDAPTeamsMappingChanges();
let LDAP_Enable_LDAP_Roles_To_RC_Roles;
let LDAP_Enable_LDAP_Groups_To_RC_Teams;
let callbackEnabled = false;
let LDAP_Sync_User_Active_State;
const updateCallbackState = () => {
if (callbackEnabled) {
if (!LDAP_Enable_LDAP_Roles_To_RC_Roles && !LDAP_Enable_LDAP_Groups_To_RC_Teams) {
callbacks.remove('afterLDAPLogin', 'checkRoleMapping');
callbackEnabled = false;
}
return;
}
if (LDAP_Enable_LDAP_Roles_To_RC_Roles || LDAP_Enable_LDAP_Groups_To_RC_Teams) {
callbackEnabled = true;
callbacks.add('afterLDAPLogin', onLdapLogin, callbacks.priority.MEDIUM, 'checkRoleMapping');
}
};
settings.get('LDAP_Enable_LDAP_Roles_To_RC_Roles', (key, value) => {
LDAP_Enable_LDAP_Roles_To_RC_Roles = value;
updateCallbackState();
});
settings.get('LDAP_Enable_LDAP_Groups_To_RC_Teams', (key, value) => {
LDAP_Enable_LDAP_Groups_To_RC_Teams = value;
updateCallbackState();
});
settings.get('LDAP_Sync_User_Active_State', (key, value) => {
if (LDAP_Sync_User_Active_State === value) {
return;
}
if (value === 'none') {
// If it changed to 'none', disable
callbacks.remove('ldap.afterSyncExistentUser', 'ldap-sync-user-active-state');
} else if (LDAP_Sync_User_Active_State === 'none' || !LDAP_Sync_User_Active_State) {
// If it changed from 'none' to something else, enable
callbacks.add('ldap.afterSyncExistentUser', syncExistentUser, callbacks.priority.MEDIUM, 'ldap-sync-user-active-state');
}
LDAP_Sync_User_Active_State = value;
});
callbacks.add('ldap.beforeSearchAll', beforeSearchAll, callbacks.priority.MEDIUM, 'ldap-return-attribute-AccountLockedTime');
});
});

@ -1,144 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Roles } from '../../../../app/models';
import { Logger } from '../../../../app/logger';
import { settings } from '../../../../app/settings';
import { Team } from '../../../../server/sdk';
const logger = new Logger('ldapEnterprise');
const mustBeAnArrayOfStrings = (array) => Array.isArray(array) && array.length && array.every((item) => typeof item === 'string');
const validateRoleMapping = (mappedRoles) => {
const allRocketChatUserRoles = Roles.find({ scope: 'Users' }).fetch().map((role) => role._id);
const mappedRocketChatRoles = Object.values(mappedRoles);
const validRolesMapping = mappedRocketChatRoles.every((roles) => roles.every((role) => allRocketChatUserRoles.includes(role)));
if (!validRolesMapping) {
throw new Error('Please verify your mapping for LDAP X RocketChat Roles. There is some invalid Rocket Chat Role.');
}
};
const validateLDAPRolesMappingStructure = (mappedRoles) => {
const mappedRocketChatRoles = Object.values(mappedRoles);
const validStructureMapping = mappedRocketChatRoles.every(mustBeAnArrayOfStrings);
if (!validStructureMapping) {
throw new Error('Please verify your mapping for LDAP X RocketChat Roles. The structure is invalid, the structure should be an object like: {key: LdapRole, value: [An array of rocket.chat roles]}');
}
};
const validateLDAPTeamsMappingStructure = (mappedTeams) => {
const mappedRocketChatTeams = Object.values(mappedTeams);
const validStructureMapping = mappedRocketChatTeams.every(mustBeAnArrayOfStrings);
if (!validStructureMapping) {
throw new Error('Please verify your mapping for LDAP X RocketChat Teams. The structure is invalid, the structure should be an object like: {key: LdapTeam, value: [An array of rocket.chat teams]}');
}
};
export const getLdapRolesByUsername = (username, ldap) => {
const searchOptions = {
filter: settings.get('LDAP_Query_To_Get_User_Groups').replace(/#{username}/g, username),
scope: ldap.options.User_Search_Scope || 'sub',
sizeLimit: ldap.options.Search_Size_Limit,
};
const getLdapRoles = (ldapUserGroups) => ldapUserGroups.filter((field) => field && field.ou).map((field) => field.ou);
const ldapUserGroups = ldap.searchAllSync(ldap.options.BaseDN, searchOptions);
return Array.isArray(ldapUserGroups) ? getLdapRoles(ldapUserGroups) : [];
};
export const getLdapTeamsByUsername = (username, ldap) => {
const searchOptions = {
filter: settings.get('LDAP_Query_To_Get_User_Teams').replace(/#{username}/g, username),
scope: ldap.options.User_Search_Scope || 'sub',
sizeLimit: ldap.options.Search_Size_Limit,
};
const ldapUserGroups = ldap.searchAllSync(ldap.options.BaseDN, searchOptions);
if (!Array.isArray(ldapUserGroups)) {
return [];
}
return ldapUserGroups.filter((field) => field && field.ou).map((field) => field.ou).flat();
};
export const getRocketChatRolesByLdapRoles = (mappedRoles, ldapUserRoles) => {
const mappedLdapRoles = Object.keys(mappedRoles);
if (!ldapUserRoles.length) {
logger.error('The LDAP user has no role, so we set the default role value');
return [settings.get('LDAP_Default_Role_To_User')];
}
const unmappedLdapRoles = ldapUserRoles.filter((ldapRole) => !mappedLdapRoles.includes(ldapRole));
const getRocketChatMappedRoles = (acc, role) => acc.concat(mappedRoles[role]);
const removeRepeatedRoles = (acc, role) => (acc.includes(role) ? acc : acc.concat(role));
if (unmappedLdapRoles.length) {
logger.error(`The following LDAP roles is/are not mapped in Rocket.Chat: "${ unmappedLdapRoles.join(', ') }". Because it, we set the default LDAP role.`);
return [settings.get('LDAP_Default_Role_To_User')];
}
return ldapUserRoles
.reduce(getRocketChatMappedRoles, [])
.reduce(removeRepeatedRoles, []);
};
export const getRocketChatTeamsByLdapTeams = (mappedTeams, ldapUserTeams) => {
const mappedLdapTeams = Object.keys(mappedTeams);
const filteredTeams = ldapUserTeams.filter((ldapTeam) => mappedLdapTeams.includes(ldapTeam));
if (filteredTeams.length < ldapUserTeams.length) {
const unmappedLdapTeams = ldapUserTeams.filter((ldapRole) => !mappedLdapTeams.includes(ldapRole));
logger.error(`The following LDAP teams are not mapped in Rocket.Chat: "${ unmappedLdapTeams.join(', ') }".`);
}
if (!filteredTeams.length) {
return [];
}
return [...new Set(filteredTeams.map((ldapTeam) => mappedTeams[ldapTeam]).flat())];
};
export const updateUserUsingMappedLdapRoles = (userId, roles) => {
Meteor.users.update({ _id: userId }, { $set: { roles } });
};
async function updateUserUsingMappedLdapTeamsAsync(userId, teamNames, map) {
const allTeamNames = [...new Set(Object.values(map).flat())];
const allTeams = await Team.listByNames(allTeamNames, { projection: { _id: 1, name: 1 } });
const inTeamIds = allTeams.filter(({ name }) => teamNames.includes(name)).map(({ _id }) => _id);
const notInTeamIds = allTeams.filter(({ name }) => !teamNames.includes(name)).map(({ _id }) => _id);
const currentTeams = await Team.listTeamsBySubscriberUserId(userId, { projection: { teamId: 1 } });
const currentTeamIds = await currentTeams.map(({ teamId }) => teamId);
const teamsToRemove = currentTeamIds.filter((teamId) => notInTeamIds.includes(teamId));
const teamsToAdd = inTeamIds.filter((teamId) => !currentTeamIds.includes(teamId));
await Team.insertMemberOnTeams(userId, teamsToAdd);
await Team.removeMemberFromTeams(userId, teamsToRemove);
}
export const updateUserUsingMappedLdapTeams = (userId, teamNames, map) => Promise.await(updateUserUsingMappedLdapTeamsAsync(userId, teamNames, map));
export const validateLDAPRolesMappingChanges = () => {
settings.get('LDAP_Roles_To_Rocket_Chat_Roles', (key, value) => {
try {
if (value) {
const mappedRoles = JSON.parse(value);
validateLDAPRolesMappingStructure(mappedRoles);
validateRoleMapping(mappedRoles);
}
} catch (error) {
logger.error(error);
}
});
};
export const validateLDAPTeamsMappingChanges = () => {
settings.get('LDAP_Groups_To_Rocket_Chat_Teams', (key, value) => {
try {
if (value) {
const mappedTeams = JSON.parse(value);
validateLDAPTeamsMappingStructure(mappedTeams);
}
} catch (error) {
logger.error(error);
}
});
};

@ -1,30 +0,0 @@
import { settings } from '../../../../app/settings';
import {
getLdapRolesByUsername,
getRocketChatRolesByLdapRoles,
updateUserUsingMappedLdapRoles,
getLdapTeamsByUsername,
getRocketChatTeamsByLdapTeams,
updateUserUsingMappedLdapTeams,
} from './ldapEnterprise';
export const onLdapLogin = ({ user, ldapUser, ldap }) => {
const userExists = user._id;
const userId = userExists ? user._id : user.userId;
const mapRoles = settings.get('LDAP_Enable_LDAP_Roles_To_RC_Roles') && (!userExists || settings.get('LDAP_Validate_Roles_For_Each_Login'));
const mapTeams = settings.get('LDAP_Enable_LDAP_Groups_To_RC_Teams') && (!userExists || settings.get('LDAP_Validate_Teams_For_Each_Login'));
if (mapRoles) {
const ldapUserRoles = getLdapRolesByUsername(ldapUser.uid, ldap);
const roles = getRocketChatRolesByLdapRoles(JSON.parse(settings.get('LDAP_Roles_To_Rocket_Chat_Roles')), ldapUserRoles);
updateUserUsingMappedLdapRoles(userId, roles);
}
if (mapTeams) {
const ldapUserTeams = getLdapTeamsByUsername(ldapUser.uid, ldap);
const map = JSON.parse(settings.get('LDAP_Groups_To_Rocket_Chat_Teams'));
const teams = getRocketChatTeamsByLdapTeams(map, ldapUserTeams);
updateUserUsingMappedLdapTeams(userId, teams, map);
}
};

@ -1,112 +0,0 @@
import { settings } from '../../../../app/settings';
import { Roles } from '../../../../app/models';
export const createSettings = () => {
settings.addGroup('LDAP', function() {
this.section('Role_Mapping', function() {
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',
],
});
});
this.section('Team_Mapping', function() {
this.add('LDAP_Enable_LDAP_Groups_To_RC_Teams', false, {
type: 'boolean',
enableQuery: { _id: 'LDAP_Enable', value: true },
enterprise: true,
invalidValue: false,
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Groups_To_Rocket_Chat_Teams', '{}', {
type: 'code',
enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true },
enterprise: true,
invalidValue: '{}',
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Validate_Teams_For_Each_Login', false, {
type: 'boolean',
enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true },
enterprise: true,
invalidValue: false,
modules: [
'ldap-enterprise',
],
});
this.add('LDAP_Query_To_Get_User_Teams', '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))', {
type: 'string',
enableQuery: { _id: 'LDAP_Enable_LDAP_Groups_To_RC_Teams', value: true },
enterprise: true,
invalidValue: '(&(ou=*)(uniqueMember=uid=#{username},dc=example,dc=com))',
modules: [
'ldap-enterprise',
],
});
});
this.section('LDAP_Advanced_Sync', function() {
this.add('LDAP_Sync_User_Active_State', 'disable', {
type: 'select',
values: [
{ key: 'none', i18nLabel: 'LDAP_Sync_User_Active_State_Nothing' },
{ key: 'disable', i18nLabel: 'LDAP_Sync_User_Active_State_Disable' },
{ key: 'both', i18nLabel: 'LDAP_Sync_User_Active_State_Both' },
],
i18nDescription: 'LDAP_Sync_User_Active_State_Description',
enableQuery: { _id: 'LDAP_Enable', value: true },
enterprise: true,
invalidValue: 'none',
modules: [
'ldap-enterprise',
],
});
});
});
};

@ -1,32 +1,64 @@
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../../../app/callbacks';
import { callbacks } from '../../../../../app/callbacks/server';
import { Users } from '../../../../../app/models/server/raw';
import { settings } from '../../../../../app/settings';
import { settings } from '../../../../../app/settings/server';
import { getMaxNumberSimultaneousChat } from '../lib/Helper';
import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper';
import { cbLogger } from '../lib/logger';
import { Livechat } from '../../../../../app/livechat/server';
callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({ agent, inquiry, options }) => {
if (!settings.get('Livechat_waiting_queue')) {
cbLogger.debug('Skipping callback. Disabled by setting');
return agent;
callbacks.add('livechat.checkAgentBeforeTakeInquiry', async ({
agent,
inquiry,
options,
}: {
agent: {
agentId: string;
};
inquiry: {
_id: string;
department: string;
};
options: {
forwardingToDepartment? : {
oldDepartmentId: string;
transferData: any;
};
clientAction? : boolean;
};
}) => {
if (!inquiry?._id || !agent?.agentId) {
cbLogger.debug('Callback with error. No inquiry or agent provided');
return null;
}
const {
agentId,
} = agent;
if (!inquiry || !agent) {
cbLogger.debug('Callback with error. No inquiry or agent provided');
if (!Livechat.checkOnlineAgents(null, agent)) {
cbLogger.debug('Callback with error. provided agent is not online');
return null;
}
if (!settings.get('Livechat_waiting_queue')) {
cbLogger.debug('Skipping callback. Disabled by setting');
return agent;
}
if (allowAgentSkipQueue(agent)) {
cbLogger.debug(`Callback success. Agent ${ agent.agentId } can skip queue`);
return agent;
}
const { department: departmentId } = inquiry;
const { agentId } = agent;
const {
department: departmentId,
} = inquiry;
const maxNumberSimultaneousChat = getMaxNumberSimultaneousChat({ agentId, departmentId });
const maxNumberSimultaneousChat = getMaxNumberSimultaneousChat({
agentId,
departmentId,
});
if (maxNumberSimultaneousChat === 0) {
cbLogger.debug(`Callback success. Agent ${ agentId } max number simultaneous chats on range`);
return agent;

@ -3,6 +3,7 @@ import { LivechatInquiry, Subscriptions, LivechatRooms } from '../../../../../ap
import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager';
import { settings } from '../../../../../app/settings/server';
import { cbLogger } from '../lib/logger';
import { dispatchAgentDelegated } from '../../../../../app/livechat/server/lib/Helper';
const handleOnAgentAssignmentFailed = async ({ inquiry, room, options }: { inquiry: any; room: any; options: { forwardingToDepartment?: { oldDepartmentId: string; transferData: any }; clientAction?: boolean} }): Promise<any> => {
if (!inquiry || !room) {
@ -15,10 +16,12 @@ const handleOnAgentAssignmentFailed = async ({ inquiry, room, options }: { inqui
const { _id: roomId } = room;
const { _id: inquiryId } = inquiry;
LivechatInquiry.readyInquiry(inquiryId);
LivechatInquiry.queueInquiry(inquiryId);
LivechatInquiry.removeDefaultAgentById(inquiryId);
LivechatRooms.removeAgentByRoomId(roomId);
Subscriptions.removeByRoomId(roomId);
dispatchAgentDelegated(roomId, null);
const newInquiry = LivechatInquiry.findOneById(inquiryId);
await queueInquiry(room, newInquiry);

@ -63,7 +63,7 @@ const CannedResponseList: FC<{
return (
<>
<VerticalBar.Header>
<VerticalBar.Text>{t('Canned Responses')}</VerticalBar.Text>
<VerticalBar.Text>{t('Canned_Responses')}</VerticalBar.Text>
<VerticalBar.Close onClick={onClose} />
</VerticalBar.Header>

@ -0,0 +1,5 @@
export interface ILDAPEEConnectionOptions {
authentication: boolean;
authenticationUserDN: string;
authenticationPassword: string;
}

@ -1 +1,2 @@
import './ldap';
import './licenses';

@ -0,0 +1,31 @@
import { hasRole } from '../../../app/authorization/server';
import { settings } from '../../../app/settings/server';
import { API } from '../../../app/api/server/api';
import { LDAPEE } from '../sdk';
import { hasLicense } from '../../app/license/server/license';
API.v1.addRoute('ldap.syncNow', { authRequired: true }, {
post() {
if (!this.userId) {
throw new Error('error-invalid-user');
}
if (!hasRole(this.userId, 'admin')) {
throw new Error('error-not-authorized');
}
if (!hasLicense('ldap-enterprise')) {
throw new Error('error-not-authorized');
}
if (settings.get('LDAP_Enable') !== true) {
throw new Error('LDAP_disabled');
}
LDAPEE.sync();
return API.v1.success({
message: 'Sync_in_progress',
});
},
});

@ -0,0 +1,57 @@
import { Meteor } from 'meteor/meteor';
import { Promise } from 'meteor/promise';
import _ from 'underscore';
import { LDAPEE } from '../sdk';
import { settings } from '../../../app/settings/server';
import { logger } from '../../../server/lib/ldap/Logger';
import { cronJobs } from '../../../app/utils/server/lib/cron/Cronjobs';
import { LDAPEEConnection } from '../lib/ldap/Connection';
import { LDAPEEManager } from '../lib/ldap/Manager';
import { callbacks } from '../../../app/callbacks/server';
import type { LDAPConnection } from '../../../server/lib/ldap/Connection';
import type { IImportUser } from '../../../definition/IImportUser';
import type { ILDAPEntry } from '../../../definition/ldap/ILDAPEntry';
import { onLicense } from '../../app/license/server';
onLicense('ldap-enterprise', () => {
// Configure background sync cronjob
const jobName = 'LDAP_Sync';
const addCronJob = _.debounce(Meteor.bindEnvironment(function addCronJobDebounced() {
if (settings.get('LDAP_Background_Sync') !== true) {
logger.info('Disabling LDAP Background Sync');
if (cronJobs.nextScheduledAtDate(jobName)) {
cronJobs.remove(jobName);
}
return;
}
const schedule = settings.get<string>('LDAP_Background_Sync_Interval');
if (schedule) {
logger.info('Enabling LDAP Background Sync');
cronJobs.add(jobName, schedule, () => Promise.await(LDAPEE.sync()), 'text');
}
}), 500);
Meteor.defer(() => {
settings.get('LDAP_Background_Sync', addCronJob);
settings.get('LDAP_Background_Sync_Interval', addCronJob);
settings.get('LDAP_Groups_To_Rocket_Chat_Teams', (_key, value) => {
try {
LDAPEEManager.validateLDAPTeamsMappingChanges(value as string);
} catch (error) {
logger.error(error);
}
});
});
callbacks.add('getLDAPConnectionClass', function(): typeof LDAPConnection {
return LDAPEEConnection;
}, callbacks.priority.HIGH, 'replaceLDAPConnectionClass');
callbacks.add('mapLDAPUserData', (userData: IImportUser, ldapUser: ILDAPEntry) => {
LDAPEEManager.copyCustomFields(ldapUser, userData);
LDAPEEManager.copyActiveState(ldapUser, userData);
}, callbacks.priority.MEDIUM, 'mapLDAPCustomFields');
});

@ -7,8 +7,9 @@ import '../app/auditing/server/index';
import '../app/authorization/server/index';
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';
import '../app/teams-mention/server/index';
import './api';
import './local-services/ldap/service';
import './configuration/ldap';

@ -0,0 +1,65 @@
import ldapjs from 'ldapjs';
import { LDAPConnection } from '../../../../server/lib/ldap/Connection';
import { logger, bindLogger } from '../../../../server/lib/ldap/Logger';
import { settings } from '../../../../app/settings/server';
import type { ILDAPEEConnectionOptions } from '../../../definition/ldap/ILDAPEEOptions';
export class LDAPEEConnection extends LDAPConnection {
public eeOptions: ILDAPEEConnectionOptions;
private usingAuthentication: boolean;
constructor() {
super();
this.eeOptions = {
authentication: settings.get<boolean>('LDAP_Authentication') ?? false,
authenticationUserDN: settings.get<string>('LDAP_Authentication_UserDN') ?? '',
authenticationPassword: settings.get<string>('LDAP_Authentication_Password') ?? '',
};
}
/*
Bind UserDN and Password if specified and not yet bound
*/
public async maybeBindDN(): Promise<void> {
if (this.usingAuthentication) {
return;
}
if (!this.eeOptions.authentication) {
return;
}
if (!this.eeOptions.authenticationUserDN) {
logger.error('Invalid UserDN for authentication');
return;
}
bindLogger.info({ msg: 'Binding UserDN', userDN: this.eeOptions.authenticationUserDN });
await this.bindDN(this.eeOptions.authenticationUserDN, this.eeOptions.authenticationPassword);
this.usingAuthentication = true;
}
public disconnect(): void {
this.usingAuthentication = false;
super.disconnect();
}
public async testConnection(): Promise<void> {
await super.testConnection();
await this.maybeBindDN();
}
protected async runBeforeSearch(searchOptions: ldapjs.SearchOptions): Promise<void> {
await this.maybeBindDN();
if (!Array.isArray(searchOptions.attributes)) {
searchOptions.attributes = searchOptions.attributes ? [searchOptions.attributes] : ['*'];
}
searchOptions.attributes.push('pwdAccountLockedTime');
super.runBeforeSearch(searchOptions);
}
}

@ -0,0 +1,501 @@
import _ from 'underscore';
import type ldapjs from 'ldapjs';
import { ILDAPEntry } from '../../../../definition/ldap/ILDAPEntry';
import type { IUser } from '../../../../definition/IUser';
import type { IRoom, ICreatedRoom } from '../../../../definition/IRoom';
import type { IRole } from '../../../../definition/IRole';
import { IImportUser } from '../../../../definition/IImportUser';
import { ImporterAfterImportCallback } from '../../../../app/importer/server/definitions/IConversionCallbacks';
import { settings } from '../../../../app/settings/server';
import { Roles, Rooms } from '../../../../app/models/server';
import {
Users as UsersRaw,
Roles as RolesRaw,
Subscriptions as SubscriptionsRaw,
} from '../../../../app/models/server/raw';
import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter';
import type { LDAPConnection } from '../../../../server/lib/ldap/Connection';
import { LDAPManager } from '../../../../server/lib/ldap/Manager';
import { logger } from '../../../../server/lib/ldap/Logger';
import { templateVarHandler } from '../../../../app/utils/lib/templateVarHandler';
import { LDAPEEConnection } from './Connection';
import { api } from '../../../../server/sdk/api';
import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions';
import { Team } from '../../../../server/sdk';
export class LDAPEEManager extends LDAPManager {
public static async sync(): Promise<void> {
if (settings.get('LDAP_Enable') !== true) {
return;
}
const options = this.getConverterOptions();
const ldap = new LDAPEEConnection();
const converter = new LDAPDataConverter(true, options);
try {
await ldap.connect();
try {
const createNewUsers = settings.get<boolean>('LDAP_Background_Sync_Import_New_Users') ?? true;
const updateExistingUsers = settings.get<boolean>('LDAP_Background_Sync_Keep_Existant_Users_Updated') ?? true;
if (createNewUsers) {
await this.importNewUsers(ldap, converter, updateExistingUsers);
} else if (updateExistingUsers) {
await this.updateExistingUsers(ldap, converter);
}
} finally {
ldap.disconnect();
}
converter.convertUsers({
afterImportFn: ((data: IImportUser, _type: string, isNewRecord: boolean): void => Promise.await(this.advancedSync(ldap, data, converter, isNewRecord))) as ImporterAfterImportCallback,
});
} catch (error) {
logger.error(error);
}
}
public static validateLDAPTeamsMappingChanges(json: string): void {
if (!json) {
return;
}
const mustBeAnArrayOfStrings = (array: Array<string>): boolean => Boolean(Array.isArray(array) && array.length && array.every((item) => typeof item === 'string'));
const mappedTeams = this.parseJson(json);
if (!mappedTeams) {
return;
}
const mappedRocketChatTeams = Object.values(mappedTeams);
const validStructureMapping = mappedRocketChatTeams.every(mustBeAnArrayOfStrings);
if (!validStructureMapping) {
throw new Error('Please verify your mapping for LDAP X RocketChat Teams. The structure is invalid, the structure should be an object like: {key: LdapTeam, value: [An array of rocket.chat teams]}');
}
}
private static async advancedSync(ldap: LDAPConnection, importUser: IImportUser, converter: LDAPDataConverter, isNewRecord: boolean): Promise<void> {
const user = converter.findExistingUser(importUser);
if (!user || user.username) {
return;
}
const dn = importUser.importIds[0];
await this.syncUserRoles(ldap, user, dn);
await this.syncUserChannels(ldap, user, dn);
await this.syncUserTeams(ldap, user, isNewRecord);
}
private static async isUserInGroup(ldap: LDAPConnection, baseDN: string, filter: string, { dn, username }: { dn: string; username: string }, groupName: string): Promise<boolean> {
if (!filter || !baseDN) {
logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.');
return false;
}
const searchOptions: ldapjs.SearchOptions = {
filter: filter.replace(/#{username}/g, username).replace(/#{groupName}/g, groupName).replace(/#{userdn}/g, dn),
scope: 'sub',
};
const result = await ldap.searchRaw(baseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
logger.debug(`${ username } is not in ${ groupName } group!!!`);
} else {
logger.debug(`${ username } is in ${ groupName } group.`);
return true;
}
return false;
}
private static parseJson(json: string): Record<string, any> | undefined {
try {
return JSON.parse(json);
} catch (err) {
logger.error(`Unexpected error : ${ err.message }`);
}
}
private static broadcastRoleChange(type: string, _id: string, uid: string, username: string): void {
// #ToDo: would be better to broadcast this only once for all users and roles, or at least once by user.
if (!settings.get('UI_DisplayRoles')) {
return;
}
api.broadcast('user.roleUpdate', {
type,
_id,
u: {
_id: uid,
username,
},
});
}
private static async syncUserRoles(ldap: LDAPConnection, user: IUser, dn: string): Promise<void> {
const { username } = user;
if (!username) {
logger.debug('User has no username');
return;
}
const syncUserRoles = settings.get<boolean>('LDAP_Sync_User_Data_Roles') ?? false;
const syncUserRolesAutoRemove = settings.get<boolean>('LDAP_Sync_User_Data_Roles_AutoRemove') ?? false;
const syncUserRolesFieldMap = (settings.get<string>('LDAP_Sync_User_Data_RolesMap') ?? '').trim();
const syncUserRolesFilter = (settings.get<string>('LDAP_Sync_User_Data_Roles_Filter') ?? '').trim();
const syncUserRolesBaseDN = (settings.get<string>('LDAP_Sync_User_Data_Roles_BaseDN') ?? '').trim();
if (!syncUserRoles || !syncUserRolesFieldMap) {
logger.debug('not syncing user roles');
return;
}
const roles = await RolesRaw.find({}, {
fields: {
_updatedAt: 0,
},
}).toArray() as Array<IRole>;
if (!roles) {
return;
}
const fieldMap = this.parseJson(syncUserRolesFieldMap);
if (!fieldMap) {
return;
}
Object.keys(fieldMap).forEach(async (ldapField) => {
if (!fieldMap.hasOwnProperty(ldapField)) {
return;
}
const userField = fieldMap[ldapField];
const [roleName] = userField.split(/\.(.+)/);
if (!_.find<IRole>(roles, (el) => el._id === roleName)) {
logger.debug(`User Role doesn't exist: ${ roleName }`);
return;
}
logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`);
if (await this.isUserInGroup(ldap, syncUserRolesBaseDN, syncUserRolesFilter, { dn, username }, ldapField)) {
if (Roles.addUserRoles(user._id, roleName)) {
this.broadcastRoleChange('added', roleName, user._id, username);
}
logger.debug(`Synced user group ${ roleName } from LDAP for ${ user.username }`);
return;
}
if (!syncUserRolesAutoRemove) {
return;
}
if (Roles.removeUserRoles(user._id, roleName)) {
this.broadcastRoleChange('removed', roleName, user._id, username);
}
});
}
private static createRoomForSync(channel: string): IRoom | undefined {
logger.debug(`Channel '${ channel }' doesn't exist, creating it.`);
const roomOwner = settings.get('LDAP_Sync_User_Data_Channels_Admin') || '';
// #ToDo: Remove typecastings when createRoom is converted to ts.
const room = createRoom('c', channel, roomOwner, [], false, { customFields: { ldap: true } } as any) as unknown as ICreatedRoom | undefined;
if (!room?.rid) {
logger.error(`Unable to auto-create channel '${ channel }' during ldap sync.`);
return;
}
room._id = room.rid;
return room;
}
private static async syncUserChannels(ldap: LDAPConnection, user: IUser, dn: string): Promise<void> {
const syncUserChannels = settings.get<boolean>('LDAP_Sync_User_Data_Channels') ?? false;
const syncUserChannelsRemove = settings.get<boolean>('LDAP_Sync_User_Data_Channels_Enforce_AutoChannels') ?? false;
const syncUserChannelsFieldMap = (settings.get<string>('LDAP_Sync_User_Data_ChannelsMap') ?? '').trim();
const syncUserChannelsFilter = (settings.get<string>('LDAP_Sync_User_Data_Channels_Filter') ?? '').trim();
const syncUserChannelsBaseDN = (settings.get<string>('LDAP_Sync_User_Data_Channels_BaseDN') ?? '').trim();
if (!syncUserChannels || !syncUserChannelsFieldMap) {
logger.debug('not syncing groups to channels');
return;
}
const fieldMap = this.parseJson(syncUserChannelsFieldMap);
if (!fieldMap) {
return;
}
const username = user.username as string;
_.map(fieldMap, (channels, ldapField) => {
if (!Array.isArray(channels)) {
channels = [channels];
}
channels.forEach(async (channel: string) => {
const room: IRoom | undefined = Rooms.findOneByNonValidatedName(channel) || this.createRoomForSync(channel);
if (!room) {
return;
}
if (await this.isUserInGroup(ldap, syncUserChannelsBaseDN, syncUserChannelsFilter, { dn, username }, ldapField)) {
if (room.teamMain) {
logger.error(`Can't add user to channel ${ channel } because it is a team.`);
} else {
addUserToRoom(room._id, user);
logger.debug(`Synced user channel ${ room._id } from LDAP for ${ username }`);
}
} else if (syncUserChannelsRemove && !room.teamMain) {
const subscription = await SubscriptionsRaw.findOneByRoomIdAndUserId(room._id, user._id);
if (subscription) {
removeUserFromRoom(room._id, user);
}
}
});
});
}
private static async syncUserTeams(ldap: LDAPConnection, user: IUser, isNewRecord: boolean): Promise<void> {
if (!user.username) {
return;
}
const mapTeams = settings.get<boolean>('LDAP_Enable_LDAP_Groups_To_RC_Teams') && (isNewRecord || settings.get<boolean>('LDAP_Validate_Teams_For_Each_Login'));
if (!mapTeams) {
return;
}
const ldapUserTeams = await this.getLdapTeamsByUsername(ldap, user.username);
const mapJson = settings.get<string>('LDAP_Groups_To_Rocket_Chat_Teams');
if (!mapJson) {
return;
}
const map = this.parseJson(mapJson) as Record<string, string>;
if (!map) {
return;
}
const teamNames = this.getRocketChatTeamsByLdapTeams(map, ldapUserTeams);
const allTeamNames = [...new Set(Object.values(map).flat())];
const allTeams = await Team.listByNames(allTeamNames, { projection: { _id: 1, name: 1 } });
const inTeamIds = allTeams.filter(({ name }) => teamNames.includes(name)).map(({ _id }) => _id);
const notInTeamIds = allTeams.filter(({ name }) => !teamNames.includes(name)).map(({ _id }) => _id);
const currentTeams = await Team.listTeamsBySubscriberUserId(user._id, { projection: { teamId: 1 } });
const currentTeamIds = currentTeams && currentTeams.map(({ teamId }) => teamId);
const teamsToRemove = currentTeamIds && currentTeamIds.filter((teamId) => notInTeamIds.includes(teamId));
const teamsToAdd = inTeamIds.filter((teamId) => !currentTeamIds?.includes(teamId));
await Team.insertMemberOnTeams(user._id, teamsToAdd);
if (teamsToRemove) {
await Team.removeMemberFromTeams(user._id, teamsToRemove);
}
}
private static getRocketChatTeamsByLdapTeams(mappedTeams: Record<string, string>, ldapUserTeams: Array<string>): Array<string> {
const mappedLdapTeams = Object.keys(mappedTeams);
const filteredTeams = ldapUserTeams.filter((ldapTeam) => mappedLdapTeams.includes(ldapTeam));
if (filteredTeams.length < ldapUserTeams.length) {
const unmappedLdapTeams = ldapUserTeams.filter((ldapTeam) => !mappedLdapTeams.includes(ldapTeam));
logger.error(`The following LDAP teams are not mapped in Rocket.Chat: "${ unmappedLdapTeams.join(', ') }".`);
}
if (!filteredTeams.length) {
return [];
}
return [...new Set(filteredTeams.map((ldapTeam) => mappedTeams[ldapTeam]).flat())];
}
private static async getLdapTeamsByUsername(ldap: LDAPConnection, username: string): Promise<Array<string>> {
const query = settings.get<string>('LDAP_Query_To_Get_User_Teams');
if (!query) {
return [];
}
const searchOptions = {
filter: query.replace(/#{username}/g, username),
scope: ldap.options.userSearchScope || 'sub',
sizeLimit: ldap.options.searchSizeLimit,
};
const ldapUserGroups = await ldap.searchRaw(ldap.options.baseDN, searchOptions);
if (!Array.isArray(ldapUserGroups)) {
return [];
}
return ldapUserGroups.filter((entry) => entry?.raw?.ou).map((entry) => (ldap.extractLdapAttribute(entry.raw.ou) as string)).flat();
}
public static copyActiveState(ldapUser: ILDAPEntry, userData: IImportUser): void {
if (!ldapUser) {
return;
}
const syncUserState = settings.get('LDAP_Sync_User_Active_State');
if (syncUserState === 'none') {
return;
}
const deleted = Boolean(ldapUser.pwdAccountLockedTime);
if (deleted === userData.deleted || (userData.deleted === undefined && !deleted)) {
return;
}
if (syncUserState === 'disable' && !deleted) {
return;
}
userData.deleted = deleted;
logger.debug(`${ deleted ? 'Deactivating' : 'Activating' } user ${ userData.name } (${ userData.username })`);
}
public static copyCustomFields(ldapUser: ILDAPEntry, userData: IImportUser): void {
if (!settings.get<boolean>('LDAP_Sync_Custom_Fields')) {
return;
}
const customFieldsSettings = settings.get<string>('Accounts_CustomFields');
const customFieldsMap = settings.get<string>('LDAP_CustomFieldMap');
if (!customFieldsMap || !customFieldsSettings) {
if (customFieldsMap) {
logger.debug('Skipping LDAP custom fields because there are no custom fields configured.');
}
return;
}
let map: Record<string, string>;
try {
map = JSON.parse(customFieldsMap) as Record<string, string>;
} catch (error) {
logger.error('Failed to parse LDAP Custom Fields mapping');
logger.error(error);
return;
}
let customFields: Record<string, any>;
try {
customFields = JSON.parse(customFieldsSettings) as Record<string, any>;
} catch (error) {
logger.error('Failed to parse Custom Fields');
logger.error(error);
return;
}
_.map(map, (userField, ldapField) => {
if (!this.getCustomField(customFields, userField)) {
logger.debug(`User attribute does not exist: ${ userField }`);
return;
}
if (!userData.customFields) {
userData.customFields = {};
}
const value = templateVarHandler(ldapField, ldapUser);
if (value) {
let ref: Record<string, any> = userData.customFields;
const attributeNames = userField.split('.');
let previousKey: string | undefined;
for (const key of attributeNames) {
if (previousKey) {
if (ref[previousKey] === undefined) {
ref[previousKey] = {};
} else if (typeof ref[previousKey] !== 'object') {
logger.error(`Failed to assign custom field: ${ userField }`);
return;
}
ref = ref[previousKey];
}
previousKey = key;
}
if (previousKey) {
ref[previousKey] = value;
logger.debug(`user.customFields.${ userField } changed to: ${ value }`);
}
}
});
}
private static async importNewUsers(ldap: LDAPConnection, converter: LDAPDataConverter, updateExistingUsers: boolean): Promise<void> {
return new Promise((resolve, reject) => {
let count = 0;
ldap.searchAllUsers<IImportUser>({
entryCallback: (entry: ldapjs.SearchEntry): IImportUser | undefined => {
const data = ldap.extractLdapEntryData(entry);
count++;
if (!updateExistingUsers) {
const existingUser = Promise.await(this.findExistingLDAPUser(data));
if (existingUser) {
return;
}
}
const userData = this.mapUserData(data);
converter.addUser(userData);
return userData;
},
endCallback: (error: any): void => {
if (error) {
logger.error(error);
reject(error);
return;
}
logger.info('LDAP finished importing. New users imported:', count);
resolve();
},
});
});
}
private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
const users = await UsersRaw.findLDAPUsers();
for await (const user of users) {
let ldapUser: ILDAPEntry | undefined;
if (user.services?.ldap?.id) {
ldapUser = await ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute);
} else if (user.username) {
ldapUser = await ldap.findOneByUsername(user.username);
}
if (ldapUser) {
const userData = this.mapUserData(ldapUser, user.username);
converter.addUser(userData);
}
}
resolve();
} catch (error) {
reject(error);
}
});
}
private static getCustomField(customFields: Record<string, any>, property: string): any {
try {
return _.reduce(property.split('.'), (acc, el) => acc[el], customFields);
} catch {
// ignore errors
}
}
}

@ -0,0 +1,16 @@
import '../../broker';
import { api } from '../../../../server/sdk/api';
import { LDAPEEManager } from '../../lib/ldap/Manager';
import { ILDAPEEService } from '../../sdk/types/ILDAPEEService';
import { ServiceClass } from '../../../../server/sdk/types/ServiceClass';
export class LDAPEEService extends ServiceClass implements ILDAPEEService {
protected name = 'ldap-enterprise';
async sync(): Promise<void> {
return LDAPEEManager.sync();
}
}
api.registerService(new LDAPEEService());

@ -0,0 +1,4 @@
import { ILDAPEEService } from './types/ILDAPEEService';
import { proxifyWithWait } from '../../../server/sdk/lib/proxify';
export const LDAPEE = proxifyWithWait<ILDAPEEService>('ldap-enterprise');

@ -0,0 +1,3 @@
export interface ILDAPEEService {
sync(): Promise<void>;
}

154
package-lock.json generated

@ -5421,9 +5421,9 @@
"integrity": "sha512-RUHkphBOG6C7PULTLK8fPZmL+/2jYcUKozsfsNtJG+iY87zGe8STN62e5NZgjbUqhTErGx4JjZ/0w4D9jOoYeA=="
},
"@rocket.chat/livechat": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@rocket.chat/livechat/-/livechat-1.9.4.tgz",
"integrity": "sha512-82VSUUm9ueH941MzsDFsLSE6qTXKdgqHFjQgyMxyCTpU/BX6Oq6aXuCR6rEITLdgEXRQlzn59MRvCpo88xH+rw==",
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@rocket.chat/livechat/-/livechat-1.9.5.tgz",
"integrity": "sha512-fiWUdl6q/h+BrR1+m+kIdNTPuVF11wo++X24Fkjs7ng5fXT1xF3/kEPU0zrxRHT59FWae3rAgdlSiFullKjb9A==",
"dev": true,
"requires": {
"@kossnocorp/desvg": "^0.2.0",
@ -11251,6 +11251,15 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"@types/ldapjs": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.1.tgz",
"integrity": "sha512-rctCsjTBpG86j352gd6BfFevpcG2Rvh2K6t3BLO3VJMe6JSGDI5rovA+8O/rxL5PbXz0UFBdDu7wv/1hA/0GjA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/less": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.2.tgz",
@ -12447,6 +12456,11 @@
"event-target-shim": "^5.0.0"
}
},
"abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@ -16074,17 +16088,6 @@
"resolved": "https://registry.npmjs.org/bulk-replace/-/bulk-replace-0.0.1.tgz",
"integrity": "sha1-8JVoKolqvUs9ngjeQJzCIuIT+d0="
},
"bunyan": {
"version": "1.8.15",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz",
"integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==",
"requires": {
"dtrace-provider": "~0.8",
"moment": "^2.19.3",
"mv": "~2",
"safe-json-stringify": "~1"
}
},
"busboy": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz",
@ -18891,15 +18894,6 @@
}
}
},
"dtrace-provider": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.7.tgz",
"integrity": "sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ=",
"optional": true,
"requires": {
"nan": "^2.10.0"
}
},
"duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -25814,42 +25808,26 @@
"integrity": "sha512-81+uVrL8X5fl17Da5lImUCCp+jkJoaDRfL2k2ECkRJ1S+H8Z+my76eq0eWA/KM5/Y4/5mdtH8evRz0AvdqPy2w=="
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
"integrity": "sha1-KxTGiiqdQQTb28kQocqF/Riel5c=",
"requires": {
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
"assert-plus": "^1.0.0"
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.1.tgz",
"integrity": "sha512-kf0tHHLrpwKaBAQOhYHXgdeh2PkFuCCxWgLb1MRn67ZQVo787D2pij3mmHVZx193GIdM8xcfi8HF6AIYYnj0fQ==",
"requires": {
"asn1": "0.2.3",
"abstract-logging": "^2.0.0",
"asn1": "^0.2.4",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"bunyan": "^1.8.3",
"dashdash": "^1.14.0",
"dtrace-provider": "~0.8",
"ldap-filter": "0.2.2",
"ldap-filter": "^0.3.3",
"once": "^1.4.0",
"vasync": "^1.6.4",
"vasync": "^2.2.0",
"verror": "^1.8.1"
},
"dependencies": {
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
}
}
},
"left-pad": {
@ -29061,47 +29039,6 @@
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
"glob": "^6.0.1"
}
}
}
},
"nan": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz",
"integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==",
"optional": true
},
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@ -29162,12 +29099,6 @@
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
"dev": true
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
@ -34217,12 +34148,6 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-json-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true
},
"safe-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@ -38573,26 +38498,11 @@
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.0.tgz",
"integrity": "sha1-z951GGChWCLbOxMrxZsRakra8Bs=",
"requires": {
"verror": "1.6.0"
},
"dependencies": {
"extsprintf": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.2.0.tgz",
"integrity": "sha1-WtlGwi9bMrp/jNdCZxHG6KP8JSk="
},
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
}
}
"verror": "1.10.0"
}
},
"verror": {

@ -58,7 +58,7 @@
"@babel/preset-react": "^7.14.5",
"@babel/register": "^7.14.5",
"@rocket.chat/eslint-config": "^0.4.0",
"@rocket.chat/livechat": "^1.9.4",
"@rocket.chat/livechat": "^1.9.5",
"@settlin/spacebars-loader": "^1.0.9",
"@storybook/addon-essentials": "^6.3.6",
"@storybook/addon-postcss": "^2.0.0",
@ -77,6 +77,7 @@
"@types/fibers": "^3.1.0",
"@types/imap": "^0.8.35",
"@types/jsdom": "^16.2.12",
"@types/ldapjs": "^2.2.1",
"@types/less": "^3.0.2",
"@types/lodash.get": "^4.4.6",
"@types/mailparser": "^3.0.2",
@ -187,7 +188,6 @@
"blockstack": "19.3.0",
"body-parser": "1.19.0",
"bson": "^4.5.1",
"bunyan": "^1.8.15",
"busboy": "^0.3.1",
"bytebuffer": "5.0.1",
"cas": "https://github.com/kcbanner/node-cas/tarball/fcd27dad333223b3b75a048bce27973fb3ca0f62",
@ -233,7 +233,7 @@
"juice": "^8.0.0",
"katex": "^0.11.1",
"ldap-escape": "^2.0.5",
"ldapjs": "^1.0.2",
"ldapjs": "^2.3.1",
"less": "https://github.com/meteor/less.js/tarball/8130849eb3d7f0ecf0ca8d0af7c4207b0442e3f6",
"less-plugin-autoprefixer": "^2.1.0",
"limax": "^3.0.0",

@ -1519,7 +1519,6 @@
"LDAP_Connect_Timeout": "Connection Time-out (ms)",
"LDAP_Default_Domain": "Verstek Domein",
"LDAP_Default_Domain_Description": "As dit voorsien word, sal die standaard domein gebruik word om 'n unieke e-pos vir gebruikers te skep waar e-pos nie vanaf LDAP ingevoer is nie. Die e-pos sal gemonteer word as `gebruikersnaam@default_domain` of `unique_id@default_domain`.<br/>Voorbeeld: `rocket.chat`",
"LDAP_Description": "LDAP is 'n hiërargiese databasis wat baie maatskappye gebruik om 'n enkele teken aan te bied - 'n fasiliteit om een wagwoord tussen verskeie webwerwe en dienste te deel. Vir gevorderde konfigurasie inligting en voorbeelde, raadpleeg asseblief ons wiki: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "in staat te stel",
"LDAP_Enable_Description": "Probeer om LDAP vir verifikasie te gebruik.",
"LDAP_Encryption": "enkripsie",
@ -1561,11 +1560,6 @@
"LDAP_Sync_Now": "Agtergrondsinkronisering Nou",
"LDAP_Sync_Now_Description": "Sal die ** Agtergrondsinkronisering ** nou uitvoer eerder as om die ** Sync Interval ** te wag, selfs al is ** Agtergrondsinkronisering ** vals. <br/>Hierdie aksie is asynchroon. Sien asseblief die logs vir meer inligting oor die proses",
"LDAP_Sync_User_Avatar": "Sinkroniseer gebruiker avatar",
"LDAP_Sync_User_Data": "Sinkroniseer gebruikersdata",
"LDAP_Sync_User_Data_Description": "Hou gebruikersdata in lyn met die bediener op ** aanmelding ** of op ** agtergrondsinkronisering ** (bv. Naam, e-pos).",
"LDAP_Sync_User_Data_FieldMap": "Gebruiker data veld kaart",
"LDAP_Sync_User_Data_FieldMap_Description": "Stel in hoe gebruikersrekeningvelde (soos e-pos) gevul word uit 'n rekord in LDAP (een keer gevind). <br/>As 'n voorbeeld, sal `{\" cn \":\" naam \",\" pos \":\" e-pos \"}` 'n mens se leesbare naam uit die cn-kenmerk en hul e-pos van die posattribuut kies. Daarbenewens is dit moontlik om veranderlikes te gebruik, byvoorbeeld: `{\" # {givenName} # {sn} \":\" naam \",\" pos \":\" e-pos \"}` gebruik 'n kombinasie van die gebruiker se voornaam en van raket chat `naam` veld.<br/>Beskikbare velde in Rocket.Chat:` name`, `email` en` customFields`.",
"LDAP_Test_Connection": "Toets verbinding",
"LDAP_Timeout": "Time-out (ms)",
"LDAP_Timeout_Description": "Hoeveel kilometer-sekondes wag vir 'n soekresultaat voordat 'n fout terugkom",
"LDAP_Unique_Identifier_Field": "Unieke Identifiseringsveld",

@ -1537,7 +1537,6 @@
"LDAP_Connect_Timeout": "انتهاء مدة الاتصال (بالملي ثانية)",
"LDAP_Default_Domain": "المجال الافتراضي",
"LDAP_Default_Domain_Description": "إذا تم توفير النطاق الافتراضي سيتم استخدامه لإنشاء بريد إلكتروني فريد للمستخدمين حيث لم يتم استيراد البريد الإلكتروني من لداب. سيتم تثبيت الرسالة الإلكترونية باسم `أوزرنام @ default_domain` أور` unique_id @ default_domain`. <br/>مثال: `rocket.chat`",
"LDAP_Description": "LDAP هي قاعدة بيانات هرمية التي تستخدم العديد من الشركات لتقديم إشارة واحدة على - مرفق لتقاسم كلمة مرور واحدة بين المواقع وخدمات متعددة. للحصول على معلومات التكوين المتقدمة والأمثلة، يرجى الرجوع لدينا ويكي: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "تفعيل",
"LDAP_Enable_Description": "محاولة استخدام LDAP للمصادقة.",
"LDAP_Encryption": "التشفير",
@ -1579,11 +1578,6 @@
"LDAP_Sync_Now": "خلفية مزامنة الآن",
"LDAP_Sync_Now_Description": "سيتم تنفيذ ** مزامنة الخلفية ** الآن بدلا من الانتظار ** مزامنة الفاصل الزمني ** حتى لو ** مزامنة الخلفية ** هو خطأ. <br/>هذا الإجراء غير متزامن، يرجى الاطلاع على سجلات لمزيد من المعلومات حول معالجة",
"LDAP_Sync_User_Avatar": "تزامن العضو الرمزية",
"LDAP_Sync_User_Data": "مزامنة البيانات",
"LDAP_Sync_User_Data_Description": "حافظ على بيانات المستخدم في تزامن مع الخادم على تسجيل الدخول (على سبيل المثال: الاسم، البريد الإلكتروني).",
"LDAP_Sync_User_Data_FieldMap": "العضو بيانات خريطة الميدان",
"LDAP_Sync_User_Data_FieldMap_Description": "تكوين كيف يتم ملؤها الحقول حساب المستخدم (مثل البريد الإلكتروني) من مستوى قياسي في LDAP (وجدت مرة واحدة). <br/> وكمثال على ذلك، `{\" CN \":\" اسم \"،\" البريد الإلكتروني \":\" البريد الإلكتروني \"}` سيختار اسم الشخص البشري للقراءة من سمة CN، وبريدهم الإلكتروني من السمة البريد. <br/> وتشمل المجالات المتاحة `name`، و` email`.",
"LDAP_Test_Connection": "اختبار الاتصال",
"LDAP_Timeout": "مهلة (مللي ثانية)",
"LDAP_Timeout_Description": "كم عدد الأميال التي تنتظر نتيجة بحث قبل إرجاع خطأ",
"LDAP_Unique_Identifier_Field": "معرف الميدان وفريدة من نوعها",

@ -1519,7 +1519,6 @@
"LDAP_Connect_Timeout": "Bağlantı Təminatı (ms)",
"LDAP_Default_Domain": "Default Domain",
"LDAP_Default_Domain_Description": "Varsa, Default Domain e-poçt LDAP-dan idxal olunmayan istifadəçilər üçün unikal bir e-poçt yaratmaq üçün istifadə olunacaq. E-poçt 'username @ default_domain' və ya 'unique_id @ default_domain' kimi quraşdırılacaq.<br/>Məsələn: 'rocket.chat`",
"LDAP_Description": "LDAP, bir çox şirkət və birdən çox sayt arasında bir parol paylaşma imkanı olan bir çox şirkətin vahid işarəsi təmin etmək üçün istifadə etdiyi hiyerarşik bir verilənlər bazasıdır. Ətraflı konfiqurasiya məlumatları və nümunələr üçün, bizim wiki müraciət edin: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Enable",
"LDAP_Enable_Description": "Doğrulama üçün LDAP-dan istifadə etməyə cəhd.",
"LDAP_Encryption": "Şifrələmə",
@ -1561,11 +1560,6 @@
"LDAP_Sync_Now": "İndi Arxa Sinxronlaşdırın",
"LDAP_Sync_Now_Description": "** Arxa Sinxronlaşdırma ** indi ** Sinxronlaşdırma Aralığını ** Arxa Sinxronlaşdırma ** səhv olsa da gözləyin.<br/>Bu Fəaliyyət asinxron deyil, xahiş edirik, prosesi",
"LDAP_Sync_User_Avatar": "İstifadəçi Avatarını sinxronlaşdırın",
"LDAP_Sync_User_Data": "İstifadəçi məlumatlarını sinxronlaşdırın",
"LDAP_Sync_User_Data_Description": "Istifadəçi məlumatlarını ** girişdə server ilə senkronize edin ** və ya ** fonda sinxronlaşdırma zamanı ** (məsələn, adı, e-poçt).",
"LDAP_Sync_User_Data_FieldMap": "İstifadəçi Məlumatın Sahəsi xəritəsi",
"LDAP_Sync_User_Data_FieldMap_Description": "LDAP-da (bir dəfə tapıldı) bir istifadəçi hesabının sahələrinin (e-poçt kimi) necə yerləşdirildiyini konfiqurasiya.<br/>Məsələn, `{\" cn \":\" adı \",\" poçt \":\" e-poçt \"}, bir şəxsin cn xüsusiyyətindən insan oxunaqlı adını və onların e-poçtunu poçt xassəsindən seçəcək. Əlavə olaraq, dəyişənləri istifadə etmək mümkündür, məsələn: `{\" # {givenName} # {sn} \":\" adı \",\" poçt \":\" e-poçt \"}` istifadəçinin adı və soyadının birləşməsini istifadə edir roket chat` adı `sahəsində. <br/>Rocket.Chat-də mövcud sahələr:` adı`, `email` və` customFields`.",
"LDAP_Test_Connection": "Test keçid",
"LDAP_Timeout": "Timeout (ms)",
"LDAP_Timeout_Description": "Bir neçə dəqiqə ara bir axtarış nəticə gözləməyincə, bir səhv qaytarır",
"LDAP_Unique_Identifier_Field": "Nadir Təsdiq Alan",

@ -1532,7 +1532,6 @@
"LDAP_Connect_Timeout": "Час чакання злучэння (мс)",
"LDAP_Default_Domain": "Дамен па змаўчанні",
"LDAP_Default_Domain_Description": "Пры наяўнасці дамен па змаўчанні будзе выкарыстоўвацца для стварэння унікальнага адрасы электроннай пошты для карыстальнікаў, дзе электронная пошта не была завезена з LDAP. . Адрас электроннай пошты будзе змантаваны як `імя карыстальніка @ default_domain` или` unique_id @ default_domain` <br/>Прыклад:` rocket.chat`",
"LDAP_Description": "LDAP ўяўляе сабой іерархічную базу дадзеных, што многія кампаніі выкарыстоўваюць для забеспячэння адзінага ўваходу - аб'ект для абмену адзін пароля паміж некалькімі сайтамі і паслугамі. Для папярэдняй інфармацыі аб канфігурацыі і прыклады, калі ласка, звярніцеся ў наш вікі: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Ўключыць",
"LDAP_Enable_Description": "Спроба выкарыстоўваць LDAP для аўтэнтыфікацыі.",
"LDAP_Encryption": "шыфраванне",
@ -1574,11 +1573,6 @@
"LDAP_Sync_Now": "Фонавая сінхранізацыя Цяпер",
"LDAP_Sync_Now_Description": "Ці будзе выконваць ** Фонавая сінхранізацыя ** зараз, а не чакаць ** Інтэрвал сінхранізацыі з ** нават калі ** Фонавая сінхранізацыя ** з'яўляецца false. <br/>Гэта дзеянне з'яўляецца асінхронным, калі ласка, глядзіце часопісы для атрымання дадатковых звестак аб працэс",
"LDAP_Sync_User_Avatar": "Сінхранізацыя Аватар карыстальніка",
"LDAP_Sync_User_Data": "Сінхранізацыя дадзеных карыстальніка",
"LDAP_Sync_User_Data_Description": "Захоўваць дадзеныя карыстальніка ў сінхранізацыі з серверам на ** лагін ** або ** ** фон сінхранізацыі (напрыклад, імя, адрас электроннай пошты).",
"LDAP_Sync_User_Data_FieldMap": "Дадзеныя карыстальніка Карта Поле",
"LDAP_Sync_User_Data_FieldMap_Description": "Настройка як поля ўліковага запісу карыстальніка (напрыклад, электронная пошта) запаўняюцца з запісу ў LDAP (калісьці знайшло). <br/>У якасці прыкладу, `{« сп »:« Імя »,« пошта »:« электронная пошта »}` падбярэ лёгкачытальным імя чалавека з атрыбуту сп, і іх электронную пошту ад атрыбуту пошты. Акрамя таго, можна выкарыстоўваць зменныя, напрыклад: `{« # {GivenName} # {ЗП} »:« Імя »,« пошта »:« электронная пошта »}` выкарыстоўвае камбінацыю імя карыстальніка і прозвішча для ракета чат `name` поле <br/>Даступныя поля ў Rocket.Chat: .` name`,` `email` і customFields`.",
"LDAP_Test_Connection": "Пров.соед",
"LDAP_Timeout": "Тайм-аўт (мс)",
"LDAP_Timeout_Description": "Колькі mileseconds чакаць выніку пошуку, перш чым вярнуць памылку",
"LDAP_Unique_Identifier_Field": "Унікальны ідэнтыфікатар поля",

@ -1517,7 +1517,6 @@
"LDAP_Connect_Timeout": "Времетраене на връзката (ms)",
"LDAP_Default_Domain": "Начален домейн",
"LDAP_Default_Domain_Description": "Ако е предвидено, Домейнът по подразбиране ще бъде използван за създаване на уникален имейл за потребителите, в които имейлът не е бил импортиран от LDAP. Имейлът ще бъде монтиран като \"username @ default_domain\" или \"unique_id @ default_domain\".<br/>Пример: `rocket.chat`",
"LDAP_Description": "LDAP е йерархична база данни, която много компании използват за осигуряване на единичен подпис - възможност за споделяне на една парола между множество сайтове и услуги. За информация за разширената конфигурация и примери, моля, вижте нашата уики: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Активиране",
"LDAP_Enable_Description": "Опит за използване на LDAP за удостоверяване.",
"LDAP_Encryption": "Encryption",
@ -1559,11 +1558,6 @@
"LDAP_Sync_Now": "Синхронизиране на фона сега",
"LDAP_Sync_Now_Description": "Ще изпълни ** синхронизирането на фона ** сега, вместо да чака ** интервала за синхронизиране **, дори когато ** синхронизирането на фона ** е невярно. <br/>Това действие е асинхронно, моля вижте регистрационните файлове за повече информация за процес",
"LDAP_Sync_User_Avatar": "Аватар на потребител за синхронизиране",
"LDAP_Sync_User_Data": "Синхронизиране на потребителски данни",
"LDAP_Sync_User_Data_Description": "Съхранявайте потребителските данни в синхрон със сървъра на ** вход ** или на ** синхронизиране на фона ** (напр .: име, имейл).",
"LDAP_Sync_User_Data_FieldMap": "Карта на поле на потребителски данни",
"LDAP_Sync_User_Data_FieldMap_Description": "Конфигурирайте как се попълват полетата за потребителски акаунти (като имейл) от запис в LDAP (веднъж намерен).<br/>Като пример, \"{\" cn \":\" name \",\" mail \":\" имейл \"} ще избира човекно четливо име от атрибута cn и имейла от атрибута. Освен това е възможно да се използват променливи, например: \"{\" # {givenName} # {sn} \":\" име \",\" mail \":\" имейл \"} използва комбинация от първото и последното име на потребителя ракетен чат \"име\" поле.<br/>Наличните полета в Rocket.Chat: \"име\", \"имейл\" и \"customFields\".",
"LDAP_Test_Connection": "Тест връзка",
"LDAP_Timeout": "Timeout (ms)",
"LDAP_Timeout_Description": "Колко милисекунди чакате за резултат от търсенето, преди да върнете грешка",
"LDAP_Unique_Identifier_Field": "Уникално поле на идентификатора",

@ -1516,7 +1516,6 @@
"LDAP_Connect_Timeout": "Vrijeme veze (ms)",
"LDAP_Default_Domain": "Zadana domena",
"LDAP_Default_Domain_Description": "Ako je predviđeno, zadana domena će se koristiti za stvaranje jedinstvene e-pošte za korisnike gdje e-pošta nije uvezena iz LDAP-a. E-adresa će biti postavljena kao \"korisničko ime @ default_domain\" ili \"unique_id @ default_domain\".<br/>Primjer: `rocket.chat`",
"LDAP_Description": "LDAP je hijerarhijska baza podataka koja mnoge tvrtke koriste za pružanje jedinstvene prijave - usluge za dijeljenje jedne lozinke između više web-mjesta i usluga. Za napredne podatke o konfiguraciji i primjere, obratite se našem wiki: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Omogući",
"LDAP_Enable_Description": "Pokušaj koristiti LDAP za provjeru autentičnosti.",
"LDAP_Encryption": "Šifriranje",
@ -1558,11 +1557,6 @@
"LDAP_Sync_Now": "Pozadinska sinkronizacija sada",
"LDAP_Sync_Now_Description": "Će izvršiti ** pozadinsku sinkronizaciju ** sada, a ne čekati ** Sync Interval **, čak i ako je ** Sinkronizacija pozadine ** lažna.<br/>Ova akcija je asinkroni, pogledajte zapisnike za više informacija o postupak",
"LDAP_Sync_User_Avatar": "Sinkronizacija Korisnikovog Avatara",
"LDAP_Sync_User_Data": "Sinkronizacija podataka",
"LDAP_Sync_User_Data_Description": "Održavajte korisničke podatke sinkronizirane sa serverom pri prijavi (npr: ime i prezime, e-mail).",
"LDAP_Sync_User_Data_FieldMap": "Mapa korisničkih podataka",
"LDAP_Sync_User_Data_FieldMap_Description": "Konfigurirajte kako će korisnička polja (poput emaila) biti popunje iz zapisa na LDAPu (nakon što ih nađemo). <br/> Na primjer, `{\"cn\":\"name\", \"mail\":\"email\"}` će odabrati čitljivo ime iz cn atributa i njihov email i mail atributa. <br/> Dostupna polja uključuju `name`i `email`.",
"LDAP_Test_Connection": "Testiraj vezu",
"LDAP_Timeout": "Vrijeme (ms)",
"LDAP_Timeout_Description": "Koliko milja čekaju rezultat pretraživanja prije nego što se vrati pogreška",
"LDAP_Unique_Identifier_Field": "Jedinstveni identifikator polja",

@ -2432,12 +2432,8 @@
"LDAP_Connect_Timeout": "Temps d'espera connexió (ms)",
"LDAP_Default_Domain": "Domini predeterminat",
"LDAP_Default_Domain_Description": "Si es proporciona el domini per defecte s'utilitzarà per crear un correu electrònic únic per als usuaris en què el correu electrònic no s'ha importat des de LDAP. El correu electrònic es muntarà com a `nomusuari@dominiperdefecte` o `nom_usuari_unic@dominiperdefecte`.<br/>Exemple: `rocket.chat`",
"LDAP_Default_Role_To_User": "Funció predeterminada de l'usuari",
"LDAP_Default_Role_To_User_Description": "El paper RC predeterminat que s'aplicarà a l'usuari si l'usuari té algun paper LDAP que no està assignat.",
"LDAP_Description": "LDAP és una base de dades jeràrquica que moltes empreses utilitzen per proporcionar un inici de sessió únic –una eina per tenir una sola contrasenya a múltiples llocs web i serveis. Per a exemples i informació sobre configuracions més avançades, consulteu la nostra wiki: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Activa",
"LDAP_Enable_Description": "Intentar utilitzar LDAP com a mètode d'autenticació",
"LDAP_Enable_LDAP_Roles_To_RC_Roles": "Activa el mapeig de rols de LDAP a Rocket.Chat",
"LDAP_Enable_LDAP_Groups_To_RC_Teams": "Habiliteu el mapeig de l'equip de LDAP a Rocket.Chat",
"LDAP_Encryption": "Xifrat",
"LDAP_Encryption_Description": "Mètode de xifrat utilitzat per a la comunicació segura cap al servidor LDAP. Alguns exemples 'sense xifrat', 'SSL / LDAPS (xifrat des de l'inici), i' StartTLS '(actualitzar a comunicacions xifrades una vegada connectat).",
@ -2468,15 +2464,11 @@
"LDAP_Merge_Existing_Users_Description": "* Precaució! * A l'importar un usuari de LDAP i ja existeix un usuari amb el mateix nom d'usuari, la informació i la contrasenya d'LDAP s'establiran en l'usuari existent.",
"LDAP_Port": "Port",
"LDAP_Port_Description": "Port per accedir a LDAP. Ex. `389` o `636` per LDAPS",
"LDAP_Query_To_Get_User_Groups": "Consulta LDAP per obtenir grups d'usuaris",
"LDAP_Query_To_Get_User_Groups_Description": "Consulta LDAP per obtenir els grups LDAP dels quals l'usuari forma part.",
"LDAP_Query_To_Get_User_Teams": "Consulta LDAP per obtenir grups d'usuaris",
"LDAP_Reconnect": "Reconnecta",
"LDAP_Reconnect_Description": "Proveu tornar a connectar-se automàticament quan la connexió s'interrompi per algun motiu mentre executa operacions",
"LDAP_Reject_Unauthorized": "Rebutja no autoritzat",
"LDAP_Reject_Unauthorized_Description": "Desactiveu aquesta opció per permetre certificats que no es poden verificar. En general, els certificats autofirmados requeriran que aquesta opció estigui desactivada per funcionar",
"LDAP_Roles_To_Rocket_Chat_Roles": "Assignació de rols de LDAP a Rocket.Chat.",
"LDAP_Roles_To_Rocket_Chat_Roles_Description": "Mapeig de rols en format d'objectes on la clau d'objecte ha de ser el rol LDAP i el valor d'objecte ha de ser una matriu de rols RC. Exemple: { 'ldapRole': ['rcRole', 'anotherRCRole'] }",
"LDAP_Search_Page_Size": "Mida de la pàgina de cerca",
"LDAP_Search_Page_Size_Description": "El nombre màxim d'entrades que cada pàgina de resultats tornarà a processar",
"LDAP_Search_Size_Limit": "Límit de la mida de la cerca",
@ -2489,29 +2481,28 @@
"LDAP_Sync_User_Active_State_Disable": "Habilitar usuaris",
"LDAP_Sync_User_Active_State_Nothing": "No fer res",
"LDAP_Sync_User_Avatar": "Sincronitzar avatar de l'usuari",
"LDAP_Sync_User_Data": "Sincronitzar dades d'usuari",
"LDAP_Sync_User_Data_Description": "Mantenir les dades de l'usuari sincronitzades amb el servidor **quan s'identifiqui** o en **sincronització en background** (ex: nom, adreça de correu, etcètera).",
"LDAP_Sync_User_Data_FieldMap": "Mapatge de camps de dades d'usuari",
"LDAP_Sync_User_Data_FieldMap_Description": "Configura com els camps del compte d'usuari (com el de correu-e) s'omplen des del registre LDAP (un cop trobat).<br/>A tall d'exemple, `{\"cn\":\"name\", \"mail\":\"email\"}` triarà el nom des de l'atribut `cn`, i l'adreça-e des de l'atribut `mail`. Addicionalment, és possible utilitzar variables, per exemple: `{ \"#{givenName} #{sn}\": \"name\", \"mail\": \"email\" }` utilitza una combinació del nom de pila i del cognom per al camp `name` de l'usuari de rocket chat. <br/>Camps disponibles a Rocket.Chat: `name`, `email` i `customFields`.",
"LDAP_Sync_User_Data_Groups": "Sincronitza grups LDAP",
"LDAP_Sync_User_Data_Groups_AutoChannels": "Sincronització automàtica de grups LDAP a Channels",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin": "Channel Administrador",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin_Description": "Quan es creen canals automàticament que no existeixen durant una sincronització, aquest usuari es convertirà automàticament en l'administrador del canal.",
"LDAP_Sync_User_Data_Groups_AutoChannels_Description": "Activeu aquesta funció per afegir usuaris automàticament a un canal en funció del seu grup LDAP. Si voleu eliminar també els usuaris d'un canal, consulteu l'opció següent per eliminar usuaris automàticament.",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap": "Mapa del Channel Grup LDAP",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Default": "// Habiliteu la sincronització automàtica de grups LDAP amb els Channel anteriors",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Description": "Assigneu grups LDAP a canals Rocket.Chat. <br/> Com a exemple, `{\"employee\":\"general\"}` afegirà qualsevol usuari del treballador del grup LDAP al canal general.",
"LDAP_Sync_User_Data_Groups_AutoRemove": "Elimina automàticament els rols d'usuari",
"LDAP_Sync_User_Data_Groups_AutoRemove_Description": "** Atenció **: si activeu això, s'eliminaran automàticament els usuaris d'una funció si no estan assignats a LDAP. Això només eliminarà automàticament els rols que es defineixen al mapa de grups de dades d'usuaris que apareix a continuació.",
"LDAP_Sync_User_Data_Groups_BaseDN": "Grup LDAP BaseDN",
"LDAP_Sync_User_Data_Groups_BaseDN_Description": "LDAP basedn utilitzat per buscar usuaris.",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels": "Elimina automàticament els usuaris de Channel s",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels_Description": "** Atenció **: Habilitar això eliminarà a qualsevol usuari en un canal que no tingui el grup LDAP corresponent! Habiliteu això només si sap el que està fent.",
"LDAP_Sync_User_Data_Groups_Filter": "Filtre de grups d'usuaris",
"LDAP_Sync_User_Data_Groups_Filter_Description": "El filtre de cerca LDAP que s'usa per verificar si un usuari està en un grup.",
"LDAP_Sync_User_Data_GroupsMap": "Mapa de grup de dades d'usuari",
"LDAP_Sync_User_Data_GroupsMap_Description": "Mapeja els grups LDAP als rols d'usuari de Rocket.Chat <br/> Com a exemple, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` mapejarà el grup LDAP de rocket- admin a el paper de \"admin\" de Rocket.",
"LDAP_Test_Connection": "Prova la connexió",
"LDAP_Sync_User_Data_Roles": "Sincronitza grups LDAP",
"LDAP_Sync_User_Data_Channels": "Sincronització automàtica de grups LDAP a Channels",
"LDAP_Sync_User_Data_Channels_Admin": "Channel Administrador",
"LDAP_Sync_User_Data_Channels_Admin_Description": "Quan es creen canals automàticament que no existeixen durant una sincronització, aquest usuari es convertirà automàticament en l'administrador del canal.",
"LDAP_Sync_User_Data_Channels_BaseDN": "Grup LDAP BaseDN",
"LDAP_Sync_User_Data_Channels_BaseDN_Description": "LDAP basedn utilitzat per buscar usuaris.",
"LDAP_Sync_User_Data_Channels_Description": "Activeu aquesta funció per afegir usuaris automàticament a un canal en funció del seu grup LDAP. Si voleu eliminar també els usuaris d'un canal, consulteu l'opció següent per eliminar usuaris automàticament.",
"LDAP_Sync_User_Data_Channels_Enforce_AutoChannels": "Elimina automàticament els usuaris de Channel s",
"LDAP_Sync_User_Data_Channels_Enforce_AutoChannels_Description": "** Atenció **: Habilitar això eliminarà a qualsevol usuari en un canal que no tingui el grup LDAP corresponent! Habiliteu això només si sap el que està fent.",
"LDAP_Sync_User_Data_Channels_Filter": "Filtre de grups d'usuaris",
"LDAP_Sync_User_Data_Channels_Filter_Description": "El filtre de cerca LDAP que s'usa per verificar si un usuari està en un grup.",
"LDAP_Sync_User_Data_ChannelsMap": "Mapa del Channel Grup LDAP",
"LDAP_Sync_User_Data_ChannelsMap_Default": "// Habiliteu la sincronització automàtica de grups LDAP amb els Channel anteriors",
"LDAP_Sync_User_Data_ChannelsMap_Description": "Assigneu grups LDAP a canals Rocket.Chat. <br/> Com a exemple, `{\"employee\":\"general\"}` afegirà qualsevol usuari del treballador del grup LDAP al canal general.",
"LDAP_Sync_User_Data_Roles_AutoRemove": "Elimina automàticament els rols d'usuari",
"LDAP_Sync_User_Data_Roles_AutoRemove_Description": "** Atenció **: si activeu això, s'eliminaran automàticament els usuaris d'una funció si no estan assignats a LDAP. Això només eliminarà automàticament els rols que es defineixen al mapa de grups de dades d'usuaris que apareix a continuació.",
"LDAP_Sync_User_Data_Roles_BaseDN": "Grup LDAP BaseDN",
"LDAP_Sync_User_Data_Roles_BaseDN_Description": "LDAP basedn utilitzat per buscar usuaris.",
"LDAP_Sync_User_Data_Roles_Filter": "Filtre de grups d'usuaris",
"LDAP_Sync_User_Data_Roles_Filter_Description": "El filtre de cerca LDAP que s'usa per verificar si un usuari està en un grup.",
"LDAP_Sync_User_Data_RolesMap": "Mapa de grup de dades d'usuari",
"LDAP_Sync_User_Data_RolesMap_Description": "Mapeja els grups LDAP als rols d'usuari de Rocket.Chat <br/> Com a exemple, `{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}` mapejarà el grup LDAP de rocket- admin a el paper de \"admin\" de Rocket.",
"LDAP_Timeout": "Temps d'espera (ms)",
"LDAP_Timeout_Description": "Quants mil·lisegons esperen un resultat de cerca abans de tornar un error",
"LDAP_Unique_Identifier_Field": "Camp d'identificador únic",
@ -2525,8 +2516,6 @@
"LDAP_Username_Field_Description": "El camp que s'utilitzarà com a nom d'usuari (*username*) per als nous usuaris. Deixar en blanc per a utilitzar el nom d'usuari proporcionat durant l'inici de sessió.<br/>També es poden utilitzar plantilles d'etiqueta, com `#{givenName}.#{sn}`.<br/>El valor per defecte és `sAMAccountName`.",
"LDAP_Validate_Teams_For_Each_Login": "Validar l'assignació per a cada inici de sessió",
"LDAP_Validate_Teams_For_Each_Login_Description": "Determineu si els equips dels usuaris s'han d'actualitzar cada vegada que inicien sessió en Rocket.Chat. Si està desactivat, l'equip es carregarà només en el seu primer inici de sessió.",
"LDAP_Validate_Roles_For_Each_Login": "Validar l'assignació per a cada inici de sessió",
"LDAP_Validate_Roles_For_Each_Login_Description": "Si la validació ha d'ocórrer per a cada inici de sessió (vagi amb compte amb aquesta configuració perquè sobreescriurà els rols d'usuari en cada inici de sessió, en cas contrari, això es validarà només en el moment de la creació de l'usuari).",
"Lead_capture_email_regex": "Regex de correu electrònic de captura de clients potencials",
"Lead_capture_phone_regex": "Regex de telèfon de captura clients potencials",
"Leave": "Sortir ",

@ -2149,12 +2149,8 @@
"LDAP_Connect_Timeout": "Prodleva připojení (ms)",
"LDAP_Default_Domain": "Výchozí doména",
"LDAP_Default_Domain_Description": "Pokud je vyplněna, Výchozí doména bude použita pro vytvoření unikátního emailu pro ty uživatele, u kterých v LDAP chybí. Bude použit formát `username@default_domain` nebo `unique_id@default_domain`. <br/>Například `rocket.chat`",
"LDAP_Default_Role_To_User": "Výchozí role pro uživatele",
"LDAP_Default_Role_To_User_Description": "Výchozí role přidaná uživateli v případě že LDAP role není namapovaná",
"LDAP_Description": "LDAP je hierarchická databáze, kterou mnohé společnosti používají pro jednotné přihlášení - systém pro sdílení jednoho hesla mezi více webů a služeb. Pro pokročilé informace a příklady konfigurace se podívejte na naši wiki: https://rocket.chat/docs/administrator-guides/authentication/ldap/",
"LDAP_Enable": "Povolit",
"LDAP_Enable_Description": "Pokusit se použít LDAP pro autentizaci.",
"LDAP_Enable_LDAP_Roles_To_RC_Roles": "Povolit mapování rolí z LDAP do Rocket.Chat",
"LDAP_Encryption": "Šifrování",
"LDAP_Encryption_Description": "Metoda šifrování používaná k zabezpečené komunikace se serverem LDAP. Jako příklady lze uvést `plain` (bez šifrování),` SSL/LDAPS` (šifrovaný od začátku), a `StartTLS` (Šifrovaná komunikaci až po připojení).",
"LDAP_Find_User_After_Login": "Najít uživatele po přihlášení",
@ -2183,15 +2179,11 @@
"LDAP_Merge_Existing_Users_Description": "*Upozornění!* Pokud importujete uživatele z LDAP a uživatel se stejným jménem již existuje, LDAP údaje a heslo budou přiřazeny tomuto uživateli",
"LDAP_Port": "Port",
"LDAP_Port_Description": "Port pro přístup k LDAP. např: `389` nebo `636` pro LDAPS",
"LDAP_Query_To_Get_User_Groups": "LDAP query pro získání uživatelských skupin",
"LDAP_Query_To_Get_User_Groups_Description": "LDAP query pro získání skupin do kterých patří uživatel.",
"LDAP_Query_To_Get_User_Teams": "LDAP query pro získání uživatelských skupin",
"LDAP_Reconnect": "Znovu připojit",
"LDAP_Reconnect_Description": "Pokusit se znovu připojit automaticky pokud bylo při zpracování požadavků spojení přerušeno",
"LDAP_Reject_Unauthorized": "Odmítnout neoprávněné",
"LDAP_Reject_Unauthorized_Description": "Zakažte tuto volbu pro povolení neověřených certifikatů. Většinou je potřeba tuto volbu vypnout pokud se pracuje se self signed certifikáty",
"LDAP_Roles_To_Rocket_Chat_Roles": "Mapování rolí z LDAP do Rocket.Chat",
"LDAP_Roles_To_Rocket_Chat_Roles_Description": "Mapování rolí v objektovém formátu, kde klíč je LDAP role a hodnota pole Rocket.Chat rolí. Například { 'ldapRole': ['rcRole', 'anotherRCRole'] }",
"LDAP_Search_Page_Size": "Počet výsledků vyhledávání na stránce",
"LDAP_Search_Page_Size_Description": "Maximální počet záznamů na každé stránce výsledků pro zpracování",
"LDAP_Search_Size_Limit": "Limit velikosti vyhledávání ",
@ -2204,28 +2196,27 @@
"LDAP_Sync_User_Active_State_Disable": "Zakázat uživatele",
"LDAP_Sync_User_Active_State_Nothing": "Nedělat nic",
"LDAP_Sync_User_Avatar": "Synchronizace uživatelských avatarů",
"LDAP_Sync_User_Data": "Synchronizace uživatelských dat",
"LDAP_Sync_User_Data_Description": "Udržujte uživatelská data synchronizovaná se serverem po přihlášení nebo při **synchronizaci na pozadí** (např: jméno, e-mail).",
"LDAP_Sync_User_Data_FieldMap": "Mapa polí uživatelských dat",
"LDAP_Sync_User_Data_FieldMap_Description": "Konfiguruje pole uživatelských dat (například e-mailu) v záznamu v LDAP. <br/> Například `{\"cn\":\"name\", \"mail\":\"email\"}` vybere jméno osoby z atributu `cn`, a jeho e-mail z atributu `mail`. Lze používat proměnné například: `{ \"#{givenName} #{sn}\": \"name\", \"mail\": \"email\" }` použije kombinaci křestínho jména a příjmení pro pole `name` v Rocket.Chat.<br/>Dostupná pole Rocket.Chatu jsou `name`, a `email`.",
"LDAP_Sync_User_Data_Groups": "Synchronizovat skupiny LDAP",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin": "Administrátor místnosti",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin_Description": "Pokud jsou automaticky vytvořeny kanály, které během synchronizace neexistují, stane se tento uživatel automaticky správcem kanálu.",
"LDAP_Sync_User_Data_Groups_AutoChannels_Description": "Povolením této funkce automaticky přidáte uživatele do kanálu na základě jejich skupiny LDAP. Pokud chcete uživatele také odebrat z kanálu, přečtěte si níže uvedenou možnost automatického odebrání uživatelů.",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap": "Mapa LDAP skupiny místností",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Default": "// Povolit automatické synchronizace skupin LDAP do výše uvedených místností",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Description": "Mapovat skupiny LDAP do kanálů Rocket.Chat. <br/>Jako příklad, \"{\"employee\":\"general\"}\" přidá do obecného kanálu libovolného uživatele ve skupině zaměstnanců LDAP.",
"LDAP_Sync_User_Data_Groups_AutoRemove": "Automaticky odebrat uživatelské role",
"LDAP_Sync_User_Data_Groups_AutoRemove_Description": "** Pozor **: Povolení této funkce automaticky odstraní uživatele z role, pokud nejsou přiřazeni v LDAP! Tím se odstraní pouze role, které jsou nastaveny v níže uvedené mapě skupin dat uživatelů.",
"LDAP_Sync_User_Data_Groups_BaseDN": "BaseDN LDAP skupiny",
"LDAP_Sync_User_Data_Groups_BaseDN_Description": "LDAP BaseDN užívaný k vyhledávání uživatelů.",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels": "Automaticky odebírat uživatele z místností",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels_Description": "** Pozor **: Povolení této funkce se odstraní všichni uživatelé v kanálu, kteří nemají korespondující skupinu LDAP! Povolte ji, pouze pokud víte, co děláte.",
"LDAP_Sync_User_Data_Groups_Filter": "Filtr skupiny uživatelů",
"LDAP_Sync_User_Data_Groups_Filter_Description": "Vyhledávací filtr LDAP používaný ke kontrole, zda je uživatel ve skupině.",
"LDAP_Sync_User_Data_GroupsMap": "Mapa skupin uživatelských dat",
"LDAP_Sync_User_Data_GroupsMap_Description": "Mapuje skupiny LDAP na Rocket.Chat uživatelské role <br/>Jako příklad bude \"{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}\" mapovat skupinu LDAP Rocket-admin do role „admin“ v Rocket.Chat.",
"LDAP_Test_Connection": "Test připojení",
"LDAP_Sync_User_Data_Roles": "Synchronizovat skupiny LDAP",
"LDAP_Sync_User_Data_Channels_Admin": "Administrátor místnosti",
"LDAP_Sync_User_Data_Channels_Admin_Description": "Pokud jsou automaticky vytvořeny kanály, které během synchronizace neexistují, stane se tento uživatel automaticky správcem kanálu.",
"LDAP_Sync_User_Data_Channels_BaseDN": "BaseDN LDAP skupiny",
"LDAP_Sync_User_Data_Channels_BaseDN_Description": "LDAP BaseDN užívaný k vyhledávání uživatelů.",
"LDAP_Sync_User_Data_Channels_Description": "Povolením této funkce automaticky přidáte uživatele do kanálu na základě jejich skupiny LDAP. Pokud chcete uživatele také odebrat z kanálu, přečtěte si níže uvedenou možnost automatického odebrání uživatelů.",
"LDAP_Sync_User_Data_Channels_Enforce_AutoChannels": "Automaticky odebírat uživatele z místností",
"LDAP_Sync_User_Data_Channels_Enforce_AutoChannels_Description": "** Pozor **: Povolení této funkce se odstraní všichni uživatelé v kanálu, kteří nemají korespondující skupinu LDAP! Povolte ji, pouze pokud víte, co děláte.",
"LDAP_Sync_User_Data_Channels_Filter": "Filtr skupiny uživatelů",
"LDAP_Sync_User_Data_Channels_Filter_Description": "Vyhledávací filtr LDAP používaný ke kontrole, zda je uživatel ve skupině.",
"LDAP_Sync_User_Data_ChannelsMap": "Mapa LDAP skupiny místností",
"LDAP_Sync_User_Data_ChannelsMap_Default": "// Povolit automatické synchronizace skupin LDAP do výše uvedených místností",
"LDAP_Sync_User_Data_ChannelsMap_Description": "Mapovat skupiny LDAP do kanálů Rocket.Chat. <br/>Jako příklad, \"{\"employee\":\"general\"}\" přidá do obecného kanálu libovolného uživatele ve skupině zaměstnanců LDAP.",
"LDAP_Sync_User_Data_Roles_AutoRemove": "Automaticky odebrat uživatelské role",
"LDAP_Sync_User_Data_Roles_AutoRemove_Description": "** Pozor **: Povolení této funkce automaticky odstraní uživatele z role, pokud nejsou přiřazeni v LDAP! Tím se odstraní pouze role, které jsou nastaveny v níže uvedené mapě skupin dat uživatelů.",
"LDAP_Sync_User_Data_Roles_BaseDN": "BaseDN LDAP skupiny",
"LDAP_Sync_User_Data_Roles_BaseDN_Description": "LDAP BaseDN užívaný k vyhledávání uživatelů.",
"LDAP_Sync_User_Data_Roles_Filter": "Filtr skupiny uživatelů",
"LDAP_Sync_User_Data_Roles_Filter_Description": "Vyhledávací filtr LDAP používaný ke kontrole, zda je uživatel ve skupině.",
"LDAP_Sync_User_Data_RolesMap": "Mapa skupin uživatelských dat",
"LDAP_Sync_User_Data_RolesMap_Description": "Mapuje skupiny LDAP na Rocket.Chat uživatelské role <br/>Jako příklad bude \"{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}\" mapovat skupinu LDAP Rocket-admin do role „admin“ v Rocket.Chat.",
"LDAP_Timeout": "Časový limit (ms)",
"LDAP_Timeout_Description": "Kolik milisekund počkat na vyhledávání než bude vrácena chyba",
"LDAP_Unique_Identifier_Field": "Jedinečný identifikátor",
@ -2238,8 +2229,6 @@
"LDAP_Username_Field": "Pole Uživatelského jména",
"LDAP_Username_Field_Description": "Které pole budou použity jako *Jméno* pro nové uživatele. Ponechte prázdné pro použítí jména z přihlašovací stránky. <br/> Můžete použít šablony a tagy jako například `#{givenName}.#{sn}`.<br/>Výchozí hodnota je `sAMAccountName`.",
"LDAP_Validate_Teams_For_Each_Login": "Validovat mapování pro každé přihlášení",
"LDAP_Validate_Roles_For_Each_Login": "Validovat mapování pro každé přihlášení",
"LDAP_Validate_Roles_For_Each_Login_Description": "Zda má validace probíhat při každém přihlášení (Opatrně, taková akce přepíše uživatelské role při každém přihlášení. V opačném případě je validováno jen při vytvoření uživatele",
"Lead_capture_email_regex": "Regulární výraz pro zachycení Leadu na email",
"Lead_capture_phone_regex": "Regulární výraz pro zachycení Leadu na telefon",
"Leave": "Opustit",

@ -1516,7 +1516,6 @@
"LDAP_Connect_Timeout": "Amserlen Cysylltiad (ms)",
"LDAP_Default_Domain": "Maes Diofyn",
"LDAP_Default_Domain_Description": "Os rhoddir y Parth Diofyn yn cael ei ddefnyddio i greu e-bost unigryw i ddefnyddwyr lle na chafodd yr e-bost ei fewnforio o LDAP. Bydd yr e-bost yn cael ei osod fel `username @ default_domain` neu` unique_id @ default_domain`. <br / >Enghraifft: `rocket.chat`",
"LDAP_Description": "Cronfa ddata hierarchaidd yw LDAP y mae llawer o gwmnïau'n ei ddefnyddio i ddarparu arwydd sengl - cyfleuster ar gyfer rhannu un cyfrinair rhwng lluosog o safleoedd a gwasanaethau. I gael gwybodaeth ac enghreifftiau datrysiadau uwch, ewch i'n wiki: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Galluogi",
"LDAP_Enable_Description": "Ceisiwch ddefnyddio LDAP ar gyfer dilysu.",
"LDAP_Encryption": "Amgryptio",
@ -1558,11 +1557,6 @@
"LDAP_Sync_Now": "Cefndir Sync Nawr",
"LDAP_Sync_Now_Description": "A fydd yn gweithredu ** Cefndir Sync ** nawr yn hytrach na disgwyl ** Cyfnod Sync ** hyd yn oed os yw ** Cefndir Sync ** yn Ffug. <br / >Mae'r Weithred hon yn asyncronous, gweler y logiau i gael rhagor o wybodaeth am y broses",
"LDAP_Sync_User_Avatar": "Sync Defnyddiwr Avatar",
"LDAP_Sync_User_Data": "Sync Data Defnyddiwr",
"LDAP_Sync_User_Data_Description": "Cadwch ddata defnyddwyr mewn sync gyda gweinyddwr ** ** mewngofnodi ** neu ar ** sync cefndir ** (ee: enw, e-bost).",
"LDAP_Sync_User_Data_FieldMap": "Map Maes Data Defnyddwyr",
"LDAP_Sync_User_Data_FieldMap_Description": "Ffurfweddu sut mae meysydd cyfrif defnyddwyr (fel e-bost) wedi'u poblogi o gofnod yn LDAP (unwaith y darganfyddwyd). <br / >Fel enghraifft, `{\" cn \":\" name \",\" mail \":\" email \"}` bydd yn dewis enw darllenadwy dynol y person o'r priodwedd cn, a'i e-bost o briodoldeb y post. Yn ogystal, mae'n bosibl defnyddio newidynnau, er enghraifft: `{\" # {givenName} # {sn} \":\" name \",\" mail \":\" email \"}` yn defnyddio cyfuniad o enw cyntaf y defnyddiwr ac enw olaf y roc sgwrsio `enw` maes. <br / >Caeau sydd ar gael yn Rocket.Chat:` name`, `email` and` customFields`.",
"LDAP_Test_Connection": "Cysylltiad Prawf",
"LDAP_Timeout": "Amserlen (ms)",
"LDAP_Timeout_Description": "Faint o filltiroedd milltir sy'n aros am ganlyniad chwilio cyn dychwelyd gwall",
"LDAP_Unique_Identifier_Field": "Maes Adnabod Unigryw",

@ -2161,12 +2161,8 @@
"LDAP_Connect_Timeout": "Forbindelses-timeout (ms)",
"LDAP_Default_Domain": "Standarddomæne",
"LDAP_Default_Domain_Description": "Hvis det leveres, vil standarddomenet blive brugt til at oprette en unik e-mail til brugere, hvor e-mail ikke blev importeret fra LDAP. E-mailen vil blive monteret som `brugernavn @ default_domain` eller` unique_id @ default_domain`.<br/>Eksempel: `rocket.chat`",
"LDAP_Default_Role_To_User": "Standardrolle for bruger",
"LDAP_Default_Role_To_User_Description": "Standard RC-rolle der skal tildeles en bruger, hvis brugeren har en LDAP-rolle der ikke er knyttet til noget",
"LDAP_Description": "LDAP er en hierarkisk database, som mange virksomheder bruger til at give et enkelt tegn på - en facilitet til deling af en adgangskode mellem flere websteder og tjenester. For avanceret konfigurationsinformation og eksempler, se venligst vores wiki: https://rocket.chat/docs/administrator-guides/authentication/ldap/.",
"LDAP_Enable": "Aktivér",
"LDAP_Enable_Description": "Forsøg at anvende LDAP til godkendelse.",
"LDAP_Enable_LDAP_Roles_To_RC_Roles": "Aktivér rolletilknytning fra LDAP til Rocket.Chat",
"LDAP_Encryption": "Kryptering",
"LDAP_Encryption_Description": "Krypteringsmetoden bruges til at sikre kommunikation til LDAP-serveren. Eksempler er `plain` (ingen kryptering),` SSL / LDAPS` (krypteret fra starten) og `StartTLS` (opgradering til krypteret kommunikation, når den er tilsluttet).",
"LDAP_Find_User_After_Login": "Find brugeren efter login",
@ -2195,15 +2191,11 @@
"LDAP_Merge_Existing_Users_Description": "* Forsigtig! * Når der importeres en bruger fra LDAP, og en bruger med samme brugernavn allerede eksisterer, indstilles LDAP-info og adgangskode til den eksisterende bruger.",
"LDAP_Port": "Port",
"LDAP_Port_Description": "Port for at få adgang til LDAP. fx: `389` eller` 636` for LDAPS",
"LDAP_Query_To_Get_User_Groups": "LDAP-forespørgsel for at hente brugergrupper",
"LDAP_Query_To_Get_User_Groups_Description": "LDAP-forespørgsel for at hente de LDAP-grupper, som brugeren er en del af.",
"LDAP_Query_To_Get_User_Teams": "LDAP-forespørgsel for at hente brugergrupper",
"LDAP_Reconnect": "Tilslut",
"LDAP_Reconnect_Description": "Prøv at genoprette forbindelse automatisk, når forbindelsen afbrydes af en eller anden grund under udførelsen af operationer",
"LDAP_Reject_Unauthorized": "Afvis uautoriseret",
"LDAP_Reject_Unauthorized_Description": "Deaktiver denne mulighed for at tillade certifikater, der ikke kan verificeres. Normalt vil selvkendte certifikater kræve, at denne indstilling er deaktiveret til arbejde",
"LDAP_Roles_To_Rocket_Chat_Roles": "Rolletilknytning fra LDAP til Rocket.Chat.",
"LDAP_Roles_To_Rocket_Chat_Roles_Description": "Rolletilknytning i objektformat hvor objektnøglen skal være LDAP-rollen og objektværdien skal være en array af RC-roller. Eksempel: { 'ldapRole': ['rcRole', 'anotherRCRole'] }",
"LDAP_Search_Page_Size": "Søg sidestørrelse",
"LDAP_Search_Page_Size_Description": "Det maksimale antal indlæg hver resultatside vender tilbage for at blive behandlet",
"LDAP_Search_Size_Limit": "Søg størrelsesbegrænsning",
@ -2216,29 +2208,28 @@
"LDAP_Sync_User_Active_State_Disable": "Deaktiver brugere",
"LDAP_Sync_User_Active_State_Nothing": "Gør ikke noget",
"LDAP_Sync_User_Avatar": "Synkroniser brugerens Avatar",
"LDAP_Sync_User_Data": "Synkroniser brugerdata",
"LDAP_Sync_User_Data_Description": "Hold brugerdata synkroniseret med serveren på **login** eller på **baggrunds synkronisering** (fx: navn, email).",
"LDAP_Sync_User_Data_FieldMap": "Brugerdata felt-mapping",
"LDAP_Sync_User_Data_FieldMap_Description": "Konfigurer, hvordan brugerkonto felter (som e-mail) er befolket fra en post i LDAP (en gang fundet).<br/>Som eksempel kan '{\"cn\": \"navn\", \"mail\": \"email\"} `vælge en persons menneskelige læsbare navn fra cn-attributten og deres e-mail fra postattributtet. Derudover er det muligt at bruge variabler, for eksempel: `{\" # {givenName} # {sn} \":\" navn \",\" mail \":\" email \"}` bruger en kombination af brugerens fornavn og efternavn til rocket chat `name`-feltet. <br/>Fås i felterne i Rocket.Chat:` name`, `email` og` customFields`.",
"LDAP_Sync_User_Data_Groups": "Synkroniser LDAP-grupper",
"LDAP_Sync_User_Data_Groups_AutoChannels": "Automatisk synkronisering af LDAP-grupper til kanaler",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin": "Channel Admin",
"LDAP_Sync_User_Data_Groups_AutoChannels_Admin_Description": "Når der automatisk oprettes kanaler, der ikke findes under en synkronisering, bliver denne bruger automatisk administrator for kanalen.",
"LDAP_Sync_User_Data_Groups_AutoChannels_Description": "Aktivér denne funktion for automatisk at tilføje brugere til en kanal baseret på deres LDAP-gruppe. Hvis du også gerne vil fjerne brugere fra en kanal, kan du se indstillingen nedenfor vedrørende automatisk fjernelse af brugere.",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap": "LDAP-gruppe kanaloversigt",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Default": "// Automatisk synkronisering af LDAP-grupper til kanaler ovenfor",
"LDAP_Sync_User_Data_Groups_AutoChannelsMap_Description": "Kortlæg LDAP-grupper til Rocket.Chat-kanaler. <br/>. F.eks. tilføjer '{\"employee\":\"general\"}' enhver bruger i LDAP-gruppe-medarbejderen til den generelle kanal.",
"LDAP_Sync_User_Data_Groups_AutoRemove": "Fjern automatisk brugerroller",
"LDAP_Sync_User_Data_Groups_AutoRemove_Description": "** OBS **: Aktivering af dette fjerner automatisk brugere fra en rolle, hvis de ikke er tildelt i LDAP! Dette fjerner kun roller automatisk der er indstillet under brugerdata-gruppeoversigten nedenfor.",
"LDAP_Sync_User_Data_Groups_BaseDN": "LDAP Group BaseDN",
"LDAP_Sync_User_Data_Groups_BaseDN_Description": "LDAP BaseDN til at slå brugere op med.",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels": "Fjern automatisk brugere fra kanaler",
"LDAP_Sync_User_Data_Groups_Enforce_AutoChannels_Description": "** OBS **: Aktivering af dette fjerner alle brugere i en kanal, der ikke har den tilsvarende LDAP-gruppe! Aktivér kun dette hvis du ved hvad du laver.",
"LDAP_Sync_User_Data_Groups_Filter": "Brugergruppefilter",
"LDAP_Sync_User_Data_Groups_Filter_Description": "LDAP-søgefilteret der bruges til at kontrollere om en bruger er i en gruppe.",
"LDAP_Sync_User_Data_GroupsMap": "Brugerdata-gruppeoversigt",
"LDAP_Sync_User_Data_GroupsMap_Description": "Relatér LDAP-grupper til Rocket.Chat-brugerroller <br/> F.eks. vil '{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}' relatere rocket-admin-LDAP-gruppen til Rockets \"admin\" -rolle.",
"LDAP_Test_Connection": "Testforbindelse",
"LDAP_Sync_User_Data_Roles": "Synkroniser LDAP-grupper",
"LDAP_Sync_User_Data_Channels": "Automatisk synkronisering af LDAP-grupper til kanaler",
"LDAP_Sync_User_Data_Channels_Admin": "Channel Admin",
"LDAP_Sync_User_Data_Channels_Admin_Description": "Når der automatisk oprettes kanaler, der ikke findes under en synkronisering, bliver denne bruger automatisk administrator for kanalen.",
"LDAP_Sync_User_Data_Channels_BaseDN": "LDAP Group BaseDN",
"LDAP_Sync_User_Data_Channels_BaseDN_Description": "LDAP BaseDN til at slå brugere op med.",
"LDAP_Sync_User_Data_Channels_Description": "Aktivér denne funktion for automatisk at tilføje brugere til en kanal baseret på deres LDAP-gruppe. Hvis du også gerne vil fjerne brugere fra en kanal, kan du se indstillingen nedenfor vedrørende automatisk fjernelse af brugere.",
"LDAP_Sync_User_Data_Channels_Enforce_AutoChannels": "Fjern automatisk brugere fra kanaler",
"LDAP_Sync_User_Data_Channels_Enforce_AutoChannels_Description": "** OBS **: Aktivering af dette fjerner alle brugere i en kanal, der ikke har den tilsvarende LDAP-gruppe! Aktivér kun dette hvis du ved hvad du laver.",
"LDAP_Sync_User_Data_Channels_Filter": "Brugergruppefilter",
"LDAP_Sync_User_Data_Channels_Filter_Description": "LDAP-søgefilteret der bruges til at kontrollere om en bruger er i en gruppe.",
"LDAP_Sync_User_Data_ChannelsMap": "LDAP-gruppe kanaloversigt",
"LDAP_Sync_User_Data_ChannelsMap_Default": "// Automatisk synkronisering af LDAP-grupper til kanaler ovenfor",
"LDAP_Sync_User_Data_ChannelsMap_Description": "Kortlæg LDAP-grupper til Rocket.Chat-kanaler. <br/>. F.eks. tilføjer '{\"employee\":\"general\"}' enhver bruger i LDAP-gruppe-medarbejderen til den generelle kanal.",
"LDAP_Sync_User_Data_Roles_AutoRemove": "Fjern automatisk brugerroller",
"LDAP_Sync_User_Data_Roles_AutoRemove_Description": "** OBS **: Aktivering af dette fjerner automatisk brugere fra en rolle, hvis de ikke er tildelt i LDAP! Dette fjerner kun roller automatisk der er indstillet under brugerdata-gruppeoversigten nedenfor.",
"LDAP_Sync_User_Data_Roles_BaseDN": "LDAP Group BaseDN",
"LDAP_Sync_User_Data_Roles_BaseDN_Description": "LDAP BaseDN til at slå brugere op med.",
"LDAP_Sync_User_Data_Roles_Filter": "Brugergruppefilter",
"LDAP_Sync_User_Data_Roles_Filter_Description": "LDAP-søgefilteret der bruges til at kontrollere om en bruger er i en gruppe.",
"LDAP_Sync_User_Data_RolesMap": "Brugerdata-gruppeoversigt",
"LDAP_Sync_User_Data_RolesMap_Description": "Relatér LDAP-grupper til Rocket.Chat-brugerroller <br/> F.eks. vil '{\"rocket-admin\":\"admin\", \"tech-support\":\"support\"}' relatere rocket-admin-LDAP-gruppen til Rockets \"admin\" -rolle.",
"LDAP_Timeout": "Timeout (ms)",
"LDAP_Timeout_Description": "Hvor mange mileseconds venter på et søgeresultat, før du returnerer en fejl",
"LDAP_Unique_Identifier_Field": "Unikt identifikationsfelt",
@ -2251,8 +2242,6 @@
"LDAP_Username_Field": "Brugernavns-felt",
"LDAP_Username_Field_Description": "Hvilket felt vil blive brugt som * brugernavn * til nye brugere. Lad være tom for at bruge brugernavnet informeret på login side.<br/>Du kan også bruge skabeloner, som `# {givenName}. # {Sn}`. <br/>Standardværdien er `sAMAccountName`.",
"LDAP_Validate_Teams_For_Each_Login": "Validér tilknytning for hvert login",
"LDAP_Validate_Roles_For_Each_Login": "Validér tilknytning for hvert login",
"LDAP_Validate_Roles_For_Each_Login_Description": "Hvis validering skal ske for hvert login (Vær forsigtig med denne indstilling, fordi den vil overskrive brugerrollerne i hvert login. Ellers valideres dette kun på det tidspunkt hvor brugeren oprettes).",
"Lead_capture_email_regex": "Lead capture email regex",
"Lead_capture_phone_regex": "Lead capture phone regex",
"Leave": "Forlad",

@ -1522,7 +1522,6 @@
"LDAP_Connect_Timeout": "Verbindungstimeout (ms)",
"LDAP_Default_Domain": "Standard-Domain",
"LDAP_Default_Domain_Description": "Wenn die Standarddomäne bereitgestellt wird, wird eine eindeutige E-Mail für Benutzer erstellt, deren E-Mail nicht aus LDAP importiert wurde. Die E-Mail wird als `username @ default_domain` oder` unique_id @ default_domain` gemountet.<br/>Beispiel: `rocket.chat`",
"LDAP_Description": "LDAP ist ein Frontend zu hierarchischen Datenbanken, die viele Unternehmen nutzen, um eine eine Einmalanmeldung (SSO) zu ermöglichen. Über SSO kann \"ein Benutzer nach einer einmaligen Authentifizierung an einem Arbeitsplatz auf alle Rechner und Dienste, für die er lokal berechtigt ist, am selben Arbeitsplatz zugreifen kann, ohne sich jedes Mal neu anmelden zu müssen\". Genauere Informationen zur Konfiguration von LDAP mit Konfigurationsbeispielen erhalten Sie unter folgendem Link: https://rocket.chat/docs/administrator-guides/authentication/ldap/",
"LDAP_Enable": "LDAP aktivieren",
"LDAP_Enable_Description": "LDAP für die Authentifizierung verwenden.",
"LDAP_Encryption": "Verschlüsselung",
@ -1564,11 +1563,6 @@
"LDAP_Sync_Now": "Hintergrund-Synchronisierung jetzt",
"LDAP_Sync_Now_Description": "Wird die ** Hintergrundsynchronisation ** jetzt ausführen, anstatt das ** Synchronisierungsintervall ** zu warten, selbst wenn ** Hintergrundsynchronisation ** Falsch ist.<br/>Diese Aktion ist asynchron, bitte sehen Sie in den Protokollen nach, um weitere Informationen zu erhalten verarbeiten",
"LDAP_Sync_User_Avatar": "Profilbilder synchronisieren",
"LDAP_Sync_User_Data": "Daten synchronisieren",
"LDAP_Sync_User_Data_Description": "Bei der Anmeldung die Benutzerdaten mit dem Server synchronisieren (Beispiel: Name, E-Mail).",
"LDAP_Sync_User_Data_FieldMap": "Nutzerdaten-Feldkarte",
"LDAP_Sync_User_Data_FieldMap_Description": "Konfigurieren Sie, wie Benutzer-Account-Felder (wie die E-Mail-Adresse) aus einem LDAP-Datensatz (falls gefunden) geladen werden.<br/>Beispiel: {\"cn\":\"name\", \"mail\":\"email\"} nimmt einen von Menschen lesbaren Namen von dem cn-Attribut und die E-Mail-Adresse vom Mail-Attribut.<br/>Verfügbare Felder beinhalten den Namen und die E-Mail-Adresse.",
"LDAP_Test_Connection": "Testverbindung",
"LDAP_Timeout": "Zeitüberschreitung (ms)",
"LDAP_Timeout_Description": "Wie viele Meilen warten auf ein Suchergebnis, bevor ein Fehler zurückgegeben wird",
"LDAP_Unique_Identifier_Field": "Eindeutige Kennung des Felds",

@ -1765,7 +1765,6 @@
"LDAP_Connect_Timeout": "Verbindungs-Timeout (ms)",
"LDAP_Default_Domain": "Standard-Domain",
"LDAP_Default_Domain_Description": "Wenn eine Standard-Domäne angegeben wurde, wird diese zur Erzeugung von E-Mail-Adressen verwendet, sofern keine E-Mail-Adresse aus dem LDAP importiert wurde. Die E-Mail wird konstruiert als `benutzername@standard-domäne` oder `unique_id@standard-domäne`<br/>Beispiel: `rocket.chat`",
"LDAP_Description": "LDAP ist eine hierarchische Datenbank, die viele Unternehmen nutzen, um eine eine Einmalanmeldung (SSO) zu ermöglichen. Über SSO kann \"ein Benutzer nach einer einmaligen Authentifizierung an einem Arbeitsplatz auf alle Rechner und Dienste, für die er lokal berechtigt ist, am selben Arbeitsplatz zugreifen kann, ohne sich jedes Mal neu anmelden zu müssen\". Genauere Informationen zur Konfiguration von LDAP mit Konfigurationsbeispielen erhältst Du unter folgendem Link: https://rocket.chat/docs/administrator-guides/authentication/ldap/",
"LDAP_Enable": "LDAP",
"LDAP_Enable_Description": "LDAP zur Authentifizierung verwenden",
"LDAP_Encryption": "Verschlüsselung",
@ -1807,11 +1806,6 @@
"LDAP_Sync_Now": "Jetzt im Hintergrund synchronisieren",
"LDAP_Sync_Now_Description": "Führt jetzt eine **Synchronisierung im Hintergrund** aus, anstatt auf die nächste planmäßige Synchronisierung zu warten. Das funktioniert auch, wenn die Synchronisierung im Hintergrund deaktiviert ist. <br/>Die Aktion läuft asynchron ab, der Fortschritt kann im Log verfolgt werden.",
"LDAP_Sync_User_Avatar": "Profilbilder synchronisieren",
"LDAP_Sync_User_Data": "Benutzerdaten synchronisieren",
"LDAP_Sync_User_Data_Description": "Bei der Anmeldung die Benutzerdaten mit dem Server synchronisieren (Bspw. Name, E-Mail-Adresse).",
"LDAP_Sync_User_Data_FieldMap": "Zuordnung der Benutzer-Attribute",
"LDAP_Sync_User_Data_FieldMap_Description": "Konfiguriere, wie Benutzer-Account-Eigenschaften (wie die E-Mail-Adresse) aus einem LDAP-Datensatz (falls gefunden) geladen werden.<br/>Beispiel: {\"cn\":\"name\", \"mail\":\"email\"} nimmt einen von Menschen lesbaren Namen aus dem cn-Attribut und die E-Mail-Adresse aus dem Mail-Attribut. Zusätzlich ist die Verwendung von Variablen möglich, wie z.B.: `{ \"#{givenName} #{sn}\": \"name\", \"mail\": \"email\" }`. Hierbei wird eine Kombination des Vor- und Nachnamens verwendet.<br/>Verfügbare Felder in Rocket.Chat sind `name` und `email`.",
"LDAP_Test_Connection": "Verbindung prüfen",
"LDAP_Timeout": "Timeout (ms)",
"LDAP_Timeout_Description": "Wie lange auf ein Suchergebnis gewartet werden soll, bevor ein Fehler ausgegeben wird",
"LDAP_Unique_Identifier_Field": "Feld für eindeutige Identifizierung",

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save