394c60f2cf
Revert "Revert "[web] Convert models and self-referential test files to ESM "" GitOrigin-RevId: f64000ae31d298b075a8722dfc51f294c71bc021
266 lines
7.3 KiB
JavaScript
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,
|
|
}
|