diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 8e8ef10a9e273..c72ebfa74e1c0 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -121,23 +121,21 @@ export const security = (kibana) => new kibana.Plugin({ request, }) => { const adminCluster = server.plugins.elasticsearch.getCluster('admin'); + const { callWithRequest, callWithInternalUser } = adminCluster; + const callCluster = (...args) => callWithRequest(request, ...args); - if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) { - const { callWithRequest } = adminCluster; - const callCluster = (...args) => callWithRequest(request, ...args); - - const repository = savedObjects.getSavedObjectsRepository(callCluster); + const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster); - return new savedObjects.SavedObjectsClient(repository); + if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) { + return new savedObjects.SavedObjectsClient(callWithRequestRepository); } const hasPrivileges = hasPrivilegesWithRequest(request); - const { callWithInternalUser } = adminCluster; - - const repository = savedObjects.getSavedObjectsRepository(callWithInternalUser); + const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser); return new SecureSavedObjectsClient({ - repository, + internalRepository, + callWithRequestRepository, errors: savedObjects.SavedObjectsClient.errors, hasPrivileges, auditLogger, diff --git a/x-pack/plugins/security/server/lib/authorization/has_privileges.js b/x-pack/plugins/security/server/lib/authorization/has_privileges.js index 67de50730959c..2f68c7fadaf95 100644 --- a/x-pack/plugins/security/server/lib/authorization/has_privileges.js +++ b/x-pack/plugins/security/server/lib/authorization/has_privileges.js @@ -6,96 +6,12 @@ import { getClient } from '../../../../../server/lib/get_client_shield'; import { DEFAULT_RESOURCE } from '../../../common/constants'; -import { buildPrivilegeMap, getVersionPrivilege, getLoginPrivilege } from '../privileges'; - -const hasApplicationPrivileges = async (callWithRequest, request, kibanaVersion, application, privileges) => { - const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application, - resources: [DEFAULT_RESOURCE], - privileges - }] - } - }); - - const hasPrivileges = privilegeCheck.application[application][DEFAULT_RESOURCE]; - - // We include the login action in all privileges, so the existence of it and not the version privilege - // lets us know that we're running in an incorrect configuration. Without the login privilege check, we wouldn't - // know whether the user just wasn't authorized for this instance of Kibana in general - if (!hasPrivileges[getVersionPrivilege(kibanaVersion)] && hasPrivileges[getLoginPrivilege()]) { - throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.'); - } - - return { - username: privilegeCheck.username, - hasAllRequested: privilegeCheck.has_all_requested, - privileges: hasPrivileges - }; -}; - -const hasLegacyPrivileges = async ( - savedObjectTypes, - deprecationLogger, - callWithRequest, - request, - kibanaVersion, - application, - kibanaIndex, - privileges -) => { - const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', { - body: { - index: [{ - names: [kibanaIndex], - privileges: ['read', 'index'] - }] - } - }); - - const createPrivileges = (cb) => { - return privileges.reduce((acc, name) => { - acc[name] = cb(name); - return acc; - }, {}); - }; +import { getVersionPrivilege, getLoginPrivilege } from '../privileges'; - const logDeprecation = () => { - deprecationLogger( - `Relying on implicit privileges determined from the index privileges is deprecated and will be removed in Kibana 7.0` - ); - }; - - // if they have the index privilege, then we grant them all actions - if (privilegeCheck.index[kibanaIndex].index) { - logDeprecation(); - const implicitPrivileges = createPrivileges(() => true); - return { - username: privilegeCheck.username, - hasAllRequested: true, - privileges: implicitPrivileges - }; - } - - // if they have the read privilege, then we only grant them the read actions - if (privilegeCheck.index[kibanaIndex].read) { - logDeprecation(); - const privilegeMap = buildPrivilegeMap(savedObjectTypes, application, kibanaVersion); - const implicitPrivileges = createPrivileges(name => privilegeMap.read.actions.includes(name)); - - return { - username: privilegeCheck.username, - hasAllRequested: Object.values(implicitPrivileges).every(x => x), - privileges: implicitPrivileges, - }; - } - - return { - username: privilegeCheck.username, - hasAllRequested: false, - privileges: createPrivileges(() => false) - }; +export const HAS_PRIVILEGES_RESULT = { + UNAUTHORIZED: Symbol(), + AUTHORIZED: Symbol(), + LEGACY: Symbol(), }; export function hasPrivilegesWithServer(server) { @@ -105,45 +21,86 @@ export function hasPrivilegesWithServer(server) { const kibanaVersion = config.get('pkg.version'); const application = config.get('xpack.security.rbac.application'); const kibanaIndex = config.get('kibana.index'); - const savedObjectTypes = server.savedObjects.types; - const deprecationLogger = (msg) => server.log(['warning', 'deprecated', 'security'], msg); + + const loginPrivilege = getLoginPrivilege(); + const versionPrivilege = getVersionPrivilege(kibanaVersion); return function hasPrivilegesWithRequest(request) { - return async function hasPrivileges(privileges) { - const loginPrivilege = getLoginPrivilege(); - const versionPrivilege = getVersionPrivilege(kibanaVersion); + const hasApplicationPrivileges = async (privileges) => { + const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', { + body: { + applications: [{ + application, + resources: [DEFAULT_RESOURCE], + privileges + }] + } + }); + + const hasPrivileges = privilegeCheck.application[application][DEFAULT_RESOURCE]; + + // We include the login action in all privileges, so the existence of it and not the version privilege + // lets us know that we're running in an incorrect configuration. Without the login privilege check, we wouldn't + // know whether the user just wasn't authorized for this instance of Kibana in general + if (!hasPrivileges[getVersionPrivilege(kibanaVersion)] && hasPrivileges[getLoginPrivilege()]) { + throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.'); + } + + return { + username: privilegeCheck.username, + hasAllRequested: privilegeCheck.has_all_requested, + privileges: hasPrivileges + }; + }; + + const hasPrivilegesOnKibanaIndex = async () => { + const privilegeCheck = await callWithRequest(request, 'shield.hasPrivileges', { + body: { + index: [{ + names: [kibanaIndex], + privileges: ['create', 'delete', 'read', 'view_index_metadata'] + }] + } + }); + + return Object.values(privilegeCheck.index[kibanaIndex]).includes(true); + }; + + return async function hasPrivileges(privileges) { const allPrivileges = [versionPrivilege, loginPrivilege, ...privileges]; - let privilegesCheck = await hasApplicationPrivileges( - callWithRequest, - request, - kibanaVersion, - application, - allPrivileges - ); - - if (!privilegesCheck.privileges[loginPrivilege]) { - privilegesCheck = await hasLegacyPrivileges( - savedObjectTypes, - deprecationLogger, - callWithRequest, - request, - kibanaVersion, - application, - kibanaIndex, - allPrivileges - ); + const privilegesCheck = await hasApplicationPrivileges(allPrivileges); + + const username = privilegesCheck.username; + + // We don't want to expose the version privilege to consumers, as it's an implementation detail only to detect version mismatch + const missing = Object.keys(privilegesCheck.privileges) + .filter(p => !privilegesCheck.privileges[p]) + .filter(p => p !== versionPrivilege); + + if (privilegesCheck.hasAllRequested) { + return { + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + missing, + }; } - const success = privilegesCheck.hasAllRequested; + if (!privilegesCheck.privileges[loginPrivilege] && await hasPrivilegesOnKibanaIndex()) { + const msg = `Relying on implicit privileges determined from the index privileges is deprecated and will be removed in Kibana 7.0`; + server.log(['warning', 'deprecated', 'security'], msg); + + return { + result: HAS_PRIVILEGES_RESULT.LEGACY, + username, + missing, + }; + } return { - success, - // We don't want to expose the version privilege to consumers, as it's an implementation detail only to detect version mismatch - missing: Object.keys(privilegesCheck.privileges) - .filter(key => privilegesCheck.privileges[key] === false) - .filter(p => p !== versionPrivilege), - username: privilegesCheck.username, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, + missing, }; }; }; diff --git a/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js b/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js index 5203d477b16c8..7f38ba3965b78 100644 --- a/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js +++ b/x-pack/plugins/security/server/lib/authorization/has_privileges.test.js @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { hasPrivilegesWithServer } from './has_privileges'; +import { hasPrivilegesWithServer, HAS_PRIVILEGES_RESULT } from './has_privileges'; import { getClient } from '../../../../../server/lib/get_client_shield'; import { DEFAULT_RESOURCE } from '../../../common/constants'; -import { getLoginPrivilege, getVersionPrivilege, buildPrivilegeMap } from '../privileges'; +import { getLoginPrivilege, getVersionPrivilege } from '../privileges'; jest.mock('../../../../../server/lib/get_client_shield', () => ({ getClient: jest.fn() })); -const defaultKibanaIndex = 'default-kibana-index'; const defaultVersion = 'default-version'; const defaultApplication = 'default-application'; +const defaultKibanaIndex = 'default-index'; const savedObjectTypes = ['foo-type', 'bar-type']; const createMockServer = ({ settings = {} } = {}) => { @@ -27,9 +27,9 @@ const createMockServer = ({ settings = {} } = {}) => { }; const defaultSettings = { - 'kibana.index': defaultKibanaIndex, 'pkg.version': defaultVersion, - 'xpack.security.rbac.application': defaultApplication + 'xpack.security.rbac.application': defaultApplication, + 'kibana.index': defaultKibanaIndex, }; mockServer.config().get.mockImplementation(key => { @@ -55,10 +55,8 @@ const mockApplicationPrivilegeResponse = ({ hasAllRequested, privileges, applica }; }; -const mockLegacyResponse = ({ hasAllRequested, privileges, index = defaultKibanaIndex, username = '' }) => { +const mockKibanaIndexPrivilegesResponse = ({ privileges, index = defaultKibanaIndex }) => { return { - username: username, - has_all_requested: hasAllRequested, index: { [index]: privileges } @@ -86,7 +84,7 @@ const expectDeprecationLogged = (mockServer) => { expect(mockServer.log).toHaveBeenCalledWith(['warning', 'deprecated', 'security'], expect.stringContaining('deprecated')); }; -test(`returns success of true if they have all application privileges`, async () => { +test(`returns authorized if they have all application privileges`, async () => { const privilege = `action:saved_objects/${savedObjectTypes[0]}/get`; const username = 'foo-username'; const mockServer = createMockServer(); @@ -122,13 +120,13 @@ test(`returns success of true if they have all application privileges`, async () } }); expect(result).toEqual({ - success: true, + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, missing: [], - username }); }); -test(`returns success of false if they have only one application privilege`, async () => { +test(`returns unauthorized they have only one application privilege`, async () => { const privilege1 = `action:saved_objects/${savedObjectTypes[0]}/get`; const privilege2 = `action:saved_objects/${savedObjectTypes[0]}/create`; const username = 'foo-username'; @@ -166,9 +164,9 @@ test(`returns success of false if they have only one application privilege`, asy } }); expect(result).toEqual({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [privilege2], - username }); }); @@ -193,90 +191,8 @@ test(`throws error if missing version privilege and has login privilege`, async expectNoDeprecationLogged(mockServer); }); -test(`uses application privileges if the user has the login privilege`, async () => { - const privilege = `action:saved_objects/${savedObjectTypes[0]}/get`; - const username = 'foo-username'; - const mockServer = createMockServer(); - const callWithRequest = createMockCallWithRequest([ - mockApplicationPrivilegeResponse({ - hasAllRequested: false, - privileges: { - [getVersionPrivilege(defaultVersion)]: true, - [getLoginPrivilege()]: true, - [privilege]: false, - }, - username, - }), - ]); - - const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer); - const request = Symbol(); - const hasPrivileges = hasPrivilegesWithRequest(request); - const privileges = [privilege]; - const result = await hasPrivileges(privileges); - - expectNoDeprecationLogged(mockServer); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application: defaultApplication, - resources: [DEFAULT_RESOURCE], - privileges: [ - getVersionPrivilege(defaultVersion), getLoginPrivilege(), ...privileges - ] - }] - } - }); - expect(result).toEqual({ - success: false, - missing: [...privileges], - username, - }); -}); - -test(`returns success of false using application privileges if the user has the login privilege`, async () => { - const privilege = `action:saved_objects/${savedObjectTypes[0]}/get`; - const username = 'foo-username'; - const mockServer = createMockServer(); - const callWithRequest = createMockCallWithRequest([ - mockApplicationPrivilegeResponse({ - hasAllRequested: false, - privileges: { - [getVersionPrivilege(defaultVersion)]: true, - [getLoginPrivilege()]: true, - [privilege]: false, - }, - username, - }), - ]); - - const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer); - const request = Symbol(); - const hasPrivileges = hasPrivilegesWithRequest(request); - const privileges = [privilege]; - const result = await hasPrivileges(privileges); - - expectNoDeprecationLogged(mockServer); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application: defaultApplication, - resources: [DEFAULT_RESOURCE], - privileges: [ - getVersionPrivilege(defaultVersion), getLoginPrivilege(), ...privileges - ] - }] - } - }); - expect(result).toEqual({ - success: false, - missing: [...privileges], - username, - }); -}); - describe('legacy fallback with no application privileges', () => { - test(`returns success of false if the user has no legacy privileges`, async () => { + test(`returns unauthorized if they have no privileges on the kibana index`, async () => { const privilege = `action:saved_objects/${savedObjectTypes[0]}/get`; const username = 'foo-username'; const mockServer = createMockServer(); @@ -290,13 +206,13 @@ describe('legacy fallback with no application privileges', () => { }, username, }), - mockLegacyResponse({ - hasAllRequested: false, + mockKibanaIndexPrivilegesResponse({ privileges: { + create: false, + delete: false, read: false, - index: false, + view_index_metadata: false, }, - username, }) ]); @@ -322,198 +238,20 @@ describe('legacy fallback with no application privileges', () => { body: { index: [{ names: [ defaultKibanaIndex ], - privileges: ['read', 'index'] - }] - } - }); - expect(result).toEqual({ - success: false, - missing: [getLoginPrivilege(), ...privileges], - username, - }); - }); - - test(`returns success of true if the user has index privilege on kibana index`, async () => { - const privilege = 'something-completely-arbitrary'; - const username = 'foo-username'; - const mockServer = createMockServer(); - const callWithRequest = createMockCallWithRequest([ - mockApplicationPrivilegeResponse({ - hasAllRequested: false, - privileges: { - [getVersionPrivilege(defaultVersion)]: false, - [getLoginPrivilege()]: false, - [privilege]: false, - }, - username, - }), - mockLegacyResponse({ - hasAllRequested: false, - privileges: { - read: false, - index: true, - }, - username, - }) - ]); - - const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer); - const request = Symbol(); - const hasPrivileges = hasPrivilegesWithRequest(request); - const privileges = ['foo']; - const result = await hasPrivileges(privileges); - - expectDeprecationLogged(mockServer); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application: defaultApplication, - resources: [DEFAULT_RESOURCE], - privileges: [ - getVersionPrivilege(defaultVersion), getLoginPrivilege(), ...privileges - ] - }] - } - }); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - index: [{ - names: [ defaultKibanaIndex ], - privileges: ['read', 'index'] - }] - } - }); - expect(result).toEqual({ - success: true, - missing: [], - username, - }); - }); - - test(`returns success of false if the user has the read privilege on kibana index but the privilege isn't a read action`, async () => { - const privilege = 'something-completely-arbitrary'; - const username = 'foo-username'; - const mockServer = createMockServer(); - const callWithRequest = createMockCallWithRequest([ - mockApplicationPrivilegeResponse({ - hasAllRequested: false, - privileges: { - [getVersionPrivilege(defaultVersion)]: false, - [getLoginPrivilege()]: false, - [privilege]: false, - }, - username, - }), - mockLegacyResponse({ - hasAllRequested: false, - privileges: { - read: true, - index: false, - }, - username, - }) - ]); - - const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer); - const request = Symbol(); - const hasPrivileges = hasPrivilegesWithRequest(request); - const privileges = [privilege]; - const result = await hasPrivileges(privileges); - - expectDeprecationLogged(mockServer); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application: defaultApplication, - resources: [DEFAULT_RESOURCE], - privileges: [ - getVersionPrivilege(defaultVersion), getLoginPrivilege(), ...privileges - ] - }] - } - }); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - index: [{ - names: [ defaultKibanaIndex ], - privileges: ['read', 'index'] + privileges: ['create', 'delete', 'read', 'view_index_metadata'] }] } }); expect(result).toEqual({ - success: false, - missing: [ privilege ], + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, username, + missing: [getLoginPrivilege(), ...privileges], }); }); - test(`returns success of false if the user has the read privilege on kibana index but one privilege isn't a read action`, async () => { - const privilegeMap = buildPrivilegeMap(savedObjectTypes, defaultApplication, defaultVersion); - - const actions = privilegeMap.read.actions.filter(a => a !== getVersionPrivilege(defaultVersion) && a !== getLoginPrivilege()); - for (const action of actions) { - const privilege1 = 'something-completely-arbitrary'; - const privilege2 = action; - const username = 'foo-username'; - const mockServer = createMockServer(); - const callWithRequest = createMockCallWithRequest([ - mockApplicationPrivilegeResponse({ - hasAllRequested: false, - privileges: { - [getVersionPrivilege(defaultVersion)]: false, - [getLoginPrivilege()]: false, - [privilege1]: false, - [privilege2]: true - }, - username, - }), - mockLegacyResponse({ - hasAllRequested: false, - privileges: { - read: true, - index: false, - }, - username, - }) - ]); - const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer); - const request = Symbol(); - const hasPrivileges = hasPrivilegesWithRequest(request); - const privileges = [privilege1, privilege2]; - const result = await hasPrivileges(privileges); - - expectDeprecationLogged(mockServer); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application: defaultApplication, - resources: [DEFAULT_RESOURCE], - privileges: [ - getVersionPrivilege(defaultVersion), getLoginPrivilege(), ...privileges - ] - }] - } - }); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - index: [{ - names: [ defaultKibanaIndex ], - privileges: ['read', 'index'] - }] - } - }); - expect(result).toEqual({ - success: false, - missing: [ privilege1 ], - username, - }); - } - }); - - test(`returns success of true if the user has the read privilege on kibana index and the privilege is a read action`, async () => { - const privilegeMap = buildPrivilegeMap(savedObjectTypes, defaultApplication, defaultVersion); - for (const action of privilegeMap.read.actions) { - const privilege = action; + ['create', 'delete', 'read', 'view_index_metadata'].forEach(indexPrivilege => { + test(`returns legacy if they have ${indexPrivilege} privilege on the kibana index`, async () => { + const privilege = `action:saved_objects/${savedObjectTypes[0]}/get`; const username = 'foo-username'; const mockServer = createMockServer(); const callWithRequest = createMockCallWithRequest([ @@ -526,13 +264,14 @@ describe('legacy fallback with no application privileges', () => { }, username, }), - mockLegacyResponse({ - hasAllRequested: false, + mockKibanaIndexPrivilegesResponse({ privileges: { - read: true, - index: false, + create: false, + delete: false, + read: false, + view_index_metadata: false, + [indexPrivilege]: true }, - username, }) ]); @@ -558,75 +297,15 @@ describe('legacy fallback with no application privileges', () => { body: { index: [{ names: [ defaultKibanaIndex ], - privileges: ['read', 'index'] + privileges: ['create', 'delete', 'read', 'view_index_metadata'] }] } }); expect(result).toEqual({ - success: true, - missing: [], + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [getLoginPrivilege(), ...privileges], }); - } - }); - - test(`returns success of true if the user has the read privilege on kibana index and all privileges are read actions`, async () => { - const privilegeMap = buildPrivilegeMap(savedObjectTypes, defaultApplication, defaultVersion); - const privileges = privilegeMap.read.actions; - const username = 'foo-username'; - const mockServer = createMockServer(); - const callWithRequest = createMockCallWithRequest([ - mockApplicationPrivilegeResponse({ - hasAllRequested: false, - privileges: { - [getVersionPrivilege(defaultVersion)]: false, - [getLoginPrivilege()]: false, - ...privileges.reduce((acc, name) => { - acc[name] = false; - return acc; - }, {}) - }, - username, - }), - mockLegacyResponse({ - hasAllRequested: false, - privileges: { - read: true, - index: false, - }, - username, - }) - ]); - - const hasPrivilegesWithRequest = hasPrivilegesWithServer(mockServer); - const request = Symbol(); - const hasPrivileges = hasPrivilegesWithRequest(request); - const result = await hasPrivileges(privileges); - - expectDeprecationLogged(mockServer); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - applications: [{ - application: defaultApplication, - resources: [DEFAULT_RESOURCE], - privileges: [ - getVersionPrivilege(defaultVersion), getLoginPrivilege(), ...privileges - ] - }] - } - }); - expect(callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', { - body: { - index: [{ - names: [ defaultKibanaIndex ], - privileges: ['read', 'index'] - }] - } - }); - expect(result).toEqual({ - success: true, - missing: [], - username, }); }); }); diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js index 83e6123f06df5..fce39c03d6e06 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.js @@ -5,6 +5,7 @@ */ import { get, uniq } from 'lodash'; +import { HAS_PRIVILEGES_RESULT } from '../authorization/has_privileges'; const getPrivilege = (type, action) => { return `action:saved_objects/${type}/${action}`; @@ -14,124 +15,147 @@ export class SecureSavedObjectsClient { constructor(options) { const { errors, - repository, + internalRepository, + callWithRequestRepository, hasPrivileges, auditLogger, savedObjectTypes, } = options; this.errors = errors; - this._repository = repository; + this._internalRepository = internalRepository; + this._callWithRequestRepository = callWithRequestRepository; this._hasPrivileges = hasPrivileges; this._auditLogger = auditLogger; this._savedObjectTypes = savedObjectTypes; } async create(type, attributes = {}, options = {}) { - await this._performAuthorizationCheck(type, 'create', { + return await this._execute( type, - attributes, - options, - }); - - return await this._repository.create(type, attributes, options); + 'create', + { type, attributes, options }, + repository => repository.create(type, attributes, options), + ); } async bulkCreate(objects, options = {}) { const types = uniq(objects.map(o => o.type)); - await this._performAuthorizationCheck(types, 'bulk_create', { - objects, - options, - }); - - return await this._repository.bulkCreate(objects, options); + return await this._execute( + types, + 'bulk_create', + { objects, options }, + repository => repository.bulkCreate(objects, options), + ); } async delete(type, id) { - await this._performAuthorizationCheck(type, 'delete', { + return await this._execute( type, - id, - }); - - return await this._repository.delete(type, id); + 'delete', + { type, id }, + repository => repository.delete(type, id), + ); } async find(options = {}) { - const action = 'find'; - - // when we have the type or types, it makes our life easy if (options.type) { - await this._performAuthorizationCheck(options.type, action, { options }); - return await this._repository.find(options); + return await this._findWithTypes(options); } - // otherwise, we have to filter for only their authorized types - const types = this._savedObjectTypes; - const typesToPrivilegesMap = new Map(types.map(type => [type, getPrivilege(type, action)])); - const hasPrivilegesResult = await this._hasSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values())); - const authorizedTypes = Array.from(typesToPrivilegesMap.entries()) - .filter(([ , privilege]) => !hasPrivilegesResult.missing.includes(privilege)) - .map(([type]) => type); - - if (authorizedTypes.length === 0) { - this._auditLogger.savedObjectsAuthorizationFailure( - hasPrivilegesResult.username, - action, - types, - hasPrivilegesResult.missing, - { options } - ); - throw this.errors.decorateForbiddenError(new Error(`Not authorized to find saved_object`)); - } - this._auditLogger.savedObjectsAuthorizationSuccess(hasPrivilegesResult.username, action, authorizedTypes, { options }); + return await this._findAcrossAllTypes(options); + } - return await this._repository.find({ - ...options, - type: authorizedTypes - }); + async _findWithTypes(options) { + return await this._execute( + options.type, + 'find', + { options }, + repository => repository.find(options) + ); } async bulkGet(objects = []) { const types = uniq(objects.map(o => o.type)); - await this._performAuthorizationCheck(types, 'bulk_get', { - objects, - }); - - return await this._repository.bulkGet(objects); + return await this._execute( + types, + 'bulk_get', + { objects }, + repository => repository.bulkGet(objects) + ); } async get(type, id) { - await this._performAuthorizationCheck(type, 'get', { + return await this._execute( type, - id, - }); - - return await this._repository.get(type, id); + 'get', + { type, id }, + repository => repository.get(type, id) + ); } async update(type, id, attributes, options = {}) { - await this._performAuthorizationCheck(type, 'update', { + return await this._execute( type, - id, - attributes, - options, - }); - - return await this._repository.update(type, id, attributes, options); + 'update', + { type, id, attributes, options }, + repository => repository.update(type, id, attributes, options) + ); } - async _performAuthorizationCheck(typeOrTypes, action, args) { + async _execute(typeOrTypes, action, args, fn) { const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const privileges = types.map(type => getPrivilege(type, action)); - const result = await this._hasSavedObjectPrivileges(privileges); - - 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.sort().join(',')}, missing ${result.missing.sort().join(',')}`; - throw this.errors.decorateForbiddenError(new Error(msg)); + const { result, username, missing } = await this._hasSavedObjectPrivileges(privileges); + + switch (result) { + case HAS_PRIVILEGES_RESULT.AUTHORIZED: + this._auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args); + return await fn(this._internalRepository); + case HAS_PRIVILEGES_RESULT.LEGACY: + return await fn(this._callWithRequestRepository); + case HAS_PRIVILEGES_RESULT.UNAUTHORIZED: + this._auditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args); + const msg = `Unable to ${action} ${types.sort().join(',')}, missing ${missing.sort().join(',')}`; + throw this.errors.decorateForbiddenError(new Error(msg)); + default: + throw new Error('Unexpected result from hasPrivileges'); + } + } + + async _findAcrossAllTypes(options) { + const action = 'find'; + + // we have to filter for only their authorized types + const types = this._savedObjectTypes; + const typesToPrivilegesMap = new Map(types.map(type => [type, getPrivilege(type, action)])); + const { result, username, missing } = await this._hasSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values())); + + if (result === HAS_PRIVILEGES_RESULT.LEGACY) { + return await this._callWithRequestRepository.find(options); + } + + const authorizedTypes = Array.from(typesToPrivilegesMap.entries()) + .filter(([ , privilege]) => !missing.includes(privilege)) + .map(([type]) => type); + + if (authorizedTypes.length === 0) { + this._auditLogger.savedObjectsAuthorizationFailure( + username, + action, + types, + missing, + { options } + ); + throw this.errors.decorateForbiddenError(new Error(`Not authorized to find saved_object`)); } + + this._auditLogger.savedObjectsAuthorizationSuccess(username, action, authorizedTypes, { options }); + + return await this._internalRepository.find({ + ...options, + type: authorizedTypes + }); } async _hasSavedObjectPrivileges(privileges) { diff --git a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js index 0f9fa6610f683..4d77c48c56b95 100644 --- a/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js +++ b/x-pack/plugins/security/server/lib/saved_objects_client/secure_saved_objects_client.test.js @@ -5,6 +5,7 @@ */ import { SecureSavedObjectsClient } from './secure_saved_objects_client'; +import { HAS_PRIVILEGES_RESULT } from '../authorization/has_privileges'; const createMockErrors = () => { const forbiddenError = new Error('Mock ForbiddenError'); @@ -36,16 +37,37 @@ describe('#errors', () => { }); describe('#create', () => { - test(`throws decorated ForbiddenError when user doesn't have privileges`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/create`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type = 'foo'; const username = Symbol(); const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type}/create` ], - username })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -74,28 +96,39 @@ describe('#create', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + test(`returns result of internalRepository.create when authorized`, async () => { const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + create: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const attributes = Symbol(); + const options = Symbol(); - await expect(client.create(type)).rejects.toThrowError(mockErrors.generalError); + const result = await client.create(type, attributes, options); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/create`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { + type, + attributes, + options, + }); }); - test(`calls and returns result of repository.create`, async () => { + test(`returns result of callWithRequestRepository.create when legacy`, async () => { const type = 'foo'; const username = Symbol(); const returnValue = Symbol(); @@ -103,12 +136,15 @@ describe('#create', () => { create: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + `action:saved_objects/${type}/create` + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -119,27 +155,44 @@ describe('#create', () => { expect(result).toBe(returnValue); expect(mockRepository.create).toHaveBeenCalledWith(type, attributes, options); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'create', [type], { - type, - attributes, - options, - }); }); }); describe('#bulkCreate', () => { - test(`throws decorated ForbiddenError when user doesn't have privileges`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/bulk_create`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type1 = 'foo'; const type2 = 'bar'; const username = Symbol(); const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type1}/bulk_create` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -173,28 +226,42 @@ describe('#bulkCreate', () => { ); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + test(`returns result of internalRepository.bulkCreate when authorized`, async () => { + const username = Symbol(); + const type1 = 'foo'; + const type2 = 'bar'; + const returnValue = Symbol(); + const mockRepository = { + bulkCreate: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const objects = [ + { type: type1, otherThing: 'sup' }, + { type: type2, otherThing: 'everyone' }, + ]; + const options = Symbol(); - await expect(client.bulkCreate([{ type }])).rejects.toThrowError(mockErrors.generalError); + const result = await client.bulkCreate(objects, options); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/bulk_create`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { + objects, + options, + }); }); - test(`calls and returns result of repository.bulkCreate`, async () => { + test(`returns result of callWithRequestRepository.bulkCreate when legacy`, async () => { const username = Symbol(); const type1 = 'foo'; const type2 = 'bar'; @@ -203,12 +270,16 @@ describe('#bulkCreate', () => { bulkCreate: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + `action:saved_objects/${type1}/bulk_create`, + `action:saved_objects/${type2}/bulk_create`, + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -223,24 +294,42 @@ describe('#bulkCreate', () => { expect(result).toBe(returnValue); expect(mockRepository.bulkCreate).toHaveBeenCalledWith(objects, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_create', [type1, type2], { - objects, - options, - }); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); }); describe('#delete', () => { - test(`throws decorated ForbiddenError when user doesn't have privileges`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/delete`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type = 'foo'; const username = Symbol(); const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type}/delete` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -267,28 +356,37 @@ describe('#delete', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + test(`returns result of internalRepository.delete when authorized`, async () => { const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + delete: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const id = Symbol(); - await expect(client.delete(type)).rejects.toThrowError(mockErrors.generalError); + const result = await client.delete(type, id); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/delete`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.delete).toHaveBeenCalledWith(type, id); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { + type, + id, + }); }); - test(`calls and returns result of repository.delete`, async () => { + test(`returns result of internalRepository.delete when legacy`, async () => { const type = 'foo'; const username = Symbol(); const returnValue = Symbol(); @@ -296,12 +394,15 @@ describe('#delete', () => { delete: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + `action:saved_objects/${type}/delete` + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -312,31 +413,49 @@ describe('#delete', () => { expect(result).toBe(returnValue); expect(mockRepository.delete).toHaveBeenCalledWith(type, id); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete', [type], { - type, - id, - }); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); }); describe('#find', () => { describe('type', () => { - test(`throws decorated ForbiddenError when type is sinuglar and user isn't authorized`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/find`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { const type = 'foo'; const username = Symbol(); const mockRepository = {}; const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type}/find` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ errors: mockErrors, - repository: mockRepository, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -358,23 +477,21 @@ describe('#find', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated ForbiddenError when type is an array and user isn't authorized for one type`, async () => { + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { const type1 = 'foo'; const type2 = 'bar'; const username = Symbol(); - const mockRepository = {}; const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type1}/find` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ errors: mockErrors, - repository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -396,19 +513,19 @@ describe('#find', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated ForbiddenError when type is an array and user isn't authorized for either type`, async () => { + test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { const type1 = 'foo'; const type2 = 'bar'; const username = Symbol(); const mockRepository = {}; const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type1}/find`, `action:saved_objects/${type2}/find` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -435,28 +552,36 @@ describe('#find', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + test(`returns result of internalRepository.find when authorized`, async () => { const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + find: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const options = { type }; - await expect(client.find({ type })).rejects.toThrowError(mockErrors.generalError); + const result = await client.find(options); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/find`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.find).toHaveBeenCalledWith({ type }); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { + options, + }); }); - test(`calls and returns result of repository.find`, async () => { + test(`returns result of callWithRequestRepository.find when legacy`, async () => { const type = 'foo'; const username = Symbol(); const returnValue = Symbol(); @@ -464,12 +589,15 @@ describe('#find', () => { find: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + `action:saved_objects/${type}/find` + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -480,24 +608,47 @@ describe('#find', () => { expect(result).toBe(returnValue); expect(mockRepository.find).toHaveBeenCalledWith({ type }); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], { - options, - }); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); }); describe('no type', () => { - test(`throws decorated ForbiddenError when user has no authorized types`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const mockRepository = {}; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + repository: mockRepository, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + savedObjectTypes: [type1, type2] + }); + + await expect(client.find()).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type1}/find`, `action:saved_objects/${type2}/find`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type = 'foo'; const username = Symbol(); const mockRepository = {}; const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type}/find` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -525,27 +676,36 @@ describe('#find', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type1 = 'foo'; - const type2 = 'bar'; - const mockRepository = {}; + test(`returns result of callWithRequestRepository.find when legacy`, async () => { + const type = 'foo'; + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + find: jest.fn().mockReturnValue(returnValue) + }; const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.LEGACY, + username, + missing: [ + `action:saved_objects/${type}/find` + ], + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ errors: mockErrors, - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, - savedObjectTypes: [type1, type2] + savedObjectTypes: [type] }); + const options = Symbol(); - await expect(client.find()).rejects.toThrowError(mockErrors.generalError); + const result = await client.find(options); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type1}/find`, `action:saved_objects/${type2}/find`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/find`]); + expect(mockRepository.find).toHaveBeenCalledWith(options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); @@ -558,7 +718,7 @@ describe('#find', () => { }; const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, missing: [ `action:saved_objects/${type1}/find` ] @@ -566,7 +726,7 @@ describe('#find', () => { const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ errors: mockErrors, - repository: mockRepository, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, savedObjectTypes: [type1, type2] @@ -580,7 +740,7 @@ describe('#find', () => { })); }); - test(`calls and returns result of repository.find`, async () => { + test(`returns result of repository.find`, async () => { const type = 'foo'; const username = Symbol(); const returnValue = Symbol(); @@ -588,13 +748,13 @@ describe('#find', () => { find: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, - missing: [], + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, username, + missing: [], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, savedObjectTypes: [type] @@ -614,17 +774,38 @@ describe('#find', () => { }); describe('#bulkGet', () => { - test(`throws decorated ForbiddenError when user doesn't have privileges`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith(['action:saved_objects/foo/bulk_get']); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type1 = 'foo'; const type2 = 'bar'; const username = Symbol(); const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type1}/bulk_get` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -654,28 +835,40 @@ describe('#bulkGet', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { - const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + test(`returns result of internalRepository.bulkGet when authorized`, async () => { + const type1 = 'foo'; + const type2 = 'bar'; + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + bulkGet: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const objects = [ + { type: type1, id: 'foo-id' }, + { type: type2, id: 'bar-id' }, + ]; - await expect(client.bulkGet([{ type }])).rejects.toThrowError(mockErrors.generalError); + const result = await client.bulkGet(objects); - expect(mockHasPrivileges).toHaveBeenCalledWith(['action:saved_objects/foo/bulk_get']); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { + objects, + }); }); - test(`calls and returns result of repository.bulkGet`, async () => { + test(`returns result of callWithRequestRepository.bulkGet when legacy`, async () => { const type1 = 'foo'; const type2 = 'bar'; const username = Symbol(); @@ -684,12 +877,16 @@ describe('#bulkGet', () => { bulkGet: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + `action:saved_objects/${type1}/bulk_get`, + `action:saved_objects/${type2}/bulk_get` + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -703,23 +900,42 @@ describe('#bulkGet', () => { expect(result).toBe(returnValue); expect(mockRepository.bulkGet).toHaveBeenCalledWith(objects); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'bulk_get', [type1, type2], { - objects, - }); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); }); describe('#get', () => { - test(`throws decorated ForbiddenError when user doesn't have privileges`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/get`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type = 'foo'; const username = Symbol(); const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ `action:saved_objects/${type}/get` ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -746,28 +962,37 @@ describe('#get', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + test(`returns result of internalRepository.get when authorized`, async () => { const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + get: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const id = Symbol(); - await expect(client.get(type)).rejects.toThrowError(mockErrors.generalError); + const result = await client.get(type, id); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/get`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.get).toHaveBeenCalledWith(type, id); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { + type, + id, + }); }); - test(`calls and returns result of repository.get`, async () => { + test(`returns result of callWithRequestRepository.get when user isn't authorized and has legacy fallback`, async () => { const type = 'foo'; const username = Symbol(); const returnValue = Symbol(); @@ -775,12 +1000,15 @@ describe('#get', () => { get: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + `action:saved_objects/${type}/get` + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -791,24 +1019,42 @@ describe('#get', () => { expect(result).toBe(returnValue); expect(mockRepository.get).toHaveBeenCalledWith(type, id); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [type], { - type, - id, - }); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); }); describe('#update', () => { - test(`throws decorated ForbiddenError when user doesn't have privileges`, async () => { + test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + const type = 'foo'; + const mockErrors = createMockErrors(); + const mockHasPrivileges = jest.fn().mockImplementation(async () => { + throw new Error(); + }); + const mockAuditLogger = createMockAuditLogger(); + const client = new SecureSavedObjectsClient({ + errors: mockErrors, + hasPrivileges: mockHasPrivileges, + auditLogger: mockAuditLogger, + }); + + await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); + + expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/update`]); + expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + }); + + test(`throws decorated ForbiddenError when unauthorized`, async () => { const type = 'foo'; const username = Symbol(); const mockErrors = createMockErrors(); const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: false, + result: HAS_PRIVILEGES_RESULT.UNAUTHORIZED, + username, missing: [ 'action:saved_objects/foo/update' ], - username, })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ @@ -839,28 +1085,41 @@ describe('#update', () => { expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); - test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => { + test(`returns result of repository.update when authorized`, async () => { const type = 'foo'; - const mockErrors = createMockErrors(); - const mockHasPrivileges = jest.fn().mockImplementation(async () => { - throw new Error(); - }); + const username = Symbol(); + const returnValue = Symbol(); + const mockRepository = { + update: jest.fn().mockReturnValue(returnValue) + }; + const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ + result: HAS_PRIVILEGES_RESULT.AUTHORIZED, + username, + })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - errors: mockErrors, + internalRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); + const id = Symbol(); + const attributes = Symbol(); + const options = Symbol(); - await expect(client.update(type)).rejects.toThrowError(mockErrors.generalError); + const result = await client.update(type, id, attributes, options); - expect(mockHasPrivileges).toHaveBeenCalledWith([`action:saved_objects/${type}/update`]); - expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1); + expect(result).toBe(returnValue); + expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { + type, + id, + attributes, + options, + }); }); - test(`calls and returns result of repository.update`, async () => { + test(`returns result of repository.update when legacy`, async () => { const type = 'foo'; const username = Symbol(); const returnValue = Symbol(); @@ -868,12 +1127,15 @@ describe('#update', () => { update: jest.fn().mockReturnValue(returnValue) }; const mockHasPrivileges = jest.fn().mockImplementation(async () => ({ - success: true, + result: HAS_PRIVILEGES_RESULT.LEGACY, username, + missing: [ + 'action:saved_objects/foo/update' + ], })); const mockAuditLogger = createMockAuditLogger(); const client = new SecureSavedObjectsClient({ - repository: mockRepository, + callWithRequestRepository: mockRepository, hasPrivileges: mockHasPrivileges, auditLogger: mockAuditLogger, }); @@ -886,11 +1148,6 @@ describe('#update', () => { expect(result).toBe(returnValue); expect(mockRepository.update).toHaveBeenCalledWith(type, id, attributes, options); expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled(); - expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'update', [type], { - type, - id, - attributes, - options, - }); + expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js b/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js index 12a25607a92ac..6089aec5d89d0 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/bulk_get.js @@ -68,7 +68,7 @@ export default function ({ getService }) { }); }; - const expectForbidden = resp => { + const expectRbacForbidden = resp => { //eslint-disable-next-line max-len const missingActions = `action:login,action:saved_objects/config/bulk_get,action:saved_objects/dashboard/bulk_get,action:saved_objects/visualization/bulk_get`; expect(resp.body).to.eql({ @@ -102,7 +102,7 @@ export default function ({ getService }) { tests: { default: { statusCode: 403, - response: expectForbidden, + response: expectRbacForbidden, } } }); diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/create.js b/x-pack/test/rbac_api_integration/apis/saved_objects/create.js index c8ca5be09b6ad..cff5da3502838 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/create.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/create.js @@ -29,7 +29,7 @@ export default function ({ getService }) { }); }; - const createExpectForbidden = canLogin => resp => { + const createExpectRbacForbidden = canLogin => resp => { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -37,6 +37,15 @@ export default function ({ getService }) { }); }; + const createExpectLegacyForbidden = username => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + //eslint-disable-next-line max-len + message: `action [indices:data/write/index] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/index] is unauthorized for user [${username}]` + }); + }; + const createTest = (description, { auth, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/basic')); @@ -64,7 +73,7 @@ export default function ({ getService }) { tests: { default: { statusCode: 403, - response: createExpectForbidden(false), + response: createExpectRbacForbidden(false), }, } }); @@ -103,7 +112,7 @@ export default function ({ getService }) { tests: { default: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), }, } }); @@ -129,7 +138,7 @@ export default function ({ getService }) { tests: { default: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectRbacForbidden(true), }, } }); diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js b/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js index d6bd0d61dd646..b75ab5342c6ba 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/delete.js @@ -25,7 +25,7 @@ export default function ({ getService }) { }); }; - const createExpectForbidden = canLogin => resp => { + const createExpectRbacForbidden = canLogin => resp => { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -33,6 +33,15 @@ export default function ({ getService }) { }); }; + const createExpectLegacyForbidden = username => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + //eslint-disable-next-line max-len + message: `action [indices:data/write/delete] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/delete] is unauthorized for user [${username}]` + }); + }; + const deleteTest = (description, { auth, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/basic')); @@ -64,11 +73,11 @@ export default function ({ getService }) { tests: { actualId: { statusCode: 403, - response: createExpectForbidden(false), + response: createExpectRbacForbidden(false), }, invalidId: { statusCode: 403, - response: createExpectForbidden(false), + response: createExpectRbacForbidden(false), } } }); @@ -115,11 +124,11 @@ export default function ({ getService }) { tests: { actualId: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), }, invalidId: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), } } }); @@ -149,11 +158,11 @@ export default function ({ getService }) { tests: { actualId: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectRbacForbidden(true), }, invalidId: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectRbacForbidden(true), } } }); diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js index 59cc3dc12d538..0498021e5daae 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/find.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/find.js @@ -31,7 +31,7 @@ export default function ({ getService }) { }); }; - const expectAllResults = (resp) => { + const expectResultsWithValidTypes = (resp) => { expect(resp.body).to.eql({ page: 1, per_page: 20, @@ -69,6 +69,50 @@ export default function ({ getService }) { }); }; + const expectAllResultsIncludingInvalidTypes = (resp) => { + expect(resp.body).to.eql({ + page: 1, + per_page: 20, + total: 5, + saved_objects: [ + { + id: '91200a00-9efd-11e7-acb3-3dab96693fab', + type: 'index-pattern', + updated_at: '2017-09-21T18:49:16.270Z', + version: 1, + attributes: resp.body.saved_objects[0].attributes + }, + { + id: '7.0.0-alpha1', + type: 'config', + updated_at: '2017-09-21T18:49:16.302Z', + version: 1, + attributes: resp.body.saved_objects[1].attributes + }, + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + type: 'visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: 1, + attributes: resp.body.saved_objects[2].attributes + }, + { + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: 1, + attributes: resp.body.saved_objects[3].attributes + }, + { + id: 'visualization:dd7caf20-9efd-11e7-acb3-3dab96693faa', + type: 'not-a-visualization', + updated_at: '2017-09-21T18:51:23.794Z', + version: 1 + }, + ] + }); + }; + const createExpectEmpty = (page, perPage, total) => (resp) => { expect(resp.body).to.eql({ page: page, @@ -78,7 +122,7 @@ export default function ({ getService }) { }); }; - const createExpectActionForbidden = (canLogin, type) => resp => { + const createExpectRbacForbidden = (canLogin, type) => resp => { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -158,22 +202,22 @@ export default function ({ getService }) { normal: { description: 'forbidden login and find visualization message', statusCode: 403, - response: createExpectActionForbidden(false, 'visualization'), + response: createExpectRbacForbidden(false, 'visualization'), }, unknownType: { description: 'forbidden login and find wigwags message', statusCode: 403, - response: createExpectActionForbidden(false, 'wigwags'), + response: createExpectRbacForbidden(false, 'wigwags'), }, pageBeyondTotal: { description: 'forbidden login and find visualization message', statusCode: 403, - response: createExpectActionForbidden(false, 'visualization'), + response: createExpectRbacForbidden(false, 'visualization'), }, unknownSearchField: { description: 'forbidden login and find wigwags message', statusCode: 403, - response: createExpectActionForbidden(false, 'wigwags'), + response: createExpectRbacForbidden(false, 'wigwags'), }, noType: { description: `forbidded can't find any types`, @@ -212,7 +256,7 @@ export default function ({ getService }) { noType: { description: 'all objects', statusCode: 200, - response: expectAllResults, + response: expectResultsWithValidTypes, }, }, }); @@ -246,7 +290,7 @@ export default function ({ getService }) { noType: { description: 'all objects', statusCode: 200, - response: expectAllResults, + response: expectAllResultsIncludingInvalidTypes, }, }, }); @@ -263,9 +307,9 @@ export default function ({ getService }) { response: expectVisualizationResults, }, unknownType: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectActionForbidden(true, 'wigwags'), + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), }, pageBeyondTotal: { description: 'empty result', @@ -273,14 +317,14 @@ export default function ({ getService }) { response: createExpectEmpty(100, 100, 1), }, unknownSearchField: { - description: 'forbidden find wigwags message', - statusCode: 403, - response: createExpectActionForbidden(true, 'wigwags'), + description: 'empty result', + statusCode: 200, + response: createExpectEmpty(1, 20, 0), }, noType: { description: 'all objects', statusCode: 200, - response: expectAllResults, + response: expectAllResultsIncludingInvalidTypes, }, } }); @@ -314,7 +358,7 @@ export default function ({ getService }) { noType: { description: 'all objects', statusCode: 200, - response: expectAllResults, + response: expectResultsWithValidTypes, }, }, }); @@ -333,7 +377,7 @@ export default function ({ getService }) { unknownType: { description: 'forbidden find wigwags message', statusCode: 403, - response: createExpectActionForbidden(true, 'wigwags'), + response: createExpectRbacForbidden(true, 'wigwags'), }, pageBeyondTotal: { description: 'empty result', @@ -343,12 +387,12 @@ export default function ({ getService }) { unknownSearchField: { description: 'forbidden find wigwags message', statusCode: 403, - response: createExpectActionForbidden(true, 'wigwags'), + response: createExpectRbacForbidden(true, 'wigwags'), }, noType: { description: 'all objects', statusCode: 200, - response: expectAllResults, + response: expectResultsWithValidTypes, }, } }); diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/get.js b/x-pack/test/rbac_api_integration/apis/saved_objects/get.js index e055d9ef75e48..ac7cc6b70f50a 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/get.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/get.js @@ -39,7 +39,7 @@ export default function ({ getService }) { }); }; - const expectForbidden = resp => { + const expectRbacForbidden = resp => { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -80,11 +80,11 @@ export default function ({ getService }) { tests: { exists: { statusCode: 403, - response: expectForbidden, + response: expectRbacForbidden, }, doesntExist: { statusCode: 403, - response: expectForbidden, + response: expectRbacForbidden, }, } }); diff --git a/x-pack/test/rbac_api_integration/apis/saved_objects/update.js b/x-pack/test/rbac_api_integration/apis/saved_objects/update.js index e3740ede1aaaf..edcec1ffb6124 100644 --- a/x-pack/test/rbac_api_integration/apis/saved_objects/update.js +++ b/x-pack/test/rbac_api_integration/apis/saved_objects/update.js @@ -38,7 +38,7 @@ export default function ({ getService }) { }); }; - const createExpectForbidden = canLogin => resp => { + const createExpectRbacForbidden = canLogin => resp => { expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', @@ -46,6 +46,15 @@ export default function ({ getService }) { }); }; + const createExpectLegacyForbidden = username => resp => { + expect(resp.body).to.eql({ + statusCode: 403, + error: 'Forbidden', + //eslint-disable-next-line max-len + message: `action [indices:data/write/update] is unauthorized for user [${username}]: [security_exception] action [indices:data/write/update] is unauthorized for user [${username}]` + }); + }; + const updateTest = (description, { auth, tests }) => { describe(description, () => { before(() => esArchiver.load('saved_objects/basic')); @@ -88,11 +97,11 @@ export default function ({ getService }) { tests: { exists: { statusCode: 403, - response: createExpectForbidden(false), + response: createExpectRbacForbidden(false), }, doesntExist: { statusCode: 403, - response: createExpectForbidden(false), + response: createExpectRbacForbidden(false), }, } }); @@ -139,11 +148,11 @@ export default function ({ getService }) { tests: { exists: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), }, doesntExist: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME), }, } }); @@ -173,11 +182,11 @@ export default function ({ getService }) { tests: { exists: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectRbacForbidden(true), }, doesntExist: { statusCode: 403, - response: createExpectForbidden(true), + response: createExpectRbacForbidden(true), }, } });