Add session ID to user login audit event (#124299)

This commit is contained in:
Joe Portner 2022-02-04 16:37:57 -05:00 committed by GitHub
parent b92e180e85
commit ddc300069d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 304 additions and 244 deletions

View file

@ -462,7 +462,7 @@ When "thom" logs in, a "user_login" {kib} audit event is written:
[source,json]
-------------
{"event":{"action":"user_login","category":["authentication"],"outcome":"success"},"user":{"name":"thom","roles":["superuser"]},"@timestamp":"2022-01-25T09:40:39.267-05:00","message":"User [thom] has logged in using basic provider [name=basic]","trace":{"id":"818cbf3..."}}
{"event":{"action":"user_login","category":["authentication"],"outcome":"success"},"kibana":{"session_id":"ab93zdA..."},"user":{"name":"thom","roles":["superuser"]},"@timestamp":"2022-01-25T09:40:39.267-05:00","message":"User [thom] has logged in using basic provider [name=basic]","trace":{"id":"818cbf3..."}}
-------------
The `trace.id` value `"818cbf3..."` in the {kib} audit event can be correlated with the `opaque_id` value in these six {es} audit events:

View file

@ -19,7 +19,7 @@ import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '../../licensing/server/mocks';
import { httpServerMock, loggingSystemMock } from '../../../../src/core/server/mocks';
import { auditServiceMock } from '../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../security/server/audit/mocks';
import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock';
import {
@ -72,7 +72,7 @@ const authorization = actionsAuthorizationMock.create();
const executionEnqueuer = jest.fn();
const ephemeralExecutionEnqueuer = jest.fn();
const request = httpServerMock.createKibanaRequest();
const auditLogger = auditServiceMock.create().asScoped(request);
const auditLogger = auditLoggerMock.create();
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
const logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
import { RegistryRuleType } from '../../rule_type_registry';
@ -27,7 +26,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -16,8 +16,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization, ActionsClient } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
import { getDefaultRuleMonitoring } from '../../task_runner/task_runner';
@ -34,7 +33,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v8.0.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup } from './lib';
const taskManager = taskManagerMock.createStart();
@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -15,8 +15,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { InvalidatePendingApiKey } from '../../types';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { eventLoggerMock } from '../../../../event_log/server/event_logger.mock';
import { TaskStatus } from '../../../../task_manager/server';
@ -31,7 +30,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const eventLogger = eventLoggerMock.create();
const kibanaVersion = 'v7.10.0';

View file

