diff --git a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml index 1d945690c02269c9dbffe84e8264f30e45af74f9..566a57194de5ce25f63fe4a4b75c2151e0eae824 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 @@ -88,10 +90,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: @@ -120,6 +122,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/SonarManager.ts b/ExpressAPI/src/managers/SonarManager.ts index 9c8946ecaaa17cf974934a717ed613d58a0344c2..ed082834c805dc2077645f9d5c8793361aa63b8f 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 1110c107f068052a254b5c3e1c55c681c2b9f461..d8bad0314a78847b9a6c65d5faa0561536830246 100644 --- a/ExpressAPI/src/routes/ApiRoutesManager.ts +++ b/ExpressAPI/src/routes/ApiRoutesManager.ts @@ -7,6 +7,7 @@ import GitlabRoutes from './GitlabRoutes.js'; import ExerciseRoutes from './ExerciseRoutes.js'; import TagsRoutes from './TagRoutes'; import UserRoutes from './UserRoutes'; +import SonarRoutes from './SonarRoutes'; class AdminRoutesManager implements RoutesManager { @@ -18,6 +19,7 @@ class AdminRoutesManager implements RoutesManager { ExerciseRoutes.registerOnBackend(backend); TagsRoutes.registerOnBackend(backend); UserRoutes.registerOnBackend(backend); + SonarRoutes.registerOnBackend(backend); } } diff --git a/ExpressAPI/src/routes/SonarRoutes.ts b/ExpressAPI/src/routes/SonarRoutes.ts new file mode 100644 index 0000000000000000000000000000000000000000..50d7a924ef14eda39b9c7fdae2a07e4e0fb38e69 --- /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();