From e22e8ff7a84c58ef6434d55f7ab086e09fb6c640 Mon Sep 17 00:00:00 2001 From: Miguel Serrano Date: Tue, 31 Oct 2023 13:16:49 +0100 Subject: [PATCH] [web] SSO linking for existing group members (#15471) * [web] SSO linking for existing group users GitOrigin-RevId: 22a5a5a28a213860f88ae0284c1ef51a31bb268f --- .../Features/Subscription/GroupSSOHandler.js | 100 ++++++++++++++++++ services/web/app/src/models/User.js | 11 +- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 services/web/app/src/Features/Subscription/GroupSSOHandler.js diff --git a/services/web/app/src/Features/Subscription/GroupSSOHandler.js b/services/web/app/src/Features/Subscription/GroupSSOHandler.js new file mode 100644 index 0000000000..0b880119ab --- /dev/null +++ b/services/web/app/src/Features/Subscription/GroupSSOHandler.js @@ -0,0 +1,100 @@ +const { SSOConfig } = require('../../models/SSOConfig') +const UserAuditLogHandler = require('../User/UserAuditLogHandler') +const UserUpdater = require('../User/UserUpdater') +const SAMLIdentityManager = require('../User/SAMLIdentityManager') +const { User } = require('../../models/User') +const Errors = require('../Errors/Errors') + +async function canEnrollInSubscription(userId, subscription) { + const ssoEnabled = await isSSOEnabled(subscription) + if (!ssoEnabled) { + return false + } + + const userIsMember = subscription.member_ids.some( + memberId => memberId.toString() === userId.toString() + ) + if (!userIsMember) { + return false + } + + const user = await User.findOne( + { _id: userId }, + { projection: { enrollment: 1 } } + ).exec() + + const userIsEnrolled = user.enrollment?.sso?.some( + enrollment => enrollment.groupId.toString() === subscription._id.toString() + ) + if (userIsEnrolled) { + return false + } + return true +} + +async function enrollInSubscription( + userId, + subscription, + externalUserId, + userIdAttribute, + auditLog +) { + const canEnroll = await canEnrollInSubscription(userId, subscription) + if (!canEnroll) { + throw new Errors.SubscriptionNotFoundError( + 'cannot enroll user in SSO subscription', + { + info: { userId, subscription }, + } + ) + } + const providerId = `ol-group-subscription-id:${subscription._id.toString()}` + + const userBySamlIdentifier = await SAMLIdentityManager.getUser( + providerId, + externalUserId, + userIdAttribute + ) + + if (userBySamlIdentifier) { + throw new Errors.SAMLIdentityExistsError() + } + + const samlIdentifiers = { + externalUserId, + userIdAttribute, + providerId, + } + + await UserUpdater.promises.updateUser(userId, { + $push: { + samlIdentifiers, + 'enrollment.sso': { + groupId: subscription._id, + linkedAt: new Date(), + primary: true, + }, + }, + }) + + await UserAuditLogHandler.promises.addEntry( + userId, + 'group-sso-link', + auditLog.initiatorId, + auditLog.ipAddress, + samlIdentifiers + ) +} + +async function isSSOEnabled(subscription) { + const ssoConfig = await SSOConfig.findById(subscription.ssoConfig).exec() + return ssoConfig?.enabled +} + +module.exports = { + promises: { + canEnrollInSubscription, + enrollInSubscription, + isSSOEnabled, + }, +} diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index 826a57d566..7a45bfdf05 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -32,7 +32,16 @@ const UserSchema = new Schema( institution: { type: String, default: '' }, hashedPassword: String, enrollment: { - // sso: { type: Boolean, default: false }, + sso: [ + { + groupId: { + type: ObjectId, + ref: 'Subscription', + }, + linkedAt: Date, + primary: { type: Boolean, default: false }, + }, + ], managedBy: { type: ObjectId, ref: 'Subscription',