[BREAK] Moved SAML custom field map to EE (#23319)

Co-authored-by: Diego Sampaio <chinello@gmail.com>
pull/23328/head
Leonardo Ostjen Couto 4 years ago committed by GitHub
parent 6c55ddc817
commit 932be7bc42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/meteor-accounts-saml/server/definition/IAttributeMapping.ts
  2. 1
      app/meteor-accounts-saml/server/definition/ISAMLUser.ts
  3. 4
      app/meteor-accounts-saml/server/lib/SAML.ts
  4. 17
      app/meteor-accounts-saml/server/lib/Utils.ts
  5. 4
      app/meteor-accounts-saml/server/loginHandler.ts
  6. 86
      app/meteor-accounts-saml/tests/server.tests.ts
  7. 8
      app/models/server/models/Users.js
  8. 21
      ee/server/configuration/saml.ts
  9. 10
      ee/server/settings/saml.ts
  10. 4
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -5,7 +5,6 @@ export interface IAttributeMapping {
}
export interface IUserDataMap {
customFields: Map<string, IAttributeMapping>;
attributeList: Set<string>;
identifier: {
type: string;

@ -1,5 +1,4 @@
export interface ISAMLUser {
customFields: Map<string, any>;
emailList: Array<string>;
fullName: string | null;
roles: Array<string>;

@ -170,10 +170,6 @@ export class SAML {
updateData[`services.saml.${ customIdentifierAttributeName }`] = userObject.attributeList.get(customIdentifierAttributeName);
}
for (const [customField, value] of userObject.customFields) {
updateData[`customFields.${ customField }`] = value;
}
// Overwrite mail if needed
if (mailOverwrite === true && (customIdentifierMatch === true || immutableProperty !== 'EMail')) {
updateData.emails = emails;

@ -202,6 +202,7 @@ export class SAMLUtils {
public static getUserDataMapping(): IUserDataMap {
const { userDataFieldMap, immutableProperty } = globalSettings;
const defaultMappingFields = ['email', 'name', 'username'];
let map: Record<string, any>;
try {
@ -213,7 +214,6 @@ export class SAMLUtils {
}
const parsedMap: IUserDataMap = {
customFields: new Map(),
attributeList: new Set(),
email: {
fieldName: 'email',
@ -235,6 +235,9 @@ export class SAMLUtils {
if (!map.hasOwnProperty(spFieldName)) {
continue;
}
if (!defaultMappingFields.includes(spFieldName)) {
continue;
}
const attribute = map[spFieldName];
if (typeof attribute !== 'string' && typeof attribute !== 'object') {
@ -298,12 +301,11 @@ export class SAMLUtils {
if (attributeMap) {
if (spFieldName === 'email' || spFieldName === 'username' || spFieldName === 'name') {
parsedMap[spFieldName] = attributeMap;
} else {
parsedMap.customFields.set(spFieldName, attributeMap);
}
}
}
if (identifier) {
const defaultTypes = [
'email',
@ -318,7 +320,6 @@ export class SAMLUtils {
parsedMap.attributeList.add(identifier);
}
}
return parsedMap;
}
@ -432,7 +433,6 @@ export class SAMLUtils {
}
attributeList.set(attributeName, profile[attributeName]);
}
const email = this.getProfileValue(profile, userDataMap.email);
const profileUsername = this.getProfileValue(profile, userDataMap.username, true);
const name = this.getProfileValue(profile, userDataMap.name);
@ -443,7 +443,6 @@ export class SAMLUtils {
}
const userObject: ISAMLUser = {
customFields: new Map(),
samlLogin: {
provider: this.relayState,
idp: profile.issuer,
@ -474,12 +473,6 @@ export class SAMLUtils {
}
}
for (const [fieldName, customField] of userDataMap.customFields) {
const value = this.getProfileValue(profile, customField);
if (value) {
userObject.customFields.set(fieldName, value);
}
}
this.events.emit('mapUser', { profile, userObject });

@ -30,8 +30,10 @@ Accounts.registerLoginHandler('saml', function(loginRequest) {
try {
const userObject = SAMLUtils.mapProfileToUserObject(loginResult.profile);
const updatedUser = SAML.insertOrUpdateSAMLUser(userObject);
SAMLUtils.events.emit('updateCustomFields', loginResult, updatedUser);
return SAML.insertOrUpdateSAMLUser(userObject);
return updatedUser;
} catch (error: any) {
SystemLogger.error(error);

@ -632,9 +632,6 @@ describe('SAML', () => {
username: 'anotherUsername',
email: 'singleEmail',
name: 'anotherName',
customField1: 'customField1',
customField2: 'customField2',
customField3: 'customField3',
};
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
@ -654,13 +651,6 @@ describe('SAML', () => {
expect(userObject).to.have.property('username').that.is.equal('[AnotherUserName]');
expect(userObject).to.have.property('roles').that.is.an('array').with.members(['user']);
expect(userObject).to.have.property('channels').that.is.an('array').with.members(['pets', 'pics', 'funny', 'random', 'babies']);
const map = new Map();
map.set('customField1', 'value1');
map.set('customField2', 'value2');
map.set('customField3', 'value3');
expect(userObject).to.have.property('customFields').that.is.a('Map').and.is.deep.equal(map);
});
it('should join array values if username receives an array of values', () => {
@ -850,59 +840,6 @@ describe('SAML', () => {
expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('user-1');
});
it('should collect the values of every attribute on the field map', () => {
const { globalSettings } = SAMLUtils;
const fieldMap = {
username: 'anotherUsername',
email: 'singleEmail',
name: 'anotherName',
others: {
fieldNames: [
'issuer',
'sessionIndex',
'nameID',
'displayName',
'username',
'roles',
'otherRoles',
'language',
'channels',
'customField1',
],
},
};
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
SAMLUtils.updateGlobalSettings(globalSettings);
const userObject = SAMLUtils.mapProfileToUserObject(profile);
expect(userObject).to.be.an('object');
expect(userObject).to.have.property('attributeList').that.is.a('Map').that.have.keys([
'anotherUsername',
'singleEmail',
'anotherName',
'issuer',
'sessionIndex',
'nameID',
'displayName',
'username',
'roles',
'otherRoles',
'language',
'channels',
'customField1',
]);
// Workaround because chai doesn't handle Maps very well
for (const [key, value] of userObject.attributeList) {
// @ts-ignore
expect(value).to.be.equal(profile[key]);
}
});
it('should use the immutable property as default identifier', () => {
const { globalSettings } = SAMLUtils;
@ -920,26 +857,6 @@ describe('SAML', () => {
expect(newUserObject).to.be.an('object');
expect(newUserObject).to.have.property('identifier').that.has.property('type').that.is.equal('username');
});
it('should collect the identifier from the fieldset', () => {
const { globalSettings } = SAMLUtils;
const fieldMap = {
username: 'anotherUsername',
email: 'singleEmail',
name: 'anotherName',
__identifier__: 'customField3',
};
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
SAMLUtils.updateGlobalSettings(globalSettings);
const userObject = SAMLUtils.mapProfileToUserObject(profile);
expect(userObject).to.be.an('object');
expect(userObject).to.have.property('identifier').that.has.property('type').that.is.equal('custom');
expect(userObject).to.have.property('identifier').that.has.property('attribute').that.is.equal('customField3');
});
});
});
@ -969,7 +886,6 @@ describe('SAML', () => {
template: 'user-__uid__',
},
email: 'email',
epa: 'eduPersonAffiliation',
};
globalSettings.userDataFieldMap = JSON.stringify(fieldMap);
@ -991,8 +907,6 @@ describe('SAML', () => {
const map = new Map();
map.set('epa', 'group1');
expect(userObject).to.have.property('customFields').that.is.a('Map').and.is.deep.equal(map);
});
});
});

@ -1603,6 +1603,14 @@ Find users to send a message by email if:
return this.find(query, options);
}
updateCustomFieldsById(userId, customFields) {
return this.update(userId, {
$set: {
customFields,
},
});
}
}
export default new Users(Meteor.users, true);

@ -3,6 +3,8 @@ import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definit
import { SAMLUtils } from '../../../app/meteor-accounts-saml/server/lib/Utils';
import { settings } from '../../../app/settings/server';
import { addSettings } from '../settings/saml';
import { Users } from '../../../app/models/server';
onLicense('saml-enterprise', () => {
SAMLUtils.events.on('mapUser', ({ profile, userObject }: { profile: Record<string, any>; userObject: ISAMLUser}) => {
@ -38,6 +40,25 @@ onLicense('saml-enterprise', () => {
metadataTemplate: settings.get(`${ service }_Metadata_template`),
});
});
SAMLUtils.events.on('updateCustomFields', (loginResult: Record<string, any>, updatedUser: {userId: string; token: string}) => {
const userDataCustomFieldMap = settings.get('SAML_Custom_Default_user_data_custom_fieldmap') as string;
const customMap: Record<string, any> = JSON.parse(userDataCustomFieldMap);
const customFieldsList: Record<string, any> = {};
for (const spCustomFieldName in customMap) {
if (!customMap.hasOwnProperty(spCustomFieldName)) {
continue;
}
const customAttribute = customMap[spCustomFieldName];
const value = SAMLUtils.getProfileValue(loginResult.profile, { fieldName: spCustomFieldName });
customFieldsList[customAttribute] = value;
}
Users.updateCustomFieldsById(updatedUser.userId, customFieldsList);
});
});
// For setting creation we add the listener first because the event is emmited during startup

@ -108,6 +108,16 @@ export const addSettings = function(name: string): void {
multiline: true,
});
});
this.section('SAML_Section_5_Mapping', function() {
// Data Mapping Settings
this.add(`SAML_Custom_${ name }_user_data_custom_fieldmap`, '{"custom1":"custom1", "custom2":"custom2", "custom3":"custom3"}', {
type: 'string',
invalidValue: '',
i18nLabel: 'SAML_Custom_user_data_custom_fieldmap',
i18nDescription: 'SAML_Custom_user_data_custom_fieldmap_description',
multiline: true,
});
});
});
});
};

@ -3659,7 +3659,9 @@
"SAML_Custom_signature_validation_type": "Signature Validation Type",
"SAML_Custom_signature_validation_type_description": "This setting will be ignored if no Custom Certificate is provided.",
"SAML_Custom_user_data_fieldmap": "User Data Field Map",
"SAML_Custom_user_data_fieldmap_description": "Configure how user account fields (like email) are populated from a record in SAML (once found). \nAs an example, `{\"name\":\"cn\", \"email\":\"mail\"}` will choose a person's human readable name from the cn attribute, and their email from the mail attribute.\nAvailable fields in Rocket.Chat: `name`, `email` and `username`, everything else will be saved as `customFields`.\nAssign the name of an immutable attribute to the '__identifier__' key to use it as user identifier.\nYou can also use regexes and templates. Templates will be processed first except when they reference the result of the regex.\n```{\n \"email\": \"mail\",\n \"username\": {\n \"fieldName\": \"mail\",\n \"regex\": \"(.*)@.+$\",\n \"template\": \"user-__regex__\"\n },\n \"name\": {\n \"fieldNames\": [\n \"firstName\",\n \"lastName\"\n ],\n \"template\": \"__firstName__ __lastName__\"\n },\n \"__identifier__\": \"uid\"\n}```\n",
"SAML_Custom_user_data_fieldmap_description": "Configure how user account fields (like email) are populated from a record in SAML (once found). \nAs an example, `{\"name\":\"cn\", \"email\":\"mail\"}` will choose a person's human readable name from the cn attribute, and their email from the mail attribute.\nAvailable fields in Rocket.Chat: `name`, `email` and `username`, everything else will be discarted.\n```{\n \"email\": \"mail\",\n \"username\": {\n \"fieldName\": \"mail\",\n \"regex\": \"(.*)@.+$\",\n \"template\": \"user-__regex__\"\n },\n \"name\": {\n \"fieldNames\": [\n \"firstName\",\n \"lastName\"\n ],\n \"template\": \"__firstName__ __lastName__\"\n },\n \"__identifier__\": \"uid\"\n}\n",
"SAML_Custom_user_data_custom_fieldmap": "User Data Custom Field Map",
"SAML_Custom_user_data_custom_fieldmap_description": "Configure how user custom fields are populated from a record in SAML (once found).",
"SAML_Custom_Username_Field": "Username field name",
"SAML_Custom_Username_Normalize": "Normalize username",
"SAML_Custom_Username_Normalize_Lowercase": "To Lowercase",

Loading…
Cancel
Save