Skip to content

Commit

Permalink
N21-1889 Launch school external tools (#5003)
Browse files Browse the repository at this point in the history
* add launch of school external tools
---------

Co-authored-by: Igor Richter <[email protected]>
  • Loading branch information
MarvinOehlerkingCap and IgorCapCoder authored May 16, 2024
1 parent 7397737 commit 9f418a5
Show file tree
Hide file tree
Showing 33 changed files with 1,212 additions and 124 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { ValidationError } from '@shared/common';
import { ContextExternalTool } from '../../../context-external-tool/domain';
import { ContextExternalTool, ContextExternalToolLaunchable } from '../../../context-external-tool/domain';
import { ExternalTool } from '../../../external-tool/domain';
import { SchoolExternalTool } from '../../../school-external-tool/domain';
import { CustomParameter } from '../../domain';
Expand All @@ -12,7 +12,7 @@ import {
ParameterArrayValidator,
} from './rules';

export type ValidatableTool = SchoolExternalTool | ContextExternalTool;
export type ValidatableTool = SchoolExternalTool | ContextExternalTool | ContextExternalToolLaunchable;

@Injectable()
export class CommonToolValidationService {
Expand Down
195 changes: 193 additions & 2 deletions apps/server/src/modules/tool/common/uc/tool-permission-helper.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { ObjectId } from '@mikro-orm/mongodb';
import {
AuthorizableReferenceType,
AuthorizationContext,
AuthorizationContextBuilder,
AuthorizationService,
ForbiddenLoggableException,
AuthorizableReferenceType,
} from '@modules/authorization';
import { BoardDoAuthorizableService, ContentElementService } from '@modules/board';
import { CourseService } from '@modules/learnroom';
Expand All @@ -14,12 +15,13 @@ import { Test, TestingModule } from '@nestjs/testing';
import { BoardDoAuthorizable, ExternalToolElement } from '@shared/domain/domainobject';
import { Permission } from '@shared/domain/interface';
import {
boardDoAuthorizableFactory,
contextExternalToolFactory,
courseFactory,
externalToolElementFactory,
schoolExternalToolFactory,
setupEntities,
userFactory,
boardDoAuthorizableFactory,
} from '@shared/testing';
import { ContextExternalTool, ContextRef } from '../../context-external-tool/domain';
import { ToolContextType } from '../enum';
Expand Down Expand Up @@ -233,4 +235,193 @@ describe('ToolPermissionHelper', () => {
});
});
});

describe('ensureContextPermissionsForSchool', () => {
describe('when a school external tool for context "course" is given', () => {
const setup = () => {
const user = userFactory.buildWithId();
const course = courseFactory.buildWithId();
const schoolExternalTool = schoolExternalToolFactory.buildWithId();
const contextRef = new ContextRef({
id: course.id,
type: ToolContextType.COURSE,
});
const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]);

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
courseService.findById.mockResolvedValueOnce(course);

return {
user,
course,
schoolExternalTool,
contextRef,
context,
};
};

it('should check permission for school external tool', async () => {
const { user, course, schoolExternalTool, context, contextRef } = setup();

await helper.ensureContextPermissionsForSchool(
user,
schoolExternalTool,
contextRef.id,
contextRef.type,
context
);

expect(authorizationService.checkPermission).toHaveBeenCalledTimes(2);
expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, schoolExternalTool, context);
expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, course, context);
});
});

describe('when a school external tool for context "board element" is given', () => {
const setup = () => {
const user = userFactory.buildWithId();
const externalToolElement: ExternalToolElement = externalToolElementFactory.build();
const schoolExternalTool = schoolExternalToolFactory.buildWithId();
const contextRef = new ContextRef({
id: externalToolElement.id,
type: ToolContextType.BOARD_ELEMENT,
});
const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build();
const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]);

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
contentElementService.findById.mockResolvedValueOnce(externalToolElement);
boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board);

return {
user,
board,
schoolExternalTool,
contextRef,
context,
};
};

