diff --git a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 index e369044ea85..a3290e08f3e 100644 --- a/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 +++ b/ansible/roles/h5p-library-management/templates/api-h5p-library-management-cronjob.yml.j2 @@ -30,11 +30,6 @@ spec: git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true volumes: - name: libraries-list configMap: diff --git a/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 index 95443ef537b..84d6f11e4fb 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-delete-s3-files-cronjob.yml.j2 @@ -20,11 +20,6 @@ spec: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: delete-s3-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 index a8c02d02769..7f350b86c97 100644 --- a/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/data-deletion-trigger-cronjob.yml.j2 @@ -29,11 +29,6 @@ spec: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: data-deletion-trigger-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 index 42edd22f4a0..f9b76dc34a7 100644 --- a/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/migration-job.yml.j2 @@ -11,11 +11,6 @@ spec: labels: app: api-migration spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: api-migration-job image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 index 3f702e42e72..80b8e5e5e41 100644 --- a/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/tldraw-delete-files-cronjob.yml.j2 @@ -20,11 +20,6 @@ spec: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: tldraw-delete-files-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 index 66d4e563983..b6c777a1ef2 100644 --- a/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/job_init.yml.j2 @@ -7,11 +7,6 @@ metadata: spec: template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: api-init image: quay.io/schulcloudverbund/infra-tools:latest diff --git a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 index 44e1d95d5c4..41a4d21c783 100644 --- a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 +++ b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-sync-full-cronjob.yml.j2 @@ -20,11 +20,6 @@ spec: activeDeadlineSeconds: {{ SERVER_LDAP_SYNC_FULL_CRONJOB_TIMEOUT|default("39600", true) }} template: spec: - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - runAsNonRoot: true containers: - name: api-ldapsync-cronjob image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} diff --git a/apps/server/src/infra/authorization-client/authorization-client.adapter.ts b/apps/server/src/infra/authorization-client/authorization-client.adapter.ts index daf155fdf14..711aec8717f 100644 --- a/apps/server/src/infra/authorization-client/authorization-client.adapter.ts +++ b/apps/server/src/infra/authorization-client/authorization-client.adapter.ts @@ -1,9 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { RawAxiosRequestConfig } from 'axios'; -import cookie from 'cookie'; import { Request } from 'express'; -import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt'; +import { extractJwtFromHeader } from '@shared/common'; import { AuthorizationApi, AuthorizationBodyParams } from './authorization-api-client'; import { AuthorizationErrorLoggableException, AuthorizationForbiddenLoggableException } from './error'; @@ -19,9 +18,9 @@ export class AuthorizationClientAdapter { } public async hasPermissionsByReference(params: AuthorizationBodyParams): Promise { - const options = this.createOptionParams(params); - try { + const options = this.createOptionParams(); + const response = await this.authorizationApi.authorizationReferenceControllerAuthorizeByReference( params, options @@ -34,34 +33,20 @@ export class AuthorizationClientAdapter { } } - private createOptionParams(params: AuthorizationBodyParams): RawAxiosRequestConfig { - const jwt = this.getJWT(params); + private createOptionParams(): RawAxiosRequestConfig { + const jwt = this.getJwt(); const options: RawAxiosRequestConfig = { headers: { authorization: `Bearer ${jwt}` } }; return options; } - private getJWT(params: AuthorizationBodyParams): string { - const getJWT = ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), this.fromCookie('jwt')]); - const jwt = getJWT(this.request) || this.request.headers.authorization; + private getJwt(): string { + const jwt = extractJwtFromHeader(this.request) || this.request.headers.authorization; if (!jwt) { - const error = new Error('Authentication is required.'); - throw new AuthorizationErrorLoggableException(error, params); + throw new Error('Authentication is required.'); } return jwt; } - - private fromCookie(name: string): JwtFromRequestFunction { - return (request: Request) => { - let token: string | null = null; - const cookies = cookie.parse(request.headers.cookie || ''); - if (cookies && cookies[name]) { - token = cookies[name]; - } - - return token; - }; - } } diff --git a/apps/server/src/modules/authentication/decorator/auth.decorator.ts b/apps/server/src/modules/authentication/decorator/auth.decorator.ts index 0bcfb55abc4..583799977f2 100644 --- a/apps/server/src/modules/authentication/decorator/auth.decorator.ts +++ b/apps/server/src/modules/authentication/decorator/auth.decorator.ts @@ -8,10 +8,9 @@ import { } from '@nestjs/common'; import { ApiBearerAuth } from '@nestjs/swagger'; import { Request } from 'express'; -import { ExtractJwt } from 'passport-jwt'; +import { extractJwtFromHeader } from '@shared/common'; import { JwtAuthGuard } from '../guard/jwt-auth.guard'; import { ICurrentUser, isICurrentUser } from '../interface/user'; -import { JwtExtractor } from '../helper/jwt-extractor'; const STRATEGIES = ['jwt'] as const; type Strategies = typeof STRATEGIES; @@ -56,9 +55,8 @@ export const CurrentUser = createParamDecorator((_, * @requires Authenticated */ export const JWT = createParamDecorator((_, ctx: ExecutionContext) => { - const getJWT = ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), JwtExtractor.fromCookie('jwt')]); const req: Request = ctx.switchToHttp().getRequest(); - const jwt = getJWT(req) || req.headers.authorization; + const jwt = extractJwtFromHeader(req) || req.headers.authorization; if (!jwt) { throw new UnauthorizedException('Authentication is required.'); diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index 124a4c419b8..b2ecb5f4609 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -44,7 +44,7 @@ export class AuthenticationService { async generateJwt(user: CreateJwtPayload): Promise { const jti = randomUUID(); - const result: LoginDto = new LoginDto({ + const result = new LoginDto({ accessToken: this.jwtService.sign(user, { subject: user.accountId, jwtid: jti, diff --git a/apps/server/src/modules/authentication/strategy/jwt.strategy.ts b/apps/server/src/modules/authentication/strategy/jwt.strategy.ts index 0dbe9e39bf4..84014531093 100644 --- a/apps/server/src/modules/authentication/strategy/jwt.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/jwt.strategy.ts @@ -1,21 +1,18 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Strategy } from 'passport-jwt'; +import { extractJwtFromHeader } from '@shared/common'; import { jwtConstants } from '../constants'; import { ICurrentUser } from '../interface'; import { JwtPayload } from '../interface/jwt-payload'; import { CurrentUserMapper } from '../mapper'; -import { JwtExtractor } from '../helper/jwt-extractor'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor(private readonly jwtValidationAdapter: JwtValidationAdapter) { super({ - jwtFromRequest: ExtractJwt.fromExtractors([ - ExtractJwt.fromAuthHeaderAsBearerToken(), - JwtExtractor.fromCookie('jwt'), - ]), + jwtFromRequest: extractJwtFromHeader, ignoreExpiration: false, secretOrKey: jwtConstants.secret, ...jwtConstants.jwtOptions, diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.ts b/apps/server/src/modules/authentication/strategy/local.strategy.ts index 3e762219f7d..30e3f191b2c 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.ts @@ -3,7 +3,7 @@ import { Account } from '@modules/account'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import { GuardAgainst } from '@shared/common/utils/guard-against'; +import { TypeGuard } from '@shared/common'; import { UserRepo } from '@shared/repo'; import bcrypt from 'bcryptjs'; import { Strategy } from 'passport-local'; @@ -28,13 +28,13 @@ export class LocalStrategy extends PassportStrategy(Strategy) { if (this.configService.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED')) { const jwt = await this.idmOauthService.resourceOwnerPasswordGrant(username, password); - GuardAgainst.nullOrUndefined(jwt, new UnauthorizedException()); + TypeGuard.checkNotNullOrUndefined(jwt, new UnauthorizedException()); } else { - const accountPassword = GuardAgainst.nullOrUndefined(account.password, new UnauthorizedException()); + const accountPassword = TypeGuard.checkNotNullOrUndefined(account.password, new UnauthorizedException()); await this.checkCredentials(password, accountPassword, account); } - const accountUserId = GuardAgainst.nullOrUndefined( + const accountUserId = TypeGuard.checkNotNullOrUndefined( account.userId, new Error(`login failing, because account ${account.id} has no userId`) ); @@ -44,8 +44,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) { } private cleanupInput(username?: string, password?: string): { username: string; password: string } { - username = GuardAgainst.nullOrUndefined(username, new UnauthorizedException()); - password = GuardAgainst.nullOrUndefined(password, new UnauthorizedException()); + username = TypeGuard.checkNotNullOrUndefined(username, new UnauthorizedException()); + password = TypeGuard.checkNotNullOrUndefined(password, new UnauthorizedException()); username = this.authenticationService.normalizeUsername(username); password = this.authenticationService.normalizePassword(password); return { username, password }; diff --git a/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts b/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts index ef9bf54b67c..ea76a267da0 100644 --- a/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ws-jwt.strategy.ts @@ -2,11 +2,11 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { WsException } from '@nestjs/websockets'; import { ExtractJwt, Strategy } from 'passport-jwt'; +import { JwtExtractor } from '@shared/common'; import { jwtConstants } from '../constants'; import { ICurrentUser } from '../interface'; import { JwtPayload } from '../interface/jwt-payload'; import { CurrentUserMapper } from '../mapper'; -import { JwtExtractor } from '../helper/jwt-extractor'; import { JwtValidationAdapter } from '../helper/jwt-validation.adapter'; @Injectable() diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts index 9c643f80698..258f06f10c6 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts @@ -3,7 +3,6 @@ import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { ConverterUtil } from '@shared/common'; import { axiosResponseFactory } from '@shared/testing'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; @@ -112,7 +111,6 @@ describe(BBBService.name, () => { let module: TestingModule; let service: BBBServiceTest; let httpService: DeepMocked; - let converterUtil: DeepMocked; let configService: DeepMocked>; beforeAll(async () => { @@ -127,15 +125,10 @@ describe(BBBService.name, () => { provide: HttpService, useValue: createMock(), }, - { - provide: ConverterUtil, - useValue: createMock(), - }, ], }).compile(); service = module.get(BBBServiceTest); httpService = module.get(HttpService); - converterUtil = module.get(ConverterUtil); configService = module.get(ConfigService); }); @@ -147,6 +140,10 @@ describe(BBBService.name, () => { configService.get.mockReturnValue('https://mocked'); }); + afterEach(() => { + jest.resetAllMocks(); + }); + describe('create', () => { describe('when valid parameter passed and the BBB response well', () => { const setup = () => { @@ -156,20 +153,20 @@ describe(BBBService.name, () => { const param = createBBBCreateConfig(); - httpService.post.mockReturnValue(of(bbbCreateResponse)); - converterUtil.xml2object.mockReturnValue(bbbCreateResponse.data); + httpService.post.mockReturnValueOnce(of(bbbCreateResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbCreateResponse.data); - return { param, bbbCreateResponse }; + return { param, bbbCreateResponse, spy }; }; it('should return a response with returncode success', async () => { - const { bbbCreateResponse, param } = setup(); + const { bbbCreateResponse, param, spy } = setup(); const result = await service.create(param); expect(result).toBeDefined(); expect(httpService.post).toHaveBeenCalledTimes(1); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbCreateResponse.data); + expect(spy).toHaveBeenCalledWith(bbbCreateResponse.data); }); }); @@ -182,8 +179,8 @@ describe(BBBService.name, () => { const param = createBBBCreateConfig(); - httpService.post.mockReturnValue(of(bbbCreateResponse)); - converterUtil.xml2object.mockReturnValue(bbbCreateResponse.data); + httpService.post.mockReturnValueOnce(of(bbbCreateResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbCreateResponse.data); const error = new InternalServerErrorException( `${bbbCreateResponse.data.response.messageKey}, ${bbbCreateResponse.data.response.message}` @@ -202,16 +199,6 @@ describe(BBBService.name, () => { await expect(service.create(param)).rejects.toThrowError(expectedError); }); }); - - it('should return a xml configuration with provided presentation url', () => { - const presentationUrl = 'https://s3.hidrive.strato.com/cloud-instances/bbb/presentation.pdf'; - - const result = service.getBbbRequestConfig(presentationUrl); - - expect(result).toBe( - "" - ); - }); }); describe('end', () => { @@ -222,20 +209,20 @@ describe(BBBService.name, () => { ); const bbbBaseMeetingConfig: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbBaseResponse)); - converterUtil.xml2object.mockReturnValue(bbbBaseResponse.data); + httpService.get.mockReturnValueOnce(of(bbbBaseResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbBaseResponse.data); - return { bbbBaseResponse, bbbBaseMeetingConfig }; + return { bbbBaseResponse, bbbBaseMeetingConfig, spy }; }; it('should return a response with returncode success', async () => { - const { bbbBaseResponse, bbbBaseMeetingConfig } = setup(); + const { bbbBaseResponse, bbbBaseMeetingConfig, spy } = setup(); const result = await service.end(bbbBaseMeetingConfig); expect(result).toBeDefined(); expect(httpService.get).toBeCalled(); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbBaseResponse.data); + expect(spy).toHaveBeenCalledWith(bbbBaseResponse.data); }); }); @@ -247,8 +234,8 @@ describe(BBBService.name, () => { bbbBaseResponse.data.response.returncode = BBBStatus.ERROR; const param: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbBaseResponse)); - converterUtil.xml2object.mockReturnValue(bbbBaseResponse.data); + httpService.get.mockReturnValueOnce(of(bbbBaseResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbBaseResponse.data); const error = new InternalServerErrorException( `${bbbBaseResponse.data.response.messageKey}, ${bbbBaseResponse.data.response.message}` @@ -277,19 +264,19 @@ describe(BBBService.name, () => { ); const param: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); - return { bbbMeetingInfoResponse, param }; + return { bbbMeetingInfoResponse, param, spy }; }; it('should return a response with returncode success', async () => { - const { bbbMeetingInfoResponse, param } = setup(); + const { bbbMeetingInfoResponse, param, spy } = setup(); const result = await service.getMeetingInfo(param); expect(result).toBeDefined(); expect(httpService.get).toBeCalled(); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); + expect(spy).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); }); }); @@ -301,8 +288,8 @@ describe(BBBService.name, () => { bbbMeetingInfoResponse.data.response.returncode = BBBStatus.ERROR; const param: BBBBaseMeetingConfig = { meetingID: 'meetingId' }; - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); const error = new InternalServerErrorException( `${bbbMeetingInfoResponse.data.response.messageKey}: ${bbbMeetingInfoResponse.data.response.message}` @@ -331,20 +318,20 @@ describe(BBBService.name, () => { ); const param: BBBJoinConfig = createBBBJoinConfig(); - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + const spy = jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); - return { param, bbbMeetingInfoResponse }; + return { param, bbbMeetingInfoResponse, spy }; }; it('should create a join link to a bbb meeting', async () => { - const { param, bbbMeetingInfoResponse } = setup(); + const { param, bbbMeetingInfoResponse, spy } = setup(); const url = await service.join(param); expect(url).toBeDefined(); expect(httpService.get).toBeCalled(); - expect(converterUtil.xml2object).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); + expect(spy).toHaveBeenCalledWith(bbbMeetingInfoResponse.data); }); }); @@ -356,8 +343,8 @@ describe(BBBService.name, () => { bbbMeetingInfoResponse.data.response.returncode = BBBStatus.ERROR; const param: BBBJoinConfig = createBBBJoinConfig(); - httpService.get.mockReturnValue(of(bbbMeetingInfoResponse)); - converterUtil.xml2object.mockReturnValue(bbbMeetingInfoResponse.data); + httpService.get.mockReturnValueOnce(of(bbbMeetingInfoResponse)); + jest.spyOn(service, 'xml2object').mockReturnValueOnce(bbbMeetingInfoResponse.data); const error = new InternalServerErrorException( `${bbbMeetingInfoResponse.data.response.messageKey}: ${bbbMeetingInfoResponse.data.response.message}` @@ -378,9 +365,9 @@ describe(BBBService.name, () => { }); it('toParams: should return params based on bbb configs', () => { - const createConfig: BBBCreateConfig = createBBBCreateConfig(); + const createConfig = createBBBCreateConfig(); - const params: URLSearchParams = service.superToParams(createConfig); + const params = service.superToParams(createConfig); expect(params.get('name')).toEqual(createConfig.name); expect(params.get('meetingID')).toEqual(createConfig.meetingID); @@ -392,32 +379,40 @@ describe(BBBService.name, () => { expect(params.get('allowModsToUnmuteUsers')).toEqual(String(createConfig.allowModsToUnmuteUsers)); }); - it('generateChecksum: should generate a checksum for queryParams', () => { - // Arrange + const setup = () => { const hashMock: Hash = { update: jest.fn().mockReturnThis(), - digest: jest.fn().mockReturnValue('encrypt 123'), + digest: jest.fn().mockReturnValueOnce('encrypt 123').mockReturnValueOnce('encrypt 123'), } as unknown as Hash; const createHashMock = jest.spyOn(crypto, 'createHash').mockImplementation((): Hash => hashMock); - const createConfig: BBBCreateConfig = createBBBCreateConfig(); + const createConfig = createBBBCreateConfig(); const callName = 'create'; - const urlSearchParams: URLSearchParams = service.superToParams(createConfig); - const queryString: string = urlSearchParams.toString(); + + return { + callName, + createConfig, + createHashMock, + }; + }; + + it('generateChecksum: should generate a checksum for queryParams', () => { + const { callName, createConfig, createHashMock } = setup(); + const urlSearchParams = service.superToParams(createConfig); + const queryString = urlSearchParams.toString(); const sha = crypto.createHash('sha1'); - const expectedChecksum: string = sha.update(callName + queryString + service.getSalt()).digest('hex'); + const expectedChecksum = sha.update(callName + queryString + service.getSalt()).digest('hex'); - const checksum: string = service.superGenerateChecksum(callName, urlSearchParams); + const checksum = service.superGenerateChecksum(callName, urlSearchParams); expect(checksum).toEqual(expectedChecksum); expect(createHashMock).toBeCalledWith('sha1'); }); it('getUrl: should return composed url', () => { - const createConfig = createBBBCreateConfig(); - const callName = 'create'; - const params: URLSearchParams = service.superToParams(createConfig); + const { callName, createConfig } = setup(); + const params = service.superToParams(createConfig); - const url: string = service.superGetUrl(callName, params); + const url = service.superGetUrl(callName, params); expect(url.toString()).toContain(`${service.getBaseUrl()}/bigbluebutton/api/${callName}`); expect(url.includes('checksum')).toBeTruthy(); diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.ts index b40e47053b4..86e2f972ef3 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.ts @@ -1,12 +1,12 @@ import { HttpService } from '@nestjs/axios'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { ConverterUtil } from '@shared/common/utils'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; import crypto from 'crypto'; import { firstValueFrom, Observable } from 'rxjs'; import { URL, URLSearchParams } from 'url'; +import xml2json from '@hendt/xml2json/lib'; import { VideoConferenceConfig } from '../video-conference-config'; import { BbbConfig } from './bbb-config'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig } from './request'; @@ -16,8 +16,7 @@ import { BBBBaseResponse, BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse export class BBBService { constructor( private readonly configService: ConfigService, - private readonly httpService: HttpService, - private readonly converterUtil: ConverterUtil + private readonly httpService: HttpService ) {} protected get baseUrl(): string { @@ -32,13 +31,26 @@ export class BBBService { return this.configService.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION'); } + /** Note no guard, or type check. Should be private. */ + public xml2object(xml: string): T { + const json = xml2json(xml) as T; + + return json; + } + + private checkIfResponseSucces( + bbbResp: BBBResponse | BBBResponse | BBBResponse + ): void { + if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { + throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); + } + } + /** * Creates a new BBB Meeting. The create call is idempotent: you can call it multiple times with the same parameters without side effects. - * @param {BBBCreateConfig} config - * @returns {Promise>} * @throws {InternalServerErrorException} */ - create(config: BBBCreateConfig): Promise> { + public create(config: BBBCreateConfig): Promise> { const url: string = this.getUrl('create', this.toParams(config)); const conf = { headers: { 'Content-Type': 'application/xml' } }; const data = this.getBbbRequestConfig(this.presentationUrl); @@ -46,13 +58,10 @@ export class BBBService { return firstValueFrom(observable) .then((resp: AxiosResponse) => { - const bbbResp = this.converterUtil.xml2object | BBBResponse>( - resp.data - ); - if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { - throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); - } - return bbbResp as BBBResponse; + const bbbResp = this.xml2object>(resp.data); + this.checkIfResponseSucces(bbbResp); + + return bbbResp; }) .catch((error) => { throw new InternalServerErrorException(null, ErrorUtils.createHttpExceptionOptions(error, 'BBBService:create')); @@ -60,7 +69,7 @@ export class BBBService { } // it should be a private method - getBbbRequestConfig(presentationUrl: string): string { + private getBbbRequestConfig(presentationUrl: string): string { if (presentationUrl === '') return ''; return ``; } @@ -71,7 +80,7 @@ export class BBBService { * @returns {Promise} The join url * @throws {InternalServerErrorException} */ - async join(config: BBBJoinConfig): Promise { + public async join(config: BBBJoinConfig): Promise { await this.getMeetingInfo(new BBBBaseMeetingConfig({ meetingID: config.meetingID })); return this.getUrl('join', this.toParams(config)); @@ -83,16 +92,15 @@ export class BBBService { * @returns {BBBResponse} * @throws {InternalServerErrorException} */ - end(config: BBBBaseMeetingConfig): Promise> { + public end(config: BBBBaseMeetingConfig): Promise> { const url: string = this.getUrl('end', this.toParams(config)); const observable: Observable> = this.httpService.get(url); return firstValueFrom(observable) .then((resp: AxiosResponse) => { - const bbbResp = this.converterUtil.xml2object>(resp.data); - if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { - throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); - } + const bbbResp = this.xml2object>(resp.data); + this.checkIfResponseSucces(bbbResp); + return bbbResp; }) .catch((error) => { @@ -102,23 +110,18 @@ export class BBBService { /** * Returns information about a BBB Meeting. - * @param {BBBBaseMeetingConfig} config - * @returns {Promise} * @throws {InternalServerErrorException} */ - getMeetingInfo(config: BBBBaseMeetingConfig): Promise> { + public getMeetingInfo(config: BBBBaseMeetingConfig): Promise> { const url: string = this.getUrl('getMeetingInfo', this.toParams(config)); const observable: Observable> = this.httpService.get(url); return firstValueFrom(observable) .then((resp: AxiosResponse) => { - const bbbResp = this.converterUtil.xml2object< - BBBResponse | BBBResponse - >(resp.data); - if (bbbResp.response.returncode !== BBBStatus.SUCCESS) { - throw new InternalServerErrorException(`${bbbResp.response.messageKey}: ${bbbResp.response.message}`); - } - return bbbResp as BBBResponse; + const bbbResp = this.xml2object>(resp.data); + this.checkIfResponseSucces(bbbResp); + + return bbbResp; }) .catch((error) => { throw new InternalServerErrorException( @@ -128,26 +131,22 @@ export class BBBService { }); } - // should be private /** * Returns a SHA1 encoded checksum for the input parameters. - * @param {string} callName - * @param {URLSearchParams} queryParams - * @returns {string} + * should be private */ protected generateChecksum(callName: string, queryParams: URLSearchParams): string { const queryString: string = queryParams.toString(); const sha = crypto.createHash('sha1'); sha.update(callName + queryString + this.salt); const checksum: string = sha.digest('hex'); + return checksum; } - // should be private /** * Extracts fields from a javascript object and builds a URLSearchParams object from it. - * @param {object} object - * @returns {URLSearchParams} + * should be private */ protected toParams(object: BBBCreateConfig | BBBBaseMeetingConfig): URLSearchParams { const params: URLSearchParams = new URLSearchParams(); @@ -156,15 +155,13 @@ export class BBBService { params.append(key, String(object[key])); } }); + return params; } - // should be private /** * Builds the url for BBB. - * @param callName Name of the BBB api function. - * @param queryParams Parameters for the endpoint. - * @returns {string} A callable url. + * should be private */ protected getUrl(callName: string, queryParams: URLSearchParams): string { const checksum: string = this.generateChecksum(callName, queryParams); diff --git a/apps/server/src/modules/video-conference/video-conference.module.ts b/apps/server/src/modules/video-conference/video-conference.module.ts index 0b7bf02841a..72c2be9fb6e 100644 --- a/apps/server/src/modules/video-conference/video-conference.module.ts +++ b/apps/server/src/modules/video-conference/video-conference.module.ts @@ -5,7 +5,6 @@ import { LegacySchoolModule } from '@modules/legacy-school'; import { UserModule } from '@modules/user'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; -import { ConverterUtil } from '@shared/common'; import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; import { LoggerModule } from '@src/core/logger'; @@ -32,7 +31,6 @@ import { VideoConferenceDeprecatedUc } from './uc'; VideoConferenceRepo, // TODO: N21-1010 clean up video conferences - remove repos TeamsRepo, - ConverterUtil, VideoConferenceService, // TODO: N21-885 remove VideoConferenceDeprecatedUc from providers VideoConferenceDeprecatedUc, diff --git a/apps/server/src/shared/common/guards/type.guard.spec.ts b/apps/server/src/shared/common/guards/type.guard.spec.ts index 9f129244685..fdf31d42889 100644 --- a/apps/server/src/shared/common/guards/type.guard.spec.ts +++ b/apps/server/src/shared/common/guards/type.guard.spec.ts @@ -212,4 +212,96 @@ describe('TypeGuard', () => { }); }); }); + + describe('isNull', () => { + describe('when passing type of value is null', () => { + it('should be return true', () => { + expect(TypeGuard.isNull(null)).toBe(true); + }); + }); + + describe('when passing type of value is NOT null', () => { + it('should be return false', () => { + expect(TypeGuard.isNull(undefined)).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isNull('string')).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isNull('')).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isNull({})).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isNull(1)).toBe(false); + }); + }); + }); + + describe('isUndefined', () => { + describe('when passing type of value is undefined', () => { + it('should be return true', () => { + expect(TypeGuard.isUndefined(undefined)).toBe(true); + }); + }); + + describe('when passing type of value is NOT undefined', () => { + it('should be return false', () => { + expect(TypeGuard.isUndefined(null)).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isUndefined('string')).toBe(false); + }); + + it('should be return true', () => { + expect(TypeGuard.isUndefined('')).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isUndefined({})).toBe(false); + }); + + it('should be return false', () => { + expect(TypeGuard.isUndefined(1)).toBe(false); + }); + }); + }); + + describe('checkNotNullOrUndefined', () => { + describe('when value is null', () => { + it('should throw error if it is passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(null, new Error('Test'))).toThrow('Test'); + }); + + it('should throw default error if not error passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(null)).toThrow('Type is null.'); + }); + }); + + describe('when value is undefined', () => { + it('should throw error if it is passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(undefined, new Error('Test'))).toThrow('Test'); + }); + + it('should throw default error if not error passed', () => { + expect(() => TypeGuard.checkNotNullOrUndefined(undefined)).toThrow('Type is undefined.'); + }); + }); + + describe('when value is defined', () => { + it('should return value if error is passed', () => { + expect(TypeGuard.checkNotNullOrUndefined('', new Error('Test'))).toBe(''); + }); + + it('should return value', () => { + expect(TypeGuard.checkNotNullOrUndefined('')).toBe(''); + }); + }); + }); }); diff --git a/apps/server/src/shared/common/guards/type.guard.ts b/apps/server/src/shared/common/guards/type.guard.ts index 171ca40b596..f7500085d2f 100644 --- a/apps/server/src/shared/common/guards/type.guard.ts +++ b/apps/server/src/shared/common/guards/type.guard.ts @@ -46,4 +46,28 @@ export class TypeGuard { return isObject; } + + static isNull(value: unknown): value is null { + const isNull = value === null; + + return isNull; + } + + static isUndefined(value: unknown): value is undefined { + const isUndefined = value === undefined; + + return isUndefined; + } + + static checkNotNullOrUndefined(value: T | null | undefined, toThrow?: Error): T { + if (TypeGuard.isNull(value)) { + throw toThrow || new Error('Type is null.'); + } + + if (TypeGuard.isUndefined(value)) { + throw toThrow || new Error('Type is undefined.'); + } + + return value; + } } diff --git a/apps/server/src/shared/common/utils/converter.util.spec.ts b/apps/server/src/shared/common/utils/converter.util.spec.ts deleted file mode 100644 index 004166c7c28..00000000000 --- a/apps/server/src/shared/common/utils/converter.util.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ConverterUtil } from '@shared/common'; - -class TestObject { - test: string; - - constructor(test: string) { - this.test = test; - } -} -describe('ConverterUtil', () => { - let service: ConverterUtil; - beforeAll(() => { - service = new ConverterUtil(); - }); - describe('xml2Object', () => { - it('should map correctly to TestObject', () => { - const test = 'test'; - const ret = service.xml2object(test); - expect(ret.test).toEqual('test'); - }); - }); -}); diff --git a/apps/server/src/shared/common/utils/converter.util.ts b/apps/server/src/shared/common/utils/converter.util.ts deleted file mode 100644 index 814bdfda096..00000000000 --- a/apps/server/src/shared/common/utils/converter.util.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import xml2json from '@hendt/xml2json'; - -/** - * This class encapsulates - */ -@Injectable() -export class ConverterUtil { - xml2object(xml: string): T { - return xml2json(xml) as T; - } -} diff --git a/apps/server/src/shared/common/utils/guard-against.spec.ts b/apps/server/src/shared/common/utils/guard-against.spec.ts deleted file mode 100644 index 0f46e6e96b6..00000000000 --- a/apps/server/src/shared/common/utils/guard-against.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GuardAgainst } from '@shared/common'; - -describe('GuardAgainst', () => { - describe('nullOrUndefined', () => { - describe('when value is null', () => { - const error = new Error('value is null'); - - it('should throw', () => { - const value: string | null = null; - expect(() => GuardAgainst.nullOrUndefined(value, error)).toThrow(error.message); - }); - }); - - describe('when value is undefined', () => { - const error = new Error('value is undefined'); - - it('should throw', () => { - const value: string | undefined = undefined; - expect(() => GuardAgainst.nullOrUndefined(value, error)).toThrow(error.message); - }); - }); - - describe('when value is defined', () => { - const error = new Error('value is null'); - - it('should return value', () => { - const value = ''; - expect(GuardAgainst.nullOrUndefined(value, error)).toBe(''); - }); - }); - }); -}); diff --git a/apps/server/src/shared/common/utils/guard-against.ts b/apps/server/src/shared/common/utils/guard-against.ts deleted file mode 100644 index 6425399681a..00000000000 --- a/apps/server/src/shared/common/utils/guard-against.ts +++ /dev/null @@ -1,14 +0,0 @@ -export class GuardAgainst { - /** - * Guards against null or undefined and throws specified exception. - * @param value The value to check. - * @param toThrow The exception to be thrown on failure. - * @returns The narrowed value or throws. - */ - static nullOrUndefined(value: T | null | undefined, toThrow: unknown): T | never { - if (value === null || value === undefined) { - throw toThrow; - } - return value; - } -} diff --git a/apps/server/src/shared/common/utils/index.ts b/apps/server/src/shared/common/utils/index.ts index 90dadf998ed..6c6e6ffe2be 100644 --- a/apps/server/src/shared/common/utils/index.ts +++ b/apps/server/src/shared/common/utils/index.ts @@ -1,3 +1,3 @@ -export * from './converter.util'; -export * from './guard-against'; export { SortHelper } from './sort-helper'; +export { getResolvedValues, isFulfilled } from './promise'; +export { extractJwtFromHeader, JwtExtractor } from './jwt'; diff --git a/apps/server/src/modules/authentication/helper/jwt-extractor.spec.ts b/apps/server/src/shared/common/utils/jwt.spec.ts similarity index 95% rename from apps/server/src/modules/authentication/helper/jwt-extractor.spec.ts rename to apps/server/src/shared/common/utils/jwt.spec.ts index 8186287171a..f0d9a74f22f 100644 --- a/apps/server/src/modules/authentication/helper/jwt-extractor.spec.ts +++ b/apps/server/src/shared/common/utils/jwt.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Request } from 'express'; import { JwtFromRequestFunction } from 'passport-jwt'; -import { JwtExtractor } from './jwt-extractor'; +import { JwtExtractor } from './jwt'; describe('JwtExtractor', () => { let request: DeepMocked; diff --git a/apps/server/src/modules/authentication/helper/jwt-extractor.ts b/apps/server/src/shared/common/utils/jwt.ts similarity index 60% rename from apps/server/src/modules/authentication/helper/jwt-extractor.ts rename to apps/server/src/shared/common/utils/jwt.ts index d54807c2ac2..ebc589236dc 100644 --- a/apps/server/src/modules/authentication/helper/jwt-extractor.ts +++ b/apps/server/src/shared/common/utils/jwt.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { JwtFromRequestFunction } from 'passport-jwt'; +import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt'; import cookie from 'cookie'; export class JwtExtractor { @@ -7,12 +7,15 @@ export class JwtExtractor { return (request: Request) => { let token: string | null = null; const cookies = cookie.parse(request.headers.cookie || ''); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (cookies && cookies[name]) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access token = cookies[name]; } return token; }; } } + +export const extractJwtFromHeader = ExtractJwt.fromExtractors([ + ExtractJwt.fromAuthHeaderAsBearerToken(), + JwtExtractor.fromCookie('jwt'), +]);