Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Refactor how permissions get serialized for sessions into using a new strategy #3222

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
DanielBiegler marked this conversation as resolved.
Show resolved Hide resolved
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));
DanielBiegler marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
 * @description
 * A RolePermissionResolverStrategy defines how role-based permissions for a user should be resolved.
 * This strategy is used to determine the permissions assigned to a user based on their roles per channel.
 *
 * By default {@link DefaultRolePermissionResolverStrategy} is used. However, for more complex environments using
 * multiple channels and roles {@link ChannelRolePermissionResolverStrategy} is recommended.
 * 
 * :::info
 *
 * This is configured via the `authOptions.rolePermissionResolverStrategy` properties of your VendureConfig.
 *
 * :::
 *
 * @docsCategory auth
 * @since 3.3.0
 */

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, thanks. 👍 Will add comments once we finalize the PR and everything is set.

*/
export interface RolePermissionResolverStrategy extends InjectableStrategy {
resolvePermissions(user: User): Promise<UserChannelPermissions[]>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a bit nitpicky, but more consistent with the rest of the codebase: saveUserRoles and getPermissionsForUser, instead of resolve/persists?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the channelIds: ID[] seems pretty crucial for this PoC to work? But the PR is still in draft, right?

}
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)
DanielBiegler marked this conversation as resolved.
Show resolved Hide resolved
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`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to deprecate this first? This helper isn't exported, so I would say it's ok to just mention in the changelog that you should use configService.authOptions.rolePermissionResolverStrategy.resolvePermissions from now on.

*/
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
Loading