Files
Verso/services/web/test/acceptance/src/helpers/groupSSO.mjs
T
Andrew Rumble 394c60f2cf Merge pull request #29659 from overleaf/revert-29656-revert-29521-ar-models-es-conversion
Revert "Revert "[web] Convert models and self-referential test files to ESM ""

GitOrigin-RevId: f64000ae31d298b075a8722dfc51f294c71bc021
2025-11-18 09:04:56 +00:00

266 lines
7.3 KiB
JavaScript

import fs from 'node:fs'
import Path from 'node:path'
import UserModule from './User.mjs'
import SubscriptionHelper from './Subscription.mjs'
import { SSOConfig } from '../../../../app/src/models/SSOConfig.mjs'
import UserHelper from './UserHelper.mjs'
import SAMLHelper from './SAMLHelper.mjs'
import Settings from '@overleaf/settings'
import GroupUtils from '../../../../app/src/Features/Subscription/GroupUtils.mjs'
import UserGetter from '../../../../app/src/Features/User/UserGetter.mjs'
import { fileURLToPath } from 'node:url'
import { Subscription as SubscriptionModel } from '../../../../app/src/models/Subscription.mjs'
const { promises: User } = UserModule
const { promises: Subscription } = SubscriptionHelper
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const SAML_TEST_CERT = fs
.readFileSync(Path.resolve(__dirname, '../../files/saml-cert.crt'), 'utf8')
.replace(/-----BEGIN CERTIFICATE-----/, '')
.replace(/-----END CERTIFICATE-----/, '')
.replace(/\n/g, '')
function getEnrollmentUrl(groupId) {
return `/subscription/${groupId}/sso_enrollment`
}
const userIdAttribute = 'nameID'
export const baseSsoConfig = {
entryPoint: 'http://example-sso.com/saml',
certificates: [SAML_TEST_CERT],
signatureAlgorithm: 'sha256',
userIdAttribute,
} // the database also sets enabled and validated, but we cannot set that in the POST request for /manage/groups/:ID/settings/sso
export async function createGroupSSO(
SSOConfigValidated = true,
useSettingsUKAMF
) {
const nonSSOMemberHelper = await UserHelper.createUser()
const nonSSOMember = nonSSOMemberHelper.user
const groupAdminUser = new User()
const memberUser = new User()
await groupAdminUser.ensureUserExists()
await memberUser.ensureUserExists()
const ssoConfigDoc = {
...baseSsoConfig,
enabled: true,
validated: SSOConfigValidated,
}
if (useSettingsUKAMF) {
ssoConfigDoc.useSettingsUKAMF = useSettingsUKAMF // conditionally adding because this field was added after group SSO was released and it will be undefined for some groups
}
const ssoConfig = new SSOConfig(ssoConfigDoc)
await ssoConfig.save()
const subscription = new Subscription({
adminId: groupAdminUser._id,
memberIds: [memberUser._id, nonSSOMember._id, groupAdminUser._id],
groupPlan: true,
planCode: 'group_professional_10_enterprise',
features: {
groupSSO: true,
},
ssoConfig: ssoConfig._id,
membersLimit: 10,
})
await subscription.ensureExists()
const subscriptionId = subscription._id.toString()
const enrollmentUrl = getEnrollmentUrl(subscriptionId)
const internalProviderId = GroupUtils.getProviderId(subscriptionId)
if (SSOConfigValidated) {
await linkGroupMember(
memberUser.email,
memberUser.password,
subscriptionId,
'mock@email.com',
useSettingsUKAMF
)
}
const userHelper = new UserHelper()
return {
ssoConfig,
internalProviderId,
userIdAttribute,
subscription,
subscriptionId,
groupAdminUser,
memberUser,
nonSSOMemberHelper,
nonSSOMember,
userHelper,
enrollmentUrl,
certificates: baseSsoConfig.certificates,
}
}
export async function linkGroupMember(
userEmail,
userPassword,
groupId,
externalUserId,
usingSettingsUKAMF
) {
const samlSettings = usingSettingsUKAMF
? Settings.saml.ukamf
: Settings.saml.groupSSO
// eslint-disable-next-line no-restricted-syntax
const subscription = await SubscriptionModel.findById(groupId)
.populate('ssoConfig')
.exec()
const userIdAttribute = subscription?.ssoConfig?.userIdAttribute
const internalProviderId = GroupUtils.getProviderId(groupId)
const enrollmentUrl = getEnrollmentUrl(groupId)
const userHelper = await UserHelper.loginUser(
{
email: userEmail,
password: userPassword,
},
`/subscription/${groupId}/sso_enrollment`
)
const { headers } = await userHelper.fetch(enrollmentUrl, {
method: 'POST',
})
if (
!headers.get('location') ||
!headers.get('location').includes(samlSettings.initPath)
) {
throw new Error('invalid redirect when linking to group SSO')
}
const redirectTo = new URL(headers.get('location'))
const initSSOResponse = await userHelper.fetch(redirectTo)
// redirect to IdP
const idpEntryPointUrl = new URL(initSSOResponse.headers.get('location'))
const requestId = await SAMLHelper.getRequestId(idpEntryPointUrl)
const response = await userHelper.fetch(samlSettings.path, {
method: 'POST',
body: new URLSearchParams({
SAMLResponse: SAMLHelper.createMockSamlResponse({
requestId,
userIdAttribute,
uniqueId: externalUserId,
issuer: samlSettings.issuer,
}),
}),
})
if (
response.status !== 302 &&
response.headers.get('location').path !== '/project'
) {
throw new Error('failed to link group SSO')
}
// ensure user linked
const user = await UserGetter.promises.getUser(
{ email: userEmail },
{ samlIdentifiers: 1, enrollment: 1 }
)
const { enrollment, samlIdentifiers } = user
const linkedToGroupSSO = samlIdentifiers.some(
identifier => identifier.providerId === internalProviderId
)
const userIsEnrolledInSSO = enrollment.sso.some(
sso => sso.groupId.toString() === groupId.toString()
)
if (!linkedToGroupSSO || !userIsEnrolledInSSO) {
throw new Error('error setting up test user with group SSO linked')
}
return userHelper
}
export async function checkUserHasSSOLinked(userId, groupId) {
const internalProviderId = GroupUtils.getProviderId(groupId)
const user = await UserGetter.promises.getUser(
{ _id: userId },
{ samlIdentifiers: 1, enrollment: 1 }
)
const { enrollment, samlIdentifiers } = user
const linkedToGroupSSO = samlIdentifiers.some(
identifier => identifier.providerId === internalProviderId
)
if (!linkedToGroupSSO) {
throw new Error('user saml identifiers are not linked to subscription')
}
const userIsEnrolledInSSO = enrollment.sso.some(
sso => sso.groupId.toString() === groupId.toString()
)
if (!userIsEnrolledInSSO) {
throw new Error('user is not enrolled in subscription')
}
}
export async function setConfigAndEnableSSO(
subscriptionHelper,
adminEmailPassword,
config
) {
config = config || {
entryPoint: 'http://idp.example.com/entry_point',
certificates: [SAML_TEST_CERT],
userIdAttribute: 'email',
userLastNameAttribute: 'lastName',
}
const { email, password } = adminEmailPassword
const userHelper = await UserHelper.loginUser({
email,
password,
})
const createResponse = await userHelper.fetch(
`/manage/groups/${subscriptionHelper._id}/settings/sso`,
{
method: 'POST',
body: JSON.stringify(config),
headers: {
'Content-Type': 'application/json',
},
}
)
if (createResponse.status !== 201) {
throw new Error(
`failed to set SSO config. Status = ${createResponse.status}`
)
}
await subscriptionHelper.setValidatedSSO()
const enableResponse = await userHelper.fetch(
`/manage/groups/${subscriptionHelper._id}/settings/enableSSO`,
{ method: 'POST' }
)
if (enableResponse.status !== 200) {
throw new Error(`failed to enable SSO. Status = ${enableResponse.status}`)
}
}
export default {
createGroupSSO,
linkGroupMember,
baseSsoConfig,
setConfigAndEnableSSO,
}