Skip to content

Commit

Permalink
Get availablePermissions from DiscoveryService
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxachun committed Feb 15, 2024
1 parent f416510 commit 0c0d506
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 118 deletions.
1 change: 0 additions & 1 deletion demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ export class AppModule {
AuthModule,
UserPermissionsModule.forRootAsync({
useFactory: (userService: UserService, accessControlService: AccessControlService) => ({
availablePermissions: ["news", "products"],
availableContentScopes: [
{ domain: "main", language: "de" },
{ domain: "main", language: "en" },
Expand Down
8 changes: 0 additions & 8 deletions demo/api/src/auth/permission.interface.ts

This file was deleted.

18 changes: 3 additions & 15 deletions docs/docs/migration/migration-from-v5-to-v6.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,7 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES

It is not possible anymore to use a custom CurrentUserLoader neither to augment/use the CurrentUserInterface.

3. Create interface for `availablePermissions` similar to the already existing interface `interface ContentScope`

```ts
declare module "@comet/cms-api" {
interface Permission {
// e.g. `products: string;`
}
}
export {};
```

4. Create necessary services for the `UserPermissionsModule` (either in a new module or where it fits best)
3. Create necessary services for the `UserPermissionsModule` (either in a new module or where it fits best)

```ts
// Attention: might already being provided by the library which syncs the users
Expand All @@ -113,7 +102,7 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES
}
```

5. Replace `ContentScopeModule` with `UserPermissionsModule`
4. Replace `ContentScopeModule` with `UserPermissionsModule`

Remove `ContentScopeModule`:

Expand All @@ -128,7 +117,6 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES
```ts
UserPermissionsModule.forRootAsync({
useFactory: (userService: UserService, accessControlService: AccessControlService) => ({
availablePermissions: [/* Array of strings defined in interface Permission */],
availableContentScopes: [/* Array of content Scopes */],
userService,
accessControlService,
Expand All @@ -138,7 +126,7 @@ It automatically installs the new versions of all `@comet` libraries, runs an ES
}),
```

6. Adapt decorators
5. Adapt decorators

Add `@RequiredPermission` to resolvers and controllers

Expand Down
1 change: 1 addition & 0 deletions packages/api/cms-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"@comet/blocks-api": "workspace:^6.0.0",
"@golevelup/nestjs-discovery": "^4.0.0",
"@hapi/accept": "^5.0.2",
"@nestjs/jwt": "^9.0.0",
"@nestjs/mapped-types": "^1.2.2",
Expand Down
1 change: 0 additions & 1 deletion packages/api/cms-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export { CurrentUser } from "./user-permissions/dto/current-user";
export { FindUsersArgs } from "./user-permissions/dto/paginated-user-list";
export { User } from "./user-permissions/dto/user";
export { ContentScope } from "./user-permissions/interfaces/content-scope.interface";
export { Permission } from "./user-permissions/interfaces/user-permission.interface";
export { UserPermissionsModule } from "./user-permissions/user-permissions.module";
export {
AccessControlServiceInterface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { Injectable } from "@nestjs/common";

import { CurrentUser } from "./dto/current-user";
import { ContentScope } from "./interfaces/content-scope.interface";
import { Permission } from "./interfaces/user-permission.interface";
import { AccessControlServiceInterface } from "./user-permissions.types";

@Injectable()
export abstract class AbstractAccessControlService implements AccessControlServiceInterface {
private checkContentScope(userContentScopes: ContentScope[], contentScope: ContentScope): boolean {
return userContentScopes.some((cs) => Object.entries(contentScope).every(([scope, value]) => cs[scope] === value));
}
isAllowed(user: CurrentUser, permission: keyof Permission, contentScope?: ContentScope): boolean {
isAllowed(user: CurrentUser, permission: string, contentScope?: ContentScope): boolean {
if (!user.permissions) return false;
return user.permissions.some((p) => p.permission === permission && (!contentScope || this.checkContentScope(p.contentScopes, contentScope)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ export class UserPermissionsGuard implements CanActivate {
}
}

const requiredPermissions = Array.isArray(requiredPermission.requiredPermission)
? requiredPermission.requiredPermission
: [requiredPermission.requiredPermission];
const requiredPermissions = requiredPermission.requiredPermission;
if (requiredPermissions.length === 0) {
throw new Error(`RequiredPermission decorator has empty permissions in ${context.getClass().name}::${context.getHandler().name}()`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { CustomDecorator, SetMetadata } from "@nestjs/common";

import { Permission } from "../interfaces/user-permission.interface";

type RequiredPermissionOptions = {
skipScopeCheck?: boolean;
};

export type RequiredPermissionMetadata = {
requiredPermission: (keyof Permission)[] | keyof Permission;
requiredPermission: string[];
options: RequiredPermissionOptions | undefined;
};

export const RequiredPermission = (
requiredPermission: (keyof Permission)[] | keyof Permission,
options?: RequiredPermissionOptions,
): CustomDecorator<string> => {
return SetMetadata<string, RequiredPermissionMetadata>("requiredPermission", { requiredPermission, options });
export const RequiredPermission = (requiredPermission: string | string[], options?: RequiredPermissionOptions): CustomDecorator<string> => {
return SetMetadata<string, RequiredPermissionMetadata>("requiredPermission", {
requiredPermission: Array.isArray(requiredPermission) ? requiredPermission : [requiredPermission],
options,
});
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DiscoveryModule } from "@golevelup/nestjs-discovery";
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { DynamicModule, Global, Module, Provider } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
Expand All @@ -20,7 +21,7 @@ import {

@Global()
@Module({
imports: [MikroOrmModule.forFeature([UserPermission, UserContentScopes])],
imports: [MikroOrmModule.forFeature([UserPermission, UserContentScopes]), DiscoveryModule],
providers: [
UserPermissionsService,
UserResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { DiscoveryService } from "@golevelup/nestjs-discovery";
import { EntityRepository } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { Inject, Injectable } from "@nestjs/common";
import { isFuture, isPast } from "date-fns";
import isEqual from "lodash.isequal";
import getUuid from "uuid-by-string";

import { RequiredPermissionMetadata } from "./decorators/required-permission.decorator";
import { CurrentUser } from "./dto/current-user";
import { FindUsersArgs } from "./dto/paginated-user-list";
import { User } from "./dto/user";
import { UserContentScopes } from "./entities/user-content-scopes.entity";
import { UserPermission, UserPermissionSource } from "./entities/user-permission.entity";
import { ContentScope } from "./interfaces/content-scope.interface";
import { Permission } from "./interfaces/user-permission.interface";
import { ACCESS_CONTROL_SERVICE, USER_PERMISSIONS_OPTIONS, USER_PERMISSIONS_USER_SERVICE } from "./user-permissions.constants";
import {
AccessControlServiceInterface,
Expand All @@ -28,15 +29,25 @@ export class UserPermissionsService {
@Inject(ACCESS_CONTROL_SERVICE) private readonly accessControlService: AccessControlServiceInterface,
@InjectRepository(UserPermission) private readonly permissionRepository: EntityRepository<UserPermission>,
@InjectRepository(UserContentScopes) private readonly contentScopeRepository: EntityRepository<UserContentScopes>,
private readonly discoveryService: DiscoveryService,
) {}

async getAvailableContentScopes(): Promise<ContentScope[]> {
return this.options.availableContentScopes ?? [];
}

async getAvailablePermissions(): Promise<(keyof Permission)[]> {
async getAvailablePermissions(): Promise<string[]> {
return [
...new Set<keyof Permission>(["dam", "pageTree", "userPermissions", "cronJobs", "builds", ...(this.options.availablePermissions ?? [])]),
...new Set(
[
...(await this.discoveryService.providerMethodsWithMetaAtKey<RequiredPermissionMetadata>("requiredPermission")),
...(await this.discoveryService.providersWithMetaAtKey<RequiredPermissionMetadata>("requiredPermission")),
...(await this.discoveryService.controllerMethodsWithMetaAtKey<RequiredPermissionMetadata>("requiredPermission")),
...(await this.discoveryService.controllerMethodsWithMetaAtKey<RequiredPermissionMetadata>("requiredPermission")),
]
.flatMap((p) => p.meta.requiredPermission)
.sort(),
),
];
}

Expand Down Expand Up @@ -88,10 +99,7 @@ export class UserPermissionsService {

return permissions
.filter((value) => availablePermissions.some((p) => p === value.permission)) // Filter out permissions that are not defined in availablePermissions (e.g. outdated database entries)
.sort(
(a, b) =>
availablePermissions.indexOf(a.permission as keyof Permission) - availablePermissions.indexOf(b.permission as keyof Permission),
);
.sort((a, b) => availablePermissions.indexOf(a.permission) - availablePermissions.indexOf(b.permission));
}

async getContentScopes(userId: string, includeContentScopesManual = true): Promise<ContentScope[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { FindUsersArgs } from "./dto/paginated-user-list";
import { User } from "./dto/user";
import { UserPermission } from "./entities/user-permission.entity";
import { ContentScope } from "./interfaces/content-scope.interface";
import { Permission } from "./interfaces/user-permission.interface";

export enum UserPermissions {
allContentScopes = "all-content-scopes",
Expand All @@ -15,15 +14,15 @@ export enum UserPermissions {
export type Users = [User[], number];

type PermissionForUser = {
permission: keyof Permission;
permission: string;
contentScopes?: ContentScope[];
} & Pick<UserPermission, "validFrom" | "validTo" | "reason" | "requestedBy" | "approvedBy">;
export type PermissionsForUser = PermissionForUser[] | UserPermissions.allPermissions;

export type ContentScopesForUser = ContentScope[] | UserPermissions.allContentScopes;

export interface AccessControlServiceInterface {
isAllowed(user: CurrentUser, permission: keyof Permission, contentScope?: ContentScope): boolean;
isAllowed(user: CurrentUser, permission: string, contentScope?: ContentScope): boolean;
getPermissionsForUser?: (user: User) => Promise<PermissionsForUser> | PermissionsForUser;
getContentScopesForUser?: (user: User) => Promise<ContentScopesForUser> | ContentScopesForUser;
}
Expand All @@ -34,7 +33,6 @@ export interface UserPermissionsUserServiceInterface {
}

export interface UserPermissionsOptions {
availablePermissions?: (keyof Permission)[];
availableContentScopes?: ContentScope[];
}
export interface UserPermissionsModuleSyncOptions extends UserPermissionsOptions {
Expand Down
Loading

0 comments on commit 0c0d506

Please sign in to comment.