Skip to content

Commit

Permalink
RBAC - SecurityAuditLogger (elastic#19571)
Browse files Browse the repository at this point in the history
* Manually porting over the AuditLogger for use within the security audit
logger

* HasPrivileges now returns the user from the request

* Has privileges returns username from privilegeCheck

* Adding first eventType to the security audit logger

* Adding authorization success message

* Logging arguments when authorization success

* Fixing test description

* Logging args during audit failures
  • Loading branch information
kobelb authored and legrego committed Jun 22, 2018
1 parent fb843cd commit 9380239
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 24 deletions.
7 changes: 7 additions & 0 deletions x-pack/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { registerPrivilegesWithCluster } from './server/lib/privileges';
import { createDefaultRoles } from './server/lib/authorization/create_default_roles';
import { initPrivilegesApi } from './server/routes/api/v1/privileges';
import { hasPrivilegesWithServer } from './server/lib/authorization/has_privileges';
import { SecurityAuditLogger } from './server/lib/audit_logger';
import { AuditLogger } from '../../server/lib/audit_logger';

export const security = (kibana) => new kibana.Plugin({
id: 'security',
Expand Down Expand Up @@ -51,6 +53,9 @@ export const security = (kibana) => new kibana.Plugin({
`may contain alphanumeric characters (a-z, A-Z, 0-9), underscores and hyphens`
),
}).default(),
audit: Joi.object({
enabled: Joi.boolean().default(false)
}).default(),
}).default();
},

Expand Down Expand Up @@ -98,6 +103,8 @@ export const security = (kibana) => new kibana.Plugin({
await createDefaultRoles(server);
});

server.expose('auditLogger', new SecurityAuditLogger(server.config(), new AuditLogger(server, 'security')));

// Register a function that is called whenever the xpack info changes,
// to re-compute the license check results for this plugin
xpackMainPlugin.info.feature(this.id).registerLicenseCheckResultsGenerator(checkLicense);
Expand Down
47 changes: 47 additions & 0 deletions x-pack/plugins/security/server/lib/audit_logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

export class SecurityAuditLogger {
constructor(config, auditLogger) {
this._enabled = config.get('xpack.security.audit.enabled');
this._auditLogger = auditLogger;
}

savedObjectsAuthorizationFailure(username, action, types, missing, args) {
if (!this._enabled) {
return;
}

this._auditLogger.log(
'saved_objects_authorization_failure',
`${username} unauthorized to ${action} ${types.join(',')}, missing ${missing.join(',')}`,
{
username,
action,
types,
missing,
args
}
);
}

savedObjectsAuthorizationSuccess(username, action, types, args) {
if (!this._enabled) {
return;
}

this._auditLogger.log(
'saved_objects_authorization_success',
`${username} authorized to ${action} ${types.join(',')}`,
{
username,
action,
types,
args,
}
);
}
}
113 changes: 113 additions & 0 deletions x-pack/plugins/security/server/lib/audit_logger.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SecurityAuditLogger } from './audit_logger';


const createMockConfig = (settings) => {
const mockConfig = {
get: jest.fn()
};

const defaultSettings = {};

mockConfig.get.mockImplementation(key => {
return key in settings ? settings[key] : defaultSettings[key];
});

return mockConfig;
};

const createMockAuditLogger = () => {
return {
log: jest.fn()
};
};

describe(`#savedObjectsAuthorizationFailure`, () => {
test(`doesn't log anything when xpack.security.audit.enabled is false`, () => {
const config = createMockConfig({
'xpack.security.audit.enabled': false
});
const auditLogger = createMockAuditLogger();

const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
securityAuditLogger.savedObjectsAuthorizationFailure();

expect(auditLogger.log).toHaveBeenCalledTimes(0);
});

test('logs via auditLogger when xpack.security.audit.enabled is true', () => {
const config = createMockConfig({
'xpack.security.audit.enabled': true
});
const auditLogger = createMockAuditLogger();
const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
const username = 'foo-user';
const action = 'foo-action';
const types = [ 'foo-type-1', 'foo-type-2' ];
const missing = [`action:saved-objects/${types[0]}/foo-action`, `action:saved-objects/${types[1]}/foo-action`];
const args = {
'foo': 'bar',
'baz': 'quz',
};

securityAuditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args);

expect(auditLogger.log).toHaveBeenCalledWith(
'saved_objects_authorization_failure',
expect.stringContaining(`${username} unauthorized to ${action}`),
{
username,
action,
types,
missing,
args,
}
);
});
});

