Skip to content

Commit

Permalink
feat(core): Refactor how permissions get serialized for sessions into…
Browse files Browse the repository at this point in the history
… using a new strategy
  • Loading branch information
DanielBiegler committed Nov 20, 2024
1 parent 3777555 commit 2929b64
Show file tree
Hide file tree
Showing 16 changed files with 154 additions and 29 deletions.
9 changes: 5 additions & 4 deletions packages/core/src/api/resolvers/base/base-auth.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
AuthenticationResult as ShopAuthenticationResult,
PasswordValidationError,
AuthenticationResult as ShopAuthenticationResult,
} from '@vendure/common/lib/generated-shop-types';
import {
AuthenticationResult as AdminAuthenticationResult,
Expand All @@ -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';
Expand Down Expand Up @@ -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<CurrentUser> {
return {
id: user.id,
identifier: user.identifier,
channels: getUserChannelsPermissions(user) as CurrentUserChannel[],
channels: (await this.configService.authOptions.rolePermissionResolverStrategy.resolvePermissions(
user,
)) as CurrentUserChannel[],
};
}
}
Original file line number Diff line number Diff line change
@@ -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<UserChannelPermissions[]> {
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<UserChannelPermissions>(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;
}
}
Original file line number Diff line number Diff line change
@@ -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<UserChannelPermissions[]> {
return getChannelPermissions(user.roles);
}
}
10 changes: 10 additions & 0 deletions packages/core/src/config/auth/role-permission-resolver-strategy.ts
Original file line number Diff line number Diff line change
@@ -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<UserChannelPermissions[]>;
}
3 changes: 2 additions & 1 deletion packages/core/src/config/config.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -117,6 +117,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
sessionCacheStrategy,
passwordHashingStrategy,
passwordValidationStrategy,
rolePermissionResolverStrategy,
assetNamingStrategy,
assetPreviewStrategy,
assetStorageStrategy,
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -473,6 +474,7 @@ export interface AuthOptions {
* @default DefaultPasswordValidationStrategy
*/
passwordValidationStrategy?: PasswordValidationStrategy;
rolePermissionResolverStrategy?: RolePermissionResolverStrategy;
}

/**
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/entity/channel-role/channel-role.ts
Original file line number Diff line number Diff line change
@@ -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<ChannelRole>) {
super(input);
}

@Column(type => CustomChannelRoleFields)
customFields: CustomChannelRoleFields;

@ManyToOne(type => User)
user: User;

@ManyToOne(type => Channel)
channel: Channel;

@ManyToOne(type => Role)
role: Role;
}
1 change: 1 addition & 0 deletions packages/core/src/entity/custom-entity-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
2 changes: 2 additions & 0 deletions packages/core/src/entity/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -143,6 +144,7 @@ export const coreEntitiesMap = {
TaxCategory,
TaxRate,
User,
ChannelRole,
Seller,
Zone,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/service/services/channel-role.service.ts
Original file line number Diff line number Diff line change
@@ -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) {}
}
7 changes: 2 additions & 5 deletions packages/core/src/service/services/role.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
},
);

Expand Down
22 changes: 13 additions & 9 deletions packages/core/src/service/services/session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -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;

Expand All @@ -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 =
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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 {
Expand All @@ -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<CachedSession> {
const expiry =
Math.floor(new Date().getTime() / 1000) + this.configService.authOptions.sessionCacheTTL;
const serializedSession: CachedSession = {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
Loading

0 comments on commit 2929b64

Please sign in to comment.