4b06f42db7
* sort of works * type fixes * added option to run old way too
404 lines
13 KiB
TypeScript
404 lines
13 KiB
TypeScript
/* istanbul ignore file */
|
|
import { expect } from 'chai'
|
|
|
|
import { beforeEachContext } from '@/test/hooks'
|
|
|
|
import { validateScopes, authorizeResolver } from '@/modules/shared'
|
|
import { buildContext } from '@/modules/shared/middleware'
|
|
import type { AvailableRoles, ServerRoles } from '@speckle/shared'
|
|
import { Roles, Scopes } from '@speckle/shared'
|
|
import { throwForNotHavingServerRole } from '@/modules/shared/authz'
|
|
import { ForbiddenError } from '@/modules/shared/errors'
|
|
import {
|
|
getStreamFactory,
|
|
createStreamFactory,
|
|
grantStreamPermissionsFactory,
|
|
getStreamRolesFactory
|
|
} from '@/modules/core/repositories/streams'
|
|
import { db } from '@/db/knex'
|
|
import {
|
|
legacyCreateStreamFactory,
|
|
createStreamReturnRecordFactory
|
|
} from '@/modules/core/services/streams/management'
|
|
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
|
|
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
|
|
import {
|
|
findUserByTargetFactory,
|
|
insertInviteAndDeleteOldFactory,
|
|
deleteServerOnlyInvitesFactory,
|
|
updateAllInviteTargetsFactory,
|
|
findInviteFactory,
|
|
deleteInvitesByTargetFactory
|
|
} from '@/modules/serverinvites/repositories/serverInvites'
|
|
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
|
|
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
|
|
import { getEventBus } from '@/modules/shared/services/eventBus'
|
|
import { createBranchFactory } from '@/modules/core/repositories/branches'
|
|
import {
|
|
getUsersFactory,
|
|
getUserFactory,
|
|
storeUserFactory,
|
|
countAdminUsersFactory,
|
|
storeUserAclFactory
|
|
} from '@/modules/core/repositories/users'
|
|
import {
|
|
findEmailFactory,
|
|
createUserEmailFactory,
|
|
ensureNoPrimaryEmailForUserFactory
|
|
} from '@/modules/core/repositories/userEmails'
|
|
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
|
|
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
|
|
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
|
import { sendEmail } from '@/modules/emails/services/sending'
|
|
import { createUserFactory } from '@/modules/core/services/users/management'
|
|
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
|
import {
|
|
finalizeInvitedServerRegistrationFactory,
|
|
finalizeResourceInviteFactory
|
|
} from '@/modules/serverinvites/services/processing'
|
|
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
|
import { mockAdminOverride } from '@/test/mocks/global'
|
|
import {
|
|
processFinalizedProjectInviteFactory,
|
|
validateProjectInviteBeforeFinalizationFactory
|
|
} from '@/modules/serverinvites/services/coreFinalization'
|
|
import {
|
|
addOrUpdateStreamCollaboratorFactory,
|
|
validateStreamAccessFactory
|
|
} from '@/modules/core/services/streams/access'
|
|
import type { Request } from 'express'
|
|
|
|
const buildFinalizeProjectInvite = () =>
|
|
finalizeResourceInviteFactory({
|
|
findInvite: findInviteFactory({ db }),
|
|
validateInvite: validateProjectInviteBeforeFinalizationFactory({
|
|
getProject: getStream
|
|
}),
|
|
processInvite: processFinalizedProjectInviteFactory({
|
|
getProject: getStream,
|
|
addProjectRole: addOrUpdateStreamCollaboratorFactory({
|
|
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
|
getUser,
|
|
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
|
getStreamRoles: getStreamRolesFactory({ db }),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
}),
|
|
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
|
emitEvent: (...args) => getEventBus().emit(...args),
|
|
findEmail: findEmailFactory({ db }),
|
|
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
|
|
createUserEmail: createUserEmailFactory({ db }),
|
|
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
|
|
findEmail: findEmailFactory({ db }),
|
|
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
|
|
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
|
|
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
|
|
}),
|
|
requestNewEmailVerification: requestNewEmailVerificationFactory({
|
|
findEmail: findEmailFactory({ db }),
|
|
getUser,
|
|
getServerInfo,
|
|
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
|
|
db
|
|
}),
|
|
renderEmail,
|
|
sendEmail
|
|
})
|
|
}),
|
|
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
|
|
getStream
|
|
}),
|
|
getUser,
|
|
getServerInfo
|
|
})
|
|
|
|
const getServerInfo = getServerInfoFactory({ db })
|
|
const getUser = getUserFactory({ db })
|
|
const getUsers = getUsersFactory({ db })
|
|
const getStream = getStreamFactory({ db })
|
|
const createStream = legacyCreateStreamFactory({
|
|
createStreamReturnRecord: createStreamReturnRecordFactory({
|
|
inviteUsersToProject: inviteUsersToProjectFactory({
|
|
createAndSendInvite: createAndSendInviteFactory({
|
|
findUserByTarget: findUserByTargetFactory({ db }),
|
|
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
|
|
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
|
|
getStream
|
|
}),
|
|
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
|
|
getStream
|
|
}),
|
|
emitEvent: ({ eventName, payload }) =>
|
|
getEventBus().emit({
|
|
eventName,
|
|
payload
|
|
}),
|
|
getUser,
|
|
getServerInfo,
|
|
finalizeInvite: buildFinalizeProjectInvite()
|
|
}),
|
|
getUsers
|
|
}),
|
|
createStream: createStreamFactory({ db }),
|
|
createBranch: createBranchFactory({ db }),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
})
|
|
|
|
const findEmail = findEmailFactory({ db })
|
|
const requestNewEmailVerification = requestNewEmailVerificationFactory({
|
|
findEmail,
|
|
getUser: getUserFactory({ db }),
|
|
getServerInfo,
|
|
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
|
|
renderEmail,
|
|
sendEmail
|
|
})
|
|
const createUser = createUserFactory({
|
|
getServerInfo,
|
|
findEmail,
|
|
storeUser: storeUserFactory({ db }),
|
|
countAdminUsers: countAdminUsersFactory({ db }),
|
|
storeUserAcl: storeUserAclFactory({ db }),
|
|
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
|
|
createUserEmail: createUserEmailFactory({ db }),
|
|
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
|
|
findEmail,
|
|
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
|
|
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
|
|
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
|
|
}),
|
|
requestNewEmailVerification
|
|
}),
|
|
emitEvent: getEventBus().emit
|
|
})
|
|
const adminOverrideMock = mockAdminOverride()
|
|
|
|
describe('Generic AuthN & AuthZ controller tests', () => {
|
|
before(async () => {
|
|
await beforeEachContext()
|
|
})
|
|
|
|
it('Validate scopes', async () => {
|
|
await validateScopes(undefined, undefined as unknown as string)
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) =>
|
|
expect('Your auth token does not have the required scope.').to.equal(
|
|
err.message
|
|
)
|
|
)
|
|
|
|
await validateScopes(['a'], 'b')
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) =>
|
|
expect('Your auth token does not have the required scope: b.').to.equal(
|
|
err.message
|
|
)
|
|
)
|
|
|
|
await validateScopes(['a', 'b'], 'b') // should pass
|
|
})
|
|
;(<const>[
|
|
['BS header', { req: { headers: { authorization: 'Bearer BS' } } as Request }],
|
|
[
|
|
'Null header',
|
|
{ req: { headers: { authorization: null as string | null } } as Request }
|
|
],
|
|
['Undefined header', { req: { headers: { authorization: undefined } } as Request }],
|
|
['BS token', { token: 'Bearer BS' }],
|
|
['Null token', { token: null }],
|
|
['Undefined token', { token: undefined }]
|
|
]).map(([caseName, contextInput]) =>
|
|
it(`Should create proper context ${caseName}`, async () => {
|
|
const res = await buildContext(contextInput)
|
|
expect(res.auth).to.equal(false)
|
|
})
|
|
)
|
|
|
|
it('Should validate server role', async () => {
|
|
await throwForNotHavingServerRole(
|
|
{ auth: true, role: Roles.Server.User },
|
|
Roles.Server.Admin
|
|
)
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) =>
|
|
expect('You do not have the required server role').to.equal(err.message)
|
|
)
|
|
|
|
await throwForNotHavingServerRole(
|
|
{ auth: true, role: 'HACZOR' as ServerRoles },
|
|
'133TCR3w' as ServerRoles
|
|
)
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) =>
|
|
expect('Invalid role requirement specified').to.equal(err.message)
|
|
)
|
|
|
|
await throwForNotHavingServerRole(
|
|
{ auth: true, role: Roles.Server.Admin },
|
|
'133TCR3w' as ServerRoles
|
|
)
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) =>
|
|
expect('Invalid role requirement specified').to.equal(err.message)
|
|
)
|
|
|
|
const test = await throwForNotHavingServerRole(
|
|
{ auth: true, role: Roles.Server.Admin },
|
|
Roles.Server.User
|
|
)
|
|
expect(test).to.equal(true)
|
|
})
|
|
|
|
it('Resolver Authorization Should fail nicely when roles & resources are wanky', async () => {
|
|
await authorizeResolver(null, 'foo', 'bar' as AvailableRoles, null)
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) => expect('Unknown role: bar').to.equal(err.message))
|
|
|
|
// this caught me out, but streams:read is not a valid role for now
|
|
await authorizeResolver(
|
|
'foo',
|
|
'bar' as AvailableRoles,
|
|
Scopes.Streams.Read as AvailableRoles,
|
|
null
|
|
)
|
|
.then(() => {
|
|
throw new Error('This should have been rejected')
|
|
})
|
|
.catch((err) => expect('Unknown role: streams:read').to.equal(err.message))
|
|
})
|
|
|
|
describe('Authorize resolver ', () => {
|
|
const myStream = {
|
|
name: 'My Stream 2',
|
|
isPublic: true,
|
|
id: ''
|
|
}
|
|
const notMyStream = {
|
|
name: 'Not My Stream 1',
|
|
isPublic: false,
|
|
id: ''
|
|
}
|
|
const serverOwner = {
|
|
name: 'Itsa Me',
|
|
email: 'me@example.org',
|
|
password: 'sn3aky-1337-b1m',
|
|
id: ''
|
|
}
|
|
const otherGuy = {
|
|
name: 'Some Other DUde',
|
|
email: 'otherguy@example.org',
|
|
password: 'sn3aky-1337-b1m',
|
|
id: ''
|
|
}
|
|
|
|
before(async function () {
|
|
// Seeding
|
|
serverOwner.id = await createUser(serverOwner)
|
|
otherGuy.id = await createUser(otherGuy)
|
|
|
|
await Promise.all([
|
|
createStream({ ...myStream, ownerId: serverOwner.id }).then(
|
|
(id) => (myStream.id = id)
|
|
),
|
|
createStream({ ...notMyStream, ownerId: otherGuy.id }).then(
|
|
(id) => (notMyStream.id = id)
|
|
)
|
|
])
|
|
})
|
|
|
|
afterEach(() => {
|
|
adminOverrideMock.disable()
|
|
})
|
|
after(() => {
|
|
adminOverrideMock.disable()
|
|
})
|
|
it('should allow stream:owners to be stream:owners', async () => {
|
|
await authorizeResolver(
|
|
serverOwner.id,
|
|
myStream.id,
|
|
Roles.Stream.Contributor,
|
|
null
|
|
)
|
|
})
|
|
|
|
it('should get the passed in role for server:admins if override enabled', async () => {
|
|
adminOverrideMock.enable(true)
|
|
await authorizeResolver(
|
|
serverOwner.id,
|
|
myStream.id,
|
|
Roles.Stream.Contributor,
|
|
null
|
|
)
|
|
})
|
|
it('should not allow server:admins to be anything if adminOverride is disabled', async () => {
|
|
try {
|
|
await authorizeResolver(
|
|
serverOwner.id,
|
|
notMyStream.id,
|
|
Roles.Stream.Contributor,
|
|
null
|
|
)
|
|
throw new Error('This should have thrown')
|
|
} catch (e) {
|
|
expect(e instanceof ForbiddenError)
|
|
}
|
|
})
|
|
|
|
it('should allow server:admins to be anything if adminOverride is enabled', async () => {
|
|
adminOverrideMock.enable(true)
|
|
|
|
await authorizeResolver(
|
|
serverOwner.id,
|
|
notMyStream.id,
|
|
Roles.Stream.Contributor,
|
|
null
|
|
)
|
|
})
|
|
|
|
it('should not allow server:users to be anything if adminOverride is disabled', async () => {
|
|
try {
|
|
await authorizeResolver(
|
|
otherGuy.id,
|
|
myStream.id,
|
|
Roles.Stream.Contributor,
|
|
null
|
|
)
|
|
throw new Error('This should have thrown')
|
|
} catch (e) {
|
|
expect(e instanceof ForbiddenError)
|
|
}
|
|
})
|
|
|
|
it('should not allow server:users to be anything if adminOverride is enabled', async () => {
|
|
adminOverrideMock.enable(true)
|
|
|
|
try {
|
|
await authorizeResolver(
|
|
otherGuy.id,
|
|
myStream.id,
|
|
Roles.Stream.Contributor,
|
|
null
|
|
)
|
|
throw new Error('This should have thrown')
|
|
} catch (e) {
|
|
expect(e instanceof ForbiddenError)
|
|
}
|
|
})
|
|
})
|
|
})
|