[BREAK] Moved advanced oAuth features to EE (#23201)

Co-authored-by: Pierre Lehnen <pierre.lehnen@rocket.chat>
pull/23218/head
Leonardo Ostjen Couto 4 years ago committed by GitHub
parent a41ac6498a
commit 5e458320b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 33
      app/custom-oauth/server/custom_oauth_server.js
  2. 73
      app/custom-oauth/server/oauth_helpers.js
  3. 35
      app/lib/server/functions/addOAuthService.js
  4. 1
      ee/app/license/server/bundles.ts
  5. 1
      ee/server/configuration/index.ts
  6. 81
      ee/server/configuration/oauth.ts
  7. 73
      ee/server/lib/oauth/Manager.ts

@ -7,11 +7,11 @@ import { ServiceConfiguration } from 'meteor/service-configuration';
import _ from 'underscore';
import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers';
import { mapRolesFromSSO, mapSSOGroupsToChannels, updateRolesFromSSO } from './oauth_helpers';
import { Logger } from '../../logger';
import { Users } from '../../models';
import { isURL } from '../../utils/lib/isURL';
import { registerAccessTokenService } from '../../lib/server/oauth/oauth';
import { callbacks } from '../../callbacks/server';
const logger = new Logger('CustomOAuth');
@ -79,23 +79,10 @@ export class CustomOAuth {
this.nameField = (options.nameField || '').trim();
this.avatarField = (options.avatarField || '').trim();
this.mergeUsers = options.mergeUsers;
this.mergeRoles = options.mergeRoles || false;
this.mapChannels = options.mapChannels || false;
this.rolesClaim = options.rolesClaim || 'roles';
this.groupsClaim = options.groupsClaim || 'groups';
this.accessTokenParam = options.accessTokenParam;
this.channelsAdmin = options.channelsAdmin || 'rocket.cat';
if (this.mapChannels) {
const channelsMap = (options.channelsMap || '{}').trim();
try {
this.channelsMap = JSON.parse(channelsMap);
} catch (err) {
logger.error(`Unexpected error : ${ err.message }`);
}
}
if (this.identityTokenSentVia == null || this.identityTokenSentVia === 'default') {
this.identityTokenSentVia = this.tokenSentVia;
}
@ -347,14 +334,6 @@ export class CustomOAuth {
return;
}
if (this.mergeRoles) {
updateRolesFromSSO(user, serviceData, this.rolesClaim);
}
if (this.mapChannels) {
mapSSOGroupsToChannels(user, serviceData, this.groupsClaim, this.channelsMap, this.channelsAdmin);
}
// User already created or merged and has identical name as before
if (user.services && user.services[serviceName] && user.services[serviceName].id === serviceData.id && user.name === serviceData.name) {
return;
@ -364,6 +343,8 @@ export class CustomOAuth {
throw new Meteor.Error('CustomOAuth', `User with username ${ user.username } already exists`);
}
callbacks.run('afterProcessOAuthUser', { serviceName, serviceData, user });
const serviceIdKey = `services.${ serviceName }.id`;
const update = {
$set: {
@ -393,13 +374,7 @@ export class CustomOAuth {
user.name = user.services[this.name].name;
}
if (this.mergeRoles) {
user.roles = mapRolesFromSSO(user.services[this.name], this.rolesClaim);
}
if (this.mapChannels) {
mapSSOGroupsToChannels(user, user.services[this.name], this.groupsClaim, this.channelsMap, this.channelsAdmin);
}
callbacks.run('afterValidateNewOAuthUser', { identity: user.services[this.name], serviceName: this.name, user });
return true;
});

@ -1,73 +0,0 @@
import { addUserRoles, removeUserFromRoles } from '../../authorization';
import { Roles, Rooms } from '../../models';
import { addUserToRoom, createRoom } from '../../lib/server/functions';
import { Logger } from '../../logger';
export const logger = new Logger('OAuth');
// Returns list of roles from SSO identity
export function mapRolesFromSSO(identity, roleClaimName) {
let roles = [];
if (identity && roleClaimName) {
// Adding roles
if (identity[roleClaimName] && Array.isArray(identity[roleClaimName])) {
roles = identity[roleClaimName].filter((val) => val !== 'offline_access' && val !== 'uma_authorization' && Roles.findOneByIdOrName(val));
}
}
return roles;
}
// Updates the user with roles from SSO identity
export function updateRolesFromSSO(user, identity, roleClaimName) {
if (user && identity && roleClaimName) {
const rolesFromSSO = mapRolesFromSSO(identity, roleClaimName);
if (!Array.isArray(user.roles)) {
user.roles = [];
}
const toRemove = user.roles.filter((val) => !rolesFromSSO.includes(val));
// loop through roles that user has that sso doesnt have and remove
toRemove.forEach(function(role) {
removeUserFromRoles(user._id, role);
});
const toAdd = rolesFromSSO.filter((val) => !user.roles.includes(val));
// loop through roles sso has that user doesnt and add
toAdd.forEach(function(role) {
addUserRoles(user._id, role);
});
}
}
export function mapSSOGroupsToChannels(user, identity, groupClaimName, channelsMap, channelsAdmin) {
if (user && identity && groupClaimName) {
const groupsFromSSO = identity[groupClaimName] || [];
for (const ssoGroup in channelsMap) {
if (typeof ssoGroup === 'string') {
let channels = channelsMap[ssoGroup];
if (!Array.isArray(channels)) {
channels = [channels];
}
for (const channel of channels) {
let room = Rooms.findOneByNonValidatedName(channel);
if (!room) {
room = createRoom('c', channel, channelsAdmin, [], false);
if (!room || !room.rid) {
logger.error(`could not create channel ${ channel }`);
return;
}
}
if (Array.isArray(groupsFromSSO) && groupsFromSSO.includes(ssoGroup)) {
addUserToRoom(room._id, user);
}
}
}
}
}
}

@ -27,12 +27,37 @@ export function addOAuthService(name, values = {}) {
settings.add(`Accounts_OAuth_Custom-${ name }-email_field` , values.emailField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Email_Field', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-name_field` , values.nameField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Name_Field', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-avatar_field` , values.avatarField || '' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Avatar_Field', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-roles_claim` , values.rolesClaim || 'roles' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Roles_Claim', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-groups_claim` , values.groupsClaim || 'groups' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Groups_Claim', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-roles_claim` , values.rolesClaim || 'roles', { type: 'string',
group: 'OAuth',
section: `Custom OAuth: ${ name }`,
i18nLabel: 'Accounts_OAuth_Custom_Roles_Claim',
enterprise: true,
invalidValue: 'roles',
modules: ['oauth-enterprise'] });
settings.add(`Accounts_OAuth_Custom-${ name }-groups_claim` , values.groupsClaim || 'groups', { type: 'string',
group: 'OAuth',
section: `Custom OAuth: ${ name }`,
i18nLabel: 'Accounts_OAuth_Custom_Groups_Claim',
enterprise: true,
invalidValue: 'groups',
modules: ['oauth-enterprise'] });
settings.add(`Accounts_OAuth_Custom-${ name }-channels_admin` , values.channelsAdmin || 'rocket.cat' , { type: 'string' , group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Channel_Admin', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-map_channels` , values.mapChannels || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Map_Channels', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-merge_roles` , values.mergeRoles || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Merge_Roles', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-merge_users` , values.mergeUsers || false , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Merge_Users', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-map_channels` , values.mapChannels || false, { type: 'boolean',
group: 'OAuth',
section: `Custom OAuth: ${ name }`,
i18nLabel: 'Accounts_OAuth_Custom_Map_Channels',
enterprise: true,
invalidValue: false,
modules: ['oauth-enterprise'] });
settings.add(`Accounts_OAuth_Custom-${ name }-merge_roles` , values.mergeRoles || false, { type: 'boolean',
group: 'OAuth',
section: `Custom OAuth: ${ name }`,
i18nLabel: 'Accounts_OAuth_Custom_Merge_Roles',
enterprise: true,
invalidValue: false,
modules: ['oauth-enterprise'] });
settings.add(`Accounts_OAuth_Custom-${ name }-merge_users`, values.mergeUsers || false, { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Merge_Users', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-show_button` , values.showButton || true , { type: 'boolean', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Show_Button_On_Login_Page', persistent: true });
settings.add(`Accounts_OAuth_Custom-${ name }-groups_channel_map` , values.channelsMap || '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}' , { type: 'code' , multiline: true, code: 'application/json', group: 'OAuth', section: `Custom OAuth: ${ name }`, i18nLabel: 'Accounts_OAuth_Custom_Channel_Map', persistent: true });
}

@ -14,6 +14,7 @@ const bundles: IBundle = {
'scalability',
'teams-mention',
'saml-enterprise',
'oauth-enterprise',
],
pro: [
],

@ -1,2 +1,3 @@
import './ldap';
import './oauth';
import './saml';

@ -0,0 +1,81 @@
import { capitalize } from '@rocket.chat/string-helpers';
import { OAuthEEManager } from '../lib/oauth/Manager';
import { onLicense } from '../../app/license/server';
import { callbacks } from '../../../app/callbacks/server';
import { settings } from '../../../app/settings/server';
import { Logger } from '../../../app/logger/server';
interface IOAuthUserService {
serviceName: string;
serviceData: Record<string, any>;
user: Record<string, any>;
}
interface IOAuthUserIdentity {
serviceName: string;
identity: Record<string, any>;
user: Record<string, any>;
}
interface IOAuthSettings {
mapChannels: string;
mergeRoles: string;
rolesClaim: string;
groupsClaim: string;
channelsAdmin: string;
channelsMap: string;
}
const logger = new Logger('EECustomOAuth');
function getOAuthSettings(serviceName: string): IOAuthSettings {
return {
mapChannels: settings.get(`Accounts_OAuth_Custom-${ serviceName }-map_channels`) as string,
mergeRoles: settings.get(`Accounts_OAuth_Custom-${ serviceName }-merge_roles`) as string,
rolesClaim: settings.get(`Accounts_OAuth_Custom-${ serviceName }-roles_claim`) as string,
groupsClaim: settings.get(`Accounts_OAuth_Custom-${ serviceName }-groups_claim`) as string,
channelsAdmin: settings.get(`Accounts_OAuth_Custom-${ serviceName }-channels_admin`) as string,
channelsMap: settings.get(`Accounts_OAuth_Custom-${ serviceName }-channels_map`) as string,
};
}
function getChannelsMap(channelsMap: string): Record<string, any> | undefined {
channelsMap = (channelsMap || '{}').trim();
try {
return JSON.parse(channelsMap);
} catch (err) {
logger.error(`Unexpected error : ${ err }`);
}
}
onLicense('oauth-enterprise', () => {
callbacks.add('afterProcessOAuthUser', (auth: IOAuthUserService) => {
auth.serviceName = capitalize(auth.serviceName);
const settings = getOAuthSettings(auth.serviceName);
if (settings.mapChannels) {
const channelsMap = getChannelsMap(settings.channelsMap);
OAuthEEManager.mapSSOGroupsToChannels(auth.user, auth.serviceData, settings.groupsClaim, channelsMap, settings.channelsAdmin);
}
if (settings.mergeRoles) {
OAuthEEManager.updateRolesFromSSO(auth.user, auth.serviceData, settings.rolesClaim);
}
});
callbacks.add('afterValidateNewOAuthUser', (auth: IOAuthUserIdentity) => {
auth.serviceName = capitalize(auth.serviceName);
const settings = getOAuthSettings(auth.serviceName);
if (settings.mapChannels) {
const channelsMap = getChannelsMap(settings.channelsMap);
OAuthEEManager.mapSSOGroupsToChannels(auth.user, auth.identity, settings.groupsClaim, channelsMap, settings.channelsAdmin);
}
if (settings.mergeRoles) {
auth.user.roles = OAuthEEManager.mapRolesFromSSO(auth.identity, settings.rolesClaim);
}
});
});

@ -0,0 +1,73 @@
import { addUserRoles, removeUserFromRoles } from '../../../../app/authorization/server';
import { Roles, Rooms } from '../../../../app/models/server';
import { addUserToRoom, createRoom } from '../../../../app/lib/server/functions';
import { Logger } from '../../../../app/logger/server';
export const logger = new Logger('OAuth');
export class OAuthEEManager {
static mapSSOGroupsToChannels(user: Record<string, any>, identity: Record<string, any>, groupClaimName: string, channelsMap: Record<string, any> | undefined, channelsAdmin: string): void {
if (user && identity && groupClaimName) {
const groupsFromSSO = identity[groupClaimName] || [];
for (const ssoGroup in channelsMap) {
if (typeof ssoGroup === 'string') {
let channels = channelsMap[ssoGroup];
if (!Array.isArray(channels)) {
channels = [channels];
}
for (const channel of channels) {
let room = Rooms.findOneByNonValidatedName(channel);
if (!room) {
room = createRoom('c', channel, channelsAdmin, [], false);
if (!room || !room.rid) {
logger.error(`could not create channel ${ channel }`);
return;
}
}
if (Array.isArray(groupsFromSSO) && groupsFromSSO.includes(ssoGroup)) {
addUserToRoom(room._id, user);
}
}
}
}
}
}
static updateRolesFromSSO(user: Record<string, any>, identity: Record<string, any>, roleClaimName: string): void {
if (user && identity && roleClaimName) {
const rolesFromSSO = this.mapRolesFromSSO(identity, roleClaimName);
if (!Array.isArray(user.roles)) {
user.roles = [];
}
const toRemove = user.roles.filter((val: any) => !rolesFromSSO.includes(val));
// loop through roles that user has that sso doesnt have and remove each one
toRemove.forEach(function(role: any) {
removeUserFromRoles(user._id, role);
});
const toAdd = rolesFromSSO.filter((val: any) => !user.roles.includes(val));
// loop through sso roles and add the new ones
toAdd.forEach(function(role: any) {
addUserRoles(user._id, role);
});
}
}
// Returns list of roles from SSO identity
static mapRolesFromSSO(identity: Record<string, any>, roleClaimName: string): string[] {
let roles: string[] = [];
if (identity && roleClaimName) {
// Adding roles
if (identity[roleClaimName] && Array.isArray(identity[roleClaimName])) {
roles = identity[roleClaimName].filter((val: string) => val !== 'offline_access' && val !== 'uma_authorization' && Roles.findOneByIdOrName(val));
}
}
return roles;
}
}
Loading…
Cancel
Save