From 2929b648bc98625c9e76d8c8e0fae2c64ee7c376 Mon Sep 17 00:00:00 2001 From: Daniel Biegler Date: Wed, 20 Nov 2024 23:23:10 +0100 Subject: [PATCH] feat(core): Refactor how permissions get serialized for sessions into using a new strategy --- .../api/resolvers/base/base-auth.resolver.ts | 9 +++-- ...annel-role-permission-resolver-strategy.ts | 39 +++++++++++++++++++ ...fault-role-permission-resolver-strategy.ts | 13 +++++++ .../auth/role-permission-resolver-strategy.ts | 10 +++++ packages/core/src/config/config.module.ts | 3 +- packages/core/src/config/default-config.ts | 6 ++- packages/core/src/config/vendure-config.ts | 2 + .../src/entity/channel-role/channel-role.ts | 34 ++++++++++++++++ .../core/src/entity/custom-entity-fields.ts | 1 + packages/core/src/entity/entities.ts | 2 + .../request-context.service.ts | 5 ++- .../utils/get-user-channels-permissions.ts | 1 + .../service/services/channel-role.service.ts | 16 ++++++++ .../core/src/service/services/role.service.ts | 7 +--- .../src/service/services/session.service.ts | 22 ++++++----- packages/dev-server/dev-config.ts | 13 +++---- 16 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/config/auth/channel-role-permission-resolver-strategy.ts create mode 100644 packages/core/src/config/auth/default-role-permission-resolver-strategy.ts create mode 100644 packages/core/src/config/auth/role-permission-resolver-strategy.ts create mode 100644 packages/core/src/entity/channel-role/channel-role.ts create mode 100644 packages/core/src/service/services/channel-role.service.ts diff --git a/packages/core/src/api/resolvers/base/base-auth.resolver.ts b/packages/core/src/api/resolvers/base/base-auth.resolver.ts index edbdc94bf0..5213c42b1a 100644 --- a/packages/core/src/api/resolvers/base/base-auth.resolver.ts +++ b/packages/core/src/api/resolvers/base/base-auth.resolver.ts @@ -1,6 +1,6 @@ import { - AuthenticationResult as ShopAuthenticationResult, PasswordValidationError, + AuthenticationResult as ShopAuthenticationResult, } from '@vendure/common/lib/generated-shop-types'; import { AuthenticationResult as AdminAuthenticationResult, @@ -22,7 +22,6 @@ import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentic import { ConfigService } from '../../../config/config.service'; import { LogLevel } from '../../../config/logger/vendure-logger'; import { User } from '../../../entity/user/user.entity'; -import { getUserChannelsPermissions } from '../../../service/helpers/utils/get-user-channels-permissions'; import { AdministratorService } from '../../../service/services/administrator.service'; import { AuthService } from '../../../service/services/auth.service'; import { UserService } from '../../../service/services/user.service'; @@ -143,11 +142,13 @@ export class BaseAuthResolver { /** * Exposes a subset of the User properties which we want to expose to the public API. */ - protected publiclyAccessibleUser(user: User): CurrentUser { + protected async publiclyAccessibleUser(user: User): Promise { return { id: user.id, identifier: user.identifier, - channels: getUserChannelsPermissions(user) as CurrentUserChannel[], + channels: (await this.configService.authOptions.rolePermissionResolverStrategy.resolvePermissions( + user, + )) as CurrentUserChannel[], }; } } diff --git a/packages/core/src/config/auth/channel-role-permission-resolver-strategy.ts b/packages/core/src/config/auth/channel-role-permission-resolver-strategy.ts new file mode 100644 index 0000000000..c219528bb6 --- /dev/null +++ b/packages/core/src/config/auth/channel-role-permission-resolver-strategy.ts @@ -0,0 +1,39 @@ +import { Injector } from '../../common'; +import { TransactionalConnection } from '../../connection'; +import { User } from '../../entity'; +import { ChannelRole } from '../../entity/channel-role/channel-role'; +import { UserChannelPermissions } from '../../service/helpers/utils/get-user-channels-permissions'; + +import { RolePermissionResolverStrategy } from './role-permission-resolver-strategy'; + +export class ChannelRolePermissionResolverStrategy implements RolePermissionResolverStrategy { + private connection: TransactionalConnection; + + async init(injector: Injector) { + this.connection = injector.get(TransactionalConnection); + } + + async resolvePermissions(user: User): Promise { + console.log('---- BEGIN RESOLVE'); + const channelRoleEntries = await this.connection.rawConnection.getRepository(ChannelRole).find({ + where: { user: { id: user.id } }, + relations: ['user', 'channel', 'role'], + }); + console.log('---- RESOLVE -- ENTRIES:', JSON.stringify(channelRoleEntries)); + + const channelRolePermissions = new Array(channelRoleEntries.length); + for (let i = 0; i < channelRoleEntries.length; i++) { + channelRolePermissions[i] = { + id: channelRoleEntries[i].channel.id, + token: channelRoleEntries[i].channel.token, + code: channelRoleEntries[i].channel.code, + permissions: channelRoleEntries[i].role.permissions, + }; + } + channelRoleEntries.sort((a, b) => (a.id < b.id ? -1 : 1)); + console.log('---- RESOLVE -- OUTPUT:', channelRolePermissions); + + console.log('---- END RESOLVE'); + return channelRolePermissions; + } +} diff --git a/packages/core/src/config/auth/default-role-permission-resolver-strategy.ts b/packages/core/src/config/auth/default-role-permission-resolver-strategy.ts new file mode 100644 index 0000000000..ef1cba3f4d --- /dev/null +++ b/packages/core/src/config/auth/default-role-permission-resolver-strategy.ts @@ -0,0 +1,13 @@ +import { User } from '../../entity'; +import { + getChannelPermissions, + UserChannelPermissions, +} from '../../service/helpers/utils/get-user-channels-permissions'; + +import { RolePermissionResolverStrategy } from './role-permission-resolver-strategy'; + +export class DefaultRolePermissionResolverStrategy implements RolePermissionResolverStrategy { + async resolvePermissions(user: User): Promise { + return getChannelPermissions(user.roles); + } +} diff --git a/packages/core/src/config/auth/role-permission-resolver-strategy.ts b/packages/core/src/config/auth/role-permission-resolver-strategy.ts new file mode 100644 index 0000000000..44636df234 --- /dev/null +++ b/packages/core/src/config/auth/role-permission-resolver-strategy.ts @@ -0,0 +1,10 @@ +import { InjectableStrategy } from '../../common'; +import { User } from '../../entity'; +import { UserChannelPermissions } from '../../service/helpers/utils/get-user-channels-permissions'; + +/** + * @description TODO + */ +export interface RolePermissionResolverStrategy extends InjectableStrategy { + resolvePermissions(user: User): Promise; +} diff --git a/packages/core/src/config/config.module.ts b/packages/core/src/config/config.module.ts index 26b9662268..d36e79ec7a 100644 --- a/packages/core/src/config/config.module.ts +++ b/packages/core/src/config/config.module.ts @@ -1,6 +1,5 @@ import { Module, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; import { ConfigurableOperationDef } from '../common/configurable-operation'; import { Injector } from '../common/injector'; @@ -83,6 +82,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo sessionCacheStrategy, passwordHashingStrategy, passwordValidationStrategy, + rolePermissionResolverStrategy, } = this.configService.authOptions; const { taxZoneStrategy, taxLineCalculationStrategy } = this.configService.taxOptions; const { jobQueueStrategy, jobBufferStorageStrategy } = this.configService.jobQueueOptions; @@ -117,6 +117,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo sessionCacheStrategy, passwordHashingStrategy, passwordValidationStrategy, + rolePermissionResolverStrategy, assetNamingStrategy, assetPreviewStrategy, assetStorageStrategy, diff --git a/packages/core/src/config/default-config.ts b/packages/core/src/config/default-config.ts index dfd9fe3e38..ebb083d45b 100644 --- a/packages/core/src/config/default-config.ts +++ b/packages/core/src/config/default-config.ts @@ -1,9 +1,9 @@ import { LanguageCode } from '@vendure/common/lib/generated-types'; import { DEFAULT_AUTH_TOKEN_HEADER_KEY, + DEFAULT_CHANNEL_TOKEN_KEY, SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD, - DEFAULT_CHANNEL_TOKEN_KEY, } from '@vendure/common/lib/shared-constants'; import { randomBytes } from 'crypto'; @@ -17,6 +17,7 @@ import { NoAssetPreviewStrategy } from './asset-preview-strategy/no-asset-previe import { NoAssetStorageStrategy } from './asset-storage-strategy/no-asset-storage-strategy'; import { BcryptPasswordHashingStrategy } from './auth/bcrypt-password-hashing-strategy'; import { DefaultPasswordValidationStrategy } from './auth/default-password-validation-strategy'; +import { DefaultRolePermissionResolverStrategy } from './auth/default-role-permission-resolver-strategy'; import { NativeAuthenticationStrategy } from './auth/native-authentication-strategy'; import { defaultCollectionFilters } from './catalog/default-collection-filters'; import { DefaultProductVariantPriceCalculationStrategy } from './catalog/default-product-variant-price-calculation-strategy'; @@ -109,6 +110,9 @@ export const defaultConfig: RuntimeVendureConfig = { customPermissions: [], passwordHashingStrategy: new BcryptPasswordHashingStrategy(), passwordValidationStrategy: new DefaultPasswordValidationStrategy({ minLength: 4 }), + rolePermissionResolverStrategy: new DefaultRolePermissionResolverStrategy(), + // TODO: remove once the weird type mismatch from dev-config gets fixed + // rolePermissionResolverStrategy: new ChannelRolePermissionResolverStrategy(), }, catalogOptions: { collectionFilters: defaultCollectionFilters, diff --git a/packages/core/src/config/vendure-config.ts b/packages/core/src/config/vendure-config.ts index 017dfce4c5..0a13f538f3 100644 --- a/packages/core/src/config/vendure-config.ts +++ b/packages/core/src/config/vendure-config.ts @@ -17,6 +17,7 @@ import { AssetStorageStrategy } from './asset-storage-strategy/asset-storage-str import { AuthenticationStrategy } from './auth/authentication-strategy'; import { PasswordHashingStrategy } from './auth/password-hashing-strategy'; import { PasswordValidationStrategy } from './auth/password-validation-strategy'; +import { RolePermissionResolverStrategy } from './auth/role-permission-resolver-strategy'; import { CollectionFilter } from './catalog/collection-filter'; import { ProductVariantPriceCalculationStrategy } from './catalog/product-variant-price-calculation-strategy'; import { ProductVariantPriceSelectionStrategy } from './catalog/product-variant-price-selection-strategy'; @@ -473,6 +474,7 @@ export interface AuthOptions { * @default DefaultPasswordValidationStrategy */ passwordValidationStrategy?: PasswordValidationStrategy; + rolePermissionResolverStrategy?: RolePermissionResolverStrategy; } /** diff --git a/packages/core/src/entity/channel-role/channel-role.ts b/packages/core/src/entity/channel-role/channel-role.ts new file mode 100644 index 0000000000..db10d10ced --- /dev/null +++ b/packages/core/src/entity/channel-role/channel-role.ts @@ -0,0 +1,34 @@ +import { DeepPartial } from '@vendure/common/lib/shared-types'; +import { Column, Entity, ManyToOne } from 'typeorm'; + +import { HasCustomFields } from '../../config'; +import { VendureEntity } from '../base/base.entity'; +import { Channel } from '../channel/channel.entity'; +import { CustomChannelRoleFields } from '../custom-entity-fields'; +import { Role } from '../role/role.entity'; +import { User } from '../user/user.entity'; + +/** + * @description + * TODO + * + * @docsCategory entities + */ +@Entity() +export class ChannelRole extends VendureEntity implements HasCustomFields { + constructor(input?: DeepPartial) { + super(input); + } + + @Column(type => CustomChannelRoleFields) + customFields: CustomChannelRoleFields; + + @ManyToOne(type => User) + user: User; + + @ManyToOne(type => Channel) + channel: Channel; + + @ManyToOne(type => Role) + role: Role; +} diff --git a/packages/core/src/entity/custom-entity-fields.ts b/packages/core/src/entity/custom-entity-fields.ts index 298368199e..80aeb9b1eb 100644 --- a/packages/core/src/entity/custom-entity-fields.ts +++ b/packages/core/src/entity/custom-entity-fields.ts @@ -35,5 +35,6 @@ export class CustomShippingMethodFieldsTranslation {} export class CustomStockLocationFields {} export class CustomTaxCategoryFields {} export class CustomTaxRateFields {} +export class CustomChannelRoleFields {} export class CustomUserFields {} export class CustomZoneFields {} diff --git a/packages/core/src/entity/entities.ts b/packages/core/src/entity/entities.ts index 54e1c245f3..55e903c89e 100644 --- a/packages/core/src/entity/entities.ts +++ b/packages/core/src/entity/entities.ts @@ -5,6 +5,7 @@ import { AuthenticationMethod } from './authentication-method/authentication-met import { ExternalAuthenticationMethod } from './authentication-method/external-authentication-method.entity'; import { NativeAuthenticationMethod } from './authentication-method/native-authentication-method.entity'; import { Channel } from './channel/channel.entity'; +import { ChannelRole } from './channel-role/channel-role'; import { CollectionAsset } from './collection/collection-asset.entity'; import { CollectionTranslation } from './collection/collection-translation.entity'; import { Collection } from './collection/collection.entity'; @@ -143,6 +144,7 @@ export const coreEntitiesMap = { TaxCategory, TaxRate, User, + ChannelRole, Seller, Zone, }; diff --git a/packages/core/src/service/helpers/request-context/request-context.service.ts b/packages/core/src/service/helpers/request-context/request-context.service.ts index f87c5216a2..a3b357870d 100644 --- a/packages/core/src/service/helpers/request-context/request-context.service.ts +++ b/packages/core/src/service/helpers/request-context/request-context.service.ts @@ -14,7 +14,6 @@ import { CachedSession, CachedSessionUser } from '../../../config/session-cache/ import { Channel } from '../../../entity/channel/channel.entity'; import { User } from '../../../entity/user/user.entity'; import { ChannelService } from '../../services/channel.service'; -import { getUserChannelsPermissions } from '../utils/get-user-channels-permissions'; /** * @description @@ -58,7 +57,9 @@ export class RequestContextService { } let session: CachedSession | undefined; if (user) { - const channelPermissions = user.roles ? getUserChannelsPermissions(user) : []; + const channelPermissions = user.roles + ? await this.configService.authOptions.rolePermissionResolverStrategy.resolvePermissions(user) + : []; session = { user: { id: user.id, diff --git a/packages/core/src/service/helpers/utils/get-user-channels-permissions.ts b/packages/core/src/service/helpers/utils/get-user-channels-permissions.ts index 748719f8e8..c8727fa170 100644 --- a/packages/core/src/service/helpers/utils/get-user-channels-permissions.ts +++ b/packages/core/src/service/helpers/utils/get-user-channels-permissions.ts @@ -14,6 +14,7 @@ export interface UserChannelPermissions { /** * Returns an array of Channels and permissions on those Channels for the given User. + * @deprecated See `RolePermissionResolverStrategy` */ export function getUserChannelsPermissions(user: User): UserChannelPermissions[] { return getChannelPermissions(user.roles); diff --git a/packages/core/src/service/services/channel-role.service.ts b/packages/core/src/service/services/channel-role.service.ts new file mode 100644 index 0000000000..a52068a600 --- /dev/null +++ b/packages/core/src/service/services/channel-role.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; + +import { TransactionalConnection } from '../../connection'; + +/** + * @description + * Contains methods relating to {@link ChannelRole} entities. + * + * @todo TODO + * + * @docsCategory services + */ +@Injectable() +export class ChannelRoleService { + constructor(private connection: TransactionalConnection) {} +} diff --git a/packages/core/src/service/services/role.service.ts b/packages/core/src/service/services/role.service.ts index f551de8bdb..f88841ce5b 100644 --- a/packages/core/src/service/services/role.service.ts +++ b/packages/core/src/service/services/role.service.ts @@ -35,10 +35,7 @@ import { User } from '../../entity/user/user.entity'; import { EventBus } from '../../event-bus'; import { RoleEvent } from '../../event-bus/events/role-event'; import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder'; -import { - getChannelPermissions, - getUserChannelsPermissions, -} from '../helpers/utils/get-user-channels-permissions'; +import { getChannelPermissions } from '../helpers/utils/get-user-channels-permissions'; import { patchEntity } from '../helpers/utils/patch-entity'; import { ChannelService } from './channel.service'; @@ -222,7 +219,7 @@ export class RoleService { const user = await this.connection.getEntityOrThrow(ctx, User, activeUserId, { relations: ['roles', 'roles.channels'], }); - return getUserChannelsPermissions(user); + return this.configService.authOptions.rolePermissionResolverStrategy.resolvePermissions(user); }, ); diff --git a/packages/core/src/service/services/session.service.ts b/packages/core/src/service/services/session.service.ts index 13c4568329..1c9f1c16f7 100644 --- a/packages/core/src/service/services/session.service.ts +++ b/packages/core/src/service/services/session.service.ts @@ -5,6 +5,7 @@ import ms from 'ms'; import { EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm'; import { RequestContext } from '../../api/common/request-context'; +import { RolePermissionResolverStrategy } from '../../config/auth/role-permission-resolver-strategy'; import { ConfigService } from '../../config/config.service'; import { CachedSession, SessionCacheStrategy } from '../../config/session-cache/session-cache-strategy'; import { TransactionalConnection } from '../../connection/transactional-connection'; @@ -15,7 +16,6 @@ import { AnonymousSession } from '../../entity/session/anonymous-session.entity' import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity'; import { Session } from '../../entity/session/session.entity'; import { User } from '../../entity/user/user.entity'; -import { getUserChannelsPermissions } from '../helpers/utils/get-user-channels-permissions'; import { OrderService } from './order.service'; @@ -28,6 +28,7 @@ import { OrderService } from './order.service'; @Injectable() export class SessionService implements EntitySubscriberInterface { private sessionCacheStrategy: SessionCacheStrategy; + private rolePermissionResolverStrategy: RolePermissionResolverStrategy; private readonly sessionDurationInMs: number; private readonly sessionCacheTimeoutMs = 50; @@ -37,6 +38,7 @@ export class SessionService implements EntitySubscriberInterface { private orderService: OrderService, ) { this.sessionCacheStrategy = this.configService.authOptions.sessionCacheStrategy; + this.rolePermissionResolverStrategy = this.configService.authOptions.rolePermissionResolverStrategy; const { sessionDuration } = this.configService.authOptions; this.sessionDurationInMs = @@ -101,7 +103,9 @@ export class SessionService implements EntitySubscriberInterface { invalidated: false, }), ); - await this.withTimeout(this.sessionCacheStrategy.set(this.serializeSession(authenticatedSession))); + await this.withTimeout( + this.sessionCacheStrategy.set(await this.serializeSession(authenticatedSession)), + ); return authenticatedSession; } @@ -119,7 +123,7 @@ export class SessionService implements EntitySubscriberInterface { }); // save the new session const newSession = await this.connection.rawConnection.getRepository(AnonymousSession).save(session); - const serializedSession = this.serializeSession(newSession); + const serializedSession = await this.serializeSession(newSession); await this.withTimeout(this.sessionCacheStrategy.set(serializedSession)); return serializedSession; } @@ -135,7 +139,7 @@ export class SessionService implements EntitySubscriberInterface { if (!serializedSession || stale || expired) { const session = await this.findSessionByToken(sessionToken); if (session) { - serializedSession = this.serializeSession(session); + serializedSession = await this.serializeSession(session); await this.withTimeout(this.sessionCacheStrategy.set(serializedSession)); return serializedSession; } else { @@ -149,7 +153,7 @@ export class SessionService implements EntitySubscriberInterface { * @description * Serializes a {@link Session} instance into a simplified plain object suitable for caching. */ - serializeSession(session: AuthenticatedSession | AnonymousSession): CachedSession { + async serializeSession(session: AuthenticatedSession | AnonymousSession): Promise { const expiry = Math.floor(new Date().getTime() / 1000) + this.configService.authOptions.sessionCacheTTL; const serializedSession: CachedSession = { @@ -167,7 +171,7 @@ export class SessionService implements EntitySubscriberInterface { id: user.id, identifier: user.identifier, verified: user.verified, - channelPermissions: getUserChannelsPermissions(user), + channelPermissions: await this.rolePermissionResolverStrategy.resolvePermissions(user), }; } return serializedSession; @@ -222,7 +226,7 @@ export class SessionService implements EntitySubscriberInterface { if (session) { session.activeOrder = order; await this.connection.getRepository(ctx, Session).save(session, { reload: false }); - const updatedSerializedSession = this.serializeSession(session); + const updatedSerializedSession = await this.serializeSession(session); await this.withTimeout(this.sessionCacheStrategy.set(updatedSerializedSession)); return updatedSerializedSession; } @@ -242,7 +246,7 @@ export class SessionService implements EntitySubscriberInterface { if (session) { session.activeOrder = null; await this.connection.getRepository(ctx, Session).save(session); - const updatedSerializedSession = this.serializeSession(session); + const updatedSerializedSession = await this.serializeSession(session); await this.configService.authOptions.sessionCacheStrategy.set(updatedSerializedSession); return updatedSerializedSession; } @@ -262,7 +266,7 @@ export class SessionService implements EntitySubscriberInterface { if (session) { session.activeChannel = channel; await this.connection.rawConnection.getRepository(Session).save(session, { reload: false }); - const updatedSerializedSession = this.serializeSession(session); + const updatedSerializedSession = await this.serializeSession(session); await this.withTimeout(this.sessionCacheStrategy.set(updatedSerializedSession)); return updatedSerializedSession; } diff --git a/packages/dev-server/dev-config.ts b/packages/dev-server/dev-config.ts index 68824177de..4bd3436b9b 100644 --- a/packages/dev-server/dev-config.ts +++ b/packages/dev-server/dev-config.ts @@ -9,20 +9,14 @@ import { DefaultSearchPlugin, dummyPaymentHandler, FacetValue, - LanguageCode, LogLevel, VendureConfig, } from '@vendure/core'; -import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin'; import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin'; -import { BullMQJobQueuePlugin } from '@vendure/job-queue-plugin/package/bullmq'; import 'dotenv/config'; -import { compileUiExtensions } from '@vendure/ui-devkit/compiler'; import path from 'path'; import { DataSourceOptions } from 'typeorm'; -import { MultivendorPlugin } from './example-plugins/multivendor-plugin/multivendor.plugin'; - /** * Config settings used during development */ @@ -52,6 +46,10 @@ export const devConfig: VendureConfig = { cookieOptions: { secret: 'abc', }, + // TODO THIS DOESNT WORK? TYPE MISMATCH?? + // but it works in default config... huhhhh?? why + // For now if you wanna test, change the default-config.ts + // rolePermissionResolverStrategy: new ChannelRolePermissionResolverStrategy(), }, dbConnectionOptions: { synchronize: false, @@ -79,7 +77,8 @@ export const devConfig: VendureConfig = { }, ], }, - logger: new DefaultLogger({ level: LogLevel.Info }), + // TODO remove if this feature is closer to merging + logger: new DefaultLogger({ level: LogLevel.Debug }), importExportOptions: { importAssetsDir: path.join(__dirname, 'import-assets'), },