"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.SecurityAction = exports.SavedObjectsSecurityExtension = exports.AuditAction = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _internal_bulk_resolve = require("@kbn/core-saved-objects-api-server-internal/src/lib/apis/internals/internal_bulk_resolve"); var _coreSavedObjectsBaseServerInternal = require("@kbn/core-saved-objects-base-server-internal"); var _coreSavedObjectsServer = require("@kbn/core-saved-objects-server"); var _coreSavedObjectsUtilsServer = require("@kbn/core-saved-objects-utils-server"); var _authorization_utils = require("./authorization_utils"); var _constants = require("../../common/constants"); var _audit = require("../audit"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ /** * The SecurityAction enumeration contains values for all valid shared object * security actions. The string for each value correlates to the ES operation. */ let SecurityAction; /** * The AuditAction enumeration contains values for all * valid audit actions for use in AddAuditEventParams. */ exports.SecurityAction = SecurityAction; (function (SecurityAction) { SecurityAction[SecurityAction["CHECK_CONFLICTS"] = 0] = "CHECK_CONFLICTS"; SecurityAction[SecurityAction["CLOSE_POINT_IN_TIME"] = 1] = "CLOSE_POINT_IN_TIME"; SecurityAction[SecurityAction["COLLECT_MULTINAMESPACE_REFERENCES"] = 2] = "COLLECT_MULTINAMESPACE_REFERENCES"; SecurityAction[SecurityAction["COLLECT_MULTINAMESPACE_REFERENCES_UPDATE_SPACES"] = 3] = "COLLECT_MULTINAMESPACE_REFERENCES_UPDATE_SPACES"; SecurityAction[SecurityAction["CREATE"] = 4] = "CREATE"; SecurityAction[SecurityAction["BULK_CREATE"] = 5] = "BULK_CREATE"; SecurityAction[SecurityAction["DELETE"] = 6] = "DELETE"; SecurityAction[SecurityAction["BULK_DELETE"] = 7] = "BULK_DELETE"; SecurityAction[SecurityAction["FIND"] = 8] = "FIND"; SecurityAction[SecurityAction["GET"] = 9] = "GET"; SecurityAction[SecurityAction["BULK_GET"] = 10] = "BULK_GET"; SecurityAction[SecurityAction["INTERNAL_BULK_RESOLVE"] = 11] = "INTERNAL_BULK_RESOLVE"; SecurityAction[SecurityAction["OPEN_POINT_IN_TIME"] = 12] = "OPEN_POINT_IN_TIME"; SecurityAction[SecurityAction["REMOVE_REFERENCES"] = 13] = "REMOVE_REFERENCES"; SecurityAction[SecurityAction["UPDATE"] = 14] = "UPDATE"; SecurityAction[SecurityAction["BULK_UPDATE"] = 15] = "BULK_UPDATE"; SecurityAction[SecurityAction["UPDATE_OBJECTS_SPACES"] = 16] = "UPDATE_OBJECTS_SPACES"; })(SecurityAction || (exports.SecurityAction = SecurityAction = {})); let AuditAction; // this is separate from 'saved_object_update' because the user is only updating an object's metadata /** * The AddAuditEventParams interface contains settings for adding * audit events via the ISavedObjectsSecurityExtension. This is * used only for the private addAuditEvent method. */ exports.AuditAction = AuditAction; (function (AuditAction) { AuditAction["CREATE"] = "saved_object_create"; AuditAction["GET"] = "saved_object_get"; AuditAction["RESOLVE"] = "saved_object_resolve"; AuditAction["UPDATE"] = "saved_object_update"; AuditAction["DELETE"] = "saved_object_delete"; AuditAction["FIND"] = "saved_object_find"; AuditAction["REMOVE_REFERENCES"] = "saved_object_remove_references"; AuditAction["OPEN_POINT_IN_TIME"] = "saved_object_open_point_in_time"; AuditAction["CLOSE_POINT_IN_TIME"] = "saved_object_close_point_in_time"; AuditAction["COLLECT_MULTINAMESPACE_REFERENCES"] = "saved_object_collect_multinamespace_references"; AuditAction["UPDATE_OBJECTS_SPACES"] = "saved_object_update_objects_spaces"; })(AuditAction || (exports.AuditAction = AuditAction = {})); class SavedObjectsSecurityExtension { constructor({ actions, auditLogger, errors, checkPrivileges }) { (0, _defineProperty2.default)(this, "actions", void 0); (0, _defineProperty2.default)(this, "auditLogger", void 0); (0, _defineProperty2.default)(this, "errors", void 0); (0, _defineProperty2.default)(this, "checkPrivilegesFunc", void 0); (0, _defineProperty2.default)(this, "actionMap", void 0); this.actions = actions; this.auditLogger = auditLogger; this.errors = errors; this.checkPrivilegesFunc = checkPrivileges; // This comment block is a quick reference for the action map, which maps authorization actions // and audit actions to a "security action" as used by the authorization methods. // Security Action ES AUTH ACTION AUDIT ACTION // ----------------------------------------------------------------------------------------- // Check Conflicts 'bulk_create' N/A // Close PIT N/A AuditAction.CLOSE_POINT_IN_TIME // Collect References 'bulk_get' AuditAction.COLLECT_MULTINAMESPACE_REFERENCES // Collect Refs For Updating Spaces 'share_to_space' AuditAction.COLLECT_MULTINAMESPACE_REFERENCES // Create 'create' AuditAction.CREATE // Bulk Create 'bulk_create' AuditAction.CREATE // Delete 'delete' AuditAction.DELETE // Bulk Delete 'bulk_delete' AuditAction.DELETE // Find 'find' AuditAction.FIND // Get 'get' AuditAction.GET // Bulk Get 'bulk_get' AuditAction.GET // Internal Bulk Resolve 'bulk_get' AuditAction.RESOLVE // Open PIT 'open_point_in_time' AuditAction.OPEN_POINT_IN_TIME // Remove References 'delete' AuditAction.REMOVE_REFERENCES // Update 'update' AuditAction.UPDATE // Bulk Update 'bulk_update' AuditAction.UPDATE // Update Objects Spaces 'share_to_space' AuditAction.UPDATE_OBJECTS_SPACES this.actionMap = new Map([[SecurityAction.CHECK_CONFLICTS, { authzAction: 'bulk_create', auditAction: undefined }], [SecurityAction.CLOSE_POINT_IN_TIME, { authzAction: undefined, auditAction: AuditAction.CLOSE_POINT_IN_TIME }], [SecurityAction.COLLECT_MULTINAMESPACE_REFERENCES, { authzAction: 'bulk_get', auditAction: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES }], [SecurityAction.COLLECT_MULTINAMESPACE_REFERENCES_UPDATE_SPACES, { authzAction: 'share_to_space', auditAction: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES }], [SecurityAction.CREATE, { authzAction: 'create', auditAction: AuditAction.CREATE }], [SecurityAction.BULK_CREATE, { authzAction: 'bulk_create', auditAction: AuditAction.CREATE }], [SecurityAction.DELETE, { authzAction: 'delete', auditAction: AuditAction.DELETE }], [SecurityAction.BULK_DELETE, { authzAction: 'bulk_delete', auditAction: AuditAction.DELETE }], [SecurityAction.FIND, { authzAction: 'find', auditAction: AuditAction.FIND }], [SecurityAction.GET, { authzAction: 'get', auditAction: AuditAction.GET }], [SecurityAction.BULK_GET, { authzAction: 'bulk_get', auditAction: AuditAction.GET }], [SecurityAction.INTERNAL_BULK_RESOLVE, { authzAction: 'bulk_get', auditAction: AuditAction.RESOLVE }], [SecurityAction.OPEN_POINT_IN_TIME, { authzAction: 'open_point_in_time', auditAction: AuditAction.OPEN_POINT_IN_TIME }], [SecurityAction.REMOVE_REFERENCES, { authzAction: 'delete', auditAction: AuditAction.REMOVE_REFERENCES }], [SecurityAction.UPDATE, { authzAction: 'update', auditAction: AuditAction.UPDATE }], [SecurityAction.BULK_UPDATE, { authzAction: 'bulk_update', auditAction: AuditAction.UPDATE }], [SecurityAction.UPDATE_OBJECTS_SPACES, { authzAction: 'share_to_space', auditAction: AuditAction.UPDATE_OBJECTS_SPACES }]]); } assertObjectsArrayNotEmpty(objects, action) { if (objects.length === 0) { var _this$actionMap$get$a, _this$actionMap$get; throw new Error(`No objects specified for ${(_this$actionMap$get$a = (_this$actionMap$get = this.actionMap.get(action)) === null || _this$actionMap$get === void 0 ? void 0 : _this$actionMap$get.authzAction) !== null && _this$actionMap$get$a !== void 0 ? _this$actionMap$get$a : 'unknown'} authorization`); } } translateActions(securityActions) { const authzActions = new Set(); const auditActions = new Set(); for (const secAction of securityActions) { const { authzAction, auditAction } = this.decodeSecurityAction(secAction); if (authzAction) authzActions.add(authzAction); if (auditAction) auditActions.add(auditAction); } return { authzActions, auditActions }; } decodeSecurityAction(securityAction) { const { authzAction, auditAction } = this.actionMap.get(securityAction); return { authzAction, auditAction }; } async checkAuthorization(params) { const { types, spaces, actions, options = { allowGlobalResource: false } } = params; const { allowGlobalResource } = options; if (types.size === 0) { throw new Error('No types specified for authorization check'); } if (spaces.size === 0) { throw new Error('No spaces specified for authorization check'); } if (actions.size === 0) { throw new Error('No actions specified for authorization check'); } const typesArray = [...types]; const actionsArray = [...actions]; const privilegeActionsMap = new Map(typesArray.flatMap(type => actionsArray.map(action => [this.actions.savedObject.get(type, action), { type, action }]))); const privilegeActions = [...privilegeActionsMap.keys(), this.actions.login]; // Always check login action, we will need it later for redacting namespaces const { hasAllRequested, privileges } = await this.checkPrivileges(privilegeActions, getAuthorizableSpaces(spaces, allowGlobalResource)); const missingPrivileges = getMissingPrivileges(privileges); const typeMap = privileges.kibana.reduce((acc, { resource, privilege }) => { var _missingPrivileges$ge, _missingPrivileges$ge2; const missingPrivilegesAtResource = resource && ((_missingPrivileges$ge = missingPrivileges.get(resource)) === null || _missingPrivileges$ge === void 0 ? void 0 : _missingPrivileges$ge.has(privilege)) || !resource && ((_missingPrivileges$ge2 = missingPrivileges.get(undefined)) === null || _missingPrivileges$ge2 === void 0 ? void 0 : _missingPrivileges$ge2.has(privilege)); if (missingPrivilegesAtResource) { return acc; } let objTypes; let action; if (privilege === this.actions.login) { // Technically, 'login:' is not a saved object action, it is a Kibana privilege -- however, we include it in the `typeMap` results // for ease of use with the `redactNamespaces` function. The user is never actually authorized to "login" for a given object type, // they are authorized to log in on a per-space basis, and this is applied to each object type in the typeMap result accordingly. objTypes = typesArray; action = this.actions.login; } else { const entry = privilegeActionsMap.get(privilege); // always defined objTypes = [entry.type]; action = entry.action; } for (const type of objTypes) { var _acc$get, _actionAuthorizations; const actionAuthorizations = (_acc$get = acc.get(type)) !== null && _acc$get !== void 0 ? _acc$get : {}; const authorization = (_actionAuthorizations = actionAuthorizations[action]) !== null && _actionAuthorizations !== void 0 ? _actionAuthorizations : { authorizedSpaces: [] }; if (resource === undefined) { acc.set(type, { ...actionAuthorizations, [action]: { ...authorization, isGloballyAuthorized: true } }); } else { acc.set(type, { ...actionAuthorizations, [action]: { ...authorization, authorizedSpaces: authorization.authorizedSpaces.concat(resource) } }); } } return acc; }, new Map()); if (hasAllRequested) { return { typeMap, status: 'fully_authorized' }; } else if (typeMap.size > 0) { for (const entry of typeMap.values()) { const typeActions = Object.keys(entry); if (actionsArray.some(a => typeActions.includes(a))) { // Only return 'partially_authorized' if the user is actually authorized for one of the actions they requested // (e.g., not just the 'login:' action) return { typeMap, status: 'partially_authorized' }; } } } return { typeMap, status: 'unauthorized' }; } auditHelper(params) { const { action, useSuccessOutcome, objects, error, addToSpaces, deleteFromSpaces, unauthorizedSpaces, unauthorizedTypes } = params; // If there are no objects, we at least want to add a single audit log for the action const toAudit = !!objects && (objects === null || objects === void 0 ? void 0 : objects.length) > 0 ? objects : [undefined]; for (const obj of toAudit) { this.addAuditEvent({ action, ...(!!obj && { savedObject: { type: obj.type, id: obj.id } }), error, // By default, if authorization was a success the outcome is 'unknown' because the operation has not occurred yet // The GET action is one of the few exceptions to this, and hence it passes true to useSuccessOutcome ...(!error && { outcome: useSuccessOutcome ? 'success' : 'unknown' }), addToSpaces, deleteFromSpaces, unauthorizedSpaces, unauthorizedTypes }); } } /** * The enforce method uses the result of an authorization check authorization map) and a map * of types to spaces (type map) to determine if the action is authorized for all types and spaces * within the type map. If unauthorized for any type this method will throw. */ enforceAuthorization(params) { var _ref; const { typesAndSpaces, action, typeMap, auditOptions } = params; const { objects: auditObjects, bypass = 'never', // default for bypass useSuccessOutcome, addToSpaces, deleteFromSpaces } = (_ref = auditOptions) !== null && _ref !== void 0 ? _ref : {}; const { authzAction, auditAction } = this.decodeSecurityAction(action); const unauthorizedTypes = new Set(); if (authzAction) { for (const [type, spaces] of typesAndSpaces) { const spacesArray = [...spaces]; if (!(0, _authorization_utils.isAuthorizedInAllSpaces)(type, authzAction, spacesArray, typeMap)) { unauthorizedTypes.add(type); } } } if (unauthorizedTypes.size > 0) { const targetTypes = [...unauthorizedTypes].sort().join(','); const msg = `Unable to ${authzAction} ${targetTypes}`; const error = this.errors.decorateForbiddenError(new Error(msg)); if (auditAction && bypass !== 'always' && bypass !== 'on_failure') { this.auditHelper({ action: auditAction, objects: auditObjects, useSuccessOutcome, addToSpaces, deleteFromSpaces, error }); } throw error; } if (auditAction && bypass !== 'always' && bypass !== 'on_success') { this.auditHelper({ action: auditAction, objects: auditObjects, useSuccessOutcome, addToSpaces, deleteFromSpaces }); } } /** * The authorize method is the central method for authorization within the extension. It handles * checking and enforcing authorization, and passing audit parameters down to the enforce method. * * If an enforce map is not provided, this method will NOT enforce authorization nor audit the action. * If an enforce map is provided and the action is unauthorized for any type in any space mapped for * that type, this method will throw (because the enforce method will throw). * * This method not marked as private, but not exposed via the interface * This allows us to test it thoroughly in the unit test suite, but keep it from being exposed to consumers. * @param params actions, types, and spaces to check, the enforce map (types to enforce in which spaces), options, and audit options * @returns CheckAuthorizationResult - the result from the authorizations check */ async authorize(params) { var _params$options; if (params.actions.size === 0) { throw new Error('No actions specified for authorization'); } if (params.types.size === 0) { throw new Error('No types specified for authorization'); } if (params.spaces.size === 0) { throw new Error('No spaces specified for authorization'); } const { authzActions } = this.translateActions(params.actions); const checkResult = await this.checkAuthorization({ types: params.types, spaces: params.spaces, actions: authzActions, options: { allowGlobalResource: ((_params$options = params.options) === null || _params$options === void 0 ? void 0 : _params$options.allowGlobalResource) === true } }); const typesAndSpaces = params.enforceMap; if (typesAndSpaces !== undefined && checkResult) { params.actions.forEach(action => { this.enforceAuthorization({ typesAndSpaces, action, typeMap: checkResult.typeMap, auditOptions: params.auditOptions }); }); } return checkResult; } addAuditEvent(params) { if (this.auditLogger.enabled) { const auditEvent = (0, _audit.savedObjectEvent)(params); this.auditLogger.log(auditEvent); } } async checkPrivileges(actions, namespaceOrNamespaces) { try { return await this.checkPrivilegesFunc(actions, namespaceOrNamespaces); } catch (error) { throw this.errors.decorateGeneralError(error, error.body && error.body.reason); } } redactNamespaces(params) { var _actionRecord$loginAc, _savedObject$namespac, _savedObject$namespac2; const { savedObject, typeMap } = params; const loginAction = this.actions.login; // This typeMap came from the `checkAuthorization` function, which always checks privileges for the "login" action (in addition to what the consumer requested) const actionRecord = typeMap.get(savedObject.type); const entry = (_actionRecord$loginAc = actionRecord === null || actionRecord === void 0 ? void 0 : actionRecord[loginAction]) !== null && _actionRecord$loginAc !== void 0 ? _actionRecord$loginAc : { authorizedSpaces: [] }; // fail-secure if attribute is not defined const { authorizedSpaces, isGloballyAuthorized } = entry; if (isGloballyAuthorized || !((_savedObject$namespac = savedObject.namespaces) !== null && _savedObject$namespac !== void 0 && _savedObject$namespac.length)) { return savedObject; } const authorizedSpacesSet = new Set(authorizedSpaces); const redactedSpaces = (_savedObject$namespac2 = savedObject.namespaces) === null || _savedObject$namespac2 === void 0 ? void 0 : _savedObject$namespac2.map(x => x === _constants.ALL_SPACES_ID || authorizedSpacesSet.has(x) ? x : _constants.UNKNOWN_SPACE).sort(namespaceComparator); return { ...savedObject, namespaces: redactedSpaces }; } async authorizeCreate(params) { return this.internalAuthorizeCreate({ namespace: params.namespace, objects: [params.object] }); } async authorizeBulkCreate(params) { return this.internalAuthorizeCreate(params, { forceBulkAction: true }); } async internalAuthorizeCreate(params, options) { const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(params.namespace); const { objects } = params; const action = options !== null && options !== void 0 && options.forceBulkAction || objects.length > 1 ? SecurityAction.BULK_CREATE : SecurityAction.CREATE; this.assertObjectsArrayNotEmpty(objects, action); const enforceMap = new Map(); const spacesToAuthorize = new Set([namespaceString]); // Always check authZ for the active space // If a user tries to create an object with `initialNamespaces: ['*']`, they need to have 'create' privileges for the Global Resource // (e.g., All privileges for All Spaces). // Inversely, if a user tries to overwrite an object that already exists in '*', they don't need to 'create' privileges for the Global // Resource, so in that case we have to filter out that string from spacesToAuthorize (because `allowGlobalResource: true` is used // below.) for (const obj of objects) { var _enforceMap$get; const spacesToEnforce = (_enforceMap$get = enforceMap.get(obj.type)) !== null && _enforceMap$get !== void 0 ? _enforceMap$get : new Set([namespaceString]); // Always enforce authZ for the active space for (const space of (_obj$initialNamespace = obj.initialNamespaces) !== null && _obj$initialNamespace !== void 0 ? _obj$initialNamespace : []) { var _obj$initialNamespace; spacesToEnforce.add(space); spacesToAuthorize.add(space); } enforceMap.set(obj.type, spacesToEnforce); for (const space of obj.existingNamespaces) { // Don't accidentally check for global privileges when the object exists in '*' if (space !== _coreSavedObjectsUtilsServer.ALL_NAMESPACES_STRING) { spacesToAuthorize.add(space); // existing namespaces are included so we can later redact if necessary } } } const authorizationResult = await this.authorize({ actions: new Set([action]), types: new Set(enforceMap.keys()), spaces: spacesToAuthorize, enforceMap, options: { allowGlobalResource: true }, auditOptions: { objects } }); return authorizationResult; } async authorizeUpdate(params) { return this.internalAuthorizeUpdate({ namespace: params.namespace, objects: [params.object] }); } async authorizeBulkUpdate(params) { return this.internalAuthorizeUpdate(params, { forceBulkAction: true }); } async internalAuthorizeUpdate(params, options) { const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(params.namespace); const { objects } = params; const action = options !== null && options !== void 0 && options.forceBulkAction || objects.length > 1 ? SecurityAction.BULK_UPDATE : SecurityAction.UPDATE; this.assertObjectsArrayNotEmpty(objects, action); const enforceMap = new Map(); const spacesToAuthorize = new Set([namespaceString]); // Always check authZ for the active space for (const obj of objects) { var _enforceMap$get2; const { type, objectNamespace: objectNamespace, existingNamespaces: existingNamespaces } = obj; const objectNamespaceString = objectNamespace !== null && objectNamespace !== void 0 ? objectNamespace : namespaceString; const spacesToEnforce = (_enforceMap$get2 = enforceMap.get(type)) !== null && _enforceMap$get2 !== void 0 ? _enforceMap$get2 : new Set([namespaceString]); // Always enforce authZ for the active space spacesToEnforce.add(objectNamespaceString); enforceMap.set(type, spacesToEnforce); spacesToAuthorize.add(objectNamespaceString); for (const space of existingNamespaces) { spacesToAuthorize.add(space); // existing namespaces are included so we can later redact if necessary } } const authorizationResult = await this.authorize({ actions: new Set([action]), types: new Set(enforceMap.keys()), spaces: spacesToAuthorize, enforceMap, auditOptions: { objects } }); return authorizationResult; } async authorizeDelete(params) { return this.internalAuthorizeDelete({ namespace: params.namespace, // delete params does not contain existingNamespaces because authz // occurs prior to the preflight check. This is ok because we are // only concerned with enforcing the current space. objects: [{ ...params.object, existingNamespaces: [] }] }); } async authorizeBulkDelete(params) { return this.internalAuthorizeDelete(params, { forceBulkAction: true }); } async internalAuthorizeDelete(params, options) { const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(params.namespace); const { objects } = params; const enforceMap = new Map(); const spacesToAuthorize = new Set([namespaceString]); // Always check authZ for the active space const action = options !== null && options !== void 0 && options.forceBulkAction || objects.length > 1 ? SecurityAction.BULK_DELETE : SecurityAction.DELETE; this.assertObjectsArrayNotEmpty(objects, action); for (const obj of objects) { var _enforceMap$get3; const { type } = obj; const spacesToEnforce = (_enforceMap$get3 = enforceMap.get(type)) !== null && _enforceMap$get3 !== void 0 ? _enforceMap$get3 : new Set([namespaceString]); // Always enforce authZ for the active space enforceMap.set(type, spacesToEnforce); for (const space of obj.existingNamespaces) { spacesToAuthorize.add(space); // existing namespaces are authorized but not enforced } } return this.authorize({ actions: new Set([action]), types: new Set(enforceMap.keys()), spaces: spacesToAuthorize, enforceMap, auditOptions: { objects } }); } async authorizeGet(params) { const { namespace, object, objectNotFound } = params; const spacesToEnforce = new Set([_coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space const existingNamespaces = object.existingNamespaces; return await this.authorize({ actions: new Set([SecurityAction.GET]), types: new Set([object.type]), spaces: new Set([...spacesToEnforce, ...existingNamespaces]), // existing namespaces are included so we can later redact if necessary enforceMap: new Map([[object.type, spacesToEnforce]]), auditOptions: { objects: [object], bypass: objectNotFound ? 'on_success' : 'never' } // Do not audit on success if the object was not found }); } async authorizeBulkGet(params) { const action = SecurityAction.BULK_GET; const namespace = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(params.namespace); const { objects } = params; this.assertObjectsArrayNotEmpty(objects, action); const successAuditObjects = new Array(); const enforceMap = new Map(); const spacesToAuthorize = new Set([namespace]); // Always check authZ for the active space for (const obj of objects) { var _enforceMap$get4; const spacesToEnforce = (_enforceMap$get4 = enforceMap.get(obj.type)) !== null && _enforceMap$get4 !== void 0 ? _enforceMap$get4 : new Set([namespace]); // Always enforce authZ for the active space // Object namespaces are passed into the repo's bulkGet method per object for (const space of (_obj$objectNamespaces = obj.objectNamespaces) !== null && _obj$objectNamespaces !== void 0 ? _obj$objectNamespaces : []) { var _obj$objectNamespaces; spacesToEnforce.add(space); enforceMap.set(obj.type, spacesToEnforce); spacesToAuthorize.add(space); } // Existing namespaces are populated fom the bulkGet response docs for (const space of obj.existingNamespaces) { spacesToAuthorize.add(space); // existing namespaces are included so we can later redact if necessary } // We only log success events for objects that were actually found (and are being returned to the user) // If enforce fails, we audit for all objects if (!obj.error) { successAuditObjects.push(obj); } } const authorizationResult = await this.authorize({ actions: new Set([action]), types: new Set(enforceMap.keys()), spaces: spacesToAuthorize, enforceMap, auditOptions: { objects, useSuccessOutcome: true, bypass: 'on_success' // We will override the success case below } }); // if we made it here, enforce was a success, so let's audit... const { auditAction } = this.decodeSecurityAction(SecurityAction.BULK_GET); if (auditAction) { this.auditHelper({ action: auditAction, objects: successAuditObjects.length ? successAuditObjects : undefined, useSuccessOutcome: true }); } return authorizationResult; } async authorizeCheckConflicts(params) { const action = SecurityAction.CHECK_CONFLICTS; const { namespace, objects } = params; this.assertObjectsArrayNotEmpty(objects, action); const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(namespace); const typesAndSpaces = new Map(); for (const obj of params.objects) { typesAndSpaces.set(obj.type, new Set([namespaceString])); // Always enforce authZ for the active space } return this.authorize({ actions: new Set([SecurityAction.CHECK_CONFLICTS]), types: new Set(typesAndSpaces.keys()), spaces: new Set([namespaceString]), // Always check authZ for the active space enforceMap: typesAndSpaces, // auditing is intentionally bypassed, this function in the previous Security SOC wrapper implementation // did not have audit logging. This is primarily because it is only used by Kibana and is not exposed in a // public HTTP API auditOptions: { bypass: 'always' } }); } async authorizeRemoveReferences(params) { // TODO: Improve authorization and auditing (https://github.com/elastic/kibana/issues/135259) const { namespace, object } = params; const spaces = new Set([_coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(namespace)]); // Always check/enforce authZ for the active space return this.authorize({ actions: new Set([SecurityAction.REMOVE_REFERENCES]), types: new Set([object.type]), spaces, enforceMap: new Map([[object.type, spaces]]), auditOptions: { objects: [object] } }); } async authorizeOpenPointInTime(params) { const { namespaces, types } = params; const preAuthorizationResult = await this.authorize({ actions: new Set([SecurityAction.OPEN_POINT_IN_TIME]), types, spaces: namespaces // No need to bypass in audit options - enforce is completely bypassed (no enforce map) }); if ((preAuthorizationResult === null || preAuthorizationResult === void 0 ? void 0 : preAuthorizationResult.status) === 'unauthorized') { // If the user is unauthorized to find *anything* they requested, throw this.addAuditEvent({ action: AuditAction.OPEN_POINT_IN_TIME, error: new Error('User is unauthorized for any requested types/spaces'), unauthorizedTypes: [...types], unauthorizedSpaces: [...namespaces] }); throw _coreSavedObjectsServer.SavedObjectsErrorHelpers.decorateForbiddenError(new Error('unauthorized')); } this.addAuditEvent({ action: AuditAction.OPEN_POINT_IN_TIME, outcome: 'unknown' }); return preAuthorizationResult; } auditClosePointInTime() { this.addAuditEvent({ action: AuditAction.CLOSE_POINT_IN_TIME, outcome: 'unknown' }); } async authorizeAndRedactMultiNamespaceReferences(params) { var _await$this$authorize; const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(params.namespace); const { objects, options = {} } = params; if (objects.length === 0) return objects; const { purpose } = options; // Check authorization based on all *found* object types / spaces const typesToAuthorize = new Set(); const spacesToAuthorize = new Set([namespaceString]); const addSpacesToAuthorize = (spaces = []) => { for (const space of spaces) spacesToAuthorize.add(space); }; for (const obj of objects) { typesToAuthorize.add(obj.type); addSpacesToAuthorize(obj.spaces); addSpacesToAuthorize(obj.spacesWithMatchingAliases); addSpacesToAuthorize(obj.spacesWithMatchingOrigins); } const action = purpose === 'updateObjectsSpaces' ? SecurityAction.COLLECT_MULTINAMESPACE_REFERENCES_UPDATE_SPACES : SecurityAction.COLLECT_MULTINAMESPACE_REFERENCES; // Enforce authorization based on all *requested* object types and the current space const typesAndSpaces = objects.reduce((acc, { type }) => acc.has(type) ? acc : acc.set(type, new Set([namespaceString])), // Always enforce authZ for the active space new Map()); const { typeMap } = (_await$this$authorize = await this.authorize({ actions: new Set([action]), types: typesToAuthorize, spaces: spacesToAuthorize, enforceMap: typesAndSpaces, auditOptions: { bypass: 'on_success' } // We will audit success results below, after redaction })) !== null && _await$this$authorize !== void 0 ? _await$this$authorize : { typeMap: new Map() }; // Now, filter/redact the results. Most SOR functions just redact the `namespaces` field from each returned object. However, this function // will actually filter the returned object graph itself. // This is done in two steps: (1) objects which the user can't access *in this space* are filtered from the graph, and the // graph is rearranged to avoid leaking information. (2) any spaces that the user can't access are redacted from each individual object. // After we finish filtering, we can write audit events for each object that is going to be returned to the user. const requestedObjectsSet = objects.reduce((acc, { type, id }) => acc.add(`${type}:${id}`), new Set()); const retrievedObjectsSet = objects.reduce((acc, { type, id }) => acc.add(`${type}:${id}`), new Set()); const traversedObjects = new Set(); const filteredObjectsMap = new Map(); const getIsAuthorizedForInboundReference = inbound => { const found = filteredObjectsMap.get(`${inbound.type}:${inbound.id}`); return found && !found.isMissing; // If true, this object can be linked back to one of the requested objects }; let objectsToProcess = [...objects]; while (objectsToProcess.length > 0) { const obj = objectsToProcess.shift(); const { type, id, spaces, inboundReferences } = obj; const objKey = `${type}:${id}`; traversedObjects.add(objKey); // Is the user authorized to access this object in this space? let isAuthorizedForObject = true; try { this.enforceAuthorization({ typesAndSpaces: new Map([[type, new Set([namespaceString])]]), action, typeMap, auditOptions: { bypass: 'always' } // never audit here }); } catch (err) { isAuthorizedForObject = false; } // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access const redactedInboundReferences = inboundReferences.filter(inbound => { if (inbound.type === type && inbound.id === id) { // circular reference, don't redact it return true; } return getIsAuthorizedForInboundReference(inbound); }); // If the user is not authorized to access at least one inbound reference of this object, then we should omit this object. const isAuthorizedForGraph = requestedObjectsSet.has(objKey) || // If true, this is one of the requested objects, and we checked authorization above redactedInboundReferences.some(getIsAuthorizedForInboundReference); if (isAuthorizedForObject && isAuthorizedForGraph) { if (spaces.length) { // Only generate success audit records for "non-empty results" with 1+ spaces // ("empty result" means the object was a non-multi-namespace type, or hidden type, or not found) this.addAuditEvent({ action: AuditAction.COLLECT_MULTINAMESPACE_REFERENCES, savedObject: { type, id } }); } filteredObjectsMap.set(objKey, obj); } else if (!isAuthorizedForObject && isAuthorizedForGraph) { filteredObjectsMap.set(objKey, { ...obj, spaces: [], isMissing: true }); } else if (isAuthorizedForObject && !isAuthorizedForGraph) { const hasUntraversedInboundReferences = inboundReferences.some(ref => !traversedObjects.has(`${ref.type}:${ref.id}`) && retrievedObjectsSet.has(`${ref.type}:${ref.id}`)); if (hasUntraversedInboundReferences) { // this object has inbound reference(s) that we haven't traversed yet; bump it to the back of the list objectsToProcess = [...objectsToProcess, obj]; } else { // There should never be a missing inbound reference. // If there is, then something has gone terribly wrong. const missingInboundReference = inboundReferences.find(ref => !traversedObjects.has(`${ref.type}:${ref.id}`) && !retrievedObjectsSet.has(`${ref.type}:${ref.id}`)); if (missingInboundReference) { throw new Error(`Unexpected inbound reference to "${missingInboundReference.type}:${missingInboundReference.id}"`); } } } } const filteredAndRedactedObjects = [...filteredObjectsMap.values()].map(obj => { const { type, id, spaces, spacesWithMatchingAliases, spacesWithMatchingOrigins, inboundReferences } = obj; // Redact the inbound references so we don't leak any info about other objects that the user is not authorized to access const redactedInboundReferences = inboundReferences.filter(inbound => { if (inbound.type === type && inbound.id === id) { // circular reference, don't redact it return true; } return getIsAuthorizedForInboundReference(inbound); }); /** Simple wrapper for the `redactNamespaces` function that expects a saved object in its params. */ const getRedactedSpaces = spacesArray => { if (!spacesArray) return; const savedObject = { type, namespaces: spacesArray }; // Other SavedObject attributes aren't required const result = this.redactNamespaces({ savedObject, typeMap }); return result.namespaces; }; const redactedSpaces = getRedactedSpaces(spaces); const redactedSpacesWithMatchingAliases = getRedactedSpaces(spacesWithMatchingAliases); const redactedSpacesWithMatchingOrigins = getRedactedSpaces(spacesWithMatchingOrigins); return { ...obj, spaces: redactedSpaces, ...(redactedSpacesWithMatchingAliases && { spacesWithMatchingAliases: redactedSpacesWithMatchingAliases }), ...(redactedSpacesWithMatchingOrigins && { spacesWithMatchingOrigins: redactedSpacesWithMatchingOrigins }), inboundReferences: redactedInboundReferences }; }); return filteredAndRedactedObjects; } async authorizeAndRedactInternalBulkResolve(params) { const { namespace, objects } = params; const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(namespace); const typesAndSpaces = new Map(); const spacesToAuthorize = new Set(); const auditableObjects = []; for (const result of objects) { let auditableObject; if ((0, _internal_bulk_resolve.isBulkResolveError)(result)) { const { type, id, error } = result; if (!_coreSavedObjectsServer.SavedObjectsErrorHelpers.isBadRequestError(error)) { // Only "not found" errors should show up as audit events (not "unsupported type" errors) auditableObject = { type, id }; } } else { const { type, id, namespaces = [] } = result.saved_object; auditableObject = { type, id }; for (const space of namespaces) { spacesToAuthorize.add(space); } } if (auditableObject) { var _typesAndSpaces$get; auditableObjects.push(auditableObject); const spacesToEnforce = (_typesAndSpaces$get = typesAndSpaces.get(auditableObject.type)) !== null && _typesAndSpaces$get !== void 0 ? _typesAndSpaces$get : new Set([namespaceString]); // Always enforce authZ for the active space spacesToEnforce.add(namespaceString); typesAndSpaces.set(auditableObject.type, spacesToEnforce); spacesToAuthorize.add(namespaceString); } } if (typesAndSpaces.size === 0) { // We only had "unsupported type" errors, there are no types to check privileges for, just return early return objects; } const { typeMap } = await this.authorize({ actions: new Set([SecurityAction.INTERNAL_BULK_RESOLVE]), types: new Set(typesAndSpaces.keys()), spaces: spacesToAuthorize, enforceMap: typesAndSpaces, auditOptions: { objects: auditableObjects, useSuccessOutcome: true } }); return objects.map(result => { if ((0, _internal_bulk_resolve.isBulkResolveError)(result)) { return result; } return { ...result, saved_object: this.redactNamespaces({ typeMap, savedObject: result.saved_object }) }; }); } async authorizeUpdateSpaces(params) { const action = SecurityAction.UPDATE_OBJECTS_SPACES; const { objects, spacesToAdd, spacesToRemove } = params; this.assertObjectsArrayNotEmpty(objects, action); const namespaceString = _coreSavedObjectsUtilsServer.SavedObjectsUtils.namespaceIdToString(params.namespace); const typesAndSpaces = new Map(); const spacesToAuthorize = new Set(); for (const obj of objects) { var _typesAndSpaces$get2; const { type, existingNamespaces } = obj; const spacesToEnforce = (_typesAndSpaces$get2 = typesAndSpaces.get(type)) !== null && _typesAndSpaces$get2 !== void 0 ? _typesAndSpaces$get2 : new Set([...spacesToAdd, ...spacesToRemove, namespaceString]); // Always enforce authZ for the active space typesAndSpaces.set(type, spacesToEnforce); for (const space of spacesToEnforce) { spacesToAuthorize.add(space); } for (const space of existingNamespaces) { // Existing namespaces are included so we can later redact if necessary // If this is a specific space, add it to the spaces we'll check privileges for (don't accidentally check for global privileges) if (space === _coreSavedObjectsUtilsServer.ALL_NAMESPACES_STRING) continue; spacesToAuthorize.add(space); } } const addToSpaces = spacesToAdd.length ? spacesToAdd : undefined; const deleteFromSpaces = spacesToRemove.length ? spacesToRemove : undefined; return await this.authorize({ // If a user tries to share/unshare an object to/from '*', they need to have 'share_to_space' privileges for the Global Resource // (e.g., All privileges for All Spaces). actions: new Set([SecurityAction.UPDATE_OBJECTS_SPACES]), types: new Set(typesAndSpaces.keys()), spaces: spacesToAuthorize, enforceMap: typesAndSpaces, options: { allowGlobalResource: true }, auditOptions: { objects, addToSpaces, deleteFromSpaces } }); } async authorizeFind(params) { const { types, namespaces } = params; const preAuthorizationResult = await this.authorize({ actions: new Set([SecurityAction.FIND]), types, spaces: namespaces }); if ((preAuthorizationResult === null || preAuthorizationResult === void 0 ? void 0 : preAuthorizationResult.status) === 'unauthorized') { // If the user is unauthorized to find *anything* they requested, audit but don't throw // This is one of the last remaining calls to addAuditEvent outside of the sec ext this.addAuditEvent({ action: AuditAction.FIND, error: new Error(`User is unauthorized for any requested types/spaces`), unauthorizedTypes: [...types], unauthorizedSpaces: [...namespaces] }); } return preAuthorizationResult; } async getFindRedactTypeMap(params) { const { previouslyCheckedNamespaces: authorizeNamespaces, objects } = params; const spacesToAuthorize = new Set(authorizeNamespaces); // only for namespace redaction for (const { type, id, existingNamespaces } of objects) { for (const space of existingNamespaces) { spacesToAuthorize.add(space); } this.addAuditEvent({ action: AuditAction.FIND, savedObject: { type, id } }); } if (spacesToAuthorize.size > authorizeNamespaces.size) { // If there are any namespaces in the object results that were not already checked during pre-authorization, we need *another* // authorization check so we can correctly redact the object namespaces below. const authorizationResult = await this.authorize({ actions: new Set([SecurityAction.FIND]), types: new Set(objects.map(obj => obj.type)), spaces: spacesToAuthorize }); return authorizationResult.typeMap; } } async authorizeDisableLegacyUrlAliases(aliases) { if (aliases.length === 0) throw new Error(`No aliases specified for authorization`); const [uniqueSpaces, typesAndSpaces] = aliases.reduce(([spaces, typesAndSpacesMap], { targetSpace, targetType }) => { var _typesAndSpacesMap$ge; const spacesForType = (_typesAndSpacesMap$ge = typesAndSpacesMap.get(targetType)) !== null && _typesAndSpacesMap$ge !== void 0 ? _typesAndSpacesMap$ge : new Set(); return [spaces.add(targetSpace), typesAndSpacesMap.set(targetType, spacesForType.add(targetSpace))]; }, [new Set(), new Map()]); await this.authorize({ actions: new Set([SecurityAction.BULK_UPDATE]), types: new Set(typesAndSpaces.keys()), spaces: uniqueSpaces, enforceMap: typesAndSpaces, auditOptions: { objects: aliases.map(alias => { return { type: _coreSavedObjectsBaseServerInternal.LEGACY_URL_ALIAS_TYPE, id: `${alias.targetSpace}:${alias.targetType}:${alias.sourceId}` }; }) } }); } auditObjectsForSpaceDeletion(spaceId, resultObjects) { resultObjects.forEach(obj => { const { namespaces = [] } = obj; const isOnlySpace = namespaces.length === 1; // We can always rely on the `namespaces` field having >=1 element if (namespaces.includes(_constants.ALL_SPACES_ID) && !namespaces.includes(spaceId)) { // This object exists in All Spaces and its `namespaces` field isn't going to change; there's nothing to audit return; } this.addAuditEvent({ action: isOnlySpace ? AuditAction.DELETE : AuditAction.UPDATE_OBJECTS_SPACES, outcome: 'unknown', savedObject: { type: obj.type, id: obj.id }, ...(!isOnlySpace && { deleteFromSpaces: [spaceId] }) }); }); } } /** * The '*' string is an identifier for All Spaces, but that is also the identifier for the Global Resource. We should not check * authorization against it unless explicitly specified, because you can only check privileges for the Global Resource *or* individual * resources (not both). */ exports.SavedObjectsSecurityExtension = SavedObjectsSecurityExtension; function getAuthorizableSpaces(spaces, allowGlobalResource) { const spacesArray = [...spaces]; if (allowGlobalResource) return spacesArray; return spacesArray.filter(x => x !== _constants.ALL_SPACES_ID); } function getMissingPrivileges(privileges) { return privileges.kibana.reduce((acc, { resource, privilege, authorized }) => { if (!authorized) { if (resource) { acc.set(resource, (acc.get(resource) || new Set()).add(privilege)); } // Fail-secure: if a user is not authorized for a specific resource, they are not authorized for the global resource too (global resource is undefined) // The inverse is not true; if a user is not authorized for the global resource, they may still be authorized for a specific resource acc.set(undefined, (acc.get(undefined) || new Set()).add(privilege)); } return acc; }, new Map()); } /** * Utility function to sort potentially redacted namespaces. * Sorts in a case-insensitive manner, and ensures that redacted namespaces ('?') always show up at the end of the array. */ function namespaceComparator(a, b) { if (a === _constants.UNKNOWN_SPACE && b !== _constants.UNKNOWN_SPACE) { return 1; } else if (a !== _constants.UNKNOWN_SPACE && b === _constants.UNKNOWN_SPACE) { return -1; } const A = a.toUpperCase(); const B = b.toUpperCase(); return A > B ? 1 : A < B ? -1 : 0; }