mirror of
https://github.com/docmost/docmost.git
synced 2025-11-15 17:21:49 +10:00
frontend permissions
* rework backend workspace permissions
This commit is contained in:
@ -47,7 +47,7 @@ export class AuthenticationExtension implements Extension {
|
||||
|
||||
const page = await this.pageRepo.findById(pageId);
|
||||
if (!page) {
|
||||
this.logger.warn(`Page not found: ${pageId}}`);
|
||||
this.logger.warn(`Page not found: ${pageId}`);
|
||||
throw new NotFoundException('Page not found');
|
||||
}
|
||||
|
||||
@ -59,13 +59,13 @@ export class AuthenticationExtension implements Extension {
|
||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||
|
||||
if (!userSpaceRole) {
|
||||
this.logger.warn(`User authorized to access page: ${pageId}}`);
|
||||
this.logger.warn(`User not authorized to access page: ${pageId}`);
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
if (userSpaceRole === SpaceRole.READER) {
|
||||
data.connection.readOnly = true;
|
||||
this.logger.warn(`User granted readonly access to page: ${pageId}}`);
|
||||
this.logger.debug(`User granted readonly access to page: ${pageId}`);
|
||||
}
|
||||
|
||||
this.logger.debug(`Authenticated user ${user.id} on page ${pageId}`);
|
||||
|
||||
@ -57,7 +57,7 @@ export class PersistenceExtension implements Extension {
|
||||
return ydoc;
|
||||
}
|
||||
|
||||
this.logger.debug(`creating fresh ydoc': ${pageId}`);
|
||||
this.logger.debug(`creating fresh ydoc: ${pageId}`);
|
||||
return new Y.Doc();
|
||||
}
|
||||
|
||||
|
||||
@ -33,13 +33,16 @@ import {
|
||||
MAX_AVATAR_SIZE,
|
||||
MAX_FILE_SIZE,
|
||||
} from './attachment.constants';
|
||||
import CaslAbilityFactory from '../casl/abilities/casl-ability.factory';
|
||||
import {
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
} from '../casl/interfaces/space-ability.type';
|
||||
import { Action } from '../casl/ability.action';
|
||||
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
|
||||
import {
|
||||
WorkspaceCaslAction,
|
||||
WorkspaceCaslSubject,
|
||||
} from '../casl/interfaces/workspace-ability.type';
|
||||
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
||||
|
||||
@Controller('attachments')
|
||||
export class AttachmentController {
|
||||
@ -48,7 +51,7 @@ export class AttachmentController {
|
||||
constructor(
|
||||
private readonly attachmentService: AttachmentService,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly caslAbility: CaslAbilityFactory,
|
||||
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
||||
private readonly spaceAbility: SpaceAbilityFactory,
|
||||
) {}
|
||||
|
||||
@ -155,8 +158,13 @@ export class AttachmentController {
|
||||
}
|
||||
|
||||
if (attachmentType === AttachmentType.WorkspaceLogo) {
|
||||
const ability = this.caslAbility.createForUser(user, workspace);
|
||||
if (ability.cannot(Action.Manage, 'Workspace')) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(
|
||||
WorkspaceCaslAction.Manage,
|
||||
WorkspaceCaslSubject.Settings,
|
||||
)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
AbilityBuilder,
|
||||
createMongoAbility,
|
||||
ExtractSubjectType,
|
||||
MongoAbility,
|
||||
} from '@casl/ability';
|
||||
import { Action } from '../ability.action';
|
||||
import { UserRole } from '../../../helpers/types/permission';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
|
||||
export type Subjects =
|
||||
| 'Workspace'
|
||||
| 'Space'
|
||||
| 'SpaceMember'
|
||||
| 'Group'
|
||||
| 'GroupUser'
|
||||
| 'Attachment'
|
||||
| 'Comment'
|
||||
| 'Page'
|
||||
| 'User'
|
||||
| 'WorkspaceUser'
|
||||
| 'all';
|
||||
export type AppAbility = MongoAbility<[Action, Subjects]>;
|
||||
|
||||
@Injectable()
|
||||
export default class CaslAbilityFactory {
|
||||
createForUser(user: User, workspace: Workspace) {
|
||||
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
||||
|
||||
const userRole = user.role;
|
||||
|
||||
if (userRole === UserRole.OWNER || userRole === UserRole.ADMIN) {
|
||||
// Workspace Users
|
||||
can([Action.Manage], 'Workspace');
|
||||
can([Action.Manage], 'WorkspaceUser');
|
||||
|
||||
// Groups
|
||||
can([Action.Manage], 'Group');
|
||||
can([Action.Manage], 'GroupUser');
|
||||
|
||||
// Attachments
|
||||
can([Action.Manage], 'Attachment');
|
||||
}
|
||||
|
||||
if (userRole === UserRole.MEMBER) {
|
||||
can([Action.Read], 'WorkspaceUser');
|
||||
|
||||
// Groups
|
||||
can([Action.Read], 'Group');
|
||||
can([Action.Read], 'GroupUser');
|
||||
|
||||
// Attachments
|
||||
can([Action.Read, Action.Create], 'Attachment');
|
||||
}
|
||||
|
||||
return build({
|
||||
detectSubjectType: (item) => item as ExtractSubjectType<Subjects>,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,7 @@ import { User } from '@docmost/db/types/entity.types';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
import {
|
||||
SpaceCaslAction,
|
||||
SpaceAbility,
|
||||
ISpaceAbility,
|
||||
SpaceCaslSubject,
|
||||
} from '../interfaces/space-ability.type';
|
||||
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
||||
@ -39,7 +39,7 @@ export default class SpaceAbilityFactory {
|
||||
}
|
||||
|
||||
function buildSpaceAdminAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<SpaceAbility>>(
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
|
||||
@ -49,7 +49,7 @@ function buildSpaceAdminAbility() {
|
||||
}
|
||||
|
||||
function buildSpaceWriterAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<SpaceAbility>>(
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||
@ -59,7 +59,7 @@ function buildSpaceWriterAbility() {
|
||||
}
|
||||
|
||||
function buildSpaceReaderAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<SpaceAbility>>(
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ISpaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
AbilityBuilder,
|
||||
createMongoAbility,
|
||||
MongoAbility,
|
||||
} from '@casl/ability';
|
||||
import { UserRole } from '../../../helpers/types/permission';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
import {
|
||||
IWorkspaceAbility,
|
||||
WorkspaceCaslAction,
|
||||
WorkspaceCaslSubject,
|
||||
} from '../interfaces/workspace-ability.type';
|
||||
|
||||
@Injectable()
|
||||
export default class WorkspaceAbilityFactory {
|
||||
createForUser(user: User, workspace: Workspace) {
|
||||
const userRole = user.role;
|
||||
|
||||
switch (userRole) {
|
||||
case UserRole.OWNER:
|
||||
return buildWorkspaceOwnerAbility();
|
||||
case UserRole.ADMIN:
|
||||
return buildWorkspaceAdminAbility();
|
||||
case UserRole.MEMBER:
|
||||
return buildWorkspaceMemberAbility();
|
||||
default:
|
||||
throw new NotFoundException('Workspace permissions not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkspaceOwnerAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<IWorkspaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Space);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Attachment);
|
||||
|
||||
return build();
|
||||
}
|
||||
|
||||
function buildWorkspaceAdminAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<IWorkspaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Space);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Attachment);
|
||||
|
||||
return build();
|
||||
}
|
||||
|
||||
function buildWorkspaceMemberAbility() {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<IWorkspaceAbility>>(
|
||||
createMongoAbility,
|
||||
);
|
||||
can(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Settings);
|
||||
can(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Member);
|
||||
can(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Space);
|
||||
can(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Group);
|
||||
can(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Attachment);
|
||||
|
||||
return build();
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export enum Action {
|
||||
Manage = 'manage',
|
||||
Create = 'create',
|
||||
Read = 'read',
|
||||
Update = 'update',
|
||||
Delete = 'delete',
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import CaslAbilityFactory from './abilities/casl-ability.factory';
|
||||
import SpaceAbilityFactory from './abilities/space-ability.factory';
|
||||
import WorkspaceAbilityFactory from './abilities/workspace-ability.factory';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [CaslAbilityFactory, SpaceAbilityFactory],
|
||||
exports: [CaslAbilityFactory, SpaceAbilityFactory],
|
||||
providers: [WorkspaceAbilityFactory, SpaceAbilityFactory],
|
||||
exports: [WorkspaceAbilityFactory, SpaceAbilityFactory],
|
||||
})
|
||||
export class CaslModule {}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import { PolicyHandler } from '../interfaces/policy-handler.interface';
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const CHECK_POLICIES_KEY = 'check_policy';
|
||||
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
|
||||
SetMetadata(CHECK_POLICIES_KEY, handlers);
|
||||
@ -1,40 +0,0 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import CaslAbilityFactory, {
|
||||
AppAbility,
|
||||
} from '../abilities/casl-ability.factory';
|
||||
import { PolicyHandler } from '../interfaces/policy-handler.interface';
|
||||
import { CHECK_POLICIES_KEY } from '../decorators/policies.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class PoliciesGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private caslAbilityFactory: CaslAbilityFactory,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const policyHandlers =
|
||||
this.reflector.get<PolicyHandler[]>(
|
||||
CHECK_POLICIES_KEY,
|
||||
context.getHandler(),
|
||||
) || [];
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user.user;
|
||||
const workspace = request.user.workspace;
|
||||
|
||||
const ability = this.caslAbilityFactory.createForUser(user, workspace);
|
||||
|
||||
return policyHandlers.every((handler) =>
|
||||
this.execPolicyHandler(handler, ability),
|
||||
);
|
||||
}
|
||||
|
||||
private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
|
||||
if (typeof handler === 'function') {
|
||||
return handler(ability);
|
||||
}
|
||||
return handler.handle(ability);
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { AppAbility } from '../abilities/casl-ability.factory';
|
||||
|
||||
interface IPolicyHandler {
|
||||
handle(ability: AppAbility): boolean;
|
||||
}
|
||||
|
||||
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
|
||||
|
||||
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
|
||||
@ -11,7 +11,7 @@ export enum SpaceCaslSubject {
|
||||
Page = 'page',
|
||||
}
|
||||
|
||||
export type SpaceAbility =
|
||||
export type ISpaceAbility =
|
||||
| [SpaceCaslAction, SpaceCaslSubject.Settings]
|
||||
| [SpaceCaslAction, SpaceCaslSubject.Member]
|
||||
| [SpaceCaslAction, SpaceCaslSubject.Page];
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
export enum WorkspaceCaslAction {
|
||||
Manage = 'manage',
|
||||
Create = 'create',
|
||||
Read = 'read',
|
||||
Edit = 'edit',
|
||||
Delete = 'delete',
|
||||
}
|
||||
export enum WorkspaceCaslSubject {
|
||||
Settings = 'settings',
|
||||
Member = 'member',
|
||||
Space = 'space',
|
||||
Group = 'group',
|
||||
Attachment = 'attachment',
|
||||
}
|
||||
|
||||
export type IWorkspaceAbility =
|
||||
| [WorkspaceCaslAction, WorkspaceCaslSubject.Settings]
|
||||
| [WorkspaceCaslAction, WorkspaceCaslSubject.Member]
|
||||
| [WorkspaceCaslAction, WorkspaceCaslSubject.Space]
|
||||
| [WorkspaceCaslAction, WorkspaceCaslSubject.Group]
|
||||
| [WorkspaceCaslAction, WorkspaceCaslSubject.Attachment];
|
||||
@ -5,6 +5,7 @@ import {
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { GroupService } from './services/group.service';
|
||||
import { CreateGroupDto } from './dto/create-group.dto';
|
||||
@ -16,12 +17,13 @@ import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
|
||||
import { AddGroupUserDto } from './dto/add-group-user.dto';
|
||||
import { RemoveGroupUserDto } from './dto/remove-group-user.dto';
|
||||
import { UpdateGroupDto } from './dto/update-group.dto';
|
||||
import { Action } from '../casl/ability.action';
|
||||
import { PoliciesGuard } from '../casl/guards/policies.guard';
|
||||
import { CheckPolicies } from '../casl/decorators/policies.decorator';
|
||||
import { AppAbility } from '../casl/abilities/casl-ability.factory';
|
||||
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
|
||||
import {
|
||||
WorkspaceCaslAction,
|
||||
WorkspaceCaslSubject,
|
||||
} from '../casl/interfaces/workspace-ability.type';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('groups')
|
||||
@ -29,10 +31,9 @@ export class GroupController {
|
||||
constructor(
|
||||
private readonly groupService: GroupService,
|
||||
private readonly groupUserService: GroupUserService,
|
||||
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
||||
) {}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'Group'))
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/')
|
||||
getWorkspaceGroups(
|
||||
@ -40,11 +41,14 @@ export class GroupController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (ability.cannot(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Group)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.groupService.getWorkspaceGroups(workspace.id, pagination);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'Group'))
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/info')
|
||||
getGroup(
|
||||
@ -52,11 +56,13 @@ export class GroupController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (ability.cannot(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Group)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.groupService.getGroupInfo(groupIdDto.groupId, workspace.id);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('create')
|
||||
createGroup(
|
||||
@ -64,11 +70,15 @@ export class GroupController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.groupService.createGroup(user, workspace.id, createGroupDto);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('update')
|
||||
updateGroup(
|
||||
@ -76,18 +86,29 @@ export class GroupController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.groupService.updateGroup(workspace.id, updateGroupDto);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, 'GroupUser'))
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members')
|
||||
getGroupMembers(
|
||||
@Body() groupIdDto: GroupIdDto,
|
||||
@Body() pagination: PaginationOptions,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (ability.cannot(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Group)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.groupUserService.getGroupUsers(
|
||||
groupIdDto.groupId,
|
||||
workspace.id,
|
||||
@ -95,10 +116,6 @@ export class GroupController {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'GroupUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members/add')
|
||||
addGroupMember(
|
||||
@ -106,6 +123,13 @@ export class GroupController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.groupUserService.addUsersToGroupBatch(
|
||||
addGroupUserDto.userIds,
|
||||
addGroupUserDto.groupId,
|
||||
@ -113,17 +137,20 @@ export class GroupController {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'GroupUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members/remove')
|
||||
removeGroupMember(
|
||||
@Body() removeGroupUserDto: RemoveGroupUserDto,
|
||||
//@AuthUser() user: User,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.groupUserService.removeUserFromGroup(
|
||||
removeGroupUserDto.userId,
|
||||
removeGroupUserDto.groupId,
|
||||
@ -131,8 +158,6 @@ export class GroupController {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can(Action.Manage, 'Group'))
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('delete')
|
||||
deleteGroup(
|
||||
@ -140,6 +165,12 @@ export class GroupController {
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Group)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return this.groupService.deleteGroup(groupIdDto.groupId, workspace.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +106,7 @@ export class PageController {
|
||||
}
|
||||
|
||||
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
|
||||
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
|
||||
if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
await this.pageService.forceDelete(pageIdDto.pageId);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateSpaceDto {
|
||||
@MinLength(4)
|
||||
@MinLength(2)
|
||||
@MaxLength(64)
|
||||
@IsString()
|
||||
name: string;
|
||||
@ -10,7 +10,7 @@ export class CreateSpaceDto {
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@MinLength(4)
|
||||
@MinLength(2)
|
||||
@MaxLength(64)
|
||||
@IsString()
|
||||
slug: string;
|
||||
|
||||
@ -48,10 +48,6 @@ export class SpaceService {
|
||||
updateSpaceDto: UpdateSpaceDto,
|
||||
workspaceId: string,
|
||||
): Promise<Space> {
|
||||
if (!updateSpaceDto.name && !updateSpaceDto.description) {
|
||||
throw new BadRequestException('Please provide fields to update');
|
||||
}
|
||||
|
||||
return await this.spaceRepo.updateSpace(
|
||||
{
|
||||
name: updateSpaceDto.name,
|
||||
|
||||
@ -26,6 +26,8 @@ import {
|
||||
SpaceCaslSubject,
|
||||
} from '../casl/interfaces/space-ability.type';
|
||||
import { UpdateSpaceDto } from './dto/update-space.dto';
|
||||
import { findHighestUserSpaceRole } from '@docmost/db/repos/space/utils';
|
||||
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('spaces')
|
||||
@ -33,6 +35,7 @@ export class SpaceController {
|
||||
constructor(
|
||||
private readonly spaceService: SpaceService,
|
||||
private readonly spaceMemberService: SpaceMemberService,
|
||||
private readonly spaceMemberRepo: SpaceMemberRepo,
|
||||
private readonly spaceAbility: SpaceAbilityFactory,
|
||||
) {}
|
||||
|
||||
@ -67,7 +70,20 @@ export class SpaceController {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return space;
|
||||
const userSpaceRoles = await this.spaceMemberRepo.getUserSpaceRoles(
|
||||
user.id,
|
||||
space.id,
|
||||
);
|
||||
|
||||
const userSpaceRole = findHighestUserSpaceRole(userSpaceRoles);
|
||||
|
||||
const membership = {
|
||||
userId: user.id,
|
||||
role: userSpaceRole,
|
||||
permissions: ability.rules,
|
||||
};
|
||||
|
||||
return { ...space, membership };
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
ForbiddenException,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
@ -21,12 +22,13 @@ import {
|
||||
InviteUserDto,
|
||||
RevokeInviteDto,
|
||||
} from '../dto/invitation.dto';
|
||||
import { Action } from '../../casl/ability.action';
|
||||
import { CheckPolicies } from '../../casl/decorators/policies.decorator';
|
||||
import { AppAbility } from '../../casl/abilities/casl-ability.factory';
|
||||
import { PoliciesGuard } from '../../casl/guards/policies.guard';
|
||||
import { JwtAuthGuard } from '../../../guards/jwt-auth.guard';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
import WorkspaceAbilityFactory from '../../casl/abilities/workspace-ability.factory';
|
||||
import {
|
||||
WorkspaceCaslAction,
|
||||
WorkspaceCaslSubject,
|
||||
} from '../../casl/interfaces/workspace-ability.type';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('workspace')
|
||||
@ -34,12 +36,13 @@ export class WorkspaceController {
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly workspaceInvitationService: WorkspaceInvitationService,
|
||||
private readonly workspaceAbility: WorkspaceAbilityFactory,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/public')
|
||||
async getWorkspacePublicInfo(@Req() req) {
|
||||
async getWorkspacePublicInfo(@Req() req: any) {
|
||||
return this.workspaceService.getWorkspacePublicData(req.raw.workspaceId);
|
||||
}
|
||||
|
||||
@ -49,72 +52,89 @@ export class WorkspaceController {
|
||||
return this.workspaceService.getWorkspaceInfo(workspace.id);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'Workspace'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('update')
|
||||
async updateWorkspace(
|
||||
@Body() updateWorkspaceDto: UpdateWorkspaceDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Settings)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceService.update(workspace.id, updateWorkspaceDto);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Read, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members')
|
||||
async getWorkspaceMembers(
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (ability.cannot(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Member)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceService.getWorkspaceUsers(workspace.id, pagination);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
// @CheckPolicies((ability: AppAbility) =>
|
||||
// ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
// )
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members/deactivate')
|
||||
async deactivateWorkspaceMember() {
|
||||
async deactivateWorkspaceMember(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceService.deactivateUser();
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('members/change-role')
|
||||
async updateWorkspaceMemberRole(
|
||||
@Body() workspaceUserRoleDto: UpdateWorkspaceUserRoleDto,
|
||||
@AuthUser() authUser: User,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceService.updateWorkspaceUserRole(
|
||||
authUser,
|
||||
user,
|
||||
workspaceUserRoleDto,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Read, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites')
|
||||
async getInvitations(
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Body()
|
||||
pagination: PaginationOptions,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (ability.cannot(WorkspaceCaslAction.Read, WorkspaceCaslSubject.Member)) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceInvitationService.getInvitations(
|
||||
workspace.id,
|
||||
pagination,
|
||||
@ -131,50 +151,61 @@ export class WorkspaceController {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites/create')
|
||||
async inviteUser(
|
||||
@Body() inviteUserDto: InviteUserDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@AuthUser() authUser: User,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceInvitationService.createInvitation(
|
||||
inviteUserDto,
|
||||
workspace.id,
|
||||
authUser,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites/resend')
|
||||
async resendInvite(
|
||||
@Body() revokeInviteDto: RevokeInviteDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceInvitationService.resendInvitation(
|
||||
revokeInviteDto.invitationId,
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) =>
|
||||
ability.can(Action.Manage, 'WorkspaceUser'),
|
||||
)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('invites/revoke')
|
||||
async revokeInvite(
|
||||
@Body() revokeInviteDto: RevokeInviteDto,
|
||||
@AuthUser() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
) {
|
||||
const ability = this.workspaceAbility.createForUser(user, workspace);
|
||||
if (
|
||||
ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member)
|
||||
) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return this.workspaceInvitationService.revokeInvitation(
|
||||
revokeInviteDto.invitationId,
|
||||
workspace.id,
|
||||
|
||||
Reference in New Issue
Block a user