Skip to content

Commit

Permalink
BC-5522-Implementation of an API for deleting data (#4533)
Browse files Browse the repository at this point in the history
* endpoints preparation

* auth startegy impl

* api impl

* some changes

* some fixes need in KNL module

* changes in uc and register entities

* Pr fixes and minor changes

* rename file

* some fixes

* add exposing admin API port from the api-svc

* add server test module + delete FileEntity from allEntities

* change module

* admin api server module test impl

* add some tests

* add some test for API

* add some test

* x-api-key.strategy tests

* x-api-key-strategy-tests impl

* remove test for api-setup-helper, remove unused imports

* remove test file

* add info for the Admin API port

* add test for controller

* add test for deletionRequestResponse

* remove not needed parameter from constructor

* add test for deletionRequestLogResponse

* add test for executionParams and requestbodyParams

* replace hard-coded Admin API server port value with the one taken from the var

* change creation of API method location

* change default json

* fix import in server module

* fixes soma issues

* fixes after review

* add admin api object

* default json chnages

* Revert "default json chnages"

This reverts commit 6764f95.

* Revert "add admin api object"

This reverts commit aba6e1a.

* Revert "fixes after review"

This reverts commit 4fc4c70.

* Revert "fixes soma issues"

This reverts commit 3b58d13.

* fixes after review

* Revert "small fixes"

This reverts commit ccf0aef, reversing
changes made to 011b0f7.

* Revert "Revert "small fixes""

This reverts commit 30ab3f4.

* default ADMIN_API_KEY

* fix some imports

* small change in x-api-key.strategy

* Update apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts

Co-authored-by: Bartosz Nowicki <[email protected]>

* change default.schema.json

* new changes to default.schema.json

* fix after review

* add logger for error in deeltionRequestUC

* fix problem with fileentity

* fix imports

* add logger to test in uc of deletionModule

* change module config

* hard-coded Admin API server port after the discussion with Paul

* fixes bug during deletion uders data from lessons

* fix pipeline

* fix pipeline #2

* fix pipeline

* fix lint

* fix modules imports

* fix providers in exports in deletion module

* split deletion module and  deletion-api.module

* move setup sessions to server config

* try to fix test coverage

* Revert "try to fix test coverage"

This reverts commit e336196.

* Revert "move setup sessions to server config"

This reverts commit 86b176d.

* remove api key from default.json

* changes to server module

* use timers in tests

* revert last commit

* add testXApiKeyClient

* deployment impl

* add newlines

* add test

* change character length

* fix imports

* fix deployment in PR

* PR fixes part 1

* fix build and push

* fix prettier

* default schema changes

* small fixes

* fix lint

* add registration pins module

* change sorting of code lines

* small fixes after review

* add enabled in admiApiServer

* add process.exit

* test revert enabled (testing purposes)

* Revert "test revert enabled (testing purposes)"

This reverts commit b2fa12f.

* add default for testers

* Revert "add default for testers"

This reverts commit d589435.

* fix with elsson entity

* fix lint

* changes in main.yml

* fix enabled

---------

Co-authored-by: WojciechGrancow <[email protected]>
Co-authored-by: Bartosz Nowicki <[email protected]>
Co-authored-by: WojciechGrancow <[email protected]>
Co-authored-by: Cedric Evers <[email protected]>
  • Loading branch information
5 people authored and bergatco committed Dec 6, 2023
1 parent 9c79462 commit dc47095
Show file tree
Hide file tree
Showing 101 changed files with 1,469 additions and 218 deletions.
15 changes: 15 additions & 0 deletions ansible/roles/schulcloud-server-core/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@
- KEDA_ENABLED is defined and KEDA_ENABLED|bool
- SCALED_PREVIEW_GENERATOR_ENABLED is defined and SCALED_PREVIEW_GENERATOR_ENABLED|bool


- name: admin api server deployment
kubernetes.core.k8s:
kubeconfig: ~/.kube/config
namespace: "{{ NAMESPACE }}"
template: admin-api-server-deployment.yml.j2
when: WITH_API_ADMIN is defined and WITH_API_ADMIN|bool

- name: admin api server service
kubernetes.core.k8s:
kubeconfig: ~/.kube/config
namespace: "{{ NAMESPACE }}"
template: admin-api-server-svc.yml.j2
when: WITH_API_ADMIN is defined and WITH_API_ADMIN|bool

- name: TlDraw server deployment
kubernetes.core.k8s:
kubeconfig: ~/.kube/config
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: admin-api-deployment
namespace: {{ NAMESPACE }}
labels:
app: api-admin
app.kubernetes.io/part-of: schulcloud-verbund
app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }}
app.kubernetes.io/name: api-admin
app.kubernetes.io/component: server
app.kubernetes.io/managed-by: ansible
git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }}
git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }}
spec:
replicas: {{ ADMIN_API_SERVER_REPLICAS|default("1", true) }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
#maxUnavailable: 1
revisionHistoryLimit: 4
paused: false
selector:
matchLabels:
app: api-admin
template:
metadata:
labels:
app: api-admin
app.kubernetes.io/part-of: schulcloud-verbund
app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }}
app.kubernetes.io/name: api-admin
app.kubernetes.io/component: server
app.kubernetes.io/managed-by: ansible
git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }}
git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }}
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
runAsNonRoot: true
containers:
- name: api-admin
image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }}
imagePullPolicy: IfNotPresent
ports:
- containerPort: 4030
name: admin
protocol: TCP
envFrom:
- configMapRef:
name: api-configmap
- secretRef:
name: api-secret
command: ['npm', 'run', 'nest:start:admin-api-server:prod']
resources:
limits:
cpu: {{ ADMIN_API_SERVER_CPU_LIMITS|default("2000m", true) }}
memory: {{ ADMIN_API_SERVER_MEMORY_LIMITS|default("4Gi", true) }}
requests:
cpu: {{ ADMIN_API_SERVER_CPU_REQUESTS|default("100m", true) }}
memory: {{ ADMIN_API_SERVER_MEMORY_REQUESTS|default("150Mi", true) }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: api-admin-svc
namespace: {{ NAMESPACE }}
labels:
app: api-admin
spec:
type: ClusterIP
ports:
# port for http managing drawing data
- port: 4030
targetPort: 4030
protocol: TCP
name: admin
selector:
app: api-admin
43 changes: 43 additions & 0 deletions apps/server/src/apps/admin-api-server.app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* istanbul ignore file */
import { NestFactory } from '@nestjs/core';
import { install as sourceMapInstall } from 'source-map-support';
import { LegacyLogger, Logger } from '@src/core/logger';
import { enableOpenApiDocs } from '@shared/controller/swagger';
import { AppStartLoggable } from '@src/apps/helpers/app-start-loggable';
import { ExpressAdapter } from '@nestjs/platform-express';
import express from 'express';
import { AdminApiServerModule } from '@src/modules/server/admin-api.server.module';
import { Configuration } from '@hpi-schul-cloud/commons/lib';

async function bootstrap() {
sourceMapInstall();

const nestAdminServerExpress = express();
const nestAdminServerExpressAdapter = new ExpressAdapter(nestAdminServerExpress);
nestAdminServerExpressAdapter.disable('x-powered-by');

const nestAdminServerApp = await NestFactory.create(AdminApiServerModule, nestAdminServerExpressAdapter);
const logger = await nestAdminServerApp.resolve(Logger);
const legacyLogger = await nestAdminServerApp.resolve(LegacyLogger);
nestAdminServerApp.useLogger(legacyLogger);
nestAdminServerApp.enableCors();

enableOpenApiDocs(nestAdminServerApp, 'docs');
nestAdminServerApp.setGlobalPrefix('/admin/api/v1');

await nestAdminServerApp.init();

const adminApiServerPort = Configuration.get('ADMIN_API__PORT') as number;

nestAdminServerExpress.listen(adminApiServerPort, () => {
logger.info(
new AppStartLoggable({
appName: 'Admin API server app',
port: adminApiServerPort,
mountsDescription: `/admin/api/v1 --> Admin API Server`,
})
);
});
}

void bootstrap();
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { JwtStrategy } from './strategy/jwt.strategy';
import { LdapStrategy } from './strategy/ldap.strategy';
import { LocalStrategy } from './strategy/local.strategy';
import { Oauth2Strategy } from './strategy/oauth2.strategy';
import { XApiKeyStrategy } from './strategy/x-api-key.strategy';

// values copied from Algorithm definition. Type does not exist at runtime and can't be checked anymore otherwise
const algorithms = [
Expand Down Expand Up @@ -76,6 +77,7 @@ const jwtModuleOptions: JwtModuleOptions = {
LdapService,
LdapStrategy,
Oauth2Strategy,
XApiKeyStrategy,
],
exports: [AuthenticationService],
})
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/authentication/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './x-api-key.config';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface XApiKeyConfig {
ADMIN_API__ALLOWED_API_KEYS: string[];
}
1 change: 1 addition & 0 deletions apps/server/src/modules/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { AuthenticationModule } from './authentication.module';
export { Authenticate, CurrentUser, JWT } from './decorator';
export { ICurrentUser } from './interface';
export { AuthenticationService } from './services';
export { XApiKeyConfig } from './config';
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { UnauthorizedException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { createMock } from '@golevelup/ts-jest';
import { XApiKeyStrategy } from './x-api-key.strategy';
import { XApiKeyConfig } from '../config/x-api-key.config';

describe('XApiKeyStrategy', () => {
let module: TestingModule;
let strategy: XApiKeyStrategy;
let configService: ConfigService<XApiKeyConfig, true>;

beforeAll(async () => {
module = await Test.createTestingModule({
imports: [],
providers: [
XApiKeyStrategy,
{
provide: ConfigService,
useValue: createMock<ConfigService<XApiKeyConfig, true>>({ get: () => ['1ab2c3d4e5f61ab2c3d4e5f6'] }),
},
],
}).compile();

strategy = module.get(XApiKeyStrategy);
configService = module.get(ConfigService<XApiKeyConfig, true>);
});

afterAll(async () => {
await module.close();
});

beforeEach(() => {
jest.resetAllMocks();
});

describe('validate', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const done = jest.fn((error: Error | null, data: boolean | null) => {});
describe('when a valid api key is provided', () => {
const setup = () => {
const CORRECT_API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6';

return { CORRECT_API_KEY, done };
};
it('should do nothing', () => {
const { CORRECT_API_KEY } = setup();
strategy.validate(CORRECT_API_KEY, done);
expect(done).toBeCalledWith(null, true);
});
});

describe('when a invalid api key is provided', () => {
const setup = () => {
const INVALID_API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6778173';

return { INVALID_API_KEY, done };
};
it('should throw error', () => {
const { INVALID_API_KEY } = setup();
strategy.validate(INVALID_API_KEY, done);
expect(done).toBeCalledWith(new UnauthorizedException(), null);
});
});
});

describe('constructor', () => {
it('should create strategy', () => {
const ApiKeyStrategy = new XApiKeyStrategy(configService);
expect(ApiKeyStrategy).toBeDefined();
expect(ApiKeyStrategy).toBeInstanceOf(XApiKeyStrategy);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import Strategy from 'passport-headerapikey';
import { XApiKeyConfig } from '../config/x-api-key.config';

@Injectable()
export class XApiKeyStrategy extends PassportStrategy(Strategy, 'api-key') {
private readonly allowedApiKeys: string[];

constructor(private readonly configService: ConfigService<XApiKeyConfig, true>) {
super({ header: 'X-API-KEY' }, false);
this.allowedApiKeys = this.configService.get<string[]>('ADMIN_API__ALLOWED_API_KEYS');
}

public validate = (apiKey: string, done: (error: Error | null, data: boolean | null) => void) => {
if (this.allowedApiKeys.includes(apiKey)) {
done(null, true);
}
done(new UnauthorizedException(), null);
};
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ObjectId } from '@mikro-orm/mongodb';
import { BatchDeletionSummaryDetail } from '..';
import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services';
import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../services';
import { BatchDeletionSummaryDetailBuilder } from './batch-deletion-summary-detail.builder';
import { BatchDeletionSummaryDetail } from '../interface';

describe(BatchDeletionSummaryDetailBuilder.name, () => {
describe(BatchDeletionSummaryDetailBuilder.build.name, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services';
import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../services';
import { BatchDeletionSummaryDetail } from '../interface';

export class BatchDeletionSummaryDetailBuilder {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum';
import { DeletionLogStatisticBuilder } from './deletion-log-statistic.builder';
import { DeletionDomainModel } from '../domain/types';
import { DeletionLogStatisticBuilder } from '.';

describe(DeletionLogStatisticBuilder.name, () => {
afterAll(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum';
import { DeletionDomainModel } from '../domain/types';
import { DeletionLogStatistic } from '../interface';

export class DeletionLogStatisticBuilder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ObjectId } from 'bson';
import { DeletionDomainModel } from '../domain/types';
import { DeletionRequestBodyPropsBuilder } from './deletion-request-body-props.builder';

describe(DeletionRequestBodyPropsBuilder.name, () => {
afterAll(() => {
jest.clearAllMocks();
});
describe('when create deletionRequestBodyParams', () => {
const setup = () => {
const domain = DeletionDomainModel.PSEUDONYMS;
const refId = new ObjectId().toHexString();
const deleteInMinutes = 1000;
return { domain, refId, deleteInMinutes };
};
it('should build deletionRequestBodyParams with all attributes', () => {
const { domain, refId, deleteInMinutes } = setup();

const result = DeletionRequestBodyPropsBuilder.build(domain, refId, deleteInMinutes);

// Assert
expect(result.targetRef.domain).toEqual(domain);
expect(result.targetRef.id).toEqual(refId);
expect(result.deleteInMinutes).toEqual(deleteInMinutes);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EntityId } from '@shared/domain';
import { DeletionDomainModel } from '../domain/types';
import { DeletionRequestBodyProps } from '../controller/dto';

export class DeletionRequestBodyPropsBuilder {
static build(domain: DeletionDomainModel, id: EntityId, deleteInMinutes?: number): DeletionRequestBodyProps {
const deletionRequestItem = {
targetRef: { domain, id },
deleteInMinutes,
};

return deletionRequestItem;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum';
import { DeletionLogStatisticBuilder } from './deletion-log-statistic.builder';
import { DeletionRequestLogBuilder } from './deletion-request-log.builder';
import { DeletionTargetRefBuilder } from './deletion-target-ref.builder';
import { DeletionDomainModel } from '../domain/types';
import { DeletionLogStatisticBuilder, DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder } from './index';

describe(DeletionRequestLogBuilder.name, () => {
describe(DeletionRequestLogResponseBuilder.name, () => {
afterAll(() => {
jest.clearAllMocks();
});
Expand All @@ -18,7 +16,7 @@ describe(DeletionRequestLogBuilder.name, () => {
const deletedCount = 2;
const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, modifiedCount, deletedCount)];

const result = DeletionRequestLogBuilder.build(targetRef, deletionPlannedAt, statistics);
const result = DeletionRequestLogResponseBuilder.build(targetRef, deletionPlannedAt, statistics);

// Assert
expect(result.targetRef).toEqual(targetRef);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DeletionLogStatistic, DeletionRequestLog, DeletionTargetRef } from '../interface';
import { DeletionRequestLogResponse } from '../controller/dto';
import { DeletionLogStatistic, DeletionTargetRef } from '../interface';

export class DeletionRequestLogBuilder {
export class DeletionRequestLogResponseBuilder {
static build(
targetRef: DeletionTargetRef,
deletionPlannedAt: Date,
statistics?: DeletionLogStatistic[]
): DeletionRequestLog {
): DeletionRequestLogResponse {
const deletionRequestLog = { targetRef, deletionPlannedAt, statistics };

return deletionRequestLog;
Expand Down
Loading

0 comments on commit dc47095

Please sign in to comment.