This commit is contained in:
Bastian Wagner
2026-07-17 10:50:10 +02:00
parent 8c6ad294b2
commit 201c4e03f8
22 changed files with 1275 additions and 21 deletions

View File

@@ -1,6 +1,10 @@
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Ber, BerWriter, Client } from 'ldapts';
import { ApplicationErrorCategory, ApplicationErrorCode } from '../application-error-log/application-error-codes';
import { ApplicationErrorLoggerService } from '../application-error-log/application-error-logger.service';
import { markErrorAsLogged } from '../application-error-log/logged-error-marker';
import { RequestContextService } from '../common/request-context.service';
interface LldapUserInput {
username: string;
@@ -75,7 +79,11 @@ export class LldapService {
private cachedHeaders?: { expiresAt: number; headers: Record<string, string> };
constructor(private readonly config: ConfigService) {}
constructor(
private readonly config: ConfigService,
private readonly applicationErrorLogger: ApplicationErrorLoggerService,
private readonly requestContext: RequestContextService,
) {}
async createUser(input: LldapUserInput): Promise<void> {
await this.graphql(
@@ -113,7 +121,26 @@ export class LldapService {
this.passwordModifyRequestValue(this.userDn(username), password),
);
} catch (error) {
throw new InternalServerErrorException(`LLDAP password change failed: ${this.errorMessage(error)}`);
await this.applicationErrorLogger.log({
error,
category: ApplicationErrorCategory.EXTERNAL_API,
code: ApplicationErrorCode.EXTERNAL_API_REQUEST_FAILED,
module: 'LldapModule',
service: LldapService.name,
operation: 'setPassword',
requestContext: {
...this.requestContext.get(),
userId: username,
},
context: {
provider: 'LLDAP',
protocol: 'LDAP',
},
handled: true,
});
const exception = new InternalServerErrorException(`LLDAP password change failed: ${this.errorMessage(error)}`);
markErrorAsLogged(exception);
throw exception;
} finally {
await client.unbind().catch(() => undefined);
}
@@ -394,15 +421,22 @@ export class LldapService {
private async graphql<T = unknown>(query: string, variables: Record<string, unknown>): Promise<T> {
const endpoint = `${this.config.getOrThrow<string>('LLDAP_URL').replace(/\/$/, '')}/api/graphql`;
const headers = await this.adminHeaders();
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
...headers,
},
body: JSON.stringify({ query, variables }),
});
let response: Response;
try {
const headers = await this.adminHeaders();
response = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
...headers,
},
body: JSON.stringify({ query, variables }),
});
} catch (error) {
await this.logGraphqlFailure(error, endpoint, 'request_failed');
markErrorAsLogged(error);
throw error;
}
const payload = (await response.json().catch(() => ({}))) as {
data?: T;
@@ -414,11 +448,21 @@ export class LldapService {
if (/not found/i.test(message)) {
throw new NotFoundException('LLDAP user not found');
}
throw new InternalServerErrorException(`LLDAP GraphQL request failed: ${message}`);
const exception = new InternalServerErrorException(`LLDAP GraphQL request failed: ${message}`);
await this.logGraphqlFailure(exception, endpoint, 'bad_response', {
status: response.status,
statusText: response.statusText,
errorMessages: payload.errors?.map((error) => error.message),
});
markErrorAsLogged(exception);
throw exception;
}
if (!payload.data) {
throw new InternalServerErrorException('LLDAP GraphQL response did not contain data');
const exception = new InternalServerErrorException('LLDAP GraphQL response did not contain data');
await this.logGraphqlFailure(exception, endpoint, 'missing_data', { status: response.status });
markErrorAsLogged(exception);
throw exception;
}
return payload.data;
@@ -484,4 +528,28 @@ export class LldapService {
private errorMessage(error: unknown): string {
return error instanceof Error ? error.message : 'unknown error';
}
private async logGraphqlFailure(
error: unknown,
endpoint: string,
reason: string,
context: Record<string, unknown> = {},
): Promise<void> {
await this.applicationErrorLogger.log({
error,
category: ApplicationErrorCategory.EXTERNAL_API,
code: ApplicationErrorCode.EXTERNAL_API_REQUEST_FAILED,
module: 'LldapModule',
service: LldapService.name,
operation: 'graphql',
requestContext: this.requestContext.get(),
context: {
provider: 'LLDAP',
endpoint,
reason,
...context,
},
handled: true,
});
}
}