it('should check permission for school external tool', async () => {
const { user, board, schoolExternalTool, contextRef, context } = setup();

await helper.ensureContextPermissionsForSchool(
user,
schoolExternalTool,
contextRef.id,
contextRef.type,
context
);

expect(authorizationService.checkPermission).toHaveBeenCalledTimes(2);
expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, schoolExternalTool, context);
expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, board, context);
});
});

describe('when a school external tool for context "media board" is given', () => {
const setup = () => {
const user = userFactory.buildWithId();
const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build();
const schoolExternalTool = schoolExternalToolFactory.buildWithId();
const contextRef = new ContextRef({
id: board.id,
type: ToolContextType.MEDIA_BOARD,
});
const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]);

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
boardDoAuthorizableService.findById.mockResolvedValueOnce(board);

return {
user,
board,
schoolExternalTool,
contextRef,
context,
};
};

it('should check permission for school external tool', async () => {
const { user, board, schoolExternalTool, contextRef, context } = setup();

await helper.ensureContextPermissionsForSchool(
user,
schoolExternalTool,
contextRef.id,
contextRef.type,
context
);

expect(authorizationService.checkPermission).toHaveBeenCalledTimes(2);
expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, schoolExternalTool, context);
expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, board, context);
});
});

describe('when the school external tool has an unknown context', () => {
const setup = () => {
const user = userFactory.buildWithId();
const schoolExternalTool = schoolExternalToolFactory.buildWithId();
const contextRef = new ContextRef({
id: new ObjectId().toHexString(),
type: 'unknown type' as unknown as ToolContextType,
});
const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]);

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);

return {
user,
schoolExternalTool,
contextRef,
context,
};
};

it('should throw a forbidden loggable exception', async () => {
const { user, schoolExternalTool, contextRef, context } = setup();

await expect(
helper.ensureContextPermissionsForSchool(user, schoolExternalTool, contextRef.id, contextRef.type, context)
).rejects.toThrowError(
new ForbiddenLoggableException(user.id, AuthorizableReferenceType.ContextExternalToolEntity, context)
);
});
});

describe('when user is unauthorized', () => {
const setup = () => {
const user = userFactory.buildWithId();
const schoolExternalTool = schoolExternalToolFactory.buildWithId();
const contextRef = new ContextRef({
id: new ObjectId().toHexString(),
type: ToolContextType.COURSE,
});
const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]);
const error = new ForbiddenException();

authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);
authorizationService.checkPermission.mockImplementationOnce(() => {
throw error;
});

return {
user,
schoolExternalTool,
contextRef,
context,
error,
};
};

it('should check permission for school external tool and fail', async () => {
const { user, schoolExternalTool, contextRef, context, error } = setup();

await expect(
helper.ensureContextPermissionsForSchool(user, schoolExternalTool, contextRef.id, contextRef.type, context)
).rejects.toThrowError(error);
});
});
});
});
43 changes: 35 additions & 8 deletions apps/server/src/modules/tool/common/uc/tool-permission-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,70 @@ import { CourseService } from '@modules/learnroom';
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { BoardDoAuthorizable } from '@shared/domain/domainobject';
import { Course, User } from '@shared/domain/entity';
import { EntityId } from '@shared/domain/types';
import { ContextExternalTool } from '../../context-external-tool/domain';
import { SchoolExternalTool } from '../../school-external-tool/domain';
import { ToolContextType } from '../enum';

