Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-3991 - Exchange AuthorizationReferenceService with AuthorizationClientAdapter #5154

Merged
merged 11 commits into from
Aug 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export const AuthorizationBodyParamsReferenceType = {
SUBMISSIONS: 'submissions',
SCHOOL_EXTERNAL_TOOLS: 'school-external-tools',
BOARDNODES: 'boardnodes',
CONTEXT_EXTERNAL_TOOLS: 'context-external-tools'
CONTEXT_EXTERNAL_TOOLS: 'context-external-tools',
EXTERNAL_TOOLS: 'external-tools',
INSTANCES: 'instances'
} as const;

export type AuthorizationBodyParamsReferenceType = typeof AuthorizationBodyParamsReferenceType[keyof typeof AuthorizationBodyParamsReferenceType];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
* This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1.
*
* The version of the OpenAPI document: 3.0
*
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/


// May contain unused imports in some cases
// @ts-ignore
import type { Action } from './action';
Expand All @@ -21,24 +20,21 @@ import type { Action } from './action';
import type { Permission } from './permission';

/**
*
*
* @export
* @interface AuthorizationContextParams
*/
export interface AuthorizationContextParams {
/**
*
* @type {Action}
* @memberof AuthorizationContextParams
*/
'action': Action;
/**
* User permissions that are needed to execute the operation.
* @type {Array<Permission>}
* @memberof AuthorizationContextParams
*/
'requiredPermissions': Array<Permission>;
/**
*
* @type {Action}
* @memberof AuthorizationContextParams
*/
action: Action;
/**
* User permissions that are needed to execute the operation.
* @type {Array<Permission>}
* @memberof AuthorizationContextParams
*/
requiredPermissions: Array<Permission>;
}



Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { extractJwtFromHeader } from '@shared/common';
import { RawAxiosRequestConfig } from 'axios';
import { Request } from 'express';
import { extractJwtFromHeader } from '@shared/common';
import { AuthorizationApi, AuthorizationBodyParams } from './authorization-api-client';
import {
AuthorizationApi,
AuthorizationBodyParamsReferenceType,
AuthorizationContextParams,
} from './authorization-api-client';
import { AuthorizationErrorLoggableException, AuthorizationForbiddenLoggableException } from './error';

