Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementer mer audit-logging #80

Merged
merged 17 commits into from
Dec 8, 2023
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"scripts": {
"build": "tsc --build",
"start": "nodemon src/server.ts",
"test": "jest"
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"compression": "^1.7.4",
Expand Down
103 changes: 82 additions & 21 deletions server/src/kandidatsøk/kandidatsøk.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { RequestHandler } from 'express';
import { Request, RequestHandler } from 'express';
import { EsQuery } from '../../../src/felles/domene/elastic/ElasticSearch';
import Kandidat from '../../../src/felles/domene/kandidat/Kandidat';
import { hentGrupper, hentNavIdent } from '../azureAd';
import { auditLog, logger, opprettLoggmeldingForAuditlogg, secureLog } from '../logger';
import { retrieveToken } from '../middlewares';
import { SearchQuery } from './elasticSearchTyper';

export const { AD_GRUPPE_MODIA_GENERELL_TILGANG, AD_GRUPPE_MODIA_OPPFOLGING } = process.env;

Expand Down Expand Up @@ -58,34 +59,70 @@ export const leggTilAuthorizationForKandidatsøkEs =
next();
};

export const loggSøkPåFnrEllerAktørId: RequestHandler = async (request, _, next) => {
const erSøkPåKandidater = request.body && request.body._source !== false;
export const loggSøkPåFnrEllerAktørId: RequestHandler = (request, response, next) => {
try {
const requestOmSpesifikkPerson = requestBerOmSpesifikkPerson(request);

if (erSøkPåKandidater) {
try {
const fnrEllerAktørId = hentFnrEllerAktørIdFraESBody(request.body);
if (requestOmSpesifikkPerson !== null) {
const brukerensAccessToken = retrieveToken(request.headers);
const navIdent = hentNavIdent(brukerensAccessToken);

if (fnrEllerAktørId) {
const brukerensAccessToken = retrieveToken(request.headers);
const navIdent = hentNavIdent(brukerensAccessToken);

const melding = opprettLoggmeldingForAuditlogg(
'NAV-ansatt har gjort spesifikt kandidatsøk på brukeren',
fnrEllerAktørId,
navIdent
);
const melding = opprettLoggmeldingForAuditlogg(
requestOmSpesifikkPerson.melding,
requestOmSpesifikkPerson.fnrEllerAktørId,
navIdent
);

auditLog.info(melding);
}
} catch (e) {
logger.error('Klarte ikke å logge søk på fnr eller aktørId:', e);
auditLog.info(melding);
secureLog.info(`Auditlogget handling: ${melding}`);
}
} catch (e) {
const feilmelding =
'Klarte ikke å verifisere eller logge henting av persondata via kandidatsøk-proxy:';
logger.error(feilmelding, e);

return response.status(500).send(feilmelding);
}

next();
};

export const hentFnrEllerAktørIdFraESBody = (request: SearchQuery): string | null => {
type MeldingTilAuditlog = {
melding: string;
fnrEllerAktørId: string;
};

const requestBerOmSpesifikkPerson = (
request: Request<unknown, unknown, EsQuery<Kandidat>>
): null | MeldingTilAuditlog => {
const berOmData = request.body && request.body._source !== false;

if (!berOmData) {
return null;
}

const idInniSpesifikkPersonQuery = erSpesifikkPersonQuery(request.body);
const idInniHentKandidatQuery = erHentKandidatQuery(request.body);
const idInniFinnStillingQuery = erFinnStillingQuery(request.body);

if (idInniSpesifikkPersonQuery) {
return {
melding: 'NAV-ansatt har gjort spesifikt kandidatsøk på brukeren',
fnrEllerAktørId: idInniSpesifikkPersonQuery,
};
} else if (idInniHentKandidatQuery) {
return {
melding: 'NAV-ansatt har åpnet CV-en til bruker',
kjesvale marked this conversation as resolved.
Show resolved Hide resolved
fnrEllerAktørId: idInniHentKandidatQuery,
};
} else if (idInniFinnStillingQuery) {
return null; // TODO: Bør audit-logges?
}

return null;
};

export const erSpesifikkPersonQuery = (request: EsQuery<Kandidat>): string | null => {
let fnrEllerAktørId = null;

request.query?.bool?.must?.forEach((mustQuery) =>
Expand All @@ -98,3 +135,27 @@ export const hentFnrEllerAktørIdFraESBody = (request: SearchQuery): string | nu

return fnrEllerAktørId;
};

export const erHentKandidatQuery = (request: EsQuery<Kandidat>): string | null => {
if (
request.size === 1 &&
request._source === undefined &&
request.query?.term?.['kandidatnr'] !== undefined
) {
return request.query.term['kandidatnr'];
}

return null;
};

export const erFinnStillingQuery = (request: EsQuery<Kandidat>): string | null => {
if (
request.size === 1 &&
request._source &&
request.query?.term?.['kandidatnr'] !== undefined
) {
return request.query.term['kandidatnr'];
}

return null;
};
10 changes: 7 additions & 3 deletions server/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs';
import os from 'os';
import winston from 'winston';
import winstonSyslog from 'winston-syslog';
import os from 'os';

// Sett tidssonen eksplisitt for å sikre riktig timestamp i logging til archsight
process.env.TZ = 'Europe/Oslo';
Expand Down Expand Up @@ -44,15 +44,19 @@ export const auditLog = winston.createLogger({
export const opprettLoggmeldingForAuditlogg = (
melding: string,
fnrEllerAktørId: string,
navIdent: string
navIdent: string,
end = currentTimeForAuditlogg()
): string => {
const header = `CEF:0|Rekrutteringsbistand|${NAIS_APP_NAME}|1.0|audit:access|Sporingslogg|INFO`;

const extension = `flexString1=Permit\
msg=${melding}\
duid=${fnrEllerAktørId}
flexString1Label=Decision\
end=${Date.now()}\
end=${end}\
suid=${navIdent}`.replace(/\s+/g, ' ');

return `${header}|${extension}`;
};

export const currentTimeForAuditlogg = () => Date.now();
91 changes: 91 additions & 0 deletions server/test/Auditlogging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { NextFunction, Request, Response } from 'express';
import { EsQuery } from '../../src/felles/domene/elastic/ElasticSearch';
import Kandidat from '../../src/felles/domene/kandidat/Kandidat';
import * as azureAd from '../src/azureAd';
import * as kandidatsøk from '../src/kandidatsøk/kandidatsøk';
import * as logger from '../src/logger';
import * as queries from './queriesMotSpesifikkPerson';

describe('Auditlogging av personspesifikt kandidatsøk', () => {
let mockRequest: Partial<Request<unknown, unknown, EsQuery<Kandidat>>>;
let mockResponse: Partial<Response>;
let nextFunction: NextFunction = jest.fn();

beforeEach(() => {
mockResponse = {
status: jest.fn(() => mockResponse),
send: jest.fn(),
} as Partial<Response>;

mockRequest = {
headers: {
authorization: '',
},
};

nextFunction = jest.fn();
});

afterEach(() => {
jest.restoreAllMocks();
});

test('Kall mot kandidatsøk med spesifikk person-query på fødselsnummer skal logges', async () => {
const dateNow = 1;
const navIdent = 'A123456';
const personId = '12345678910';
const melding = `CEF:0|Rekrutteringsbistand|undefined|1.0|audit:access|Sporingslogg|INFO|flexString1=Permit msg=NAV-ansatt har gjort spesifikt kandidatsøk på brukeren duid=${personId} flexString1Label=Decision end=${dateNow} suid=${navIdent}`;

mockRequest.body = queries.queryTilKandidatsøkMedAktørIdOgFødselsnummer(personId);

jest.spyOn(logger, 'currentTimeForAuditlogg').mockReturnValue(1);
jest.spyOn(azureAd, 'hentNavIdent').mockReturnValue(navIdent);
const auditLog = jest.spyOn(logger.auditLog, 'info');
const secureLog = jest.spyOn(logger.secureLog, 'info');

auditLog.mockImplementation(() => logger.auditLog);
secureLog.mockImplementation(() => logger.secureLog);

kandidatsøk.loggSøkPåFnrEllerAktørId(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toBeCalled();
expect(auditLog).toBeCalledTimes(1);
expect(auditLog).toHaveBeenCalledWith(melding);
expect(secureLog).toBeCalledTimes(1);
expect(secureLog).toHaveBeenCalledWith('Auditlogget handling: ' + melding);
});

test('Kall mot kandidatsøk med hent person-query på fødselsnummer skal logges', async () => {
const dateNow = 1;
const navIdent = 'A123456';
const personId = '12345678910';
const melding = `CEF:0|Rekrutteringsbistand|undefined|1.0|audit:access|Sporingslogg|INFO|flexString1=Permit msg=NAV-ansatt har åpnet CV-en til bruker duid=${personId} flexString1Label=Decision end=${dateNow} suid=${navIdent}`;

mockRequest.body = queries.queryTilHentKandidat(personId);

jest.spyOn(logger, 'currentTimeForAuditlogg').mockReturnValue(1);
jest.spyOn(azureAd, 'hentNavIdent').mockReturnValue(navIdent);
const auditLog = jest.spyOn(logger.auditLog, 'info');
const secureLog = jest.spyOn(logger.secureLog, 'info');

auditLog.mockImplementation((_) => logger.auditLog);
secureLog.mockImplementation(() => logger.secureLog);

kandidatsøk.loggSøkPåFnrEllerAktørId(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toBeCalled();
expect(auditLog).toBeCalledTimes(1);
expect(auditLog).toHaveBeenCalledWith(melding);
expect(secureLog).toBeCalledTimes(1);
expect(secureLog).toHaveBeenCalledWith('Auditlogget handling: ' + melding);
});
});
88 changes: 88 additions & 0 deletions server/test/Tilgangskontroll.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
import { NextFunction, Request, Response } from 'express';
import * as azureAd from '../src/azureAd';
import * as kandidatsøk from '../src/kandidatsøk/kandidatsøk';
import * as middlewares from '../src/middlewares';

describe('Tilgangskontroll for kandidatsøket', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let nextFunction: NextFunction = jest.fn();

beforeEach(() => {
mockResponse = {
status: jest.fn(() => mockResponse),
send: jest.fn(),
} as Partial<Response>;

mockRequest = {
headers: {
authorization: '',
},
body: {},
};

nextFunction = jest.fn();
});

test('En bruker med ModiaGenerellTilgang skal få tilgang til kandidatsøket', async () => {
jest.spyOn(azureAd, 'hentNavIdent').mockReturnValue('A123456');
jest.spyOn(azureAd, 'hentGrupper').mockReturnValue([
kandidatsøk.AD_GRUPPE_MODIA_GENERELL_TILGANG!,
]);

kandidatsøk.harTilgangTilKandidatsøk(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toBeCalled();
});

test('En bruker med ModiaOppfølging skal få tilgang til kandidatsøket', async () => {
jest.spyOn(azureAd, 'hentNavIdent').mockReturnValue('A123456');
jest.spyOn(azureAd, 'hentGrupper').mockReturnValue([
kandidatsøk.AD_GRUPPE_MODIA_OPPFOLGING!,
]);

kandidatsøk.harTilgangTilKandidatsøk(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toBeCalled();
});

test('En bruker med andre tilganger skal ikke få tilgang til kandidatsøket', async () => {
const andreTilganger = ['en-annen-tilgang'];

jest.spyOn(azureAd, 'hentNavIdent').mockReturnValue('A123456');
jest.spyOn(azureAd, 'hentGrupper').mockReturnValue(andreTilganger);

kandidatsøk.harTilgangTilKandidatsøk(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toBeCalledTimes(0);
expect(mockResponse.status).toBeCalledWith(403);
});

test('En bruker uten noen tilganger skal ikke få tilgang til kandidatsøket', async () => {
jest.spyOn(middlewares, 'retrieveToken').mockReturnValue('');
jest.spyOn(azureAd, 'hentNavIdent').mockReturnValue('A123456');
jest.spyOn(azureAd, 'hentGrupper').mockReturnValue([]);

kandidatsøk.harTilgangTilKandidatsøk(
mockRequest as Request,
mockResponse as Response,
nextFunction
);

expect(nextFunction).toBeCalledTimes(0);
expect(mockResponse.status).toBeCalledWith(403);
});
});
Loading