Exposes security audit logging from core (#181644)

Fix https://github.com/elastic/kibana/issues/178934

Exposes the audit service from core's security service.

---------

Co-authored-by: pgayvallet <pierre.gayvallet@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christiane (Tina) Heiligers 2024-05-02 20:38:26 +02:00 committed by GitHub
parent 2360af9ec6
commit 991170bb19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 255 additions and 18 deletions

View file

@ -367,6 +367,7 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>({
},
security: {
authc: deps.security.authc,
audit: deps.security.audit,
},
userProfile: deps.userProfile,
};

View file

@ -10,12 +10,13 @@ import type { KibanaRequest } from '@kbn/core-http-server';
import type {
SecurityRequestHandlerContext,
AuthcRequestHandlerContext,
AuditRequestHandlerContext,
} from '@kbn/core-security-server';
import type { InternalSecurityServiceStart } from './internal_contracts';
export class CoreSecurityRouteHandlerContext implements SecurityRequestHandlerContext {
#authc?: AuthcRequestHandlerContext;
#audit?: AuditRequestHandlerContext;
constructor(
private readonly securityStart: InternalSecurityServiceStart,
private readonly request: KibanaRequest
@ -29,4 +30,13 @@ export class CoreSecurityRouteHandlerContext implements SecurityRequestHandlerCo
}
return this.#authc;
}
public get audit() {
if (this.#audit == null) {
this.#audit = {
logger: this.securityStart.audit.asScoped(this.request),
};
}
return this.#audit;
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AuditLogger } from '@kbn/core-security-server';
export type MockedAuditLogger = jest.Mocked<AuditLogger>;
export const createAuditLoggerMock = {
create(): MockedAuditLogger {
return {
log: jest.fn(),
enabled: true,
};
},
};

View file

@ -8,11 +8,22 @@
import type { CoreSecurityDelegateContract } from '@kbn/core-security-server';
import { convertSecurityApi } from './convert_security_api';
import { createAuditLoggerMock } from '../test_helpers/create_audit_logger.mock';
describe('convertSecurityApi', () => {
it('returns the API from the source', () => {
const source: CoreSecurityDelegateContract = { authc: { getCurrentUser: jest.fn() } };
const source: CoreSecurityDelegateContract = {
authc: {
getCurrentUser: jest.fn(),
},
audit: {
asScoped: jest.fn().mockReturnValue(createAuditLoggerMock.create()),
withoutRequest: createAuditLoggerMock.create(),
},
};
const output = convertSecurityApi(source);
expect(output.authc.getCurrentUser).toBe(source.authc.getCurrentUser);
expect(output.audit.asScoped).toBe(source.audit.asScoped);
expect(output.audit.withoutRequest).toBe(source.audit.withoutRequest);
});
});

View file

@ -22,4 +22,19 @@ describe('getDefaultSecurityImplementation', () => {
expect(user).toBeNull();
});
});
describe('audit.asScoped', () => {
it('returns null', async () => {
const logger = implementation.audit.asScoped({} as any);
expect(logger.log({ message: 'something' })).toBeUndefined();
});
});
describe('audit.withoutRequest', () => {
it('does not log', async () => {
const logger = implementation.audit.withoutRequest;
expect(logger.enabled).toBe(false);
expect(logger.log({ message: 'no request' })).toBeUndefined();
});
});
});

View file

@ -13,5 +13,14 @@ export const getDefaultSecurityImplementation = (): CoreSecurityDelegateContract
authc: {
getCurrentUser: () => null,
},
audit: {
asScoped: () => {
return { log: () => undefined, enabled: false };
},
withoutRequest: {
log: () => undefined,
enabled: false,
},
},
};
};

View file

