mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-23 02:06:29 -05:00
Merge pull request #777 from hedgedoc/feat/permissionchecking
This commit is contained in:
commit
c6bc0dc85c
18 changed files with 953 additions and 147 deletions
|
@ -119,6 +119,11 @@ entity "note_group_permission" {
|
|||
*canEdit : boolean
|
||||
}
|
||||
|
||||
entity "group_members_user" {
|
||||
*group : number <<FK group>>
|
||||
*member : uuid <<FK user>>
|
||||
}
|
||||
|
||||
entity "tag" {
|
||||
*id: number <<generated>>
|
||||
*name: text
|
||||
|
@ -144,16 +149,16 @@ entity "history_entry" {
|
|||
|
||||
user "1" -- "0..*" note: owner
|
||||
user "1" -u- "1..*" identity
|
||||
user "1" - "1..*" auth_token: authTokens
|
||||
user "1" -l- "1..*" session
|
||||
user "1" - "0..*" media_upload
|
||||
user "1" -l- "1..*" auth_token: authTokens
|
||||
user "1" -r- "1..*" session
|
||||
user "1" -- "0..*" media_upload
|
||||
user "1" - "0..*" history_entry
|
||||
user "0..*" -- "0..*" note
|
||||
user "1" - "0..*" authorship
|
||||
user "1" -- "0..*" authorship
|
||||
|
||||
(user, note) . author_colors
|
||||
|
||||
revision "0..*" - "0..*" authorship
|
||||
revision "0..*" -- "0..*" authorship
|
||||
(revision, authorship) .. revision_authorship
|
||||
|
||||
media_upload "0..*" -- "1" note
|
||||
|
@ -161,9 +166,11 @@ note "1" - "1..*" revision
|
|||
note "1" - "0..*" history_entry
|
||||
note "0..*" -l- "0..*" tag
|
||||
note "0..*" -- "0..*" group
|
||||
user "1..*" -- "0..*" group
|
||||
|
||||
user "0..*" -- "0..*" note
|
||||
(user, note) . note_user_permission
|
||||
(note, group) . note_group_permission
|
||||
(user, group) . group_members_user
|
||||
|
||||
@enduml
|
||||
|
|
|
@ -20,6 +20,8 @@ import { User } from '../../../users/user.entity';
|
|||
import { UsersModule } from '../../../users/users.module';
|
||||
import { MeController } from './me.controller';
|
||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
||||
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||
|
||||
describe('Me Controller', () => {
|
||||
let controller: MeController;
|
||||
|
@ -47,6 +49,10 @@ describe('Me Controller', () => {
|
|||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(HistoryEntry))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
|
||||
controller = module.get<MeController>(MeController);
|
||||
|
|
|
@ -22,6 +22,8 @@ import { AuthToken } from '../../../auth/auth-token.entity';
|
|||
import { Identity } from '../../../users/identity.entity';
|
||||
import { User } from '../../../users/user.entity';
|
||||
import { MediaController } from './media.controller';
|
||||
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||
|
||||
describe('Media Controller', () => {
|
||||
let controller: MediaController;
|
||||
|
@ -57,6 +59,10 @@ describe('Media Controller', () => {
|
|||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
|
||||
controller = module.get<MediaController>(MediaController);
|
||||
|
|
|
@ -19,8 +19,11 @@ import { Identity } from '../../../users/identity.entity';
|
|||
import { User } from '../../../users/user.entity';
|
||||
import { UsersModule } from '../../../users/users.module';
|
||||
import { NotesController } from './notes.controller';
|
||||
import { PermissionsModule } from '../../../permissions/permissions.module';
|
||||
import { HistoryModule } from '../../../history/history.module';
|
||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
||||
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||
|
||||
describe('Notes Controller', () => {
|
||||
let controller: NotesController;
|
||||
|
@ -39,7 +42,13 @@ describe('Notes Controller', () => {
|
|||
useValue: {},
|
||||
},
|
||||
],
|
||||
imports: [RevisionsModule, UsersModule, LoggerModule, HistoryModule],
|
||||
imports: [
|
||||
RevisionsModule,
|
||||
UsersModule,
|
||||
LoggerModule,
|
||||
PermissionsModule,
|
||||
HistoryModule,
|
||||
],
|
||||
})
|
||||
.overrideProvider(getRepositoryToken(Note))
|
||||
.useValue({})
|
||||
|
@ -61,6 +70,10 @@ describe('Notes Controller', () => {
|
|||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(HistoryEntry))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
|
||||
controller = module.get<NotesController>(NotesController);
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
Post,
|
||||
Put,
|
||||
Request,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { NotInDBError } from '../../../errors/errors';
|
||||
|
@ -33,6 +34,8 @@ import { NoteDto } from '../../../notes/note.dto';
|
|||
import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
|
||||
import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto';
|
||||
import { RevisionDto } from '../../../revisions/revision.dto';
|
||||
import { PermissionsService } from '../../../permissions/permissions.service';
|
||||
import { Note } from '../../../notes/note.entity';
|
||||
|
||||
@ApiTags('notes')
|
||||
@ApiSecurity('token')
|
||||
|
@ -42,6 +45,7 @@ export class NotesController {
|
|||
private readonly logger: ConsoleLoggerService,
|
||||
private noteService: NotesService,
|
||||
private revisionsService: RevisionsService,
|
||||
private permissionsService: PermissionsService,
|
||||
private historyService: HistoryService,
|
||||
) {
|
||||
this.logger.setContext(NotesController.name);
|
||||
|
@ -54,7 +58,10 @@ export class NotesController {
|
|||
@MarkdownBody() text: string,
|
||||
): Promise<NoteDto> {
|
||||
// ToDo: provide user for createNoteDto
|
||||
this.logger.debug('Got raw markdown:\n' + text, 'createNote');
|
||||
if (!this.permissionsService.mayCreate(req.user)) {
|
||||
throw new UnauthorizedException('Creating note denied!');
|
||||
}
|
||||
this.logger.debug('Got raw markdown:\n' + text);
|
||||
return this.noteService.toNoteDto(
|
||||
await this.noteService.createNote(text, undefined, req.user),
|
||||
);
|
||||
|
@ -62,18 +69,24 @@ export class NotesController {
|
|||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
@Get(':noteIdOrAlias')
|
||||
async getNote(@Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string) {
|
||||
// ToDo: check if user is allowed to view this note
|
||||
async getNote(
|
||||
@Request() req,
|
||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
): Promise<NoteDto> {
|
||||
let note: Note;
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
await this.historyService.createOrUpdateHistoryEntry(note, req.user);
|
||||
return this.noteService.toNoteDto(note);
|
||||
note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
} catch (e) {
|
||||
if (e instanceof NotInDBError) {
|
||||
throw new NotFoundException(e.message);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (!this.permissionsService.mayRead(req.user, note)) {
|
||||
throw new UnauthorizedException('Reading note denied!');
|
||||
}
|
||||
await this.historyService.createOrUpdateHistoryEntry(note, req.user);
|
||||
return this.noteService.toNoteDto(note);
|
||||
}
|
||||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
|
@ -83,7 +96,9 @@ export class NotesController {
|
|||
@Param('noteAlias') noteAlias: string,
|
||||
@MarkdownBody() text: string,
|
||||
): Promise<NoteDto> {
|
||||
// ToDo: check if user is allowed to view this note
|
||||
if (!this.permissionsService.mayCreate(req.user)) {
|
||||
throw new UnauthorizedException('Creating note denied!');
|
||||
}
|
||||
this.logger.debug('Got raw markdown:\n' + text, 'createNamedNote');
|
||||
return this.noteService.toNoteDto(
|
||||
await this.noteService.createNote(text, noteAlias, req.user),
|
||||
|
@ -96,18 +111,21 @@ export class NotesController {
|
|||
@Request() req,
|
||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
): Promise<void> {
|
||||
// ToDo: check if user is allowed to delete this note
|
||||
this.logger.debug('Deleting note: ' + noteIdOrAlias, 'deleteNote');
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.isOwner(req.user, note)) {
|
||||
throw new UnauthorizedException('Deleting note denied!');
|
||||
}
|
||||
this.logger.debug('Deleting note: ' + noteIdOrAlias, 'deleteNote');
|
||||
await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias);
|
||||
this.logger.debug('Successfully deleted ' + noteIdOrAlias, 'deleteNote');
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e instanceof NotInDBError) {
|
||||
throw new NotFoundException(e.message);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this.logger.debug('Successfully deleted ' + noteIdOrAlias, 'deleteNote');
|
||||
return;
|
||||
}
|
||||
|
||||
@UseGuards(TokenAuthGuard)
|
||||
|
@ -117,9 +135,12 @@ export class NotesController {
|
|||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
@MarkdownBody() text: string,
|
||||
): Promise<NoteDto> {
|
||||
// ToDo: check if user is allowed to change this note
|
||||
this.logger.debug('Got raw markdown:\n' + text, 'updateNote');
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.mayWrite(req.user, note)) {
|
||||
throw new UnauthorizedException('Updating note denied!');
|
||||
}
|
||||
this.logger.debug('Got raw markdown:\n' + text, 'updateNote');
|
||||
return this.noteService.toNoteDto(
|
||||
await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text),
|
||||
);
|
||||
|
@ -138,8 +159,11 @@ export class NotesController {
|
|||
@Request() req,
|
||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
): Promise<string> {
|
||||
// ToDo: check if user is allowed to view this notes content
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.mayRead(req.user, note)) {
|
||||
throw new UnauthorizedException('Reading note denied!');
|
||||
}
|
||||
return await this.noteService.getNoteContent(noteIdOrAlias);
|
||||
} catch (e) {
|
||||
if (e instanceof NotInDBError) {
|
||||
|
@ -155,8 +179,11 @@ export class NotesController {
|
|||
@Request() req,
|
||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
): Promise<NoteMetadataDto> {
|
||||
// ToDo: check if user is allowed to view this notes metadata
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.mayRead(req.user, note)) {
|
||||
throw new UnauthorizedException('Reading note denied!');
|
||||
}
|
||||
return this.noteService.toNoteMetadataDto(
|
||||
await this.noteService.getNoteByIdOrAlias(noteIdOrAlias),
|
||||
);
|
||||
|
@ -175,8 +202,11 @@ export class NotesController {
|
|||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
@Body() updateDto: NotePermissionsUpdateDto,
|
||||
): Promise<NotePermissionsDto> {
|
||||
// ToDo: check if user is allowed to view this notes permissions
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.isOwner(req.user, note)) {
|
||||
throw new UnauthorizedException('Updating note denied!');
|
||||
}
|
||||
return this.noteService.toNotePermissionsDto(
|
||||
await this.noteService.updateNotePermissions(noteIdOrAlias, updateDto),
|
||||
);
|
||||
|
@ -194,8 +224,11 @@ export class NotesController {
|
|||
@Request() req,
|
||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
): Promise<RevisionMetadataDto[]> {
|
||||
// ToDo: check if user is allowed to view this notes revisions
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.mayRead(req.user, note)) {
|
||||
throw new UnauthorizedException('Reading note denied!');
|
||||
}
|
||||
const revisions = await this.revisionsService.getAllRevisions(
|
||||
noteIdOrAlias,
|
||||
);
|
||||
|
@ -219,8 +252,11 @@ export class NotesController {
|
|||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||
@Param('revisionId') revisionId: number,
|
||||
): Promise<RevisionDto> {
|
||||
// ToDo: check if user is allowed to view this notes revision
|
||||
try {
|
||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||
if (!this.permissionsService.mayRead(req.user, note)) {
|
||||
throw new UnauthorizedException('Reading note denied!');
|
||||
}
|
||||
return this.revisionsService.toRevisionDto(
|
||||
await this.revisionsService.getRevision(noteIdOrAlias, revisionId),
|
||||
);
|
||||
|
|
|
@ -16,6 +16,7 @@ import { MeController } from './me/me.controller';
|
|||
import { NotesController } from './notes/notes.controller';
|
||||
import { MediaController } from './media/media.controller';
|
||||
import { MonitoringController } from './monitoring/monitoring.controller';
|
||||
import { PermissionsModule } from '../../permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -26,6 +27,7 @@ import { MonitoringController } from './monitoring/monitoring.controller';
|
|||
MonitoringModule,
|
||||
LoggerModule,
|
||||
MediaModule,
|
||||
PermissionsModule,
|
||||
],
|
||||
controllers: [
|
||||
MeController,
|
||||
|
|
|
@ -4,7 +4,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../users/user.entity';
|
||||
|
||||
@Entity()
|
||||
export class Group {
|
||||
|
@ -26,4 +33,11 @@ export class Group {
|
|||
*/
|
||||
@Column()
|
||||
special: boolean;
|
||||
|
||||
@ManyToMany((_) => User, (user) => user.groups, {
|
||||
eager: true,
|
||||
cascade: true,
|
||||
})
|
||||
@JoinTable()
|
||||
members: User[];
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import { AuthToken } from '../auth/auth-token.entity';
|
|||
import { Revision } from '../revisions/revision.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotInDBError } from '../errors/errors';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
|
||||
describe('HistoryService', () => {
|
||||
let service: HistoryService;
|
||||
|
@ -54,6 +56,10 @@ describe('HistoryService', () => {
|
|||
.useClass(Repository)
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
|
||||
service = module.get<HistoryService>(HistoryService);
|
||||
|
@ -99,7 +105,7 @@ describe('HistoryService', () => {
|
|||
describe('createOrUpdateHistoryEntry', () => {
|
||||
describe('works', () => {
|
||||
it('without an preexisting entry', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||
jest
|
||||
|
@ -118,7 +124,7 @@ describe('HistoryService', () => {
|
|||
});
|
||||
|
||||
it('with an preexisting entry', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
const historyEntry = HistoryEntry.create(
|
||||
user,
|
||||
|
@ -148,7 +154,7 @@ describe('HistoryService', () => {
|
|||
describe('updateHistoryEntry', () => {
|
||||
describe('works', () => {
|
||||
it('with an entry', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
const note = Note.create(user, alias);
|
||||
const historyEntry = HistoryEntry.create(user, note);
|
||||
|
@ -173,7 +179,7 @@ describe('HistoryService', () => {
|
|||
});
|
||||
|
||||
it('without an entry', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
const note = Note.create(user, alias);
|
||||
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||
|
@ -192,7 +198,7 @@ describe('HistoryService', () => {
|
|||
describe('deleteHistoryEntry', () => {
|
||||
describe('works', () => {
|
||||
it('with an entry', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
const note = Note.create(user, alias);
|
||||
const historyEntry = HistoryEntry.create(user, note);
|
||||
|
@ -208,7 +214,7 @@ describe('HistoryService', () => {
|
|||
});
|
||||
|
||||
it('without an entry', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
const note = Note.create(user, alias);
|
||||
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||
|
@ -225,7 +231,7 @@ describe('HistoryService', () => {
|
|||
describe('toHistoryEntryDto', () => {
|
||||
describe('works', () => {
|
||||
it('with aliased note', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const alias = 'alias';
|
||||
const title = 'title';
|
||||
const tags = ['tag1', 'tag2'];
|
||||
|
@ -247,7 +253,7 @@ describe('HistoryService', () => {
|
|||
});
|
||||
|
||||
it('with regular note', async () => {
|
||||
const user = new User();
|
||||
const user = {} as User;
|
||||
const title = 'title';
|
||||
const id = 'id';
|
||||
const tags = ['tag1', 'tag2'];
|
||||
|
|
|
@ -25,6 +25,8 @@ import { MediaService } from './media.service';
|
|||
import { Repository } from 'typeorm';
|
||||
import { promises as fs } from 'fs';
|
||||
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
|
||||
describe('MediaService', () => {
|
||||
let service: MediaService;
|
||||
|
@ -70,6 +72,10 @@ describe('MediaService', () => {
|
|||
.useClass(Repository)
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(MediaUpload))
|
||||
.useClass(Repository)
|
||||
.compile();
|
||||
|
|
|
@ -13,10 +13,18 @@ import { AuthorColor } from './author-color.entity';
|
|||
import { Note } from './note.entity';
|
||||
import { NotesService } from './notes.service';
|
||||
import { Tag } from './tag.entity';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Note, AuthorColor, Tag]),
|
||||
TypeOrmModule.forFeature([
|
||||
Note,
|
||||
AuthorColor,
|
||||
Tag,
|
||||
NoteGroupPermission,
|
||||
NoteUserPermission,
|
||||
]),
|
||||
forwardRef(() => RevisionsModule),
|
||||
UsersModule,
|
||||
LoggerModule,
|
||||
|
|
|
@ -18,6 +18,8 @@ import { AuthorColor } from './author-color.entity';
|
|||
import { Note } from './note.entity';
|
||||
import { NotesService } from './notes.service';
|
||||
import { Tag } from './tag.entity';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
|
||||
describe('NotesService', () => {
|
||||
let service: NotesService;
|
||||
|
@ -53,6 +55,10 @@ describe('NotesService', () => {
|
|||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
service = module.get<NotesService>(NotesService);
|
||||
});
|
||||
|
|
|
@ -159,6 +159,7 @@ export class NotesService {
|
|||
historyEntries: [],
|
||||
updatedAt: new Date(),
|
||||
userName: 'Testy',
|
||||
groups: [],
|
||||
},
|
||||
description: 'Very descriptive text.',
|
||||
userPermissions: [],
|
||||
|
|
|
@ -9,11 +9,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { NoteGroupPermission } from './note-group-permission.entity';
|
||||
import { NoteUserPermission } from './note-user-permission.entity';
|
||||
import { PermissionsService } from './permissions.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([NoteUserPermission, NoteGroupPermission]),
|
||||
LoggerModule,
|
||||
],
|
||||
exports: [PermissionsService],
|
||||
providers: [PermissionsService],
|
||||
})
|
||||
export class PermissionsModule {}
|
||||
|
|
526
src/permissions/permissions.service.spec.ts
Normal file
526
src/permissions/permissions.service.spec.ts
Normal file
|
@ -0,0 +1,526 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { GuestPermission, PermissionsService } from './permissions.service';
|
||||
import { User } from '../users/user.entity';
|
||||
import { Note } from '../notes/note.entity';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { NotesModule } from '../notes/notes.module';
|
||||
import { PermissionsModule } from './permissions.module';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { NoteGroupPermission } from './note-group-permission.entity';
|
||||
import { NoteUserPermission } from './note-user-permission.entity';
|
||||
import { Identity } from '../users/identity.entity';
|
||||
import { AuthToken } from '../auth/auth-token.entity';
|
||||
import { Authorship } from '../revisions/authorship.entity';
|
||||
import { AuthorColor } from '../notes/author-color.entity';
|
||||
import { Revision } from '../revisions/revision.entity';
|
||||
import { Tag } from '../notes/tag.entity';
|
||||
import { Group } from '../groups/group.entity';
|
||||
|
||||
jest.mock('../permissions/note-group-permission.entity.ts');
|
||||
jest.mock('../groups/group.entity.ts');
|
||||
jest.mock('../notes/note.entity.ts');
|
||||
jest.mock('../users/user.entity.ts');
|
||||
|
||||
describe('PermissionsService', () => {
|
||||
let permissionsService: PermissionsService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [PermissionsService],
|
||||
imports: [PermissionsModule, UsersModule, LoggerModule, NotesModule],
|
||||
})
|
||||
.overrideProvider(getRepositoryToken(User))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(AuthToken))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Identity))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Authorship))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(AuthorColor))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Revision))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Note))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
permissionsService = module.get<PermissionsService>(PermissionsService);
|
||||
});
|
||||
|
||||
// The two users we test with:
|
||||
const user2 = {} as User;
|
||||
user2.id = '2';
|
||||
const user1 = {} as User;
|
||||
user1.id = '1';
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(permissionsService).toBeDefined();
|
||||
});
|
||||
|
||||
function createNote(owner: User): Note {
|
||||
const note = {} as Note;
|
||||
note.userPermissions = [];
|
||||
note.groupPermissions = [];
|
||||
note.owner = owner;
|
||||
return note;
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates the permission objects for UserPermission for two users with write and with out write permission
|
||||
*/
|
||||
function createNoteUserPermissionNotes(): Note[] {
|
||||
const note0 = createNote(user1);
|
||||
const note1 = createNote(user2);
|
||||
const note2 = createNote(user2);
|
||||
const note3 = createNote(user2);
|
||||
const note4 = createNote(user2);
|
||||
const note5 = createNote(user2);
|
||||
const note6 = createNote(user2);
|
||||
const note7 = createNote(user2);
|
||||
const noteUserPermission1 = {} as NoteUserPermission;
|
||||
noteUserPermission1.user = user1;
|
||||
const noteUserPermission2 = {} as NoteUserPermission;
|
||||
noteUserPermission2.user = user2;
|
||||
const noteUserPermission3 = {} as NoteUserPermission;
|
||||
noteUserPermission3.user = user1;
|
||||
noteUserPermission3.canEdit = true;
|
||||
const noteUserPermission4 = {} as NoteUserPermission;
|
||||
noteUserPermission4.user = user2;
|
||||
noteUserPermission4.canEdit = true;
|
||||
|
||||
note1.userPermissions.push(noteUserPermission1);
|
||||
|
||||
note2.userPermissions.push(noteUserPermission1);
|
||||
note2.userPermissions.push(noteUserPermission2);
|
||||
|
||||
note3.userPermissions.push(noteUserPermission2);
|
||||
note3.userPermissions.push(noteUserPermission1);
|
||||
|
||||
note4.userPermissions.push(noteUserPermission3);
|
||||
|
||||
note5.userPermissions.push(noteUserPermission3);
|
||||
note5.userPermissions.push(noteUserPermission4);
|
||||
|
||||
note6.userPermissions.push(noteUserPermission4);
|
||||
note6.userPermissions.push(noteUserPermission3);
|
||||
|
||||
note7.userPermissions.push(noteUserPermission2);
|
||||
|
||||
const everybody = {} as Group;
|
||||
everybody.name = 'everybody';
|
||||
everybody.special = true;
|
||||
const noteEverybodyRead = createNote(user1);
|
||||
|
||||
const noteGroupPermissionRead = {} as NoteGroupPermission;
|
||||
noteGroupPermissionRead.group = everybody;
|
||||
noteGroupPermissionRead.canEdit = false;
|
||||
noteGroupPermissionRead.note = noteEverybodyRead;
|
||||
noteEverybodyRead.groupPermissions = [noteGroupPermissionRead];
|
||||
|
||||
const noteEverybodyWrite = createNote(user1);
|
||||
|
||||
const noteGroupPermissionWrite = {} as NoteGroupPermission;
|
||||
noteGroupPermissionWrite.group = everybody;
|
||||
noteGroupPermissionWrite.canEdit = true;
|
||||
noteGroupPermissionWrite.note = noteEverybodyWrite;
|
||||
noteEverybodyWrite.groupPermissions = [noteGroupPermissionWrite];
|
||||
|
||||
return [
|
||||
note0,
|
||||
note1,
|
||||
note2,
|
||||
note3,
|
||||
note4,
|
||||
note5,
|
||||
note6,
|
||||
note7,
|
||||
noteEverybodyRead,
|
||||
noteEverybodyWrite,
|
||||
];
|
||||
}
|
||||
const notes = createNoteUserPermissionNotes();
|
||||
|
||||
describe('mayRead works with', () => {
|
||||
it('Owner', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayRead(user1, notes[0])).toBeTruthy();
|
||||
expect(permissionsService.mayRead(user1, notes[7])).toBeFalsy();
|
||||
});
|
||||
it('userPermission read', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayRead(user1, notes[1])).toBeTruthy();
|
||||
expect(permissionsService.mayRead(user1, notes[2])).toBeTruthy();
|
||||
expect(permissionsService.mayRead(user1, notes[3])).toBeTruthy();
|
||||
});
|
||||
it('userPermission write', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayRead(user1, notes[4])).toBeTruthy();
|
||||
expect(permissionsService.mayRead(user1, notes[5])).toBeTruthy();
|
||||
expect(permissionsService.mayRead(user1, notes[6])).toBeTruthy();
|
||||
expect(permissionsService.mayRead(user1, notes[7])).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('guest permission', () => {
|
||||
it('CREATE_ALIAS', () => {
|
||||
permissionsService.guestPermission = GuestPermission.CREATE_ALIAS;
|
||||
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy();
|
||||
});
|
||||
it('CREATE', () => {
|
||||
permissionsService.guestPermission = GuestPermission.CREATE;
|
||||
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy();
|
||||
});
|
||||
it('WRITE', () => {
|
||||
permissionsService.guestPermission = GuestPermission.WRITE;
|
||||
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy();
|
||||
});
|
||||
it('READ', () => {
|
||||
permissionsService.guestPermission = GuestPermission.READ;
|
||||
expect(permissionsService.mayRead(null, notes[8])).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mayWrite works with', () => {
|
||||
it('Owner', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayWrite(user1, notes[0])).toBeTruthy();
|
||||
expect(permissionsService.mayWrite(user1, notes[7])).toBeFalsy();
|
||||
});
|
||||
it('userPermission read', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayWrite(user1, notes[1])).toBeFalsy();
|
||||
expect(permissionsService.mayWrite(user1, notes[2])).toBeFalsy();
|
||||
expect(permissionsService.mayWrite(user1, notes[3])).toBeFalsy();
|
||||
});
|
||||
it('userPermission write', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayWrite(user1, notes[4])).toBeTruthy();
|
||||
expect(permissionsService.mayWrite(user1, notes[5])).toBeTruthy();
|
||||
expect(permissionsService.mayWrite(user1, notes[6])).toBeTruthy();
|
||||
expect(permissionsService.mayWrite(user1, notes[7])).toBeFalsy();
|
||||
});
|
||||
describe('guest permission', () => {
|
||||
it('CREATE_ALIAS', () => {
|
||||
permissionsService.guestPermission = GuestPermission.CREATE_ALIAS;
|
||||
expect(permissionsService.mayWrite(null, notes[9])).toBeTruthy();
|
||||
});
|
||||
it('CREATE', () => {
|
||||
permissionsService.guestPermission = GuestPermission.CREATE;
|
||||
expect(permissionsService.mayWrite(null, notes[9])).toBeTruthy();
|
||||
});
|
||||
it('WRITE', () => {
|
||||
permissionsService.guestPermission = GuestPermission.WRITE;
|
||||
expect(permissionsService.mayWrite(null, notes[9])).toBeTruthy();
|
||||
});
|
||||
it('READ', () => {
|
||||
permissionsService.guestPermission = GuestPermission.READ;
|
||||
expect(permissionsService.mayWrite(null, notes[9])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Helper Object that arranges a list of GroupPermissions and if they allow a user to read or write a particular note.
|
||||
*/
|
||||
class NoteGroupPermissionWithResultForUser {
|
||||
permissions: NoteGroupPermission[];
|
||||
allowsRead: boolean;
|
||||
allowsWrite: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
* Setup function to create all the groups we use in the tests.
|
||||
*/
|
||||
function createGroups(): { [id: string]: Group } {
|
||||
const result: { [id: string]: Group } = {};
|
||||
|
||||
const everybody: Group = new Group();
|
||||
everybody.special = true;
|
||||
everybody.name = 'everybody';
|
||||
result['everybody'] = everybody;
|
||||
|
||||
const loggedIn = new Group();
|
||||
loggedIn.special = true;
|
||||
loggedIn.name = 'loggedIn';
|
||||
result['loggedIn'] = loggedIn;
|
||||
|
||||
const user1group = new Group();
|
||||
user1group.name = 'user1group';
|
||||
user1group.members = [user1];
|
||||
result['user1group'] = user1group;
|
||||
|
||||
const user2group = new Group();
|
||||
user2group.name = 'user2group';
|
||||
user2group.members = [user2];
|
||||
result['user2group'] = user2group;
|
||||
|
||||
const user1and2group = new Group();
|
||||
user1and2group.name = 'user1and2group';
|
||||
user1and2group.members = [user1, user2];
|
||||
result['user1and2group'] = user1and2group;
|
||||
|
||||
const user2and1group = new Group();
|
||||
user2and1group.name = 'user2and1group';
|
||||
user2and1group.members = [user2, user1];
|
||||
result['user2and1group'] = user2and1group;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create all GroupPermissions: For each group two GroupPermissions are created one with read permission and one with write permission.
|
||||
*/
|
||||
function createAllNoteGroupPermissions(): NoteGroupPermission[][] {
|
||||
const groups = createGroups();
|
||||
|
||||
/*
|
||||
* Helper function for creating GroupPermissions
|
||||
*/
|
||||
function createNoteGroupPermission(
|
||||
group: Group,
|
||||
write: boolean,
|
||||
): NoteGroupPermission {
|
||||
const noteGroupPermission = new NoteGroupPermission();
|
||||
noteGroupPermission.canEdit = write;
|
||||
noteGroupPermission.group = group;
|
||||
return noteGroupPermission;
|
||||
}
|
||||
|
||||
const everybodyRead = createNoteGroupPermission(groups['everybody'], false);
|
||||
const everybodyWrite = createNoteGroupPermission(groups['everybody'], true);
|
||||
|
||||
const loggedInRead = createNoteGroupPermission(groups['loggedIn'], false);
|
||||
const loggedInWrite = createNoteGroupPermission(groups['loggedIn'], true);
|
||||
|
||||
const user1groupRead = createNoteGroupPermission(
|
||||
groups['user1group'],
|
||||
false,
|
||||
);
|
||||
const user1groupWrite = createNoteGroupPermission(
|
||||
groups['user1group'],
|
||||
true,
|
||||
);
|
||||
|
||||
const user2groupRead = createNoteGroupPermission(
|
||||
groups['user2group'],
|
||||
false,
|
||||
);
|
||||
const user2groupWrite = createNoteGroupPermission(
|
||||
groups['user2group'],
|
||||
true,
|
||||
);
|
||||
|
||||
const user1and2groupRead = createNoteGroupPermission(
|
||||
groups['user1and2group'],
|
||||
false,
|
||||
);
|
||||
const user1and2groupWrite = createNoteGroupPermission(
|
||||
groups['user1and2group'],
|
||||
true,
|
||||
);
|
||||
|
||||
const user2and1groupRead = createNoteGroupPermission(
|
||||
groups['user2and1group'],
|
||||
false,
|
||||
);
|
||||
const user2and1groupWrite = createNoteGroupPermission(
|
||||
groups['user2and1group'],
|
||||
true,
|
||||
);
|
||||
|
||||
return [
|
||||
[user1groupRead, user1and2groupRead, user2and1groupRead, null], // group0: allow user1 to read via group
|
||||
[user2and1groupWrite, user1and2groupWrite, user1groupWrite, null], // group1: allow user1 to write via group
|
||||
[everybodyRead, everybodyWrite, null], // group2: permissions of the special group everybody
|
||||
[loggedInRead, loggedInWrite, null], // group3: permissions of the special group loggedIn
|
||||
[user2groupWrite, user2groupRead, null], // group4: don't allow user1 to read or write via group
|
||||
];
|
||||
}
|
||||
/*
|
||||
* creates the matrix multiplication of group0 to group4 of createAllNoteGroupPermissions
|
||||
*/
|
||||
function createNoteGroupPermissionsCombinations(
|
||||
guestPermission: GuestPermission,
|
||||
): NoteGroupPermissionWithResultForUser[] {
|
||||
// for logged in users
|
||||
const noteGroupPermissions = createAllNoteGroupPermissions();
|
||||
const result: NoteGroupPermissionWithResultForUser[] = [];
|
||||
for (const group0 of noteGroupPermissions[0]) {
|
||||
for (const group1 of noteGroupPermissions[1]) {
|
||||
for (const group2 of noteGroupPermissions[2]) {
|
||||
for (const group3 of noteGroupPermissions[3]) {
|
||||
for (const group4 of noteGroupPermissions[4]) {
|
||||
const insert = [];
|
||||
let readPermission = false;
|
||||
let writePermission = false;
|
||||
if (group0 !== null) {
|
||||
// user1 in ReadGroups
|
||||
readPermission = true;
|
||||
insert.push(group0);
|
||||
}
|
||||
if (group1 !== null) {
|
||||
// user1 in WriteGroups
|
||||
readPermission = true;
|
||||
writePermission = true;
|
||||
insert.push(group1);
|
||||
}
|
||||
|
||||
if (group2 !== null) {
|
||||
// everybody group TODO config options
|
||||
switch (guestPermission) {
|
||||
case GuestPermission.CREATE_ALIAS:
|
||||
case GuestPermission.CREATE:
|
||||
case GuestPermission.WRITE:
|
||||
writePermission = writePermission || group2.canEdit;
|
||||
readPermission = true;
|
||||
break;
|
||||
case GuestPermission.READ:
|
||||
readPermission = true;
|
||||
}
|
||||
insert.push(group2);
|
||||
}
|
||||
if (group3 !== null) {
|
||||
// loggedIn users
|
||||
readPermission = true;
|
||||
writePermission = writePermission || group3.canEdit;
|
||||
insert.push(group3);
|
||||
}
|
||||
if (group4 !== null) {
|
||||
// user not in group
|
||||
insert.push(group4);
|
||||
}
|
||||
result.push({
|
||||
permissions: insert,
|
||||
allowsRead: readPermission,
|
||||
allowsWrite: writePermission,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// inspired by https://stackoverflow.com/questions/9960908/permutations-in-javascript
|
||||
function permutator(
|
||||
inputArr: NoteGroupPermission[],
|
||||
): NoteGroupPermission[][] {
|
||||
const results = [];
|
||||
|
||||
function permute(arr, memo) {
|
||||
let cur;
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
cur = arr.splice(i, 1);
|
||||
if (arr.length === 0) {
|
||||
results.push(memo.concat(cur));
|
||||
}
|
||||
permute(arr.slice(), memo.concat(cur));
|
||||
arr.splice(i, 0, cur[0]);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
return permute(inputArr, []);
|
||||
}
|
||||
|
||||
// takes each set of permissions from createNoteGroupPermissionsCombinations, permute them and add them to the list
|
||||
function permuteNoteGroupPermissions(
|
||||
noteGroupPermissions: NoteGroupPermissionWithResultForUser[],
|
||||
): NoteGroupPermissionWithResultForUser[] {
|
||||
const result: NoteGroupPermissionWithResultForUser[] = [];
|
||||
for (const permission of noteGroupPermissions) {
|
||||
const permutations = permutator(permission.permissions);
|
||||
for (const permutation of permutations) {
|
||||
result.push({
|
||||
permissions: permutation,
|
||||
allowsRead: permission.allowsRead,
|
||||
allowsWrite: permission.allowsWrite,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('check if groups work with', () => {
|
||||
const guestPermission = GuestPermission.WRITE;
|
||||
const rawPermissions = createNoteGroupPermissionsCombinations(
|
||||
guestPermission,
|
||||
);
|
||||
const permissions = permuteNoteGroupPermissions(rawPermissions);
|
||||
let i = 0;
|
||||
for (const permission of permissions) {
|
||||
const note = createNote(user2);
|
||||
note.groupPermissions = permission.permissions;
|
||||
let permissionString = '';
|
||||
for (const perm of permission.permissions) {
|
||||
permissionString += ' ' + perm.group.name + ':' + perm.canEdit;
|
||||
}
|
||||
it('mayWrite - test #' + i + ':' + permissionString, () => {
|
||||
permissionsService.guestPermission = guestPermission;
|
||||
expect(permissionsService.mayWrite(user1, note)).toEqual(
|
||||
permission.allowsWrite,
|
||||
);
|
||||
});
|
||||
it('mayRead - test #' + i + ':' + permissionString, () => {
|
||||
permissionsService.guestPermission = guestPermission;
|
||||
expect(permissionsService.mayRead(user1, note)).toEqual(
|
||||
permission.allowsRead,
|
||||
);
|
||||
});
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
describe('mayCreate works for', () => {
|
||||
it('logged in', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayCreate(user1)).toBeTruthy();
|
||||
});
|
||||
it('guest denied', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.mayCreate(null)).toBeFalsy();
|
||||
});
|
||||
it('guest read', () => {
|
||||
permissionsService.guestPermission = GuestPermission.READ;
|
||||
expect(permissionsService.mayCreate(null)).toBeFalsy();
|
||||
});
|
||||
it('guest write', () => {
|
||||
permissionsService.guestPermission = GuestPermission.WRITE;
|
||||
expect(permissionsService.mayCreate(null)).toBeFalsy();
|
||||
});
|
||||
it('guest create', () => {
|
||||
permissionsService.guestPermission = GuestPermission.CREATE;
|
||||
expect(permissionsService.mayCreate(null)).toBeTruthy();
|
||||
});
|
||||
it('guest create alias', () => {
|
||||
permissionsService.guestPermission = GuestPermission.CREATE_ALIAS;
|
||||
expect(permissionsService.mayCreate(null)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOwner works', () => {
|
||||
it('for positive case', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.isOwner(user1, notes[0])).toBeTruthy();
|
||||
});
|
||||
it('for negative case', () => {
|
||||
permissionsService.guestPermission = GuestPermission.DENY;
|
||||
expect(permissionsService.isOwner(user1, notes[1])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
129
src/permissions/permissions.service.ts
Normal file
129
src/permissions/permissions.service.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from '../users/user.entity';
|
||||
import { Note } from '../notes/note.entity';
|
||||
|
||||
// TODO move to config or remove
|
||||
export enum GuestPermission {
|
||||
DENY = 'deny',
|
||||
READ = 'read',
|
||||
WRITE = 'write',
|
||||
CREATE = 'create',
|
||||
CREATE_ALIAS = 'createAlias',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsService {
|
||||
public guestPermission: GuestPermission; // TODO change to configOption
|
||||
mayRead(user: User, note: Note): boolean {
|
||||
if (this.isOwner(user, note)) return true;
|
||||
|
||||
if (this.hasPermissionUser(user, note, false)) return true;
|
||||
|
||||
// noinspection RedundantIfStatementJS
|
||||
if (this.hasPermissionGroup(user, note, false)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
mayWrite(user: User, note: Note): boolean {
|
||||
if (this.isOwner(user, note)) return true;
|
||||
|
||||
if (this.hasPermissionUser(user, note, true)) return true;
|
||||
|
||||
// noinspection RedundantIfStatementJS
|
||||
if (this.hasPermissionGroup(user, note, true)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
mayCreate(user: User): boolean {
|
||||
if (user) {
|
||||
return true;
|
||||
} else {
|
||||
if (
|
||||
this.guestPermission == GuestPermission.CREATE ||
|
||||
this.guestPermission == GuestPermission.CREATE_ALIAS
|
||||
) {
|
||||
// TODO change to guestPermission to config option
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isOwner(user: User, note: Note): boolean {
|
||||
if (!user) return false;
|
||||
if (!note.owner) return false;
|
||||
return note.owner.id === user.id;
|
||||
}
|
||||
|
||||
private hasPermissionUser(
|
||||
user: User,
|
||||
note: Note,
|
||||
wantEdit: boolean,
|
||||
): boolean {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
for (const userPermission of note.userPermissions) {
|
||||
if (
|
||||
userPermission.user.id === user.id &&
|
||||
(userPermission.canEdit || !wantEdit)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private hasPermissionGroup(
|
||||
user: User,
|
||||
note: Note,
|
||||
wantEdit: boolean,
|
||||
): boolean {
|
||||
// TODO: Get real config value
|
||||
let guestsAllowed = false;
|
||||
switch (this.guestPermission) {
|
||||
case GuestPermission.CREATE_ALIAS:
|
||||
case GuestPermission.CREATE:
|
||||
case GuestPermission.WRITE:
|
||||
guestsAllowed = true;
|
||||
break;
|
||||
case GuestPermission.READ:
|
||||
guestsAllowed = !wantEdit;
|
||||
}
|
||||
for (const groupPermission of note.groupPermissions) {
|
||||
if (groupPermission.canEdit || !wantEdit) {
|
||||
// Handle special groups
|
||||
if (groupPermission.group.special) {
|
||||
if (groupPermission.group.name == 'loggedIn') {
|
||||
// TODO: Name of group for logged in users
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
groupPermission.group.name == 'everybody' &&
|
||||
(groupPermission.canEdit || !wantEdit) &&
|
||||
guestsAllowed
|
||||
) {
|
||||
// TODO: Name of group in which everybody even guests can edit
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Handle normal groups
|
||||
if (user) {
|
||||
for (const member of groupPermission.group.members) {
|
||||
if (member.id === user.id) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ import { Authorship } from './authorship.entity';
|
|||
import { Revision } from './revision.entity';
|
||||
import { RevisionsService } from './revisions.service';
|
||||
import { Tag } from '../notes/tag.entity';
|
||||
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||
|
||||
describe('RevisionsService', () => {
|
||||
let service: RevisionsService;
|
||||
|
@ -48,6 +50,10 @@ describe('RevisionsService', () => {
|
|||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(Tag))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||
.useValue({})
|
||||
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||
.useValue({})
|
||||
.compile();
|
||||
|
||||
service = module.get<RevisionsService>(RevisionsService);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
@ -14,6 +15,7 @@ import { Column, OneToMany } from 'typeorm';
|
|||
import { Note } from '../notes/note.entity';
|
||||
import { AuthToken } from '../auth/auth-token.entity';
|
||||
import { Identity } from './identity.entity';
|
||||
import { Group } from '../groups/group.entity';
|
||||
import { HistoryEntry } from '../history/history-entry.entity';
|
||||
|
||||
@Entity()
|
||||
|
@ -52,9 +54,15 @@ export class User {
|
|||
@OneToMany((_) => Identity, (identity) => identity.user)
|
||||
identities: Identity[];
|
||||
|
||||
@ManyToMany((_) => Group, (group) => group.members)
|
||||
groups: Group[];
|
||||
|
||||
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
|
||||
historyEntries: HistoryEntry[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
private constructor() {}
|
||||
|
||||
public static create(
|
||||
userName: string,
|
||||
displayName: string,
|
||||
|
|
|
@ -20,11 +20,15 @@ import { PermissionsModule } from '../../src/permissions/permissions.module';
|
|||
import { AuthModule } from '../../src/auth/auth.module';
|
||||
import { TokenAuthGuard } from '../../src/auth/token-auth.guard';
|
||||
import { MockAuthGuard } from '../../src/auth/mock-auth.guard';
|
||||
import { UsersService } from '../../src/users/users.service';
|
||||
import { User } from '../../src/users/user.entity';
|
||||
import { UsersModule } from '../../src/users/users.module';
|
||||
|
||||
describe('Notes', () => {
|
||||
let app: INestApplication;
|
||||
let notesService: NotesService;
|
||||
let user: User;
|
||||
let content: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
|
@ -56,14 +60,16 @@ describe('Notes', () => {
|
|||
app = moduleRef.createNestApplication();
|
||||
await app.init();
|
||||
notesService = moduleRef.get(NotesService);
|
||||
const userService = moduleRef.get(UsersService);
|
||||
user = await userService.createUser('hardcoded', 'Testy');
|
||||
content = 'This is a test note.';
|
||||
});
|
||||
|
||||
it(`POST /notes`, async () => {
|
||||
const newNote = 'This is a test note.';
|
||||
it('POST /notes', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/notes')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.send(newNote)
|
||||
.send(content)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(201);
|
||||
expect(response.body.metadata?.id).toBeDefined();
|
||||
|
@ -71,88 +77,98 @@ describe('Notes', () => {
|
|||
await notesService.getCurrentContent(
|
||||
await notesService.getNoteByIdOrAlias(response.body.metadata.id),
|
||||
),
|
||||
).toEqual(newNote);
|
||||
).toEqual(content);
|
||||
});
|
||||
|
||||
it(`GET /notes/{note}`, async () => {
|
||||
// check if we can succefully get a note that exists
|
||||
await notesService.createNote('This is a test note.', 'test1');
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test1')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body.content).toEqual('This is a test note.');
|
||||
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
describe('GET /notes/{note}', () => {
|
||||
it('works with an existing note', async () => {
|
||||
// check if we can succefully get a note that exists
|
||||
await notesService.createNote(content, 'test1', user);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test1')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body.content).toEqual(content);
|
||||
});
|
||||
it('fails with an non-existing note', async () => {
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it(`POST /notes/{note}`, async () => {
|
||||
const newNote = 'This is a test note.';
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/notes/test2')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.send(newNote)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(201);
|
||||
expect(response.body.metadata?.id).toBeDefined();
|
||||
return expect(
|
||||
await notesService.getCurrentContent(
|
||||
await notesService.getNoteByIdOrAlias(response.body.metadata?.id),
|
||||
),
|
||||
).toEqual(newNote);
|
||||
describe('POST /notes/{note}', () => {
|
||||
it('works with a non-existing alias', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/notes/test2')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.send(content)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(201);
|
||||
expect(response.body.metadata?.id).toBeDefined();
|
||||
return expect(
|
||||
await notesService.getCurrentContent(
|
||||
await notesService.getNoteByIdOrAlias(response.body.metadata?.id),
|
||||
),
|
||||
).toEqual(content);
|
||||
});
|
||||
});
|
||||
|
||||
it(`DELETE /notes/{note}`, async () => {
|
||||
await notesService.createNote('This is a test note.', 'test3');
|
||||
await request(app.getHttpServer()).delete('/notes/test3').expect(200);
|
||||
await expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual(
|
||||
new NotInDBError("Note with id/alias 'test3' not found."),
|
||||
);
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.delete('/notes/i_dont_exist')
|
||||
.expect(404);
|
||||
describe('DELETE /notes/{note}', () => {
|
||||
it('works with an existing alias', async () => {
|
||||
await notesService.createNote(content, 'test3', user);
|
||||
await request(app.getHttpServer()).delete('/notes/test3').expect(200);
|
||||
await expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual(
|
||||
new NotInDBError("Note with id/alias 'test3' not found."),
|
||||
);
|
||||
});
|
||||
it('fails with a non-existing alias', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.delete('/notes/i_dont_exist')
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it(`PUT /notes/{note}`, async () => {
|
||||
await notesService.createNote('This is a test note.', 'test4');
|
||||
const response = await request(app.getHttpServer())
|
||||
.put('/notes/test4')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.send('New note text')
|
||||
.expect(200);
|
||||
await expect(
|
||||
await notesService.getCurrentContent(
|
||||
await notesService.getNoteByIdOrAlias('test4'),
|
||||
),
|
||||
).toEqual('New note text');
|
||||
expect(response.body.content).toEqual('New note text');
|
||||
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.put('/notes/i_dont_exist')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
describe('PUT /notes/{note}', () => {
|
||||
const changedContent = 'New note text';
|
||||
it('works with existing alias', async () => {
|
||||
await notesService.createNote(content, 'test4', user);
|
||||
const response = await request(app.getHttpServer())
|
||||
.put('/notes/test4')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.send(changedContent)
|
||||
.expect(200);
|
||||
await expect(
|
||||
await notesService.getCurrentContent(
|
||||
await notesService.getNoteByIdOrAlias('test4'),
|
||||
),
|
||||
).toEqual(changedContent);
|
||||
expect(response.body.content).toEqual(changedContent);
|
||||
});
|
||||
it('fails with a non-existing alias', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.put('/notes/i_dont_exist')
|
||||
.set('Content-Type', 'text/markdown')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /notes/{note}/metadata', () => {
|
||||
it(`returns complete metadata object`, async () => {
|
||||
await notesService.createNote('This is a test note.', 'test6');
|
||||
it('returns complete metadata object', async () => {
|
||||
await notesService.createNote(content, 'test5', user);
|
||||
const metadata = await request(app.getHttpServer())
|
||||
.get('/notes/test6/metadata')
|
||||
.get('/notes/test5/metadata')
|
||||
.expect(200);
|
||||
expect(typeof metadata.body.id).toEqual('string');
|
||||
expect(metadata.body.alias).toEqual('test6');
|
||||
expect(metadata.body.alias).toEqual('test5');
|
||||
expect(metadata.body.title).toBeNull();
|
||||
expect(metadata.body.description).toBeNull();
|
||||
expect(typeof metadata.body.createTime).toEqual('string');
|
||||
expect(metadata.body.editedBy).toEqual([]);
|
||||
expect(metadata.body.permissions.owner).toBeNull();
|
||||
expect(metadata.body.permissions.owner.userName).toEqual('hardcoded');
|
||||
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
|
||||
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
|
||||
expect(metadata.body.tags).toEqual([]);
|
||||
|
@ -163,7 +179,9 @@ describe('Notes', () => {
|
|||
expect(typeof metadata.body.updateUser.photo).toEqual('string');
|
||||
expect(typeof metadata.body.viewCount).toEqual('number');
|
||||
expect(metadata.body.editedBy).toEqual([]);
|
||||
});
|
||||
|
||||
it('fails with non-existing alias', async () => {
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/metadata')
|
||||
|
@ -173,66 +191,75 @@ describe('Notes', () => {
|
|||
|
||||
it('has the correct update/create dates', async () => {
|
||||
// create a note
|
||||
const note = await notesService.createNote(
|
||||
'This is a test note.',
|
||||
'test6a',
|
||||
);
|
||||
const note = await notesService.createNote(content, 'test5a', user);
|
||||
// save the creation time
|
||||
const createDate = (await note.revisions)[0].createdAt;
|
||||
// wait one second
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
// update the note
|
||||
await notesService.updateNoteByIdOrAlias('test6a', 'More test content');
|
||||
await notesService.updateNoteByIdOrAlias('test5a', 'More test content');
|
||||
const metadata = await request(app.getHttpServer())
|
||||
.get('/notes/test6a/metadata')
|
||||
.get('/notes/test5a/metadata')
|
||||
.expect(200);
|
||||
expect(metadata.body.createTime).toEqual(createDate.toISOString());
|
||||
expect(metadata.body.updateTime).not.toEqual(createDate.toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
it(`GET /notes/{note}/revisions`, async () => {
|
||||
await notesService.createNote('This is a test note.', 'test7');
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test7/revisions')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body).toHaveLength(1);
|
||||
describe('GET /notes/{note}/revisions', () => {
|
||||
it('works with existing alias', async () => {
|
||||
await notesService.createNote(content, 'test6', user);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test6/revisions')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body).toHaveLength(1);
|
||||
});
|
||||
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/revisions')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
it('fails with non-existing alias', async () => {
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/revisions')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it(`GET /notes/{note}/revisions/{revision-id}`, async () => {
|
||||
const note = await notesService.createNote('This is a test note.', 'test8');
|
||||
const revision = await notesService.getLatestRevision(note);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test8/revisions/' + revision.id)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body.content).toEqual('This is a test note.');
|
||||
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/revisions/1')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
describe('GET /notes/{note}/revisions/{revision-id}', () => {
|
||||
it('works with an existing alias', async () => {
|
||||
const note = await notesService.createNote(content, 'test7', user);
|
||||
const revision = await notesService.getLatestRevision(note);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test7/revisions/' + revision.id)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body.content).toEqual(content);
|
||||
});
|
||||
it('fails with non-existing alias', async () => {
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/revisions/1')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it(`GET /notes/{note}/content`, async () => {
|
||||
await notesService.createNote('This is a test note.', 'test9');
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test9/content')
|
||||
.expect(200);
|
||||
expect(response.text).toEqual('This is a test note.');
|
||||
describe('GET /notes/{note}/content', () => {
|
||||
it('works with an existing alias', async () => {
|
||||
await notesService.createNote(content, 'test8', user);
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/notes/test8/content')
|
||||
.expect(200);
|
||||
expect(response.text).toEqual(content);
|
||||
});
|
||||
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/content')
|
||||
.expect(404);
|
||||
it('fails with non-existing alias', async () => {
|
||||
// check if a missing note correctly returns 404
|
||||
await request(app.getHttpServer())
|
||||
.get('/notes/i_dont_exist/content')
|
||||
.expect('Content-Type', /text\/markdown/)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
Loading…
Reference in a new issue