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:
Gergő Jedlicska
2025-03-27 11:30:54 +01:00
committed by GitHub
parent 591730d0ce
commit 4c28697d0c
28 changed files with 936 additions and 721 deletions
@@ -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 }),
+36 -39
View File
@@ -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)
})
})
+33 -10
View File
@@ -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
}
+23 -4
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
})