This reverts commit cb8aa31b66.
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
import { getStreamFactory } from '@/modules/core/repositories/streams'
|
||||
import { defineLoaders } from '@/modules/loaders'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { db } from '@/db/knex'
|
||||
import { getUserServerRoleFactory } from '@/modules/shared/repositories/acl'
|
||||
|
||||
export const defineModuleLoaders = () => {
|
||||
const getStream = getStreamFactory({ db })
|
||||
const getUserServerRole = getUserServerRoleFactory({ db })
|
||||
|
||||
defineLoaders({
|
||||
getEnv: getFeatureFlags,
|
||||
getProject: async ({ projectId }) => {
|
||||
const project = await getStream({ streamId: projectId })
|
||||
if (!project) return null
|
||||
return { ...project, projectId: project.id }
|
||||
},
|
||||
getProjectRole: async ({ userId, projectId }) => {
|
||||
const project = await getStream({ streamId: projectId, userId })
|
||||
return project?.role ?? null
|
||||
},
|
||||
getServerRole: async ({ userId }) => {
|
||||
const role = await getUserServerRole({ userId })
|
||||
return role ?? null
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -81,6 +81,7 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
|
||||
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
|
||||
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
|
||||
import { authorizeResolver, validateScopes } from '@/modules/shared'
|
||||
import { throwForNotHavingServerRole } from '@/modules/shared/authz'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import {
|
||||
filteredSubscribe,
|
||||
@@ -88,9 +89,6 @@ import {
|
||||
UserSubscriptions
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import { has } from 'lodash'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUsers = getUsersFactory({ db })
|
||||
@@ -179,31 +177,28 @@ const getUserStreamsCount = getUserStreamsCountFactory({ db })
|
||||
export = {
|
||||
Query: {
|
||||
async project(_parent, args, context) {
|
||||
const canQuery = await context.authPolicies.project.canQuery({
|
||||
projectId: args.id,
|
||||
const getStream = getStreamFactory({ db })
|
||||
const stream = await getStream({
|
||||
streamId: args.id,
|
||||
userId: context.userId
|
||||
})
|
||||
|
||||
if (!canQuery.authorized) {
|
||||
switch (canQuery.error.code) {
|
||||
case Authz.ProjectNotFoundError.code:
|
||||
throw new StreamNotFoundError()
|
||||
case Authz.ProjectNoAccessError.code:
|
||||
case Authz.WorkspaceNoAccessError.code:
|
||||
case Authz.WorkspaceSsoSessionInvalidError.code:
|
||||
throw new ForbiddenError(canQuery.error.message)
|
||||
default:
|
||||
throwUncoveredError(canQuery.error)
|
||||
}
|
||||
if (!stream) {
|
||||
throw new StreamNotFoundError('Project not found')
|
||||
}
|
||||
|
||||
const project = await getStream({ streamId: args.id })
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.id,
|
||||
Roles.Stream.Reviewer,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
if (!project?.isPublic || !project.isDiscoverable) {
|
||||
if (!stream.isPublic) {
|
||||
await throwForNotHavingServerRole(context, Roles.Server.Guest)
|
||||
validateScopes(context.scopes, Scopes.Streams.Read)
|
||||
}
|
||||
|
||||
return project
|
||||
return stream
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
|
||||
@@ -23,7 +23,6 @@ import { reportSubscriptionEventsFactory } from '@/modules/core/events/subscript
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { publish } from '@/modules/shared/utils/subscriptions'
|
||||
import { getStreamCollaboratorsFactory } from '@/modules/core/repositories/streams'
|
||||
import { defineModuleLoaders } from '@/modules/core/authz'
|
||||
|
||||
let stopTestSubs: (() => void) | undefined = undefined
|
||||
|
||||
@@ -88,8 +87,6 @@ const coreModule: SpeckleModule<{
|
||||
getStreamCollaborators: getStreamCollaboratorsFactory({ db })
|
||||
})()
|
||||
}
|
||||
|
||||
defineModuleLoaders()
|
||||
},
|
||||
async shutdown() {
|
||||
await shutdownResultListener()
|
||||
|
||||
@@ -3,14 +3,11 @@ import {
|
||||
UserStreamRole
|
||||
} from '@/modules/shared/domain/rolesAndScopes/types'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import { RoleInfo } from '@speckle/shared'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
// Conventions:
|
||||
// "weight: 1000" => resource owner
|
||||
// "weight: 100" => resource viewer / basic user
|
||||
// Anything in between 100 and 1000 can be used for escalating privileges.
|
||||
const keysToPick = ['weight', 'description'] as const
|
||||
|
||||
const coreUserRoles: Array<UserServerRole | UserStreamRole> = [
|
||||
/**
|
||||
@@ -18,16 +15,19 @@ const coreUserRoles: Array<UserServerRole | UserStreamRole> = [
|
||||
*/
|
||||
{
|
||||
name: Roles.Server.Admin,
|
||||
...pick(RoleInfo.Server[Roles.Server.Admin], keysToPick),
|
||||
description:
|
||||
'Holds supreme autocratic authority, not restricted by written laws, legislature, or customs.',
|
||||
resourceTarget: 'server',
|
||||
aclTableName: 'server_acl',
|
||||
weight: 1000,
|
||||
public: false
|
||||
},
|
||||
{
|
||||
name: Roles.Server.User,
|
||||
...pick(RoleInfo.Server[Roles.Server.User], keysToPick),
|
||||
description: 'Has normal access to the server.',
|
||||
resourceTarget: 'server',
|
||||
aclTableName: 'server_acl',
|
||||
weight: 100,
|
||||
public: false
|
||||
},
|
||||
// TODO: should this be dynamically pushed if guest role is enabled?
|
||||
@@ -36,16 +36,18 @@ const coreUserRoles: Array<UserServerRole | UserStreamRole> = [
|
||||
// can leave the guest users in a broken state
|
||||
{
|
||||
name: Roles.Server.Guest,
|
||||
...pick(RoleInfo.Server[Roles.Server.Guest], keysToPick),
|
||||
description: 'Has limited access to the server.',
|
||||
resourceTarget: 'server',
|
||||
aclTableName: 'server_acl',
|
||||
weight: 50,
|
||||
public: false
|
||||
},
|
||||
{
|
||||
name: Roles.Server.ArchivedUser,
|
||||
...pick(RoleInfo.Server[Roles.Server.ArchivedUser], keysToPick),
|
||||
description: 'No longer has access to the server.',
|
||||
resourceTarget: 'server',
|
||||
aclTableName: 'server_acl',
|
||||
weight: 10,
|
||||
public: false
|
||||
},
|
||||
/**
|
||||
@@ -53,23 +55,27 @@ const coreUserRoles: Array<UserServerRole | UserStreamRole> = [
|
||||
*/
|
||||
{
|
||||
name: Roles.Stream.Owner,
|
||||
...pick(RoleInfo.Stream[Roles.Stream.Owner], keysToPick),
|
||||
description: 'Owners have full access, including deletion rights & access control.',
|
||||
resourceTarget: 'streams',
|
||||
aclTableName: 'stream_acl',
|
||||
weight: 1000,
|
||||
public: true
|
||||
},
|
||||
{
|
||||
name: Roles.Stream.Contributor,
|
||||
...pick(RoleInfo.Stream[Roles.Stream.Contributor], keysToPick),
|
||||
description:
|
||||
'Contributors can create new branches and commits, but they cannot edit stream details or manage collaborators.',
|
||||
resourceTarget: 'streams',
|
||||
aclTableName: 'stream_acl',
|
||||
weight: 500,
|
||||
public: true
|
||||
},
|
||||
{
|
||||
name: Roles.Stream.Reviewer,
|
||||
...pick(RoleInfo.Stream[Roles.Stream.Reviewer], keysToPick),
|
||||
description: 'Reviewers can only view (read) the data from this stream.',
|
||||
resourceTarget: 'streams',
|
||||
aclTableName: 'stream_acl',
|
||||
weight: 100,
|
||||
public: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,7 +24,6 @@ import { AppMocksConfig } from '@/modules/mocks'
|
||||
import { SpeckleModuleMocksConfig } from '@/modules/shared/helpers/mocks'
|
||||
import { LogicError } from '@/modules/shared/errors'
|
||||
import type { Registry } from 'prom-client'
|
||||
import { validateLoaders } from '@/modules/loaders'
|
||||
|
||||
/**
|
||||
* Cached speckle module requires
|
||||
@@ -128,8 +127,6 @@ export const init = async (params: { app: Express; metricsRegister: Registry })
|
||||
await module.finalize?.({ app, isInitial, metricsRegister })
|
||||
}
|
||||
|
||||
validateLoaders()
|
||||
|
||||
hasInitializationOccurred = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { LoaderConfigurationError } from '@/modules/shared/errors'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
let cachedLoaders: Partial<Authz.AuthCheckContextLoaders> = {}
|
||||
|
||||
const loaderKeys: (keyof Authz.AuthCheckContextLoaders)[] = [
|
||||
'getEnv',
|
||||
'getProject',
|
||||
'getProjectRole',
|
||||
'getServerRole',
|
||||
'getWorkspace',
|
||||
'getWorkspaceRole',
|
||||
'getWorkspaceSsoProvider',
|
||||
'getWorkspaceSsoSession'
|
||||
]
|
||||
|
||||
export const defineLoaders = (
|
||||
loaders: Partial<Authz.AuthCheckContextLoaders>
|
||||
): void => {
|
||||
for (const key of Object.keys(loaders)) {
|
||||
if (!loaderKeys.includes(key as keyof Authz.AuthCheckContextLoaders)) {
|
||||
throw new LoaderConfigurationError(
|
||||
`Attempted to define loader with unknown key: ${key}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cachedLoaders = {
|
||||
...cachedLoaders,
|
||||
...loaders
|
||||
}
|
||||
}
|
||||
|
||||
const isValidLoaders = (
|
||||
loaders: Partial<Authz.AuthCheckContextLoaders>
|
||||
): loaders is Authz.AuthCheckContextLoaders => {
|
||||
return loaderKeys.every((key) => !!loaders[key])
|
||||
}
|
||||
|
||||
export const validateLoaders = () => {
|
||||
if (!isValidLoaders(cachedLoaders)) {
|
||||
throw new LoaderConfigurationError()
|
||||
}
|
||||
}
|
||||
|
||||
export const getLoaders = (): Authz.AuthCheckContextLoaders => {
|
||||
if (!isValidLoaders(cachedLoaders)) {
|
||||
throw new LoaderConfigurationError('Attempted to reference invalid loaders.')
|
||||
}
|
||||
return cachedLoaders
|
||||
}
|
||||
@@ -140,22 +140,5 @@ export class DatabaseError<I extends Info = Info> extends EnvironmentResourceErr
|
||||
}
|
||||
}
|
||||
|
||||
export class LoaderConfigurationError extends BaseError {
|
||||
static code = 'LOADER_CONFIGURATION_ERROR'
|
||||
static defaultMessage = 'Error while initializing authz loaders'
|
||||
static statusCode = 500
|
||||
|
||||
constructor(message?: string) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
export class LoaderUnsupportedError extends BaseError {
|
||||
static code = 'LOADER_UNSUPPORTED_ERROR'
|
||||
static defaultMessage =
|
||||
'Cannot invoke loader given current server configuration. Check environment variables.'
|
||||
static statusCode = 500
|
||||
}
|
||||
|
||||
export { BaseError }
|
||||
export type { Info }
|
||||
|
||||
@@ -3,8 +3,7 @@ import type {
|
||||
Optional,
|
||||
MaybeNullOrUndefined,
|
||||
MaybeAsync,
|
||||
MaybeFalsy,
|
||||
Authz
|
||||
MaybeFalsy
|
||||
} from '@speckle/shared'
|
||||
import type { RequestDataLoaders } from '@/modules/core/loaders'
|
||||
import type { AuthContext } from '@/modules/shared/authz'
|
||||
@@ -53,7 +52,6 @@ export type SpeckleModule<T extends Record<string, unknown> = Record<string, unk
|
||||
|
||||
export type GraphQLContext = BaseContext &
|
||||
AuthContext & {
|
||||
authPolicies: Authz.AuthPolices
|
||||
/**
|
||||
* Request-scoped GraphQL dataloaders
|
||||
* @see https://github.com/graphql/dataloader
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
MaybeNullOrUndefined,
|
||||
Nullable
|
||||
} from '@/modules/shared/helpers/typeHelper'
|
||||
import { Authz, Optional, wait } from '@speckle/shared'
|
||||
import { Optional, wait } from '@speckle/shared'
|
||||
import { mixpanel } from '@/modules/shared/utils/mixpanel'
|
||||
import * as Observability from '@speckle/shared/dist/commonjs/observability/index.js'
|
||||
import { pino } from 'pino'
|
||||
@@ -48,7 +48,6 @@ import { getTokenAppInfoFactory } from '@/modules/auth/repositories/apps'
|
||||
import { getUserRoleFactory } from '@/modules/core/repositories/users'
|
||||
import { UserInputError } from '@/modules/core/errors/userinput'
|
||||
import compression from 'compression'
|
||||
import { getLoaders } from '@/modules/loaders'
|
||||
|
||||
export const authMiddlewareCreator = (
|
||||
steps: AuthPipelineFunction[]
|
||||
@@ -221,14 +220,11 @@ export async function buildContext({
|
||||
await wait(delay)
|
||||
}
|
||||
|
||||
const authPolicies = Authz.authPoliciesFactory(getLoaders())
|
||||
|
||||
// Adding request data loaders
|
||||
return await addLoadersToCtx(
|
||||
{
|
||||
...ctx,
|
||||
log,
|
||||
authPolicies
|
||||
log
|
||||
},
|
||||
{ cleanLoadersEarly }
|
||||
)
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { defineLoaders } from '@/modules/loaders'
|
||||
import {
|
||||
getUserSsoSessionFactory,
|
||||
getWorkspaceSsoProviderRecordFactory
|
||||
} from '@/modules/workspaces/repositories/sso'
|
||||
import {
|
||||
getWorkspaceFactory,
|
||||
getWorkspaceRoleForUserFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
|
||||
export const defineModuleLoaders = () => {
|
||||
defineLoaders({
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
getWorkspaceRole: async ({ userId, workspaceId }) => {
|
||||
const role = await getWorkspaceRoleForUserFactory({ db })({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
return role?.role ?? null
|
||||
},
|
||||
getWorkspaceSsoSession: async ({ userId, workspaceId }) => {
|
||||
const ssoSession = await getUserSsoSessionFactory({ db })({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
return ssoSession ?? null
|
||||
},
|
||||
getWorkspaceSsoProvider: async ({ workspaceId }) => {
|
||||
const ssoProvider = await getWorkspaceSsoProviderRecordFactory({ db })({
|
||||
workspaceId
|
||||
})
|
||||
return ssoProvider ?? null
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { initializeEventListenersFactory } from '@/modules/workspaces/events/eve
|
||||
import { validateModuleLicense } from '@/modules/gatekeeper/services/validateLicense'
|
||||
import { getSsoRouter } from '@/modules/workspaces/rest/sso'
|
||||
import { InvalidLicenseError } from '@/modules/gatekeeper/errors/license'
|
||||
import { defineModuleLoaders } from '@/modules/workspaces/authz'
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED, FF_WORKSPACES_SSO_ENABLED } = getFeatureFlags()
|
||||
|
||||
@@ -45,8 +44,6 @@ const workspacesModule: SpeckleModule = {
|
||||
quitListeners = initializeEventListenersFactory({ db })()
|
||||
}
|
||||
await Promise.all([initScopes(), initRoles()])
|
||||
|
||||
defineModuleLoaders()
|
||||
},
|
||||
shutdown() {
|
||||
if (!FF_WORKSPACES_MODULE_ENABLED) return
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { UserWorkspaceRole } from '@/modules/shared/domain/rolesAndScopes/types'
|
||||
import { Roles, RoleInfo } from '@speckle/shared'
|
||||
import { pick } from 'lodash'
|
||||
import { Roles } from '@speckle/shared'
|
||||
|
||||
const aclTableName = 'workspace_acl'
|
||||
const resourceTarget = 'workspaces'
|
||||
|
||||
const keysToPick = ['weight', 'description'] as const
|
||||
|
||||
export const workspaceRoles: UserWorkspaceRole[] = [
|
||||
{
|
||||
name: Roles.Workspace.Admin,
|
||||
...pick(RoleInfo.Workspace[Roles.Workspace.Admin], keysToPick),
|
||||
description: 'Has root on the workspace',
|
||||
weight: 1000,
|
||||
public: true,
|
||||
resourceTarget,
|
||||
aclTableName
|
||||
},
|
||||
{
|
||||
name: Roles.Workspace.Member,
|
||||
...pick(RoleInfo.Workspace[Roles.Workspace.Member], keysToPick),
|
||||
description: 'A regular member of the workspace',
|
||||
weight: 100,
|
||||
public: true,
|
||||
resourceTarget,
|
||||
aclTableName
|
||||
},
|
||||
{
|
||||
name: Roles.Workspace.Guest,
|
||||
...pick(RoleInfo.Workspace[Roles.Workspace.Guest], keysToPick),
|
||||
description: 'An external guest member of the workspace with limited rights',
|
||||
weight: 50,
|
||||
public: true,
|
||||
resourceTarget,
|
||||
aclTableName
|
||||
|
||||
@@ -155,23 +155,19 @@ describe('Workspace SSO', () => {
|
||||
const resA = await memberApollo.execute(GetWorkspaceDocument, {
|
||||
workspaceId: testWorkspaceWithSso.id
|
||||
})
|
||||
expect(resA).to.haveGraphQLErrors({ message: 'gql-sso-workspace' })
|
||||
expect(resA).to.haveGraphQLErrors({
|
||||
code: 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
|
||||
})
|
||||
|
||||
const resB = await memberApollo.execute(GetWorkspaceProjectsDocument, {
|
||||
id: testWorkspaceWithSso.id
|
||||
})
|
||||
expect(resB).to.haveGraphQLErrors({ message: 'gql-sso-workspace' })
|
||||
expect(resB).to.haveGraphQLErrors({
|
||||
code: 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
|
||||
})
|
||||
|
||||
const resC = await memberApollo.execute(GetProjectDocument, {
|
||||
id: testWorkspaceWithSsoProjectId
|
||||
})
|
||||
expect(resC).to.haveGraphQLErrors({ message: 'SSO session is invalid' })
|
||||
|
||||
for (const res of [resA, resB, resC]) {
|
||||
expect(res).to.haveGraphQLErrors({ message: 'gql-sso-workspace' })
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: 'SSO_SESSION_MISSING_OR_EXPIRED_ERROR'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('should allow limited access to workspace memberships', async () => {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineLoaders } from '@/modules/loaders'
|
||||
import { LoaderUnsupportedError } from '@/modules/shared/errors'
|
||||
|
||||
export const defineModuleLoaders = () => {
|
||||
defineLoaders({
|
||||
getWorkspace: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
},
|
||||
getWorkspaceRole: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
},
|
||||
getWorkspaceSsoSession: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
},
|
||||
getWorkspaceSsoProvider: async () => {
|
||||
throw new LoaderUnsupportedError()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
import { defineModuleLoaders } from '@/modules/workspacesCore/authz'
|
||||
import { moduleLogger } from '@/observability/logging'
|
||||
|
||||
export const init: SpeckleModule['init'] = () => {
|
||||
moduleLogger.info('⚒️ Init workspaces core module')
|
||||
defineModuleLoaders()
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { addLoadersToCtx } from '@/modules/shared/middleware'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import {
|
||||
AllScopes,
|
||||
Authz,
|
||||
buildManualPromise,
|
||||
ensureError,
|
||||
MaybeAsync,
|
||||
@@ -34,7 +33,6 @@ import { PingPongDocument } from '@/test/graphql/generated/graphql'
|
||||
import { BaseError } from '@/modules/shared/errors'
|
||||
import EventEmitter from 'eventemitter2'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { getLoaders } from '@/modules/loaders'
|
||||
|
||||
type TypedGraphqlResponse<R = Record<string, any>> = GraphQLResponse<R>
|
||||
|
||||
@@ -118,7 +116,6 @@ export const createTestContext = async (
|
||||
scopes: [],
|
||||
stream: undefined,
|
||||
err: undefined,
|
||||
authPolicies: Authz.authPoliciesFactory(getLoaders()),
|
||||
...(ctx || {})
|
||||
})
|
||||
|
||||
@@ -132,7 +129,6 @@ export const createAuthedTestContext = async (
|
||||
role: Roles.Server.User,
|
||||
token: 'asd',
|
||||
scopes: AllScopes,
|
||||
authPolicies: Authz.authPoliciesFactory(getLoaders()),
|
||||
...(ctxOverrides || {})
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint": "yarn lint:eslint && yarn lint:tsc",
|
||||
"lint:ci": "yarn lint:tsc",
|
||||
"test": "vitest src/authz/policies/canQueryProject.spec.ts",
|
||||
"test": "vitest",
|
||||
"test:single-run": "vitest run"
|
||||
},
|
||||
"sideEffects": false,
|
||||
@@ -56,7 +56,6 @@
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@typescript-eslint/eslint-plugin": "^7.12.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"eslint": "^9.4.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"knex": "^2.5.1",
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requireExactProjectVisibility } from './projects.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Project } from '../domain/projects/types.js'
|
||||
|
||||
describe('requireExactProjectVisibility returns a function, that', () => {
|
||||
it('throws if project does not exist', async () => {
|
||||
const test = requireExactProjectVisibility({
|
||||
loaders: {
|
||||
getProject: () => Promise.resolve(null)
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
test({
|
||||
projectVisibility: 'linkShareable',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
it('correctly asserts link shareable projects', async () => {
|
||||
const result = await requireExactProjectVisibility({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve({
|
||||
isDiscoverable: true
|
||||
} as Project)
|
||||
}
|
||||
})({
|
||||
projectVisibility: 'linkShareable',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('correctly asserts public projects', async () => {
|
||||
const result = await requireExactProjectVisibility({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve({
|
||||
isPublic: true
|
||||
} as Project)
|
||||
}
|
||||
})({
|
||||
projectVisibility: 'public',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('correct asserts private projects', async () => {
|
||||
const result = await requireExactProjectVisibility({
|
||||
loaders: {
|
||||
getProject: () =>
|
||||
Promise.resolve({
|
||||
isDiscoverable: false,
|
||||
isPublic: false
|
||||
} as Project)
|
||||
}
|
||||
})({
|
||||
projectVisibility: 'private',
|
||||
projectId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { StreamRoles, throwUncoveredError } from '../../core/index.js'
|
||||
import { AuthCheckContext } from '../domain/loaders.js'
|
||||
import { isMinimumProjectRole } from '../domain/projects/logic.js'
|
||||
import { ProjectVisibility } from '../domain/projects/types.js'
|
||||
|
||||
export const requireExactProjectVisibility =
|
||||
({ loaders }: AuthCheckContext<'getProject'>) =>
|
||||
async (args: {
|
||||
projectVisibility: ProjectVisibility
|
||||
projectId: string
|
||||
}): Promise<boolean> => {
|
||||
const { projectId, projectVisibility } = args
|
||||
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (!project) throw new Error(`Project not found`)
|
||||
|
||||
switch (projectVisibility) {
|
||||
case 'linkShareable':
|
||||
return project.isDiscoverable === true
|
||||
case 'public':
|
||||
return project.isPublic === true
|
||||
case 'private':
|
||||
return project.isPublic !== true && project.isDiscoverable !== true
|
||||
default:
|
||||
throwUncoveredError(projectVisibility)
|
||||
}
|
||||
}
|
||||
|
||||
export const requireMinimumProjectRole =
|
||||
({ loaders }: AuthCheckContext<'getProjectRole'>) =>
|
||||
async (args: {
|
||||
userId: string
|
||||
projectId: string
|
||||
role: StreamRoles
|
||||
}): Promise<boolean> => {
|
||||
const { userId, projectId, role: requiredProjectRole } = args
|
||||
|
||||
const userProjectRole = await loaders.getProjectRole({ userId, projectId })
|
||||
return userProjectRole
|
||||
? isMinimumProjectRole(userProjectRole, requiredProjectRole)
|
||||
: false
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requireExactServerRole } from './serverRole.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
describe('requireExactServerRole returns a function, that', () => {
|
||||
it('returns false for mismatch roles', async () => {
|
||||
const result = await requireExactServerRole({
|
||||
loaders: {
|
||||
getServerRole: () => Promise.resolve('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(null)
|
||||
}
|
||||
})({
|
||||
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('server:admin')
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
role: 'server:admin'
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ServerRoles } from '../../core/constants.js'
|
||||
import { AuthCheckContext } from '../domain/loaders.js'
|
||||
|
||||
export const requireExactServerRole =
|
||||
({ loaders }: AuthCheckContext<'getServerRole'>) =>
|
||||
async (args: { userId: string; role: ServerRoles }): Promise<boolean> => {
|
||||
const { userId, role: requiredServerRole } = args
|
||||
|
||||
const userServerRole = await loaders.getServerRole({ userId })
|
||||
|
||||
return userServerRole === requiredServerRole
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
requireAnyWorkspaceRole,
|
||||
requireMinimumWorkspaceRole
|
||||
} from './workspaceRole.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
describe('requireAnyWorkspaceRole returns a function, that', () => {
|
||||
it('returns false if the user has no role', async () => {
|
||||
const result = await requireAnyWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve(null)
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true if the user has a role', async () => {
|
||||
const result = await requireAnyWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:member')
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 })
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
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(null)
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
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('workspace:member')
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
role: 'workspace:admin'
|
||||
})
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
it('returns true if user matches target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:member')
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
it('returns true if user exceeds target role', async () => {
|
||||
const result = await requireMinimumWorkspaceRole({
|
||||
loaders: {
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:admin')
|
||||
}
|
||||
})({
|
||||
userId: cryptoRandomString({ length: 9 }),
|
||||
workspaceId: cryptoRandomString({ length: 9 }),
|
||||
role: 'workspace:member'
|
||||
})
|
||||
expect(result).toEqual(true)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { WorkspaceRoles } from '../../core/constants.js'
|
||||
import { AuthCheckContext } from '../domain/loaders.js'
|
||||
import { isMinimumWorkspaceRole } from '../domain/workspaces/logic.js'
|
||||
|
||||
export const requireAnyWorkspaceRole =
|
||||
({ loaders }: AuthCheckContext<'getWorkspaceRole'>) =>
|
||||
async (args: { userId: string; workspaceId: string }): Promise<boolean> => {
|
||||
const { userId, workspaceId } = args
|
||||
|
||||
const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId })
|
||||
|
||||
return userWorkspaceRole !== null
|
||||
}
|
||||
|
||||
export const requireMinimumWorkspaceRole =
|
||||
({ loaders }: AuthCheckContext<'getWorkspaceRole'>) =>
|
||||
async (args: {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
role: WorkspaceRoles
|
||||
}): Promise<boolean> => {
|
||||
const { userId, workspaceId, role: requiredWorkspaceRole } = args
|
||||
|
||||
const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId })
|
||||
|
||||
return userWorkspaceRole
|
||||
? isMinimumWorkspaceRole(userWorkspaceRole, requiredWorkspaceRole)
|
||||
: false
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { requireValidWorkspaceSsoSession } from './workspaceSso.js'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
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(null)
|
||||
}
|
||||
})({
|
||||
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({
|
||||
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({
|
||||
userId,
|
||||
providerId,
|
||||
validUntil
|
||||
})
|
||||
}
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { AuthCheckContext } from '../domain/loaders.js'
|
||||
|
||||
export const requireValidWorkspaceSsoSession =
|
||||
({ loaders }: AuthCheckContext<'getWorkspaceSsoSession'>) =>
|
||||
async (args: { userId: string; workspaceId: string }): Promise<boolean> => {
|
||||
const { userId, workspaceId } = args
|
||||
|
||||
const workspaceSsoSession = await loaders.getWorkspaceSsoSession({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
|
||||
const isExpiredSession =
|
||||
new Date().getTime() > (workspaceSsoSession?.validUntil?.getTime() ?? 0)
|
||||
|
||||
return !!workspaceSsoSession && !isExpiredSession
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
type AuthSuccess = {
|
||||
authorized: true
|
||||
}
|
||||
|
||||
export type AuthFailure<T> = {
|
||||
authorized: false
|
||||
error: T
|
||||
}
|
||||
|
||||
export type AuthResult<T> = AuthSuccess | AuthFailure<T>
|
||||
|
||||
export const authorized = (): AuthSuccess => ({
|
||||
authorized: true
|
||||
})
|
||||
|
||||
export const unauthorized = <T>(error: T): AuthFailure<T> => ({
|
||||
authorized: false,
|
||||
error
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
import { ServerRoles } from '../../../core/constants.js'
|
||||
|
||||
export type GetServerRole = (args: { userId: string }) => Promise<ServerRoles | null>
|
||||
@@ -1,36 +0,0 @@
|
||||
type AuthError<ErrorCode extends string> = {
|
||||
code: ErrorCode
|
||||
message: string
|
||||
}
|
||||
|
||||
export const defineAuthError = <ErrorCode extends string>(params: {
|
||||
code: ErrorCode
|
||||
message: string
|
||||
}): AuthError<ErrorCode> => {
|
||||
const { code, message } = params
|
||||
|
||||
return {
|
||||
code,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
export const ProjectNotFoundError = defineAuthError({
|
||||
code: 'ProjectNotFound',
|
||||
message: 'Project not found'
|
||||
})
|
||||
|
||||
export const ProjectNoAccessError = defineAuthError({
|
||||
code: 'ProjectNoAccess',
|
||||
message: 'You do not have access to the project'
|
||||
})
|
||||
|
||||
export const WorkspaceNoAccessError = defineAuthError({
|
||||
code: 'WorkspaceNoAccess',
|
||||
message: 'You do not have access to the workspace'
|
||||
})
|
||||
|
||||
export const WorkspaceSsoSessionInvalidError = defineAuthError({
|
||||
code: 'WorkspaceSsoSessionInvalid',
|
||||
message: 'Your workspace SSO session is invalid'
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import { GetServerRole } from './core/operations.js'
|
||||
import { GetProject, GetProjectRole } from './projects/operations.js'
|
||||
import {
|
||||
GetEnv,
|
||||
GetWorkspace,
|
||||
GetWorkspaceRole,
|
||||
GetWorkspaceSsoProvider,
|
||||
GetWorkspaceSsoSession
|
||||
} from './workspaces/operations.js'
|
||||
|
||||
export type AuthCheckContext<LoaderKeys extends keyof AuthCheckContextLoaders> = {
|
||||
loaders: Pick<AuthCheckContextLoaders, LoaderKeys>
|
||||
}
|
||||
|
||||
export type AuthCheckContextLoaders = {
|
||||
getEnv: GetEnv
|
||||
getProject: GetProject
|
||||
getProjectRole: GetProjectRole
|
||||
getServerRole: GetServerRole
|
||||
getWorkspace: GetWorkspace
|
||||
getWorkspaceRole: GetWorkspaceRole
|
||||
getWorkspaceSsoProvider: GetWorkspaceSsoProvider
|
||||
getWorkspaceSsoSession: GetWorkspaceSsoSession
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type ProjectContext = { projectId: string }
|
||||
|
||||
export type UserContext = { userId?: string }
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isMinimumProjectRole } from './logic.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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import { StreamRoles, RoleInfo } from '../../../core/constants.js'
|
||||
|
||||
export const isMinimumProjectRole = (
|
||||
role: StreamRoles,
|
||||
targetRole: StreamRoles
|
||||
): boolean => {
|
||||
const roleWeight = RoleInfo.Stream[role].weight
|
||||
const targetRoleWeight = RoleInfo.Stream[targetRole].weight
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { StreamRoles } from '../../../core/constants.js'
|
||||
import { Project } from './types.js'
|
||||
|
||||
// TODO: this should probably just throw an error if the project doesn't exist
|
||||
export type GetProject = (args: { projectId: string }) => Promise<Project | null>
|
||||
|
||||
export type GetProjectRole = (args: {
|
||||
userId: string
|
||||
projectId: string
|
||||
}) => Promise<StreamRoles | null>
|
||||
@@ -1,8 +0,0 @@
|
||||
export type Project = {
|
||||
// TODO: Deprecated field?
|
||||
isDiscoverable: boolean
|
||||
isPublic: boolean
|
||||
workspaceId: string | null
|
||||
}
|
||||
|
||||
export type ProjectVisibility = 'public' | 'linkShareable' | 'private'
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isMinimumWorkspaceRole } from './logic.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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
import { RoleInfo, WorkspaceRoles } from '../../../core/constants.js'
|
||||
|
||||
export const isMinimumWorkspaceRole = (
|
||||
role: WorkspaceRoles,
|
||||
targetRole: WorkspaceRoles
|
||||
): boolean => {
|
||||
const roleWeight = RoleInfo.Workspace[role].weight
|
||||
const targetRoleWeight = RoleInfo.Workspace[targetRole].weight
|
||||
|
||||
return roleWeight >= targetRoleWeight
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { WorkspaceRoles } from '../../../core/constants.js'
|
||||
import { FeatureFlags } from '../../../environment/index.js'
|
||||
import { Workspace, WorkspaceSsoProvider, WorkspaceSsoSession } from './types.js'
|
||||
|
||||
export type GetWorkspace = (args: { workspaceId: string }) => Promise<Workspace | null>
|
||||
|
||||
export type GetWorkspaceRole = (args: {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
}) => Promise<WorkspaceRoles | null>
|
||||
|
||||
export type GetWorkspaceSsoProvider = (args: {
|
||||
workspaceId: string
|
||||
}) => Promise<WorkspaceSsoProvider | null>
|
||||
|
||||
export type GetWorkspaceSsoSession = (args: {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
}) => Promise<WorkspaceSsoSession | null>
|
||||
|
||||
export type GetEnv = () => FeatureFlags
|
||||
@@ -1,13 +0,0 @@
|
||||
export type Workspace = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type WorkspaceSsoProvider = {
|
||||
providerId: string
|
||||
}
|
||||
|
||||
export type WorkspaceSsoSession = {
|
||||
userId: string
|
||||
providerId: string
|
||||
validUntil: Date
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { authPoliciesFactory, AuthPolices } from './policies/index.js'
|
||||
export { AuthCheckContextLoaders } from './domain/loaders.js'
|
||||
|
||||
export * from './domain/errors.js'
|
||||
@@ -1,410 +0,0 @@
|
||||
import { describe, expect, it, assert } from 'vitest'
|
||||
import { canQueryProjectPolicyFactory } from './canQueryProject.js'
|
||||
import { parseFeatureFlags } from '../../environment/index.js'
|
||||
import crs from 'crypto-random-string'
|
||||
import { merge } from 'lodash'
|
||||
import { Project } from '../domain/projects/types.js'
|
||||
import { Roles } from '../../core/constants.js'
|
||||
import { ProjectNoAccessError, ProjectNotFoundError } from '../domain/errors.js'
|
||||
|
||||
const fakeGetFactory =
|
||||
<T extends Record<string, unknown>>(defaults: T) =>
|
||||
(overrides?: Partial<T>) =>
|
||||
(): Promise<T> => {
|
||||
if (overrides) {
|
||||
return Promise.resolve(merge(defaults, overrides))
|
||||
}
|
||||
return Promise.resolve(defaults)
|
||||
}
|
||||
|
||||
const getProjectFake = fakeGetFactory<Project>({
|
||||
isPublic: false,
|
||||
isDiscoverable: false,
|
||||
workspaceId: null
|
||||
})
|
||||
|
||||
const canQueryProjectArgs = () => {
|
||||
const projectId = crs({ length: 10 })
|
||||
const userId = crs({ length: 10 })
|
||||
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: () => parseFeatureFlags({}),
|
||||
getProject: () => Promise.resolve(null),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
|
||||
expect(canQuery.authorized).toBe(false)
|
||||
if (!canQuery.authorized) {
|
||||
expect(canQuery.error.code).toBe(ProjectNotFoundError.code)
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('project visibility', () => {
|
||||
it('allows anyone on a public project', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({ isPublic: true }),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
it('allows anyone on a linkShareable project', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({ isDiscoverable: true }),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('project roles', () => {
|
||||
it.each(Object.values(Roles.Stream))(
|
||||
'allows access to private projects with role %',
|
||||
async (role) => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getProjectRole: () => Promise.resolve(role),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
}
|
||||
)
|
||||
it('does not allow access to private projects without a project role', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({}),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getProjectRole: () => Promise.resolve(null),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(false)
|
||||
if (!canQuery.authorized) {
|
||||
expect(canQuery.error.code).toBe(ProjectNoAccessError.code)
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('admin override', () => {
|
||||
it('allows server admins without project roles on private projects if admin override is enabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'true' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getServerRole: () => Promise.resolve(Roles.Server.Admin),
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
|
||||
it('does not allow server admins without project roles on private projects if admin override is disabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({ FF_ADMIN_OVERRIDE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({ isDiscoverable: false, isPublic: false }),
|
||||
getServerRole: () => Promise.resolve(Roles.Server.Admin),
|
||||
getProjectRole: () => {
|
||||
return Promise.resolve(null)
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(false)
|
||||
if (!canQuery.authorized) {
|
||||
expect(canQuery.error.code).toBe(ProjectNoAccessError.code)
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('the workspace world', () => {
|
||||
it('does not check workspace rules if the workspaces module is not enabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'false' }),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
it('does not allow project access without a workspace role', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve(null),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(false)
|
||||
})
|
||||
it('allows project access via workspace role if user does not have project role', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve(null),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:admin'),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => Promise.resolve(null)
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
it('does not check SSO sessions if user is workspace guest', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:guest'),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => {
|
||||
assert.fail()
|
||||
}
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
it('does not check SSO sessions if workspace does not have it enabled', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:member'),
|
||||
getWorkspaceSsoSession: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceSsoProvider: () => Promise.resolve(null)
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
it('does not allow project access if SSO session is missing', async () => {
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:member'),
|
||||
getWorkspaceSsoSession: () => Promise.resolve(null),
|
||||
getWorkspaceSsoProvider: () => Promise.resolve({ providerId: 'foo' })
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(false)
|
||||
})
|
||||
it('does not allow project access if SSO session is expired or invalid', async () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() - 1)
|
||||
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:member'),
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve({ validUntil: date, userId: 'foo', providerId: 'foo' }),
|
||||
getWorkspaceSsoProvider: () => Promise.resolve({ providerId: 'foo' })
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(false)
|
||||
})
|
||||
it('allows project access if SSO session is valid', async () => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + 1)
|
||||
|
||||
const canQueryProject = canQueryProjectPolicyFactory({
|
||||
getEnv: () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
isDiscoverable: false,
|
||||
isPublic: false,
|
||||
workspaceId: crs({ length: 10 })
|
||||
}),
|
||||
getProjectRole: () => Promise.resolve('stream:contributor'),
|
||||
getServerRole: () => {
|
||||
assert.fail()
|
||||
},
|
||||
getWorkspaceRole: () => Promise.resolve('workspace:member'),
|
||||
getWorkspaceSsoSession: () =>
|
||||
Promise.resolve({ validUntil: date, userId: 'foo', providerId: 'foo' }),
|
||||
getWorkspaceSsoProvider: () => Promise.resolve({ providerId: 'foo' })
|
||||
})
|
||||
const canQuery = await canQueryProject(canQueryProjectArgs())
|
||||
expect(canQuery.authorized).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,142 +0,0 @@
|
||||
import {
|
||||
requireAnyWorkspaceRole,
|
||||
requireMinimumWorkspaceRole
|
||||
} from '../checks/workspaceRole.js'
|
||||
import { AuthResult, authorized, unauthorized } from '../domain/authResult.js'
|
||||
import {
|
||||
requireExactProjectVisibility,
|
||||
requireMinimumProjectRole
|
||||
} from '../checks/projects.js'
|
||||
import { AuthCheckContextLoaders } from '../domain/loaders.js'
|
||||
import { ProjectContext, UserContext } from '../domain/policies.js'
|
||||
import { requireExactServerRole } from '../checks/serverRole.js'
|
||||
import { requireValidWorkspaceSsoSession } from '../checks/workspaceSso.js'
|
||||
import { Roles } from '../../core/constants.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
ProjectNotFoundError,
|
||||
WorkspaceNoAccessError,
|
||||
WorkspaceSsoSessionInvalidError
|
||||
} from '../domain/errors.js'
|
||||
|
||||
export const canQueryProjectPolicyFactory =
|
||||
(
|
||||
loaders: Pick<
|
||||
AuthCheckContextLoaders,
|
||||
| 'getEnv'
|
||||
| 'getProject'
|
||||
| 'getProjectRole'
|
||||
| 'getServerRole'
|
||||
| 'getWorkspaceRole'
|
||||
| 'getWorkspaceSsoProvider'
|
||||
| 'getWorkspaceSsoSession'
|
||||
>
|
||||
) =>
|
||||
async ({
|
||||
userId,
|
||||
projectId
|
||||
}: UserContext & ProjectContext): Promise<
|
||||
AuthResult<
|
||||
| typeof ProjectNotFoundError
|
||||
| typeof ProjectNoAccessError
|
||||
| typeof WorkspaceNoAccessError
|
||||
| typeof WorkspaceSsoSessionInvalidError
|
||||
>
|
||||
> => {
|
||||
const { FF_ADMIN_OVERRIDE_ENABLED, FF_WORKSPACES_MODULE_ENABLED } = loaders.getEnv()
|
||||
|
||||
const project = await loaders.getProject({ projectId })
|
||||
// hiding the project not found, to stop id brute force lookups
|
||||
if (!project) return unauthorized(ProjectNotFoundError)
|
||||
|
||||
// All users may read public projects
|
||||
const isPublicResult = await requireExactProjectVisibility({ loaders })({
|
||||
projectId,
|
||||
projectVisibility: 'public'
|
||||
})
|
||||
if (isPublicResult) {
|
||||
return authorized()
|
||||
}
|
||||
|
||||
// All users may read link-shareable projects
|
||||
const isLinkShareableResult = await requireExactProjectVisibility({ loaders })({
|
||||
projectId,
|
||||
projectVisibility: 'linkShareable'
|
||||
})
|
||||
if (isLinkShareableResult) {
|
||||
return authorized()
|
||||
}
|
||||
// From this point on, you cannot pass as an unknown user
|
||||
if (!userId) {
|
||||
return unauthorized(ProjectNoAccessError)
|
||||
}
|
||||
|
||||
// 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 authorized()
|
||||
}
|
||||
}
|
||||
|
||||
const { workspaceId } = project
|
||||
|
||||
// When a project belongs to a workspace
|
||||
if (FF_WORKSPACES_MODULE_ENABLED && !!workspaceId) {
|
||||
// User must have a workspace role to read project data
|
||||
const hasWorkspaceRoleResult = await requireAnyWorkspaceRole({ loaders })({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!hasWorkspaceRoleResult) {
|
||||
// Should we hide the fact, the project is in a workspace?
|
||||
return unauthorized(WorkspaceNoAccessError)
|
||||
}
|
||||
|
||||
const hasMinimumMemberRole = await requireMinimumWorkspaceRole({
|
||||
loaders
|
||||
})({
|
||||
userId,
|
||||
workspaceId,
|
||||
role: 'workspace:member'
|
||||
})
|
||||
|
||||
if (hasMinimumMemberRole) {
|
||||
const workspaceSsoProvider = await loaders.getWorkspaceSsoProvider({
|
||||
workspaceId
|
||||
})
|
||||
if (!!workspaceSsoProvider) {
|
||||
// Member and admin user must have a valid SSO session to read project data
|
||||
const hasValidSsoSessionResult = await requireValidWorkspaceSsoSession({
|
||||
loaders
|
||||
})({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!hasValidSsoSessionResult) {
|
||||
return unauthorized(WorkspaceSsoSessionInvalidError)
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace members get to go through without an explicit project role
|
||||
return authorized()
|
||||
} else {
|
||||
// just fall through to the generic project role check for workspace:guest-s
|
||||
}
|
||||
}
|
||||
|
||||
// User must have at least stream reviewer role to read project data
|
||||
const hasMinimumProjectRoleResult = await requireMinimumProjectRole({ loaders })({
|
||||
userId,
|
||||
projectId,
|
||||
role: 'stream:reviewer'
|
||||
})
|
||||
if (hasMinimumProjectRoleResult) {
|
||||
return authorized()
|
||||
}
|
||||
return unauthorized(ProjectNoAccessError)
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { AuthCheckContextLoaders } from '../domain/loaders.js'
|
||||
import { canQueryProjectPolicyFactory } from './canQueryProject.js'
|
||||
|
||||
export const authPoliciesFactory = (loaders: AuthCheckContextLoaders) => ({
|
||||
project: {
|
||||
canQuery: canQueryProjectPolicyFactory(loaders)
|
||||
}
|
||||
})
|
||||
|
||||
export type AuthPolices = ReturnType<typeof authPoliciesFactory>
|
||||
@@ -28,57 +28,47 @@ export const RoleInfo = Object.freeze(<const>{
|
||||
Stream: {
|
||||
[Roles.Stream.Owner]: {
|
||||
title: 'Owner',
|
||||
description: 'Can edit project, including settings, collaborators and all models',
|
||||
weight: 1000
|
||||
description: 'Can edit project, including settings, collaborators and all models'
|
||||
},
|
||||
[Roles.Stream.Contributor]: {
|
||||
title: 'Contributor',
|
||||
description: 'Can create models, publish model versions, and comment',
|
||||
weight: 500
|
||||
description: 'Can create models, publish model versions, and comment'
|
||||
},
|
||||
[Roles.Stream.Reviewer]: {
|
||||
title: 'Reviewer',
|
||||
description: 'Can view models, load model data, and comment',
|
||||
weight: 100
|
||||
description: 'Can view models, load model data, and comment'
|
||||
}
|
||||
},
|
||||
Server: {
|
||||
[Roles.Server.Admin]: {
|
||||
title: 'Admin',
|
||||
description: 'Can edit server, including settings, users and all projects',
|
||||
weight: 1000
|
||||
description: 'Can edit server, including settings, users and all projects'
|
||||
},
|
||||
[Roles.Server.User]: {
|
||||
title: 'User',
|
||||
description: 'Can create and own projects',
|
||||
weight: 100
|
||||
description: 'Can create and own projects'
|
||||
},
|
||||
[Roles.Server.Guest]: {
|
||||
title: 'Guest',
|
||||
description: "Can contribute to projects they're invited to",
|
||||
weight: 50
|
||||
description: "Can contribute to projects they're invited to"
|
||||
},
|
||||
[Roles.Server.ArchivedUser]: {
|
||||
title: 'Archived',
|
||||
description: 'Can no longer access server',
|
||||
weight: 10
|
||||
description: 'Can no longer access server'
|
||||
}
|
||||
},
|
||||
Workspace: {
|
||||
[Roles.Workspace.Admin]: {
|
||||
title: 'Admin',
|
||||
description: 'Can edit workspace, including settings, members and all projects',
|
||||
weight: 1000
|
||||
description: 'Can edit workspace, including settings, members and all projects'
|
||||
},
|
||||
[Roles.Workspace.Member]: {
|
||||
title: 'Member',
|
||||
description: 'Can create and own projects',
|
||||
weight: 100
|
||||
description: 'Can create and own projects'
|
||||
},
|
||||
[Roles.Workspace.Guest]: {
|
||||
title: 'Guest',
|
||||
description: "Can contribute to projects they're invited to",
|
||||
weight: 50
|
||||
description: "Can contribute to projects they're invited to"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,18 +6,10 @@ const isDisableAllFFsMode = () =>
|
||||
const isEnableAllFFsMode = () =>
|
||||
['true', '1'].includes(process.env.ENABLE_ALL_FFS || '')
|
||||
|
||||
export const parseFeatureFlags = (
|
||||
input: // | Record<string, string | undefined>
|
||||
Partial<Record<keyof FeatureFlags, 'true' | 'false' | undefined>>
|
||||
): FeatureFlags => {
|
||||
const parseFeatureFlags = () => {
|
||||
//INFO
|
||||
// As a convention all feature flags should be prefixed with a FF_
|
||||
const res = parseEnv(input, {
|
||||
// Enables the admin override feature
|
||||
FF_ADMIN_OVERRIDE_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
defaults: { production: false, _: false }
|
||||
},
|
||||
const res = parseEnv(process.env, {
|
||||
// Enables the automate module.
|
||||
FF_AUTOMATE_MODULE_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
@@ -101,10 +93,9 @@ export const parseFeatureFlags = (
|
||||
return res
|
||||
}
|
||||
|
||||
let parsedFlags: FeatureFlags | undefined
|
||||
let parsedFlags: ReturnType<typeof parseFeatureFlags> | undefined
|
||||
|
||||
export type FeatureFlags = {
|
||||
FF_ADMIN_OVERRIDE_ENABLED: boolean
|
||||
export function getFeatureFlags(): {
|
||||
FF_AUTOMATE_MODULE_ENABLED: boolean
|
||||
FF_GENDOAI_MODULE_ENABLED: boolean
|
||||
FF_WORKSPACES_MODULE_ENABLED: boolean
|
||||
@@ -119,10 +110,7 @@ export type FeatureFlags = {
|
||||
FF_OBJECTS_STREAMING_FIX: boolean
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: boolean
|
||||
FF_NO_PERSONAL_EMAILS_ENABLED: boolean
|
||||
}
|
||||
|
||||
export function getFeatureFlags(): FeatureFlags {
|
||||
//@ts-expect-error this way, the parse function typing is a lot better
|
||||
if (!parsedFlags) parsedFlags = parseFeatureFlags(process.env)
|
||||
} {
|
||||
if (!parsedFlags) parsedFlags = parseFeatureFlags()
|
||||
return parsedFlags
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ export * as RichTextEditor from './rich-text-editor/index.js'
|
||||
export * as SpeckleViewer from './viewer/index.js'
|
||||
// export * as Environment from './environment/index.js' // Import from @speckle/shared/dist/...
|
||||
export * as Automate from './automate/index.js'
|
||||
export * as Authz from './authz/index.js'
|
||||
export * from './core/index.js'
|
||||
export * from './workspaces/index.js'
|
||||
export * from './onboarding/index.js'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"target": "es2019" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
|
||||
@@ -16982,7 +16982,6 @@ __metadata:
|
||||
"@types/ua-parser-js": "npm:^0.7.39"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.12.0"
|
||||
"@typescript-eslint/parser": "npm:^7.12.0"
|
||||
crypto-random-string: "npm:^5.0.0"
|
||||
eslint: "npm:^9.4.0"
|
||||
eslint-config-prettier: "npm:^9.1.0"
|
||||
knex: "npm:^2.5.1"
|
||||
|
||||
Reference in New Issue
Block a user