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 all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export class DynamicFormInputComponent
({
id: this.listId++,
control: new UntypedFormControl(getConfigArgValue(value)),
} as InputListItem),
}) as InputListItem,
);
this.renderList$.next();
}
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/api/resolvers/base/base-auth.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {
AuthenticationResult as ShopAuthenticationResult,
PasswordValidationError,
AuthenticationResult as ShopAuthenticationResult,
} from '@vendure/common/lib/generated-shop-types';
import {
AuthenticationResult as AdminAuthenticationResult,
CurrentUser,
CurrentUserChannel,
MutationAuthenticateArgs,
MutationLoginArgs,
Success,
Expand All @@ -22,7 +21,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 +141,12 @@ 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),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CreateAdministratorInput } from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';

import { RequestContext } from '../../api';
import { EntityNotFoundError, Injector } from '../../common';
import { TransactionalConnection } from '../../connection';
import { Role, User } from '../../entity';
import {
getChannelPermissions,
UserChannelPermissions,
} from '../../service/helpers/utils/get-user-channels-permissions';

import { ChannelRoleInput, RolePermissionResolverStrategy } from './role-permission-resolver-strategy';

export class DefaultRolePermissionResolverStrategy implements RolePermissionResolverStrategy {
private connection: TransactionalConnection;

async init(injector: Injector) {
this.connection = injector.get(TransactionalConnection);
}

async getChannelIdsFromCreateAdministratorInput(ctx: RequestContext, input: CreateAdministratorInput) {
const roles = await this.getRolesFromIds(ctx, input.roleIds);
const channelRoles = [];
for (const role of roles) {
for (const channel of role.channels) {
channelRoles.push({ roleId: role.id, channelId: channel.id });
}
}
return channelRoles;
}

/**
* @description TODO
*/
async persistUserAndTheirRoles(
ctx: RequestContext,
user: User,
channelRoles: ChannelRoleInput[],
): Promise<void> {
const roleIds = channelRoles.map(channelRole => channelRole.roleId);
const roles = await this.getRolesFromIds(ctx, roleIds);
// Copy so as to not mutate the original user object when setting roles
const userCopy = new User({ ...user, roles });
await this.connection.getRepository(ctx, User).save(userCopy, { reload: false });
}

private async getRolesFromIds(ctx: RequestContext, roleIds: ID[]): Promise<Role[]> {
const roles =
// Empty array is important because that would return every row when used in the query
roleIds.length === 0
? []
: await this.connection.getRepository(ctx, Role).findBy(roleIds.map(id => ({ id })));
for (const roleId of roleIds) {
const foundRole = roles.find(role => role.id === roleId);
if (!foundRole) throw new EntityNotFoundError('Role', roleId);
}
return roles;
}

async resolvePermissions(user: User): Promise<UserChannelPermissions[]> {
return getChannelPermissions(user.roles);
}
}
33 changes: 33 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,33 @@
import { CreateAdministratorInput, UpdateAdministratorInput } from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';

import { RequestContext } from '../../api';
import { InjectableStrategy } from '../../common';
import { User } from '../../entity';
import { UserChannelPermissions } from '../../service/helpers/utils/get-user-channels-permissions';

export type ChannelRoleInput = { channelId: ID; roleId: ID };

/**
* @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 {
getChannelIdsFromCreateAdministratorInput<T extends CreateAdministratorInput | UpdateAdministratorInput>(
ctx: RequestContext,
input: T,
): Promise<ChannelRoleInput[]>;

/**
* TODO
*/
persistUserAndTheirRoles(
ctx: RequestContext,
user: User,
channelRoles: ChannelRoleInput[],
): Promise<void>;

/**
* @param user User for which you want to retrieve permissions
*/
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
4 changes: 3 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,7 @@ export const defaultConfig: RuntimeVendureConfig = {
customPermissions: [],
passwordHashingStrategy: new BcryptPasswordHashingStrategy(),
passwordValidationStrategy: new DefaultPasswordValidationStrategy({ minLength: 4 }),
rolePermissionResolverStrategy: new DefaultRolePermissionResolverStrategy(),
},
catalogOptions: {
collectionFilters: defaultCollectionFilters,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export * from './auth/default-password-validation-strategy';
export * from './auth/native-authentication-strategy';
export * from './auth/password-hashing-strategy';
export * from './auth/password-validation-strategy';
export * from './auth/role-permission-resolver-strategy';
export * from '../plugin/channel-role-plugin/config/channel-role-permission-resolver-strategy';
export * from './auth/default-role-permission-resolver-strategy';
export * from './catalog/collection-filter';
export * from './catalog/default-collection-filters';
export * from './catalog/default-product-variant-price-selection-strategy';
Expand Down
8 changes: 8 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,13 @@ export interface AuthOptions {
* @default DefaultPasswordValidationStrategy
*/
passwordValidationStrategy?: PasswordValidationStrategy;
/**
* @todo TODO
* @description TODO By default, it uses the {@link DefaultRolePermissionResolverStrategy}, which TODO
* @since TODO
* @default DefaultRolePermissionResolverStrategy
*/
rolePermissionResolverStrategy?: RolePermissionResolverStrategy;
}

/**
Expand Down
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 '../plugin/channel-role-plugin/entities/channel-role.entity';
import { CollectionAsset } from './collection/collection-asset.entity';
import { CollectionTranslation } from './collection/collection-translation.entity';
import { Collection } from './collection/collection.entity';
Expand Down Expand Up @@ -83,6 +84,7 @@ export const coreEntitiesMap = {
AuthenticationMethod,
Cancellation,
Channel,
ChannelRole,
Collection,
CollectionAsset,
CollectionTranslation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import gql from 'graphql-tag';
import { VendurePlugin } from '../vendure-plugin';
import { ChannelRolePermissionResolverStrategy } from './config/channel-role-permission-resolver-strategy';
import { ChannelRole } from './entities/channel-role.entity';
import { ChannelRoleService } from './services/channel-role.service';

@VendurePlugin({
entities: [ChannelRole],
providers: [ChannelRoleService],
configuration: config => {
config.authOptions.rolePermissionResolverStrategy = new ChannelRolePermissionResolverStrategy();
return config;
},
adminApiExtensions: {
schema: gql`
input ChannelRoleInput {
roleId: ID!
channelId: ID!
}
extend input CreateAdministratorInput {
channelIds: [ChannelRoleInput!]!
}
`,
},
})
export class ChannelRolePlugin {}
Loading
Loading