Files
speckle-server/packages/server/modules/workspaces/tests/integration/sso.spec.ts
T
Chuck Driesler b195df37d6 feat(sso): active user sso information (#3432)
* 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): sketch active user auth

* fix(sso): expose search via gql

* fix(sso): active user session information

* chore(sso): sso session test utils

* chore(sso): test sso session repo/services

* chore(sso): gqlgen

* fix(sso): simplify gql resolver structure

* chore(sso): gqlgen

---------

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

372 lines
11 KiB
TypeScript

import {
getUserSsoSessionFactory,
getWorkspaceSsoProviderFactory,
listUserSsoSessionsFactory,
listWorkspaceSsoMembershipsFactory,
upsertUserSsoSessionFactory
} from '@/modules/workspaces/repositories/sso'
import {
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 listUserSsoSessions = listUserSsoSessionsFactory({ db })
const listWorkspaceSsoMemberships = listWorkspaceSsoMembershipsFactory({ db })
const upsertUserSsoSession = upsertUserSsoSessionFactory({ db })
const getUserSsoSession = getUserSsoSessionFactory({ 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 () => {
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
})
})
})