@Injectable()
export class AuthorizationClientAdapter {
constructor(private readonly authorizationApi: AuthorizationApi, @Inject(REQUEST) private request: Request) {}

public async checkPermissionsByReference(params: AuthorizationBodyParams): Promise<void> {
const hasPermission = await this.hasPermissionsByReference(params);
public async checkPermissionsByReference(
referenceType: AuthorizationBodyParamsReferenceType,
referenceId: string,
context: AuthorizationContextParams
): Promise<void> {
const hasPermission = await this.hasPermissionsByReference(referenceType, referenceId, context);

if (!hasPermission) {
throw new AuthorizationForbiddenLoggableException(params);
throw new AuthorizationForbiddenLoggableException({ referenceType, referenceId, context });
}
}

public async hasPermissionsByReference(params: AuthorizationBodyParams): Promise<boolean> {
public async hasPermissionsByReference(
referenceType: AuthorizationBodyParamsReferenceType,
referenceId: string,
context: AuthorizationContextParams
): Promise<boolean> {
const params = {
referenceType,
referenceId,
context,
};

try {
const options = this.createOptionParams();

Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/infra/authorization-client/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { Action, AuthorizationBodyParamsReferenceType, AuthorizationContextParams } from './authorization-api-client';
export { AuthorizationClientAdapter } from './authorization-client.adapter';
export { AuthorizationClientModule } from './authorization-client.module';
export { AuthorizationContextBuilder } from './mapper';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Permission } from '@shared/domain/interface';
import { Action, AuthorizationContextParams } from '../authorization-api-client';

export class AuthorizationContextBuilder {
static build(requiredPermissions: Array<Permission>, action: Action): AuthorizationContextParams {
return {
action,
requiredPermissions,
};
}

static write(requiredPermissions: Permission[]): AuthorizationContextParams {
const context = this.build(requiredPermissions, Action.WRITE);

return context;
}

static read(requiredPermissions: Permission[]): AuthorizationContextParams {
const context = this.build(requiredPermissions, Action.READ);

return context;
}
}
1 change: 1 addition & 0 deletions apps/server/src/infra/authorization-client/mapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authorization-context.builder';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Configuration } from '@hpi-schul-cloud/commons';
import { AuthorizationClientConfig } from '@src/infra/authorization-client/authorization-client.module';

export const authorizationClientConfig: AuthorizationClientConfig = {
basePath: `${Configuration.get('API_HOST') as string}/v3/`,
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
};
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class FilesStorageController {
@Res({ passthrough: true }) response: Response,
@Headers('Range') bytesRange?: string
): Promise<StreamableFile> {
const fileResponse = await this.filesStorageUC.download(currentUser.userId, params, bytesRange);
const fileResponse = await this.filesStorageUC.download(params, bytesRange);

const streamableFile = this.streamFileToClient(req, fileResponse, response, bytesRange);

Expand Down Expand Up @@ -190,10 +190,9 @@ export class FilesStorageController {
@Get('/list/:storageLocation/:storageLocationId/:parentType/:parentId')
async list(
@Param() params: FileRecordParams,
@CurrentUser() currentUser: ICurrentUser,
@Query() pagination: PaginationParams
): Promise<FileRecordListResponse> {
const [fileRecords, total] = await this.filesStorageUC.getFileRecordsOfParent(currentUser.userId, params);
const [fileRecords, total] = await this.filesStorageUC.getFileRecordsOfParent(params);
const { skip, limit } = pagination;
const response = FileRecordMapper.mapToFileRecordListResponse(fileRecords, total, skip, limit);

Expand All @@ -214,10 +213,9 @@ export class FilesStorageController {
@UseInterceptors(RequestLoggingInterceptor)
async patchFilename(
@Param() params: SingleFileParams,
@Body() renameFileParam: RenameFileParams,
@CurrentUser() currentUser: ICurrentUser
@Body() renameFileParam: RenameFileParams
): Promise<FileRecordResponse> {
const fileRecord = await this.filesStorageUC.patchFilename(currentUser.userId, params, renameFileParam);
const fileRecord = await this.filesStorageUC.patchFilename(params, renameFileParam);

const response = FileRecordMapper.mapToFileRecordResponse(fileRecord);

Expand All @@ -234,11 +232,8 @@ export class FilesStorageController {
@ApiResponse({ status: 500, type: InternalServerErrorException })
@Delete('/delete/:storageLocation/:storageLocationId/:parentType/:parentId')
@UseInterceptors(RequestLoggingInterceptor)
async deleteByParent(
@Param() params: FileRecordParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<FileRecordListResponse> {
const [fileRecords, total] = await this.filesStorageUC.deleteFilesOfParent(currentUser.userId, params);
async deleteByParent(@Param() params: FileRecordParams): Promise<FileRecordListResponse> {
const [fileRecords, total] = await this.filesStorageUC.deleteFilesOfParent(params);
const response = FileRecordMapper.mapToFileRecordListResponse(fileRecords, total);

return response;
Expand All @@ -251,11 +246,8 @@ export class FilesStorageController {
@ApiResponse({ status: 500, type: InternalServerErrorException })
@Delete('/delete/:fileRecordId')
@UseInterceptors(RequestLoggingInterceptor)
async deleteFile(
@Param() params: SingleFileParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<FileRecordResponse> {
const fileRecord = await this.filesStorageUC.deleteOneFile(currentUser.userId, params);
async deleteFile(@Param() params: SingleFileParams): Promise<FileRecordResponse> {
const fileRecord = await this.filesStorageUC.deleteOneFile(params);

const response = FileRecordMapper.mapToFileRecordResponse(fileRecord);

Expand All @@ -267,11 +259,8 @@ export class FilesStorageController {
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@Post('/restore/:storageLocation/:storageLocationId/:parentType/:parentId')
async restore(
@Param() params: FileRecordParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<FileRecordListResponse> {
const [fileRecords, total] = await this.filesStorageUC.restoreFilesOfParent(currentUser.userId, params);
async restore(@Param() params: FileRecordParams): Promise<FileRecordListResponse> {
const [fileRecords, total] = await this.filesStorageUC.restoreFilesOfParent(params);

const response = FileRecordMapper.mapToFileRecordListResponse(fileRecords, total);

Expand All @@ -283,11 +272,8 @@ export class FilesStorageController {
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@Post('/restore/:fileRecordId')
async restoreFile(
@Param() params: SingleFileParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<FileRecordResponse> {
const fileRecord = await this.filesStorageUC.restoreOneFile(currentUser.userId, params);
async restoreFile(@Param() params: SingleFileParams): Promise<FileRecordResponse> {
const fileRecord = await this.filesStorageUC.restoreOneFile(params);

const response = FileRecordMapper.mapToFileRecordResponse(fileRecord);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { AuthorizationClientModule } from '@infra/authorization-client';
import { AuthenticationModule } from '@modules/authentication';
import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module';
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { createConfigModuleOptions } from '@src/config';
import { CoreModule } from '@src/core';
import { authorizationClientConfig } from './authorization-client-config';
import { FileSecurityController, FilesStorageConfigController, FilesStorageController } from './controller';
import { config } from './files-storage.config';
import { FilesStorageModule } from './files-storage.module';
import { FilesStorageUC } from './uc';

@Module({
imports: [
AuthorizationReferenceModule,
FilesStorageModule,
AuthenticationModule,
AuthorizationClientModule.register(authorizationClientConfig),
CoreModule,
HttpModule,
ConfigModule.forRoot(createConfigModuleOptions(config)),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AuthorizationContextBuilder } from '@infra/authorization-client';
import { Permission } from '@shared/domain/interface';
import { AuthorizationContextBuilder } from '../authorization';

export enum FilesStorageInternalActions {
downloadBySecurityToken = '/file-security/download/:token',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AuthorizableReferenceType } from '@modules/authorization/domain';
import { NotImplementedException, StreamableFile } from '@nestjs/common';
import { AuthorizationBodyParamsReferenceType } from '@src/infra/authorization-client/authorization-api-client';
import { plainToClass } from 'class-transformer';
bischofmax marked this conversation as resolved.
Show resolved Hide resolved
import {
DownloadFileParams,
Expand All @@ -12,20 +12,20 @@ import { FileRecord } from '../entity';
import { FileRecordParentType, GetFileResponse } from '../interface';

export class FilesStorageMapper {
private static authorizationEntityMap: Map<FileRecordParentType, AuthorizableReferenceType> = new Map([
[FileRecordParentType.Task, AuthorizableReferenceType.Task],
[FileRecordParentType.Course, AuthorizableReferenceType.Course],
[FileRecordParentType.User, AuthorizableReferenceType.User],
[FileRecordParentType.School, AuthorizableReferenceType.School],
[FileRecordParentType.Lesson, AuthorizableReferenceType.Lesson],
[FileRecordParentType.Submission, AuthorizableReferenceType.Submission],
[FileRecordParentType.Grading, AuthorizableReferenceType.Submission],
[FileRecordParentType.BoardNode, AuthorizableReferenceType.BoardNode],
[FileRecordParentType.ExternalTool, AuthorizableReferenceType.ExternalTool],
private static authorizationEntityMap: Map<FileRecordParentType, AuthorizationBodyParamsReferenceType> = new Map([
[FileRecordParentType.Task, AuthorizationBodyParamsReferenceType.TASKS],
[FileRecordParentType.Course, AuthorizationBodyParamsReferenceType.COURSES],
[FileRecordParentType.User, AuthorizationBodyParamsReferenceType.USERS],
[FileRecordParentType.School, AuthorizationBodyParamsReferenceType.SCHOOLS],
[FileRecordParentType.Lesson, AuthorizationBodyParamsReferenceType.LESSONS],
[FileRecordParentType.Submission, AuthorizationBodyParamsReferenceType.SUBMISSIONS],
[FileRecordParentType.Grading, AuthorizationBodyParamsReferenceType.SUBMISSIONS],
[FileRecordParentType.BoardNode, AuthorizationBodyParamsReferenceType.BOARDNODES],
[FileRecordParentType.ExternalTool, AuthorizationBodyParamsReferenceType.EXTERNAL_TOOLS],
]);

public static mapToAllowedAuthorizationEntityType(type: FileRecordParentType): AuthorizableReferenceType {
const res: AuthorizableReferenceType | undefined = this.authorizationEntityMap.get(type);
public static mapToAllowedAuthorizationEntityType(type: FileRecordParentType): AuthorizationBodyParamsReferenceType {
const res: AuthorizationBodyParamsReferenceType | undefined = this.authorizationEntityMap.get(type);

if (!res) {
throw new NotImplementedException();
Expand Down
Loading
Loading