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

Automatically discover available permissions #1706

Merged
merged 23 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0c0d506
Get availablePermissions from DiscoveryService
fraxachun Feb 15, 2024
0466ae6
Fix function call
fraxachun Feb 15, 2024
6c96981
Merge remote-tracking branch 'origin/main' into user-permissions-perm…
fraxachun Feb 19, 2024
7a19ebf
Merge remote-tracking branch 'origin/main' into user-permissions-perm…
fraxachun Feb 19, 2024
ae77efc
Merge remote-tracking branch 'origin/main' into user-permissions-perm…
fraxachun Feb 19, 2024
1689c72
Add changelog
fraxachun Feb 19, 2024
f3acebb
Merge branch 'main' into user-permissions-permission-discovery
fraxachun Feb 21, 2024
6605d91
Export comet-permissions
fraxachun Feb 21, 2024
7f043fe
Revert "Export comet-permissions"
fraxachun Feb 22, 2024
735b3de
Export comet-permissions
fraxachun Feb 22, 2024
90d477d
Update .changeset/angry-plums-retire.md
fraxachun Feb 22, 2024
1bca1ce
camelCase
fraxachun Feb 22, 2024
7efdfc7
deprecate
fraxachun Feb 22, 2024
a36b23a
Merge branch 'main' into user-permissions-permission-discovery
fraxachun Feb 22, 2024
3c0d453
Update packages/api/cms-api/src/user-permissions/user-permissions.typ…
fraxachun Feb 22, 2024
3e6b6bd
Update .changeset/angry-plums-retire.md
fraxachun Feb 22, 2024
7f8cd06
Demo: Remove product package dimensions (#1730)
Ben-Ho Feb 22, 2024
20a6a5d
Merge remote-tracking branch 'origin/main' into user-permissions-perm…
fraxachun Feb 22, 2024
894e174
Use cometPermissions in demo
fraxachun Feb 22, 2024
b333187
Revert "Use cometPermissions in demo"
fraxachun Feb 22, 2024
5b2a849
Remove cometPermissions-object
fraxachun Feb 22, 2024
aeda8f5
Remove deprecation
fraxachun Feb 22, 2024
d47a77b
Merge remote-tracking branch 'origin/main' into user-permissions-perm…
fraxachun Feb 22, 2024
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
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.controllersWithMetaAtKey<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
Loading