diff --git a/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts b/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts index 126a300eea0..a7fe88d1910 100644 --- a/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts +++ b/app/meteor-accounts-saml/server/definition/ISAMLGlobalSettings.ts @@ -4,8 +4,6 @@ export interface ISAMLGlobalSettings { mailOverwrite: boolean; immutableProperty: string; defaultUserRole: string; - roleAttributeName: string; - roleAttributeSync: boolean; userDataFieldMap: string; usernameNormalize: string; channelsAttributeUpdate: boolean; diff --git a/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts b/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts index db045b2e6c6..acc67681fcd 100644 --- a/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts +++ b/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts @@ -9,8 +9,6 @@ export interface IServiceProviderOptions { customAuthnContext: string; authnContextComparison: string; defaultUserRole: string; - roleAttributeName: string; - roleAttributeSync: boolean; allowedClockDrift: number; signatureValidationType: string; identifierFormat: string; diff --git a/app/meteor-accounts-saml/server/lib/SAML.ts b/app/meteor-accounts-saml/server/lib/SAML.ts index 9327aa44ac7..e6e3a690746 100644 --- a/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/app/meteor-accounts-saml/server/lib/SAML.ts @@ -72,7 +72,7 @@ export class SAML { } public static insertOrUpdateSAMLUser(userObject: ISAMLUser): {userId: string; token: string} { - const { roleAttributeSync, generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate } = SAMLUtils.globalSettings; + const { generateUsername, immutableProperty, nameOverwrite, mailOverwrite, channelsAttributeUpdate } = SAMLUtils.globalSettings; let customIdentifierMatch = false; let customIdentifierAttributeName: string | null = null; @@ -103,8 +103,8 @@ export class SAML { address: email, verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), })); - const globalRoles = userObject.roles; + const { roles } = userObject; let { username } = userObject; const active = !settings.get('Accounts_ManuallyApproveNewUsers'); @@ -113,7 +113,7 @@ export class SAML { const newUser: Record = { name: userObject.fullName, active, - globalRoles, + globalRoles: roles, emails, services: { saml: { @@ -184,8 +184,8 @@ export class SAML { updateData.name = userObject.fullName; } - if (roleAttributeSync) { - updateData.roles = globalRoles; + if (roles) { + updateData.roles = roles; } if (userObject.channels && channelsAttributeUpdate === true) { @@ -216,7 +216,7 @@ export class SAML { res.writeHead(200); res.write(serviceProvider.generateServiceProviderMetadata()); res.end(); - } catch (err) { + } catch (err: any) { showErrorMessage(res, err); } } @@ -300,7 +300,7 @@ export class SAML { redirect(url); }); - } catch (e) { + } catch (e: any) { SystemLogger.error(e); redirect(); } @@ -472,7 +472,7 @@ export class SAML { } } } - } catch (err) { + } catch (err: any) { SystemLogger.error(err); } } diff --git a/app/meteor-accounts-saml/server/lib/Utils.ts b/app/meteor-accounts-saml/server/lib/Utils.ts index aa4156c8c56..5492116f982 100644 --- a/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/app/meteor-accounts-saml/server/lib/Utils.ts @@ -1,4 +1,5 @@ import zlib from 'zlib'; +import { EventEmitter } from 'events'; import _ from 'underscore'; @@ -7,15 +8,12 @@ import { ISAMLUser } from '../definition/ISAMLUser'; import { ISAMLGlobalSettings } from '../definition/ISAMLGlobalSettings'; import { IUserDataMap, IAttributeMapping } from '../definition/IAttributeMapping'; import { StatusCode } from './constants'; - -// @ToDo remove this ts-ignore someday -// @ts-ignore skip checking if Logger exists to avoid having to import the Logger class here (it would bring a lot of baggage with its dependencies, affecting the unit tests) -type NullableLogger = Logger | null; +import { Logger } from '../../../../server/lib/logger/Logger'; let providerList: Array = []; let debug = false; let relayState: string | null = null; -let logger: NullableLogger = null; +let logger: Logger | undefined; const globalSettings: ISAMLGlobalSettings = { generateUsername: false, @@ -23,8 +21,6 @@ const globalSettings: ISAMLGlobalSettings = { mailOverwrite: false, immutableProperty: 'EMail', defaultUserRole: 'user', - roleAttributeName: '', - roleAttributeSync: false, userDataFieldMap: '{"username":"username", "email":"email", "cn": "name"}', usernameNormalize: 'None', channelsAttributeUpdate: false, @@ -32,6 +28,8 @@ const globalSettings: ISAMLGlobalSettings = { }; export class SAMLUtils { + public static events: EventEmitter; + public static get isDebugging(): boolean { return debug; } @@ -53,8 +51,7 @@ export class SAMLUtils { } public static getServiceProviderOptions(providerName: string): IServiceProviderOptions | undefined { - this.log(providerName); - this.log(providerList); + this.log(providerName, providerList); return _.find(providerList, (providerOptions) => providerOptions.provider === providerName); } @@ -63,7 +60,7 @@ export class SAMLUtils { providerList = list; } - public static setLoggerInstance(instance: NullableLogger): void { + public static setLoggerInstance(instance: Logger): void { logger = instance; } @@ -74,7 +71,6 @@ export class SAMLUtils { globalSettings.generateUsername = Boolean(samlConfigs.generateUsername); globalSettings.nameOverwrite = Boolean(samlConfigs.nameOverwrite); globalSettings.mailOverwrite = Boolean(samlConfigs.mailOverwrite); - globalSettings.roleAttributeSync = Boolean(samlConfigs.roleAttributeSync); globalSettings.channelsAttributeUpdate = Boolean(samlConfigs.channelsAttributeUpdate); globalSettings.includePrivateChannelsInUpdate = Boolean(samlConfigs.includePrivateChannelsInUpdate); @@ -90,10 +86,6 @@ export class SAMLUtils { globalSettings.defaultUserRole = samlConfigs.defaultUserRole; } - if (samlConfigs.roleAttributeName && typeof samlConfigs.roleAttributeName === 'string') { - globalSettings.roleAttributeName = samlConfigs.roleAttributeName; - } - if (samlConfigs.userDataFieldMap && typeof samlConfigs.userDataFieldMap === 'string') { globalSettings.userDataFieldMap = samlConfigs.userDataFieldMap; } @@ -139,15 +131,15 @@ export class SAMLUtils { return newTemplate; } - public static log(...args: Array): void { + public static log(obj: any, ...args: Array): void { if (debug && logger) { - logger.debug(...args); + logger.debug(obj, ...args); } } - public static error(...args: Array): void { + public static error(obj: any, ...args: Array): void { if (logger) { - logger.error(...args); + logger.error(obj, ...args); } } @@ -421,7 +413,7 @@ export class SAMLUtils { public static mapProfileToUserObject(profile: Record): ISAMLUser { const userDataMap = this.getUserDataMapping(); SAMLUtils.log('parsed userDataMap', userDataMap); - const { defaultUserRole = 'user', roleAttributeName } = this.globalSettings; + const { defaultUserRole = 'user' } = this.globalSettings; if (userDataMap.identifier.type === 'custom') { if (!userDataMap.identifier.attribute) { @@ -470,15 +462,6 @@ export class SAMLUtils { userObject.username = this.normalizeUsername(profileUsername); } - if (roleAttributeName && profile[roleAttributeName]) { - let value = profile[roleAttributeName] || ''; - if (typeof value === 'string') { - value = value.split(','); - } - - userObject.roles = this.ensureArray(value); - } - if (profile.language) { userObject.language = profile.language; } @@ -498,6 +481,10 @@ export class SAMLUtils { } } + this.events.emit('mapUser', { profile, userObject }); + return userObject; } } + +SAMLUtils.events = new EventEmitter(); diff --git a/app/meteor-accounts-saml/server/lib/parsers/Response.ts b/app/meteor-accounts-saml/server/lib/parsers/Response.ts index 4a21f29cf88..89bbc075809 100644 --- a/app/meteor-accounts-saml/server/lib/parsers/Response.ts +++ b/app/meteor-accounts-saml/server/lib/parsers/Response.ts @@ -342,7 +342,7 @@ export class ResponseParser { private validateNotBeforeNotOnOrAfterAssertions(element: Element): boolean { const sysnow = new Date(); - const allowedclockdrift = this.serviceProviderOptions.allowedClockDrift; + const allowedclockdrift = this.serviceProviderOptions.allowedClockDrift || 0; const now = new Date(sysnow.getTime() + allowedclockdrift); diff --git a/app/meteor-accounts-saml/server/lib/settings.ts b/app/meteor-accounts-saml/server/lib/settings.ts index b71cc6551af..38b2c8e9aa9 100644 --- a/app/meteor-accounts-saml/server/lib/settings.ts +++ b/app/meteor-accounts-saml/server/lib/settings.ts @@ -19,7 +19,7 @@ import { } from './constants'; export const getSamlConfigs = function(service: string): Record { - return { + const configs = { buttonLabelText: settings.get(`${ service }_button_label_text`), buttonLabelColor: settings.get(`${ service }_button_label_color`), buttonColor: settings.get(`${ service }_button_color`), @@ -36,11 +36,7 @@ export const getSamlConfigs = function(service: string): Record { mailOverwrite: settings.get(`${ service }_mail_overwrite`), issuer: settings.get(`${ service }_issuer`), logoutBehaviour: settings.get(`${ service }_logout_behaviour`), - customAuthnContext: settings.get(`${ service }_custom_authn_context`), - authnContextComparison: settings.get(`${ service }_authn_context_comparison`), defaultUserRole: settings.get(`${ service }_default_user_role`), - roleAttributeName: settings.get(`${ service }_role_attribute_name`), - roleAttributeSync: settings.get(`${ service }_role_attribute_sync`), secret: { privateKey: settings.get(`${ service }_private_key`), publicCert: settings.get(`${ service }_public_cert`), @@ -50,17 +46,22 @@ export const getSamlConfigs = function(service: string): Record { signatureValidationType: settings.get(`${ service }_signature_validation_type`), userDataFieldMap: settings.get(`${ service }_user_data_fieldmap`), allowedClockDrift: settings.get(`${ service }_allowed_clock_drift`), - identifierFormat: settings.get(`${ service }_identifier_format`), - nameIDPolicyTemplate: settings.get(`${ service }_NameId_template`), - authnContextTemplate: settings.get(`${ service }_AuthnContext_template`), - authRequestTemplate: settings.get(`${ service }_AuthRequest_template`), - logoutResponseTemplate: settings.get(`${ service }_LogoutResponse_template`), - logoutRequestTemplate: settings.get(`${ service }_LogoutRequest_template`), - metadataCertificateTemplate: settings.get(`${ service }_MetadataCertificate_template`), - metadataTemplate: settings.get(`${ service }_Metadata_template`), + customAuthnContext: defaultAuthnContext, + authnContextComparison: 'exact', + identifierFormat: defaultIdentifierFormat, + nameIDPolicyTemplate: defaultNameIDTemplate, + authnContextTemplate: defaultAuthnContextTemplate, + authRequestTemplate: defaultAuthRequestTemplate, + logoutResponseTemplate: defaultLogoutResponseTemplate, + logoutRequestTemplate: defaultLogoutRequestTemplate, + metadataCertificateTemplate: defaultMetadataCertificateTemplate, + metadataTemplate: defaultMetadataTemplate, channelsAttributeUpdate: settings.get(`${ service }_channels_update`), includePrivateChannelsInUpdate: settings.get(`${ service }_include_private_channels_update`), }; + + SAMLUtils.events.emit('loadConfigs', service, configs); + return configs; }; export const configureSamlService = function(samlConfigs: Record): IServiceProviderOptions { @@ -87,8 +88,6 @@ export const configureSamlService = function(samlConfigs: Record): customAuthnContext: samlConfigs.customAuthnContext, authnContextComparison: samlConfigs.authnContextComparison, defaultUserRole: samlConfigs.defaultUserRole, - roleAttributeName: samlConfigs.roleAttributeName, - roleAttributeSync: samlConfigs.roleAttributeSync, allowedClockDrift: parseInt(samlConfigs.allowedClockDrift) || 0, signatureValidationType: samlConfigs.signatureValidationType, identifierFormat: samlConfigs.identifierFormat, @@ -136,290 +135,159 @@ export const addSamlService = function(name: string): void { }; export const addSettings = function(name: string): void { - settings.add(`SAML_Custom_${ name }`, false, { - type: 'boolean', - group: 'SAML', - i18nLabel: 'Accounts_OAuth_Custom_Enable', - }); - settings.add(`SAML_Custom_${ name }_provider`, 'provider-name', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_Provider', - }); - settings.add(`SAML_Custom_${ name }_entry_point`, 'https://example.com/simplesaml/saml2/idp/SSOService.php', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_Entry_point', - }); - settings.add(`SAML_Custom_${ name }_idp_slo_redirect_url`, 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_IDP_SLO_Redirect_URL', - }); - settings.add(`SAML_Custom_${ name }_issuer`, 'https://your-rocket-chat/_saml/metadata/provider-name', { - type: 'string', - group: 'SAML', - i18nLabel: 'SAML_Custom_Issuer', - }); - settings.add(`SAML_Custom_${ name }_debug`, false, { - type: 'boolean', - group: 'SAML', - i18nLabel: 'SAML_Custom_Debug', - }); - - // UI Settings - settings.add(`SAML_Custom_${ name }_button_label_text`, 'SAML', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_1_User_Interface', - i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', - }); - settings.add(`SAML_Custom_${ name }_button_label_color`, '#FFFFFF', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_1_User_Interface', - i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', - }); - settings.add(`SAML_Custom_${ name }_button_color`, '#1d74f5', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_1_User_Interface', - i18nLabel: 'Accounts_OAuth_Custom_Button_Color', - }); - - // Certificate settings - settings.add(`SAML_Custom_${ name }_cert`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_2_Certificate', - i18nLabel: 'SAML_Custom_Cert', - multiline: true, - secret: true, - }); - settings.add(`SAML_Custom_${ name }_public_cert`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_2_Certificate', - multiline: true, - i18nLabel: 'SAML_Custom_Public_Cert', - }); - settings.add(`SAML_Custom_${ name }_signature_validation_type`, 'All', { - type: 'select', - values: [ - { key: 'Response', i18nLabel: 'SAML_Custom_signature_validation_response' }, - { key: 'Assertion', i18nLabel: 'SAML_Custom_signature_validation_assertion' }, - { key: 'Either', i18nLabel: 'SAML_Custom_signature_validation_either' }, - { key: 'All', i18nLabel: 'SAML_Custom_signature_validation_all' }, - ], - group: 'SAML', - section: 'SAML_Section_2_Certificate', - i18nLabel: 'SAML_Custom_signature_validation_type', - i18nDescription: 'SAML_Custom_signature_validation_type_description', - }); - settings.add(`SAML_Custom_${ name }_private_key`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_2_Certificate', - multiline: true, - i18nLabel: 'SAML_Custom_Private_Key', - secret: true, - }); - - // Settings to customize behavior - settings.add(`SAML_Custom_${ name }_generate_username`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Generate_Username', - }); - settings.add(`SAML_Custom_${ name }_username_normalize`, 'None', { - type: 'select', - values: [ - { key: 'None', i18nLabel: 'SAML_Custom_Username_Normalize_None' }, - { key: 'Lowercase', i18nLabel: 'SAML_Custom_Username_Normalize_Lowercase' }, - ], - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Username_Normalize', - }); - settings.add(`SAML_Custom_${ name }_immutable_property`, 'EMail', { - type: 'select', - values: [ - { key: 'Username', i18nLabel: 'SAML_Custom_Immutable_Property_Username' }, - { key: 'EMail', i18nLabel: 'SAML_Custom_Immutable_Property_EMail' }, - ], - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Immutable_Property', - }); - settings.add(`SAML_Custom_${ name }_name_overwrite`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_name_overwrite', - }); - settings.add(`SAML_Custom_${ name }_mail_overwrite`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_mail_overwrite', - }); - settings.add(`SAML_Custom_${ name }_logout_behaviour`, 'SAML', { - type: 'select', - values: [ - { key: 'SAML', i18nLabel: 'SAML_Custom_Logout_Behaviour_Terminate_SAML_Session' }, - { key: 'Local', i18nLabel: 'SAML_Custom_Logout_Behaviour_End_Only_RocketChat' }, - ], - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_Logout_Behaviour', - }); - settings.add(`SAML_Custom_${ name }_channels_update`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_channels_update', - i18nDescription: 'SAML_Custom_channels_update_description', - }); - settings.add(`SAML_Custom_${ name }_include_private_channels_update`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_3_Behavior', - i18nLabel: 'SAML_Custom_include_private_channels_update', - i18nDescription: 'SAML_Custom_include_private_channels_update_description', - }); - - // Roles Settings - settings.add(`SAML_Custom_${ name }_default_user_role`, 'user', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_4_Roles', - i18nLabel: 'SAML_Default_User_Role', - i18nDescription: 'SAML_Default_User_Role_Description', - }); - settings.add(`SAML_Custom_${ name }_role_attribute_name`, '', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_4_Roles', - i18nLabel: 'SAML_Role_Attribute_Name', - i18nDescription: 'SAML_Role_Attribute_Name_Description', - }); - settings.add(`SAML_Custom_${ name }_role_attribute_sync`, false, { - type: 'boolean', - group: 'SAML', - section: 'SAML_Section_4_Roles', - i18nLabel: 'SAML_Role_Attribute_Sync', - i18nDescription: 'SAML_Role_Attribute_Sync_Description', - }); - - - // Data Mapping Settings - settings.add(`SAML_Custom_${ name }_user_data_fieldmap`, '{"username":"username", "email":"email", "name": "cn"}', { - type: 'string', - group: 'SAML', - section: 'SAML_Section_5_Mapping', - i18nLabel: 'SAML_Custom_user_data_fieldmap', - i18nDescription: 'SAML_Custom_user_data_fieldmap_description', - multiline: true, - }); - - // Advanced settings - settings.add(`SAML_Custom_${ name }_allowed_clock_drift`, 0, { - type: 'int', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Allowed_Clock_Drift', - i18nDescription: 'SAML_Allowed_Clock_Drift_Description', - }); - settings.add(`SAML_Custom_${ name }_identifier_format`, defaultIdentifierFormat, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Identifier_Format', - i18nDescription: 'SAML_Identifier_Format_Description', - }); - - settings.add(`SAML_Custom_${ name }_NameId_template`, defaultNameIDTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_NameIdPolicy_Template', - i18nDescription: 'SAML_NameIdPolicy_Template_Description', - multiline: true, - }); - - settings.add(`SAML_Custom_${ name }_custom_authn_context`, defaultAuthnContext, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Custom_Authn_Context', - i18nDescription: 'SAML_Custom_Authn_Context_description', - }); - settings.add(`SAML_Custom_${ name }_authn_context_comparison`, 'exact', { - type: 'select', - values: [ - { key: 'better', i18nLabel: 'Better' }, - { key: 'exact', i18nLabel: 'Exact' }, - { key: 'maximum', i18nLabel: 'Maximum' }, - { key: 'minimum', i18nLabel: 'Minimum' }, - ], - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Custom_Authn_Context_Comparison', - }); + settings.addGroup('SAML', function() { + this.set({ + tab: 'SAML_Connection', + }, function() { + this.add(`SAML_Custom_${ name }`, false, { + type: 'boolean', + i18nLabel: 'Accounts_OAuth_Custom_Enable', + }); + this.add(`SAML_Custom_${ name }_provider`, 'provider-name', { + type: 'string', + i18nLabel: 'SAML_Custom_Provider', + }); + this.add(`SAML_Custom_${ name }_entry_point`, 'https://example.com/simplesaml/saml2/idp/SSOService.php', { + type: 'string', + i18nLabel: 'SAML_Custom_Entry_point', + }); + this.add(`SAML_Custom_${ name }_idp_slo_redirect_url`, 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', { + type: 'string', + i18nLabel: 'SAML_Custom_IDP_SLO_Redirect_URL', + }); + this.add(`SAML_Custom_${ name }_issuer`, 'https://your-rocket-chat/_saml/metadata/provider-name', { + type: 'string', + i18nLabel: 'SAML_Custom_Issuer', + }); + this.add(`SAML_Custom_${ name }_debug`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_Debug', + }); - settings.add(`SAML_Custom_${ name }_AuthnContext_template`, defaultAuthnContextTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_AuthnContext_Template', - i18nDescription: 'SAML_AuthnContext_Template_Description', - multiline: true, - }); + this.section('SAML_Section_2_Certificate', function() { + this.add(`SAML_Custom_${ name }_cert`, '', { + type: 'string', + i18nLabel: 'SAML_Custom_Cert', + multiline: true, + secret: true, + }); + this.add(`SAML_Custom_${ name }_public_cert`, '', { + type: 'string', + multiline: true, + i18nLabel: 'SAML_Custom_Public_Cert', + }); + this.add(`SAML_Custom_${ name }_signature_validation_type`, 'All', { + type: 'select', + values: [ + { key: 'Response', i18nLabel: 'SAML_Custom_signature_validation_response' }, + { key: 'Assertion', i18nLabel: 'SAML_Custom_signature_validation_assertion' }, + { key: 'Either', i18nLabel: 'SAML_Custom_signature_validation_either' }, + { key: 'All', i18nLabel: 'SAML_Custom_signature_validation_all' }, + ], + i18nLabel: 'SAML_Custom_signature_validation_type', + i18nDescription: 'SAML_Custom_signature_validation_type_description', + }); + this.add(`SAML_Custom_${ name }_private_key`, '', { + type: 'string', + multiline: true, + i18nLabel: 'SAML_Custom_Private_Key', + secret: true, + }); + }); + }); + this.set({ + tab: 'SAML_General', + }, function() { + this.section('SAML_Section_1_User_Interface', function() { + this.add(`SAML_Custom_${ name }_button_label_text`, 'SAML', { + type: 'string', + i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text', + }); + this.add(`SAML_Custom_${ name }_button_label_color`, '#FFFFFF', { + type: 'string', + i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color', + }); + this.add(`SAML_Custom_${ name }_button_color`, '#1d74f5', { + type: 'string', + i18nLabel: 'Accounts_OAuth_Custom_Button_Color', + }); + }); - settings.add(`SAML_Custom_${ name }_AuthRequest_template`, defaultAuthRequestTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_AuthnRequest_Template', - i18nDescription: 'SAML_AuthnRequest_Template_Description', - multiline: true, - }); + this.section('SAML_Section_3_Behavior', function() { + // Settings to customize behavior + this.add(`SAML_Custom_${ name }_generate_username`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_Generate_Username', + }); + this.add(`SAML_Custom_${ name }_username_normalize`, 'None', { + type: 'select', + values: [ + { key: 'None', i18nLabel: 'SAML_Custom_Username_Normalize_None' }, + { key: 'Lowercase', i18nLabel: 'SAML_Custom_Username_Normalize_Lowercase' }, + ], + i18nLabel: 'SAML_Custom_Username_Normalize', + }); + this.add(`SAML_Custom_${ name }_immutable_property`, 'EMail', { + type: 'select', + values: [ + { key: 'Username', i18nLabel: 'SAML_Custom_Immutable_Property_Username' }, + { key: 'EMail', i18nLabel: 'SAML_Custom_Immutable_Property_EMail' }, + ], + i18nLabel: 'SAML_Custom_Immutable_Property', + }); + this.add(`SAML_Custom_${ name }_name_overwrite`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_name_overwrite', + }); + this.add(`SAML_Custom_${ name }_mail_overwrite`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_mail_overwrite', + }); + this.add(`SAML_Custom_${ name }_logout_behaviour`, 'SAML', { + type: 'select', + values: [ + { key: 'SAML', i18nLabel: 'SAML_Custom_Logout_Behaviour_Terminate_SAML_Session' }, + { key: 'Local', i18nLabel: 'SAML_Custom_Logout_Behaviour_End_Only_RocketChat' }, + ], + i18nLabel: 'SAML_Custom_Logout_Behaviour', + }); + this.add(`SAML_Custom_${ name }_channels_update`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_channels_update', + i18nDescription: 'SAML_Custom_channels_update_description', + }); + this.add(`SAML_Custom_${ name }_include_private_channels_update`, false, { + type: 'boolean', + i18nLabel: 'SAML_Custom_include_private_channels_update', + i18nDescription: 'SAML_Custom_include_private_channels_update_description', + }); - settings.add(`SAML_Custom_${ name }_LogoutResponse_template`, defaultLogoutResponseTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_LogoutResponse_Template', - i18nDescription: 'SAML_LogoutResponse_Template_Description', - multiline: true, - }); + this.add(`SAML_Custom_${ name }_default_user_role`, 'user', { + type: 'string', + i18nLabel: 'SAML_Default_User_Role', + i18nDescription: 'SAML_Default_User_Role_Description', + }); - settings.add(`SAML_Custom_${ name }_LogoutRequest_template`, defaultLogoutRequestTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_LogoutRequest_Template', - i18nDescription: 'SAML_LogoutRequest_Template_Description', - multiline: true, - }); + this.add(`SAML_Custom_${ name }_allowed_clock_drift`, false, { + type: 'int', + invalidValue: 0, + i18nLabel: 'SAML_Allowed_Clock_Drift', + i18nDescription: 'SAML_Allowed_Clock_Drift_Description', + }); + }); - settings.add(`SAML_Custom_${ name }_MetadataCertificate_template`, defaultMetadataCertificateTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_MetadataCertificate_Template', - i18nDescription: 'SAML_Metadata_Certificate_Template_Description', - multiline: true, - }); + this.section('SAML_Section_5_Mapping', function() { + // Data Mapping Settings + this.add(`SAML_Custom_${ name }_user_data_fieldmap`, '{"username":"username", "email":"email", "name": "cn"}', { + type: 'string', + i18nLabel: 'SAML_Custom_user_data_fieldmap', + i18nDescription: 'SAML_Custom_user_data_fieldmap_description', + multiline: true, + }); + }); + }); - settings.add(`SAML_Custom_${ name }_Metadata_template`, defaultMetadataTemplate, { - type: 'string', - group: 'SAML', - section: 'SAML_Section_6_Advanced', - i18nLabel: 'SAML_Metadata_Template', - i18nDescription: 'SAML_Metadata_Template_Description', - multiline: true, + SAMLUtils.events.emit('addSettings', name); }); }; diff --git a/app/meteor-accounts-saml/server/startup.ts b/app/meteor-accounts-saml/server/startup.ts index a3067c26dfd..b915ad7b60f 100644 --- a/app/meteor-accounts-saml/server/startup.ts +++ b/app/meteor-accounts-saml/server/startup.ts @@ -6,8 +6,6 @@ import { loadSamlServiceProviders, addSettings } from './lib/settings'; import { Logger } from '../../logger/server'; import { SAMLUtils } from './lib/Utils'; -settings.addGroup('SAML'); - export const logger = new Logger('steffo:meteor-accounts-saml'); SAMLUtils.setLoggerInstance(logger); @@ -15,7 +13,7 @@ const updateServices = _.debounce(Meteor.bindEnvironment(() => { loadSamlServiceProviders(); }), 2000); - -settings.get(/^SAML_.+/, updateServices); - -Meteor.startup(() => addSettings('Default')); +Meteor.startup(() => { + addSettings('Default'); + settings.get(/^SAML_.+/, updateServices); +}); diff --git a/app/meteor-accounts-saml/tests/data.ts b/app/meteor-accounts-saml/tests/data.ts index b74f59f0ed2..cf366a7564c 100644 --- a/app/meteor-accounts-saml/tests/data.ts +++ b/app/meteor-accounts-saml/tests/data.ts @@ -9,8 +9,6 @@ export const serviceProviderOptions = { customAuthnContext: 'Password', authnContextComparison: 'Whatever', defaultUserRole: 'user', - roleAttributeName: 'role', - roleAttributeSync: false, allowedClockDrift: 0, signatureValidationType: 'All', identifierFormat: 'email', diff --git a/app/meteor-accounts-saml/tests/server.tests.ts b/app/meteor-accounts-saml/tests/server.tests.ts index b806e2dd247..ad7477aa7ad 100644 --- a/app/meteor-accounts-saml/tests/server.tests.ts +++ b/app/meteor-accounts-saml/tests/server.tests.ts @@ -638,7 +638,6 @@ describe('SAML', () => { }; globalSettings.userDataFieldMap = JSON.stringify(fieldMap); - globalSettings.roleAttributeName = 'roles'; SAMLUtils.updateGlobalSettings(globalSettings); SAMLUtils.relayState = '[RelayState]'; @@ -653,7 +652,7 @@ describe('SAML', () => { expect(userObject).to.have.property('emailList').that.is.an('array').that.includes('testing@server.com'); expect(userObject).to.have.property('fullName').that.is.equal('[AnotherName]'); expect(userObject).to.have.property('username').that.is.equal('[AnotherUserName]'); - expect(userObject).to.have.property('roles').that.is.an('array').with.members(['user', 'ruler', 'admin', 'king', 'president', 'governor', 'mayor']); + 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(); @@ -738,37 +737,6 @@ describe('SAML', () => { expect(userObject).to.have.property('username').that.is.equal('[username]'); }); - it('should load multiple roles from the roleAttributeName when it has multiple values', () => { - const multipleRoles = { - ...profile, - roles: ['role1', 'role2'], - }; - - const userObject = SAMLUtils.mapProfileToUserObject(multipleRoles); - - expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['role1', 'role2']); - }); - - it('should assign the default role when the roleAttributeName is missing', () => { - const { globalSettings } = SAMLUtils; - globalSettings.roleAttributeName = ''; - SAMLUtils.updateGlobalSettings(globalSettings); - - const userObject = SAMLUtils.mapProfileToUserObject(profile); - - expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['user']); - }); - - it('should assign the default role when the value of the role attribute is missing', () => { - const { globalSettings } = SAMLUtils; - globalSettings.roleAttributeName = 'inexistentField'; - SAMLUtils.updateGlobalSettings(globalSettings); - - const userObject = SAMLUtils.mapProfileToUserObject(profile); - - expect(userObject).to.be.an('object').that.have.property('roles').that.is.an('array').with.members(['user']); - }); - it('should run custom regexes when one is used', () => { const { globalSettings } = SAMLUtils; @@ -1005,7 +973,6 @@ describe('SAML', () => { }; globalSettings.userDataFieldMap = JSON.stringify(fieldMap); - globalSettings.roleAttributeName = 'roles'; SAMLUtils.updateGlobalSettings(globalSettings); SAMLUtils.relayState = '[RelayState]'; diff --git a/ee/app/license/server/bundles.ts b/ee/app/license/server/bundles.ts index d116939fe38..d65b4c020f8 100644 --- a/ee/app/license/server/bundles.ts +++ b/ee/app/license/server/bundles.ts @@ -13,6 +13,7 @@ const bundles: IBundle = { 'push-privacy', 'scalability', 'teams-mention', + 'saml-enterprise', ], pro: [ ], diff --git a/ee/server/configuration/index.ts b/ee/server/configuration/index.ts new file mode 100644 index 00000000000..c5dd94f6bac --- /dev/null +++ b/ee/server/configuration/index.ts @@ -0,0 +1,2 @@ +import './ldap'; +import './saml'; diff --git a/ee/server/configuration/saml.ts b/ee/server/configuration/saml.ts new file mode 100644 index 00000000000..22e246d2c5d --- /dev/null +++ b/ee/server/configuration/saml.ts @@ -0,0 +1,44 @@ +import { onLicense } from '../../app/license/server'; +import type { ISAMLUser } from '../../../app/meteor-accounts-saml/server/definition/ISAMLUser'; +import { SAMLUtils } from '../../../app/meteor-accounts-saml/server/lib/Utils'; +import { settings } from '../../../app/settings/server'; +import { addSettings } from '../settings/saml'; + +onLicense('saml-enterprise', () => { + SAMLUtils.events.on('mapUser', ({ profile, userObject }: { profile: Record; userObject: ISAMLUser}) => { + const roleAttributeName = settings.get('SAML_Custom_Default_role_attribute_name') as string; + const roleAttributeSync = settings.get('SAML_Custom_Default_role_attribute_sync'); + + if (!roleAttributeSync) { + return; + } + + if (roleAttributeName && profile[roleAttributeName]) { + let value = profile[roleAttributeName] || ''; + if (typeof value === 'string') { + value = value.split(','); + } + + userObject.roles = SAMLUtils.ensureArray(value); + } + }); + + SAMLUtils.events.on('loadConfigs', (service: string, configs: Record): void => { + // Include ee settings on the configs object so that they can be copied to the login service too + Object.assign(configs, { + customAuthnContext: settings.get(`${ service }_custom_authn_context`), + authnContextComparison: settings.get(`${ service }_authn_context_comparison`), + identifierFormat: settings.get(`${ service }_identifier_format`), + nameIDPolicyTemplate: settings.get(`${ service }_NameId_template`), + authnContextTemplate: settings.get(`${ service }_AuthnContext_template`), + authRequestTemplate: settings.get(`${ service }_AuthRequest_template`), + logoutResponseTemplate: settings.get(`${ service }_LogoutResponse_template`), + logoutRequestTemplate: settings.get(`${ service }_LogoutRequest_template`), + metadataCertificateTemplate: settings.get(`${ service }_MetadataCertificate_template`), + metadataTemplate: settings.get(`${ service }_Metadata_template`), + }); + }); +}); + +// For setting creation we add the listener first because the event is emmited during startup +SAMLUtils.events.on('addSettings', (name: string): void => onLicense('saml-enterprise', () => addSettings(name))); diff --git a/ee/server/index.js b/ee/server/index.js index 5bdf7537ca1..0cf4a961937 100644 --- a/ee/server/index.js +++ b/ee/server/index.js @@ -11,5 +11,6 @@ import '../app/livechat-enterprise/server/index'; import '../app/settings/server/index'; import '../app/teams-mention/server/index'; import './api'; +import './configuration/index'; import './local-services/ldap/service'; -import './configuration/ldap'; +import './settings/index'; diff --git a/ee/server/settings/index.ts b/ee/server/settings/index.ts new file mode 100644 index 00000000000..c67512e1c28 --- /dev/null +++ b/ee/server/settings/index.ts @@ -0,0 +1 @@ +import './saml'; diff --git a/ee/server/settings/saml.ts b/ee/server/settings/saml.ts new file mode 100644 index 00000000000..411f7d44387 --- /dev/null +++ b/ee/server/settings/saml.ts @@ -0,0 +1,113 @@ +import { settings } from '../../../app/settings/server'; +import { + defaultAuthnContextTemplate, + defaultAuthRequestTemplate, + defaultLogoutResponseTemplate, + defaultLogoutRequestTemplate, + defaultNameIDTemplate, + defaultIdentifierFormat, + defaultAuthnContext, + defaultMetadataTemplate, + defaultMetadataCertificateTemplate, +} from '../../../app/meteor-accounts-saml/server/lib/constants'; + +export const addSettings = function(name: string): void { + settings.addGroup('SAML', function() { + this.set({ + tab: 'SAML_Enterprise', + enterprise: true, + modules: ['saml-enterprise'], + }, function() { + this.section('SAML_Section_4_Roles', function() { + // Roles Settings + this.add(`SAML_Custom_${ name }_role_attribute_sync`, false, { + type: 'boolean', + i18nLabel: 'SAML_Role_Attribute_Sync', + i18nDescription: 'SAML_Role_Attribute_Sync_Description', + invalidValue: false, + }); + this.add(`SAML_Custom_${ name }_role_attribute_name`, '', { + type: 'string', + i18nLabel: 'SAML_Role_Attribute_Name', + i18nDescription: 'SAML_Role_Attribute_Name_Description', + invalidValue: '', + }); + }); + + this.section('SAML_Section_6_Advanced', function() { + this.add(`SAML_Custom_${ name }_identifier_format`, defaultIdentifierFormat, { + type: 'string', + invalidValue: defaultIdentifierFormat, + i18nLabel: 'SAML_Identifier_Format', + i18nDescription: 'SAML_Identifier_Format_Description', + }); + this.add(`SAML_Custom_${ name }_NameId_template`, defaultNameIDTemplate, { + type: 'string', + invalidValue: defaultNameIDTemplate, + i18nLabel: 'SAML_NameIdPolicy_Template', + i18nDescription: 'SAML_NameIdPolicy_Template_Description', + multiline: true, + }); + this.add(`SAML_Custom_${ name }_custom_authn_context`, defaultAuthnContext, { + type: 'string', + invalidValue: defaultAuthnContext, + i18nLabel: 'SAML_Custom_Authn_Context', + i18nDescription: 'SAML_Custom_Authn_Context_description', + }); + this.add(`SAML_Custom_${ name }_authn_context_comparison`, 'exact', { + type: 'select', + values: [ + { key: 'better', i18nLabel: 'Better' }, + { key: 'exact', i18nLabel: 'Exact' }, + { key: 'maximum', i18nLabel: 'Maximum' }, + { key: 'minimum', i18nLabel: 'Minimum' }, + ], + invalidValue: 'exact', + i18nLabel: 'SAML_Custom_Authn_Context_Comparison', + }); + this.add(`SAML_Custom_${ name }_AuthnContext_template`, defaultAuthnContextTemplate, { + type: 'string', + invalidValue: defaultAuthnContextTemplate, + i18nLabel: 'SAML_AuthnContext_Template', + i18nDescription: 'SAML_AuthnContext_Template_Description', + multiline: true, + }); + this.add(`SAML_Custom_${ name }_AuthRequest_template`, defaultAuthRequestTemplate, { + type: 'string', + invalidValue: defaultAuthRequestTemplate, + i18nLabel: 'SAML_AuthnRequest_Template', + i18nDescription: 'SAML_AuthnRequest_Template_Description', + multiline: true, + }); + this.add(`SAML_Custom_${ name }_LogoutResponse_template`, defaultLogoutResponseTemplate, { + type: 'string', + invalidValue: defaultLogoutResponseTemplate, + i18nLabel: 'SAML_LogoutResponse_Template', + i18nDescription: 'SAML_LogoutResponse_Template_Description', + multiline: true, + }); + this.add(`SAML_Custom_${ name }_LogoutRequest_template`, defaultLogoutRequestTemplate, { + type: 'string', + invalidValue: defaultLogoutRequestTemplate, + i18nLabel: 'SAML_LogoutRequest_Template', + i18nDescription: 'SAML_LogoutRequest_Template_Description', + multiline: true, + }); + this.add(`SAML_Custom_${ name }_MetadataCertificate_template`, defaultMetadataCertificateTemplate, { + type: 'string', + invalidValue: defaultMetadataCertificateTemplate, + i18nLabel: 'SAML_MetadataCertificate_Template', + i18nDescription: 'SAML_Metadata_Certificate_Template_Description', + multiline: true, + }); + this.add(`SAML_Custom_${ name }_Metadata_template`, defaultMetadataTemplate, { + type: 'string', + invalidValue: defaultMetadataTemplate, + i18nLabel: 'SAML_Metadata_Template', + i18nDescription: 'SAML_Metadata_Template_Description', + multiline: true, + }); + }); + }); + }); +}; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index d96ad273993..bb8668160a3 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3581,6 +3581,9 @@ "SAML_AuthnContext_Template_Description": "You can use any variable from the AuthnRequest Template here.\n\n To add additional authn contexts, duplicate the __AuthnContextClassRef__ tag and replace the __\\_\\_authnContext\\_\\___ variable with the new context.", "SAML_AuthnRequest_Template": "AuthnRequest Template", "SAML_AuthnRequest_Template_Description": "The following variables are available:\n- **\\_\\_newId\\_\\_**: Randomly generated id string\n- **\\_\\_instant\\_\\_**: Current timestamp\n- **\\_\\_callbackUrl\\_\\_**: The Rocket.Chat callback URL.\n- **\\_\\_entryPoint\\_\\_**: The value of the __Custom Entry Point__ setting.\n- **\\_\\_issuer\\_\\_**: The value of the __Custom Issuer__ setting.\n- **\\_\\_identifierFormatTag\\_\\_**: The contents of the __NameID Policy Template__ if a valid __Identifier Format__ is configured.\n- **\\_\\_identifierFormat\\_\\_**: The value of the __Identifier Format__ setting.\n- **\\_\\_authnContextTag\\_\\_**: The contents of the __AuthnContext Template__ if a valid __Custom Authn Context__ is configured.\n- **\\_\\_authnContextComparison\\_\\_**: The value of the __Authn Context Comparison__ setting.\n- **\\_\\_authnContext\\_\\_**: The value of the __Custom Authn Context__ setting.", + "SAML_Connection": "Connection", + "SAML_Enterprise": "Enterprise", + "SAML_General": "General", "SAML_Custom_Authn_Context": "Custom Authn Context", "SAML_Custom_Authn_Context_Comparison": "Authn Context Comparison", "SAML_Custom_Authn_Context_description": "Leave this empty to omit the authn context from the request.\n\n To add multiple authn contexts, add the additional ones directly to the __AuthnContext Template__ setting.",