feat(shared): rework policy internals to work with loader errors in checks and policy fragments (#4276)
* feat(shared): rework policy internals to work with loader errors in checks and policy fragments * fix(server): auth reintegration
This commit is contained in:
@@ -11,7 +11,7 @@ export default defineModuleLoaders(async () => {
|
||||
const getUserServerRole = getUserServerRoleFactory({ db })
|
||||
|
||||
return {
|
||||
getEnv: async () => ok(getFeatureFlags()),
|
||||
getEnv: async () => getFeatureFlags(),
|
||||
getProject: async ({ projectId }) => {
|
||||
const project = await getStream({ streamId: projectId })
|
||||
if (!project) return err(Authz.ProjectNotFoundError)
|
||||
|
||||
@@ -163,7 +163,7 @@ const cloneStream = cloneStreamFactory({
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
// We want to read & write from main DB - this isn't occuring in a multi region workspace ctx
|
||||
// We want to read & write from main DB - this isn't occurring in a multi region workspace ctx
|
||||
const createOnboardingStream = createOnboardingStreamFactory({
|
||||
getOnboardingBaseProject: getOnboardingBaseProjectFactory({
|
||||
getOnboardingBaseStream: getOnboardingBaseStreamFactory({ db })
|
||||
@@ -192,6 +192,9 @@ export = {
|
||||
case Authz.WorkspaceNoAccessError.code:
|
||||
case Authz.WorkspaceSsoSessionInvalidError.code:
|
||||
throw new ForbiddenError(canQuery.error.message)
|
||||
case Authz.ServerNoAccessError.code:
|
||||
case Authz.ServerNoSessionError.code:
|
||||
throw new ForbiddenError(canQuery.error.message)
|
||||
default:
|
||||
throwUncoveredError(canQuery.error)
|
||||
}
|
||||
|
||||
@@ -1,104 +1,65 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
requireExactProjectVisibilityFactory,
|
||||
requireMinimumProjectRoleFactory
|
||||
} from './projects.js'
|
||||
import { isPubliclyReadableProject, hasMinimumProjectRole } from './projects.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Project } from '../domain/projects/types.js'
|
||||
import { Roles, UncoveredError } from '../../core/index.js'
|
||||
import { Roles } from '../../core/index.js'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { ProjectNotFoundError, ProjectRoleNotFoundError } from '../domain/authErrors.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ProjectRoleNotFoundError,
|
||||
WorkspaceSsoSessionInvalidError
|
||||
} from '../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../tests/fakes.js'
|
||||
|
||||
describe('project checks', () => {
|
||||
describe('requireExactProjectVisibilityFactory returns a function, that', () => {
|
||||
it('throws if project does not exist', async () => {
|
||||
const requireExactProjectVisibility = requireExactProjectVisibilityFactory({
|
||||
loaders: {
|
||||
getProject: () => Promise.resolve(err(ProjectNotFoundError))
|
||||
}
|
||||
})
|
||||
describe('isPubliclyReadableProject returns a function, that', () => {
|
||||
it('throws uncoveredError for unexpected loader errors', async () => {
|
||||
await expect(
|
||||
requireExactProjectVisibility({
|
||||
projectVisibility: 'linkShareable',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
).rejects.toThrow()
|
||||
isPubliclyReadableProject({
|
||||
// @ts-expect-error deliberately testing an unexpeceted error type
|
||||
getProject: async () => err(ProjectRoleNotFoundError)
|
||||
})({ projectId: cryptoRandomString({ length: 10 }) })
|
||||
).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it('correctly asserts link shareable projects', async () => {
|
||||
const result = await requireExactProjectVisibilityFactory({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
isDiscoverable: true
|
||||
} as Project)
|
||||
)
|
||||
}
|
||||
})({
|
||||
projectVisibility: 'linkShareable',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
it.each([
|
||||
ProjectNotFoundError,
|
||||
ProjectNoAccessError,
|
||||
WorkspaceSsoSessionInvalidError
|
||||
])('turns expected loader error $code into false ', async (loaderError) => {
|
||||
const result = await isPubliclyReadableProject({
|
||||
getProject: async () => err(loaderError)
|
||||
})({ projectId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true for public projects', async () => {
|
||||
const result = await isPubliclyReadableProject({
|
||||
getProject: getProjectFake({ isPublic: true })
|
||||
})({ projectId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('correctly asserts public projects', async () => {
|
||||
const result = await requireExactProjectVisibilityFactory({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
isPublic: true
|
||||
} as Project)
|
||||
)
|
||||
}
|
||||
})({
|
||||
projectVisibility: 'public',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
it('returns true for discoverable projects', async () => {
|
||||
const result = await isPubliclyReadableProject({
|
||||
getProject: getProjectFake({ isDiscoverable: true })
|
||||
})({ projectId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('correctly asserts private projects', async () => {
|
||||
const result = await requireExactProjectVisibilityFactory({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
} as Project)
|
||||
)
|
||||
}
|
||||
})({
|
||||
projectVisibility: 'private',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('throws for unknown project visibility', async () => {
|
||||
await expect(
|
||||
requireExactProjectVisibilityFactory({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
} as Project)
|
||||
)
|
||||
}
|
||||
})({
|
||||
// @ts-expect-error this is what im testing here
|
||||
projectVisibility: 'unknown',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
).rejects.toThrow(UncoveredError)
|
||||
})
|
||||
})
|
||||
describe('requireMinimumProjectRoleFactory return a function, that', () => {
|
||||
describe('hasMinimumProjectRole returns a function, that', () => {
|
||||
it('throws uncoveredError for unexpected loader errors', async () => {
|
||||
await expect(
|
||||
hasMinimumProjectRole({
|
||||
// @ts-expect-error deliberately testing an unexpeceted error type
|
||||
getProjectRole: async () => err(ProjectNotFoundError)
|
||||
})({
|
||||
projectId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
role: Roles.Stream.Contributor
|
||||
})
|
||||
).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it('returns false, if there is no role for the user', async () => {
|
||||
const result = await requireMinimumProjectRoleFactory({
|
||||
loaders: {
|
||||
getProjectRole: () => Promise.resolve(err(ProjectRoleNotFoundError))
|
||||
}
|
||||
const result = await hasMinimumProjectRole({
|
||||
getProjectRole: () => Promise.resolve(err(ProjectRoleNotFoundError))
|
||||
})({
|
||||
projectId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -107,10 +68,8 @@ describe('project checks', () => {
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false, if the role is not sufficient', async () => {
|
||||
const result = await requireMinimumProjectRoleFactory({
|
||||
loaders: {
|
||||
getProjectRole: () => Promise.resolve(ok(Roles.Stream.Reviewer))
|
||||
}
|
||||
const result = await hasMinimumProjectRole({
|
||||
getProjectRole: () => Promise.resolve(ok(Roles.Stream.Reviewer))
|
||||
})({
|
||||
projectId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
@@ -119,10 +78,8 @@ describe('project checks', () => {
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true, if the role is sufficient', async () => {
|
||||
const result = await requireMinimumProjectRoleFactory({
|
||||
loaders: {
|
||||
getProjectRole: () => Promise.resolve(ok(Roles.Stream.Contributor))
|
||||
}
|
||||
const result = await hasMinimumProjectRole({
|
||||
getProjectRole: () => Promise.resolve(ok(Roles.Stream.Contributor))
|
||||
})({
|
||||
projectId: cryptoRandomString({ length: 10 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
import { StreamRoles, throwUncoveredError } from '../../core/index.js'
|
||||
import { ProjectNotFoundError } from '../domain/errors.js'
|
||||
import { AuthCheckContext, AuthCheckContextLoaderKeys } from '../domain/loaders.js'
|
||||
import { isMinimumProjectRole } from '../domain/projects/logic.js'
|
||||
import { ProjectVisibility } from '../domain/projects/types.js'
|
||||
|
||||
export const requireExactProjectVisibilityFactory =
|
||||
({ loaders }: AuthCheckContext<typeof AuthCheckContextLoaderKeys.getProject>) =>
|
||||
async (args: {
|
||||
projectVisibility: ProjectVisibility
|
||||
projectId: string
|
||||
}): Promise<boolean> => {
|
||||
const { projectId, projectVisibility } = args
|
||||
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!project.isOk) throw new ProjectNotFoundError({ projectId })
|
||||
|
||||
switch (projectVisibility) {
|
||||
case 'linkShareable':
|
||||
return project.value.isDiscoverable === true
|
||||
case 'public':
|
||||
return project.value.isPublic === true
|
||||
case 'private':
|
||||
return project.value.isPublic !== true && project.value.isDiscoverable !== true
|
||||
default:
|
||||
throwUncoveredError(projectVisibility)
|
||||
}
|
||||
}
|
||||
|
||||
export const requireMinimumProjectRoleFactory =
|
||||
({ loaders }: AuthCheckContext<typeof AuthCheckContextLoaderKeys.getProjectRole>) =>
|
||||
async (args: {
|
||||
userId: string
|
||||
projectId: string
|
||||
role: StreamRoles
|
||||
}): Promise<boolean> => {
|
||||
const { userId, projectId, role: requiredProjectRole } = args
|
||||
import { AuthPolicyCheck, ProjectContext, UserContext } from '../domain/policies.js'
|
||||
import { isMinimumProjectRole } from '../domain/logic/roles.js'
|
||||
|
||||
export const hasMinimumProjectRole: AuthPolicyCheck<
|
||||
'getProjectRole',
|
||||
UserContext & ProjectContext & { role: StreamRoles }
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId, role: requiredProjectRole }) => {
|
||||
const userProjectRole = await loaders.getProjectRole({ userId, projectId })
|
||||
return userProjectRole.isOk
|
||||
? isMinimumProjectRole(userProjectRole.value, requiredProjectRole)
|
||||
: false
|
||||
if (userProjectRole.isErr) {
|
||||
switch (userProjectRole.error.code) {
|
||||
case 'ProjectRoleNotFound':
|
||||
return false
|
||||
default:
|
||||
throwUncoveredError(userProjectRole.error.code)
|
||||
}
|
||||
}
|
||||
return isMinimumProjectRole(userProjectRole.value, requiredProjectRole)
|
||||
}
|
||||
|
||||
export const isPubliclyReadableProject: AuthPolicyCheck<'getProject', ProjectContext> =
|
||||
(loaders) =>
|
||||
async ({ projectId }) => {
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (project.isErr) {
|
||||
switch (project.error.code) {
|
||||
case 'ProjectNotFound':
|
||||
return false
|
||||
case 'ProjectNoAccess':
|
||||
return false
|
||||
case 'WorkspaceSsoSessionInvalid':
|
||||
return false
|
||||
default:
|
||||
throwUncoveredError(project.error)
|
||||
}
|
||||
}
|
||||
return project.value.isPublic || project.value.isDiscoverable
|
||||
}
|
||||
|
||||
@@ -1,41 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requireExactServerRole } from './serverRole.js'
|
||||
import { hasMinimumServerRole, canUseAdminOverride } from './serverRole.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { ServerRoleNotFoundError } from '../domain/authErrors.js'
|
||||
import {
|
||||
ServerRoleNotFoundError,
|
||||
ProjectRoleNotFoundError
|
||||
} from '../domain/authErrors.js'
|
||||
import { parseFeatureFlags } from '../../environment/index.js'
|
||||
|
||||
describe('requireExactServerRole returns a function, that', () => {
|
||||
it('returns false for mismatch roles', async () => {
|
||||
const result = await requireExactServerRole({
|
||||
loaders: {
|
||||
getServerRole: () => Promise.resolve(ok('server:user'))
|
||||
}
|
||||
describe('hasMinimumServerRole returns a function, that', () => {
|
||||
it('throws uncoveredError for unexpected loader errors', async () => {
|
||||
await expect(
|
||||
hasMinimumServerRole({
|
||||
// @ts-expect-error deliberately testing an unexpected loader error
|
||||
getServerRole: async () => err(ProjectRoleNotFoundError)
|
||||
})({ userId: cryptoRandomString({ length: 10 }), role: 'server:user' })
|
||||
).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it.each([ServerRoleNotFoundError])(
|
||||
'turns expected loader error $code into false ',
|
||||
async (loaderError) => {
|
||||
const result = await hasMinimumServerRole({
|
||||
getServerRole: async () => err(loaderError)
|
||||
})({ userId: cryptoRandomString({ length: 10 }), role: 'server:user' })
|
||||
expect(result).toEqual(false)
|
||||
}
|
||||
)
|
||||
it('returns false for smaller roles', async () => {
|
||||
const result = await hasMinimumServerRole({
|
||||
getServerRole: async () => Promise.resolve(ok('server:user'))
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
role: 'server:admin'
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false for users without roles', async () => {
|
||||
const result = await requireExactServerRole({
|
||||
loaders: {
|
||||
getServerRole: () => Promise.resolve(err(ServerRoleNotFoundError))
|
||||
}
|
||||
it('returns true for roles with enough power', async () => {
|
||||
const result = await hasMinimumServerRole({
|
||||
getServerRole: () => Promise.resolve(ok('server:admin'))
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
role: 'server:admin'
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true for matching roles', async () => {
|
||||
const result = await requireExactServerRole({
|
||||
loaders: {
|
||||
getServerRole: () => Promise.resolve(ok('server:admin'))
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
role: 'server:admin'
|
||||
role: 'server:guest'
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('canUseAdminOverride returns a function, that', () => {
|
||||
it('returns false for admins if admin override is not enabled', async () => {
|
||||
const result = await canUseAdminOverride({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getServerRole: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({ userId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false for non admins if admin override is not enabled', async () => {
|
||||
const result = await canUseAdminOverride({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getServerRole: async () => ok('server:user')
|
||||
})({ userId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns false for non admins if admin override is enabled', async () => {
|
||||
const result = await canUseAdminOverride({
|
||||
getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
|
||||
getServerRole: async () => ok('server:user')
|
||||
})({ userId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true for admins if admin override is enabled', async () => {
|
||||
const result = await canUseAdminOverride({
|
||||
getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
|
||||
getServerRole: async () => ok('server:admin')
|
||||
})({ userId: cryptoRandomString({ length: 10 }) })
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
import { ServerRoles } from '../../core/constants.js'
|
||||
import { AuthCheckContext, AuthCheckContextLoaderKeys } from '../domain/loaders.js'
|
||||
|
||||
export const requireExactServerRole =
|
||||
({ loaders }: AuthCheckContext<typeof AuthCheckContextLoaderKeys.getServerRole>) =>
|
||||
async (args: { userId: string; role: ServerRoles }): Promise<boolean> => {
|
||||
const { userId, role: requiredServerRole } = args
|
||||
import { Roles, ServerRoles } from '../../core/constants.js'
|
||||
import { throwUncoveredError } from '../../core/index.js'
|
||||
import { isMinimumServerRole } from '../domain/logic/roles.js'
|
||||
import { AuthPolicyCheck } from '../domain/policies.js'
|
||||
|
||||
export const hasMinimumServerRole: AuthPolicyCheck<
|
||||
'getServerRole',
|
||||
{ userId: string; role: ServerRoles }
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, role: requiredServerRole }) => {
|
||||
const userServerRole = await loaders.getServerRole({ userId })
|
||||
if (!userServerRole.isOk) return false
|
||||
|
||||
return userServerRole.value === requiredServerRole
|
||||
if (userServerRole.isErr) {
|
||||
switch (userServerRole.error.code) {
|
||||
case 'ServerRoleNotFound':
|
||||
return false
|
||||
default:
|
||||
throwUncoveredError(userServerRole.error.code)
|
||||
}
|
||||
}
|
||||
return isMinimumServerRole(userServerRole.value, requiredServerRole)
|
||||
}
|
||||
|
||||
export const canUseAdminOverride: AuthPolicyCheck<
|
||||
'getEnv' | 'getServerRole',
|
||||
{ userId: string }
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId }) => {
|
||||
const { FF_ADMIN_OVERRIDE_ENABLED } = await loaders.getEnv()
|
||||
if (!FF_ADMIN_OVERRIDE_ENABLED) return false
|
||||
return await hasMinimumServerRole(loaders)({
|
||||
userId,
|
||||
role: Roles.Server.Admin
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
requireAnyWorkspaceRole,
|
||||
requireMinimumWorkspaceRole
|
||||
} from './workspaceRole.js'
|
||||
import { hasAnyWorkspaceRole, requireMinimumWorkspaceRole } from './workspaceRole.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { WorkspaceRoleNotFoundError } from '../domain/authErrors.js'
|
||||
import {
|
||||
WorkspaceRoleNotFoundError,
|
||||
ProjectRoleNotFoundError
|
||||
} from '../domain/authErrors.js'
|
||||
|
||||
describe('requireAnyWorkspaceRole returns a function, that', () => {
|
||||
describe('hasAnyWorkspaceRole returns a function, that', () => {
|
||||
it('throws uncoveredError for unexpected loader errors', async () => {
|
||||
await expect(
|
||||
hasAnyWorkspaceRole({
|
||||
// @ts-expect-error deliberately testing an unexpected loader error
|
||||
getWorkspaceRole: async () => err(ProjectRoleNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it.each([WorkspaceRoleNotFoundError])(
|
||||
'turns expected loader error $code into false ',
|
||||
async (loaderError) => {
|
||||
const result = await hasAnyWorkspaceRole({
|
||||
getWorkspaceRole: async () => err(loaderError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
}
|
||||
)
|
||||
it('returns false if the user has no role', async () => {
|
||||
const result = await requireAnyWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(err(WorkspaceRoleNotFoundError))
|
||||
}
|
||||
const result = await hasAnyWorkspaceRole({
|
||||
getWorkspaceRole: async () => err(WorkspaceRoleNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
@@ -20,10 +41,8 @@ describe('requireAnyWorkspaceRole returns a function, that', () => {
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true if the user has a role', async () => {
|
||||
const result = await requireAnyWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member'))
|
||||
}
|
||||
const result = await hasAnyWorkspaceRole({
|
||||
getWorkspaceRole: async () => ok('workspace:member')
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
@@ -33,50 +52,57 @@ describe('requireAnyWorkspaceRole returns a function, that', () => {
|
||||
})
|
||||
|
||||
describe('requireMinimumWorkspaceRole returns a function, that', () => {
|
||||
it('returns false if user does not have a role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(err(WorkspaceRoleNotFoundError))
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
it('throws uncoveredError for unexpected loader errors', async () => {
|
||||
await expect(
|
||||
requireMinimumWorkspaceRole({
|
||||
// @ts-expect-error deliberately testing an unexpected loader error
|
||||
getWorkspaceRole: async () => err(ProjectRoleNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it.each([WorkspaceRoleNotFoundError])(
|
||||
'turns expected loader error $code into false ',
|
||||
async (loaderError) => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
getWorkspaceRole: async () => err(loaderError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
}
|
||||
)
|
||||
it('returns false if user is below target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member'))
|
||||
}
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member'))
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
role: 'workspace:admin'
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true if user matches target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member'))
|
||||
}
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member'))
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('returns true if user exceeds target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:admin'))
|
||||
}
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:admin'))
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import { WorkspaceRoles } from '../../core/constants.js'
|
||||
import { AuthCheckContext, AuthCheckContextLoaderKeys } from '../domain/loaders.js'
|
||||
import { isMinimumWorkspaceRole } from '../domain/workspaces/logic.js'
|
||||
|
||||
export const requireAnyWorkspaceRole =
|
||||
({ loaders }: AuthCheckContext<typeof AuthCheckContextLoaderKeys.getWorkspaceRole>) =>
|
||||
async (args: { userId: string; workspaceId: string }): Promise<boolean> => {
|
||||
const { userId, workspaceId } = args
|
||||
import { throwUncoveredError } from '../../core/index.js'
|
||||
import { isMinimumWorkspaceRole } from '../domain/logic/roles.js'
|
||||
import { AuthPolicyCheck } from '../domain/policies.js'
|
||||
|
||||
export const requireMinimumWorkspaceRole: AuthPolicyCheck<
|
||||
'getWorkspaceRole',
|
||||
{ userId: string; workspaceId: string; role: WorkspaceRoles }
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId, role: requiredWorkspaceRole }) => {
|
||||
const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId })
|
||||
return userWorkspaceRole.isOk
|
||||
if (userWorkspaceRole.isErr) {
|
||||
switch (userWorkspaceRole.error.code) {
|
||||
case 'WorkspaceRoleNotFound':
|
||||
return false
|
||||
default:
|
||||
throwUncoveredError(userWorkspaceRole.error.code)
|
||||
}
|
||||
}
|
||||
|
||||
return isMinimumWorkspaceRole(userWorkspaceRole.value, requiredWorkspaceRole)
|
||||
}
|
||||
|
||||
export const requireMinimumWorkspaceRole =
|
||||
({ loaders }: AuthCheckContext<typeof AuthCheckContextLoaderKeys.getWorkspaceRole>) =>
|
||||
async (args: {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
role: WorkspaceRoles
|
||||
}): Promise<boolean> => {
|
||||
const { userId, workspaceId, role: requiredWorkspaceRole } = args
|
||||
|
||||
export const hasAnyWorkspaceRole: AuthPolicyCheck<
|
||||
'getWorkspaceRole',
|
||||
{ userId: string; workspaceId: string }
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId })
|
||||
|
||||
return userWorkspaceRole.isOk
|
||||
? isMinimumWorkspaceRole(userWorkspaceRole.value, requiredWorkspaceRole)
|
||||
: false
|
||||
if (userWorkspaceRole.isOk) return true
|
||||
switch (userWorkspaceRole.error.code) {
|
||||
case 'WorkspaceRoleNotFound':
|
||||
return false
|
||||
default:
|
||||
throwUncoveredError(userWorkspaceRole.error.code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requireValidWorkspaceSsoSession } from './workspaceSso.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import { WorkspaceSsoSessionNotFoundError } from '../domain/authErrors.js'
|
||||
|
||||
describe('requireValidWorkspaceSsoSession returns a function, that', () => {
|
||||
it('returns false if user does not have an SSO session', async () => {
|
||||
const result = await requireValidWorkspaceSsoSession({
|
||||
loaders: {
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve(err(WorkspaceSsoSessionNotFoundError))
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('returns false if user has an expired sso session', async () => {
|
||||
const userId = cryptoRandomString({ length: 9 })
|
||||
const providerId = cryptoRandomString({ length: 9 })
|
||||
const workspaceId = cryptoRandomString({ length: 9 })
|
||||
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() - 1)
|
||||
|
||||
const result = await requireValidWorkspaceSsoSession({
|
||||
loaders: {
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
userId,
|
||||
providerId,
|
||||
validUntil
|
||||
})
|
||||
)
|
||||
}
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
it('returns true if user has a valid sso session', async () => {
|
||||
const userId = cryptoRandomString({ length: 9 })
|
||||
const providerId = cryptoRandomString({ length: 9 })
|
||||
const workspaceId = cryptoRandomString({ length: 9 })
|
||||
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() + 1)
|
||||
|
||||
const result = await requireValidWorkspaceSsoSession({
|
||||
loaders: {
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
userId,
|
||||
providerId,
|
||||
validUntil
|
||||
})
|
||||
)
|
||||
}
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,20 +0,0 @@
|
||||
import { AuthCheckContext, AuthCheckContextLoaderKeys } from '../domain/loaders.js'
|
||||
|
||||
export const requireValidWorkspaceSsoSession =
|
||||
({
|
||||
loaders
|
||||
}: AuthCheckContext<typeof AuthCheckContextLoaderKeys.getWorkspaceSsoSession>) =>
|
||||
async (args: { userId: string; workspaceId: string }): Promise<boolean> => {
|
||||
const { userId, workspaceId } = args
|
||||
|
||||
const workspaceSsoSession = await loaders.getWorkspaceSsoSession({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!workspaceSsoSession.isOk) return false
|
||||
|
||||
const isExpiredSession =
|
||||
new Date().getTime() > workspaceSsoSession.value.validUntil.getTime()
|
||||
|
||||
return !isExpiredSession
|
||||
}
|
||||
@@ -60,6 +60,16 @@ export const WorkspaceRoleNotFoundError = defineAuthError({
|
||||
message: 'The user does not have a role in the workspace'
|
||||
})
|
||||
|
||||
export const ServerNoAccessError = defineAuthError({
|
||||
code: 'ServerNoAccess',
|
||||
message: 'You do not have access to this server'
|
||||
})
|
||||
|
||||
export const ServerNoSessionError = defineAuthError({
|
||||
code: 'ServerNoSession',
|
||||
message: 'You are not logged in to this server'
|
||||
})
|
||||
|
||||
export const ServerRoleNotFoundError = defineAuthError({
|
||||
code: 'ServerRoleNotFound',
|
||||
message: 'Could not resolve your server role'
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
export class LogicError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectNotFoundError extends Error {
|
||||
constructor({ projectId }: { projectId: string }) {
|
||||
super(`Project with id ${projectId} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidRoleError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
|
||||
@@ -36,6 +36,8 @@ type AuthContextLoaderMappingDefinition<
|
||||
/**
|
||||
* All loaders must be listed here for app startup validation to work properly
|
||||
*/
|
||||
|
||||
/* v8 ignore start */
|
||||
export const AuthCheckContextLoaderKeys = <const>{
|
||||
getEnv: 'getEnv',
|
||||
getProject: 'getProject',
|
||||
@@ -46,6 +48,7 @@ export const AuthCheckContextLoaderKeys = <const>{
|
||||
getWorkspaceSsoProvider: 'getWorkspaceSsoProvider',
|
||||
getWorkspaceSsoSession: 'getWorkspaceSsoSession'
|
||||
}
|
||||
/* v8 ignore end */
|
||||
|
||||
export type AuthCheckContextLoaderKeys =
|
||||
(typeof AuthCheckContextLoaderKeys)[keyof typeof AuthCheckContextLoaderKeys]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isMinimumProjectRole,
|
||||
isMinimumWorkspaceRole,
|
||||
isMinimumServerRole
|
||||
} from './roles.js'
|
||||
import { InvalidRoleError } from '../errors.js'
|
||||
|
||||
describe('authz domain roles', () => {
|
||||
describe('isMinimumProjectRole', () => {
|
||||
it('returns true if role has bigger weight than target role', () => {
|
||||
expect(isMinimumProjectRole('stream:owner', 'stream:contributor')).toBe(true)
|
||||
})
|
||||
it('returns true if role has the same weight as the target role', () => {
|
||||
expect(isMinimumProjectRole('stream:contributor', 'stream:contributor')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
it('returns false if role has smaller weight than target role', () => {
|
||||
expect(isMinimumProjectRole('stream:reviewer', 'stream:contributor')).toBe(false)
|
||||
})
|
||||
it('throws an error if the target role is invalid', () => {
|
||||
//@ts-expect-error im testing invalid target role here
|
||||
expect(() => isMinimumProjectRole('stream:owner', 'invalid')).toThrow(
|
||||
InvalidRoleError
|
||||
)
|
||||
})
|
||||
it('throws an error if the role is invalid', () => {
|
||||
//@ts-expect-error im testing invalid role here
|
||||
expect(() => isMinimumProjectRole('invalid', 'stream:contributor')).toThrow(
|
||||
InvalidRoleError
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('isMinimumWorkspaceRole', () => {
|
||||
it('returns true if role has bigger weight than target role', () => {
|
||||
expect(isMinimumWorkspaceRole('workspace:admin', 'workspace:member')).toBe(true)
|
||||
})
|
||||
it('returns true if role has the same weight as the target role', () => {
|
||||
expect(isMinimumWorkspaceRole('workspace:admin', 'workspace:admin')).toBe(true)
|
||||
})
|
||||
it('returns false if role has smaller weight than target role', () => {
|
||||
expect(isMinimumWorkspaceRole('workspace:guest', 'workspace:admin')).toBe(false)
|
||||
})
|
||||
it('throws an error if target role is invalid', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error im testing an invalid target role
|
||||
isMinimumWorkspaceRole('workspace:admin', 'workspace:invalid')
|
||||
).toThrow(InvalidRoleError)
|
||||
})
|
||||
it('throws an error if role is invalid', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error im testing an invalid role
|
||||
isMinimumWorkspaceRole('workspace:invalid', 'workspace:admin')
|
||||
).toThrow(InvalidRoleError)
|
||||
})
|
||||
})
|
||||
describe('isMinimumServerRole', () => {
|
||||
it('returns true if role has bigger weight than target role', () => {
|
||||
expect(isMinimumServerRole('server:admin', 'server:user')).toBe(true)
|
||||
})
|
||||
it('returns true if role has the same weight as the target role', () => {
|
||||
expect(isMinimumServerRole('server:admin', 'server:admin')).toBe(true)
|
||||
})
|
||||
it('returns false if role has smaller weight than target role', () => {
|
||||
expect(isMinimumServerRole('server:guest', 'server:admin')).toBe(false)
|
||||
})
|
||||
it('throws an error if target role is invalid', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error im testing an invalid target role
|
||||
isMinimumServerRole('server:admin', 'server:invalid')
|
||||
).toThrow(InvalidRoleError)
|
||||
})
|
||||
it('throws an error if role is invalid', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error im testing an invalid role
|
||||
isMinimumServerRole('server:invalid', 'server:admin')
|
||||
).toThrow(InvalidRoleError)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
StreamRoles,
|
||||
ServerRoles,
|
||||
RoleInfo,
|
||||
WorkspaceRoles
|
||||
} from '../../../core/constants.js'
|
||||
import { InvalidRoleError } from '../errors.js'
|
||||
|
||||
export const isMinimumProjectRole = (
|
||||
role: StreamRoles,
|
||||
targetRole: StreamRoles
|
||||
): boolean => {
|
||||
if (!(role in RoleInfo.Stream)) {
|
||||
throw new InvalidRoleError(`Invalid role ${role}`)
|
||||
}
|
||||
if (!(targetRole in RoleInfo.Stream)) {
|
||||
throw new InvalidRoleError(`Invalid target role ${targetRole}`)
|
||||
}
|
||||
const roleWeight = RoleInfo.Stream[role].weight
|
||||
const targetRoleWeight = RoleInfo.Stream[targetRole].weight
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
|
||||
export const isMinimumServerRole = (
|
||||
role: ServerRoles,
|
||||
targetRole: ServerRoles
|
||||
): boolean => {
|
||||
if (!(role in RoleInfo.Server)) {
|
||||
throw new InvalidRoleError(`Invalid role ${role}`)
|
||||
}
|
||||
if (!(targetRole in RoleInfo.Server)) {
|
||||
throw new InvalidRoleError(`Invalid target role ${targetRole}`)
|
||||
}
|
||||
const roleWeight = RoleInfo.Server[role].weight
|
||||
const targetRoleWeight = RoleInfo.Server[targetRole].weight
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
|
||||
export const isMinimumWorkspaceRole = (
|
||||
role: WorkspaceRoles,
|
||||
targetRole: WorkspaceRoles
|
||||
): boolean => {
|
||||
if (!(role in RoleInfo.Workspace)) {
|
||||
throw new InvalidRoleError(`Invalid role ${role}`)
|
||||
}
|
||||
if (!(targetRole in RoleInfo.Workspace)) {
|
||||
throw new InvalidRoleError(`Invalid target role ${targetRole}`)
|
||||
}
|
||||
const roleWeight = RoleInfo.Workspace[role].weight
|
||||
const targetRoleWeight = RoleInfo.Workspace[targetRole].weight
|
||||
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
@@ -1,15 +1,34 @@
|
||||
import Result from 'true-myth/result'
|
||||
import Unit from 'true-myth/unit'
|
||||
import { AuthError } from './authErrors.js'
|
||||
import { AuthCheckContextLoaderKeys, AuthCheckContextLoaders } from './loaders.js'
|
||||
import Maybe from 'true-myth/maybe'
|
||||
|
||||
export type ProjectContext = { projectId: string }
|
||||
export type UserContext = { userId: string }
|
||||
export type MaybeUserContext = { userId?: string }
|
||||
|
||||
export type UserContext = { userId?: string }
|
||||
|
||||
export type AuthPolicyFactory<
|
||||
// a complete policy always returns a full result
|
||||
export type AuthPolicy<
|
||||
LoaderKeys extends AuthCheckContextLoaderKeys,
|
||||
Args extends object,
|
||||
ExpectedAuthErrors extends AuthError
|
||||
> = (
|
||||
loaders: AuthCheckContextLoaders<LoaderKeys>
|
||||
) => (args: Args) => Promise<Result<true, ExpectedAuthErrors>>
|
||||
) => (args: Args) => Promise<Result<Unit, ExpectedAuthErrors>>
|
||||
|
||||
// a policy fragment is a partial policy, where it can potentially make a decision
|
||||
// but maybe it cannot
|
||||
export type AuthPolicyFragment<
|
||||
LoaderKeys extends AuthCheckContextLoaderKeys,
|
||||
Args extends object,
|
||||
ExpectedAuthErrors extends AuthError
|
||||
> = (
|
||||
loaders: AuthCheckContextLoaders<LoaderKeys>
|
||||
) => (args: Args) => Promise<Maybe<Result<Unit, ExpectedAuthErrors>>>
|
||||
// ) => (args: Args) => Promise<Maybe<Result<Unit, ExpectedAuthErrors>>>
|
||||
|
||||
export type AuthPolicyCheck<
|
||||
LoaderKeys extends AuthCheckContextLoaderKeys,
|
||||
Args extends object
|
||||
> = (loaders: AuthCheckContextLoaders<LoaderKeys>) => (args: Args) => Promise<boolean>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isMinimumProjectRole } from './logic.js'
|
||||
import { InvalidRoleError } from '../errors.js'
|
||||
|
||||
describe('project logic', () => {
|
||||
describe('isMinimumProjectRole', () => {
|
||||
it('returns true if role has bigger weight than target role', () => {
|
||||
expect(isMinimumProjectRole('stream:owner', 'stream:contributor')).toBe(true)
|
||||
})
|
||||
it('returns true if role has the same weight as the target role', () => {
|
||||
expect(isMinimumProjectRole('stream:contributor', 'stream:contributor')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
it('returns false if role has smaller weight than target role', () => {
|
||||
expect(isMinimumProjectRole('stream:reviewer', 'stream:contributor')).toBe(false)
|
||||
})
|
||||
it('throws an error if the target role is invalid', () => {
|
||||
//@ts-expect-error im testing invalid target role here
|
||||
expect(() => isMinimumProjectRole('stream:owner', 'invalid')).toThrow(
|
||||
InvalidRoleError
|
||||
)
|
||||
})
|
||||
it('throws an error if the role is invalid', () => {
|
||||
//@ts-expect-error im testing invalid role here
|
||||
expect(() => isMinimumProjectRole('invalid', 'stream:contributor')).toThrow(
|
||||
InvalidRoleError
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { StreamRoles, RoleInfo } from '../../../core/constants.js'
|
||||
import { InvalidRoleError } from '../errors.js'
|
||||
|
||||
export const isMinimumProjectRole = (
|
||||
role: StreamRoles,
|
||||
targetRole: StreamRoles
|
||||
): boolean => {
|
||||
if (!(role in RoleInfo.Stream)) {
|
||||
throw new InvalidRoleError(`Invalid role ${role}`)
|
||||
}
|
||||
if (!(targetRole in RoleInfo.Stream)) {
|
||||
throw new InvalidRoleError(`Invalid target role ${targetRole}`)
|
||||
}
|
||||
const roleWeight = RoleInfo.Stream[role].weight
|
||||
const targetRoleWeight = RoleInfo.Stream[targetRole].weight
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
@@ -4,13 +4,19 @@ import { Project } from './types.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ProjectRoleNotFoundError
|
||||
ProjectRoleNotFoundError,
|
||||
WorkspaceSsoSessionInvalidError
|
||||
} from '../authErrors.js'
|
||||
|
||||
export type GetProject = (args: {
|
||||
projectId: string
|
||||
}) => Promise<
|
||||
Result<Project, typeof ProjectNotFoundError | typeof ProjectNoAccessError>
|
||||
Result<
|
||||
Project,
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceSsoSessionInvalidError
|
||||
>
|
||||
>
|
||||
|
||||
export type GetProjectRole = (args: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export type Project = {
|
||||
// TODO: Deprecated field?
|
||||
isDiscoverable: boolean
|
||||
isPublic: boolean
|
||||
workspaceId: string | null
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isMinimumWorkspaceRole } from './logic.js'
|
||||
import { InvalidRoleError } from '../errors.js'
|
||||
|
||||
describe('project logic', () => {
|
||||
describe('isMinimumProjectRole', () => {
|
||||
it('returns true if role has bigger weight than target role', () => {
|
||||
expect(isMinimumWorkspaceRole('workspace:admin', 'workspace:member')).toBe(true)
|
||||
})
|
||||
it('returns true if role has the same weight as the target role', () => {
|
||||
expect(isMinimumWorkspaceRole('workspace:admin', 'workspace:admin')).toBe(true)
|
||||
})
|
||||
it('returns false if role has smaller weight than target role', () => {
|
||||
expect(isMinimumWorkspaceRole('workspace:guest', 'workspace:admin')).toBe(false)
|
||||
})
|
||||
it('throws an error if target role is invalid', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error im testing an invalid target role
|
||||
isMinimumWorkspaceRole('workspace:admin', 'workspace:invalid')
|
||||
).toThrow(InvalidRoleError)
|
||||
})
|
||||
it('throws an error if role is invalid', () => {
|
||||
expect(() =>
|
||||
// @ts-expect-error im testing an invalid role
|
||||
isMinimumWorkspaceRole('workspace:invalid', 'workspace:admin')
|
||||
).toThrow(InvalidRoleError)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RoleInfo, WorkspaceRoles } from '../../../core/constants.js'
|
||||
import { InvalidRoleError } from '../errors.js'
|
||||
|
||||
export const isMinimumWorkspaceRole = (
|
||||
role: WorkspaceRoles,
|
||||
targetRole: WorkspaceRoles
|
||||
): boolean => {
|
||||
if (!(role in RoleInfo.Workspace)) {
|
||||
throw new InvalidRoleError(`Invalid role ${role}`)
|
||||
}
|
||||
if (!(targetRole in RoleInfo.Workspace)) {
|
||||
throw new InvalidRoleError(`Invalid target role ${targetRole}`)
|
||||
}
|
||||
const roleWeight = RoleInfo.Workspace[role].weight
|
||||
const targetRoleWeight = RoleInfo.Workspace[targetRole].weight
|
||||
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
@@ -27,4 +27,4 @@ export type GetWorkspaceSsoSession = (args: {
|
||||
workspaceId: string
|
||||
}) => Promise<Result<WorkspaceSsoSession, typeof WorkspaceSsoSessionNotFoundError>>
|
||||
|
||||
export type GetEnv = () => Result<FeatureFlags, never>
|
||||
export type GetEnv = () => Promise<FeatureFlags>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from './workspaceSso.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import {
|
||||
ProjectNotFoundError,
|
||||
WorkspaceRoleNotFoundError,
|
||||
WorkspaceSsoProviderNotFoundError,
|
||||
WorkspaceSsoSessionInvalidError,
|
||||
WorkspaceSsoSessionNotFoundError
|
||||
} from '../domain/authErrors.js'
|
||||
import { just, nothing } from 'true-myth/maybe'
|
||||
|
||||
describe('maybeMemberRoleWithValidSsoSessionIfNeeded returns a function, that', () => {
|
||||
it('returns nothing if user does not have a workspace role', async () => {
|
||||
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => err(WorkspaceRoleNotFoundError),
|
||||
getWorkspaceSsoProvider: async () => err(WorkspaceSsoProviderNotFoundError),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(result).toStrictEqual(nothing())
|
||||
})
|
||||
it('returns nothing if user does not have a minimum workspace:member role', async () => {
|
||||
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:guest'),
|
||||
getWorkspaceSsoProvider: async () => err(WorkspaceSsoProviderNotFoundError),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(result).toStrictEqual(nothing())
|
||||
})
|
||||
it('returns just(ok()) if user is a member and workspace has no SSO provider', async () => {
|
||||
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoProvider: async () => err(WorkspaceSsoProviderNotFoundError),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(result).toStrictEqual(just(ok()))
|
||||
})
|
||||
it('throws uncovered error for unexpected ssoProvider loader errors', async () => {
|
||||
const result = maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
// @ts-expect-error testing uncovered errors
|
||||
getWorkspaceSsoProvider: async () => err(ProjectNotFoundError),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
await expect(result).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it('throws uncovered error for unexpected ssoSession loader errors', async () => {
|
||||
const result = maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoProvider: async () =>
|
||||
ok({ providerId: cryptoRandomString({ length: 10 }) }),
|
||||
// @ts-expect-error testing uncovered errors
|
||||
getWorkspaceSsoSession: async () => err(ProjectNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
await expect(result).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
it('returns WorkspaceSsoSessionInvalidError if user does not have an SSO session', async () => {
|
||||
const result = maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoProvider: async () =>
|
||||
ok({ providerId: cryptoRandomString({ length: 10 }) }),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError)
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
await expect(result).resolves.toStrictEqual(
|
||||
just(err(WorkspaceSsoSessionInvalidError))
|
||||
)
|
||||
})
|
||||
it('returns WorkspaceSsoSessionInvalidError if user has an expired sso session', async () => {
|
||||
const userId = cryptoRandomString({ length: 10 })
|
||||
const providerId = cryptoRandomString({ length: 10 })
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() - 1)
|
||||
|
||||
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoProvider: async () =>
|
||||
ok({ providerId: cryptoRandomString({ length: 10 }) }),
|
||||
getWorkspaceSsoSession: async () => ok({ providerId, validUntil, userId })
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
expect(result).toStrictEqual(just(err(WorkspaceSsoSessionInvalidError)))
|
||||
})
|
||||
it('returns true if user has a valid sso session', async () => {
|
||||
const userId = cryptoRandomString({ length: 10 })
|
||||
const providerId = cryptoRandomString({ length: 10 })
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
|
||||
const validUntil = new Date()
|
||||
validUntil.setDate(validUntil.getDate() + 100)
|
||||
|
||||
const result = await maybeMemberRoleWithValidSsoSessionIfNeeded({
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoProvider: async () =>
|
||||
ok({ providerId: cryptoRandomString({ length: 10 }) }),
|
||||
getWorkspaceSsoSession: async () => ok({ providerId, validUntil, userId })
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
expect(result).toStrictEqual(just(ok()))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { err, isErr, ok } from 'true-myth/result'
|
||||
import { throwUncoveredError } from '../../core/helpers/error.js'
|
||||
import { WorkspaceSsoSessionInvalidError } from '../domain/authErrors.js'
|
||||
import { AuthPolicyFragment } from '../domain/policies.js'
|
||||
import { requireMinimumWorkspaceRole } from '../checks/workspaceRole.js'
|
||||
import { just, nothing } from 'true-myth/maybe'
|
||||
|
||||
export const maybeMemberRoleWithValidSsoSessionIfNeeded: AuthPolicyFragment<
|
||||
'getWorkspaceRole' | 'getWorkspaceSsoProvider' | 'getWorkspaceSsoSession',
|
||||
{ userId: string; workspaceId: string },
|
||||
typeof WorkspaceSsoSessionInvalidError
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const hasMinimumMemberRole = await requireMinimumWorkspaceRole(loaders)({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: 'workspace:member'
|
||||
})
|
||||
|
||||
if (!hasMinimumMemberRole) return nothing()
|
||||
|
||||
const workspaceSsoProvider = await loaders.getWorkspaceSsoProvider({
|
||||
workspaceId
|
||||
})
|
||||
if (isErr(workspaceSsoProvider)) {
|
||||
switch (workspaceSsoProvider.error.code) {
|
||||
case 'WorkspaceSsoProviderNotFound':
|
||||
// if there is no SSO provider, we can early return here
|
||||
return just(ok())
|
||||
default:
|
||||
throwUncoveredError(workspaceSsoProvider.error.code)
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceSsoSession = await loaders.getWorkspaceSsoSession({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (isErr(workspaceSsoSession)) {
|
||||
switch (workspaceSsoSession.error.code) {
|
||||
case 'WorkspaceSsoSessionNotFound':
|
||||
return just(err(WorkspaceSsoSessionInvalidError))
|
||||
default:
|
||||
throwUncoveredError(workspaceSsoSession.error.code)
|
||||
}
|
||||
}
|
||||
|
||||
const isExpiredSession =
|
||||
new Date().getTime() > workspaceSsoSession.value.validUntil.getTime()
|
||||
|
||||
if (isExpiredSession) return just(err(WorkspaceSsoSessionInvalidError))
|
||||
return just(ok())
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, assert } from 'vitest'
|
||||
import { canQueryProjectPolicyFactory } from './canQueryProject.js'
|
||||
import { canQueryProjectPolicy } from './canQueryProject.js'
|
||||
import { parseFeatureFlags } from '../../environment/index.js'
|
||||
import crs from 'crypto-random-string'
|
||||
import { Roles } from '../../core/constants.js'
|
||||
@@ -7,12 +7,18 @@ import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ProjectRoleNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerRoleNotFoundError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceRoleNotFoundError,
|
||||
WorkspaceSsoProviderNotFoundError,
|
||||
WorkspaceSsoSessionInvalidError,
|
||||
WorkspaceSsoSessionNotFoundError
|
||||
} from '../domain/authErrors.js'
|
||||
import { getProjectFake } from '../../tests/fakes.js'
|
||||
import { err, ok } from 'true-myth/result'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
const canQueryProjectArgs = () => {
|
||||
const projectId = crs({ length: 10 })
|
||||
@@ -20,12 +26,16 @@ const canQueryProjectArgs = () => {
|
||||
return { projectId, userId }
|
||||
}
|
||||
|
||||
describe('canQueryProjectPolicyFactory creates a function, that handles ', () => {
|
||||
describe('project not found', () => {
|
||||
it('by returning project no access', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: async () => ok(parseFeatureFlags({})),
|
||||
getProject: () => Promise.resolve(err(ProjectNotFoundError)),
|
||||
describe('canQueryProjectPolicy creates a function, that handles ', () => {
|
||||
describe('project loader errors', () => {
|
||||
it.each([
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
WorkspaceSsoSessionInvalidError
|
||||
])('expected $code error by returning the error', async (expectedError) => {
|
||||
const result = await canQueryProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: async () => err(expectedError),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
@@ -41,19 +51,39 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
})(canQueryProjectArgs())
|
||||
|
||||
expect(canQuery.isOk).toBe(false)
|
||||
if (!canQuery.isOk) {
|
||||
expect(canQuery.error.code).toBe(ProjectNotFoundError.code)
|
||||
}
|
||||
expect(result).toStrictEqual(err(expectedError))
|
||||
})
|
||||
it('unexpected error by throwing UncoveredError', async () => {
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
// @ts-expect-error testing uncovered error handling
|
||||
getProject: async () => err(ProjectRoleNotFoundError),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})(canQueryProjectArgs())
|
||||
|
||||
await expect(result).rejects.toThrowError(/Uncovered error/)
|
||||
})
|
||||
})
|
||||
describe('project visibility', () => {
|
||||
it('allows anyone on a public project', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: async () => ok(parseFeatureFlags({})),
|
||||
const canQueryProject = canQueryProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({ isPublic: true }),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
@@ -75,8 +105,8 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
})
|
||||
it('allows anyone on a linkShareable project', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: async () => ok(parseFeatureFlags({})),
|
||||
const canQueryProject = canQueryProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({ isDiscoverable: true }),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
@@ -99,22 +129,76 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
})
|
||||
})
|
||||
|
||||
describe('server roles', () => {
|
||||
it('allows access for archived server users with a project role on a public project', async () => {
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: true }),
|
||||
getProjectRole: async () => ok(Roles.Stream.Owner),
|
||||
getServerRole: async () => ok(Roles.Server.ArchivedUser),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
it('does not allow access for archived server users with a project role', async () => {
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getProjectRole: async () => ok(Roles.Stream.Owner),
|
||||
getServerRole: async () => ok(Roles.Server.ArchivedUser),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(err(ServerNoAccessError))
|
||||
})
|
||||
it('does not allow access for non public projects for unknown users', async () => {
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getProjectRole: async () => ok(Roles.Stream.Owner),
|
||||
getServerRole: async () => err(ServerRoleNotFoundError),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})({ userId: undefined, projectId: cryptoRandomString({ length: 10 }) })
|
||||
await expect(result).resolves.toStrictEqual(err(ServerNoSessionError))
|
||||
})
|
||||
})
|
||||
|
||||
describe('project roles', () => {
|
||||
it.each(Object.values(Roles.Stream))(
|
||||
'allows access to private projects with role %',
|
||||
'allows access for active server users to private projects with %s role',
|
||||
async (role) => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const canQueryProject = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getProjectRole: () => Promise.resolve(ok(role)),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getProjectRole: async () => ok(role),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
@@ -130,18 +214,12 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
}
|
||||
)
|
||||
it('does not allow access to private projects without a project role', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getProjectRole: () => Promise.resolve(err(ProjectRoleNotFoundError)),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getProjectRole: async () => err(ProjectRoleNotFoundError),
|
||||
getServerRole: async () => ok(Roles.Server.Admin),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
@@ -151,21 +229,16 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(false)
|
||||
if (!canQuery.isOk) {
|
||||
expect(canQuery.error.code).toBe(ProjectNoAccessError.code)
|
||||
}
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(err(ProjectNoAccessError))
|
||||
})
|
||||
})
|
||||
describe('admin override', () => {
|
||||
it('allows server admins without project roles on private projects if admin override is enabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: async () =>
|
||||
ok(parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' })),
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getServerRole: () => Promise.resolve(ok(Roles.Server.Admin)),
|
||||
getServerRole: async () => ok(Roles.Server.Admin),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
@@ -178,25 +251,20 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
|
||||
it('does not allow server admins without project roles on private projects if admin override is disabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_ADMIN_OVERRIDE_ENABLED: 'false',
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_ADMIN_OVERRIDE_ENABLED: 'false',
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
}),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getServerRole: () => Promise.resolve(ok(Roles.Server.Admin)),
|
||||
getProjectRole: () => {
|
||||
return Promise.resolve(err(ProjectRoleNotFoundError))
|
||||
},
|
||||
getServerRole: async () => ok(Roles.Server.Admin),
|
||||
getProjectRole: async () => err(ProjectRoleNotFoundError),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
@@ -206,28 +274,22 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(false)
|
||||
if (!canQuery.isOk) {
|
||||
expect(canQuery.error.code).toBe(ProjectNoAccessError.code)
|
||||
}
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(err(ProjectNoAccessError))
|
||||
})
|
||||
})
|
||||
describe('the workspace world', () => {
|
||||
it('does not check workspace rules if the workspaces module is not enabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' })),
|
||||
parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
@@ -237,200 +299,184 @@ describe('canQueryProjectPolicyFactory creates a function, that handles ', () =>
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
it('does not allow project access without a workspace role', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(err(WorkspaceRoleNotFoundError)),
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => err(WorkspaceRoleNotFoundError),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(false)
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(err(WorkspaceNoAccessError))
|
||||
})
|
||||
it('allows project access via workspace role if user does not have project role', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(err(ProjectRoleNotFoundError)),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:admin')),
|
||||
getProjectRole: async () => err(ProjectRoleNotFoundError),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:admin'),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () =>
|
||||
Promise.resolve(err(WorkspaceSsoProviderNotFoundError))
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
getWorkspaceSsoProvider: async () => err(WorkspaceSsoProviderNotFoundError)
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
it('does not check SSO sessions if user is workspace guest', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:guest')),
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:guest'),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
it('does not check SSO sessions if workspace does not have it enabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member')),
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () =>
|
||||
Promise.resolve(err(WorkspaceSsoProviderNotFoundError))
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
getWorkspaceSsoProvider: async () => err(WorkspaceSsoProviderNotFoundError)
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
it('does not allow project access if SSO session is missing', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const canQueryProject = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member')),
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve(err(WorkspaceSsoSessionNotFoundError)),
|
||||
getWorkspaceSsoProvider: () => Promise.resolve(ok({ providerId: 'foo' }))
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError),
|
||||
getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' })
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(false)
|
||||
})
|
||||
it('does not allow project access if SSO session is expired or invalid', async () => {
|
||||
it('does not allow project access if SSO session is not found', async () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 1)
|
||||
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member')),
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve(ok({ validUntil: date, userId: 'foo', providerId: 'foo' })),
|
||||
getWorkspaceSsoProvider: () => Promise.resolve(ok({ providerId: 'foo' }))
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(false)
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoSession: async () => err(WorkspaceSsoSessionNotFoundError),
|
||||
getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' })
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(err(WorkspaceSsoSessionInvalidError))
|
||||
})
|
||||
it('does not allow project access if SSO session is expired', async () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 1)
|
||||
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoSession: async () =>
|
||||
ok({ validUntil: date, userId: 'foo', providerId: 'foo' }),
|
||||
getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' })
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(err(WorkspaceSsoSessionInvalidError))
|
||||
})
|
||||
it('allows project access if SSO session is valid', async () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 1)
|
||||
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
const result = canQueryProjectPolicy({
|
||||
getEnv: async () =>
|
||||
ok(
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
})
|
||||
),
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(ok('stream:contributor')),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(ok('workspace:member')),
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve(ok({ validUntil: date, userId: 'foo', providerId: 'foo' })),
|
||||
getWorkspaceSsoProvider: () => Promise.resolve(ok({ providerId: 'foo' }))
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.isOk).toBe(true)
|
||||
getProjectRole: async () => ok('stream:contributor'),
|
||||
getServerRole: async () => ok('server:user'),
|
||||
getWorkspaceRole: async () => ok('workspace:member'),
|
||||
getWorkspaceSsoSession: async () =>
|
||||
ok({ validUntil: date, userId: 'foo', providerId: 'foo' }),
|
||||
getWorkspaceSsoProvider: async () => ok({ providerId: 'foo' })
|
||||
})(canQueryProjectArgs())
|
||||
await expect(result).resolves.toStrictEqual(ok())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import {
|
||||
requireAnyWorkspaceRole,
|
||||
requireMinimumWorkspaceRole
|
||||
} from '../checks/workspaceRole.js'
|
||||
import {
|
||||
requireExactProjectVisibilityFactory,
|
||||
requireMinimumProjectRoleFactory
|
||||
} from '../checks/projects.js'
|
||||
import { AuthPolicyFactory, ProjectContext, UserContext } from '../domain/policies.js'
|
||||
import { requireExactServerRole } from '../checks/serverRole.js'
|
||||
import { requireValidWorkspaceSsoSession } from '../checks/workspaceSso.js'
|
||||
import { isJust } from 'true-myth/maybe'
|
||||
import { err, isErr, isOk, ok } from 'true-myth/result'
|
||||
import { Roles } from '../../core/constants.js'
|
||||
import { hasMinimumProjectRole, isPubliclyReadableProject } from '../checks/projects.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionInvalidError
|
||||
} from '../domain/authErrors.js'
|
||||
import { err, isOk, ok } from 'true-myth/result'
|
||||
import { AuthCheckContextLoaderKeys } from '../domain/loaders.js'
|
||||
import { LogicError } from '../domain/errors.js'
|
||||
import { AuthPolicy, MaybeUserContext, ProjectContext } from '../domain/policies.js'
|
||||
import { canUseAdminOverride, hasMinimumServerRole } from '../checks/serverRole.js'
|
||||
import { hasAnyWorkspaceRole } from '../checks/workspaceRole.js'
|
||||
import { maybeMemberRoleWithValidSsoSessionIfNeeded } from '../fragments/workspaceSso.js'
|
||||
import { throwUncoveredError } from '../../core/index.js'
|
||||
|
||||
export const canQueryProjectPolicyFactory: AuthPolicyFactory<
|
||||
export const canQueryProjectPolicy: AuthPolicy<
|
||||
| typeof AuthCheckContextLoaderKeys.getEnv
|
||||
| typeof AuthCheckContextLoaderKeys.getProject
|
||||
| typeof AuthCheckContextLoaderKeys.getProjectRole
|
||||
@@ -28,7 +25,9 @@ export const canQueryProjectPolicyFactory: AuthPolicyFactory<
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceRole
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoProvider
|
||||
| typeof AuthCheckContextLoaderKeys.getWorkspaceSsoSession,
|
||||
UserContext & ProjectContext,
|
||||
MaybeUserContext & ProjectContext,
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
@@ -37,109 +36,70 @@ export const canQueryProjectPolicyFactory: AuthPolicyFactory<
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
const env = await loaders.getEnv()
|
||||
if (!isOk(env)) {
|
||||
throw new LogicError('Failed to load environment variables')
|
||||
}
|
||||
|
||||
const { FF_ADMIN_OVERRIDE_ENABLED, FF_WORKSPACES_MODULE_ENABLED } = env.value
|
||||
|
||||
// we prerolad the project and early return any loading errors
|
||||
// this is a short circuit in the frontend, to surface any project load errors
|
||||
// from the backend.
|
||||
// make sure to expose all of the error types in the loader type,
|
||||
// that we care about in this early return
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!isOk(project)) {
|
||||
return err(project.error)
|
||||
// if (isErr(project)) return err(project.error)
|
||||
if (isErr(project)) {
|
||||
switch (project.error.code) {
|
||||
case 'ProjectNoAccess':
|
||||
case 'ProjectNotFound':
|
||||
case 'WorkspaceSsoSessionInvalid':
|
||||
return err(project.error)
|
||||
default:
|
||||
throwUncoveredError(project.error)
|
||||
}
|
||||
}
|
||||
|
||||
// All users may read public projects
|
||||
const isPublicResult = await requireExactProjectVisibilityFactory({ loaders })({
|
||||
projectId,
|
||||
projectVisibility: 'public'
|
||||
})
|
||||
if (isPublicResult) {
|
||||
return ok(true)
|
||||
}
|
||||
if (await isPubliclyReadableProject(loaders)({ projectId })) return ok()
|
||||
|
||||
// All users may read link-shareable projects
|
||||
const isLinkShareableResult = await requireExactProjectVisibilityFactory({
|
||||
loaders
|
||||
})({
|
||||
projectId,
|
||||
projectVisibility: 'linkShareable'
|
||||
// From this point on, you cannot pass as an unknown user, need to log in
|
||||
if (!userId) return err(ServerNoSessionError)
|
||||
const isActiveServerUser = await hasMinimumServerRole(loaders)({
|
||||
userId,
|
||||
role: Roles.Server.Guest
|
||||
})
|
||||
if (isLinkShareableResult) {
|
||||
return ok(true)
|
||||
}
|
||||
// From this point on, you cannot pass as an unknown user
|
||||
if (!userId) {
|
||||
return err(ProjectNoAccessError)
|
||||
}
|
||||
if (!isActiveServerUser) return err(ServerNoAccessError)
|
||||
|
||||
// When G O D M O D E is enabled
|
||||
if (FF_ADMIN_OVERRIDE_ENABLED) {
|
||||
// Server admins may read all project data
|
||||
const isServerAdminResult = await requireExactServerRole({ loaders })({
|
||||
userId,
|
||||
role: Roles.Server.Admin
|
||||
})
|
||||
if (isServerAdminResult) {
|
||||
return ok(true)
|
||||
}
|
||||
}
|
||||
if (await canUseAdminOverride(loaders)({ userId })) return ok()
|
||||
|
||||
const { workspaceId } = project.value
|
||||
|
||||
// When a project belongs to a workspace
|
||||
if (FF_WORKSPACES_MODULE_ENABLED && !!workspaceId) {
|
||||
if (env.FF_WORKSPACES_MODULE_ENABLED && !!workspaceId) {
|
||||
// User must have a workspace role to read project data
|
||||
const hasWorkspaceRoleResult = await requireAnyWorkspaceRole({ loaders })({
|
||||
if (!(await hasAnyWorkspaceRole(loaders)({ userId, workspaceId })))
|
||||
return err(WorkspaceNoAccessError)
|
||||
|
||||
const memberWithSsoSession = await maybeMemberRoleWithValidSsoSessionIfNeeded(
|
||||
loaders
|
||||
)({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!hasWorkspaceRoleResult) {
|
||||
// Should we hide the fact, the project is in a workspace?
|
||||
return err(WorkspaceNoAccessError)
|
||||
}
|
||||
|
||||
const hasMinimumMemberRole = await requireMinimumWorkspaceRole({
|
||||
loaders
|
||||
})({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: 'workspace:member'
|
||||
})
|
||||
|
||||
if (hasMinimumMemberRole) {
|
||||
const workspaceSsoProvider = await loaders.getWorkspaceSsoProvider({
|
||||
workspaceId
|
||||
})
|
||||
if (workspaceSsoProvider.isOk) {
|
||||
// Member and admin user must have a valid SSO session to read project data
|
||||
const hasValidSsoSessionResult = await requireValidWorkspaceSsoSession({
|
||||
loaders
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!hasValidSsoSessionResult) {
|
||||
return err(WorkspaceSsoSessionInvalidError)
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace members get to go through without an explicit project role
|
||||
return ok(true)
|
||||
if (isJust(memberWithSsoSession)) {
|
||||
// if a member, make sure it has a valid sso session
|
||||
return isOk(memberWithSsoSession.value)
|
||||
? ok()
|
||||
: err(memberWithSsoSession.value.error)
|
||||
} else {
|
||||
// just fall through to the generic project role check for workspace:guest-s
|
||||
// they do not need an sso session
|
||||
}
|
||||
}
|
||||
|
||||
// User must have at least stream reviewer role to read project data
|
||||
const hasMinimumProjectRoleResult = await requireMinimumProjectRoleFactory({
|
||||
loaders
|
||||
})({
|
||||
// User must have at least stream:reviewer role to read project data
|
||||
return (await hasMinimumProjectRole(loaders)({
|
||||
userId,
|
||||
projectId,
|
||||
role: 'stream:reviewer'
|
||||
})
|
||||
if (hasMinimumProjectRoleResult) {
|
||||
return ok(true)
|
||||
}
|
||||
return err(ProjectNoAccessError)
|
||||
}))
|
||||
? ok()
|
||||
: err(ProjectNoAccessError)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AllAuthCheckContextLoaders } from '../domain/loaders.js'
|
||||
import { canQueryProjectPolicyFactory } from './canQueryProject.js'
|
||||
import { canQueryProjectPolicy } from './canQueryProject.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AllAuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
canQuery: canQueryProjectPolicyFactory(loaders)
|
||||
canQuery: canQueryProjectPolicy(loaders)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user