describe(`#savedObjectsAuthorizationSuccess`, () => {
test(`doesn't log anything when xpack.security.audit.enabled is false`, () => {
const config = createMockConfig({
'xpack.security.audit.enabled': false
});
const auditLogger = createMockAuditLogger();

const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
securityAuditLogger.savedObjectsAuthorizationSuccess();

expect(auditLogger.log).toHaveBeenCalledTimes(0);
});

test('logs via auditLogger when xpack.security.audit.enabled is true', () => {
const config = createMockConfig({
'xpack.security.audit.enabled': true
});
const auditLogger = createMockAuditLogger();
const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
const username = 'foo-user';
const action = 'foo-action';
const types = [ 'foo-type-1', 'foo-type-2' ];
const args = {
'foo': 'bar',
'baz': 'quz',
};

securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, args);

expect(auditLogger.log).toHaveBeenCalledWith(
'saved_objects_authorization_success',
expect.stringContaining(`${username} authorized to ${action}`),
{
username,
action,
types,
args,
}
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export function hasPrivilegesWithServer(server) {

return {
success,
missing: missingPrivileges
missing: missingPrivileges,
username: privilegeCheck.username,
};
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ const createMockServer = ({ settings = {} } = {}) => {
return mockServer;
};

const mockResponse = (hasAllRequested, privileges, application = defaultApplication) => {
const mockResponse = (hasAllRequested, privileges, application = defaultApplication, username = '') => {
mockCallWithRequest.mockImplementationOnce(async () => ({
username: username,
has_all_requested: hasAllRequested,
application: {
[application]: {
Expand Down Expand Up @@ -151,30 +152,50 @@ test(`returns success when has_all_requested`, async () => {
expect(result.success).toBe(true);
});

test(`returns username from has_privileges response when has_all_requested`, async () => {
const mockServer = createMockServer();
const username = 'foo-username';
mockResponse(true, {
[getVersionPrivilege(defaultVersion)]: true,
[getLoginPrivilege()]: true,
foo: true,
}, defaultApplication, username);

const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer);
const hasPrivileges = hasPrivilegesWithRequest({});
const result = await hasPrivileges(['foo']);
expect(result.username).toBe(username);
});

test(`returns false success when has_all_requested is false`, async () => {
const mockServer = createMockServer();
mockResponse(false, {
[getVersionPrivilege(defaultVersion)]: true,
[getLoginPrivilege()]: true,
foo: false,
});
mockCallWithRequest.mockImplementationOnce(async () => ({
has_all_requested: false,
application: {
[defaultApplication]: {
[DEFAULT_RESOURCE]: {
foo: false
}
}
}
}));

const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer);
const hasPrivileges = hasPrivilegesWithRequest({});
const result = await hasPrivileges(['foo']);
expect(result.success).toBe(false);
});

test(`returns username from has_privileges when has_all_requested is false`, async () => {
const username = 'foo-username';
const mockServer = createMockServer();
mockResponse(false, {
[getVersionPrivilege(defaultVersion)]: true,
[getLoginPrivilege()]: true,
foo: false,
}, defaultApplication, username);

const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer);
const hasPrivileges = hasPrivilegesWithRequest({ });
const result = await hasPrivileges(['foo']);
expect(result.username).toBe(username);
});

test(`returns missing privileges`, async () => {
const mockServer = createMockServer();
mockResponse(false, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
export function secureSavedObjectsClientOptionsBuilder(server, hasPrivilegesWithRequest, options) {
const adminCluster = server.plugins.elasticsearch.getCluster('admin');
const { callWithInternalUser } = adminCluster;
const auditLogger = server.plugins.security.auditLogger;

return {
...options,
callCluster: callWithInternalUser,
hasPrivilegesWithRequest
hasPrivilegesWithRequest,
auditLogger
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,83 @@ export class SecureSavedObjectsClient {
request,
hasPrivilegesWithRequest,
baseClient,
auditLogger,
} = options;

this.errors = baseClient.errors;

this._client = baseClient;
this._hasPrivileges = hasPrivilegesWithRequest(request);
this._auditLogger = auditLogger;
}

async create(type, attributes = {}, options = {}) {
await this._performAuthorizationCheck(type, 'create');
await this._performAuthorizationCheck(type, 'create', {
type,
attributes,
options,
});

return await this._client.create(type, attributes, options);
}

async bulkCreate(objects, options = {}) {
const types = uniq(objects.map(o => o.type));
await this._performAuthorizationCheck(types, 'create');
await this._performAuthorizationCheck(types, 'create', {
objects,
options,
});

return await this._client.bulkCreate(objects, options);
}

async delete(type, id) {
await this._performAuthorizationCheck(type, 'delete');
await this._performAuthorizationCheck(type, 'delete', {
type,
id,
});

return await this._client.delete(type, id);
}

async find(options = {}) {
await this._performAuthorizationCheck(options.type, 'search');
await this._performAuthorizationCheck(options.type, 'search', {
options,
});

return await this._client.find(options);
}

async bulkGet(objects = []) {
for (const object of objects) {
await this._performAuthorizationCheck(object.type, 'mget');
}
const types = uniq(objects.map(o => o.type));
await this._performAuthorizationCheck(types, 'mget', {
objects,
});

return await this._client.bulkGet(objects);
}

async get(type, id) {
await this._performAuthorizationCheck(type, 'get');
await this._performAuthorizationCheck(type, 'get', {
type,
id,
});

return await this._client.get(type, id);
}

async update(type, id, attributes, options = {}) {
await this._performAuthorizationCheck(type, 'update');
await this._performAuthorizationCheck(type, 'update', {
type,
id,
attributes,
options,
});

return await this._client.update(type, id, attributes, options);
}

async _performAuthorizationCheck(typeOrTypes, action) {
async _performAuthorizationCheck(typeOrTypes, action, args) {
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
const actions = types.map(type => `action:saved-objects/${type}/${action}`);

Expand All @@ -77,7 +100,10 @@ export class SecureSavedObjectsClient {
throw this._client.errors.decorateGeneralError(error, reason);
}

if (!result.success) {
if (result.success) {
this._auditLogger.savedObjectsAuthorizationSuccess(result.username, action, types, args);
} else {
this._auditLogger.savedObjectsAuthorizationFailure(result.username, action, types, result.missing, args);
const msg = `Unable to ${action} ${types.join(',')}, missing ${result.missing.join(',')}`;
throw this._client.errors.decorateForbiddenError(new Error(msg));
}
Expand Down

0 comments on commit 9380239

Please sign in to comment.