@ -15,8 +15,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { InvalidatePendingApiKey } from '../../types';
import { getBeforeSetup, setGlobalDate } from './lib';
@ -26,7 +25,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -16,8 +16,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
import { RegistryRuleType } from '../../rule_type_registry';
@ -28,7 +27,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
@ -25,7 +24,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
@ -25,7 +24,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
const taskManager = taskManagerMock.createStart();
@ -24,7 +23,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -20,8 +20,7 @@ import { AlertingAuthorization } from '../../authorization/alerting_authorizatio
import { resolvable } from '../../test_utils';
import { ActionsAuthorization, ActionsClient } from '../../../../actions/server';
import { TaskStatus } from '../../../../task_manager/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
jest.mock('../../../../../../src/core/server/saved_objects/service/lib/utils', () => ({
@ -36,7 +35,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -14,8 +14,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '../../../../actions/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { auditServiceMock } from '../../../../security/server/audit/index.mock';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { InvalidatePendingApiKey } from '../../types';
import { getBeforeSetup, setGlobalDate } from './lib';
@ -25,7 +24,7 @@ const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v7.10.0';
const rulesClientParams: jest.Mocked<ConstructorOptions> = {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { AuditLogger } from '../../../../plugins/security/server';
import { auditLoggerMock } from '../../../../plugins/security/server/audit/mocks';
import { Operations } from '.';
import { AuthorizationAuditLogger } from './audit_logger';
import { ReadOperations } from './types';
@ -30,11 +30,7 @@ describe('audit_logger', () => {
});
describe('log function', () => {
const mockLogger: jest.Mocked<AuditLogger> = {
log: jest.fn(),
enabled: true,
};
const mockLogger = auditLoggerMock.create();
let logger: AuthorizationAuditLogger;
beforeEach(() => {

View file

@ -14,6 +14,7 @@ import { AuthorizationAuditLogger } from './audit_logger';
import { KibanaRequest } from 'kibana/server';
import { KibanaFeature } from '../../../../plugins/features/common';
import { AuditLogger, SecurityPluginStart } from '../../../security/server';
import { auditLoggerMock } from '../../../security/server/audit/mocks';
import { PluginStartContract as FeaturesPluginStart } from '../../../features/server';
describe('authorization', () => {
@ -22,10 +23,7 @@ describe('authorization', () => {
beforeEach(() => {
request = httpServerMock.createKibanaRequest();
mockLogger = {
log: jest.fn(),
enabled: true,
};
mockLogger = auditLoggerMock.create();
});
describe('create', () => {

View file

@ -11,7 +11,7 @@ import { AlertsClientFactory, AlertsClientFactoryProps } from './alerts_client_f
import { ElasticsearchClient, KibanaRequest } from 'src/core/server';
import { loggingSystemMock } from 'src/core/server/mocks';
import { securityMock } from '../../../security/server/mocks';
import { AuditLogger } from '../../../security/server';
import { auditLoggerMock } from '../../../security/server/audit/mocks';
import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock';
import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock';
@ -44,10 +44,7 @@ const fakeRequest = {
},
} as unknown as Request;
const auditLogger = {
log: jest.fn(),
enabled: true,
} as jest.Mocked<AuditLogger>;
const auditLogger = auditLoggerMock.create();
describe('AlertsClientFactory', () => {
beforeEach(() => {

View file

@ -17,16 +17,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
const auditLogger = {
log: jest.fn(),
enabled: true,
} as jest.Mocked<AuditLogger>;
const auditLogger = auditLoggerMock.create();
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
logger: loggingSystemMock.create().get(),

View file

@ -16,16 +16,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
const auditLogger = {
log: jest.fn(),
enabled: true,
} as jest.Mocked<AuditLogger>;
const auditLogger = auditLoggerMock.create();
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
logger: loggingSystemMock.create().get(),

View file

@ -17,16 +17,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
const auditLogger = {
log: jest.fn(),
enabled: true,
} as jest.Mocked<AuditLogger>;
const auditLogger = auditLoggerMock.create();
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
logger: loggingSystemMock.create().get(),

View file

@ -16,16 +16,13 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { auditLoggerMock } from '../../../../security/server/audit/mocks';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
const auditLogger = {
log: jest.fn(),
enabled: true,
} as jest.Mocked<AuditLogger>;
const auditLogger = auditLoggerMock.create();
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
logger: loggingSystemMock.create().get(),

View file

@ -239,6 +239,7 @@ describe('#userLoginEvent', () => {
authenticationResult: AuthenticationResult.succeeded(mockAuthenticatedUser()),
authenticationProvider: 'basic1',
authenticationType: 'basic',
sessionId: '123',
})
).toMatchInlineSnapshot(`
Object {
@ -255,6 +256,7 @@ describe('#userLoginEvent', () => {
"authentication_realm": "native1",
"authentication_type": "basic",
"lookup_realm": "native1",
"session_id": "123",
"space_id": undefined,
},
"message": "User [user] has logged in using basic provider [name=basic1]",
@ -293,6 +295,7 @@ describe('#userLoginEvent', () => {
"authentication_realm": undefined,
"authentication_type": "basic",
"lookup_realm": undefined,
"session_id": undefined,
"space_id": undefined,
},
"message": "Failed attempt to login using basic provider [name=basic1]",

View file

@ -97,12 +97,14 @@ export interface UserLoginParams {
authenticationResult: AuthenticationResult;
authenticationProvider?: string;
authenticationType?: string;
sessionId?: string;
}
export function userLoginEvent({
authenticationResult,
authenticationProvider,
authenticationType,
sessionId,
}: UserLoginParams): AuditEvent {
return {
message: authenticationResult.user
@ -119,6 +121,7 @@ export function userLoginEvent({
},
kibana: {
space_id: undefined, // Ensure this does not get populated by audit service
session_id: sessionId,
authentication_provider: authenticationProvider,
authentication_type: authenticationType,
authentication_realm: authenticationResult.user?.authentication_realm.name,

View file

@ -5,20 +5,23 @@
* 2.0.
*/
import type { AuditService } from './audit_service';
import type { AuditLogger, AuditService } from './audit_service';
export const auditLoggerMock = {
create() {
return {
log: jest.fn(),
enabled: true,
} as jest.Mocked<AuditLogger>;
},
};
export const auditServiceMock = {
create() {
return {
getLogger: jest.fn(),
asScoped: jest.fn().mockReturnValue({
log: jest.fn(),
enabled: true,
}),
withoutRequest: {
log: jest.fn(),
enabled: true,
},
asScoped: jest.fn().mockReturnValue(auditLoggerMock.create()),
withoutRequest: auditLoggerMock.create(),
} as jest.Mocked<ReturnType<AuditService['setup']>>;
},
};

View file

@ -39,7 +39,7 @@ import type { AuthenticatedUser, SecurityLicense } from '../../common';
import { licenseMock } from '../../common/licensing/index.mock';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
import type { AuditServiceSetup } from '../audit';
import { auditServiceMock } from '../audit/index.mock';
import { auditServiceMock } from '../audit/mocks';
import type { ConfigType } from '../config';
import { ConfigSchema, createConfig } from '../config';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';

View file

@ -27,7 +27,8 @@ import {
import type { SecurityLicenseFeatures } from '../../common/licensing';
import { licenseMock } from '../../common/licensing/index.mock';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
import { auditServiceMock } from '../audit/index.mock';
import type { AuditLogger } from '../audit';
import { auditLoggerMock, auditServiceMock } from '../audit/mocks';
import { ConfigSchema, createConfig } from '../config';
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
import { securityMock } from '../mocks';
@ -39,6 +40,7 @@ import { Authenticator } from './authenticator';
import { DeauthenticationResult } from './deauthentication_result';
import type { BasicAuthenticationProvider, SAMLAuthenticationProvider } from './providers';
let auditLogger: AuditLogger;
function getMockOptions({
providers,
http = {},
@ -48,8 +50,11 @@ function getMockOptions({
http?: Partial<AuthenticatorOptions['config']['authc']['http']>;
selector?: AuthenticatorOptions['config']['authc']['selector'];
} = {}) {
const auditService = auditServiceMock.create();
auditLogger = auditLoggerMock.create();
auditService.asScoped.mockReturnValue(auditLogger);
return {
audit: auditServiceMock.create(),
audit: auditService,
getCurrentUser: jest.fn(),
clusterClient: elasticsearchServiceMock.createClusterClient(),
basePath: httpServiceMock.createSetupContract().basePath,
@ -66,6 +71,26 @@ function getMockOptions({
};
}
interface ExpectedAuditEvent {
action: string;
outcome?: string;
kibana?: Record<string, unknown>;
}
function expectAuditEvents(...events: ExpectedAuditEvent[]) {
expect(auditLogger.log).toHaveBeenCalledTimes(events.length);
for (let i = 0; i < events.length; i++) {
const { action, outcome, kibana } = events[i];
expect(auditLogger.log).toHaveBeenNthCalledWith(
i + 1,
expect.objectContaining({
event: { action, category: ['authentication'], ...(outcome && { outcome }) },
...(kibana && { kibana }),
})
);
}
}
describe('Authenticator', () => {
let mockBasicAuthenticationProvider: jest.Mocked<PublicMethodsOf<BasicAuthenticationProvider>>;
beforeEach(() => {
@ -262,16 +287,10 @@ describe('Authenticator', () => {
let authenticator: Authenticator;
let mockOptions: ReturnType<typeof getMockOptions>;
let mockSessVal: SessionValue;
const auditLogger = {
log: jest.fn(),
enabled: true,
};
beforeEach(() => {
auditLogger.log.mockClear();
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
mockOptions.session.get.mockResolvedValue(null);
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
authenticator = new Authenticator(mockOptions);
@ -281,6 +300,7 @@ describe('Authenticator', () => {
await expect(authenticator.login(undefined as any, undefined as any)).rejects.toThrowError(
'Request should be a valid "KibanaRequest" instance, was [undefined].'
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('fails if login attempt is not provided or invalid.', async () => {
@ -304,6 +324,7 @@ describe('Authenticator', () => {
).rejects.toThrowError(
'Login attempt should be an object with non-empty "provider.type" or "provider.name" property.'
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('fails if an authentication provider fails.', async () => {
@ -317,6 +338,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.failed(failureReason));
expectAuditEvents({ action: 'user_login', outcome: 'failure' });
});
it('returns user that authentication provider returns.', async () => {
@ -332,38 +354,49 @@ describe('Authenticator', () => {
).resolves.toEqual(
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
);
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('adds audit event when successful.', async () => {
const request = httpServerMock.createKibanaRequest();
const user = mockAuthenticatedUser();
mockBasicAuthenticationProvider.login.mockResolvedValue(
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
);
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
describe('user_login audit events', () => {
// Every other test case includes audit event assertions, but the user_login event is a bit special.
// We have these separate, detailed test cases to ensure that the session ID is included for user_login success events.
// This allows us to keep audit event assertions in the other test cases simpler.
expect(auditLogger.log).toHaveBeenCalledTimes(1);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: { action: 'user_login', category: ['authentication'], outcome: 'success' },
})
);
});
it('adds audit event with session ID when successful.', async () => {
const request = httpServerMock.createKibanaRequest();
const user = mockAuthenticatedUser();
mockBasicAuthenticationProvider.login.mockResolvedValue(
AuthenticationResult.succeeded(user, {
authHeaders: { authorization: 'Basic .....' },
state: 'foo', // to ensure a new session is created
})
);
mockOptions.session.create.mockResolvedValue({ ...mockSessVal, sid: '123' });
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
it('adds audit event when not successful.', async () => {
const request = httpServerMock.createKibanaRequest();
const failureReason = new Error('Not Authorized');
mockBasicAuthenticationProvider.login.mockResolvedValue(
AuthenticationResult.failed(failureReason)
);
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
expect(mockOptions.session.create).toHaveBeenCalledTimes(1);
expectAuditEvents({
action: 'user_login',
outcome: 'success',
kibana: expect.objectContaining({ authentication_type: 'basic', session_id: '123' }),
});
});
expect(auditLogger.log).toHaveBeenCalledTimes(1);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: { action: 'user_login', category: ['authentication'], outcome: 'failure' },
})
);
it('adds audit event without session ID when not successful.', async () => {
const request = httpServerMock.createKibanaRequest();
const failureReason = new Error('Not Authorized');
mockBasicAuthenticationProvider.login.mockResolvedValue(
AuthenticationResult.failed(failureReason)
);
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
expect(mockOptions.session.create).not.toHaveBeenCalled();
expectAuditEvents({
action: 'user_login',
outcome: 'failure',
kibana: expect.objectContaining({ authentication_type: 'basic', session_id: undefined }),
});
});
});
it('does not add audit event when not handled.', async () => {
@ -396,6 +429,7 @@ describe('Authenticator', () => {
provider: mockSessVal.provider,
state: { authorization },
});
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('returns `notHandled` if login attempt is targeted to not configured provider.', async () => {
@ -476,6 +510,7 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
expect(mockSAMLAuthenticationProvider1.login).not.toHaveBeenCalled();
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('tries to login only with the provider that has specified type', async () => {
@ -493,6 +528,7 @@ describe('Authenticator', () => {
expect(mockSAMLAuthenticationProvider1.login.mock.invocationCallOrder[0]).toBeLessThan(
mockSAMLAuthenticationProvider2.login.mock.invocationCallOrder[0]
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('returns as soon as provider handles request', async () => {
@ -528,6 +564,10 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
expect(mockSAMLAuthenticationProvider2.login).not.toHaveBeenCalled();
expect(mockSAMLAuthenticationProvider1.login).toHaveBeenCalledTimes(3);
expectAuditEvents(
{ action: 'user_login', outcome: 'failure' },
{ action: 'user_login', outcome: 'success' }
);
});
it('provides session only if provider name matches', async () => {
@ -563,6 +603,7 @@ describe('Authenticator', () => {
expect(mockSAMLAuthenticationProvider2.login.mock.invocationCallOrder[0]).toBeLessThan(
mockSAMLAuthenticationProvider1.login.mock.invocationCallOrder[0]
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
});
@ -595,6 +636,10 @@ describe('Authenticator', () => {
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('clears session if provider asked to do so in `succeeded` result.', async () => {
@ -615,6 +660,10 @@ describe('Authenticator', () => {
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('clears session if provider asked to do so in `redirected` result.', async () => {
@ -634,6 +683,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
describe('with Access Agreement', () => {
@ -670,6 +720,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.succeeded(mockUser));
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('does not redirect to Access Agreement if request cannot be handled', async () => {
@ -681,6 +732,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.notHandled());
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if authentication fails', async () => {
@ -695,6 +747,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.failed(failureReason));
expectAuditEvents({ action: 'user_login', outcome: 'failure' });
});
it('does not redirect to Access Agreement if redirect is required to complete login', async () => {
@ -708,6 +761,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.redirectTo('/some-url', { state: 'some-state' }));
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if user has already acknowledged it', async () => {
@ -724,6 +778,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('does not redirect to Access Agreement its own requests', async () => {
@ -737,6 +792,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('does not redirect to Access Agreement if it is not configured', async () => {
@ -752,6 +808,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('does not redirect to Access Agreement if license doesnt allow it.', async () => {
@ -768,6 +825,7 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('redirects to Access Agreement when needed.', async () => {
@ -793,6 +851,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('redirects to Access Agreement preserving redirect URL specified in login attempt.', async () => {
@ -822,6 +881,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('redirects to Access Agreement preserving redirect URL specified in the authentication result.', async () => {
@ -848,6 +908,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('redirects AJAX requests to Access Agreement when needed.', async () => {
@ -873,6 +934,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
});
@ -909,6 +971,10 @@ describe('Authenticator', () => {
await expect(
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('does not redirect to Overwritten Session if username and provider did not change', async () => {
@ -930,6 +996,7 @@ describe('Authenticator', () => {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
expectAuditEvents({ action: 'user_login', outcome: 'success' });
});
it('does not redirect to Overwritten Session if session was unauthenticated before login', async () => {
@ -952,6 +1019,10 @@ describe('Authenticator', () => {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
expectAuditEvents(
// We do not record a user_logout event for "intermediate" sessions that are deleted, only user_login for the new session
{ action: 'user_login', outcome: 'success' }
);
});
it('redirects to Overwritten Session when username changes', async () => {
@ -977,6 +1048,10 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('redirects to Overwritten Session when provider changes', async () => {
@ -1005,6 +1080,10 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('redirects to Overwritten Session preserving redirect URL specified in login attempt.', async () => {
@ -1034,6 +1113,10 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('redirects to Overwritten Session preserving redirect URL specified in the authentication result.', async () => {
@ -1060,6 +1143,10 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
it('redirects AJAX requests to Overwritten Session when needed.', async () => {
@ -1085,6 +1172,10 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents(
{ action: 'user_logout', outcome: 'unknown' },
{ action: 'user_login', outcome: 'success' }
);
});
});
});
@ -1093,16 +1184,10 @@ describe('Authenticator', () => {
let authenticator: Authenticator;
let mockOptions: ReturnType<typeof getMockOptions>;
let mockSessVal: SessionValue;
const auditLogger = {
log: jest.fn(),
enabled: true,
};
beforeEach(() => {
auditLogger.log.mockClear();
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
mockOptions.session.get.mockResolvedValue(null);
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
authenticator = new Authenticator(mockOptions);
@ -1112,6 +1197,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(undefined as any)).rejects.toThrowError(
'Request should be a valid "KibanaRequest" instance, was [undefined].'
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('fails if an authentication provider fails.', async () => {
@ -1125,6 +1211,7 @@ describe('Authenticator', () => {
const authenticationResult = await authenticator.authenticate(request);
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('returns user that authentication provider returns.', async () => {
@ -1140,6 +1227,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('creates session whenever authentication provider returns state for system API requests', async () => {
@ -1163,6 +1251,7 @@ describe('Authenticator', () => {
provider: mockSessVal.provider,
state: { authorization },
});
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('creates session whenever authentication provider returns state for non-system API requests', async () => {
@ -1186,6 +1275,7 @@ describe('Authenticator', () => {
provider: mockSessVal.provider,
state: { authorization },
});
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not extend session for system API calls.', async () => {
@ -1207,6 +1297,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('extends session for non-system API calls.', async () => {
@ -1229,6 +1320,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => {
@ -1250,6 +1342,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => {
@ -1271,6 +1364,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('replaces existing session with the one returned by authentication provider for system API requests', async () => {
@ -1297,6 +1391,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => {
@ -1323,6 +1418,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => {
@ -1342,11 +1438,12 @@ describe('Authenticator', () => {
AuthenticationResult.failed(failureReason)
);
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => {
@ -1366,11 +1463,12 @@ describe('Authenticator', () => {
AuthenticationResult.failed(failureReason)
);
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('clears session if provider requested it via setting state to `null`.', async () => {
@ -1385,31 +1483,12 @@ describe('Authenticator', () => {
AuthenticationResult.redirectTo('some-url', { state: null })
);
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
});
it('adds audit event when invalidating session.', async () => {
const request = httpServerMock.createKibanaRequest();
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
AuthenticationResult.redirectTo('some-url', { state: null })
);
mockOptions.session.get.mockResolvedValue(mockSessVal);
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.redirectTo('some-url', { state: null })
);
expect(auditLogger.log).toHaveBeenCalledTimes(1);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: { action: 'user_logout', category: ['authentication'], outcome: 'unknown' },
})
);
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('does not clear session if provider can not handle system API request authentication with active session.', async () => {
@ -1423,10 +1502,11 @@ describe('Authenticator', () => {
AuthenticationResult.notHandled()
);
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => {
@ -1440,10 +1520,11 @@ describe('Authenticator', () => {
AuthenticationResult.notHandled()
);
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
describe('with Login Selector', () => {
@ -1464,6 +1545,7 @@ describe('Authenticator', () => {
AuthenticationResult.notHandled()
);
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect AJAX requests to Login Selector', async () => {
@ -1473,6 +1555,7 @@ describe('Authenticator', () => {
AuthenticationResult.notHandled()
);
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Login Selector if request has `Authorization` header', async () => {
@ -1484,6 +1567,7 @@ describe('Authenticator', () => {
AuthenticationResult.notHandled()
);
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Login Selector if it is not enabled', async () => {
@ -1495,6 +1579,7 @@ describe('Authenticator', () => {
AuthenticationResult.notHandled()
);
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('redirects to the Login Selector when needed.', async () => {
@ -1513,6 +1598,7 @@ describe('Authenticator', () => {
)
);
expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('redirects to the Login Selector with auth provider hint when needed.', async () => {
@ -1536,6 +1622,7 @@ describe('Authenticator', () => {
);
expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
});
@ -1565,6 +1652,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect AJAX requests to Access Agreement', async () => {
@ -1574,6 +1662,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if request cannot be handled', async () => {
@ -1587,6 +1676,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.notHandled()
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if authentication fails', async () => {
@ -1601,6 +1691,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.failed(failureReason)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if redirect is required to complete authentication', async () => {
@ -1614,6 +1705,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.redirectTo('/some-url')
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if user has already acknowledged it', async () => {
@ -1626,6 +1718,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement its own requests', async () => {
@ -1635,6 +1728,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if it is not configured', async () => {
@ -1646,6 +1740,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Access Agreement if license doesnt allow it.', async () => {
@ -1658,6 +1753,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('redirects to Access Agreement when needed.', async () => {
@ -1677,6 +1773,7 @@ describe('Authenticator', () => {
{ user: mockUser, authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' } }
)
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
});
@ -1712,6 +1809,7 @@ describe('Authenticator', () => {
await expect(authenticator.authenticate(request)).resolves.toEqual(
AuthenticationResult.succeeded(mockUser)
);
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('does not redirect AJAX requests to Overwritten Session', async () => {
@ -1731,6 +1829,7 @@ describe('Authenticator', () => {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('does not redirect to Overwritten Session if username and provider did not change', async () => {
@ -1750,6 +1849,7 @@ describe('Authenticator', () => {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('redirects to Overwritten Session when username changes', async () => {
@ -1773,6 +1873,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('redirects to Overwritten Session when provider changes', async () => {
@ -1799,6 +1900,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('redirects to Overwritten Session preserving redirect URL specified in the authentication result.', async () => {
@ -1823,6 +1925,7 @@ describe('Authenticator', () => {
}
)
);
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
});
});
@ -1831,16 +1934,10 @@ describe('Authenticator', () => {
let authenticator: Authenticator;
let mockOptions: ReturnType<typeof getMockOptions>;
let mockSessVal: SessionValue;
const auditLogger = {
log: jest.fn(),
enabled: true,
};
beforeEach(() => {
auditLogger.log.mockClear();
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
mockOptions.session.get.mockResolvedValue(null);
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
authenticator = new Authenticator(mockOptions);
@ -1863,8 +1960,8 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not redirect to Login Selector even if it is enabled if session is not available.', async () => {
@ -1885,8 +1982,8 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not clear session if provider cannot handle authentication', async () => {
@ -1905,12 +2002,12 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalledTimes(1);
expect(mockBasicAuthenticationProvider.authenticate).toBeCalledWith(
request,
mockSessVal.state
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('does not clear session if authentication fails with non-401 reason.', async () => {
@ -1930,6 +2027,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('extends session if no update is needed.', async () => {
@ -1945,11 +2043,12 @@ describe('Authenticator', () => {
AuthenticationResult.succeeded(user)
);
expect(mockOptions.session.extend).toHaveBeenCalledTimes(1);
expect(mockOptions.session.extend).toHaveBeenCalledWith(request, mockSessVal);
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).toHaveBeenCalledTimes(1);
expect(mockOptions.session.extend).toHaveBeenCalledWith(request, mockSessVal);
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('replaces existing session with the one returned by authentication provider', async () => {
@ -1966,14 +2065,15 @@ describe('Authenticator', () => {
AuthenticationResult.succeeded(user, { state: newState })
);
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).toHaveBeenCalledTimes(1);
expect(mockOptions.session.update).toHaveBeenCalledWith(request, {
...mockSessVal,
state: newState,
});
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('clears session if provider failed to authenticate request with 401.', async () => {
@ -1991,18 +2091,12 @@ describe('Authenticator', () => {
AuthenticationResult.failed(failureReason)
);
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expect(mockOptions.session.create).not.toHaveBeenCalled();
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.session.extend).not.toHaveBeenCalled();
expect(auditLogger.log).toHaveBeenCalledTimes(1);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: { action: 'user_logout', category: ['authentication'], outcome: 'unknown' },
})
);
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
});
@ -2010,15 +2104,9 @@ describe('Authenticator', () => {
let authenticator: Authenticator;
let mockOptions: ReturnType<typeof getMockOptions>;
let mockSessVal: SessionValue;
const auditLogger = {
log: jest.fn(),
enabled: true,
};
beforeEach(() => {
auditLogger.log.mockClear();
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
authenticator = new Authenticator(mockOptions);
@ -2028,6 +2116,7 @@ describe('Authenticator', () => {
await expect(authenticator.logout(undefined as any)).rejects.toThrowError(
'Request should be a valid "KibanaRequest" instance, was [undefined].'
);
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('redirects to login form if session does not exist.', async () => {
@ -2040,6 +2129,7 @@ describe('Authenticator', () => {
);
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('clears session and returns whatever authentication provider returns.', async () => {
@ -2055,25 +2145,7 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
expect(mockOptions.session.invalidate).toHaveBeenCalled();
});
it('adds audit event.', async () => {
const request = httpServerMock.createKibanaRequest();
mockBasicAuthenticationProvider.logout.mockResolvedValue(
DeauthenticationResult.redirectTo('some-url')
);
mockOptions.session.get.mockResolvedValue(mockSessVal);
await expect(authenticator.logout(request)).resolves.toEqual(
DeauthenticationResult.redirectTo('some-url')
);
expect(auditLogger.log).toHaveBeenCalledTimes(1);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: { action: 'user_logout', category: ['authentication'], outcome: 'unknown' },
})
);
expectAuditEvents({ action: 'user_logout', outcome: 'unknown' });
});
it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => {
@ -2093,6 +2165,7 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null);
expect(mockOptions.session.invalidate).toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => {
@ -2110,6 +2183,7 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request);
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('if session does not exist and providers is empty, redirects to default logout path.', async () => {
@ -2128,6 +2202,7 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('redirects to login form if session does not exist and provider name is invalid', async () => {
@ -2140,6 +2215,7 @@ describe('Authenticator', () => {
expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled();
expect(mockOptions.session.invalidate).toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
});
@ -2147,16 +2223,11 @@ describe('Authenticator', () => {
let authenticator: Authenticator;
let mockOptions: ReturnType<typeof getMockOptions>;
let mockSessionValue: SessionValue;
const auditLogger = {
log: jest.fn(),
enabled: true,
};
beforeEach(() => {
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
mockSessionValue = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
mockOptions.session.get.mockResolvedValue(mockSessionValue);
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
mockOptions.getCurrentUser.mockReturnValue(mockAuthenticatedUser());
mockOptions.license.getFeatures.mockReturnValue({
allowAccessAgreement: true,
@ -2176,6 +2247,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('fails if cannot retrieve user session', async () => {
@ -2189,6 +2261,7 @@ describe('Authenticator', () => {
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
});
it('fails if license does not allow access agreement acknowledgement', async () => {
@ -2203,6 +2276,7 @@ describe('Authenticator', () => {
);
expect(mockOptions.session.update).not.toHaveBeenCalled();
expect(auditLogger.log).not.toHaveBeenCalled();
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled();
});
@ -2215,17 +2289,10 @@ describe('Authenticator', () => {
...mockSessionValue,
accessAgreementAcknowledged: true,
});
expect(auditLogger.log).toHaveBeenCalledTimes(1);
expect(auditLogger.log).toHaveBeenCalledWith(
expect.objectContaining({
event: { action: 'access_agreement_acknowledged', category: ['authentication'] },
})
);
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).toHaveBeenCalledTimes(
1
);
expectAuditEvents({ action: 'access_agreement_acknowledged' });
});
});

View file

@ -90,6 +90,16 @@ export interface AuthenticatorOptions {
getServerBaseURL: () => string;
}
/** @internal */
interface InvalidateSessionValueParams {
/** Request instance. */
request: KibanaRequest;
/** Value of the existing session, if any. */
sessionValue: SessionValue | null;
/** If enabled, skips writing a `user_logout` audit event for this session. */
skipAuditEvent?: boolean;
}
// Mapping between provider key defined in the config and authentication
// provider class that can handle specific authentication mechanism.
const providerMap = new Map<
@ -325,6 +335,9 @@ export class Authenticator {
const auditLogger = this.options.audit.asScoped(request);
auditLogger.log(
userLoginEvent({
// We must explicitly specify the sessionId for login events because we just created the session, so
// it won't automatically get included in the audit event from the request context.
sessionId: sessionUpdateResult?.value?.sid,
authenticationResult,
authenticationProvider: providerName,
authenticationType: provider.type,
@ -446,7 +459,7 @@ export class Authenticator {
sessionValue?.provider.name ??
request.url.searchParams.get(LOGOUT_PROVIDER_QUERY_STRING_PARAMETER);
if (suggestedProviderName) {
await this.invalidateSessionValue(request, sessionValue);
await this.invalidateSessionValue({ request, sessionValue });
// Provider name may be passed in a query param and sourced from the browser's local storage;
// hence, we can't assume that this provider exists, so we have to check it.
@ -595,7 +608,7 @@ export class Authenticator {
this.logger.warn(
`Attempted to retrieve session for the "${existingSessionValue.provider.type}/${existingSessionValue.provider.name}" provider, but it is not configured.`
);
await this.invalidateSessionValue(request, existingSessionValue);
await this.invalidateSessionValue({ request, sessionValue: existingSessionValue });
return null;
}
@ -629,7 +642,7 @@ export class Authenticator {
// attempt didn't fail.
if (authenticationResult.shouldClearState()) {
this.logger.debug('Authentication provider requested to invalidate existing session.');
await this.invalidateSessionValue(request, existingSessionValue);
await this.invalidateSessionValue({ request, sessionValue: existingSessionValue });
return null;
}
@ -643,7 +656,7 @@ export class Authenticator {
if (authenticationResult.failed()) {
if (ownsSession && getErrorStatusCode(authenticationResult.error) === 401) {
this.logger.debug('Authentication attempt failed, existing session will be invalidated.');
await this.invalidateSessionValue(request, existingSessionValue);
await this.invalidateSessionValue({ request, sessionValue: existingSessionValue });
}
return null;
}
@ -681,17 +694,21 @@ export class Authenticator {
this.logger.debug(
'Authentication provider has changed, existing session will be invalidated.'
);
await this.invalidateSessionValue(request, existingSessionValue);
await this.invalidateSessionValue({ request, sessionValue: existingSessionValue });
existingSessionValue = null;
} else if (sessionHasBeenAuthenticated) {
this.logger.debug(
'Session is authenticated, existing unauthenticated session will be invalidated.'
);
await this.invalidateSessionValue(request, existingSessionValue);
await this.invalidateSessionValue({
request,
sessionValue: existingSessionValue,
skipAuditEvent: true, // Skip writing an audit event when we are replacing an intermediate session with a fullly authenticated session
});
existingSessionValue = null;
} else if (usernameHasChanged) {
this.logger.debug('Username has changed, existing session will be invalidated.');
await this.invalidateSessionValue(request, existingSessionValue);
await this.invalidateSessionValue({ request, sessionValue: existingSessionValue });
existingSessionValue = null;
}
@ -726,11 +743,13 @@ export class Authenticator {
/**
* Invalidates session value associated with the specified request.
* @param request Request instance.
* @param sessionValue Value of the existing session if any.
*/
private async invalidateSessionValue(request: KibanaRequest, sessionValue: SessionValue | null) {
if (sessionValue) {
private async invalidateSessionValue({
request,
sessionValue,
skipAuditEvent,
}: InvalidateSessionValueParams) {
if (sessionValue && !skipAuditEvent) {
const auditLogger = this.options.audit.asScoped(request);
auditLogger.log(
userLogoutEvent({

View file

@ -10,7 +10,7 @@ import type { TransportResult } from '@elastic/elasticsearch';
import { licenseMock } from '../common/licensing/index.mock';
import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock';
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
import { auditServiceMock } from './audit/index.mock';
import { auditServiceMock } from './audit/mocks';
import { authenticationServiceMock } from './authentication/authentication_service.mock';
import { authorizationMock } from './authorization/index.mock';

View file

@ -15,10 +15,10 @@ import type {
SavedObjectsResolveResponse,
SavedObjectsUpdateObjectsSpacesResponseObject,
} from 'src/core/server';
import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import type { AuditEvent } from '../audit';
import { auditServiceMock } from '../audit/index.mock';
import { auditLoggerMock } from '../audit/mocks';
import { Actions } from '../authorization';
import type { SavedObjectActions } from '../authorization/actions/saved_object';
import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
@ -65,7 +65,7 @@ const createSecureSavedObjectsClientWrapperOptions = () => {
checkSavedObjectsPrivilegesAsCurrentUser: jest.fn(),
errors,
getSpacesService,
auditLogger: auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()),
auditLogger: auditLoggerMock.create(),
forbiddenError,
generalError,
};

View file

@ -18,7 +18,7 @@ import type { ElasticsearchClient } from 'src/core/server';
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
import type { AuditLogger } from '../audit';
import { auditServiceMock } from '../audit/index.mock';
import { auditLoggerMock } from '../audit/mocks';
import { ConfigSchema, createConfig } from '../config';
import { securityMock } from '../mocks';
import { getSessionIndexTemplate, SessionIndex } from './session_index';
@ -32,7 +32,7 @@ describe('Session index', () => {
const indexTemplateName = '.kibana_some_tenant_security_session_index_template_1';
beforeEach(() => {
mockElasticsearchClient = elasticsearchServiceMock.createElasticsearchClient();
auditLogger = auditServiceMock.create().withoutRequest;
auditLogger = auditLoggerMock.create();
sessionIndex = new SessionIndex({
logger: loggingSystemMock.createLogger(),
kibanaIndexName: '.kibana_some_tenant',
@ -364,7 +364,7 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
auditLogger: auditServiceMock.create().withoutRequest,
auditLogger,
});
await sessionIndex.cleanUp();
@ -456,7 +456,7 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
auditLogger: auditServiceMock.create().withoutRequest,
auditLogger,
});
await sessionIndex.cleanUp();
@ -542,7 +542,7 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
auditLogger: auditServiceMock.create().withoutRequest,
auditLogger,
});
await sessionIndex.cleanUp();
@ -653,7 +653,7 @@ describe('Session index', () => {
{ isTLSEnabled: false }
),
elasticsearchClient: mockElasticsearchClient,
auditLogger: auditServiceMock.create().withoutRequest,
auditLogger,
});
await sessionIndex.cleanUp();

View file

@ -16,7 +16,7 @@ import type {
} from '../../../task_manager/server';
import { taskManagerMock } from '../../../task_manager/server/mocks';
import type { AuditLogger } from '../audit';
import { auditServiceMock } from '../audit/index.mock';
import { auditLoggerMock } from '../audit/mocks';
import { ConfigSchema, createConfig } from '../config';
import type { OnlineStatusRetryScheduler } from '../elasticsearch';
import { Session } from './session';
@ -37,7 +37,7 @@ describe('SessionManagementService', () => {
let auditLogger: AuditLogger;
beforeEach(() => {
service = new SessionManagementService(loggingSystemMock.createLogger());
auditLogger = auditServiceMock.create().withoutRequest;
auditLogger = auditLoggerMock.create();
});
afterEach(() => {

View file

@ -20,7 +20,7 @@ import type { GetAllSpacesPurpose, LegacyUrlAliasTarget, Space } from '../../../
import { spacesClientMock } from '../../../spaces/server/mocks';
import type { AuditEvent, AuditLogger } from '../audit';
import { SavedObjectAction, SpaceAuditAction } from '../audit';
import { auditServiceMock } from '../audit/index.mock';
import { auditLoggerMock } from '../audit/mocks';
import type {
AuthorizationServiceSetup,
AuthorizationServiceSetupInternal,
@ -98,7 +98,7 @@ const setup = ({ securityEnabled = false }: Opts = {}) => {
});
authorization.mode.useRbacForRequest.mockReturnValue(securityEnabled);
const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest());
const auditLogger = auditLoggerMock.create();
const request = httpServerMock.createKibanaRequest();

View file

@ -8,7 +8,7 @@
import { coreMock, httpServerMock } from 'src/core/server/mocks';
import { spacesMock } from '../../../spaces/server/mocks';
import { auditServiceMock } from '../audit/index.mock';
import { auditServiceMock } from '../audit/mocks';
import { authorizationMock } from '../authorization/index.mock';
import { setupSpacesClient } from './setup_spaces_client';