mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-26 19:53:59 -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
|
*canEdit : boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "group_members_user" {
|
||||||
|
*group : number <<FK group>>
|
||||||
|
*member : uuid <<FK user>>
|
||||||
|
}
|
||||||
|
|
||||||
entity "tag" {
|
entity "tag" {
|
||||||
*id: number <<generated>>
|
*id: number <<generated>>
|
||||||
*name: text
|
*name: text
|
||||||
|
@ -144,16 +149,16 @@ entity "history_entry" {
|
||||||
|
|
||||||
user "1" -- "0..*" note: owner
|
user "1" -- "0..*" note: owner
|
||||||
user "1" -u- "1..*" identity
|
user "1" -u- "1..*" identity
|
||||||
user "1" - "1..*" auth_token: authTokens
|
user "1" -l- "1..*" auth_token: authTokens
|
||||||
user "1" -l- "1..*" session
|
user "1" -r- "1..*" session
|
||||||
user "1" - "0..*" media_upload
|
user "1" -- "0..*" media_upload
|
||||||
user "1" - "0..*" history_entry
|
user "1" - "0..*" history_entry
|
||||||
user "0..*" -- "0..*" note
|
user "0..*" -- "0..*" note
|
||||||
user "1" - "0..*" authorship
|
user "1" -- "0..*" authorship
|
||||||
|
|
||||||
(user, note) . author_colors
|
(user, note) . author_colors
|
||||||
|
|
||||||
revision "0..*" - "0..*" authorship
|
revision "0..*" -- "0..*" authorship
|
||||||
(revision, authorship) .. revision_authorship
|
(revision, authorship) .. revision_authorship
|
||||||
|
|
||||||
media_upload "0..*" -- "1" note
|
media_upload "0..*" -- "1" note
|
||||||
|
@ -161,9 +166,11 @@ note "1" - "1..*" revision
|
||||||
note "1" - "0..*" history_entry
|
note "1" - "0..*" history_entry
|
||||||
note "0..*" -l- "0..*" tag
|
note "0..*" -l- "0..*" tag
|
||||||
note "0..*" -- "0..*" group
|
note "0..*" -- "0..*" group
|
||||||
|
user "1..*" -- "0..*" group
|
||||||
|
|
||||||
user "0..*" -- "0..*" note
|
user "0..*" -- "0..*" note
|
||||||
(user, note) . note_user_permission
|
(user, note) . note_user_permission
|
||||||
(note, group) . note_group_permission
|
(note, group) . note_group_permission
|
||||||
|
(user, group) . group_members_user
|
||||||
|
|
||||||
@enduml
|
@enduml
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { User } from '../../../users/user.entity';
|
||||||
import { UsersModule } from '../../../users/users.module';
|
import { UsersModule } from '../../../users/users.module';
|
||||||
import { MeController } from './me.controller';
|
import { MeController } from './me.controller';
|
||||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
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', () => {
|
describe('Me Controller', () => {
|
||||||
let controller: MeController;
|
let controller: MeController;
|
||||||
|
@ -47,6 +49,10 @@ describe('Me Controller', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(HistoryEntry))
|
.overrideProvider(getRepositoryToken(HistoryEntry))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
controller = module.get<MeController>(MeController);
|
controller = module.get<MeController>(MeController);
|
||||||
|
|
|
@ -22,6 +22,8 @@ import { AuthToken } from '../../../auth/auth-token.entity';
|
||||||
import { Identity } from '../../../users/identity.entity';
|
import { Identity } from '../../../users/identity.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { MediaController } from './media.controller';
|
import { MediaController } from './media.controller';
|
||||||
|
import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity';
|
||||||
|
import { NoteUserPermission } from '../../../permissions/note-user-permission.entity';
|
||||||
|
|
||||||
describe('Media Controller', () => {
|
describe('Media Controller', () => {
|
||||||
let controller: MediaController;
|
let controller: MediaController;
|
||||||
|
@ -57,6 +59,10 @@ describe('Media Controller', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Tag))
|
.overrideProvider(getRepositoryToken(Tag))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
controller = module.get<MediaController>(MediaController);
|
controller = module.get<MediaController>(MediaController);
|
||||||
|
|
|
@ -19,8 +19,11 @@ import { Identity } from '../../../users/identity.entity';
|
||||||
import { User } from '../../../users/user.entity';
|
import { User } from '../../../users/user.entity';
|
||||||
import { UsersModule } from '../../../users/users.module';
|
import { UsersModule } from '../../../users/users.module';
|
||||||
import { NotesController } from './notes.controller';
|
import { NotesController } from './notes.controller';
|
||||||
|
import { PermissionsModule } from '../../../permissions/permissions.module';
|
||||||
import { HistoryModule } from '../../../history/history.module';
|
import { HistoryModule } from '../../../history/history.module';
|
||||||
import { HistoryEntry } from '../../../history/history-entry.entity';
|
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', () => {
|
describe('Notes Controller', () => {
|
||||||
let controller: NotesController;
|
let controller: NotesController;
|
||||||
|
@ -39,7 +42,13 @@ describe('Notes Controller', () => {
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
imports: [RevisionsModule, UsersModule, LoggerModule, HistoryModule],
|
imports: [
|
||||||
|
RevisionsModule,
|
||||||
|
UsersModule,
|
||||||
|
LoggerModule,
|
||||||
|
PermissionsModule,
|
||||||
|
HistoryModule,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.overrideProvider(getRepositoryToken(Note))
|
.overrideProvider(getRepositoryToken(Note))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
@ -61,6 +70,10 @@ describe('Notes Controller', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(HistoryEntry))
|
.overrideProvider(getRepositoryToken(HistoryEntry))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
controller = module.get<NotesController>(NotesController);
|
controller = module.get<NotesController>(NotesController);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Request,
|
Request,
|
||||||
|
UnauthorizedException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { NotInDBError } from '../../../errors/errors';
|
import { NotInDBError } from '../../../errors/errors';
|
||||||
|
@ -33,6 +34,8 @@ import { NoteDto } from '../../../notes/note.dto';
|
||||||
import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
|
import { NoteMetadataDto } from '../../../notes/note-metadata.dto';
|
||||||
import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto';
|
import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto';
|
||||||
import { RevisionDto } from '../../../revisions/revision.dto';
|
import { RevisionDto } from '../../../revisions/revision.dto';
|
||||||
|
import { PermissionsService } from '../../../permissions/permissions.service';
|
||||||
|
import { Note } from '../../../notes/note.entity';
|
||||||
|
|
||||||
@ApiTags('notes')
|
@ApiTags('notes')
|
||||||
@ApiSecurity('token')
|
@ApiSecurity('token')
|
||||||
|
@ -42,6 +45,7 @@ export class NotesController {
|
||||||
private readonly logger: ConsoleLoggerService,
|
private readonly logger: ConsoleLoggerService,
|
||||||
private noteService: NotesService,
|
private noteService: NotesService,
|
||||||
private revisionsService: RevisionsService,
|
private revisionsService: RevisionsService,
|
||||||
|
private permissionsService: PermissionsService,
|
||||||
private historyService: HistoryService,
|
private historyService: HistoryService,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(NotesController.name);
|
this.logger.setContext(NotesController.name);
|
||||||
|
@ -54,7 +58,10 @@ export class NotesController {
|
||||||
@MarkdownBody() text: string,
|
@MarkdownBody() text: string,
|
||||||
): Promise<NoteDto> {
|
): Promise<NoteDto> {
|
||||||
// ToDo: provide user for createNoteDto
|
// 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(
|
return this.noteService.toNoteDto(
|
||||||
await this.noteService.createNote(text, undefined, req.user),
|
await this.noteService.createNote(text, undefined, req.user),
|
||||||
);
|
);
|
||||||
|
@ -62,18 +69,24 @@ export class NotesController {
|
||||||
|
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@Get(':noteIdOrAlias')
|
@Get(':noteIdOrAlias')
|
||||||
async getNote(@Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string) {
|
async getNote(
|
||||||
// ToDo: check if user is allowed to view this note
|
@Request() req,
|
||||||
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
|
): Promise<NoteDto> {
|
||||||
|
let note: Note;
|
||||||
try {
|
try {
|
||||||
const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias);
|
||||||
await this.historyService.createOrUpdateHistoryEntry(note, req.user);
|
|
||||||
return this.noteService.toNoteDto(note);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof NotInDBError) {
|
if (e instanceof NotInDBError) {
|
||||||
throw new NotFoundException(e.message);
|
throw new NotFoundException(e.message);
|
||||||
}
|
}
|
||||||
throw e;
|
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)
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ -83,7 +96,9 @@ export class NotesController {
|
||||||
@Param('noteAlias') noteAlias: string,
|
@Param('noteAlias') noteAlias: string,
|
||||||
@MarkdownBody() text: string,
|
@MarkdownBody() text: string,
|
||||||
): Promise<NoteDto> {
|
): 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');
|
this.logger.debug('Got raw markdown:\n' + text, 'createNamedNote');
|
||||||
return this.noteService.toNoteDto(
|
return this.noteService.toNoteDto(
|
||||||
await this.noteService.createNote(text, noteAlias, req.user),
|
await this.noteService.createNote(text, noteAlias, req.user),
|
||||||
|
@ -96,18 +111,21 @@ export class NotesController {
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// ToDo: check if user is allowed to delete this note
|
|
||||||
this.logger.debug('Deleting note: ' + noteIdOrAlias, 'deleteNote');
|
|
||||||
try {
|
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);
|
await this.noteService.deleteNoteByIdOrAlias(noteIdOrAlias);
|
||||||
|
this.logger.debug('Successfully deleted ' + noteIdOrAlias, 'deleteNote');
|
||||||
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof NotInDBError) {
|
if (e instanceof NotInDBError) {
|
||||||
throw new NotFoundException(e.message);
|
throw new NotFoundException(e.message);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
this.logger.debug('Successfully deleted ' + noteIdOrAlias, 'deleteNote');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
|
@ -117,9 +135,12 @@ export class NotesController {
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
@MarkdownBody() text: string,
|
@MarkdownBody() text: string,
|
||||||
): Promise<NoteDto> {
|
): Promise<NoteDto> {
|
||||||
// ToDo: check if user is allowed to change this note
|
|
||||||
this.logger.debug('Got raw markdown:\n' + text, 'updateNote');
|
|
||||||
try {
|
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(
|
return this.noteService.toNoteDto(
|
||||||
await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text),
|
await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text),
|
||||||
);
|
);
|
||||||
|
@ -138,8 +159,11 @@ export class NotesController {
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// ToDo: check if user is allowed to view this notes content
|
|
||||||
try {
|
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);
|
return await this.noteService.getNoteContent(noteIdOrAlias);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof NotInDBError) {
|
if (e instanceof NotInDBError) {
|
||||||
|
@ -155,8 +179,11 @@ export class NotesController {
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
): Promise<NoteMetadataDto> {
|
): Promise<NoteMetadataDto> {
|
||||||
// ToDo: check if user is allowed to view this notes metadata
|
|
||||||
try {
|
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(
|
return this.noteService.toNoteMetadataDto(
|
||||||
await this.noteService.getNoteByIdOrAlias(noteIdOrAlias),
|
await this.noteService.getNoteByIdOrAlias(noteIdOrAlias),
|
||||||
);
|
);
|
||||||
|
@ -175,8 +202,11 @@ export class NotesController {
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
@Body() updateDto: NotePermissionsUpdateDto,
|
@Body() updateDto: NotePermissionsUpdateDto,
|
||||||
): Promise<NotePermissionsDto> {
|
): Promise<NotePermissionsDto> {
|
||||||
// ToDo: check if user is allowed to view this notes permissions
|
|
||||||
try {
|
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(
|
return this.noteService.toNotePermissionsDto(
|
||||||
await this.noteService.updateNotePermissions(noteIdOrAlias, updateDto),
|
await this.noteService.updateNotePermissions(noteIdOrAlias, updateDto),
|
||||||
);
|
);
|
||||||
|
@ -194,8 +224,11 @@ export class NotesController {
|
||||||
@Request() req,
|
@Request() req,
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
): Promise<RevisionMetadataDto[]> {
|
): Promise<RevisionMetadataDto[]> {
|
||||||
// ToDo: check if user is allowed to view this notes revisions
|
|
||||||
try {
|
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(
|
const revisions = await this.revisionsService.getAllRevisions(
|
||||||
noteIdOrAlias,
|
noteIdOrAlias,
|
||||||
);
|
);
|
||||||
|
@ -219,8 +252,11 @@ export class NotesController {
|
||||||
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
@Param('revisionId') revisionId: number,
|
@Param('revisionId') revisionId: number,
|
||||||
): Promise<RevisionDto> {
|
): Promise<RevisionDto> {
|
||||||
// ToDo: check if user is allowed to view this notes revision
|
|
||||||
try {
|
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(
|
return this.revisionsService.toRevisionDto(
|
||||||
await this.revisionsService.getRevision(noteIdOrAlias, revisionId),
|
await this.revisionsService.getRevision(noteIdOrAlias, revisionId),
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { MeController } from './me/me.controller';
|
||||||
import { NotesController } from './notes/notes.controller';
|
import { NotesController } from './notes/notes.controller';
|
||||||
import { MediaController } from './media/media.controller';
|
import { MediaController } from './media/media.controller';
|
||||||
import { MonitoringController } from './monitoring/monitoring.controller';
|
import { MonitoringController } from './monitoring/monitoring.controller';
|
||||||
|
import { PermissionsModule } from '../../permissions/permissions.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -26,6 +27,7 @@ import { MonitoringController } from './monitoring/monitoring.controller';
|
||||||
MonitoringModule,
|
MonitoringModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
MediaModule,
|
MediaModule,
|
||||||
|
PermissionsModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
MeController,
|
MeController,
|
||||||
|
|
|
@ -4,7 +4,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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()
|
@Entity()
|
||||||
export class Group {
|
export class Group {
|
||||||
|
@ -26,4 +33,11 @@ export class Group {
|
||||||
*/
|
*/
|
||||||
@Column()
|
@Column()
|
||||||
special: boolean;
|
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 { Revision } from '../revisions/revision.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { NotInDBError } from '../errors/errors';
|
import { NotInDBError } from '../errors/errors';
|
||||||
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
|
|
||||||
describe('HistoryService', () => {
|
describe('HistoryService', () => {
|
||||||
let service: HistoryService;
|
let service: HistoryService;
|
||||||
|
@ -54,6 +56,10 @@ describe('HistoryService', () => {
|
||||||
.useClass(Repository)
|
.useClass(Repository)
|
||||||
.overrideProvider(getRepositoryToken(Tag))
|
.overrideProvider(getRepositoryToken(Tag))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
service = module.get<HistoryService>(HistoryService);
|
service = module.get<HistoryService>(HistoryService);
|
||||||
|
@ -99,7 +105,7 @@ describe('HistoryService', () => {
|
||||||
describe('createOrUpdateHistoryEntry', () => {
|
describe('createOrUpdateHistoryEntry', () => {
|
||||||
describe('works', () => {
|
describe('works', () => {
|
||||||
it('without an preexisting entry', async () => {
|
it('without an preexisting entry', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
jest
|
jest
|
||||||
|
@ -118,7 +124,7 @@ describe('HistoryService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with an preexisting entry', async () => {
|
it('with an preexisting entry', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
const historyEntry = HistoryEntry.create(
|
const historyEntry = HistoryEntry.create(
|
||||||
user,
|
user,
|
||||||
|
@ -148,7 +154,7 @@ describe('HistoryService', () => {
|
||||||
describe('updateHistoryEntry', () => {
|
describe('updateHistoryEntry', () => {
|
||||||
describe('works', () => {
|
describe('works', () => {
|
||||||
it('with an entry', async () => {
|
it('with an entry', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
const note = Note.create(user, alias);
|
const note = Note.create(user, alias);
|
||||||
const historyEntry = HistoryEntry.create(user, note);
|
const historyEntry = HistoryEntry.create(user, note);
|
||||||
|
@ -173,7 +179,7 @@ describe('HistoryService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without an entry', async () => {
|
it('without an entry', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
const note = Note.create(user, alias);
|
const note = Note.create(user, alias);
|
||||||
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
|
@ -192,7 +198,7 @@ describe('HistoryService', () => {
|
||||||
describe('deleteHistoryEntry', () => {
|
describe('deleteHistoryEntry', () => {
|
||||||
describe('works', () => {
|
describe('works', () => {
|
||||||
it('with an entry', async () => {
|
it('with an entry', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
const note = Note.create(user, alias);
|
const note = Note.create(user, alias);
|
||||||
const historyEntry = HistoryEntry.create(user, note);
|
const historyEntry = HistoryEntry.create(user, note);
|
||||||
|
@ -208,7 +214,7 @@ describe('HistoryService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without an entry', async () => {
|
it('without an entry', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
const note = Note.create(user, alias);
|
const note = Note.create(user, alias);
|
||||||
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined);
|
||||||
|
@ -225,7 +231,7 @@ describe('HistoryService', () => {
|
||||||
describe('toHistoryEntryDto', () => {
|
describe('toHistoryEntryDto', () => {
|
||||||
describe('works', () => {
|
describe('works', () => {
|
||||||
it('with aliased note', async () => {
|
it('with aliased note', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const alias = 'alias';
|
const alias = 'alias';
|
||||||
const title = 'title';
|
const title = 'title';
|
||||||
const tags = ['tag1', 'tag2'];
|
const tags = ['tag1', 'tag2'];
|
||||||
|
@ -247,7 +253,7 @@ describe('HistoryService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with regular note', async () => {
|
it('with regular note', async () => {
|
||||||
const user = new User();
|
const user = {} as User;
|
||||||
const title = 'title';
|
const title = 'title';
|
||||||
const id = 'id';
|
const id = 'id';
|
||||||
const tags = ['tag1', 'tag2'];
|
const tags = ['tag1', 'tag2'];
|
||||||
|
|
|
@ -25,6 +25,8 @@ import { MediaService } from './media.service';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { ClientError, NotInDBError, PermissionError } from '../errors/errors';
|
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', () => {
|
describe('MediaService', () => {
|
||||||
let service: MediaService;
|
let service: MediaService;
|
||||||
|
@ -70,6 +72,10 @@ describe('MediaService', () => {
|
||||||
.useClass(Repository)
|
.useClass(Repository)
|
||||||
.overrideProvider(getRepositoryToken(Tag))
|
.overrideProvider(getRepositoryToken(Tag))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(MediaUpload))
|
.overrideProvider(getRepositoryToken(MediaUpload))
|
||||||
.useClass(Repository)
|
.useClass(Repository)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
|
@ -13,10 +13,18 @@ import { AuthorColor } from './author-color.entity';
|
||||||
import { Note } from './note.entity';
|
import { Note } from './note.entity';
|
||||||
import { NotesService } from './notes.service';
|
import { NotesService } from './notes.service';
|
||||||
import { Tag } from './tag.entity';
|
import { Tag } from './tag.entity';
|
||||||
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([Note, AuthorColor, Tag]),
|
TypeOrmModule.forFeature([
|
||||||
|
Note,
|
||||||
|
AuthorColor,
|
||||||
|
Tag,
|
||||||
|
NoteGroupPermission,
|
||||||
|
NoteUserPermission,
|
||||||
|
]),
|
||||||
forwardRef(() => RevisionsModule),
|
forwardRef(() => RevisionsModule),
|
||||||
UsersModule,
|
UsersModule,
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { AuthorColor } from './author-color.entity';
|
||||||
import { Note } from './note.entity';
|
import { Note } from './note.entity';
|
||||||
import { NotesService } from './notes.service';
|
import { NotesService } from './notes.service';
|
||||||
import { Tag } from './tag.entity';
|
import { Tag } from './tag.entity';
|
||||||
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
|
|
||||||
describe('NotesService', () => {
|
describe('NotesService', () => {
|
||||||
let service: NotesService;
|
let service: NotesService;
|
||||||
|
@ -53,6 +55,10 @@ describe('NotesService', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Tag))
|
.overrideProvider(getRepositoryToken(Tag))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.compile();
|
.compile();
|
||||||
service = module.get<NotesService>(NotesService);
|
service = module.get<NotesService>(NotesService);
|
||||||
});
|
});
|
||||||
|
|
|
@ -159,6 +159,7 @@ export class NotesService {
|
||||||
historyEntries: [],
|
historyEntries: [],
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userName: 'Testy',
|
userName: 'Testy',
|
||||||
|
groups: [],
|
||||||
},
|
},
|
||||||
description: 'Very descriptive text.',
|
description: 'Very descriptive text.',
|
||||||
userPermissions: [],
|
userPermissions: [],
|
||||||
|
|
|
@ -9,11 +9,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { LoggerModule } from '../logger/logger.module';
|
import { LoggerModule } from '../logger/logger.module';
|
||||||
import { NoteGroupPermission } from './note-group-permission.entity';
|
import { NoteGroupPermission } from './note-group-permission.entity';
|
||||||
import { NoteUserPermission } from './note-user-permission.entity';
|
import { NoteUserPermission } from './note-user-permission.entity';
|
||||||
|
import { PermissionsService } from './permissions.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
exports: [PermissionsService],
|
||||||
TypeOrmModule.forFeature([NoteUserPermission, NoteGroupPermission]),
|
providers: [PermissionsService],
|
||||||
LoggerModule,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class PermissionsModule {}
|
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 { Revision } from './revision.entity';
|
||||||
import { RevisionsService } from './revisions.service';
|
import { RevisionsService } from './revisions.service';
|
||||||
import { Tag } from '../notes/tag.entity';
|
import { Tag } from '../notes/tag.entity';
|
||||||
|
import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
|
||||||
|
import { NoteUserPermission } from '../permissions/note-user-permission.entity';
|
||||||
|
|
||||||
describe('RevisionsService', () => {
|
describe('RevisionsService', () => {
|
||||||
let service: RevisionsService;
|
let service: RevisionsService;
|
||||||
|
@ -48,6 +50,10 @@ describe('RevisionsService', () => {
|
||||||
.useValue({})
|
.useValue({})
|
||||||
.overrideProvider(getRepositoryToken(Tag))
|
.overrideProvider(getRepositoryToken(Tag))
|
||||||
.useValue({})
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteGroupPermission))
|
||||||
|
.useValue({})
|
||||||
|
.overrideProvider(getRepositoryToken(NoteUserPermission))
|
||||||
|
.useValue({})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
service = module.get<RevisionsService>(RevisionsService);
|
service = module.get<RevisionsService>(RevisionsService);
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import {
|
import {
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
ManyToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
@ -14,6 +15,7 @@ import { Column, OneToMany } from 'typeorm';
|
||||||
import { Note } from '../notes/note.entity';
|
import { Note } from '../notes/note.entity';
|
||||||
import { AuthToken } from '../auth/auth-token.entity';
|
import { AuthToken } from '../auth/auth-token.entity';
|
||||||
import { Identity } from './identity.entity';
|
import { Identity } from './identity.entity';
|
||||||
|
import { Group } from '../groups/group.entity';
|
||||||
import { HistoryEntry } from '../history/history-entry.entity';
|
import { HistoryEntry } from '../history/history-entry.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@ -52,9 +54,15 @@ export class User {
|
||||||
@OneToMany((_) => Identity, (identity) => identity.user)
|
@OneToMany((_) => Identity, (identity) => identity.user)
|
||||||
identities: Identity[];
|
identities: Identity[];
|
||||||
|
|
||||||
|
@ManyToMany((_) => Group, (group) => group.members)
|
||||||
|
groups: Group[];
|
||||||
|
|
||||||
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
|
@OneToMany((_) => HistoryEntry, (historyEntry) => historyEntry.user)
|
||||||
historyEntries: HistoryEntry[];
|
historyEntries: HistoryEntry[];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
public static create(
|
public static create(
|
||||||
userName: string,
|
userName: string,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
|
|
|
@ -20,11 +20,15 @@ import { PermissionsModule } from '../../src/permissions/permissions.module';
|
||||||
import { AuthModule } from '../../src/auth/auth.module';
|
import { AuthModule } from '../../src/auth/auth.module';
|
||||||
import { TokenAuthGuard } from '../../src/auth/token-auth.guard';
|
import { TokenAuthGuard } from '../../src/auth/token-auth.guard';
|
||||||
import { MockAuthGuard } from '../../src/auth/mock-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';
|
import { UsersModule } from '../../src/users/users.module';
|
||||||
|
|
||||||
describe('Notes', () => {
|
describe('Notes', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let notesService: NotesService;
|
let notesService: NotesService;
|
||||||
|
let user: User;
|
||||||
|
let content: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const moduleRef = await Test.createTestingModule({
|
const moduleRef = await Test.createTestingModule({
|
||||||
|
@ -56,14 +60,16 @@ describe('Notes', () => {
|
||||||
app = moduleRef.createNestApplication();
|
app = moduleRef.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
notesService = moduleRef.get(NotesService);
|
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 () => {
|
it('POST /notes', async () => {
|
||||||
const newNote = 'This is a test note.';
|
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.post('/notes')
|
.post('/notes')
|
||||||
.set('Content-Type', 'text/markdown')
|
.set('Content-Type', 'text/markdown')
|
||||||
.send(newNote)
|
.send(content)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
expect(response.body.metadata?.id).toBeDefined();
|
expect(response.body.metadata?.id).toBeDefined();
|
||||||
|
@ -71,88 +77,98 @@ describe('Notes', () => {
|
||||||
await notesService.getCurrentContent(
|
await notesService.getCurrentContent(
|
||||||
await notesService.getNoteByIdOrAlias(response.body.metadata.id),
|
await notesService.getNoteByIdOrAlias(response.body.metadata.id),
|
||||||
),
|
),
|
||||||
).toEqual(newNote);
|
).toEqual(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`GET /notes/{note}`, async () => {
|
describe('GET /notes/{note}', () => {
|
||||||
// check if we can succefully get a note that exists
|
it('works with an existing note', async () => {
|
||||||
await notesService.createNote('This is a test note.', 'test1');
|
// check if we can succefully get a note that exists
|
||||||
const response = await request(app.getHttpServer())
|
await notesService.createNote(content, 'test1', user);
|
||||||
.get('/notes/test1')
|
const response = await request(app.getHttpServer())
|
||||||
.expect('Content-Type', /json/)
|
.get('/notes/test1')
|
||||||
.expect(200);
|
.expect('Content-Type', /json/)
|
||||||
expect(response.body.content).toEqual('This is a test note.');
|
.expect(200);
|
||||||
|
expect(response.body.content).toEqual(content);
|
||||||
// check if a missing note correctly returns 404
|
});
|
||||||
await request(app.getHttpServer())
|
it('fails with an non-existing note', async () => {
|
||||||
.get('/notes/i_dont_exist')
|
// check if a missing note correctly returns 404
|
||||||
.expect('Content-Type', /json/)
|
await request(app.getHttpServer())
|
||||||
.expect(404);
|
.get('/notes/i_dont_exist')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`POST /notes/{note}`, async () => {
|
describe('POST /notes/{note}', () => {
|
||||||
const newNote = 'This is a test note.';
|
it('works with a non-existing alias', async () => {
|
||||||
const response = await request(app.getHttpServer())
|
const response = await request(app.getHttpServer())
|
||||||
.post('/notes/test2')
|
.post('/notes/test2')
|
||||||
.set('Content-Type', 'text/markdown')
|
.set('Content-Type', 'text/markdown')
|
||||||
.send(newNote)
|
.send(content)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(201);
|
.expect(201);
|
||||||
expect(response.body.metadata?.id).toBeDefined();
|
expect(response.body.metadata?.id).toBeDefined();
|
||||||
return expect(
|
return expect(
|
||||||
await notesService.getCurrentContent(
|
await notesService.getCurrentContent(
|
||||||
await notesService.getNoteByIdOrAlias(response.body.metadata?.id),
|
await notesService.getNoteByIdOrAlias(response.body.metadata?.id),
|
||||||
),
|
),
|
||||||
).toEqual(newNote);
|
).toEqual(content);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`DELETE /notes/{note}`, async () => {
|
describe('DELETE /notes/{note}', () => {
|
||||||
await notesService.createNote('This is a test note.', 'test3');
|
it('works with an existing alias', async () => {
|
||||||
await request(app.getHttpServer()).delete('/notes/test3').expect(200);
|
await notesService.createNote(content, 'test3', user);
|
||||||
await expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual(
|
await request(app.getHttpServer()).delete('/notes/test3').expect(200);
|
||||||
new NotInDBError("Note with id/alias 'test3' not found."),
|
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')
|
it('fails with a non-existing alias', async () => {
|
||||||
.expect(404);
|
await request(app.getHttpServer())
|
||||||
|
.delete('/notes/i_dont_exist')
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`PUT /notes/{note}`, async () => {
|
describe('PUT /notes/{note}', () => {
|
||||||
await notesService.createNote('This is a test note.', 'test4');
|
const changedContent = 'New note text';
|
||||||
const response = await request(app.getHttpServer())
|
it('works with existing alias', async () => {
|
||||||
.put('/notes/test4')
|
await notesService.createNote(content, 'test4', user);
|
||||||
.set('Content-Type', 'text/markdown')
|
const response = await request(app.getHttpServer())
|
||||||
.send('New note text')
|
.put('/notes/test4')
|
||||||
.expect(200);
|
.set('Content-Type', 'text/markdown')
|
||||||
await expect(
|
.send(changedContent)
|
||||||
await notesService.getCurrentContent(
|
.expect(200);
|
||||||
await notesService.getNoteByIdOrAlias('test4'),
|
await expect(
|
||||||
),
|
await notesService.getCurrentContent(
|
||||||
).toEqual('New note text');
|
await notesService.getNoteByIdOrAlias('test4'),
|
||||||
expect(response.body.content).toEqual('New note text');
|
),
|
||||||
|
).toEqual(changedContent);
|
||||||
// check if a missing note correctly returns 404
|
expect(response.body.content).toEqual(changedContent);
|
||||||
await request(app.getHttpServer())
|
});
|
||||||
.put('/notes/i_dont_exist')
|
it('fails with a non-existing alias', async () => {
|
||||||
.set('Content-Type', 'text/markdown')
|
await request(app.getHttpServer())
|
||||||
.expect('Content-Type', /json/)
|
.put('/notes/i_dont_exist')
|
||||||
.expect(404);
|
.set('Content-Type', 'text/markdown')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /notes/{note}/metadata', () => {
|
describe('GET /notes/{note}/metadata', () => {
|
||||||
it(`returns complete metadata object`, async () => {
|
it('returns complete metadata object', async () => {
|
||||||
await notesService.createNote('This is a test note.', 'test6');
|
await notesService.createNote(content, 'test5', user);
|
||||||
const metadata = await request(app.getHttpServer())
|
const metadata = await request(app.getHttpServer())
|
||||||
.get('/notes/test6/metadata')
|
.get('/notes/test5/metadata')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
expect(typeof metadata.body.id).toEqual('string');
|
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.title).toBeNull();
|
||||||
expect(metadata.body.description).toBeNull();
|
expect(metadata.body.description).toBeNull();
|
||||||
expect(typeof metadata.body.createTime).toEqual('string');
|
expect(typeof metadata.body.createTime).toEqual('string');
|
||||||
expect(metadata.body.editedBy).toEqual([]);
|
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.permissions.sharedToUsers).toEqual([]);
|
expect(metadata.body.permissions.sharedToUsers).toEqual([]);
|
||||||
expect(metadata.body.tags).toEqual([]);
|
expect(metadata.body.tags).toEqual([]);
|
||||||
|
@ -163,7 +179,9 @@ describe('Notes', () => {
|
||||||
expect(typeof metadata.body.updateUser.photo).toEqual('string');
|
expect(typeof metadata.body.updateUser.photo).toEqual('string');
|
||||||
expect(typeof metadata.body.viewCount).toEqual('number');
|
expect(typeof metadata.body.viewCount).toEqual('number');
|
||||||
expect(metadata.body.editedBy).toEqual([]);
|
expect(metadata.body.editedBy).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with non-existing alias', async () => {
|
||||||
// check if a missing note correctly returns 404
|
// check if a missing note correctly returns 404
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.get('/notes/i_dont_exist/metadata')
|
.get('/notes/i_dont_exist/metadata')
|
||||||
|
@ -173,66 +191,75 @@ describe('Notes', () => {
|
||||||
|
|
||||||
it('has the correct update/create dates', async () => {
|
it('has the correct update/create dates', async () => {
|
||||||
// create a note
|
// create a note
|
||||||
const note = await notesService.createNote(
|
const note = await notesService.createNote(content, 'test5a', user);
|
||||||
'This is a test note.',
|
|
||||||
'test6a',
|
|
||||||
);
|
|
||||||
// save the creation time
|
// save the creation time
|
||||||
const createDate = (await note.revisions)[0].createdAt;
|
const createDate = (await note.revisions)[0].createdAt;
|
||||||
// wait one second
|
// wait one second
|
||||||
await new Promise((r) => setTimeout(r, 1000));
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
// update the note
|
// update the note
|
||||||
await notesService.updateNoteByIdOrAlias('test6a', 'More test content');
|
await notesService.updateNoteByIdOrAlias('test5a', 'More test content');
|
||||||
const metadata = await request(app.getHttpServer())
|
const metadata = await request(app.getHttpServer())
|
||||||
.get('/notes/test6a/metadata')
|
.get('/notes/test5a/metadata')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
expect(metadata.body.createTime).toEqual(createDate.toISOString());
|
expect(metadata.body.createTime).toEqual(createDate.toISOString());
|
||||||
expect(metadata.body.updateTime).not.toEqual(createDate.toISOString());
|
expect(metadata.body.updateTime).not.toEqual(createDate.toISOString());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`GET /notes/{note}/revisions`, async () => {
|
describe('GET /notes/{note}/revisions', () => {
|
||||||
await notesService.createNote('This is a test note.', 'test7');
|
it('works with existing alias', async () => {
|
||||||
const response = await request(app.getHttpServer())
|
await notesService.createNote(content, 'test6', user);
|
||||||
.get('/notes/test7/revisions')
|
const response = await request(app.getHttpServer())
|
||||||
.expect('Content-Type', /json/)
|
.get('/notes/test6/revisions')
|
||||||
.expect(200);
|
.expect('Content-Type', /json/)
|
||||||
expect(response.body).toHaveLength(1);
|
.expect(200);
|
||||||
|
expect(response.body).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
// check if a missing note correctly returns 404
|
it('fails with non-existing alias', async () => {
|
||||||
await request(app.getHttpServer())
|
// check if a missing note correctly returns 404
|
||||||
.get('/notes/i_dont_exist/revisions')
|
await request(app.getHttpServer())
|
||||||
.expect('Content-Type', /json/)
|
.get('/notes/i_dont_exist/revisions')
|
||||||
.expect(404);
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`GET /notes/{note}/revisions/{revision-id}`, async () => {
|
describe('GET /notes/{note}/revisions/{revision-id}', () => {
|
||||||
const note = await notesService.createNote('This is a test note.', 'test8');
|
it('works with an existing alias', async () => {
|
||||||
const revision = await notesService.getLatestRevision(note);
|
const note = await notesService.createNote(content, 'test7', user);
|
||||||
const response = await request(app.getHttpServer())
|
const revision = await notesService.getLatestRevision(note);
|
||||||
.get('/notes/test8/revisions/' + revision.id)
|
const response = await request(app.getHttpServer())
|
||||||
.expect('Content-Type', /json/)
|
.get('/notes/test7/revisions/' + revision.id)
|
||||||
.expect(200);
|
.expect('Content-Type', /json/)
|
||||||
expect(response.body.content).toEqual('This is a test note.');
|
.expect(200);
|
||||||
|
expect(response.body.content).toEqual(content);
|
||||||
// check if a missing note correctly returns 404
|
});
|
||||||
await request(app.getHttpServer())
|
it('fails with non-existing alias', async () => {
|
||||||
.get('/notes/i_dont_exist/revisions/1')
|
// check if a missing note correctly returns 404
|
||||||
.expect('Content-Type', /json/)
|
await request(app.getHttpServer())
|
||||||
.expect(404);
|
.get('/notes/i_dont_exist/revisions/1')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`GET /notes/{note}/content`, async () => {
|
describe('GET /notes/{note}/content', () => {
|
||||||
await notesService.createNote('This is a test note.', 'test9');
|
it('works with an existing alias', async () => {
|
||||||
const response = await request(app.getHttpServer())
|
await notesService.createNote(content, 'test8', user);
|
||||||
.get('/notes/test9/content')
|
const response = await request(app.getHttpServer())
|
||||||
.expect(200);
|
.get('/notes/test8/content')
|
||||||
expect(response.text).toEqual('This is a test note.');
|
.expect(200);
|
||||||
|
expect(response.text).toEqual(content);
|
||||||
|
});
|
||||||
|
|
||||||
// check if a missing note correctly returns 404
|
it('fails with non-existing alias', async () => {
|
||||||
await request(app.getHttpServer())
|
// check if a missing note correctly returns 404
|
||||||
.get('/notes/i_dont_exist/content')
|
await request(app.getHttpServer())
|
||||||
.expect(404);
|
.get('/notes/i_dont_exist/content')
|
||||||
|
.expect('Content-Type', /text\/markdown/)
|
||||||
|
.expect(404);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
Loading…
Reference in a new issue