diff --git a/packages/server/modules/core/authz.ts b/packages/server/modules/core/authz.ts deleted file mode 100644 index f417a345c..000000000 --- a/packages/server/modules/core/authz.ts +++ /dev/null @@ -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 - } - }) -} diff --git a/packages/server/modules/core/graph/resolvers/projects.ts b/packages/server/modules/core/graph/resolvers/projects.ts index 4c53ae66b..33be6040d 100644 --- a/packages/server/modules/core/graph/resolvers/projects.ts +++ b/packages/server/modules/core/graph/resolvers/projects.ts @@ -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: { diff --git a/packages/server/modules/core/index.ts b/packages/server/modules/core/index.ts index 0eace522d..8f68f4620 100644 --- a/packages/server/modules/core/index.ts +++ b/packages/server/modules/core/index.ts @@ -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() diff --git a/packages/server/modules/core/roles.ts b/packages/server/modules/core/roles.ts index 3d8696154..df5c3c872 100644 --- a/packages/server/modules/core/roles.ts +++ b/packages/server/modules/core/roles.ts @@ -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 = [ /** @@ -18,16 +15,19 @@ const coreUserRoles: Array = [ */ { 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 = [ // 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 = [ */ { 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 } ] diff --git a/packages/server/modules/index.ts b/packages/server/modules/index.ts index 37d28b4d6..61b6593b9 100644 --- a/packages/server/modules/index.ts +++ b/packages/server/modules/index.ts @@ -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 } diff --git a/packages/server/modules/loaders.ts b/packages/server/modules/loaders.ts deleted file mode 100644 index 4390f416a..000000000 --- a/packages/server/modules/loaders.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { LoaderConfigurationError } from '@/modules/shared/errors' -import { Authz } from '@speckle/shared' - -let cachedLoaders: Partial = {} - -const loaderKeys: (keyof Authz.AuthCheckContextLoaders)[] = [ - 'getEnv', - 'getProject', - 'getProjectRole', - 'getServerRole', - 'getWorkspace', - 'getWorkspaceRole', - 'getWorkspaceSsoProvider', - 'getWorkspaceSsoSession' -] - -export const defineLoaders = ( - loaders: Partial -): 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 -): 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 -} diff --git a/packages/server/modules/shared/errors/index.ts b/packages/server/modules/shared/errors/index.ts index 24e25a002..576d6a21a 100644 --- a/packages/server/modules/shared/errors/index.ts +++ b/packages/server/modules/shared/errors/index.ts @@ -140,22 +140,5 @@ export class DatabaseError 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 } diff --git a/packages/server/modules/shared/helpers/typeHelper.ts b/packages/server/modules/shared/helpers/typeHelper.ts index 27b0a180e..ddb234032 100644 --- a/packages/server/modules/shared/helpers/typeHelper.ts +++ b/packages/server/modules/shared/helpers/typeHelper.ts @@ -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 = Record { - 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 - } - }) -} diff --git a/packages/server/modules/workspaces/index.ts b/packages/server/modules/workspaces/index.ts index ea8b69fcb..05cc72fb2 100644 --- a/packages/server/modules/workspaces/index.ts +++ b/packages/server/modules/workspaces/index.ts @@ -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 diff --git a/packages/server/modules/workspaces/roles.ts b/packages/server/modules/workspaces/roles.ts index df43e865a..b0d07c273 100644 --- a/packages/server/modules/workspaces/roles.ts +++ b/packages/server/modules/workspaces/roles.ts @@ -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 diff --git a/packages/server/modules/workspaces/tests/integration/sso.graph.spec.ts b/packages/server/modules/workspaces/tests/integration/sso.graph.spec.ts index d456414ad..28df025de 100644 --- a/packages/server/modules/workspaces/tests/integration/sso.graph.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/sso.graph.spec.ts @@ -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 () => { diff --git a/packages/server/modules/workspacesCore/authz.ts b/packages/server/modules/workspacesCore/authz.ts deleted file mode 100644 index de435a3d0..000000000 --- a/packages/server/modules/workspacesCore/authz.ts +++ /dev/null @@ -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() - } - }) -} diff --git a/packages/server/modules/workspacesCore/index.ts b/packages/server/modules/workspacesCore/index.ts index 7f55f6749..e1ddc7ded 100644 --- a/packages/server/modules/workspacesCore/index.ts +++ b/packages/server/modules/workspacesCore/index.ts @@ -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() } diff --git a/packages/server/test/graphqlHelper.ts b/packages/server/test/graphqlHelper.ts index 4183a4e97..aabaf89f5 100644 --- a/packages/server/test/graphqlHelper.ts +++ b/packages/server/test/graphqlHelper.ts @@ -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> = GraphQLResponse @@ -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 || {}) }) diff --git a/packages/shared/package.json b/packages/shared/package.json index 11c7de9fa..dd937efe3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -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", diff --git a/packages/shared/src/authz/checks/projects.spec.ts b/packages/shared/src/authz/checks/projects.spec.ts deleted file mode 100644 index 4630cd93d..000000000 --- a/packages/shared/src/authz/checks/projects.spec.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/shared/src/authz/checks/projects.ts b/packages/shared/src/authz/checks/projects.ts deleted file mode 100644 index d4fe905da..000000000 --- a/packages/shared/src/authz/checks/projects.ts +++ /dev/null @@ -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 => { - 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 => { - const { userId, projectId, role: requiredProjectRole } = args - - const userProjectRole = await loaders.getProjectRole({ userId, projectId }) - return userProjectRole - ? isMinimumProjectRole(userProjectRole, requiredProjectRole) - : false - } diff --git a/packages/shared/src/authz/checks/serverRole.spec.ts b/packages/shared/src/authz/checks/serverRole.spec.ts deleted file mode 100644 index d4fed5699..000000000 --- a/packages/shared/src/authz/checks/serverRole.spec.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/shared/src/authz/checks/serverRole.ts b/packages/shared/src/authz/checks/serverRole.ts deleted file mode 100644 index 06236917d..000000000 --- a/packages/shared/src/authz/checks/serverRole.ts +++ /dev/null @@ -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 => { - const { userId, role: requiredServerRole } = args - - const userServerRole = await loaders.getServerRole({ userId }) - - return userServerRole === requiredServerRole - } diff --git a/packages/shared/src/authz/checks/workspaceRole.spec.ts b/packages/shared/src/authz/checks/workspaceRole.spec.ts deleted file mode 100644 index 9d1a0e1a5..000000000 --- a/packages/shared/src/authz/checks/workspaceRole.spec.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/shared/src/authz/checks/workspaceRole.ts b/packages/shared/src/authz/checks/workspaceRole.ts deleted file mode 100644 index 770311853..000000000 --- a/packages/shared/src/authz/checks/workspaceRole.ts +++ /dev/null @@ -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 => { - 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 => { - const { userId, workspaceId, role: requiredWorkspaceRole } = args - - const userWorkspaceRole = await loaders.getWorkspaceRole({ userId, workspaceId }) - - return userWorkspaceRole - ? isMinimumWorkspaceRole(userWorkspaceRole, requiredWorkspaceRole) - : false - } diff --git a/packages/shared/src/authz/checks/workspaceSso.spec.ts b/packages/shared/src/authz/checks/workspaceSso.spec.ts deleted file mode 100644 index e07d9d075..000000000 --- a/packages/shared/src/authz/checks/workspaceSso.spec.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/shared/src/authz/checks/workspaceSso.ts b/packages/shared/src/authz/checks/workspaceSso.ts deleted file mode 100644 index 4ed74274c..000000000 --- a/packages/shared/src/authz/checks/workspaceSso.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AuthCheckContext } from '../domain/loaders.js' - -export const requireValidWorkspaceSsoSession = - ({ loaders }: AuthCheckContext<'getWorkspaceSsoSession'>) => - async (args: { userId: string; workspaceId: string }): Promise => { - const { userId, workspaceId } = args - - const workspaceSsoSession = await loaders.getWorkspaceSsoSession({ - userId, - workspaceId - }) - - const isExpiredSession = - new Date().getTime() > (workspaceSsoSession?.validUntil?.getTime() ?? 0) - - return !!workspaceSsoSession && !isExpiredSession - } diff --git a/packages/shared/src/authz/domain/authResult.ts b/packages/shared/src/authz/domain/authResult.ts deleted file mode 100644 index 4fd888b08..000000000 --- a/packages/shared/src/authz/domain/authResult.ts +++ /dev/null @@ -1,19 +0,0 @@ -type AuthSuccess = { - authorized: true -} - -export type AuthFailure = { - authorized: false - error: T -} - -export type AuthResult = AuthSuccess | AuthFailure - -export const authorized = (): AuthSuccess => ({ - authorized: true -}) - -export const unauthorized = (error: T): AuthFailure => ({ - authorized: false, - error -}) diff --git a/packages/shared/src/authz/domain/core/operations.ts b/packages/shared/src/authz/domain/core/operations.ts deleted file mode 100644 index a44bb0b85..000000000 --- a/packages/shared/src/authz/domain/core/operations.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ServerRoles } from '../../../core/constants.js' - -export type GetServerRole = (args: { userId: string }) => Promise diff --git a/packages/shared/src/authz/domain/errors.ts b/packages/shared/src/authz/domain/errors.ts deleted file mode 100644 index 155ffb547..000000000 --- a/packages/shared/src/authz/domain/errors.ts +++ /dev/null @@ -1,36 +0,0 @@ -type AuthError = { - code: ErrorCode - message: string -} - -export const defineAuthError = (params: { - code: ErrorCode - message: string -}): AuthError => { - 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' -}) diff --git a/packages/shared/src/authz/domain/loaders.ts b/packages/shared/src/authz/domain/loaders.ts deleted file mode 100644 index 048baa755..000000000 --- a/packages/shared/src/authz/domain/loaders.ts +++ /dev/null @@ -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 = { - loaders: Pick -} - -export type AuthCheckContextLoaders = { - getEnv: GetEnv - getProject: GetProject - getProjectRole: GetProjectRole - getServerRole: GetServerRole - getWorkspace: GetWorkspace - getWorkspaceRole: GetWorkspaceRole - getWorkspaceSsoProvider: GetWorkspaceSsoProvider - getWorkspaceSsoSession: GetWorkspaceSsoSession -} diff --git a/packages/shared/src/authz/domain/policies.ts b/packages/shared/src/authz/domain/policies.ts deleted file mode 100644 index e666f1580..000000000 --- a/packages/shared/src/authz/domain/policies.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type ProjectContext = { projectId: string } - -export type UserContext = { userId?: string } diff --git a/packages/shared/src/authz/domain/projects/logic.spec.ts b/packages/shared/src/authz/domain/projects/logic.spec.ts deleted file mode 100644 index 742a95c88..000000000 --- a/packages/shared/src/authz/domain/projects/logic.spec.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/packages/shared/src/authz/domain/projects/logic.ts b/packages/shared/src/authz/domain/projects/logic.ts deleted file mode 100644 index f185918de..000000000 --- a/packages/shared/src/authz/domain/projects/logic.ts +++ /dev/null @@ -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 -} diff --git a/packages/shared/src/authz/domain/projects/operations.ts b/packages/shared/src/authz/domain/projects/operations.ts deleted file mode 100644 index d7b65a5e7..000000000 --- a/packages/shared/src/authz/domain/projects/operations.ts +++ /dev/null @@ -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 - -export type GetProjectRole = (args: { - userId: string - projectId: string -}) => Promise diff --git a/packages/shared/src/authz/domain/projects/types.ts b/packages/shared/src/authz/domain/projects/types.ts deleted file mode 100644 index 728eb1072..000000000 --- a/packages/shared/src/authz/domain/projects/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Project = { - // TODO: Deprecated field? - isDiscoverable: boolean - isPublic: boolean - workspaceId: string | null -} - -export type ProjectVisibility = 'public' | 'linkShareable' | 'private' diff --git a/packages/shared/src/authz/domain/workspaces/logic.spec.ts b/packages/shared/src/authz/domain/workspaces/logic.spec.ts deleted file mode 100644 index 78f359248..000000000 --- a/packages/shared/src/authz/domain/workspaces/logic.spec.ts +++ /dev/null @@ -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) - }) - }) -}) diff --git a/packages/shared/src/authz/domain/workspaces/logic.ts b/packages/shared/src/authz/domain/workspaces/logic.ts deleted file mode 100644 index db7f90718..000000000 --- a/packages/shared/src/authz/domain/workspaces/logic.ts +++ /dev/null @@ -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 -} diff --git a/packages/shared/src/authz/domain/workspaces/operations.ts b/packages/shared/src/authz/domain/workspaces/operations.ts deleted file mode 100644 index 60db00294..000000000 --- a/packages/shared/src/authz/domain/workspaces/operations.ts +++ /dev/null @@ -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 - -export type GetWorkspaceRole = (args: { - userId: string - workspaceId: string -}) => Promise - -export type GetWorkspaceSsoProvider = (args: { - workspaceId: string -}) => Promise - -export type GetWorkspaceSsoSession = (args: { - userId: string - workspaceId: string -}) => Promise - -export type GetEnv = () => FeatureFlags diff --git a/packages/shared/src/authz/domain/workspaces/types.ts b/packages/shared/src/authz/domain/workspaces/types.ts deleted file mode 100644 index 88a1ec882..000000000 --- a/packages/shared/src/authz/domain/workspaces/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type Workspace = { - id: string -} - -export type WorkspaceSsoProvider = { - providerId: string -} - -export type WorkspaceSsoSession = { - userId: string - providerId: string - validUntil: Date -} diff --git a/packages/shared/src/authz/index.ts b/packages/shared/src/authz/index.ts deleted file mode 100644 index 304b575da..000000000 --- a/packages/shared/src/authz/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { authPoliciesFactory, AuthPolices } from './policies/index.js' -export { AuthCheckContextLoaders } from './domain/loaders.js' - -export * from './domain/errors.js' diff --git a/packages/shared/src/authz/policies/canQueryProject.spec.ts b/packages/shared/src/authz/policies/canQueryProject.spec.ts deleted file mode 100644 index 09e8b0104..000000000 --- a/packages/shared/src/authz/policies/canQueryProject.spec.ts +++ /dev/null @@ -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 = - >(defaults: T) => - (overrides?: Partial) => - (): Promise => { - if (overrides) { - return Promise.resolve(merge(defaults, overrides)) - } - return Promise.resolve(defaults) - } - -const getProjectFake = fakeGetFactory({ - 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) - }) - }) -}) diff --git a/packages/shared/src/authz/policies/canQueryProject.ts b/packages/shared/src/authz/policies/canQueryProject.ts deleted file mode 100644 index 6583e0299..000000000 --- a/packages/shared/src/authz/policies/canQueryProject.ts +++ /dev/null @@ -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) - } diff --git a/packages/shared/src/authz/policies/index.ts b/packages/shared/src/authz/policies/index.ts deleted file mode 100644 index d8b7fdec5..000000000 --- a/packages/shared/src/authz/policies/index.ts +++ /dev/null @@ -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 diff --git a/packages/shared/src/core/constants.ts b/packages/shared/src/core/constants.ts index ef18bc4ee..3145e5fba 100644 --- a/packages/shared/src/core/constants.ts +++ b/packages/shared/src/core/constants.ts @@ -28,57 +28,47 @@ export const RoleInfo = Object.freeze({ 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" } } }) diff --git a/packages/shared/src/environment/index.ts b/packages/shared/src/environment/index.ts index 548beaf38..27edff8ac 100644 --- a/packages/shared/src/environment/index.ts +++ b/packages/shared/src/environment/index.ts @@ -6,18 +6,10 @@ const isDisableAllFFsMode = () => const isEnableAllFFsMode = () => ['true', '1'].includes(process.env.ENABLE_ALL_FFS || '') -export const parseFeatureFlags = ( - input: // | Record - Partial> -): 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 | 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 } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 30a862992..f71d37f29 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -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' diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 3e9aa1069..1966179de 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -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. */ diff --git a/yarn.lock b/yarn.lock index 64f87649e..3461c6d12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"