@Injectable()
export class ToolPermissionHelper {
constructor(
@Inject(forwardRef(() => AuthorizationService)) private readonly authorizationService: AuthorizationService,
// invalid dependency on this place it is in UC layer in a other module
// loading of ressources should be part of service layer
// if it must resolve different loadings based on the request it can be added in own service and use in UC
private readonly courseService: CourseService,
private readonly boardElementService: ContentElementService,
private readonly boardService: BoardDoAuthorizableService
) {}

// TODO build interface to get contextDO by contextType
public async ensureContextPermissionsForSchool(
user: User,
schoolExternalTool: SchoolExternalTool,
contextId: EntityId,
contextType: ToolContextType,
context: AuthorizationContext
): Promise<void> {
this.authorizationService.checkPermission(user, schoolExternalTool, context);

await this.checkPermissionsByContextRef(user, contextId, contextType, context);
}

public async ensureContextPermissions(
user: User,
contextExternalTool: ContextExternalTool,
context: AuthorizationContext
): Promise<void> {
this.authorizationService.checkPermission(user, contextExternalTool, context);

switch (contextExternalTool.contextRef.type) {
await this.checkPermissionsByContextRef(
user,
contextExternalTool.contextRef.id,
contextExternalTool.contextRef.type,
context
);
}

private async checkPermissionsByContextRef(
user: User,
contextId: EntityId,
contextType: ToolContextType,
context: AuthorizationContext
): Promise<void> {
switch (contextType) {
case ToolContextType.COURSE: {
const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id);
const course: Course = await this.courseService.findById(contextId);

this.authorizationService.checkPermission(user, course, context);
break;
}
case ToolContextType.BOARD_ELEMENT: {
const boardElement = await this.boardElementService.findById(contextExternalTool.contextRef.id);
const boardElement = await this.boardElementService.findById(contextId);
const board: BoardDoAuthorizable = await this.boardService.getBoardAuthorizable(boardElement);

this.authorizationService.checkPermission(user, board, context);
break;
}
case ToolContextType.MEDIA_BOARD: {
const board: BoardDoAuthorizable = await this.boardService.findById(contextExternalTool.contextRef.id);
const board: BoardDoAuthorizable = await this.boardService.findById(contextId);

this.authorizationService.checkPermission(user, board, context);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { ToolVersion } from '../../common/interface';
import { SchoolExternalToolRefDO } from '../../school-external-tool/domain';
import { ContextRef } from './context-ref';

export interface ContextExternalToolProps {
export interface ContextExternalToolLaunchable {
id?: string;

schoolToolRef: SchoolExternalToolRefDO;

contextRef: ContextRef;

displayName?: string;

parameters: CustomParameterEntry[];
}

export interface ContextExternalToolProps extends ContextExternalToolLaunchable {
displayName?: string;

toolVersion: number;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SchoolExternalTool } from '../../school-external-tool/domain';
import { SchoolExternalToolService } from '../../school-external-tool/service';
import {
ContextExternalTool,
ContextExternalToolLaunchable,
ContextExternalToolWithId,
ContextRef,
RestrictedContextMismatchLoggableException,
Expand Down Expand Up @@ -71,7 +72,7 @@ export class ContextExternalToolService {
return contextExternalTools;
}

public async checkContextRestrictions(contextExternalTool: ContextExternalTool): Promise<void> {
public async checkContextRestrictions(contextExternalTool: ContextExternalToolLaunchable): Promise<void> {
const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(
contextExternalTool.schoolToolRef.schoolToolId
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { CommonToolValidationService } from '../../common/service';
import { ExternalTool } from '../../external-tool/domain';
import { SchoolExternalTool } from '../../school-external-tool/domain';
import { ContextExternalTool } from '../domain';
import { ContextExternalToolLaunchable } from '../domain';

@Injectable()
export class ToolConfigurationStatusService {
Expand All @@ -17,7 +17,7 @@ export class ToolConfigurationStatusService {
public determineToolConfigurationStatus(
externalTool: ExternalTool,
schoolExternalTool: SchoolExternalTool,
contextExternalTool: ContextExternalTool
contextExternalTool: ContextExternalToolLaunchable
): ContextExternalToolConfigurationStatus {
const configurationStatus: ContextExternalToolConfigurationStatus = new ContextExternalToolConfigurationStatus({
isOutdatedOnScopeContext: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { EntityId } from '@shared/domain/types';
import { contextExternalToolFactory, schoolExternalToolFactory, setupEntities, userFactory } from '@shared/testing';
import { ToolContextType } from '../../common/enum';
import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper';
import { SchoolExternalToolWithId } from '../../school-external-tool/domain';
import { SchoolExternalToolService } from '../../school-external-tool';
import { SchoolExternalToolWithId } from '../../school-external-tool/domain';
import { ContextExternalTool, ContextExternalToolWithId } from '../domain';
import { ContextExternalToolService } from '../service';
import { ContextExternalToolValidationService } from '../service/context-external-tool-validation.service';
Expand Down
Loading

0 comments on commit 9f418a5

Please sign in to comment.