-
Notifications
You must be signed in to change notification settings - Fork 177
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sso): protect access with sso sessions (#3441)
* 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 * feat(sso): throw error on missing or expired sso session * chore(sso): tests for SSO access protection --------- Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com> Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
- Loading branch information
1 parent
1c0482b
commit d42bf7c
Showing
7 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
251 changes: 251 additions & 0 deletions
251
packages/server/modules/workspaces/tests/integration/sso.graph.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
import { | ||
assignToWorkspaces, | ||
BasicTestWorkspace, | ||
createTestOidcProvider, | ||
createTestSsoSession, | ||
createTestWorkspaces | ||
} from '@/modules/workspaces/tests/helpers/creation' | ||
import { | ||
BasicTestUser, | ||
createAuthTokenForUser, | ||
createTestUsers | ||
} from '@/test/authHelper' | ||
import { | ||
ActiveUserExpiredSsoSessionsDocument, | ||
GetActiveUserWorkspacesDocument, | ||
GetProjectDocument, | ||
GetWorkspaceDocument, | ||
GetWorkspaceProjectsDocument, | ||
GetWorkspaceSsoDocument | ||
} from '@/test/graphql/generated/graphql' | ||
import { | ||
createTestContext, | ||
testApolloServer, | ||
TestApolloServer | ||
} from '@/test/graphqlHelper' | ||
import { truncateTables } from '@/test/hooks' | ||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper' | ||
import { AllScopes, Roles } from '@speckle/shared' | ||
import { expect } from 'chai' | ||
import cryptoRandomString from 'crypto-random-string' | ||
|
||
describe('Workspace SSO', () => { | ||
let memberApollo: TestApolloServer | ||
let guestApollo: TestApolloServer | ||
|
||
const workspaceAdmin: BasicTestUser = { | ||
id: '', | ||
name: 'John Admin', | ||
email: `${cryptoRandomString({ length: 9 })}@example.org`, | ||
role: Roles.Server.Admin | ||
} | ||
|
||
const workspaceMember: BasicTestUser = { | ||
id: '', | ||
name: 'John Member', | ||
email: `${cryptoRandomString({ length: 9 })}@example.org` | ||
} | ||
|
||
const workspaceGuest: BasicTestUser = { | ||
id: '', | ||
name: 'John Guest', | ||
email: `${cryptoRandomString({ length: 9 })}@example.org` | ||
} | ||
|
||
const testWorkspaceWithSso: BasicTestWorkspace = { | ||
id: '', | ||
ownerId: '', | ||
name: 'Test SSO Workspace', | ||
slug: 'gql-sso-workspace' | ||
} | ||
let testWorkspaceWithSsoProviderId = '' | ||
let testWorkspaceWithSsoProjectId = '' | ||
|
||
const testWorkspaceWithoutSso: BasicTestWorkspace = { | ||
id: '', | ||
ownerId: '', | ||
name: 'Test Non-SSO Workspace', | ||
slug: 'gql-no-sso-workspace' | ||
} | ||
|
||
before(async () => { | ||
await createTestUsers([workspaceAdmin, workspaceMember, workspaceGuest]) | ||
await createTestWorkspaces([ | ||
[testWorkspaceWithSso, workspaceAdmin], | ||
[testWorkspaceWithoutSso, workspaceAdmin] | ||
]) | ||
testWorkspaceWithSsoProviderId = await createTestOidcProvider( | ||
testWorkspaceWithSso.id | ||
) | ||
|
||
await assignToWorkspaces([ | ||
[testWorkspaceWithSso, workspaceMember, Roles.Workspace.Member], | ||
[testWorkspaceWithSso, workspaceGuest, Roles.Workspace.Guest], | ||
[testWorkspaceWithoutSso, workspaceMember, Roles.Workspace.Member], | ||
[testWorkspaceWithoutSso, workspaceGuest, Roles.Workspace.Guest] | ||
]) | ||
|
||
memberApollo = await testApolloServer({ | ||
context: createTestContext({ | ||
auth: true, | ||
userId: workspaceMember.id, | ||
token: await createAuthTokenForUser(workspaceMember.id), | ||
role: Roles.Server.User, | ||
scopes: AllScopes | ||
}) | ||
}) | ||
guestApollo = await testApolloServer({ | ||
context: createTestContext({ | ||
auth: true, | ||
userId: workspaceGuest.id, | ||
token: await createAuthTokenForUser(workspaceGuest.id), | ||
role: Roles.Server.User, | ||
scopes: AllScopes | ||
}) | ||
}) | ||
|
||
const testProject: BasicTestStream = { | ||
id: '', | ||
ownerId: '', | ||
isPublic: false, | ||
name: 'Workspace Project', | ||
workspaceId: testWorkspaceWithSso.id | ||
} | ||
|
||
await createTestStream(testProject, workspaceAdmin) | ||
testWorkspaceWithSsoProjectId = testProject.id | ||
}) | ||
|
||
afterEach(async () => { | ||
truncateTables(['user_sso_sessions']) | ||
}) | ||
|
||
describe('given a workspace with SSO configured', () => { | ||
describe('when a workspace member requests workspace information', () => { | ||
describe('with a valid SSO session', () => { | ||
beforeEach(async () => { | ||
await createTestSsoSession(workspaceMember.id, testWorkspaceWithSso.id) | ||
}) | ||
|
||
it('should allow the request', async () => { | ||
const res = await memberApollo.execute(GetWorkspaceDocument, { | ||
workspaceId: testWorkspaceWithSso.id | ||
}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.workspace.slug).to.equal('gql-sso-workspace') | ||
}) | ||
|
||
it('should provide active SSO session information on workspace type', async () => { | ||
const res = await memberApollo.execute(GetWorkspaceSsoDocument, { | ||
id: testWorkspaceWithSso.id | ||
}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.workspace.sso).to.not.be.undefined | ||
expect(res.data?.workspace.sso?.provider?.id).to.equal( | ||
testWorkspaceWithSsoProviderId | ||
) | ||
expect(res.data?.workspace.sso?.session).to.not.be.undefined | ||
}) | ||
}) | ||
|
||
describe('without a valid SSO session', () => { | ||
it('should throw and provide redirect information', async () => { | ||
const resA = await memberApollo.execute(GetWorkspaceDocument, { | ||
workspaceId: testWorkspaceWithSso.id | ||
}) | ||
const resB = await memberApollo.execute(GetWorkspaceProjectsDocument, { | ||
id: testWorkspaceWithSso.id | ||
}) | ||
const resC = await memberApollo.execute(GetProjectDocument, { | ||
id: testWorkspaceWithSsoProjectId | ||
}) | ||
|
||
for (const res of [resA, resB, resC]) { | ||
expect(res).to.haveGraphQLErrors({ message: 'gql-sso-workspace' }) | ||
expect(res).to.haveGraphQLErrors({ | ||
code: 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR' | ||
}) | ||
} | ||
}) | ||
|
||
it('should allow limited access to workspace memberships', async () => { | ||
const res = await memberApollo.execute(GetActiveUserWorkspacesDocument, {}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.activeUser?.workspaces.items.length).to.equal(2) | ||
}) | ||
|
||
it('should surface expired session', async () => { | ||
const res = await memberApollo.execute( | ||
ActiveUserExpiredSsoSessionsDocument, | ||
{} | ||
) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.activeUser?.expiredSsoSessions.length).to.equal(1) | ||
expect(res.data?.activeUser?.expiredSsoSessions[0].slug).to.equal( | ||
'gql-sso-workspace' | ||
) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('when a workspace guest requests workspace information', () => { | ||
describe('without a valid SSO session', () => { | ||
it('should allow the request', async () => { | ||
const res = await guestApollo.execute(GetWorkspaceDocument, { | ||
workspaceId: testWorkspaceWithSso.id | ||
}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.workspace.slug).to.equal('gql-sso-workspace') | ||
}) | ||
|
||
it('should not show the workspace as an expired SSO session', async () => { | ||
const res = await guestApollo.execute( | ||
ActiveUserExpiredSsoSessionsDocument, | ||
{} | ||
) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.activeUser?.expiredSsoSessions.length).to.equal(0) | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('given a workspace without SSO configured', () => { | ||
describe('when a workspace member requests workspace information', () => { | ||
it('should allow the request', async () => { | ||
const res = await memberApollo.execute(GetWorkspaceDocument, { | ||
workspaceId: testWorkspaceWithoutSso.id | ||
}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.workspace.slug).to.equal('gql-no-sso-workspace') | ||
}) | ||
|
||
it('should return workspace provider information as `null`', async () => { | ||
const res = await memberApollo.execute(GetWorkspaceSsoDocument, { | ||
id: testWorkspaceWithoutSso.id | ||
}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.workspace.sso).to.be.null | ||
}) | ||
}) | ||
|
||
describe('when a workspace guest requests workspace information', () => { | ||
it('should allow the request', async () => { | ||
const res = await guestApollo.execute(GetWorkspaceDocument, { | ||
workspaceId: testWorkspaceWithoutSso.id | ||
}) | ||
|
||
expect(res).to.not.haveGraphQLErrors() | ||
expect(res.data?.workspace.slug).to.equal('gql-no-sso-workspace') | ||
}) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.