@ -7,3 +7,5 @@
*/
export { securityServiceMock } from './src/security_service.mock';
export type { InternalSecurityStartMock, SecurityStartMock } from './src/security_service.mock';
export { auditLoggerMock } from './src/audit.mock';

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaRequest } from '@kbn/core-http-server';
import type { AuditLogger } from '@kbn/core-security-server';
export type MockedAuditLogger = jest.Mocked<AuditLogger>;
export const auditLoggerMock = {
create(): MockedAuditLogger {
return {
log: jest.fn(),
enabled: true,
};
},
};
export interface MockedAuditService {
asScoped: (request: KibanaRequest) => MockedAuditLogger;
withoutRequest: MockedAuditLogger;
}
export const auditServiceMock = {
create(): MockedAuditService {
return {
asScoped: jest.fn().mockReturnValue(auditLoggerMock.create()),
withoutRequest: auditLoggerMock.create(),
};
},
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type {
import {
SecurityServiceSetup,
SecurityServiceStart,
SecurityRequestHandlerContext,
@ -15,6 +15,7 @@ import type {
InternalSecurityServiceSetup,
InternalSecurityServiceStart,
} from '@kbn/core-security-server-internal';
import { auditServiceMock, type MockedAuditService } from './audit.mock';
const createSetupMock = () => {
const mock: jest.Mocked<SecurityServiceSetup> = {
@ -24,11 +25,16 @@ const createSetupMock = () => {
return mock;
};
const createStartMock = () => {
const mock: jest.MockedObjectDeep<SecurityServiceStart> = {
export type SecurityStartMock = jest.MockedObjectDeep<Omit<SecurityServiceStart, 'audit'>> & {
audit: MockedAuditService;
};
const createStartMock = (): SecurityStartMock => {
const mock = {
authc: {
getCurrentUser: jest.fn(),
},
audit: auditServiceMock.create(),
};
return mock;
@ -42,11 +48,18 @@ const createInternalSetupMock = () => {
return mock;
};
const createInternalStartMock = () => {
const mock: jest.MockedObjectDeep<InternalSecurityServiceStart> = {
export type InternalSecurityStartMock = jest.MockedObjectDeep<
Omit<InternalSecurityServiceStart, 'audit'>
> & {
audit: MockedAuditService;
};
const createInternalStartMock = (): InternalSecurityStartMock => {
const mock = {
authc: {
getCurrentUser: jest.fn(),
},
audit: auditServiceMock.create(),
};
return mock;
@ -67,6 +80,12 @@ const createRequestHandlerContextMock = () => {
authc: {
getCurrentUser: jest.fn(),
},
audit: {
logger: {
log: jest.fn(),
enabled: true,
},
},
};
return mock;
};

View file

@ -16,5 +16,6 @@
"kbn_references": [
"@kbn/core-security-server",
"@kbn/core-security-server-internal",
"@kbn/core-http-server",
]
}

View file

@ -8,11 +8,21 @@
export type { SecurityServiceSetup, SecurityServiceStart } from './src/contracts';
export type { CoreAuthenticationService } from './src/authc';
export type { CoreAuditService } from './src/audit';
export type {
CoreSecurityDelegateContract,
AuthenticationServiceContract,
AuditServiceContract,
} from './src/api_provider';
export type {
SecurityRequestHandlerContext,
AuthcRequestHandlerContext,
AuditRequestHandlerContext,
} from './src/request_handler_context';
export type {
AuditEvent,
AuditHttp,
AuditKibana,
AuditRequest,
} from './src/audit_logging/audit_events';
export type { AuditLogger } from './src/audit_logging/audit_logger';

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { CoreAuditService } from './audit';
import type { CoreAuthenticationService } from './authc';
/**
@ -16,9 +17,12 @@ import type { CoreAuthenticationService } from './authc';
*/
export interface CoreSecurityDelegateContract {
authc: AuthenticationServiceContract;
audit: AuditServiceContract;
}
/**
* @public
*/
export type AuthenticationServiceContract = CoreAuthenticationService;
export type AuditServiceContract = CoreAuditService;

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaRequest } from '@kbn/core-http-server';
import type { AuditLogger } from './audit_logging/audit_logger';
export interface CoreAuditService {
/**
* Creates an {@link AuditLogger} scoped to the current request.
*
* This audit logger logs events with all required user and session info and should be used for
* all user-initiated actions.
*
* @example
* ```typescript
* const auditLogger = securitySetup.audit.asScoped(request);
* auditLogger.log(event);
* ```
*/
asScoped: (request: KibanaRequest) => AuditLogger;
/**
* {@link AuditLogger} for background tasks only.
*
* This audit logger logs events without any user or session info and should never be used to log
* user-initiated actions.
*
* @example
* ```typescript
* securitySetup.audit.withoutRequest.log(event);
* ```
*/
withoutRequest: AuditLogger;
}

View file

@ -1,11 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { LogMeta } from '@kbn/core/server';
import type { LogMeta } from '@kbn/logging';
/**
* Audit kibana schema using ECS format

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { AuditEvent } from './audit_events';

View file

@ -8,7 +8,7 @@
import type { CoreAuthenticationService } from './authc';
import type { CoreSecurityDelegateContract } from './api_provider';
import type { CoreAuditService } from './audit';
/**
* Setup contract for Core's security service.
*
@ -33,4 +33,8 @@ export interface SecurityServiceStart {
* The {@link CoreAuthenticationService | authentication service}
*/
authc: CoreAuthenticationService;
/**
* The {@link CoreAuditService | audit service}
*/
audit: CoreAuditService;
}

View file

@ -7,11 +7,17 @@
*/
import type { AuthenticatedUser } from '@kbn/core-security-common';
import { AuditLogger } from './audit_logging/audit_logger';
export interface SecurityRequestHandlerContext {
authc: AuthcRequestHandlerContext;
audit: AuditRequestHandlerContext;
}
export interface AuthcRequestHandlerContext {
getCurrentUser(): AuthenticatedUser | null;
}
export interface AuditRequestHandlerContext {
logger: AuditLogger;
}

View file

@ -16,5 +16,6 @@
"kbn_references": [
"@kbn/core-security-common",
"@kbn/core-http-server",
"@kbn/logging",
]
}

View file

@ -122,6 +122,12 @@ export type {
SecurityServiceSetup,
SecurityServiceStart,
CoreAuthenticationService,
CoreAuditService,
AuditEvent,
AuditHttp,
AuditKibana,
AuditRequest,
AuditLogger,
} from '@kbn/core-security-server';
export type {
User,

View file

@ -7,7 +7,7 @@
import type { KibanaRequest } from '@kbn/core/server';
import type { AuditLogger } from './audit_logger';
import type { AuditLogger } from '@kbn/core-security-server';
export interface AuditServiceSetup {
/**

View file

@ -6,5 +6,10 @@
*/
export type { AuditServiceSetup } from './audit_service';
export type { AuditEvent, AuditHttp, AuditKibana, AuditRequest } from './audit_events';
export type { AuditLogger } from './audit_logger';
export type {
AuditEvent,
AuditHttp,
AuditKibana,
AuditLogger,
AuditRequest,
} from '@kbn/core-security-server';

View file

@ -14,5 +14,6 @@
"@kbn/core",
"@kbn/security-plugin-types-common",
"@kbn/core-user-profile-server",
"@kbn/core-security-server",
]
}

View file

@ -6,9 +6,10 @@
*/
import { httpServerMock } from '@kbn/core-http-server-mocks';
import type { CoreSecurityDelegateContract } from '@kbn/core-security-server';
import type { AuditLogger, CoreSecurityDelegateContract } from '@kbn/core-security-server';
import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-server';
import { auditServiceMock } from './audit/mocks';
import { authenticationServiceMock } from './authentication/authentication_service.mock';
import { buildSecurityApi, buildUserProfileApi } from './build_delegate_apis';
import { securityMock } from './mocks';
@ -16,11 +17,13 @@ import { userProfileServiceMock } from './user_profile/user_profile_service.mock
describe('buildSecurityApi', () => {
let authc: ReturnType<typeof authenticationServiceMock.createStart>;
let auditService: ReturnType<typeof auditServiceMock.create>;
let api: CoreSecurityDelegateContract;
beforeEach(() => {
authc = authenticationServiceMock.createStart();
api = buildSecurityApi({ getAuthc: () => authc });
auditService = auditServiceMock.create();
api = buildSecurityApi({ getAuthc: () => authc, audit: auditService });
});
describe('authc.getCurrentUser', () => {
@ -43,6 +46,25 @@ describe('buildSecurityApi', () => {
expect(currentUser).toBe(delegateReturn);
});
});
describe('audit.asScoped', () => {
let auditLogger: AuditLogger;
it('properly delegates to the service', () => {
const request = httpServerMock.createKibanaRequest();
auditLogger = api.audit.asScoped(request);
auditLogger.log({ message: 'an event' });
expect(auditService.asScoped).toHaveBeenCalledTimes(1);
expect(auditService.asScoped).toHaveBeenCalledWith(request);
});
it('returns the result from the service', async () => {
const request = httpServerMock.createKibanaRequest();
auditLogger = api.audit.asScoped(request);
auditLogger.log({ message: 'an event' });
expect(auditService.asScoped(request).log).toHaveBeenCalledTimes(1);
expect(auditService.asScoped(request).log).toHaveBeenCalledWith({ message: 'an event' });
});
});
});
describe('buildUserProfileApi', () => {

View file

@ -7,14 +7,17 @@
import type { CoreSecurityDelegateContract } from '@kbn/core-security-server';
import type { CoreUserProfileDelegateContract } from '@kbn/core-user-profile-server';
import type { AuditServiceSetup } from '@kbn/security-plugin-types-server';
import type { InternalAuthenticationServiceStart } from './authentication';
import type { UserProfileServiceStartInternal } from './user_profile';
export const buildSecurityApi = ({
getAuthc,
audit,
}: {
getAuthc: () => InternalAuthenticationServiceStart;
audit: AuditServiceSetup;
}): CoreSecurityDelegateContract => {
return {
authc: {
@ -22,6 +25,15 @@ export const buildSecurityApi = ({
return getAuthc().getCurrentUser(request);
},
},
audit: {
asScoped(request) {
return audit.asScoped(request);
},
withoutRequest: {
log: audit.withoutRequest.log,
enabled: audit.withoutRequest.enabled,
},
},
};
};

View file

@ -298,6 +298,7 @@ export class SecurityPlugin
core.security.registerSecurityDelegate(
buildSecurityApi({
getAuthc: this.getAuthentication.bind(this),
audit: this.auditSetup,
})
);
core.userProfile.registerUserProfileDelegate(