Files
speckle-server/packages/server/modules/workspaces/tests/integration/sso.spec.ts
T
Chuck Driesler e6e65a2f7d feat(sso): list sso associations by user email (#3420)
* feat(workspaces): add workspace sso feature flag

* feat(workspaceSso): wip validate sso

* feat(workspaces): validate and add sso provider to the workspace with user sso sessions

* feat(workspaces): validate and add sso provider to the workspace with user sso sessions

* WIP

* fix(sso): restructure to handle all branches at end of flow

* fix(sso): add and validate emails used for sso

* fix(sso): park progress

* chore(workspaces): review sso login/valdate

* fix(sso): adjust validate url

* chore(sso): auth header puzzle

* fix(sso): happy-path config

* chore(gql): gqlgen

* fix(sso): almost almost

* fix(sso): auth endpoint

* a lil more terse

* fix(sso): light at the end of the tunnel

* fix(sso): improve catch block error messages

* fix(sso): session lifespan => validUntil

* fix(sso): I think we've got it

* feat(sso): limited workspace values for public sso login

* fix(sso): use factory functions

* fix(sso): til decrypt is single-use

* fix(sso): correct usage of access codes

* fix(sso): use finalize middleware in all routes

* chore(sso): cheeky tweak

* fix(sso): move some types around

* fix(sso): stencil final shape I'm sleepy

* fix(sso): more factories more factories

* fix(sso): on to final boss of factories

* fix(sso): needs a haircut but she works

* fix(sso): init rest w function, not side-effects

* fix(sso): /authn => /sso

* chore(sso): errors

* chore(sso): test test test

* chore(sso): test all the corners

* feat(sso): list workspace sso memberships

* chore(sso): tests, expose in rest

* fix(sso): expose search via gql

---------

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
2024-11-01 11:27:12 +01:00

201 lines
6.1 KiB
TypeScript

import {
associateSsoProviderWithWorkspaceFactory,
getWorkspaceSsoProviderFactory,
listWorkspaceSsoMembershipsFactory,
upsertUserSsoSessionFactory
} from '@/modules/workspaces/repositories/sso'
import {
BasicTestWorkspace,
createTestOidcProvider,
createTestWorkspace
} from '@/modules/workspaces/tests/helpers/creation'
import { BasicTestUser, createTestUser } 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'
const associateSsoProviderWithWorkspace = associateSsoProviderWithWorkspaceFactory({
db
})
const listWorkspaceSsoMemberships = listWorkspaceSsoMembershipsFactory({ db })
const upsertUserSsoSession = upsertUserSsoSessionFactory({ 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()
await associateSsoProviderWithWorkspace({ workspaceId: workspace.id, providerId })
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', () => {
it('creates a session if none exists', async () => {
const providerId = await createTestOidcProvider()
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 providerId = await createTestOidcProvider()
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 createTestWorkspace(nonSsoWorkspace, serverAdminUser)
const providerId = await createTestOidcProvider()
await associateSsoProviderWithWorkspace({
workspaceId: ssoWorkspace.id,
providerId
})
})
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)
})
})
})