diff --git a/ansible/roles/schulcloud-server-init/tasks/main.yml b/ansible/roles/schulcloud-server-init/tasks/main.yml index 9051679e0ce..c2dd43ad61f 100644 --- a/ansible/roles/schulcloud-server-init/tasks/main.yml +++ b/ansible/roles/schulcloud-server-init/tasks/main.yml @@ -20,6 +20,27 @@ tags: - configmap + - name: Management Api Configmap File + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: management-configmap.yml.j2 + when: WITH_SCHULCLOUD_INIT + tags: + - configmap + + - name: Remove Management Api Configmap File + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + state: absent + api_version: v1 + kind: ConfigMap + name: api-management-configmap + when: not WITH_SCHULCLOUD_INIT + tags: + - configmap + - name: Management Deployment kubernetes.core.k8s: kubeconfig: ~/.kube/config diff --git a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 index 0618bf19269..51f2a1f81a1 100644 --- a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 @@ -8,33 +8,46 @@ metadata: data: update.sh: | #! /bin/bash - # necessary for secret handling and legacy indexes - git clone https://github.com/hpi-schul-cloud/schulcloud-server.git - cd /schulcloud-server - git checkout {{ SCHULCLOUD_SERVER_IMAGE_TAG }} - npm ci - until mongosh $DATABASE__URL --eval "print(\"waited for connection\")" - do - sleep 1 - done - mongosh $DATABASE__URL --eval 'rs.initiate({"_id" : "rs0", "members" : [{"_id" : 0, "host" : "localhost:27017"}]})' - sleep 3 - if [[ $(mongosh --quiet --eval "db.isMaster().setName") != rs0 ]] - then - echo "replicaset config failed :(" - else - echo "gg, hacky mongo replicaset" - fi echo "seeding database" curl --retry 360 --retry-all-errors --retry-delay 10 -X POST 'http://mgmt-svc:3333/api/management/database/seed?with-indexes=true' + get_secret() { + local plainText="$1" + local url="http://mgmt-svc:3333/api/management/database/encrypt-plain-text" + + if [[ -z "$plainText" ]]; then + echo "Error: No plainText argument provided." + return 1 + fi + + # Use curl to capture both the response body and status code + local response + local http_code + response=$(curl -s -w "\n%{http_code}" -X POST "$url" \ + -H "Content-Type: text/plain" \ + --data "$plainText") + + # Split the response into body and status code + http_code=$(echo "$response" | tail -n 1) + response_body=$(echo "$response" | head -n -1) + + # Check if the response status code is 200 + if [[ "$http_code" -ne 200 ]]; then + echo "Error: Request failed with status code $http_code." + return 1 + fi + + # Return the response body (presumably the secret) + echo "$response_body" + return 0 + } # Below is a series of a MongoDB-data initializations, meant for the development and testing # purposes on various dev environments - most of them will only work there. # Test OIDC system configuration used in conjunction with OIDCMOCK deployment. - OIDCMOCK_CLIENT_SECRET=$(node scripts/secret.js -s $AES_KEY -e $OIDCMOCK__CLIENT_SECRET) + OIDCMOCK_CLIENT_SECRET=$(get_secret $OIDCMOCK__CLIENT_SECRET) # Test LDAP server (deployed in the sc-common namespace) configuration (stored in the 'systems' collection). - SEARCH_USER_PASSWORD=$(node scripts/secret.js -s $LDAP_PASSWORD_ENCRYPTION_KEY -e $SC_COMMON_LDAP_PASSWORD) + SEARCH_USER_PASSWORD=$(get_secret $SC_COMMON_LDAP_PASSWORD) mongosh $DATABASE__URL --quiet --eval 'db.systems.insertMany([ { "type" : "ldap", @@ -149,7 +162,7 @@ data: } );' # Sanis configuration (stored in the 'systems' collection + some related documents in other collections). - SANIS_CLIENT_SECRET=$(node scripts/secret.js -s $AES_KEY -e $SANIS_CLIENT_SECRET) + SANIS_CLIENT_SECRET=$(get_secret $SANIS_CLIENT_SECRET) SANIS_SYSTEM_ID=0000d186816abba584714c93 if [[ $SC_THEME == "n21" ]]; then mongosh $DATABASE__URL --quiet --eval 'db.schools.updateMany( @@ -223,8 +236,8 @@ data: ISERV_SYSTEM_ID=0000d186816abba584714c92 # Encrypt secrets that contain IServ's OAuth client secret and LDAP server's search user password. - ISERV_OAUTH_CLIENT_SECRET=$(node scripts/secret.js -s $AES_KEY -e $ISERV_OAUTH_CLIENT_SECRET) - ISERV_LDAP_SEARCH_USER_PASSWORD=$(node scripts/secret.js -s $AES_KEY -e $ISERV_LDAP_SEARCH_USER_PASSWORD) + ISERV_OAUTH_CLIENT_SECRET=$(get_secret $ISERV_OAUTH_CLIENT_SECRET) + ISERV_LDAP_SEARCH_USER_PASSWORD=$(get_secret $ISERV_LDAP_SEARCH_USER_PASSWORD) # Add (or replace) document with the Dev IServ configuration. mongosh $DATABASE__URL --quiet --eval 'db.systems.replaceOne( @@ -269,7 +282,7 @@ data: UNIVENTION_LDAP_FEDERAL_STATE_ID=0000b186816abba584714c53 # Encrypt LDAP server's search user password. - UNIVENTION_LDAP_SEARCH_USER_PASSWORD=$(node scripts/secret.js -s $AES_KEY -e $UNIVENTION_LDAP_SEARCH_USER_PASSWORD) + UNIVENTION_LDAP_SEARCH_USER_PASSWORD=$(get_secret $UNIVENTION_LDAP_SEARCH_USER_PASSWORD) # Add (or replace) document with the test BRB Univention LDAP system configuration. mongosh $DATABASE__URL --quiet --eval 'db.systems.replaceOne( @@ -329,40 +342,6 @@ data: # Perform the final Bettermarks config data init if client secret and URL has been properly set. if [ -n "$BETTERMARKS_CLIENT_SECRET" ] && [ -n "$BETTERMARKS_URL" ] && [ -n "$BETTERMARKS_REDIRECT_DOMAIN" ]; then - # Add document to the 'ltitools' collection with Bettermarks tool configuration. - mongosh $DATABASE__URL --quiet --eval 'db.getCollection("ltitools").replaceOne( - { - "name": "bettermarks", - "isTemplate": true - }, - { - "roles": [], - "privacy_permission": "anonymous", - "openNewTab": true, - "name": "bettermarks", - "url": "'$BETTERMARKS_URL'", - "key": null, - "secret": "'$BETTERMARKS_CLIENT_SECRET'", - "logo_url": "'$BETTERMARKS_LOGO_URL'", - "oAuthClientId": "'$BETTERMARKS_OAUTH_CLIENT_ID'", - "isLocal": true, - "resource_link_id": null, - "lti_version": null, - "lti_message_type": null, - "isTemplate": true, - "skipConsent": false, - "customs": [], - "createdAt": new Date(), - "updatedAt": new Date(), - "__v": 0, - "isHidden": false, - "frontchannel_logout_uri": null - }, - { - "upsert": true - } - );' - # The two steps below (Hydra call and MongoDB insert) were added to automate the actions performed inside # the server when Bettermarks' OAuth client configuration is added manually in SuperHero Dashboard. @@ -423,42 +402,6 @@ data: # This configures nextcloud in superhero dashboard as oauth2 tool and also in hydra if [ -n "$NEXTCLOUD_CLIENT_SECRET" ] && [ -n "$NEXTCLOUD_SOCIALLOGIN_OIDC_INTERNAL_NAME" ]; then - echo "Inserting nextcloud to ltitools..." - # Add document to the 'ltitools' collection - mongosh $DATABASE__URL --quiet --eval 'db.getCollection("ltitools").updateOne( - { - "name": "'$NEXTCLOUD_SOCIALLOGIN_OIDC_INTERNAL_NAME'", - "isTemplate": true - }, - { $setOnInsert: { - "roles": [], - "privacy_permission": "anonymous", - "openNewTab": true, - "name": "'$NEXTCLOUD_SOCIALLOGIN_OIDC_INTERNAL_NAME'", - "url": "'$NEXTCLOUD_BASE_URL'", - "key": null, - "secret": "'$NEXTCLOUD_CLIENT_SECRET'", - "logo_url": "", - "oAuthClientId": "'$NEXTCLOUD_CLIENT_ID'", - "isLocal": true, - "resource_link_id": null, - "lti_version": null, - "lti_message_type": null, - "isTemplate": true, - "skipConsent": true, - "customs": [], - "createdAt": new Date(), - "updatedAt": new Date(), - "__v": 0, - "isHidden": true, - "frontchannel_logout_uri": "'$NEXTCLOUD_BASE_URL'apps/schulcloud/logout" - } }, - { - "upsert": true - } - );' - echo "Inserted nextcloud to ltitools." - # Add Nextcloud client in hydra echo "POSTing nextcloud to hydra..." curl --retry 10 --retry-all-errors --retry-delay 10 \ @@ -521,7 +464,7 @@ data: if [ -n "$CTL_SEED_SECRET_ONLINE_DIA_MATHE" ]; then # Encrypt secrets of external tools that contain an lti11 config. - CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_MATHE) + CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(get_secret $CTL_SEED_SECRET_ONLINE_DIA_MATHE) mongosh $DATABASE__URL --quiet --eval 'db.getCollection("external-tools").updateOne( { "name": "Product Test Onlinediagnose Grundschule - Mathematik", @@ -536,7 +479,7 @@ data: fi if [ -n "$CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH" ]; then # Encrypt secrets of external tools that contain an lti11 config. - CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) + CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(get_secret $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) mongosh $DATABASE__URL --quiet --eval 'db.getCollection("external-tools").updateOne( { "name": "Product Test Onlinediagnose Grundschule - Deutsch", @@ -551,7 +494,7 @@ data: fi if [ -n "$CTL_SEED_SECRET_MERLIN" ]; then # Encrypt secrets of external tools that contain an lti11 config. - CTL_SEED_SECRET_MERLIN=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_MERLIN) + CTL_SEED_SECRET_MERLIN=$(get_secret $CTL_SEED_SECRET_MERLIN) mongosh $DATABASE__URL --quiet --eval 'db.getCollection("external-tools").updateOne( { "name": "Merlin Bibliothek", @@ -583,7 +526,7 @@ data: if [[ $SC_THEME == "thr" ]]; then echo "Adding TSP system to systems collection" - TSP_SYSTEM_OAUTH_CLIENT_SECRET=$(node scripts/secret.js -s $AES_KEY -e $TSP_SYSTEM_OAUTH_CLIENT_SECRET) + TSP_SYSTEM_OAUTH_CLIENT_SECRET=$(get_secret $TSP_SYSTEM_OAUTH_CLIENT_SECRET) mongosh $DATABASE__URL --quiet --eval 'db.systems.insertOne( { "_id": ObjectId("66d707f5c5202ba10c5e6256"), @@ -611,5 +554,3 @@ data: fi # ========== End of TSP system creation - # Database indexes synchronization, it's crucial until we have all the entities in NestJS app. - npm run syncIndexes diff --git a/ansible/roles/schulcloud-server-init/templates/management-configmap.yml.j2 b/ansible/roles/schulcloud-server-init/templates/management-configmap.yml.j2 new file mode 100644 index 00000000000..a044293953d --- /dev/null +++ b/ansible/roles/schulcloud-server-init/templates/management-configmap.yml.j2 @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: api-management-configmap + namespace: {{ NAMESPACE }} + labels: + app: management-deployment +data: + NEST_LOG_LEVEL: "{{ NEST_LOG_LEVEL }}" + EXIT_ON_ERROR: "true" + ENABLE_SYNC_LEGACY_INDEXES_VIA_FEATHERS_SERVICE: "true" diff --git a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 index 3f02564879b..2b66bfe55a0 100644 --- a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 @@ -57,6 +57,8 @@ spec: envFrom: - configMapRef: name: api-configmap + - configMapRef: + name: api-management-configmap - secretRef: name: api-secret - secretRef: diff --git a/apps/server/src/apps/management.app.ts b/apps/server/src/apps/management.app.ts index 90af49a26ab..04f0a2c59db 100644 --- a/apps/server/src/apps/management.app.ts +++ b/apps/server/src/apps/management.app.ts @@ -4,12 +4,11 @@ import { NestFactory } from '@nestjs/core'; import { ExpressAdapter } from '@nestjs/platform-express'; import express from 'express'; -// register source-map-support for debugging import { install as sourceMapInstall } from 'source-map-support'; - -// application imports import { LegacyLogger } from '@src/core/logger'; import { ManagementServerModule } from '@modules/management'; +import { MikroORM } from '@mikro-orm/core'; +import legacyAppPromise = require('../../../../src/app'); import { createRequestLoggerMiddleware } from './helpers/request-logger-middleware'; import { enableOpenApiDocs } from './helpers'; @@ -21,12 +20,24 @@ async function bootstrap() { const nestExpressAdapter = new ExpressAdapter(nestExpress); const nestApp = await NestFactory.create(ManagementServerModule, nestExpressAdapter); + const orm = nestApp.get(MikroORM); nestApp.use(createRequestLoggerMiddleware()); // WinstonLogger nestApp.useLogger(await nestApp.resolve(LegacyLogger)); + // load the legacy feathers/express server + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const feathersExpress = await legacyAppPromise(orm); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + await feathersExpress.setup(); + + // set reference to legacy app as an express setting so we can + // access it over the current request within FeathersServiceProvider + // TODO remove if not needed anymore, needed for legacy indexes + nestExpress.set('feathersApp', feathersExpress); + // customize nest app settings nestApp.enableCors(); enableOpenApiDocs(nestApp, 'docs'); @@ -45,8 +56,8 @@ async function bootstrap() { console.log('#################################'); console.log(`### Start Management Server ###`); - console.log(`### Port: ${port} ###`); - console.log(`### Base path: ${basePath} ###`); + console.log(`### Port: ${port} ###`); + console.log(`### Base path: ${basePath} ###`); console.log('#################################'); } void bootstrap(); diff --git a/apps/server/src/infra/encryption/encryption.module.ts b/apps/server/src/infra/encryption/encryption.module.ts index 816b5b3b94c..0b1c990ef72 100644 --- a/apps/server/src/infra/encryption/encryption.module.ts +++ b/apps/server/src/infra/encryption/encryption.module.ts @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { LegacyLogger, LoggerModule } from '@src/core/logger'; import { DefaultEncryptionService, LdapEncryptionService } from './encryption.interface'; -import { SymetricKeyEncryptionService } from './encryption.service'; +import { SymmetricKeyEncryptionService } from './encryption.service'; function encryptionProviderFactory(configService: ConfigService, logger: LegacyLogger, aesKey: string) { const key = configService.get(aesKey); - return new SymetricKeyEncryptionService(logger, key); + return new SymmetricKeyEncryptionService(logger, key); } @Module({ diff --git a/apps/server/src/infra/encryption/encryption.service.spec.ts b/apps/server/src/infra/encryption/encryption.service.spec.ts index b972ea50ba7..3beb7033e33 100644 --- a/apps/server/src/infra/encryption/encryption.service.spec.ts +++ b/apps/server/src/infra/encryption/encryption.service.spec.ts @@ -1,12 +1,12 @@ import { createMock } from '@golevelup/ts-jest'; import { LegacyLogger } from '@src/core/logger'; -import { SymetricKeyEncryptionService } from './encryption.service'; +import { SymmetricKeyEncryptionService } from './encryption.service'; describe('SymetricKeyEncryptionService', () => { describe('with configure encryption key', () => { const encryptionKey = 'abcdefghijklmnop'; const logger = createMock(); - const encryptionService = new SymetricKeyEncryptionService(logger, encryptionKey); + const encryptionService = new SymmetricKeyEncryptionService(logger, encryptionKey); const testInput = 'testInput'; it('encrypts the input', () => { @@ -33,7 +33,7 @@ describe('SymetricKeyEncryptionService', () => { describe('without configured encryption key', () => { const logger = createMock(); - const encryptionService = new SymetricKeyEncryptionService(logger); + const encryptionService = new SymmetricKeyEncryptionService(logger); const testInput = 'testInput'; beforeEach(() => { diff --git a/apps/server/src/infra/encryption/encryption.service.ts b/apps/server/src/infra/encryption/encryption.service.ts index d23a7395675..75619d9891a 100644 --- a/apps/server/src/infra/encryption/encryption.service.ts +++ b/apps/server/src/infra/encryption/encryption.service.ts @@ -5,7 +5,7 @@ import { LegacyLogger } from '@src/core/logger'; import { EncryptionService } from './encryption.interface'; @Injectable() -export class SymetricKeyEncryptionService implements EncryptionService { +export class SymmetricKeyEncryptionService implements EncryptionService { constructor(private logger: LegacyLogger, private key?: string) { if (!this.key) { this.logger.warn('No AES key defined. Encryption will no work'); diff --git a/apps/server/src/infra/feathers/feathers-service.provider.ts b/apps/server/src/infra/feathers/feathers-service.provider.ts index 6efd72aad1c..c8456623fc8 100644 --- a/apps/server/src/infra/feathers/feathers-service.provider.ts +++ b/apps/server/src/infra/feathers/feathers-service.provider.ts @@ -8,13 +8,13 @@ export interface FeathersService { * * @param id * @param params - * @deprecated Access legacy eathers service get method + * @deprecated Access legacy feathers service get method */ get(id: string, params?: FeathersServiceParams): Promise; /** * * @param params - * @deprecated Access legacy eathers service find method + * @deprecated Access legacy feathers service find method */ find(params?: FeathersServiceParams): Promise; /** diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts index cbf443d4d0a..abd4b281459 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts @@ -1,5 +1,5 @@ import { createMock } from '@golevelup/ts-jest'; -import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import { OidcConfig } from '@modules/system/domain'; import { Test, TestingModule } from '@nestjs/testing'; @@ -8,7 +8,7 @@ import { OidcIdentityProviderMapper } from './identity-provider.mapper'; describe('OidcIdentityProviderMapper', () => { let module: TestingModule; let mapper: OidcIdentityProviderMapper; - let defaultEncryptionService: SymetricKeyEncryptionService; + let defaultEncryptionService: SymmetricKeyEncryptionService; afterAll(async () => { await module.close(); @@ -19,7 +19,7 @@ describe('OidcIdentityProviderMapper', () => { imports: [], providers: [ OidcIdentityProviderMapper, - { provide: DefaultEncryptionService, useValue: createMock() }, + { provide: DefaultEncryptionService, useValue: createMock() }, ], }).compile(); defaultEncryptionService = module.get(DefaultEncryptionService); diff --git a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts index 3e18477c5c6..c23c6b70b69 100644 --- a/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts @@ -1,5 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { SymetricKeyEncryptionService } from '@infra/encryption'; +import { SymmetricKeyEncryptionService } from '@infra/encryption'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; @@ -36,7 +36,7 @@ describe('KeycloakConfigurationService Unit', () => { const kcApiClientMock = createMock(); const kcApiAuthenticationManagementMock = createMock(); const kcApiRealmsMock = createMock(); - const encryptionServiceMock = createMock(); + const encryptionServiceMock = createMock(); const adminUsername = 'admin'; const adminUser: UserRepresentation = { diff --git a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts index acd84025ad8..471a791bb2a 100644 --- a/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts +++ b/apps/server/src/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.spec.ts @@ -1,5 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, EncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; @@ -14,7 +14,7 @@ describe('KeycloakIdentityManagementService', () => { let kcIdmOauthService: KeycloakIdentityManagementOauthService; let kcAdminServiceMock: DeepMocked; let httpServiceMock: DeepMocked; - let oAuthEncryptionService: DeepMocked; + let oAuthEncryptionService: DeepMocked; const clientId = 'TheClientId'; const clientSecret = 'TheClientSecret'; diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 804e6b30513..35a015c7cde 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { JwtPayloadFactory } from '@infra/auth-guard'; -import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, EncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; import { Account, AccountService } from '@modules/account'; import { OauthConfig } from '@modules/system'; import { OauthSessionTokenService } from '@modules/oauth'; @@ -46,7 +46,7 @@ describe(AuthenticationService.name, () => { let configService: DeepMocked; let oauthSessionTokenService: DeepMocked; let httpService: DeepMocked; - let oauthEncryptionService: DeepMocked; + let oauthEncryptionService: DeepMocked; const mockAccount: Account = new Account({ id: 'mockAccountId', diff --git a/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts b/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts index 6753f6a2b06..728b22de1d8 100644 --- a/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts +++ b/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts @@ -5,6 +5,8 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { createCollections } from '@shared/testing'; import request from 'supertest'; +import { FeathersServiceProvider } from '@infra/feathers'; +import { createMock } from '@golevelup/ts-jest'; describe('Database Management Controller (API)', () => { let app: INestApplication; @@ -14,7 +16,16 @@ describe('Database Management Controller (API)', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ManagementServerTestModule], - }).compile(); + providers: [ + { + provide: FeathersServiceProvider, + useValue: createMock(), + }, + ], + }) + .overrideProvider(FeathersServiceProvider) + .useValue(createMock()) + .compile(); app = module.createNestApplication(); await app.init(); @@ -60,10 +71,18 @@ describe('Database Management Controller (API)', () => { expect(result.status).toEqual(201); }); - it('should export a collection to filesystem', async () => { + it('should create indexes', async () => { const result = await request(app.getHttpServer()).post(`/management/database/sync-indexes`); expect(result.status).toEqual(201); }); + it('should encrypt plain text', async () => { + const result = await request(app.getHttpServer()) + .post(`/management/database/encrypt-plain-text`) + .send({ plainText: 'hallo uwe' }); + + expect(result.status).toEqual(200); + expect(result.text).not.toHaveLength(0); + }); }); }); diff --git a/apps/server/src/modules/management/controller/database-management.controller.spec.ts b/apps/server/src/modules/management/controller/database-management.controller.spec.ts index e2461abd5c5..bf04549d46a 100644 --- a/apps/server/src/modules/management/controller/database-management.controller.spec.ts +++ b/apps/server/src/modules/management/controller/database-management.controller.spec.ts @@ -1,4 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { createMock } from '@golevelup/ts-jest'; +import { FeathersServiceProvider } from '@infra/feathers'; import { DatabaseManagementUc } from '../uc/database-management.uc'; import { DatabaseManagementController } from './database-management.controller'; @@ -26,6 +28,10 @@ describe('DatabaseManagementController', () => { }, }, }, + { + provide: FeathersServiceProvider, + useValue: createMock(), + }, ], }).compile(); diff --git a/apps/server/src/modules/management/controller/database-management.controller.ts b/apps/server/src/modules/management/controller/database-management.controller.ts index 247fc6edc52..805b0cf9328 100644 --- a/apps/server/src/modules/management/controller/database-management.controller.ts +++ b/apps/server/src/modules/management/controller/database-management.controller.ts @@ -1,9 +1,14 @@ -import { Controller, Param, Post, All, Query } from '@nestjs/common'; +import { Controller, Param, Post, All, Query, Body, HttpCode, Header } from '@nestjs/common'; +import { FeathersServiceProvider } from '@infra/feathers'; +import { EncryptDto } from '@modules/management/controller/dto'; import { DatabaseManagementUc } from '../uc/database-management.uc'; @Controller('management/database') export class DatabaseManagementController { - constructor(private databaseManagementUc: DatabaseManagementUc) {} + constructor( + private databaseManagementUc: DatabaseManagementUc, + private feathersServiceProvider: FeathersServiceProvider + ) {} @All('seed') async importCollections(@Query('with-indexes') withIndexes: boolean): Promise { @@ -30,7 +35,16 @@ export class DatabaseManagementController { } @Post('sync-indexes') - syncIndexes() { + async syncIndexes() { + // it is absolutely crucial to call the legacy stuff first, otherwise it will drop newly created indexes! + await this.feathersServiceProvider.getService('sync-legacy-indexes').create(); return this.databaseManagementUc.syncIndexes(); } + + @Post('encrypt-plain-text') + @Header('content-type', 'text/plain') + @HttpCode(200) + encryptPlainText(@Body() encryptDto: EncryptDto) { + return this.databaseManagementUc.encryptPlainText(encryptDto.plainText); + } } diff --git a/apps/server/src/modules/management/controller/dto/encrypt.dto.ts b/apps/server/src/modules/management/controller/dto/encrypt.dto.ts new file mode 100644 index 00000000000..95eb35e0bba --- /dev/null +++ b/apps/server/src/modules/management/controller/dto/encrypt.dto.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class EncryptDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + public plainText!: string; +} diff --git a/apps/server/src/modules/management/controller/dto/index.ts b/apps/server/src/modules/management/controller/dto/index.ts new file mode 100644 index 00000000000..c558cba77a9 --- /dev/null +++ b/apps/server/src/modules/management/controller/dto/index.ts @@ -0,0 +1 @@ +export * from './encrypt.dto'; diff --git a/apps/server/src/modules/management/management.module.ts b/apps/server/src/modules/management/management.module.ts index caa49da5688..e24bba82766 100644 --- a/apps/server/src/modules/management/management.module.ts +++ b/apps/server/src/modules/management/management.module.ts @@ -9,6 +9,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; +import { FeathersModule } from '@infra/feathers'; import { DatabaseManagementConsole } from './console/database-management.console'; import { DatabaseManagementController } from './controller/database-management.controller'; import { BsonConverter } from './converter/bson.converter'; @@ -20,6 +21,7 @@ const baseImports = [ LoggerModule, ConfigModule.forRoot(createConfigModuleOptions(serverConfig)), EncryptionModule, + FeathersModule, ]; const imports = (Configuration.get('FEATURE_IDENTITY_MANAGEMENT_ENABLED') as boolean) diff --git a/apps/server/src/modules/management/uc/database-management.uc.spec.ts b/apps/server/src/modules/management/uc/database-management.uc.spec.ts index 3c627a6ff7e..13ba1d9b3ba 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.spec.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { DatabaseManagementService } from '@infra/database'; -import { DefaultEncryptionService, LdapEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, LdapEncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; import { FileSystemAdapter } from '@infra/file-system'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { SystemEntity } from '@modules/system/entity'; @@ -21,8 +21,8 @@ describe('DatabaseManagementService', () => { let dbService: DeepMocked; let configService: DeepMocked; let logger: DeepMocked; - let defaultEncryptionService: DeepMocked; - let ldapEncryptionService: DeepMocked; + let defaultEncryptionService: DeepMocked; + let ldapEncryptionService: DeepMocked; let bsonConverter: BsonConverter; const configGetSpy = jest.spyOn(Configuration, 'get'); const configHasSpy = jest.spyOn(Configuration, 'has'); @@ -173,11 +173,11 @@ describe('DatabaseManagementService', () => { providers: [ DatabaseManagementUc, BsonConverter, - { provide: DefaultEncryptionService, useValue: createMock() }, + { provide: DefaultEncryptionService, useValue: createMock() }, { provide: ConfigService, useValue: createMock() }, { provide: LegacyLogger, useValue: createMock() }, { provide: EntityManager, useValue: createMock() }, - { provide: LdapEncryptionService, useValue: createMock() }, + { provide: LdapEncryptionService, useValue: createMock() }, { provide: FileSystemAdapter, useValue: createMock({ diff --git a/apps/server/src/modules/management/uc/database-management.uc.ts b/apps/server/src/modules/management/uc/database-management.uc.ts index e9422066b67..88b3bf78fac 100644 --- a/apps/server/src/modules/management/uc/database-management.uc.ts +++ b/apps/server/src/modules/management/uc/database-management.uc.ts @@ -44,7 +44,7 @@ export class DatabaseManagementUc { } /** - * absolute path reference for seed data base folder. + * absolute path reference for seed database folder. */ private get baseDir(): string { const folderPath = this.fileSystemAdapter.joinPath(__dirname, this.basePath); @@ -374,7 +374,7 @@ export class DatabaseManagementUc { /** * Removes all known secrets (hard coded) from the export. * Manual replacement with the intend placeholders or value is mandatory. - * Currently this affects system and storageproviders collections. + * Currently, this affects system and storageproviders collections. */ private removeSecrets(collectionName: string, jsonDocuments: unknown[]) { if (collectionName === systemsCollectionName) { @@ -418,4 +418,8 @@ export class DatabaseManagementUc { public async migrationPending(): Promise { return this.databaseManagementService.migrationPending(); } + + public encryptPlainText(plainText: string): string { + return this.defaultEncryptionService.encrypt(plainText); + } } diff --git a/apps/server/src/modules/oauth/service/hydra.service.spec.ts b/apps/server/src/modules/oauth/service/hydra.service.spec.ts index 450529bd1ae..60a66e706b2 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.spec.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; @@ -92,7 +92,7 @@ describe('HydraService', () => { }, { provide: DefaultEncryptionService, - useValue: createMock(), + useValue: createMock(), }, { provide: LegacyLogger, diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 1a1101c2dca..308a96a0951 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; -import { DefaultEncryptionService, EncryptionService, SymetricKeyEncryptionService } from '@infra/encryption'; +import { DefaultEncryptionService, EncryptionService, SymmetricKeyEncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { LegacySchoolService } from '@modules/legacy-school'; import { ProvisioningService } from '@modules/provisioning'; @@ -47,7 +47,7 @@ describe('OAuthService', () => { let module: TestingModule; let service: OAuthService; - let oAuthEncryptionService: DeepMocked; + let oAuthEncryptionService: DeepMocked; let provisioningService: DeepMocked; let userService: DeepMocked; let systemService: DeepMocked; diff --git a/config/default.schema.json b/config/default.schema.json index 196edcfc6b9..af1954dba9e 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -861,7 +861,7 @@ }, "AES_KEY": { "type": "string", - "description": "Symetric encryption key used to encrypt and decrypt secrets.", + "description": "Symmetric encryption key used to encrypt and decrypt secrets.", "pattern": ".{16}.*" }, "FEATURE_ETHERPAD_ENABLED": { @@ -1675,6 +1675,11 @@ "type": "boolean", "default": "false", "description": "Enables the external system logout feature" + }, + "ENABLE_SYNC_LEGACY_INDEXES_VIA_FEATHERS_SERVICE": { + "type": "boolean", + "default": "false", + "description": "if calling sync legacy indexes is allowed, only management module should have access to this, this should not exist in the long term when all entites and their indexes have been migrated" } }, "required": [] diff --git a/package.json b/package.json index 06a9cc0204b..4dec643c590 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,6 @@ "nest:test:cov": "jest \"^((?!\\.load\\.spec\\.ts).)*\\.spec\\.ts$\" --coverage --force-exit --maxWorkers='50%'", "nest:test:debug": "jest --runInBand", "nest:lint": "eslint apps --ignore-path .gitignore", - "syncIndexes": "node ./scripts/syncIndexes.js", "ensureIndexes": "npm run nest:start:console -- database sync-indexes", "schoolExport": "node ./scripts/schoolExport.js", "schoolImport": "node ./scripts/schoolImport.js", diff --git a/scripts/syncIndexes.js b/scripts/syncIndexes.js deleted file mode 100644 index 1ff5b89ff56..00000000000 --- a/scripts/syncIndexes.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -/* eslint-disable no-console */ -const mongoose = require('mongoose'); -const util = require('util'); -const appPromise = require('../src/app'); - -const logger = require('../src/logger'); - -const getModels = () => Object.entries(mongoose.models); - -const extractIndexFromModel = ([modelName, model]) => [modelName, (model.schema || {})._indexes]; - -const formatToLog = (data) => util.inspect(data, { depth: 5, compact: true, breakLength: 120 }); - -const syncIndexes = async () => { - try { - logger.alert('load app...'); - await appPromise(); - logger.alert('start syncIndexes..'); - const models = getModels(); - for (const [modelName, model] of models) { - logger.alert(`${modelName}.syncIndexes()`); - try { - // eslint-disable-next-line no-await-in-loop - await model.syncIndexes(); - } catch (err) { - logger.alert(err); - } - } - - logger.alert('..syncIndex finished!'); - - try { - const indexes = models.map(extractIndexFromModel); - logger.alert(formatToLog(indexes)); - } catch (err) { - logger.alert(err); - } - - logger.alert('..script finished!'); - process.exit(0); - } catch (error) { - logger.error(error); - process.exit(1); - } -}; - -syncIndexes(); diff --git a/src/services/index.js b/src/services/index.js index 930ad5fde2d..b1d4e84db8b 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -46,6 +46,7 @@ const etherpad = require('./etherpad'); const storageProvider = require('./storageProvider'); const activation = require('./activation'); const config = require('./config'); +const syncLegacyIndexes = require('./sync-legacy-indexes'); const docs = require('./docs'); module.exports = function initializeServices() { @@ -99,6 +100,7 @@ module.exports = function initializeServices() { app.configure(etherpad); app.configure(storageProvider); app.configure(activation); + app.configure(syncLegacyIndexes); app.configure(config); // initialize events diff --git a/src/services/sync-legacy-indexes/index.js b/src/services/sync-legacy-indexes/index.js new file mode 100644 index 00000000000..5b936117f25 --- /dev/null +++ b/src/services/sync-legacy-indexes/index.js @@ -0,0 +1,43 @@ +const mongoose = require('mongoose'); +const { Configuration } = require('@hpi-schul-cloud/commons'); +const logger = require('../../logger'); + +const getModels = () => Object.entries(mongoose.models); + +class Service { + constructor(options) { + this.options = options || {}; + this.docs = {}; + } + + async create(data, params) { + if (Configuration.get('ENABLE_SYNC_LEGACY_INDEXES_VIA_FEATHERS_SERVICE')) { + const models = getModels(); + for (const [modelName, model] of models) { + logger.alert(`${modelName}.syncIndexes()`); + try { + // eslint-disable-next-line no-await-in-loop + await model.syncIndexes(); + } catch (err) { + logger.alert(err); + } + } + } else { + logger.alert( + 'sync-legacy-indexes has been called without enabling ENABLE_SYNC_LEGACY_INDEXES_VIA_FEATHERS_SERVICE' + ); + } + } + + setup(app) { + this.app = app; + } +} + +module.exports = function () { + const app = this; + + app.use('/sync-legacy-indexes', new Service()); +}; + +module.exports.Service = Service;