Files
speckle-server/packages/server/modules/workspaces/tests/integration/sso.spec.ts
T
Kristaps Fabians Geikins 8c21f1e8af Merge pull request #3481 from specklesystems/fabians/multiregion-testing3
feat(server): run tests in multi region db mode
2024-11-12 12:14:00 +00:00

457 lines
14 KiB
TypeScript

import {
deleteSsoProviderFactory,
getUserSsoSessionFactory,
getWorkspaceSsoProviderFactory,
getWorkspaceSsoProviderRecordFactory,
listUserSsoSessionsFactory,
listWorkspaceSsoMembershipsFactory,
upsertUserSsoSessionFactory
} from '@/modules/workspaces/repositories/sso'
import {
assignToWorkspace,
BasicTestWorkspace,
createTestOidcProvider,
createTestSsoSession,
createTestWorkspace,
createTestWorkspaces
} from '@/modules/workspaces/tests/helpers/creation'
import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper'
import { Roles, wait } from '@speckle/shared'
import db from '@/db/knex'
import { getDecryptor } from '@/modules/workspaces/helpers/sso'
import cryptoRandomString from 'crypto-random-string'
import { expect } from 'chai'
import { UserSsoSessionRecord } from '@/modules/workspaces/domain/sso/types'
import { truncateTables } from '@/test/hooks'
import { isValidSsoSession } from '@/modules/workspaces/domain/sso/logic'
const deleteSsoProvider = deleteSsoProviderFactory({ db })
const listUserSsoSessions = listUserSsoSessionsFactory({ db })
const listWorkspaceSsoMemberships = listWorkspaceSsoMembershipsFactory({ db })
const upsertUserSsoSession = upsertUserSsoSessionFactory({ db })
const getUserSsoSession = getUserSsoSessionFactory({ db })
const getWorkspaceSsoProviderRecord = getWorkspaceSsoProviderRecordFactory({ db })
describe('Workspace SSO repositories', () => {
const serverAdminUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: 'john-sso-speckle@example.org',
role: Roles.Server.Admin
}
const testWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'My Test Workspace',
slug: 'test-workspace'
}
before(async () => {
await createTestUser(serverAdminUser)
await createTestWorkspace(testWorkspace, serverAdminUser)
})
describe('getWorkspaceSsoProviderFactory returns a function, that', () => {
const workspace: BasicTestWorkspace = {
id: '',
ownerId: '',
slug: `test-workspace-${cryptoRandomString({ length: 6 })}`,
name: 'Test Workspace'
}
it('fetches and decrypts oidc provider information for the given workspace', async () => {
await createTestWorkspace(workspace, serverAdminUser)
const providerId = await createTestOidcProvider(workspace.id)
const provider = await getWorkspaceSsoProviderFactory({
db,
decrypt: getDecryptor()
})({ workspaceId: workspace.id })
expect(provider).to.not.be.undefined
expect(provider?.id).to.equal(providerId)
expect(typeof provider?.provider).to.not.equal('string')
})
it('returns null if the provider does not exist', async () => {
const provider = await getWorkspaceSsoProviderFactory({
db,
decrypt: getDecryptor()
})({ workspaceId: cryptoRandomString({ length: 6 }) })
expect(provider).to.be.null
})
})
describe('upsertUserSsoSessionFactory returns a function, that', () => {
const testWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Test Workspace',
slug: 'upsert-session-test-workspace'
}
let providerId: string = ''
before(async () => {
await createTestWorkspace(testWorkspace, serverAdminUser)
providerId = await createTestOidcProvider(testWorkspace.id)
})
it('creates a session if none exists', async () => {
const userSsoSession: UserSsoSessionRecord = {
userId: serverAdminUser.id,
providerId,
createdAt: new Date(),
validUntil: new Date()
}
await upsertUserSsoSession({ userSsoSession })
// TODO: Use future repo function
const sessions = await db<UserSsoSessionRecord>('user_sso_sessions').where({
providerId,
userId: serverAdminUser.id
})
expect(sessions[0].providerId).to.equal(providerId)
})
it('updates an existing session, if one exists', async () => {
const initialValidUntil = new Date()
const userSsoSession: UserSsoSessionRecord = {
userId: serverAdminUser.id,
providerId,
createdAt: new Date(),
validUntil: initialValidUntil
}
await upsertUserSsoSession({ userSsoSession })
await wait(50)
await upsertUserSsoSession({
userSsoSession: {
...userSsoSession,
validUntil: new Date()
}
})
// TODO: Use future repo function
const sessions = await db<UserSsoSessionRecord>('user_sso_sessions').where({
providerId,
userId: serverAdminUser.id
})
expect(sessions.length).to.equal(1)
expect(sessions[0].validUntil.getTime()).to.not.equal(initialValidUntil.getTime())
})
})
describe('listWorkspaceSsoMembershipsFactory returns a function, that', async () => {
const ssoUser: BasicTestUser = {
id: '',
email: 'sso-speckle@example.org',
name: 'SSO Speckle',
role: Roles.Server.Admin
}
const ssoWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace With SSO',
slug: 'yes-sso'
}
const nonSsoWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace Without SSO',
slug: 'no-sso-very-sad'
}
before(async () => {
await createTestUser(ssoUser)
await createTestWorkspace(ssoWorkspace, ssoUser)
await createTestOidcProvider(ssoWorkspace.id)
await createTestWorkspace(nonSsoWorkspace, serverAdminUser)
})
it('lists correct workspaces for the given user', async () => {
const workspaces = await listWorkspaceSsoMemberships({
userId: ssoUser.id
})
// Includes workspaces with SSO
expect(workspaces.length).to.equal(1)
expect(workspaces.some((workspace) => workspace.id === ssoWorkspace.id)).to.be
.true
// Omits workspaces without SSO
expect(workspaces.some((workspace) => workspace.id === nonSsoWorkspace.id)).to.be
.false
})
it('returns an empty array if the user is not part of any workspaces', async () => {
const testServerUser: BasicTestUser = {
id: '',
name: 'Jane Speckle',
email: 'jane-sso-speckle@example.org'
}
await createTestUser(testServerUser)
const workspaces = await listWorkspaceSsoMemberships({
userId: testServerUser.id
})
expect(workspaces.length).to.equal(0)
})
it('returns an empty array if the user does not exist', async () => {
const workspaces = await listWorkspaceSsoMemberships({
userId: cryptoRandomString({ length: 9 })
})
expect(workspaces.length).to.equal(0)
})
})
describe('listUserSsoSessionsFactory returns a function, that', async () => {
const testUserA: BasicTestUser = {
id: '',
name: 'John Speckle',
email: `${cryptoRandomString({ length: 9 })}@example.org`
}
const testWorkspaceA: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Test Workspace A',
slug: 'list-sessions-workspace-a'
}
const testWorkspaceB: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Test Workspace B',
slug: 'list-sessions-workspace-b'
}
const testWorkspaceC: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Test Workspace C',
slug: 'list-sessions-workspace-c'
}
before(async () => {
await createTestUsers([testUserA])
await createTestWorkspaces([
[testWorkspaceA, testUserA],
[testWorkspaceB, testUserA],
[testWorkspaceC, testUserA]
])
await createTestOidcProvider(testWorkspaceA.id)
await createTestOidcProvider(testWorkspaceB.id)
await createTestOidcProvider(testWorkspaceC.id)
})
afterEach(async () => {
await truncateTables(['user_sso_sessions'])
})
it('returns an empty array if there are no sessions', async () => {
const sessions = await listUserSsoSessions({ userId: testUserA.id })
expect(sessions.length).to.equal(0)
})
it('returns all sessions for the given user', async () => {
await createTestSsoSession(testUserA.id, testWorkspaceA.id)
await createTestSsoSession(testUserA.id, testWorkspaceB.id)
await createTestSsoSession(testUserA.id, testWorkspaceC.id)
const sessions = await listUserSsoSessions({ userId: testUserA.id })
expect(sessions.length).to.equal(3)
})
it('includes sessions that are expired but have not yet been deleted', async () => {
await createTestSsoSession(testUserA.id, testWorkspaceA.id)
await createTestSsoSession(testUserA.id, testWorkspaceB.id)
await createTestSsoSession(testUserA.id, testWorkspaceC.id, new Date())
await wait(150)
const sessions = await listUserSsoSessions({ userId: testUserA.id })
expect(sessions.length).to.equal(3)
expect(sessions.filter((session) => isValidSsoSession(session)).length).to.equal(
2
)
})
})
describe('getUserSsoSessionFactory returns a function, that', async () => {
const testUser: BasicTestUser = {
id: '',
name: 'John Speckle',
email: `${cryptoRandomString({ length: 9 })}@example.org`
}
const testWorkspaceWithSsoA: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace With SSO A',
slug: 'workspace-with-sso-a'
}
const testWorkspaceWithSsoB: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace With SSO B',
slug: 'workspace-with-sso-b'
}
const testWorkspaceWithoutSso: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Workspace Without SSO',
slug: 'workspace-without-sso'
}
before(async () => {
await createTestUser(testUser)
await createTestWorkspace(testWorkspaceWithSsoA, testUser)
await createTestOidcProvider(testWorkspaceWithSsoA.id)
await createTestWorkspace(testWorkspaceWithSsoB, testUser)
await createTestOidcProvider(testWorkspaceWithSsoB.id)
await createTestWorkspace(testWorkspaceWithoutSso, testUser)
})
it('returns the session for the specified user and workspace', async () => {
await createTestSsoSession(testUser.id, testWorkspaceWithSsoA.id)
const session = await getUserSsoSession({
userId: testUser.id,
workspaceId: testWorkspaceWithSsoA.id
})
expect(session).to.not.be.undefined
expect(session?.workspaceId).to.equal(testWorkspaceWithSsoA.id)
})
it('returns the session if it has expired but has not yet been deleted', async () => {
const validUntil = new Date()
validUntil.setDate(validUntil.getDate() - 1)
await createTestSsoSession(testUser.id, testWorkspaceWithSsoB.id, validUntil)
const session = await getUserSsoSession({
userId: testUser.id,
workspaceId: testWorkspaceWithSsoB.id
})
expect(session).to.not.be.undefined
})
it('returns null if the session does not exist', async () => {
const session = await getUserSsoSession({
userId: testUser.id,
workspaceId: testWorkspaceWithoutSso.id
})
expect(session).to.be.null
})
it('returns null if the workspace does not exist', async () => {
const session = await getUserSsoSession({
userId: testUser.id,
workspaceId: cryptoRandomString({ length: 9 })
})
expect(session).to.be.null
})
it('returns null if the user does not exist', async () => {
const session = await getUserSsoSession({
userId: cryptoRandomString({ length: 9 }),
workspaceId: testWorkspaceWithSsoA.id
})
expect(session).to.be.null
})
})
describe('deleteSsoProviderFactory returns a function, that', async () => {
const testWorkspaceAdmin: BasicTestUser = {
id: '',
name: 'John Speckle',
email: `${cryptoRandomString({ length: 9 })}@example.org`
}
const testWorkspaceMember: BasicTestUser = {
id: '',
name: 'Jane Speckle',
email: `${cryptoRandomString({ length: 9 })}@example.org`
}
const testWorkspace: BasicTestWorkspace = {
id: '',
ownerId: '',
name: 'Test SSO Workspace',
slug: 'test-delete-sso-workspace'
}
before(async () => {
await createTestUsers([testWorkspaceAdmin, testWorkspaceMember])
await createTestWorkspace(testWorkspace, testWorkspaceAdmin)
await assignToWorkspace(testWorkspace, testWorkspaceMember)
})
beforeEach(async () => {
await createTestOidcProvider(testWorkspace.id)
await Promise.all([
createTestSsoSession(testWorkspaceAdmin.id, testWorkspace.id),
createTestSsoSession(testWorkspaceMember.id, testWorkspace.id)
])
})
afterEach(async () => {
truncateTables(['user_sso_sessions'])
})
describe('when deleting an sso provider that exists', async () => {
beforeEach(async () => {
await deleteSsoProvider({ workspaceId: testWorkspace.id })
})
it('deletes SSO encrypted provider data for specified workspace', async () => {
const provider = await getWorkspaceSsoProviderFactory({
db,
decrypt: getDecryptor()
})({ workspaceId: testWorkspace.id })
expect(provider).to.be.null
})
it('deletes all SSO sessions for provider for specified workspace', async () => {
const adminSession = await getUserSsoSession({
userId: testWorkspaceAdmin.id,
workspaceId: testWorkspace.id
})
const memberSession = await getUserSsoSession({
userId: testWorkspaceMember.id,
workspaceId: testWorkspace.id
})
expect(adminSession).to.be.null
expect(memberSession).to.be.null
})
it('deletes workspace SSO provider record for specified workspaces', async () => {
const providerRecord = await getWorkspaceSsoProviderRecord({
workspaceId: testWorkspace.id
})
expect(providerRecord).to.be.null
})
})
describe('when deleting an sso provider that does not exist', async () => {
it('should noop', async () => {
await deleteSsoProvider({ workspaceId: cryptoRandomString({ length: 9 }) })
})
})
})
})