From 5adda6d7921585728ee3523ffc134dd0cd03571f Mon Sep 17 00:00:00 2001 From: Joel von der Weid <joel.von-der-weid@hesge.ch> Date: Wed, 22 May 2024 10:27:22 +0200 Subject: [PATCH] Add route to test sonar qualities --- ExpressAPI/assets/OpenAPI/OpenAPI.yaml | 57 +++++++++++++++- ExpressAPI/src/managers/GitlabManager.ts | 1 - ExpressAPI/src/managers/SonarManager.ts | 31 ++++++++- ExpressAPI/src/routes/ApiRoutesManager.ts | 2 + ExpressAPI/src/routes/BaseRoutes.ts | 11 --- ExpressAPI/src/routes/SonarRoutes.ts | 82 +++++++++++++++++++++++ ExpressAPI/src/shared | 2 +- 7 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 ExpressAPI/src/routes/SonarRoutes.ts diff --git a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml index c435bfe..7014e5c 100644 --- a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml +++ b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml @@ -24,10 +24,12 @@ servers: tags: - name: General description: '' + - name: Sonar + description: Routes that are used to manage SonarQube information - name: Session description: Routes that are used to manage the user's session - name: Gitlab - description: Routes that are used to provide Gitlab informations + description: Routes that are used to provide Gitlab information - name: Assignment description: Routes that are used to manage assignments - name: Exercise @@ -48,10 +50,10 @@ paths: description: OK default: $ref: '#/components/responses/ERROR' - /sonar: + /sonar/info: get: tags: - - General + - Sonar summary: Check sonar status description: This route can be used to check if the server supports sonar and if the integration is enabled. responses: @@ -80,6 +82,55 @@ paths: description: OK default: $ref: '#/components/responses/ERROR' + /sonar/testqualities: + post: + tags: + - Sonar + summary: Test existence and validity of a quality gate and quality profiles + description: | + This route should be used at assignment creation to test existence and validity of a quality gate and quality profiles before creating the assignment + **🔒 Security needs:** TeachingStaff or Admin roles + security: + - Clients_Token: [ ] + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + gate: + type: string + profiles: + type: string + format: json + description: JSON string array of quality profiles + required: + - gate + - profiles + responses: + '200': + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/DojoBackendResponse' + - type: object + properties: + data: + type: object + properties: + valid: + type: boolean + badGate: + type: string + description: Name of the gate if invalid, or null + badProfiles: + type: array + items: string + description: List of invalid profiles + description: OK + default: + $ref: '#/components/responses/ERROR' /login: post: tags: diff --git a/ExpressAPI/src/managers/GitlabManager.ts b/ExpressAPI/src/managers/GitlabManager.ts index e138386..3455f8b 100644 --- a/ExpressAPI/src/managers/GitlabManager.ts +++ b/ExpressAPI/src/managers/GitlabManager.ts @@ -148,7 +148,6 @@ class GitlabManager { } async addRepositoryVariable(repoId: number, key: string, value: string, isProtected: boolean, isMasked: boolean): Promise<GitlabMember> { - console.log(key, value); const response = await axios.post<GitlabMember>(this.getApiUrl(GitlabRoute.REPOSITORY_VARIABLES_ADD).replace('{{id}}', String(repoId)), { key : key, variable_type: 'env_var', diff --git a/ExpressAPI/src/managers/SonarManager.ts b/ExpressAPI/src/managers/SonarManager.ts index 9c8946e..ed08283 100644 --- a/ExpressAPI/src/managers/SonarManager.ts +++ b/ExpressAPI/src/managers/SonarManager.ts @@ -47,11 +47,13 @@ class SonarManager { })).data; } - private async executeGetRequest<T>(url: string) { + private async executeGetRequest<T>(url: string, data?: unknown) { + return (await this.instance.get<T>(url, { headers: { Authorization: `Basic ${ btoa(SharedConfig.sonar.token + ":") }` - } + }, + params: data })).data; } @@ -128,6 +130,31 @@ class SonarManager { const resp = await this.executeGetRequest<{ languages: { key: string, name: string }[]}>(this.getApiUrl(SonarRoute.GET_LANGUAGES)) return resp.languages.map(l => l.key) } + + async testQualityGate(gateName: string) { + try { + await this.executeGetRequest(this.getApiUrl(SonarRoute.TEST_GATE), { name: gateName }); + return true; + } catch ( e ) { + return false; + } + } + + async testQualityProfile(profileName: string, language: string) { + try { + const formData = new FormData(); + formData.append('language', language); + formData.append('qualityProfile', profileName); + + const resp = await this.executeGetRequest<{ profiles: { key: string, name: string, language: string }[] }>( + this.getApiUrl(SonarRoute.TEST_PROFILE), formData + ); + + return (resp.profiles.length > 0 && resp.profiles.some(p => p.name === profileName && p.language === language)) + } catch ( e ) { + return false; + } + } } export default new SonarManager(); \ No newline at end of file diff --git a/ExpressAPI/src/routes/ApiRoutesManager.ts b/ExpressAPI/src/routes/ApiRoutesManager.ts index ec4eeb9..d1419c9 100644 --- a/ExpressAPI/src/routes/ApiRoutesManager.ts +++ b/ExpressAPI/src/routes/ApiRoutesManager.ts @@ -5,6 +5,7 @@ import SessionRoutes from './SessionRoutes'; import AssignmentRoutes from './AssignmentRoutes'; import GitlabRoutes from './GitlabRoutes'; import ExerciseRoutes from './ExerciseRoutes'; +import SonarRoutes from './SonarRoutes'; class AdminRoutesManager implements RoutesManager { @@ -14,6 +15,7 @@ class AdminRoutesManager implements RoutesManager { GitlabRoutes.registerOnBackend(backend); AssignmentRoutes.registerOnBackend(backend); ExerciseRoutes.registerOnBackend(backend); + SonarRoutes.registerOnBackend(backend); } } diff --git a/ExpressAPI/src/routes/BaseRoutes.ts b/ExpressAPI/src/routes/BaseRoutes.ts index cdb2317..474f5bb 100644 --- a/ExpressAPI/src/routes/BaseRoutes.ts +++ b/ExpressAPI/src/routes/BaseRoutes.ts @@ -2,15 +2,12 @@ import { Express } from 'express-serve-static-core'; import express from 'express'; import { StatusCodes } from 'http-status-codes'; import RoutesManager from '../express/RoutesManager'; -import SharedSonarManager from '../shared/managers/SharedSonarManager'; -import SonarManager from '../managers/SonarManager'; class BaseRoutes implements RoutesManager { registerOnBackend(backend: Express) { backend.get('/', this.homepage.bind(this)); backend.get('/health_check', this.healthCheck.bind(this)); - backend.get('/sonar', this.sonar.bind(this)); } private async homepage(req: express.Request, res: express.Response) { @@ -20,14 +17,6 @@ class BaseRoutes implements RoutesManager { private async healthCheck(req: express.Request, res: express.Response) { return req.session.sendResponse(res, StatusCodes.OK); } - - private async sonar(req: express.Request, res: express.Response) { - const data = { - sonarEnabled: await SharedSonarManager.isSonarSupported(), - languages: await SonarManager.getLanguages() - }; - return req.session.sendResponse(res, StatusCodes.OK, data); - } } diff --git a/ExpressAPI/src/routes/SonarRoutes.ts b/ExpressAPI/src/routes/SonarRoutes.ts new file mode 100644 index 0000000..50d7a92 --- /dev/null +++ b/ExpressAPI/src/routes/SonarRoutes.ts @@ -0,0 +1,82 @@ +import { Express } from 'express-serve-static-core'; +import express from 'express'; +import { StatusCodes } from 'http-status-codes'; +import RoutesManager from '../express/RoutesManager'; +import SharedSonarManager from '../shared/managers/SharedSonarManager'; +import SonarManager from '../managers/SonarManager'; +import SecurityMiddleware from '../middlewares/SecurityMiddleware'; +import SecurityCheckType from '../types/SecurityCheckType'; +import * as ExpressValidator from 'express-validator'; +import DojoValidators from '../helpers/DojoValidators'; +import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; + +class SonarRoutes implements RoutesManager { + private readonly qualitiesValidator: ExpressValidator.Schema = { + gate : { + trim : true, + notEmpty: false + }, + profiles : { + trim : true, + notEmpty : false, + customSanitizer: DojoValidators.jsonSanitizer + } + }; + + registerOnBackend(backend: Express) { + backend.get('/sonar/info', this.sonar.bind(this)); + backend.post('/sonar/testqualities', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.qualitiesValidator), this.testQualities.bind(this)); + } + + private async sonar(req: express.Request, res: express.Response) { + const data = { + sonarEnabled: await SharedSonarManager.isSonarSupported(), + languages: await SonarManager.getLanguages() + }; + return req.session.sendResponse(res, StatusCodes.OK, data); + } + + private async testQualities(req: express.Request, res: express.Response) { + const params: { + gate: string | undefined, profiles: string[] + } = req.body; + + console.log(params); + + let gateOk = true; + if ((params.gate ?? "") !== "") { + gateOk = await SonarManager.testQualityGate(params.gate ?? "") + } + + let profilesOk = true; + const badProfiles = []; + + for ( const profile of params.profiles ) { + try { + const [ lang, name ] = profile.trim().split('/'); + if ( !await SonarManager.testQualityProfile(name, lang) ) { + profilesOk = false; + badProfiles.push(profile); + } + } catch (e) { + profilesOk = false; + badProfiles.push(profile); + } + } + + console.log(gateOk, profilesOk); + + const data = { + valid: gateOk && profilesOk, + badProfiles: badProfiles, + badGate: (gateOk ? null : params.gate) + }; + + console.log(data); + + return req.session.sendResponse(res, StatusCodes.OK, data); + } +} + + +export default new SonarRoutes(); diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared index 44b2ae3..4d1e63e 160000 --- a/ExpressAPI/src/shared +++ b/ExpressAPI/src/shared @@ -1 +1 @@ -Subproject commit 44b2ae365423ca96ee035d0c21bf36a27141aa79 +Subproject commit 4d1e63ebbbe7e6fec1de74d79a2919047eea5775 -- GitLab