RBAC Phase 1 (#19723)

* partial implementation for OLS Phase 1

* Allow Saved Objects Client to be wrapped

* Add placeholder "kibana.namespace" configuration property

* revert changes to saved objects client

* Remove circular dependency

* Removing namespace setting, we're using xpack.security.rbac.application

* Adding config.getDefault

* Expose SavedObjectsClientProvider on the server for easy plugin consumption

* migrate x-pack changes into kibana

* Beginning to use the ES APIs to insert/check privileges (#18645)

* Beginning to use the ES APIs to insert/check privileges

* Removing todo comment, I think we're good with the current check

* Adding ability to edit kibana application privileges

* Introducing DEFAULT_RESOURCE constant

* Removing unused arguments when performing saved objects auth check

* Performing bulkCreate auth more efficiently

* Throwing error in SavedObjectClient.find if type isn't provided

* Fixing Reporting and removing errant console.log

* Introducing a separate hasPrivileges "service"

* Adding tests and fleshing out the has privileges "service"

* Fixing error message

* You can now edit whatever roles you want

* We're gonna throw the find error in another PR

* Changing conflicting version detection to work when user has no
application privileges

* Throwing correct error when user is forbidden

* Removing unused interceptor

* Adding warning if they're editing a role with application privileges we
can't edit

* Fixing filter...

* Beginning to only update privileges when they need to be

* More tests

* One more test...

* Restricting the rbac application name that can be chosen

* Removing DEFAULT_RESOURCE check

* Supporting 1024 characters for the role name

* Renaming some variables, fixing issue with role w/ no kibana privileges

* Throwing decorated general error when appropriate

* Fixing test description

* Dedent does nothing...

* Renaming some functions

* Adding built-in types and alphabetizing (#19306)

* Filtering out non-default resource Kibana privileges (#19321)

* Removing unused file

* Adding kibana_rbac_dashboard_only_user to dashboard only mode roles (#19511)

* Adding create default roles test (#19505)

* RBAC - SecurityAuditLogger (#19571)

* Manually porting over the AuditLogger for use within the security audit
logger

* HasPrivileges now returns the user from the request

* Has privileges returns username from privilegeCheck

* Adding first eventType to the security audit logger

* Adding authorization success message

* Logging arguments when authorization success

* Fixing test description

* Logging args during audit failures

* RBAC Integration Tests (#19647)

* Porting over the saved objects tests, a bunch are failing, I believe
because security is preventing the requests

* Running saved objects tests with rbac and xsrf disabled

* Adding users

* BulkGet now tests under 3 users

* Adding create tests

* Adding delete tests

* Adding find tests

* Adding get tests

* Adding bulkGet forbidden tests

* Adding not a kibana user tests

* Update tests

* Renaming the actions/privileges to be closer to the functions on the
saved object client itself

* Cleaning up tests and removing without index tests

I'm considering the without index tests to be out of scope for the RBAC
API testing, and we already have unit coverage for these and integration
coverage via the OSS Saved Objects API tests.

* Fixing misspelling

* Fixing "conflicts" after merging master

* Removing some white-space differences

* Deleting files that got left behind in a merge

* Adding the RBAC API Integration Tests

* SavedObjectClient.find filtering (#19708)

* Adding ability to specify filters when calling the repository

* Implementing find filtering

* Revert "Adding ability to specify filters when calling the repository"

This reverts commit 9da30a15db.

* Adding integration tests for find filtering

* Adding forbidden auth logging

* Adding asserts to make sure some audit log isn't used

* Adding more audit log specific tests

* Necessarly is not a work, unfortunately

* Fixing test

* More descriptive name than "result"

* Better unauthorized find message?

* Adding getTypes tests

* Trying to isolate cause of rbac test failures

* Adding .toLowerCase() to work around capitalization issue

* No longer exposing the auditLogger, we don't need it like that right now

* Removing some unused code

* Removing defaultSettings from test that doesn't utilize them

* Fixing misspelling

* Don't need an explicit login privilege when we have them all

* Removing unused code, fixing misspelling, adding comment

* Putting a file back

* No longer creating the roles on start-up (#19799)

* Removing kibana_rbac_dashboard_only_user from dashboard only role
defaults

* Fixing small issue with editing Kibana privileges

* [RBAC Phase 1] - Update application privileges when XPack license changes (#19839)

* Adding start to supporting basic license and switching to plat/gold

* Initialize application privilages on XPack license change

* restore mirror_status_and_initialize

* additional tests and peer review updates

* Introducing watchStatusAndLicenseToInitialize

* Adding some tests

* One more test

* Even better tests

* Removing unused mirrorStatusAndInitialize

* Throwing an error if the wrong status function is called

* RBAC Legacy Fallback (#19818)

* Basic implementation, rather sloppy

* Cleaning stuff up a bit

* Beginning to write tests, going to refactor how we build the privileges

* Making the buildPrivilegesMap no longer return application name as the
main key

* Using real privileges since we need to use them for the legacy fallback

* Adding more tests

* Fixing spelling

* Fixing test description

* Fixing comment description

* Adding similar line breaks in the has privilege calls

* No more settings

* No more rbac enabled setting, we just do RBAC

* Using describe to cleanup the test cases

* Logging deprecations when using the legacy fallback

* Cleaning up a bit...

* Using the privilegeMap for the legacy fallback tests

* Now with even less duplication

* Removing stray `rbacEnabled` from angularjs

* Fixing checkLicenses tests since we added RBAC

* [Flaky Test] - wait for page load to complete (#19895)

@kobelb this seems unrelated to our RBAC Phase 1 work, but I was able to consistently reproduce this on my machine.

* [Flaky Test] Fixes flaky role test (#19899)

Here's a fix for the latest flaky test @kobelb

* Now with even easier repository access

* Sample was including login/version privileges, which was occasionally (#19915)

causing issues that were really hard to replicate

* Dynamic types (#19925)

No more hard-coded types! This will make it so that plugins that register their own mappings just transparently work.

* start to address feedback

* Fix RBAC Phase 1 merge from master (#20226)

This updates RBAC Phase 1 to work against the latest master. Specifically:
1. Removes `xpack_main`'s `registerLicenseChangeCallback`, which we introduced in `security-app-privs`, in favor of `onLicenseInfoChange`, which was recently added to master
2. Updated `x-pack/plugins/security/server/lib/watch_status_and_license_to_initialize.js` to be compliant with rxjs v6

* Retrying initialize 20 times with a scaling backoff (#20297)

* Retrying initialize 20 times with a scaling backoff

* Logging error when we are registering the privileges

* Alternate legacy fallback (#20322)

* Beginning to use alternate callWithRequest fallback

* Only use legacy fallback when user has "some" privileges on index

* Logging useLegacyFallback when there's an authorization failure

* Adding tests, logging failure during find no types fallback

* Switching to using an enum instead of success/useLegacyFallback

* Using _execute to share some of the structure

* Moving comment to where it belongs

* No longer audit logging when we use the legacy fallback

* Setting the status to red on the first error then continually (#20343)

initializing

* Renaming get*Privilege to get*Action

* Adding "instance" to alert about other application privileges

* Revising some of the naming for the edit roles screen

* One more edit role variable renamed

* hasPrivileges is now checkPrivileges

* Revising check_license tests

* Adding 2 more privileges tests

* Moving the other _find method to be near his friend

* Spelling "returning" correctly, whoops

* Adding Privileges tests

* tests for Elasticsearch's privileges APIs

* Switching the hard-coded resource from 'default' to *

* Throw error before we  execute a POST privilege call that won't work

* Resolving issue when initially registering privileges

* Logging legacy fallback deprecation warning on login (#20493)

* Logging legacy fallback deprecation on login

* Consolidation the privileges/authorization folder

* Exposing rudimentary authorization service and fixing authenticate tests

* Moving authorization services configuration to initAuthorization

* Adding "actions" service exposed by the authorization

* Fixing misspelling

* Removing invalid and unused exports

* Adding note about only adding privileges

* Calling it initAuthorizationService

* Throwing explicit validation  error in actions.getSavedObjectAction

* Deep freezing authorization service

* Adding deepFreeze tests

* Checking privileges in one call and cleaning up tests

* Deriving application from Kibana index (#20614)

* Specifying the application on the "authorization service"

* Moving watchStatusAndLicenseToInitialize to be below initAuthorizationService

* Using short-hand propery assignment

* Validate ES has_privileges response before trusting it (#20682)

* validate elasticsearch has_privileges response before trusting it

* address feedback

* Removing unused setting

* Public Role APIs (#20732)

* Beginning to work on external role management APIs

* Refactoring GET tests and adding more permutations

* Adding test for excluding other resources

* Adding get role tests

* Splitting out the endpoints, or else it's gonna get overwhelming

* Splitting out the post and delete actions

* Beginning to work on POST and the tests

* Posting the updated role

* Adding update tests

* Modifying the UI to use the new public APIs

* Removing internal roles API

* Moving the rbac api integration setup tests to use the public role apis

* Testing field_security and query

* Adding create role tests

* We can't update the transient_metadata...

* Removing debugger

* Update and delete tests

* Returning a 204 when POSTing a Role.

* Switching POST to PUT and roles to role

* We don't need the rbacApplication client-side anymore

* Adding delete route tests

* Using not found instead of not acceptable, as that's more likely

* Only allowing us to PUT known Kibana privileges

* Removing transient_metadata

* Removing one letter variable names

* Using PUT instead of POST when saving roles

* Fixing broken tests

* Adding setting to allow the user to turn off the legacy fallback (#20766)

* Pulling the version from the kibana server

* Deleting unused file

* Add API integration tests for roles with index and app privileges (#21033)

* Rbac phase1 functional UI tests (#20949)

* rbac functional tests

*  changes to the test file

* RBAC_functional test

*  incorporating review feedback

* slight modification to the addPriv() to cover all tests

* removed the @ in secure roles and perm file in the describe block  and made it look more relevant

* Fixing role management API from users

* Set a timeout when we try/catch a find, so it doesn't pause a long time

* Changing the way we detect if a user is reserved for the ftr

* Skipping flaky test
This commit is contained in:
Brandon Kobel 2018-07-24 12:40:50 -04:00 committed by GitHub
parent cbe9d389ce
commit 248b124339
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 8341 additions and 226 deletions

View file

@ -17,6 +17,7 @@
* under the License.
*/
import { getRootPropertiesObjects } from '../../mappings';
import { SavedObjectsRepository, ScopedSavedObjectsClientProvider, SavedObjectsRepositoryProvider } from './lib';
import { SavedObjectsClient } from './saved_objects_client';
@ -58,15 +59,16 @@ export function createSavedObjectsService(server) {
}
};
const mappings = server.getKibanaIndexMappingsDsl();
const repositoryProvider = new SavedObjectsRepositoryProvider({
index: server.config().get('kibana.index'),
mappings: server.getKibanaIndexMappingsDsl(),
mappings,
onBeforeWrite,
});
const scopedClientProvider = new ScopedSavedObjectsClientProvider({
index: server.config().get('kibana.index'),
mappings: server.getKibanaIndexMappingsDsl(),
mappings,
onBeforeWrite,
defaultClientFactory({
request,
@ -81,6 +83,7 @@ export function createSavedObjectsService(server) {
});
return {
types: Object.keys(getRootPropertiesObjects(mappings)),
SavedObjectsClient,
SavedObjectsRepository,
getSavedObjectsRepository: (...args) =>

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const ALL_RESOURCE = '*';

View file

@ -8,7 +8,7 @@ import { resolve } from 'path';
import { getUserProvider } from './server/lib/get_user';
import { initAuthenticateApi } from './server/routes/api/v1/authenticate';
import { initUsersApi } from './server/routes/api/v1/users';
import { initRolesApi } from './server/routes/api/v1/roles';
import { initPublicRolesApi } from './server/routes/api/public/roles';
import { initIndicesApi } from './server/routes/api/v1/indices';
import { initLoginView } from './server/routes/views/login';
import { initLogoutView } from './server/routes/views/logout';
@ -16,7 +16,12 @@ import { validateConfig } from './server/lib/validate_config';
import { authenticateFactory } from './server/lib/auth_redirect';
import { checkLicense } from './server/lib/check_license';
import { initAuthenticator } from './server/lib/authentication/authenticator';
import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status';
import { initPrivilegesApi } from './server/routes/api/v1/privileges';
import { SecurityAuditLogger } from './server/lib/audit_logger';
import { AuditLogger } from '../../server/lib/audit_logger';
import { SecureSavedObjectsClient } from './server/lib/saved_objects_client/secure_saved_objects_client';
import { initAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization';
import { watchStatusAndLicenseToInitialize } from './server/lib/watch_status_and_license_to_initialize';
export const security = (kibana) => new kibana.Plugin({
id: 'security',
@ -37,6 +42,14 @@ export const security = (kibana) => new kibana.Plugin({
hostname: Joi.string().hostname(),
port: Joi.number().integer().min(0).max(65535)
}).default(),
authorization: Joi.object({
legacyFallback: Joi.object({
enabled: Joi.boolean().default(true)
}).default()
}).default(),
audit: Joi.object({
enabled: Joi.boolean().default(false)
}).default(),
}).default();
},
@ -64,21 +77,24 @@ export const security = (kibana) => new kibana.Plugin({
return {
secureCookies: config.get('xpack.security.secureCookies'),
sessionTimeout: config.get('xpack.security.sessionTimeout')
sessionTimeout: config.get('xpack.security.sessionTimeout'),
};
}
},
async init(server) {
const thisPlugin = this;
const plugin = this;
const config = server.config();
const xpackMainPlugin = server.plugins.xpack_main;
mirrorPluginStatus(xpackMainPlugin, thisPlugin);
const xpackInfo = xpackMainPlugin.info;
const xpackInfoFeature = xpackInfo.feature(plugin.id);
// Register a function that is called whenever the xpack info changes,
// to re-compute the license check results for this plugin
xpackMainPlugin.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense);
xpackInfoFeature.registerLicenseCheckResultsGenerator(checkLicense);
const config = server.config();
validateConfig(config, message => server.log(['security', 'warning'], message));
// Create a Hapi auth scheme that should be applied to each request.
@ -88,20 +104,59 @@ export const security = (kibana) => new kibana.Plugin({
// automatically assigned to all routes that don't contain an auth config.
server.auth.strategy('session', 'login', 'required');
// exposes server.plugins.security.authorization
initAuthorizationService(server);
watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => {
if (license.allowRbac) {
await registerPrivilegesWithCluster(server);
}
});
const auditLogger = new SecurityAuditLogger(server.config(), new AuditLogger(server, 'security'));
const { savedObjects } = server;
savedObjects.setScopedSavedObjectsClientFactory(({
request,
}) => {
const adminCluster = server.plugins.elasticsearch.getCluster('admin');
const { callWithRequest, callWithInternalUser } = adminCluster;
const callCluster = (...args) => callWithRequest(request, ...args);
const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster);
if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) {
return new savedObjects.SavedObjectsClient(callWithRequestRepository);
}
const { authorization } = server.plugins.security;
const checkPrivileges = authorization.checkPrivilegesWithRequest(request);
const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser);
return new SecureSavedObjectsClient({
internalRepository,
callWithRequestRepository,
errors: savedObjects.SavedObjectsClient.errors,
checkPrivileges,
auditLogger,
savedObjectTypes: savedObjects.types,
actions: authorization.actions,
});
});
getUserProvider(server);
await initAuthenticator(server);
initAuthenticateApi(server);
initUsersApi(server);
initRolesApi(server);
initPublicRolesApi(server);
initIndicesApi(server);
initPrivilegesApi(server);
initLoginView(server, xpackMainPlugin);
initLogoutView(server);
server.injectUiAppVars('login', () => {
const pluginId = 'security';
const xpackInfo = server.plugins.xpack_main.info;
const { showLogin, loginMessage, allowLogin } = xpackInfo.feature(pluginId).getLicenseCheckResults() || {};
const { showLogin, loginMessage, allowLogin } = xpackInfo.feature(plugin.id).getLicenseCheckResults() || {};
return {
loginState: {

View file

@ -6,7 +6,7 @@
import chrome from 'ui/chrome';
const usersUrl = chrome.addBasePath('/api/security/v1/users');
const rolesUrl = chrome.addBasePath('/api/security/v1/roles');
const rolesUrl = chrome.addBasePath('/api/security/role');
export const createApiClient = (httpClient) => {
return {

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'angular-resource';
import { uiModules } from 'ui/modules';
const module = uiModules.get('security', ['ngResource']);
module.service('ApplicationPrivileges', ($resource, chrome) => {
const baseUrl = chrome.addBasePath('/api/security/v1/privileges');
return $resource(baseUrl);
});

View file

@ -35,5 +35,6 @@ module.constant('shieldPrivileges', {
'create_index',
'view_index_metadata',
'read_cross_cluster',
]
],
applications: []
});

View file

@ -5,11 +5,20 @@
*/
import 'angular-resource';
import { omit } from 'lodash';
import angular from 'angular';
import { uiModules } from 'ui/modules';
const module = uiModules.get('security', ['ngResource']);
module.service('ShieldRole', ($resource, chrome) => {
return $resource(chrome.addBasePath('/api/security/v1/roles/:name'), {
return $resource(chrome.addBasePath('/api/security/role/:name'), {
name: '@name'
}, {
save: {
method: 'PUT',
transformRequest(data) {
return angular.toJson(omit(data, 'name', 'transient_metadata', '_unrecognized_applications'));
}
}
});
});

View file

@ -1,8 +1,10 @@
<kbn-management-app section="security" omit-breadcrumb-pages="['edit']">
<!-- This content gets injected below the localNav. -->
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem">
<!-- Subheader -->
<div class="kuiBar kuiVerticalRhythm">
<div class="kuiBarSection">
<!-- Title -->
<h1 class="kuiTitle">
@ -39,6 +41,18 @@
</div>
</div>
<div class="kuiBar kuiVerticalRhythm" ng-if="otherApplications.length > 0">
<div class="kuiInfoPanel kuiInfoPanel--warning">
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-warning"></span>
<span class="kuiInfoPanelHeader__title">
This role contains application privileges for the {{ otherApplications.join(', ') }} application(s) that can't be edited.
If they are for other instances of Kibana, you must manage those privileges on that Kibana instance.
</span>
</div>
</div>
</div>
<!-- Form -->
<form name="form" novalidate class="kuiVerticalRhythm">
<!-- Name -->
@ -56,7 +70,7 @@
ng-model="role.name"
required
pattern="[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*"
maxlength="30"
maxlength="1024"
data-test-subj="roleFormNameInput"
/>
@ -87,15 +101,36 @@
<input
class="kuiCheckBox"
type="checkbox"
ng-checked="includes(role.cluster, privilege)"
ng-click="toggle(role.cluster, privilege)"
ng-checked="includes(role.elasticsearch.cluster, privilege)"
ng-click="toggle(role.elasticsearch.cluster, privilege)"
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
data-test-subj="clusterPrivileges-{{privilege}}"
/>
<span class="kuiOptionLabel">{{privilege}}</span>
</label>
</div>
</div>
<!-- Kibana custom privileges -->
<div class="kuiFormSection">
<label class="kuiFormLabel">
Kibana Privileges
</label>
<div ng-repeat="(key, value) in kibanaPrivilegesViewModel">
<label>
<input
class="kuiCheckBox"
type="checkbox"
ng-model="kibanaPrivilegesViewModel[key]"
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
data-test-subj="kibanaPrivileges-{{key}}"
/>
<span class="kuiOptionLabel">{{key}}</span>
</label>
</div>
</div>
<!-- Run-as privileges -->
<div class="kuiFormSection">
<label class="kuiFormLabel">
@ -103,7 +138,7 @@
</label>
<ui-select
multiple
ng-model="role.run_as"
ng-model="role.elasticsearch.run_as"
ng-disabled="role.metadata._reserved || !isRoleEnabled(role)"
>
<ui-select-match placeholder="Add a user...">
@ -119,7 +154,7 @@
<div class="kuiFormSection">
<kbn-index-privileges-form
is-new-role="editRole.isNewRole"
indices="role.indices"
indices="role.elasticsearch.indices"
index-patterns="indexPatterns"
privileges="privileges"
field-options="editRole.fieldOptions"
@ -140,7 +175,7 @@
class="kuiButton kuiButton--primary"
ng-click="saveRole(role)"
ng-if="!role.metadata._reserved && isRoleEnabled(role)"
ng-disabled="form.$invalid || !areIndicesValid(role.indices)"
ng-disabled="form.$invalid || !areIndicesValid(role.elasticsearch.indices)"
data-test-subj="roleFormSaveButton"
>
Save

View file

@ -11,6 +11,7 @@ import { toggle } from 'plugins/security/lib/util';
import { isRoleEnabled } from 'plugins/security/lib/role';
import template from 'plugins/security/views/management/edit_role.html';
import 'angular-ui-select';
import 'plugins/security/services/application_privilege';
import 'plugins/security/services/shield_user';
import 'plugins/security/services/shield_role';
import 'plugins/security/services/shield_privileges';
@ -21,6 +22,42 @@ import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
import { checkLicenseError } from 'plugins/security/lib/check_license_error';
import { EDIT_ROLES_PATH, ROLES_PATH } from './management_urls';
const getKibanaPrivilegesViewModel = (applicationPrivileges, roleKibanaPrivileges) => {
const viewModel = applicationPrivileges.reduce((acc, applicationPrivilege) => {
acc[applicationPrivilege.name] = false;
return acc;
}, {});
if (!roleKibanaPrivileges || roleKibanaPrivileges.length === 0) {
return viewModel;
}
const assignedPrivileges = _.uniq(_.flatten(_.pluck(roleKibanaPrivileges, 'privileges')));
assignedPrivileges.forEach(assignedPrivilege => {
// we don't want to display privileges that aren't in our expected list of privileges
if (assignedPrivilege in viewModel) {
viewModel[assignedPrivilege] = true;
}
});
return viewModel;
};
const getKibanaPrivileges = (kibanaPrivilegesViewModel) => {
const selectedPrivileges = Object.keys(kibanaPrivilegesViewModel).filter(key => kibanaPrivilegesViewModel[key]);
// if we have any selected privileges, add a single application entry
if (selectedPrivileges.length > 0) {
return [
{
privileges: selectedPrivileges
}
];
}
return [];
};
routes.when(`${EDIT_ROLES_PATH}/:name?`, {
template,
resolve: {
@ -40,11 +77,19 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
});
}
return new ShieldRole({
cluster: [],
indices: [],
run_as: []
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [],
_unrecognized_applications: []
});
},
applicationPrivileges(ApplicationPrivileges, kbnUrl, Promise, Private) {
return ApplicationPrivileges.query().$promise
.catch(checkLicenseError(kbnUrl, Promise, Private));
},
users(ShieldUser, kbnUrl, Promise, Private) {
// $promise is used here because the result is an ngResource, not a promise itself
return ShieldUser.query().$promise
@ -69,6 +114,12 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
$scope.users = $route.current.locals.users;
$scope.indexPatterns = $route.current.locals.indexPatterns;
$scope.privileges = shieldPrivileges;
const applicationPrivileges = $route.current.locals.applicationPrivileges;
const role = $route.current.locals.role;
$scope.kibanaPrivilegesViewModel = getKibanaPrivilegesViewModel(applicationPrivileges, role.kibana);
$scope.otherApplications = role._unrecognized_applications;
$scope.rolesHref = `#${ROLES_PATH}`;
this.isNewRole = $route.current.params.name == null;
@ -89,8 +140,11 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
};
$scope.saveRole = (role) => {
role.indices = role.indices.filter((index) => index.names.length);
role.indices.forEach((index) => index.query || delete index.query);
role.elasticsearch.indices = role.elasticsearch.indices.filter((index) => index.names.length);
role.elasticsearch.indices.forEach((index) => index.query || delete index.query);
role.kibana = getKibanaPrivileges($scope.kibanaPrivilegesViewModel);
return role.$save()
.then(() => toastNotifications.addSuccess('Updated role'))
.then($scope.goToRoleList)
@ -127,13 +181,14 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
$scope.allowDocumentLevelSecurity = xpackInfo.get('features.security.allowRoleDocumentLevelSecurity');
$scope.allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity');
$scope.$watch('role.indices', (indices) => {
$scope.$watch('role.elasticsearch.indices', (indices) => {
if (!indices.length) $scope.addIndex(indices);
else indices.forEach($scope.fetchFieldOptions);
}, true);
$scope.toggle = toggle;
$scope.includes = _.includes;
$scope.union = _.flow(_.union, _.compact);
}
});

View file

@ -35,7 +35,13 @@ export function serverFixture() {
security: {
getUser: stub(),
authenticate: stub(),
deauthenticate: stub()
deauthenticate: stub(),
authorization: {
checkPrivilegesWithRequest: stub(),
actions: {
login: 'stub-login-action',
},
},
},
xpack_main: {

View file

@ -18,7 +18,6 @@ describe('check_license', function () {
feature: sinon.stub(),
license: sinon.stub({
isOneOf() {},
isActive() {}
})
};
@ -35,13 +34,13 @@ describe('check_license', function () {
showLinks: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
loginMessage: 'Login is currently disabled. Administrators should consult the Kibana logs for more details.'
});
});
it('should not show login page or other security elements if license is basic.', () => {
mockXPackInfo.license.isOneOf.withArgs(['basic']).returns(true);
mockXPackInfo.license.isActive.returns(true);
mockXPackInfo.feature.withArgs('security').returns({
isEnabled: () => { return true; }
});
@ -53,13 +52,13 @@ describe('check_license', function () {
showLinks: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
linksMessage: 'Your Basic license does not support Security. Please upgrade your license.'
});
});
it('should not show login page or other security elements if security is disabled in Elasticsearch.', () => {
mockXPackInfo.license.isOneOf.withArgs(['basic']).returns(false);
mockXPackInfo.license.isActive.returns(true);
mockXPackInfo.feature.withArgs('security').returns({
isEnabled: () => { return false; }
});
@ -71,93 +70,45 @@ describe('check_license', function () {
showLinks: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
linksMessage: 'Access is denied because Security is disabled in Elasticsearch.'
});
});
it('should allow to login but forbid document level security if license is not platinum, trial or basic.', () => {
const isBasicOrTrialOrPlatinumMatcher = sinon.match(
(licenses) => licenses.includes('basic')
|| licenses.includes('trial')
|| licenses.includes('platinum')
);
it('should allow to login and allow RBAC but forbid document level security if license is not platinum or trial.', () => {
mockXPackInfo.license.isOneOf
.returns(true)
.withArgs(isBasicOrTrialOrPlatinumMatcher).returns(false);
.returns(false)
.withArgs(['platinum', 'trial']).returns(false);
mockXPackInfo.feature.withArgs('security').returns({
isEnabled: () => { return true; }
});
mockXPackInfo.license.isActive.returns(true);
expect(checkLicense(mockXPackInfo)).to.be.eql({
showLogin: true,
allowLogin: true,
showLinks: true,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false
});
mockXPackInfo.license.isActive.returns(false);
expect(checkLicense(mockXPackInfo)).to.be.eql({
showLogin: true,
allowLogin: true,
showLinks: true,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false
allowRoleFieldLevelSecurity: false,
allowRbac: true,
});
});
it('should allow to login and document level security if license is platinum.', () => {
it('should allow to login, allow RBAC and document level security if license is platinum or trial.', () => {
mockXPackInfo.license.isOneOf
.returns(false)
.withArgs(sinon.match((licenses) => licenses.includes('platinum'))).returns(true);
.withArgs(['platinum', 'trial']).returns(true);
mockXPackInfo.feature.withArgs('security').returns({
isEnabled: () => { return true; }
});
mockXPackInfo.license.isActive.returns(true);
expect(checkLicense(mockXPackInfo)).to.be.eql({
showLogin: true,
allowLogin: true,
showLinks: true,
allowRoleDocumentLevelSecurity: true,
allowRoleFieldLevelSecurity: true
});
mockXPackInfo.license.isActive.returns(false);
expect(checkLicense(mockXPackInfo)).to.be.eql({
showLogin: true,
allowLogin: true,
showLinks: true,
allowRoleDocumentLevelSecurity: true,
allowRoleFieldLevelSecurity: true
allowRoleFieldLevelSecurity: true,
allowRbac: true,
});
});
it('should allow to login and document level security if license is trial.', () => {
mockXPackInfo.license.isOneOf
.returns(false)
.withArgs(sinon.match((licenses) => licenses.includes('trial'))).returns(true);
mockXPackInfo.feature.withArgs('security').returns({
isEnabled: () => { return true; }
});
mockXPackInfo.license.isActive.returns(true);
expect(checkLicense(mockXPackInfo)).to.be.eql({
showLogin: true,
allowLogin: true,
showLinks: true,
allowRoleDocumentLevelSecurity: true,
allowRoleFieldLevelSecurity: true
});
mockXPackInfo.license.isActive.returns(false);
expect(checkLicense(mockXPackInfo)).to.be.eql({
showLogin: true,
allowLogin: true,
showLinks: true,
allowRoleDocumentLevelSecurity: true,
allowRoleFieldLevelSecurity: true
});
});
});

View file

@ -16,7 +16,8 @@ describe('Validate config', function () {
beforeEach(() => {
config = {
get: sinon.stub(),
set: sinon.stub()
getDefault: sinon.stub(),
set: sinon.stub(),
};
log.resetHistory();
});

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export class SecurityAuditLogger {
constructor(config, auditLogger) {
this._enabled = config.get('xpack.security.audit.enabled');
this._auditLogger = auditLogger;
}
savedObjectsAuthorizationFailure(username, action, types, missing, args) {
if (!this._enabled) {
return;
}
this._auditLogger.log(
'saved_objects_authorization_failure',
`${username} unauthorized to ${action} ${types.join(',')}, missing ${missing.join(',')}`,
{
username,
action,
types,
missing,
args
}
);
}
savedObjectsAuthorizationSuccess(username, action, types, args) {
if (!this._enabled) {
return;
}
this._auditLogger.log(
'saved_objects_authorization_success',
`${username} authorized to ${action} ${types.join(',')}`,
{
username,
action,
types,
args,
}
);
}
}

View file

@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { SecurityAuditLogger } from './audit_logger';
const createMockConfig = (settings) => {
const mockConfig = {
get: jest.fn()
};
mockConfig.get.mockImplementation(key => {
return settings[key];
});
return mockConfig;
};
const createMockAuditLogger = () => {
return {
log: jest.fn()
};
};
describe(`#savedObjectsAuthorizationFailure`, () => {
test(`doesn't log anything when xpack.security.audit.enabled is false`, () => {
const config = createMockConfig({
'xpack.security.audit.enabled': false
});
const auditLogger = createMockAuditLogger();
const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
securityAuditLogger.savedObjectsAuthorizationFailure();
expect(auditLogger.log).toHaveBeenCalledTimes(0);
});
test('logs via auditLogger when xpack.security.audit.enabled is true', () => {
const config = createMockConfig({
'xpack.security.audit.enabled': true
});
const auditLogger = createMockAuditLogger();
const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
const username = 'foo-user';
const action = 'foo-action';
const types = [ 'foo-type-1', 'foo-type-2' ];
const missing = [`action:saved_objects/${types[0]}/foo-action`, `action:saved_objects/${types[1]}/foo-action`];
const args = {
'foo': 'bar',
'baz': 'quz',
};
securityAuditLogger.savedObjectsAuthorizationFailure(username, action, types, missing, args);
expect(auditLogger.log).toHaveBeenCalledWith(
'saved_objects_authorization_failure',
expect.stringContaining(`${username} unauthorized to ${action}`),
{
username,
action,
types,
missing,
args,
}
);
});
});
describe(`#savedObjectsAuthorizationSuccess`, () => {
test(`doesn't log anything when xpack.security.audit.enabled is false`, () => {
const config = createMockConfig({
'xpack.security.audit.enabled': false
});
const auditLogger = createMockAuditLogger();
const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
securityAuditLogger.savedObjectsAuthorizationSuccess();
expect(auditLogger.log).toHaveBeenCalledTimes(0);
});
test('logs via auditLogger when xpack.security.audit.enabled is true', () => {
const config = createMockConfig({
'xpack.security.audit.enabled': true
});
const auditLogger = createMockAuditLogger();
const securityAuditLogger = new SecurityAuditLogger(config, auditLogger);
const username = 'foo-user';
const action = 'foo-action';
const types = [ 'foo-type-1', 'foo-type-2' ];
const args = {
'foo': 'bar',
'baz': 'quz',
};
securityAuditLogger.savedObjectsAuthorizationSuccess(username, action, types, args);
expect(auditLogger.log).toHaveBeenCalledWith(
'saved_objects_authorization_success',
expect.stringContaining(`${username} authorized to ${action}`),
{
username,
action,
types,
args,
}
);
});
});

View file

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#getSavedObjectAction() action of "" throws error 1`] = `"action is required and must be a string"`;
exports[`#getSavedObjectAction() action of {} throws error 1`] = `"action is required and must be a string"`;
exports[`#getSavedObjectAction() action of 1 throws error 1`] = `"action is required and must be a string"`;
exports[`#getSavedObjectAction() action of null throws error 1`] = `"action is required and must be a string"`;
exports[`#getSavedObjectAction() action of true throws error 1`] = `"action is required and must be a string"`;
exports[`#getSavedObjectAction() action of undefined throws error 1`] = `"action is required and must be a string"`;
exports[`#getSavedObjectAction() type of "" throws error 1`] = `"type is required and must be a string"`;
exports[`#getSavedObjectAction() type of {} throws error 1`] = `"type is required and must be a string"`;
exports[`#getSavedObjectAction() type of 1 throws error 1`] = `"type is required and must be a string"`;
exports[`#getSavedObjectAction() type of null throws error 1`] = `"type is required and must be a string"`;
exports[`#getSavedObjectAction() type of true throws error 1`] = `"type is required and must be a string"`;
exports[`#getSavedObjectAction() type of undefined throws error 1`] = `"type is required and must be a string"`;

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`with a malformed Elasticsearch response throws a validation error when an extra index privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "index" fails because [child "default-index" fails because ["oopsAnExtraPrivilege" is not allowed]]]`;
exports[`with a malformed Elasticsearch response throws a validation error when an extra privilege is present in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "&#x2a;" fails because ["oops-an-unexpected-privilege" is not allowed]]]]`;
exports[`with a malformed Elasticsearch response throws a validation error when index privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "index" fails because [child "default-index" fails because [child "read" fails because ["read" is required]]]]`;
exports[`with a malformed Elasticsearch response throws a validation error when privileges are missing in the response 1`] = `[Error: Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child "application" fails because [child "kibana-our_application" fails because [child "&#x2a;" fails because [child "mock-action:version" fails because ["mock-action:version" is required]]]]]`;
exports[`with index privileges throws error if missing version privilege and has login privilege 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`;
exports[`with no index privileges throws error if missing version privilege and has login privilege 1`] = `[Error: Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.]`;

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`deep freezes exposed service 1`] = `"Cannot delete property 'checkPrivilegesWithRequest' of #<Object>"`;
exports[`deep freezes exposed service 2`] = `"Cannot add property foo, object is not extensible"`;
exports[`deep freezes exposed service 3`] = `"Cannot assign to read only property 'login' of object '#<Object>'"`;
exports[`deep freezes exposed service 4`] = `"Cannot assign to read only property 'application' of object '#<Object>'"`;

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action3\\" fails because [\\"action3\\" must be a boolean]]]]"`;
exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [child \\"action2\\" fails because [\\"action2\\" is required]]]]"`;
exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"action4\\" is not allowed]]]"`;
exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"otherApplication\\" is not allowed]"`;
exports[`validateEsPrivilegeResponse fails validation when an unexpected resource property is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`;
exports[`validateEsPrivilegeResponse fails validation when the "application" property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [\\"application\\" is required]"`;
exports[`validateEsPrivilegeResponse fails validation when the expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" is required]]]"`;
exports[`validateEsPrivilegeResponse fails validation when the requested application is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [\\"foo-application\\" is required]]"`;
exports[`validateEsPrivilegeResponse fails validation when the resource propertry is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"application\\" fails because [child \\"foo-application\\" fails because [child \\"foo-resource\\" fails because [\\"foo-resource\\" must be an object]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the create index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"create\\" fails because [\\"create\\" must be a boolean]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the create index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"create\\" fails because [\\"create\\" is required]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the delete index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"delete\\" fails because [\\"delete\\" must be a boolean]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the delete index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"delete\\" fails because [\\"delete\\" is required]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the index privilege response contains an extra privilege 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [\\"foo-permission\\" is not allowed]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the index privilege response returns an extra index 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [\\"anotherIndex\\" is not allowed]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the index property is missing 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [\\"index\\" is required]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the kibana index is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [\\".kibana\\" is required]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the read index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"read\\" fails because [\\"read\\" must be a boolean]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the read index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"read\\" fails because [\\"read\\" is required]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the view_index_metadata index privilege is malformed 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"view_index_metadata\\" fails because [\\"view_index_metadata\\" must be a boolean]]]"`;
exports[`validateEsPrivilegeResponse legacy should fail if the view_index_metadata index privilege is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. ValidationError: child \\"index\\" fails because [child \\".kibana\\" fails because [child \\"view_index_metadata\\" fails because [\\"view_index_metadata\\" is required]]]"`;

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { isString } from 'lodash';
export function actionsFactory(config) {
const kibanaVersion = config.get('pkg.version');
return {
getSavedObjectAction(type, action) {
if (!type || !isString(type)) {
throw new Error('type is required and must be a string');
}
if (!action || !isString(action)) {
throw new Error('action is required and must be a string');
}
return `action:saved_objects/${type}/${action}`;
},
login: `action:login`,
version: `version:${kibanaVersion}`,
};
}

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { actionsFactory } from './actions';
const createMockConfig = (settings = {}) => {
const mockConfig = {
get: jest.fn()
};
mockConfig.get.mockImplementation(key => settings[key]);
return mockConfig;
};
describe('#login', () => {
test('returns action:login', () => {
const mockConfig = createMockConfig();
const actions = actionsFactory(mockConfig);
expect(actions.login).toEqual('action:login');
});
});
describe('#version', () => {
test(`returns version:\${config.get('pkg.version')}`, () => {
const version = 'mock-version';
const mockConfig = createMockConfig({ 'pkg.version': version });
const actions = actionsFactory(mockConfig);
expect(actions.version).toEqual(`version:${version}`);
});
});
describe('#getSavedObjectAction()', () => {
test('uses type and action to build action', () => {
const mockConfig = createMockConfig();
const actions = actionsFactory(mockConfig);
const type = 'saved-object-type';
const action = 'saved-object-action';
const result = actions.getSavedObjectAction(type, action);
expect(result).toEqual(`action:saved_objects/${type}/${action}`);
});
[null, undefined, '', 1, true, {}].forEach(type => {
test(`type of ${JSON.stringify(type)} throws error`, () => {
const mockConfig = createMockConfig();
const actions = actionsFactory(mockConfig);
expect(() => actions.getSavedObjectAction(type, 'saved-object-action')).toThrowErrorMatchingSnapshot();
});
});
[null, undefined, '', 1, true, {}].forEach(action => {
test(`action of ${JSON.stringify(action)} throws error`, () => {
const mockConfig = createMockConfig();
const actions = actionsFactory(mockConfig);
expect(() => actions.getSavedObjectAction('saved-object-type', action)).toThrowErrorMatchingSnapshot();
});
});
});

View file

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { uniq } from 'lodash';
import { ALL_RESOURCE } from '../../../common/constants';
import { buildLegacyIndexPrivileges } from './privileges';
import { validateEsPrivilegeResponse } from './validate_es_response';
export const CHECK_PRIVILEGES_RESULT = {
UNAUTHORIZED: Symbol('Unauthorized'),
AUTHORIZED: Symbol('Authorized'),
LEGACY: Symbol('Legacy'),
};
export function checkPrivilegesWithRequestFactory(shieldClient, config, actions, application) {
const { callWithRequest } = shieldClient;
const kibanaIndex = config.get('kibana.index');
const hasIncompatibileVersion = (applicationPrivilegesResponse) => {
return !applicationPrivilegesResponse[actions.version] && applicationPrivilegesResponse[actions.login];
};
const hasAllApplicationPrivileges = (applicationPrivilegesResponse) => {
return Object.values(applicationPrivilegesResponse).every(val => val === true);
};
const hasNoApplicationPrivileges = (applicationPrivilegesResponse) => {
return Object.values(applicationPrivilegesResponse).every(val => val === false);
};
const isLegacyFallbackEnabled = () => {
return config.get('xpack.security.authorization.legacyFallback.enabled');
};
const hasLegacyPrivileges = (indexPrivilegesResponse) => {
return Object.values(indexPrivilegesResponse).includes(true);
};
const determineResult = (applicationPrivilegesResponse, indexPrivilegesResponse) => {
if (hasAllApplicationPrivileges(applicationPrivilegesResponse)) {
return CHECK_PRIVILEGES_RESULT.AUTHORIZED;
}
if (
isLegacyFallbackEnabled() &&
hasNoApplicationPrivileges(applicationPrivilegesResponse) &&
hasLegacyPrivileges(indexPrivilegesResponse)
) {
return CHECK_PRIVILEGES_RESULT.LEGACY;
}
return CHECK_PRIVILEGES_RESULT.UNAUTHORIZED;
};
return function checkPrivilegesWithRequest(request) {
return async function checkPrivileges(privileges) {
const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]);
const hasPrivilegesResponse = await callWithRequest(request, 'shield.hasPrivileges', {
body: {
applications: [{
application,
resources: [ALL_RESOURCE],
privileges: allApplicationPrivileges
}],
index: [{
names: [kibanaIndex],
privileges: buildLegacyIndexPrivileges()
}],
}
});
validateEsPrivilegeResponse(hasPrivilegesResponse, application, allApplicationPrivileges, [ALL_RESOURCE], kibanaIndex);
const applicationPrivilegesResponse = hasPrivilegesResponse.application[application][ALL_RESOURCE];
const indexPrivilegesResponse = hasPrivilegesResponse.index[kibanaIndex];
if (hasIncompatibileVersion(applicationPrivilegesResponse)) {
throw new Error('Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.');
}
return {
result: determineResult(applicationPrivilegesResponse, indexPrivilegesResponse),
username: hasPrivilegesResponse.username,
// we only return missing privileges that they're specifically checking for
missing: Object.keys(applicationPrivilegesResponse)
.filter(privilege => privileges.includes(privilege))
.filter(privilege => !applicationPrivilegesResponse[privilege])
};
};
};
}

View file

@ -0,0 +1,470 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { uniq } from 'lodash';
import { checkPrivilegesWithRequestFactory, CHECK_PRIVILEGES_RESULT } from './check_privileges';
import { ALL_RESOURCE } from '../../../common/constants';
const application = 'kibana-our_application';
const defaultVersion = 'default-version';
const defaultKibanaIndex = 'default-index';
const savedObjectTypes = ['foo-type', 'bar-type'];
const mockActions = {
login: 'mock-action:login',
version: 'mock-action:version',
};
const createMockConfig = (settings = {}) => {
const mockConfig = {
get: jest.fn()
};
const defaultSettings = {
'pkg.version': defaultVersion,
'kibana.index': defaultKibanaIndex,
'xpack.security.authorization.legacyFallback.enabled': true,
};
mockConfig.get.mockImplementation(key => {
return key in settings ? settings[key] : defaultSettings[key];
});
return mockConfig;
};
const createMockShieldClient = (response) => {
const mockCallWithRequest = jest.fn();
mockCallWithRequest.mockImplementationOnce(async () => response);
return {
callWithRequest: mockCallWithRequest,
};
};
const checkPrivilegesTest = (
description, {
settings,
privileges,
applicationPrivilegesResponse,
indexPrivilegesResponse,
expectedResult,
expectErrorThrown,
}) => {
test(description, async () => {
const username = 'foo-username';
const mockConfig = createMockConfig(settings);
const mockShieldClient = createMockShieldClient({
username,
application: {
[application]: {
[ALL_RESOURCE]: applicationPrivilegesResponse
}
},
index: {
[defaultKibanaIndex]: indexPrivilegesResponse
},
});
const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(mockShieldClient, mockConfig, mockActions, application);
const request = Symbol();
const checkPrivileges = checkPrivilegesWithRequest(request);
let actualResult;
let errorThrown = null;
try {
actualResult = await checkPrivileges(privileges);
} catch (err) {
errorThrown = err;
}
expect(mockShieldClient.callWithRequest).toHaveBeenCalledWith(request, 'shield.hasPrivileges', {
body: {
applications: [{
application,
resources: [ALL_RESOURCE],
privileges: uniq([
mockActions.version, mockActions.login, ...privileges
])
}],
index: [{
names: [defaultKibanaIndex],
privileges: ['create', 'delete', 'read', 'view_index_metadata']
}],
}
});
if (expectedResult) {
expect(errorThrown).toBeNull();
expect(actualResult).toEqual(expectedResult);
}
if (expectErrorThrown) {
expect(errorThrown).toMatchSnapshot();
}
});
};
describe(`with no index privileges`, () => {
const indexPrivilegesResponse = {
create: false,
delete: false,
read: false,
view_index_metadata: false,
};
checkPrivilegesTest('returns authorized if they have all application privileges', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.AUTHORIZED,
username: 'foo-username',
missing: [],
}
});
checkPrivilegesTest('returns unauthorized and missing application action when checking missing application action', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`,
`action:saved_objects/${savedObjectTypes[0]}/create`,
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
[`action:saved_objects/${savedObjectTypes[0]}/create`]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [`action:saved_objects/${savedObjectTypes[0]}/create`],
}
});
checkPrivilegesTest('returns unauthorized and missing login when checking missing login action', {
username: 'foo-username',
privileges: [
mockActions.login
],
applicationPrivilegesResponse: {
[mockActions.login]: false,
[mockActions.version]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [mockActions.login],
}
});
checkPrivilegesTest('returns unauthorized and missing version if checking missing version action', {
username: 'foo-username',
privileges: [
mockActions.version
],
applicationPrivilegesResponse: {
[mockActions.login]: false,
[mockActions.version]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [mockActions.version],
}
});
checkPrivilegesTest('throws error if missing version privilege and has login privilege', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`
],
applicationPrivilegesResponse: {
[mockActions.login]: true,
[mockActions.version]: false,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse,
expectErrorThrown: true
});
});
describe(`with index privileges`, () => {
const indexPrivilegesResponse = {
create: true,
delete: true,
read: true,
view_index_metadata: true,
};
checkPrivilegesTest('returns authorized if they have all application privileges', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.AUTHORIZED,
username: 'foo-username',
missing: [],
}
});
checkPrivilegesTest('returns unauthorized and missing application action when checking missing application action', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`,
`action:saved_objects/${savedObjectTypes[0]}/create`,
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
[`action:saved_objects/${savedObjectTypes[0]}/create`]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [`action:saved_objects/${savedObjectTypes[0]}/create`],
}
});
checkPrivilegesTest('returns legacy and missing login when checking missing login action and fallback is enabled', {
username: 'foo-username',
privileges: [
mockActions.login
],
applicationPrivilegesResponse: {
[mockActions.login]: false,
[mockActions.version]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.LEGACY,
username: 'foo-username',
missing: [mockActions.login],
}
});
checkPrivilegesTest('returns unauthorized and missing login when checking missing login action and fallback is disabled', {
settings: {
'xpack.security.authorization.legacyFallback.enabled': false,
},
username: 'foo-username',
privileges: [
mockActions.login
],
applicationPrivilegesResponse: {
[mockActions.login]: false,
[mockActions.version]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [mockActions.login],
}
});
checkPrivilegesTest('returns legacy and missing version if checking missing version action and fallback is enabled', {
username: 'foo-username',
privileges: [
mockActions.version
],
applicationPrivilegesResponse: {
[mockActions.login]: false,
[mockActions.version]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.LEGACY,
username: 'foo-username',
missing: [mockActions.version],
}
});
checkPrivilegesTest('returns unauthorized and missing version if checking missing version action and fallback is disabled', {
settings: {
'xpack.security.authorization.legacyFallback.enabled': false,
},
username: 'foo-username',
privileges: [
mockActions.version
],
applicationPrivilegesResponse: {
[mockActions.login]: false,
[mockActions.version]: false,
},
indexPrivilegesResponse,
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [mockActions.version],
}
});
checkPrivilegesTest('throws error if missing version privilege and has login privilege', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`
],
applicationPrivilegesResponse: {
[mockActions.login]: true,
[mockActions.version]: false,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse,
expectErrorThrown: true
});
});
describe('with no application privileges', () => {
['create', 'delete', 'read', 'view_index_metadata'].forEach(indexPrivilege => {
checkPrivilegesTest(`returns legacy if they have ${indexPrivilege} privilege on the kibana index and fallback is enabled`, {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`
],
applicationPrivilegesResponse: {
[mockActions.version]: false,
[mockActions.login]: false,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: false,
},
indexPrivilegesResponse: {
create: false,
delete: false,
read: false,
view_index_metadata: false,
[indexPrivilege]: true
},
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.LEGACY,
username: 'foo-username',
missing: [`action:saved_objects/${savedObjectTypes[0]}/get`],
}
});
checkPrivilegesTest(`returns unauthorized if they have ${indexPrivilege} privilege on the kibana index and fallback is disabled`, {
settings: {
'xpack.security.authorization.legacyFallback.enabled': false,
},
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`
],
applicationPrivilegesResponse: {
[mockActions.version]: false,
[mockActions.login]: false,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: false,
},
indexPrivilegesResponse: {
create: false,
delete: false,
read: false,
view_index_metadata: false,
[indexPrivilege]: true
},
expectedResult: {
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username: 'foo-username',
missing: [`action:saved_objects/${savedObjectTypes[0]}/get`],
}
});
});
});
describe('with a malformed Elasticsearch response', () => {
const indexPrivilegesResponse = {
create: true,
delete: true,
read: true,
view_index_metadata: true,
};
checkPrivilegesTest('throws a validation error when an extra privilege is present in the response', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`,
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
['oops-an-unexpected-privilege']: true,
},
indexPrivilegesResponse,
expectErrorThrown: true,
});
checkPrivilegesTest('throws a validation error when privileges are missing in the response', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`,
],
applicationPrivilegesResponse: {
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse,
expectErrorThrown: true,
});
checkPrivilegesTest('throws a validation error when an extra index privilege is present in the response', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`,
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse: {
...indexPrivilegesResponse,
oopsAnExtraPrivilege: true,
},
expectErrorThrown: true,
});
const missingIndexPrivileges = {
...indexPrivilegesResponse
};
delete missingIndexPrivileges.read;
checkPrivilegesTest('throws a validation error when index privileges are missing in the response', {
username: 'foo-username',
privileges: [
`action:saved_objects/${savedObjectTypes[0]}/get`,
],
applicationPrivilegesResponse: {
[mockActions.version]: true,
[mockActions.login]: true,
[`action:saved_objects/${savedObjectTypes[0]}/get`]: true,
},
indexPrivilegesResponse: missingIndexPrivileges,
expectErrorThrown: true,
});
});

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { isObject } from 'lodash';
export function deepFreeze(object) {
// for any properties that reference an object, makes sure that object is
// recursively frozen as well
Object.keys(object).forEach(key => {
const value = object[key];
if (isObject(value)) {
deepFreeze(value);
}
});
return Object.freeze(object);
}

View file

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { deepFreeze } from './deep_freeze';
test(`freezes result and input`, () => {
const input = {};
const result = deepFreeze(input);
Object.isFrozen(input);
Object.isFrozen(result);
});
test(`freezes top-level properties that are objects`, () => {
const result = deepFreeze({
object: {},
array: [],
fn: () => {},
number: 1,
string: '',
});
Object.isFrozen(result.object);
Object.isFrozen(result.array);
Object.isFrozen(result.fn);
Object.isFrozen(result.number);
Object.isFrozen(result.string);
});
test(`freezes child properties that are objects`, () => {
const result = deepFreeze({
object: {
object: {
},
array: [],
fn: () => {},
number: 1,
string: '',
},
array: [
{},
[],
() => {},
1,
'',
],
});
Object.isFrozen(result.object.object);
Object.isFrozen(result.object.array);
Object.isFrozen(result.object.fn);
Object.isFrozen(result.object.number);
Object.isFrozen(result.object.string);
Object.isFrozen(result.array[0]);
Object.isFrozen(result.array[1]);
Object.isFrozen(result.array[2]);
Object.isFrozen(result.array[3]);
Object.isFrozen(result.array[4]);
});
test(`freezes grand-child properties that are objects`, () => {
const result = deepFreeze({
object: {
object: {
object: {
},
array: [],
fn: () => {},
number: 1,
string: '',
},
},
array: [
[
{},
[],
() => {},
1,
'',
],
],
});
Object.isFrozen(result.object.object.object);
Object.isFrozen(result.object.object.array);
Object.isFrozen(result.object.object.fn);
Object.isFrozen(result.object.object.number);
Object.isFrozen(result.object.object.string);
Object.isFrozen(result.array[0][0]);
Object.isFrozen(result.array[0][1]);
Object.isFrozen(result.array[0][2]);
Object.isFrozen(result.array[0][3]);
Object.isFrozen(result.array[0][4]);
});

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { CHECK_PRIVILEGES_RESULT } from './check_privileges';
export { registerPrivilegesWithCluster } from './register_privileges_with_cluster';
export { buildPrivilegeMap } from './privileges';
export { initAuthorizationService } from './init';

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { actionsFactory } from './actions';
import { checkPrivilegesWithRequestFactory } from './check_privileges';
import { deepFreeze } from './deep_freeze';
import { getClient } from '../../../../../server/lib/get_client_shield';
export function initAuthorizationService(server) {
const shieldClient = getClient(server);
const config = server.config();
const actions = actionsFactory(config);
const application = `kibana-${config.get('kibana.index')}`;
server.expose('authorization', deepFreeze({
actions,
application,
checkPrivilegesWithRequest: checkPrivilegesWithRequestFactory(shieldClient, config, actions, application),
}));
}

View file

@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { initAuthorizationService } from './init';
import { actionsFactory } from './actions';
import { checkPrivilegesWithRequestFactory } from './check_privileges';
import { getClient } from '../../../../../server/lib/get_client_shield';
jest.mock('./check_privileges', () => ({
checkPrivilegesWithRequestFactory: jest.fn(),
}));
jest.mock('../../../../../server/lib/get_client_shield', () => ({
getClient: jest.fn(),
}));
jest.mock('./actions', () => ({
actionsFactory: jest.fn(),
}));
const createMockConfig = (settings = {}) => {
const mockConfig = {
get: jest.fn()
};
mockConfig.get.mockImplementation(key => settings[key]);
return mockConfig;
};
test(`calls server.expose with exposed services`, () => {
const kibanaIndex = '.a-kibana-index';
const mockConfig = createMockConfig({
'kibana.index': kibanaIndex
});
const mockServer = {
expose: jest.fn(),
config: jest.fn().mockReturnValue(mockConfig)
};
const mockShieldClient = Symbol();
getClient.mockReturnValue(mockShieldClient);
const mockCheckPrivilegesWithRequest = Symbol();
checkPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest);
const mockActions = Symbol();
actionsFactory.mockReturnValue(mockActions);
mockConfig.get.mock;
initAuthorizationService(mockServer);
const application = `kibana-${kibanaIndex}`;
expect(getClient).toHaveBeenCalledWith(mockServer);
expect(actionsFactory).toHaveBeenCalledWith(mockConfig);
expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith(mockShieldClient, mockConfig, mockActions, application);
expect(mockServer.expose).toHaveBeenCalledWith('authorization', {
actions: mockActions,
application,
checkPrivilegesWithRequest: mockCheckPrivilegesWithRequest,
});
});
test(`deep freezes exposed service`, () => {
const mockConfig = createMockConfig({
'kibana.index': ''
});
const mockServer = {
expose: jest.fn(),
config: jest.fn().mockReturnValue(mockConfig)
};
actionsFactory.mockReturnValue({
login: 'login',
});
initAuthorizationService(mockServer);
const exposed = mockServer.expose.mock.calls[0][1];
expect(() => delete exposed.checkPrivilegesWithRequest).toThrowErrorMatchingSnapshot();
expect(() => exposed.foo = 'bar').toThrowErrorMatchingSnapshot();
expect(() => exposed.actions.login = 'not-login').toThrowErrorMatchingSnapshot();
expect(() => exposed.application = 'changed').toThrowErrorMatchingSnapshot();
});

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export function buildPrivilegeMap(savedObjectTypes, application, actions) {
const buildSavedObjectsActions = (savedObjectActions) => {
return savedObjectTypes
.map(type => savedObjectActions.map(savedObjectAction => actions.getSavedObjectAction(type, savedObjectAction)))
.reduce((acc, types) => [...acc, ...types], []);
};
// the following list of privileges should only be added to, you can safely remove actions, but not privileges as
// it's a backwards compatibility issue and we'll have to at least adjust registerPrivilegesWithCluster to support it
return {
all: {
application,
name: 'all',
actions: [actions.version, 'action:*'],
metadata: {}
},
read: {
application,
name: 'read',
actions: [actions.version, actions.login, ...buildSavedObjectsActions(['get', 'bulk_get', 'find'])],
metadata: {}
}
};
}
export function buildLegacyIndexPrivileges() {
return ['create', 'delete', 'read', 'view_index_metadata'];
}

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { difference, isEmpty, isEqual } from 'lodash';
import { buildPrivilegeMap } from './privileges';
import { getClient } from '../../../../../server/lib/get_client_shield';
export async function registerPrivilegesWithCluster(server) {
const { authorization } = server.plugins.security;
const { types: savedObjectTypes } = server.savedObjects;
const { actions, application } = authorization;
const shouldRemovePrivileges = (existingPrivileges, expectedPrivileges) => {
if (isEmpty(existingPrivileges)) {
return false;
}
return difference(Object.keys(existingPrivileges[application]), Object.keys(expectedPrivileges[application])).length > 0;
};
const expectedPrivileges = {
[application]: buildPrivilegeMap(savedObjectTypes, application, actions)
};
server.log(['security', 'debug'], `Registering Kibana Privileges with Elasticsearch for ${application}`);
const callCluster = getClient(server).callWithInternalUser;
try {
// we only want to post the privileges when they're going to change as Elasticsearch has
// to clear the role cache to get these changes reflected in the _has_privileges API
const existingPrivileges = await callCluster(`shield.getPrivilege`, { privilege: application });
if (isEqual(existingPrivileges, expectedPrivileges)) {
server.log(['security', 'debug'], `Kibana Privileges already registered with Elasticearch for ${application}`);
return;
}
// The ES privileges POST endpoint only allows us to add new privileges, or update specified privileges; it doesn't
// remove unspecified privileges. We don't currently have a need to remove privileges, as this would be a
// backwards compatibility issue, and we'd have to figure out how to migrate roles, so we're throwing an Error if we
// unintentionally get ourselves in this position.
if (shouldRemovePrivileges(existingPrivileges, expectedPrivileges)) {
throw new Error(`Privileges are missing and can't be removed, currently.`);
}
server.log(['security', 'debug'], `Updated Kibana Privileges with Elasticearch for ${application}`);
await callCluster('shield.postPrivileges', {
body: expectedPrivileges
});
} catch (err) {
server.log(['security', 'error'], `Error registering Kibana Privileges with Elasticsearch for ${application}: ${err.message}`);
throw err;
}
}

View file

@ -0,0 +1,353 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { registerPrivilegesWithCluster } from './register_privileges_with_cluster';
import { getClient } from '../../../../../server/lib/get_client_shield';
import { buildPrivilegeMap } from './privileges';
jest.mock('../../../../../server/lib/get_client_shield', () => ({
getClient: jest.fn(),
}));
jest.mock('./privileges', () => ({
buildPrivilegeMap: jest.fn(),
}));
const registerPrivilegesWithClusterTest = (description, {
settings = {},
savedObjectTypes,
expectedPrivileges,
existingPrivileges,
throwErrorWhenGettingPrivileges,
throwErrorWhenPuttingPrivileges,
assert
}) => {
const registerMockCallWithInternalUser = () => {
const callWithInternalUser = jest.fn();
getClient.mockReturnValue({
callWithInternalUser,
});
return callWithInternalUser;
};
const defaultVersion = 'default-version';
const application = 'default-application';
const createMockServer = () => {
const mockServer = {
config: jest.fn().mockReturnValue({
get: jest.fn(),
}),
log: jest.fn(),
plugins: {
security: {
authorization: {
actions: Symbol(),
application
}
}
}
};
const defaultSettings = {
'pkg.version': defaultVersion,
};
mockServer.config().get.mockImplementation(key => {
return key in settings ? settings[key] : defaultSettings[key];
});
mockServer.savedObjects = {
types: savedObjectTypes
};
return mockServer;
};
const createExpectUpdatedPrivileges = (mockServer, mockCallWithInternalUser, privileges, error) => {
return () => {
expect(error).toBeUndefined();
expect(mockCallWithInternalUser).toHaveBeenCalledTimes(2);
expect(mockCallWithInternalUser).toHaveBeenCalledWith('shield.getPrivilege', {
privilege: application,
});
expect(mockCallWithInternalUser).toHaveBeenCalledWith(
'shield.postPrivileges',
{
body: {
[application]: privileges
},
}
);
expect(mockServer.log).toHaveBeenCalledWith(
['security', 'debug'],
`Registering Kibana Privileges with Elasticsearch for ${application}`
);
expect(mockServer.log).toHaveBeenCalledWith(
['security', 'debug'],
`Updated Kibana Privileges with Elasticearch for ${application}`
);
};
};
const createExpectDidntUpdatePrivileges = (mockServer, mockCallWithInternalUser, error) => {
return () => {
expect(error).toBeUndefined();
expect(mockCallWithInternalUser).toHaveBeenCalledTimes(1);
expect(mockCallWithInternalUser).toHaveBeenLastCalledWith('shield.getPrivilege', {
privilege: application
});
expect(mockServer.log).toHaveBeenCalledWith(
['security', 'debug'],
`Registering Kibana Privileges with Elasticsearch for ${application}`
);
expect(mockServer.log).toHaveBeenCalledWith(
['security', 'debug'],
`Kibana Privileges already registered with Elasticearch for ${application}`
);
};
};
const createExpectErrorThrown = (mockServer, actualError) => {
return (expectedErrorMessage) => {
expect(actualError).toBeDefined();
expect(actualError).toBeInstanceOf(Error);
expect(actualError.message).toEqual(expectedErrorMessage);
expect(mockServer.log).toHaveBeenCalledWith(
['security', 'error'],
`Error registering Kibana Privileges with Elasticsearch for ${application}: ${expectedErrorMessage}`
);
};
};
test(description, async () => {
const mockServer = createMockServer();
const mockCallWithInternalUser = registerMockCallWithInternalUser()
.mockImplementationOnce(async () => {
if (throwErrorWhenGettingPrivileges) {
throw throwErrorWhenGettingPrivileges;
}
// ES returns an empty object if we don't have any privileges
if (!existingPrivileges) {
return {};
}
return {
[application]: existingPrivileges
};
})
.mockImplementationOnce(async () => {
if (throwErrorWhenPuttingPrivileges) {
throw throwErrorWhenPuttingPrivileges;
}
});
buildPrivilegeMap.mockReturnValue(expectedPrivileges);
let error;
try {
await registerPrivilegesWithCluster(mockServer);
} catch (err) {
error = err;
}
assert({
expectUpdatedPrivileges: createExpectUpdatedPrivileges(mockServer, mockCallWithInternalUser, expectedPrivileges, error),
expectDidntUpdatePrivileges: createExpectDidntUpdatePrivileges(mockServer, mockCallWithInternalUser, error),
expectErrorThrown: createExpectErrorThrown(mockServer, error),
mocks: {
buildPrivilegeMap,
server: mockServer,
}
});
});
};
registerPrivilegesWithClusterTest(`passes saved object types, application and actions to buildPrivilegeMap`, {
settings: {
'pkg.version': 'foo-version'
},
savedObjectTypes: [
'foo-type',
'bar-type',
],
assert: ({ mocks }) => {
expect(mocks.buildPrivilegeMap).toHaveBeenCalledWith(
['foo-type', 'bar-type'],
mocks.server.plugins.security.authorization.application,
mocks.server.plugins.security.authorization.actions,
);
},
});
registerPrivilegesWithClusterTest(`inserts privileges when we don't have any existing privileges`, {
expectedPrivileges: {
expected: true
},
existingPrivileges: null,
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`updates privileges when simple top-level privileges values don't match`, {
expectedPrivileges: {
expected: true
},
existingPrivileges: {
expected: false
},
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`throws error when we have two different top-level privileges`, {
expectedPrivileges: {
notExpected: true
},
existingPrivileges: {
expected: true
},
assert: ({ expectErrorThrown }) => {
expectErrorThrown(`Privileges are missing and can't be removed, currently.`);
}
});
registerPrivilegesWithClusterTest(`updates privileges when we want to add a top-level privilege`, {
expectedPrivileges: {
expected: true,
new: false,
},
existingPrivileges: {
expected: true,
},
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`updates privileges when nested privileges values don't match`, {
expectedPrivileges: {
kibana: {
expected: true
}
},
existingPrivileges: {
kibana: {
expected: false
}
},
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`updates privileges when we have two different nested privileges`, {
expectedPrivileges: {
kibana: {
notExpected: true
}
},
existingPrivileges: {
kibana: {
expected: false
}
},
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`updates privileges when nested privileges arrays don't match`, {
expectedPrivileges: {
kibana: {
expected: ['one', 'two']
}
},
existingPrivileges: {
kibana: {
expected: ['one']
}
},
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`updates privileges when nested property array values are reordered`, {
expectedPrivileges: {
kibana: {
foo: ['one', 'two']
}
},
existingPrivileges: {
kibana: {
foo: ['two', 'one']
}
},
assert: ({ expectUpdatedPrivileges }) => {
expectUpdatedPrivileges();
}
});
registerPrivilegesWithClusterTest(`doesn't update privileges when simple top-level privileges match`, {
expectedPrivileges: {
expected: true
},
existingPrivileges: {
expected: true
},
assert: ({ expectDidntUpdatePrivileges }) => {
expectDidntUpdatePrivileges();
}
});
registerPrivilegesWithClusterTest(`doesn't update privileges when nested properties are reordered`, {
expectedPrivileges: {
kibana: {
foo: true,
bar: false
}
},
existingPrivileges: {
kibana: {
bar: false,
foo: true
}
},
assert: ({ expectDidntUpdatePrivileges }) => {
expectDidntUpdatePrivileges();
}
});
registerPrivilegesWithClusterTest(`throws and logs error when errors getting privileges`, {
throwErrorWhenGettingPrivileges: new Error('Error getting privileges'),
assert: ({ expectErrorThrown }) => {
expectErrorThrown('Error getting privileges');
}
});
registerPrivilegesWithClusterTest(`throws and logs error when errors putting privileges`, {
expectedPrivileges: {
kibana: {
foo: false,
bar: false
}
},
existingPrivileges: {
kibana: {
foo: true,
bar: true
}
},
throwErrorWhenPuttingPrivileges: new Error('Error putting privileges'),
assert: ({ expectErrorThrown }) => {
expectErrorThrown('Error putting privileges');
}
});

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { buildLegacyIndexPrivileges } from './privileges';
const legacyIndexPrivilegesSchema = Joi.object({
...buildLegacyIndexPrivileges().reduce((acc, privilege) => {
return {
...acc,
[privilege]: Joi.bool().required()
};
}, {})
}).required();
export function validateEsPrivilegeResponse(response, application, actions, resources, kibanaIndex) {
const schema = buildValidationSchema(application, actions, resources, kibanaIndex);
const { error, value } = schema.validate(response);
if (error) {
throw new Error(`Invalid response received from Elasticsearch has_privilege endpoint. ${error}`);
}
return value;
}
function buildActionsValidationSchema(actions) {
return Joi.object({
...actions.reduce((acc, action) => {
return {
...acc,
[action]: Joi.bool().required()
};
}, {})
}).required();
}
function buildValidationSchema(application, actions, resources, kibanaIndex) {
const actionValidationSchema = buildActionsValidationSchema(actions);
const resourceValidationSchema = Joi.object({
...resources.reduce((acc, resource) => {
return {
...acc,
[resource]: actionValidationSchema
};
}, {})
}).required();
return Joi.object({
username: Joi.string().required(),
has_all_requested: Joi.bool(),
cluster: Joi.object(),
application: Joi.object({
[application]: resourceValidationSchema,
}).required(),
index: Joi.object({
[kibanaIndex]: legacyIndexPrivilegesSchema
}).required()
}).required();
}

View file

@ -0,0 +1,357 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { validateEsPrivilegeResponse } from "./validate_es_response";
import { buildLegacyIndexPrivileges } from "./privileges";
const resource = 'foo-resource';
const application = 'foo-application';
const kibanaIndex = '.kibana';
const commonResponse = {
username: 'user',
has_all_requested: true,
};
describe('validateEsPrivilegeResponse', () => {
const legacyIndexResponse = {
[kibanaIndex]: {
'create': true,
'delete': true,
'read': true,
'view_index_metadata': true,
}
};
it('should validate a proper response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true,
action2: true,
action3: true
}
}
},
index: legacyIndexResponse
};
const result = validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex);
expect(result).toEqual(response);
});
it('fails validation when an action is missing in the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true,
action3: true
}
}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when an extra action is present in the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true,
action2: true,
action3: true,
action4: true,
}
}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when an action is malformed in the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true,
action2: true,
action3: 'not a boolean',
}
}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when an extra application is present in the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true,
action2: true,
action3: true,
}
},
otherApplication: {
[resource]: {
action1: true,
action2: true,
action3: true,
}
}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when the requested application is missing from the response', () => {
const response = {
...commonResponse,
application: {},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when the "application" property is missing from the response', () => {
const response = {
...commonResponse,
index: {}
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when the expected resource property is missing from the response', () => {
const response = {
...commonResponse,
application: {
[application]: {}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when an unexpected resource property is present in the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
'other-resource': {
action1: true,
action2: true,
action3: true,
}
}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('fails validation when the resource propertry is malformed in the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: 'not-an-object'
}
},
index: legacyIndexResponse
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1', 'action2', 'action3'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
describe('legacy', () => {
it('should validate a proper response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
},
index: legacyIndexResponse
};
const result = validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex);
expect(result).toEqual(response);
});
it('should fail if the index property is missing', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
}
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('should fail if the kibana index is missing from the response', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
},
index: {}
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('should fail if the index privilege response returns an extra index', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
},
index: {
...legacyIndexResponse,
'anotherIndex': {
foo: true
}
}
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
it('should fail if the index privilege response contains an extra privilege', () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
},
index: {
[kibanaIndex]: {
...legacyIndexResponse[kibanaIndex],
'foo-permission': true
}
}
};
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
buildLegacyIndexPrivileges().forEach(privilege => {
test(`should fail if the ${privilege} index privilege is missing from the response`, () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
},
index: {
[kibanaIndex]: {
...legacyIndexResponse[kibanaIndex]
}
}
};
delete response.index[kibanaIndex][privilege];
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
test(`should fail if the ${privilege} index privilege is malformed`, () => {
const response = {
...commonResponse,
application: {
[application]: {
[resource]: {
action1: true
}
}
},
index: {
[kibanaIndex]: {
...legacyIndexResponse[kibanaIndex]
}
}
};
response.index[kibanaIndex][privilege] = 'not a boolean';
expect(() =>
validateEsPrivilegeResponse(response, application, ['action1'], [resource], kibanaIndex)
).toThrowErrorMatchingSnapshot();
});
});
});
});

View file

@ -33,6 +33,7 @@ export function checkLicense(xPackInfo) {
showLinks: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
loginMessage: 'Login is currently disabled. Administrators should consult the Kibana logs for more details.'
};
}
@ -46,6 +47,7 @@ export function checkLicense(xPackInfo) {
showLinks: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
linksMessage: isLicenseBasic
? 'Your Basic license does not support Security. Please upgrade your license.'
: 'Access is denied because Security is disabled in Elasticsearch.'
@ -60,6 +62,7 @@ export function checkLicense(xPackInfo) {
showLinks: true,
// Only platinum and trial licenses are compliant with field- and document-level security.
allowRoleDocumentLevelSecurity: isLicensePlatinumOrTrial,
allowRoleFieldLevelSecurity: isLicensePlatinumOrTrial
allowRoleFieldLevelSecurity: isLicensePlatinumOrTrial,
allowRbac: true,
};
}

View file

@ -1,24 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
export const roleSchema = {
name: Joi.string().required(),
cluster: Joi.array().items(Joi.string()),
indices: Joi.array().items({
names: Joi.array().items(Joi.string()),
field_security: Joi.object().keys({
grant: Joi.array().items(Joi.string()),
except: Joi.array().items(Joi.string())
}),
privileges: Joi.array().items(Joi.string()),
query: Joi.string().allow('')
}),
run_as: Joi.array().items(Joi.string()),
metadata: Joi.object(),
transient_metadata: Joi.object()
};

View file

@ -0,0 +1,167 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get, uniq } from 'lodash';
import { CHECK_PRIVILEGES_RESULT } from '../authorization/check_privileges';
export class SecureSavedObjectsClient {
constructor(options) {
const {
errors,
internalRepository,
callWithRequestRepository,
checkPrivileges,
auditLogger,
savedObjectTypes,
actions,
} = options;
this.errors = errors;
this._internalRepository = internalRepository;
this._callWithRequestRepository = callWithRequestRepository;
this._checkPrivileges = checkPrivileges;
this._auditLogger = auditLogger;
this._savedObjectTypes = savedObjectTypes;
this._actions = actions;
}
async create(type, attributes = {}, options = {}) {
return await this._execute(
type,
'create',
{ type, attributes, options },
repository => repository.create(type, attributes, options),
);
}
async bulkCreate(objects, options = {}) {
const types = uniq(objects.map(o => o.type));
return await this._execute(
types,
'bulk_create',
{ objects, options },
repository => repository.bulkCreate(objects, options),
);
}
async delete(type, id) {
return await this._execute(
type,
'delete',
{ type, id },
repository => repository.delete(type, id),
);
}
async find(options = {}) {
if (options.type) {
return await this._findWithTypes(options);
}
return await this._findAcrossAllTypes(options);
}
async bulkGet(objects = []) {
const types = uniq(objects.map(o => o.type));
return await this._execute(
types,
'bulk_get',
{ objects },
repository => repository.bulkGet(objects)
);
}
async get(type, id) {
return await this._execute(
type,
'get',
{ type, id },
repository => repository.get(type, id)
);
}
async update(type, id, attributes, options = {}) {
return await this._execute(
type,
'update',
{ type, id, attributes, options },
repository => repository.update(type, id, attributes, options)
);
}
async _checkSavedObjectPrivileges(actions) {
try {
return await this._checkPrivileges(actions);
} catch(error) {
const { reason } = get(error, 'body.error', {});
throw this.errors.decorateGeneralError(error, reason);
}
}
async _execute(typeOrTypes, action, args, fn) {
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
const actions = types.map(type => this._actions.getSavedObjectAction(type, action));
const { result, username, missing } = await this._checkSavedObjectPrivileges(actions);
switch (result) {
case CHECK_PRIVILEGES_RESULT.AUTHORIZED:
this._auditLogger.savedObjectsAuthorizationSuccess(username, action, types, args);
return await fn(this._internalRepository);
case CHECK_PRIVILEGES_RESULT.LEGACY:
return await fn(this._callWithRequestRepository);
case CHECK_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, this._actions.getSavedObjectAction(type, action)]));
const { result, username, missing } = await this._checkSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values()));
if (result === CHECK_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 _findWithTypes(options) {
return await this._execute(
options.type,
'find',
{ options },
repository => repository.find(options)
);
}
}

View file

@ -27,4 +27,4 @@ export function validateConfig(config, log) {
} else {
config.set('xpack.security.secureCookies', true);
}
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as Rx from 'rxjs';
import { catchError, mergeMap, map, switchMap, tap } from 'rxjs/operators';
export const RETRY_SCALE_DURATION = 100;
export const RETRY_DURATION_MAX = 10000;
const calculateDuration = i => {
const duration = i * RETRY_SCALE_DURATION;
if (duration > RETRY_DURATION_MAX) {
return RETRY_DURATION_MAX;
}
return duration;
};
// we can't use a retryWhen here, because we want to propagate the red status and then retry
const propagateRedStatusAndScaleRetry = () => {
let i = 0;
return (err, caught) =>
Rx.concat(
Rx.of({
state: 'red',
message: err.message
}),
Rx.timer(calculateDuration(++i)).pipe(mergeMap(() => caught))
);
};
export function watchStatusAndLicenseToInitialize(xpackMainPlugin, downstreamPlugin, initialize) {
const xpackInfo = xpackMainPlugin.info;
const xpackInfoFeature = xpackInfo.feature(downstreamPlugin.id);
const upstreamStatus = xpackMainPlugin.status;
const currentStatus$ = Rx
.of({
state: upstreamStatus.state,
message: upstreamStatus.message,
});
const newStatus$ = Rx
.fromEvent(upstreamStatus, 'change', null, (previousState, previousMsg, state, message) => {
return {
state,
message,
};
});
const status$ = Rx.merge(currentStatus$, newStatus$);
const currentLicense$ = Rx.of(xpackInfoFeature.getLicenseCheckResults());
const newLicense$ = Rx
.fromEventPattern(xpackInfo.onLicenseInfoChange.bind(xpackInfo))
.pipe(map(() => xpackInfoFeature.getLicenseCheckResults()));
const license$ = Rx.merge(currentLicense$, newLicense$);
Rx.combineLatest(status$, license$)
.pipe(
map(([status, license]) => ({ status, license })),
switchMap(({ status, license }) => {
if (status.state !== 'green') {
return Rx.of({ state: status.state, message: status.message });
}
return Rx.defer(() => initialize(license))
.pipe(
map(() => ({
state: 'green',
message: 'Ready',
})),
catchError(propagateRedStatusAndScaleRetry())
);
}),
tap(({ state, message }) => {
downstreamPlugin.status[state](message);
})
)
.subscribe();
}

View file

@ -0,0 +1,296 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EventEmitter } from 'events';
import { watchStatusAndLicenseToInitialize, RETRY_SCALE_DURATION, RETRY_DURATION_MAX } from './watch_status_and_license_to_initialize';
const createMockXpackMainPluginAndFeature = (featureId) => {
const licenseChangeCallbacks = [];
const mockFeature = {
getLicenseCheckResults: jest.fn(),
mock: {
triggerLicenseChange: () => {
for (const callback of licenseChangeCallbacks) {
callback();
}
},
setLicenseCheckResults: (value) => {
mockFeature.getLicenseCheckResults.mockReturnValue(value);
}
}
};
const mockXpackMainPlugin = {
info: {
onLicenseInfoChange: (callback) => {
licenseChangeCallbacks.push(callback);
},
feature: (id) => {
if (id === featureId) {
return mockFeature;
}
throw new Error('Unexpected feature');
}
},
status: new EventEmitter(),
mock: {
setStatus: (state, message) => {
mockXpackMainPlugin.status.state = state;
mockXpackMainPlugin.status.message = message;
mockXpackMainPlugin.status.emit('change', null, null, state, message);
}
}
};
return { mockXpackMainPlugin, mockFeature };
};
const createMockDownstreamPlugin = (id) => {
const defaultImplementation = () => { throw new Error('Not implemented'); };
return {
id,
status: {
disabled: jest.fn().mockImplementation(defaultImplementation),
yellow: jest.fn().mockImplementation(defaultImplementation),
green: jest.fn().mockImplementation(defaultImplementation),
red: jest.fn().mockImplementation(defaultImplementation),
},
};
};
const advanceRetry = async (initializeCount) => {
await Promise.resolve();
let duration = initializeCount * RETRY_SCALE_DURATION;
if (duration > RETRY_DURATION_MAX) {
duration = RETRY_DURATION_MAX;
}
jest.advanceTimersByTime(duration);
};
['red', 'yellow', 'disabled'].forEach(state => {
test(`mirrors ${state} immediately`, () => {
const pluginId = 'foo-plugin';
const message = `${state} is now the state`;
const { mockXpackMainPlugin } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus(state, message);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
const initializeMock = jest.fn();
downstreamPlugin.status[state].mockImplementation(() => { });
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
expect(initializeMock).not.toHaveBeenCalled();
expect(downstreamPlugin.status[state]).toHaveBeenCalledTimes(1);
expect(downstreamPlugin.status[state]).toHaveBeenCalledWith(message);
});
});
test(`calls initialize and doesn't immediately set downstream status when the initial status is green`, () => {
const pluginId = 'foo-plugin';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green', 'green is now the state');
const licenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(licenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
const initializeMock = jest.fn().mockImplementation(() => new Promise(() => { }));
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
expect(initializeMock).toHaveBeenCalledTimes(1);
expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults);
expect(downstreamPlugin.status.green).toHaveBeenCalledTimes(0);
});
test(`sets downstream plugin's status to green when initialize resolves`, (done) => {
const pluginId = 'foo-plugin';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green', 'green is now the state');
const licenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(licenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
const initializeMock = jest.fn().mockImplementation(() => Promise.resolve());
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
expect(initializeMock).toHaveBeenCalledTimes(1);
expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults);
downstreamPlugin.status.green.mockImplementation(actualMessage => {
expect(actualMessage).toBe('Ready');
done();
});
});
test(`sets downstream plugin's status to red when initialize initially rejects, and continually polls initialize`, (done) => {
jest.useFakeTimers();
const pluginId = 'foo-plugin';
const errorMessage = 'the error message';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green');
const licenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(licenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
let isRed = false;
let initializeCount = 0;
const initializeMock = jest.fn().mockImplementation(() => {
++initializeCount;
// on the second retry, ensure we already set the status to red
if (initializeCount === 2) {
expect(isRed).toBe(true);
}
// this should theoretically continue indefinitely, but we only have so long to run the tests
if (initializeCount === 100) {
done();
}
// everytime this is called, we have to wait for a new promise to be resolved
// allowing the Promise the we return below to run, and then advance the timers
setImmediate(() => {
advanceRetry(initializeCount);
});
return Promise.reject(new Error(errorMessage));
});
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
expect(initializeMock).toHaveBeenCalledTimes(1);
expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults);
downstreamPlugin.status.red.mockImplementation(message => {
isRed = true;
expect(message).toBe(errorMessage);
});
});
test(`sets downstream plugin's status to green when initialize resolves after rejecting 10 times`, (done) => {
jest.useFakeTimers();
const pluginId = 'foo-plugin';
const errorMessage = 'the error message';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green');
const licenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(licenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
let initializeCount = 0;
const initializeMock = jest.fn().mockImplementation(() => {
++initializeCount;
// everytime this is called, we have to wait for a new promise to be resolved
// allowing the Promise the we return below to run, and then advance the timers
setImmediate(() => {
advanceRetry(initializeCount);
});
if (initializeCount >= 10) {
return Promise.resolve();
}
return Promise.reject(new Error(errorMessage));
});
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
expect(initializeMock).toHaveBeenCalledTimes(1);
expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults);
downstreamPlugin.status.red.mockImplementation(message => {
expect(initializeCount).toBeLessThan(10);
expect(message).toBe(errorMessage);
});
downstreamPlugin.status.green.mockImplementation(message => {
expect(initializeCount).toBe(10);
expect(message).toBe('Ready');
done();
});
});
test(`calls initialize twice when it gets a new license and the status is green`, (done) => {
const pluginId = 'foo-plugin';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green');
const firstLicenseCheckResults = Symbol();
const secondLicenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
const initializeMock = jest.fn().mockImplementation(() => Promise.resolve());
let count = 0;
downstreamPlugin.status.green.mockImplementation(message => {
expect(message).toBe('Ready');
++count;
if (count === 1) {
mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults);
mockFeature.mock.triggerLicenseChange();
}
if (count === 2) {
expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults);
expect(initializeMock).toHaveBeenCalledWith(secondLicenseCheckResults);
expect(initializeMock).toHaveBeenCalledTimes(2);
done();
}
});
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
});
test(`doesn't call initialize twice when it gets a new license when the status isn't green`, (done) => {
const pluginId = 'foo-plugin';
const redMessage = 'the red message';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green');
const firstLicenseCheckResults = Symbol();
const secondLicenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
const initializeMock = jest.fn().mockImplementation(() => Promise.resolve());
downstreamPlugin.status.green.mockImplementation(message => {
expect(message).toBe('Ready');
mockXpackMainPlugin.mock.setStatus('red', redMessage);
mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults);
mockFeature.mock.triggerLicenseChange();
});
downstreamPlugin.status.red.mockImplementation(message => {
expect(message).toBe(redMessage);
expect(initializeMock).toHaveBeenCalledTimes(1);
expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults);
done();
});
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
});
test(`calls initialize twice when the status changes to green twice`, (done) => {
const pluginId = 'foo-plugin';
const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId);
mockXpackMainPlugin.mock.setStatus('green');
const licenseCheckResults = Symbol();
mockFeature.mock.setLicenseCheckResults(licenseCheckResults);
const downstreamPlugin = createMockDownstreamPlugin(pluginId);
const initializeMock = jest.fn().mockImplementation(() => Promise.resolve());
let count = 0;
downstreamPlugin.status.green.mockImplementation(message => {
expect(message).toBe('Ready');
++count;
if (count === 1) {
mockXpackMainPlugin.mock.setStatus('green');
}
if (count === 2) {
expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults);
expect(initializeMock).toHaveBeenCalledTimes(2);
done();
}
});
watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock);
});

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import Joi from 'joi';
import { wrapError } from '../../../../lib/errors';
export function initDeleteRolesApi(server, callWithRequest, routePreCheckLicenseFn) {
server.route({
method: 'DELETE',
path: '/api/security/role/{name}',
handler(request, reply) {
const name = request.params.name;
return callWithRequest(request, 'shield.deleteRole', { name }).then(
() => reply().code(204),
_.flow(wrapError, reply));
},
config: {
validate: {
params: Joi.object()
.keys({
name: Joi.string()
.required(),
})
.required(),
},
pre: [routePreCheckLicenseFn]
}
});
}

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import Boom from 'boom';
import { initDeleteRolesApi } from './delete';
const createMockServer = () => {
const mockServer = new Hapi.Server({ debug: false });
mockServer.connection({ port: 8080 });
return mockServer;
};
const defaultPreCheckLicenseImpl = (request, reply) => reply();
describe('DELETE role', () => {
const deleteRoleTest = (
description,
{
name,
preCheckLicenseImpl,
callWithRequestImpl,
asserts,
}
) => {
test(description, async () => {
const mockServer = createMockServer();
const pre = jest.fn().mockImplementation(preCheckLicenseImpl);
const mockCallWithRequest = jest.fn();
if (callWithRequestImpl) {
mockCallWithRequest.mockImplementation(callWithRequestImpl);
}
initDeleteRolesApi(mockServer, mockCallWithRequest, pre);
const headers = {
authorization: 'foo',
};
const request = {
method: 'DELETE',
url: `/api/security/role/${name}`,
headers,
};
const { result, statusCode } = await mockServer.inject(request);
if (preCheckLicenseImpl) {
expect(pre).toHaveBeenCalled();
} else {
expect(pre).not.toHaveBeenCalled();
}
if (callWithRequestImpl) {
expect(mockCallWithRequest).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: headers.authorization,
}),
}),
'shield.deleteRole',
{ name },
);
} else {
expect(mockCallWithRequest).not.toHaveBeenCalled();
}
expect(statusCode).toBe(asserts.statusCode);
expect(result).toEqual(asserts.result);
});
};
describe('failure', () => {
deleteRoleTest(`requires name in params`, {
name: '',
asserts: {
statusCode: 404,
result: {
error: 'Not Found',
statusCode: 404,
},
},
});
deleteRoleTest(`returns result of routePreCheckLicense`, {
preCheckLicenseImpl: (request, reply) =>
reply(Boom.forbidden('test forbidden message')),
asserts: {
statusCode: 403,
result: {
error: 'Forbidden',
statusCode: 403,
message: 'test forbidden message',
},
},
});
deleteRoleTest(`returns error from callWithRequest`, {
name: 'foo-role',
preCheckLicenseImpl: defaultPreCheckLicenseImpl,
callWithRequestImpl: async () => {
throw Boom.notFound('test not found message');
},
asserts: {
statusCode: 404,
result: {
error: 'Not Found',
statusCode: 404,
message: 'test not found message',
},
},
});
});
describe('success', () => {
deleteRoleTest(`deletes role`, {
name: 'foo-role',
preCheckLicenseImpl: defaultPreCheckLicenseImpl,
callWithRequestImpl: async () => {},
asserts: {
statusCode: 204,
result: null
}
});
});
});

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import Boom from 'boom';
import { ALL_RESOURCE } from '../../../../../common/constants';
import { wrapError } from '../../../../lib/errors';
export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application) {
const transformKibanaApplicationsFromEs = (roleApplications) => {
return roleApplications
.filter(roleApplication => roleApplication.application === application)
.filter(roleApplication => roleApplication.resources.length > 0)
.filter(roleApplication => roleApplication.resources.every(resource => resource === ALL_RESOURCE))
.map(roleApplication => ({ privileges: roleApplication.privileges }));
};
const transformUnrecognizedApplicationsFromEs = (roleApplications) => {
return _.uniq(roleApplications
.filter(roleApplication => roleApplication.application !== application)
.map(roleApplication => roleApplication.application));
};
const transformRoleFromEs = (role, name) => {
return {
name,
metadata: role.metadata,
transient_metadata: role.transient_metadata,
elasticsearch: {
cluster: role.cluster,
indices: role.indices,
run_as: role.run_as,
},
kibana: transformKibanaApplicationsFromEs(role.applications),
_unrecognized_applications: transformUnrecognizedApplicationsFromEs(role.applications),
};
};
const transformRolesFromEs = (roles) => {
return _.map(roles, (role, name) => transformRoleFromEs(role, name));
};
server.route({
method: 'GET',
path: '/api/security/role',
handler(request, reply) {
return callWithRequest(request, 'shield.getRole').then(
(response) => {
return reply(transformRolesFromEs(response));
},
_.flow(wrapError, reply)
);
},
config: {
pre: [routePreCheckLicenseFn]
}
});
server.route({
method: 'GET',
path: '/api/security/role/{name}',
handler(request, reply) {
const name = request.params.name;
return callWithRequest(request, 'shield.getRole', { name }).then(
(response) => {
if (response[name]) return reply(transformRoleFromEs(response[name], name));
return reply(Boom.notFound());
},
_.flow(wrapError, reply));
},
config: {
pre: [routePreCheckLicenseFn]
}
});
}

View file

@ -0,0 +1,577 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import Boom from 'boom';
import { initGetRolesApi } from './get';
const application = 'kibana-.kibana';
const createMockServer = () => {
const mockServer = new Hapi.Server({ debug: false });
mockServer.connection({ port: 8080 });
return mockServer;
};
describe('GET roles', () => {
const getRolesTest = (
description,
{
preCheckLicenseImpl = (request, reply) => reply(),
callWithRequestImpl,
asserts,
}
) => {
test(description, async () => {
const mockServer = createMockServer();
const pre = jest.fn().mockImplementation(preCheckLicenseImpl);
const mockCallWithRequest = jest.fn();
if (callWithRequestImpl) {
mockCallWithRequest.mockImplementation(callWithRequestImpl);
}
initGetRolesApi(mockServer, mockCallWithRequest, pre, application);
const headers = {
authorization: 'foo',
};
const request = {
method: 'GET',
url: '/api/security/role',
headers,
};
const { result, statusCode } = await mockServer.inject(request);
expect(pre).toHaveBeenCalled();
if (callWithRequestImpl) {
expect(mockCallWithRequest).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: headers.authorization,
}),
}),
'shield.getRole'
);
} else {
expect(mockCallWithRequest).not.toHaveBeenCalled();
}
expect(statusCode).toBe(asserts.statusCode);
expect(result).toEqual(asserts.result);
});
};
describe('failure', () => {
getRolesTest(`returns result of routePreCheckLicense`, {
preCheckLicenseImpl: (request, reply) =>
reply(Boom.forbidden('test forbidden message')),
asserts: {
statusCode: 403,
result: {
error: 'Forbidden',
statusCode: 403,
message: 'test forbidden message',
},
},
});
getRolesTest(`returns error from callWithRequest`, {
callWithRequestImpl: async () => {
throw Boom.notAcceptable('test not acceptable message');
},
asserts: {
statusCode: 406,
result: {
error: 'Not Acceptable',
statusCode: 406,
message: 'test not acceptable message',
},
},
});
});
describe('success', () => {
getRolesTest(`transforms elasticsearch privileges`, {
callWithRequestImpl: async () => ({
first_role: {
cluster: ['manage_watcher'],
indices: [
{
names: ['.kibana*'],
privileges: ['read', 'view_index_metadata'],
},
],
applications: [],
run_as: ['other_user'],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: [
{
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: ['manage_watcher'],
indices: [
{
names: ['.kibana*'],
privileges: ['read', 'view_index_metadata'],
},
],
run_as: ['other_user'],
},
kibana: [],
_unrecognized_applications: [],
},
],
},
});
getRolesTest(`transforms matching applications to kibana privileges`, {
callWithRequestImpl: async () => ({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: ['read'],
resources: ['*'],
},
{
application,
privileges: ['all'],
resources: ['*'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: [
{
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
privileges: ['read'],
},
{
privileges: ['all'],
},
],
_unrecognized_applications: [],
},
],
},
});
getRolesTest(`excludes resources other than * from kibana privileges`, {
callWithRequestImpl: async () => ({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: ['read'],
// Elasticsearch should prevent this from happening
resources: [],
},
{
application,
privileges: ['read'],
resources: ['default', '*'],
},
{
application,
privileges: ['read'],
resources: ['some-other-space'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: [
{
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [],
_unrecognized_applications: [],
},
],
},
});
getRolesTest(`transforms unrecognized applications`, {
callWithRequestImpl: async () => ({
first_role: {
cluster: [],
indices: [],
applications: [
{
application: 'kibana-.another-kibana',
privileges: ['read'],
resources: ['*'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: [
{
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [],
_unrecognized_applications: ['kibana-.another-kibana']
},
],
},
});
});
});
describe('GET role', () => {
const getRoleTest = (
description,
{
name,
preCheckLicenseImpl = (request, reply) => reply(),
callWithRequestImpl,
asserts,
}
) => {
test(description, async () => {
const mockServer = createMockServer();
const pre = jest.fn().mockImplementation(preCheckLicenseImpl);
const mockCallWithRequest = jest.fn();
if (callWithRequestImpl) {
mockCallWithRequest.mockImplementation(callWithRequestImpl);
}
initGetRolesApi(mockServer, mockCallWithRequest, pre, 'kibana-.kibana');
const headers = {
authorization: 'foo',
};
const request = {
method: 'GET',
url: `/api/security/role/${name}`,
headers,
};
const { result, statusCode } = await mockServer.inject(request);
expect(pre).toHaveBeenCalled();
if (callWithRequestImpl) {
expect(mockCallWithRequest).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: headers.authorization,
}),
}),
'shield.getRole',
{ name }
);
} else {
expect(mockCallWithRequest).not.toHaveBeenCalled();
}
expect(statusCode).toBe(asserts.statusCode);
expect(result).toEqual(asserts.result);
});
};
describe('failure', () => {
getRoleTest(`returns result of routePreCheckLicense`, {
preCheckLicenseImpl: (request, reply) =>
reply(Boom.forbidden('test forbidden message')),
asserts: {
statusCode: 403,
result: {
error: 'Forbidden',
statusCode: 403,
message: 'test forbidden message',
},
},
});
getRoleTest(`returns error from callWithRequest`, {
name: 'foo-role',
callWithRequestImpl: async () => {
throw Boom.notAcceptable('test not acceptable message');
},
asserts: {
statusCode: 406,
result: {
error: 'Not Acceptable',
statusCode: 406,
message: 'test not acceptable message',
},
},
});
});
describe('success', () => {
getRoleTest(`transforms elasticsearch privileges`, {
name: 'first_role',
callWithRequestImpl: async () => ({
first_role: {
cluster: ['manage_watcher'],
indices: [
{
names: ['.kibana*'],
privileges: ['read', 'view_index_metadata'],
},
],
applications: [],
run_as: ['other_user'],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: {
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: ['manage_watcher'],
indices: [
{
names: ['.kibana*'],
privileges: ['read', 'view_index_metadata'],
},
],
run_as: ['other_user'],
},
kibana: [],
_unrecognized_applications: [],
},
},
});
getRoleTest(`transforms matching applications to kibana privileges`, {
name: 'first_role',
callWithRequestImpl: async () => ({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: ['read'],
resources: ['*'],
},
{
application,
privileges: ['all'],
resources: ['*'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: {
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [
{
privileges: ['read'],
},
{
privileges: ['all'],
},
],
_unrecognized_applications: [],
},
},
});
getRoleTest(`excludes resources other than * from kibana privileges`, {
name: 'first_role',
callWithRequestImpl: async () => ({
first_role: {
cluster: [],
indices: [],
applications: [
{
application,
privileges: ['read'],
// Elasticsearch should prevent this from happening
resources: [],
},
{
application,
privileges: ['read'],
resources: ['default', '*'],
},
{
application,
privileges: ['read'],
resources: ['some-other-space'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: {
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [],
_unrecognized_applications: [],
},
},
});
getRoleTest(`transforms unrecognized applications`, {
name: 'first_role',
callWithRequestImpl: async () => ({
first_role: {
cluster: [],
indices: [],
applications: [
{
application: 'kibana-.another-kibana',
privileges: ['read'],
resources: ['*'],
},
],
run_as: [],
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
},
}),
asserts: {
statusCode: 200,
result: {
name: 'first_role',
metadata: {
_reserved: true,
},
transient_metadata: {
enabled: true,
},
elasticsearch: {
cluster: [],
indices: [],
run_as: [],
},
kibana: [],
_unrecognized_applications: ['kibana-.another-kibana'],
},
},
});
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { buildPrivilegeMap } from '../../../../lib/authorization';
import { getClient } from '../../../../../../../server/lib/get_client_shield';
import { routePreCheckLicense } from '../../../../lib/route_pre_check_license';
import { initGetRolesApi } from './get';
import { initDeleteRolesApi } from './delete';
import { initPutRolesApi } from './put';
export function initPublicRolesApi(server) {
const callWithRequest = getClient(server).callWithRequest;
const routePreCheckLicenseFn = routePreCheckLicense(server);
const { application, actions } = server.plugins.security.authorization;
const savedObjectTypes = server.savedObjects.types;
const privilegeMap = buildPrivilegeMap(savedObjectTypes, application, actions);
initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, application);
initPutRolesApi(server, callWithRequest, routePreCheckLicenseFn, privilegeMap, application);
initDeleteRolesApi(server, callWithRequest, routePreCheckLicenseFn);
}

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { pick, identity } from 'lodash';
import Joi from 'joi';
import { ALL_RESOURCE } from '../../../../../common/constants';
import { wrapError } from '../../../../lib/errors';
const transformKibanaPrivilegeToEs = (application, kibanaPrivilege) => {
return {
privileges: kibanaPrivilege.privileges,
application,
resources: [ALL_RESOURCE],
};
};
const transformRolesToEs = (
application,
payload,
existingApplications = []
) => {
const { elasticsearch = {}, kibana = [] } = payload;
const otherApplications = existingApplications.filter(
roleApplication => roleApplication.application !== application
);
return pick({
metadata: payload.metadata,
cluster: elasticsearch.cluster || [],
indices: elasticsearch.indices || [],
run_as: elasticsearch.run_as || [],
applications: [
...kibana.map(kibanaPrivilege =>
transformKibanaPrivilegeToEs(application, kibanaPrivilege)
),
...otherApplications,
],
}, identity);
};
export function initPutRolesApi(
server,
callWithRequest,
routePreCheckLicenseFn,
privilegeMap,
application
) {
const schema = Joi.object().keys({
metadata: Joi.object().optional(),
elasticsearch: Joi.object().keys({
cluster: Joi.array().items(Joi.string()),
indices: Joi.array().items({
names: Joi.array().items(Joi.string()),
field_security: Joi.object().keys({
grant: Joi.array().items(Joi.string()),
except: Joi.array().items(Joi.string()),
}),
privileges: Joi.array().items(Joi.string()),
query: Joi.string().allow(''),
}),
run_as: Joi.array().items(Joi.string()),
}),
kibana: Joi.array().items({
privileges: Joi.array().items(Joi.string().valid(Object.keys(privilegeMap))),
}),
});
server.route({
method: 'PUT',
path: '/api/security/role/{name}',
async handler(request, reply) {
const name = request.params.name;
try {
const existingRoleResponse = await callWithRequest(request, 'shield.getRole', {
name,
ignore: [404],
});
const body = transformRolesToEs(
application,
request.payload,
existingRoleResponse[name] ? existingRoleResponse[name].applications : []
);
await callWithRequest(request, 'shield.putRole', { name, body });
reply().code(204);
} catch (err) {
reply(wrapError(err));
}
},
config: {
validate: {
params: Joi.object()
.keys({
name: Joi.string()
.required()
.min(1)
.max(1024),
})
.required(),
payload: schema,
},
pre: [routePreCheckLicenseFn],
},
});
}

View file

@ -0,0 +1,503 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Hapi from 'hapi';
import Boom from 'boom';
import { initPutRolesApi } from './put';
import { ALL_RESOURCE } from '../../../../../common/constants';
const application = 'kibana-.kibana';
const createMockServer = () => {
const mockServer = new Hapi.Server({ debug: false });
mockServer.connection({ port: 8080 });
return mockServer;
};
const defaultPreCheckLicenseImpl = (request, reply) => reply();
const privilegeMap = {
'test-kibana-privilege-1': {},
'test-kibana-privilege-2': {},
'test-kibana-privilege-3': {},
};
const putRoleTest = (
description,
{ name, payload, preCheckLicenseImpl, callWithRequestImpls = [], asserts }
) => {
test(description, async () => {
const mockServer = createMockServer();
const mockPreCheckLicense = jest
.fn()
.mockImplementation(preCheckLicenseImpl);
const mockCallWithRequest = jest.fn();
for (const impl of callWithRequestImpls) {
mockCallWithRequest.mockImplementationOnce(impl);
}
initPutRolesApi(
mockServer,
mockCallWithRequest,
mockPreCheckLicense,
privilegeMap,
application,
);
const headers = {
authorization: 'foo',
};
const request = {
method: 'PUT',
url: `/api/security/role/${name}`,
headers,
payload,
};
const { result, statusCode } = await mockServer.inject(request);
expect(result).toEqual(asserts.result);
expect(statusCode).toBe(asserts.statusCode);
if (preCheckLicenseImpl) {
expect(mockPreCheckLicense).toHaveBeenCalled();
} else {
expect(mockPreCheckLicense).not.toHaveBeenCalled();
}
if (asserts.callWithRequests) {
for (const args of asserts.callWithRequests) {
expect(mockCallWithRequest).toHaveBeenCalledWith(
expect.objectContaining({
headers: expect.objectContaining({
authorization: headers.authorization,
}),
}),
...args
);
}
} else {
expect(mockCallWithRequest).not.toHaveBeenCalled();
}
});
};
describe('PUT role', () => {
describe('failure', () => {
putRoleTest(`requires name in params`, {
name: '',
payload: {},
asserts: {
statusCode: 404,
result: {
error: 'Not Found',
statusCode: 404,
},
},
});
putRoleTest(`requires name in params to not exceed 1024 characters`, {
name: 'a'.repeat(1025),
payload: {},
asserts: {
statusCode: 400,
result: {
error: 'Bad Request',
message: `child "name" fails because ["name" length must be less than or equal to 1024 characters long]`,
statusCode: 400,
validation: {
keys: ['name'],
source: 'params',
},
},
},
});
putRoleTest(`only allows known Kibana privileges`, {
name: 'foo-role',
payload: {
kibana: [
{
privileges: ['foo']
}
]
},
asserts: {
statusCode: 400,
result: {
error: 'Bad Request',
//eslint-disable-next-line max-len
message: `child "kibana" fails because ["kibana" at position 0 fails because [child "privileges" fails because ["privileges" at position 0 fails because ["0" must be one of [test-kibana-privilege-1, test-kibana-privilege-2, test-kibana-privilege-3]]]]]`,
statusCode: 400,
validation: {
keys: ['kibana.0.privileges.0'],
source: 'payload',
},
},
},
});
putRoleTest(`returns result of routePreCheckLicense`, {
name: 'foo-role',
payload: {},
preCheckLicenseImpl: (request, reply) =>
reply(Boom.forbidden('test forbidden message')),
asserts: {
statusCode: 403,
result: {
error: 'Forbidden',
statusCode: 403,
message: 'test forbidden message',
},
},
});
});
describe('success', () => {
putRoleTest(`creates empty role`, {
name: 'foo-role',
payload: {},
preCheckLicenseImpl: defaultPreCheckLicenseImpl,
callWithRequestImpls: [async () => ({}), async () => {}],
asserts: {
callWithRequests: [
['shield.getRole', { name: 'foo-role', ignore: [404] }],
[
'shield.putRole',
{
name: 'foo-role',
body: {
cluster: [],
indices: [],
run_as: [],
applications: [],
},
},
],
],
statusCode: 204,
result: null,
},
});
putRoleTest(`creates role with everything`, {
name: 'foo-role',
payload: {
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['test-cluster-privilege'],
indices: [
{
field_security: {
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
},
names: ['test-index-name-1', 'test-index-name-2'],
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
query: `{ "match": { "title": "foo" } }`,
},
],
run_as: ['test-run-as-1', 'test-run-as-2'],
},
kibana: [
{
privileges: ['test-kibana-privilege-1', 'test-kibana-privilege-2'],
},
{
privileges: ['test-kibana-privilege-3'],
},
],
},
preCheckLicenseImpl: defaultPreCheckLicenseImpl,
callWithRequestImpls: [async () => ({}), async () => {}],
asserts: {
callWithRequests: [
['shield.getRole', { name: 'foo-role', ignore: [404] }],
[
'shield.putRole',
{
name: 'foo-role',
body: {
applications: [
{
application,
privileges: [
'test-kibana-privilege-1',
'test-kibana-privilege-2',
],
resources: [ALL_RESOURCE],
},
{
application,
privileges: ['test-kibana-privilege-3'],
resources: [ALL_RESOURCE],
},
],
cluster: ['test-cluster-privilege'],
indices: [
{
field_security: {
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
},
names: ['test-index-name-1', 'test-index-name-2'],
privileges: [
'test-index-privilege-1',
'test-index-privilege-2',
],
query: `{ "match": { "title": "foo" } }`,
},
],
metadata: { foo: 'test-metadata' },
run_as: ['test-run-as-1', 'test-run-as-2'],
},
},
],
],
statusCode: 204,
result: null,
},
});
putRoleTest(`updates role which has existing kibana privileges`, {
name: 'foo-role',
payload: {
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['test-cluster-privilege'],
indices: [
{
field_security: {
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
},
names: ['test-index-name-1', 'test-index-name-2'],
privileges: ['test-index-privilege-1', 'test-index-privilege-2'],
query: `{ "match": { "title": "foo" } }`,
},
],
run_as: ['test-run-as-1', 'test-run-as-2'],
},
kibana: [
{
privileges: ['test-kibana-privilege-1', 'test-kibana-privilege-2'],
},
{
privileges: ['test-kibana-privilege-3'],
},
],
},
preCheckLicenseImpl: defaultPreCheckLicenseImpl,
callWithRequestImpls: [
async () => ({
'foo-role': {
metadata: {
bar: 'old-metadata',
},
transient_metadata: {
enabled: true,
},
cluster: ['old-cluster-privilege'],
indices: [
{
field_security: {
grant: ['old-field-security-grant-1', 'old-field-security-grant-2'],
except: [ 'old-field-security-except-1', 'old-field-security-except-2' ]
},
names: ['old-index-name'],
privileges: ['old-privilege'],
query: `{ "match": { "old-title": "foo" } }`,
},
],
run_as: ['old-run-as'],
applications: [
{
application,
privileges: ['old-kibana-privilege'],
resources: ['old-resource'],
},
],
},
}),
async () => {},
],
asserts: {
callWithRequests: [
['shield.getRole', { name: 'foo-role', ignore: [404] }],
[
'shield.putRole',
{
name: 'foo-role',
body: {
applications: [
{
application,
privileges: [
'test-kibana-privilege-1',
'test-kibana-privilege-2',
],
resources: [ALL_RESOURCE],
},
{
application,
privileges: ['test-kibana-privilege-3'],
resources: [ALL_RESOURCE],
},
],
cluster: ['test-cluster-privilege'],
indices: [
{
field_security: {
grant: ['test-field-security-grant-1', 'test-field-security-grant-2'],
except: [ 'test-field-security-except-1', 'test-field-security-except-2' ]
},
names: ['test-index-name-1', 'test-index-name-2'],
privileges: [
'test-index-privilege-1',
'test-index-privilege-2',
],
query: `{ "match": { "title": "foo" } }`,
},
],
metadata: { foo: 'test-metadata' },
run_as: ['test-run-as-1', 'test-run-as-2'],
},
},
],
],
statusCode: 204,
result: null,
},
});
putRoleTest(
`updates role which has existing other application privileges`,
{
name: 'foo-role',
payload: {
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['test-cluster-privilege'],
indices: [
{
names: ['test-index-name-1', 'test-index-name-2'],
privileges: [
'test-index-privilege-1',
'test-index-privilege-2',
],
},
],
run_as: ['test-run-as-1', 'test-run-as-2'],
},
kibana: [
{
privileges: [
'test-kibana-privilege-1',
'test-kibana-privilege-2',
],
},
{
privileges: ['test-kibana-privilege-3'],
},
],
},
preCheckLicenseImpl: defaultPreCheckLicenseImpl,
callWithRequestImpls: [
async () => ({
'foo-role': {
metadata: {
bar: 'old-metadata',
},
transient_metadata: {
enabled: true,
},
cluster: ['old-cluster-privilege'],
indices: [
{
names: ['old-index-name'],
privileges: ['old-privilege'],
},
],
run_as: ['old-run-as'],
applications: [
{
application,
privileges: ['old-kibana-privilege'],
resources: ['old-resource'],
},
{
application: 'logstash-foo',
privileges: ['logstash-privilege'],
resources: ['logstash-resource'],
},
{
application: 'beats-foo',
privileges: ['beats-privilege'],
resources: ['beats-resource'],
},
],
},
}),
async () => {},
],
asserts: {
callWithRequests: [
['shield.getRole', { name: 'foo-role', ignore: [404] }],
[
'shield.putRole',
{
name: 'foo-role',
body: {
applications: [
{
application,
privileges: [
'test-kibana-privilege-1',
'test-kibana-privilege-2',
],
resources: [ALL_RESOURCE],
},
{
application,
privileges: ['test-kibana-privilege-3'],
resources: [ALL_RESOURCE],
},
{
application: 'logstash-foo',
privileges: ['logstash-privilege'],
resources: ['logstash-resource'],
},
{
application: 'beats-foo',
privileges: ['beats-privilege'],
resources: ['beats-resource'],
},
],
cluster: ['test-cluster-privilege'],
indices: [
{
names: ['test-index-name-1', 'test-index-name-2'],
privileges: [
'test-index-privilege-1',
'test-index-privilege-2',
],
},
],
metadata: { foo: 'test-metadata' },
run_as: ['test-run-as-1', 'test-run-as-2'],
},
},
],
],
statusCode: 204,
result: null,
},
}
);
});
});

View file

@ -15,6 +15,7 @@ import { AuthenticationResult } from '../../../../../server/lib/authentication/a
import { BasicCredentials } from '../../../../../server/lib/authentication/providers/basic';
import { initAuthenticateApi } from '../authenticate';
import { DeauthenticationResult } from '../../../../lib/authentication/deauthentication_result';
import { CHECK_PRIVILEGES_RESULT } from '../../../../lib/authorization';
describe('Authentication routes', () => {
let serverStub;
@ -33,6 +34,7 @@ describe('Authentication routes', () => {
let loginRoute;
let request;
let authenticateStub;
let checkPrivilegesWithRequestStub;
beforeEach(() => {
loginRoute = serverStub.route
@ -48,6 +50,7 @@ describe('Authentication routes', () => {
authenticateStub = serverStub.plugins.security.authenticate.withArgs(
sinon.match(BasicCredentials.decorateRequest({ headers: {} }, 'user', 'password'))
);
checkPrivilegesWithRequestStub = serverStub.plugins.security.authorization.checkPrivilegesWithRequest;
});
it('correctly defines route.', async () => {
@ -124,18 +127,65 @@ describe('Authentication routes', () => {
);
});
it('returns user data if authentication succeed.', async () => {
const user = { username: 'user' };
authenticateStub.returns(
Promise.resolve(AuthenticationResult.succeeded(user))
);
describe('authentication succeeds', () => {
const getDeprecationMessage = username =>
`${username} relies on index privileges on the Kibana index. This is deprecated and will be removed in Kibana 7.0`;
await loginRoute.handler(request, replyStub);
it(`returns user data and doesn't log deprecation warning if checkPrivileges result is authorized.`, async () => {
const user = { username: 'user' };
authenticateStub.returns(
Promise.resolve(AuthenticationResult.succeeded(user))
);
const checkPrivilegesStub = sinon.stub().returns({ result: CHECK_PRIVILEGES_RESULT.AUTHORIZED });
checkPrivilegesWithRequestStub.returns(checkPrivilegesStub);
sinon.assert.notCalled(replyStub);
sinon.assert.calledOnce(replyStub.continue);
sinon.assert.calledWithExactly(replyStub.continue, { credentials: user });
await loginRoute.handler(request, replyStub);
sinon.assert.calledWithExactly(checkPrivilegesWithRequestStub, request);
sinon.assert.calledWithExactly(checkPrivilegesStub, [serverStub.plugins.security.authorization.actions.login]);
sinon.assert.neverCalledWith(serverStub.log, ['warning', 'deprecated', 'security'], getDeprecationMessage(user.username));
sinon.assert.notCalled(replyStub);
sinon.assert.calledOnce(replyStub.continue);
sinon.assert.calledWithExactly(replyStub.continue, { credentials: user });
});
it(`returns user data and logs deprecation warning if checkPrivileges result is legacy.`, async () => {
const user = { username: 'user' };
authenticateStub.returns(
Promise.resolve(AuthenticationResult.succeeded(user))
);
const checkPrivilegesStub = sinon.stub().returns({ result: CHECK_PRIVILEGES_RESULT.LEGACY });
checkPrivilegesWithRequestStub.returns(checkPrivilegesStub);
await loginRoute.handler(request, replyStub);
sinon.assert.calledWithExactly(checkPrivilegesWithRequestStub, request);
sinon.assert.calledWithExactly(checkPrivilegesStub, [serverStub.plugins.security.authorization.actions.login]);
sinon.assert.calledWith(serverStub.log, ['warning', 'deprecated', 'security'], getDeprecationMessage(user.username));
sinon.assert.notCalled(replyStub);
sinon.assert.calledOnce(replyStub.continue);
sinon.assert.calledWithExactly(replyStub.continue, { credentials: user });
});
it(`returns user data and doesn't log deprecation warning if checkPrivileges result is unauthorized.`, async () => {
const user = { username: 'user' };
authenticateStub.returns(
Promise.resolve(AuthenticationResult.succeeded(user))
);
const checkPrivilegesStub = sinon.stub().returns({ result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED });
checkPrivilegesWithRequestStub.returns(checkPrivilegesStub);
await loginRoute.handler(request, replyStub);
sinon.assert.calledWithExactly(checkPrivilegesWithRequestStub, request);
sinon.assert.calledWithExactly(checkPrivilegesStub, [serverStub.plugins.security.authorization.actions.login]);
sinon.assert.neverCalledWith(serverStub.log, ['warning', 'deprecated', 'security'], getDeprecationMessage(user.username));
sinon.assert.notCalled(replyStub);
sinon.assert.calledOnce(replyStub.continue);
sinon.assert.calledWithExactly(replyStub.continue, { credentials: user });
});
});
});
describe('logout', () => {

View file

@ -9,8 +9,10 @@ import Joi from 'joi';
import { wrapError } from '../../../lib/errors';
import { BasicCredentials } from '../../../../server/lib/authentication/providers/basic';
import { canRedirectRequest } from '../../../lib/can_redirect_request';
import { CHECK_PRIVILEGES_RESULT } from '../../../../server/lib/authorization';
export function initAuthenticateApi(server) {
server.route({
method: 'POST',
path: '/api/security/v1/login',
@ -35,6 +37,14 @@ export function initAuthenticateApi(server) {
return reply(Boom.unauthorized(authenticationResult.error));
}
const { authorization } = server.plugins.security;
const checkPrivileges = authorization.checkPrivilegesWithRequest(request);
const privilegeCheck = await checkPrivileges([authorization.actions.login]);
if (privilegeCheck.result === CHECK_PRIVILEGES_RESULT.LEGACY) {
const msg = `${username} relies on index privileges on the Kibana index. This is deprecated and will be removed in Kibana 7.0`;
server.log(['warning', 'deprecated', 'security'], msg);
}
return reply.continue({ credentials: authenticationResult.user });
} catch(err) {
return reply(wrapError(err));

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { buildPrivilegeMap } from '../../../lib/authorization';
export function initPrivilegesApi(server) {
const { authorization } = server.plugins.security;
const savedObjectTypes = server.savedObjects.types;
server.route({
method: 'GET',
path: '/api/security/v1/privileges',
handler(request, reply) {
// we're returning our representation of the privileges, as opposed to the ones that are stored
// in Elasticsearch because our current thinking is that we'll associate additional structure/metadata
// with our view of them to allow users to more efficiently edit privileges for roles, and serialize it
// into a different structure for enforcement within Elasticsearch
const privileges = buildPrivilegeMap(savedObjectTypes, authorization.application, authorization.actions);
reply(Object.values(privileges));
}
});
}

View file

@ -1,83 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import _ from 'lodash';
import Boom from 'boom';
import { getClient } from '../../../../../../server/lib/get_client_shield';
import { roleSchema } from '../../../lib/role_schema';
import { wrapError } from '../../../lib/errors';
import { routePreCheckLicense } from '../../../lib/route_pre_check_license';
export function initRolesApi(server) {
const callWithRequest = getClient(server).callWithRequest;
const routePreCheckLicenseFn = routePreCheckLicense(server);
server.route({
method: 'GET',
path: '/api/security/v1/roles',
handler(request, reply) {
return callWithRequest(request, 'shield.getRole').then(
(response) => {
const roles = _.map(response, (role, name) => _.assign(role, { name }));
return reply(roles);
},
_.flow(wrapError, reply)
);
},
config: {
pre: [routePreCheckLicenseFn]
}
});
server.route({
method: 'GET',
path: '/api/security/v1/roles/{name}',
handler(request, reply) {
const name = request.params.name;
return callWithRequest(request, 'shield.getRole', { name }).then(
(response) => {
if (response[name]) return reply(_.assign(response[name], { name }));
return reply(Boom.notFound());
},
_.flow(wrapError, reply));
},
config: {
pre: [routePreCheckLicenseFn]
}
});
server.route({
method: 'POST',
path: '/api/security/v1/roles/{name}',
handler(request, reply) {
const name = request.params.name;
const body = _.omit(request.payload, 'name');
return callWithRequest(request, 'shield.putRole', { name, body }).then(
() => reply(request.payload),
_.flow(wrapError, reply));
},
config: {
validate: {
payload: roleSchema
},
pre: [routePreCheckLicenseFn]
}
});
server.route({
method: 'DELETE',
path: '/api/security/v1/roles/{name}',
handler(request, reply) {
const name = request.params.name;
return callWithRequest(request, 'shield.deleteRole', { name }).then(
() => reply().code(204),
_.flow(wrapError, reply));
},
config: {
pre: [routePreCheckLicenseFn]
}
});
}

View file

@ -67,7 +67,7 @@ describe('XPackInfo', () => {
mockServer = sinon.stub({
plugins: { elasticsearch: mockElasticsearchPlugin },
log() {}
log() { }
});
});
@ -151,9 +151,9 @@ describe('XPackInfo', () => {
expect(xPackInfo.unavailableReason()).to.be(randomError);
sinon.assert.calledWithExactly(
mockServer.log,
[ 'license', 'warning', 'xpack' ],
['license', 'warning', 'xpack'],
`License information from the X-Pack plugin could not be obtained from Elasticsearch` +
` for the [data] cluster. ${randomError}`
` for the [data] cluster. ${randomError}`
);
const badRequestError = new Error('Bad request');
@ -168,9 +168,9 @@ describe('XPackInfo', () => {
);
sinon.assert.calledWithExactly(
mockServer.log,
[ 'license', 'warning', 'xpack' ],
['license', 'warning', 'xpack'],
`License information from the X-Pack plugin could not be obtained from Elasticsearch` +
` for the [data] cluster. ${badRequestError}`
` for the [data] cluster. ${badRequestError}`
);
mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse());

View file

@ -36,6 +36,7 @@ export class XPackInfo {
*/
_licenseInfoChangedListeners = new Set();
/**
* Cache that may contain last xpack info API response or error, json representation
* of xpack info and xpack info signature.
@ -150,7 +151,7 @@ export class XPackInfo {
this._log(
['license', 'info', 'xpack'],
`Imported ${this._cache.response ? 'changed ' : ''}license information` +
` from Elasticsearch for the [${this._clusterSource}] cluster: ${licenseInfo}`
` from Elasticsearch for the [${this._clusterSource}] cluster: ${licenseInfo}`
);
}
@ -165,9 +166,9 @@ export class XPackInfo {
} catch(error) {
this._log(
[ 'license', 'warning', 'xpack' ],
['license', 'warning', 'xpack'],
`License information from the X-Pack plugin could not be obtained from Elasticsearch` +
` for the [${this._clusterSource}] cluster. ${error}`
` for the [${this._clusterSource}] cluster. ${error}`
);
this._cache = { error };

View file

@ -14,4 +14,5 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/functional/config.js'),
require.resolve('../test/api_integration/config.js'),
require.resolve('../test/saml_api_integration/config.js'),
require.resolve('../test/rbac_api_integration/config.js'),
]);

View file

@ -361,5 +361,36 @@
fmt: '/_xpack/security/oauth2/token'
}
});
shield.getPrivilege = ca({
method: 'GET',
urls: [{
fmt: '/_xpack/security/privilege/<%=privilege%>',
req: {
privilege: {
type: 'string',
required: false
}
}
}, {
fmt: '/_xpack/security/privilege'
}]
});
shield.postPrivileges = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_xpack/security/privilege'
}
});
shield.hasPrivileges = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_xpack/security/user/_has_privileges'
}
});
};
}));

View file

@ -7,5 +7,6 @@
export default function ({ loadTestFile }) {
describe('security', () => {
loadTestFile(require.resolve('./basic_login'));
loadTestFile(require.resolve('./roles'));
});
}

View file

@ -0,0 +1,221 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
export default function ({ getService }) {
const es = getService('es');
const supertest = getService('supertest');
describe('Roles', () => {
describe('Create Role', () => {
it('should allow us to create an empty role', async () => {
await supertest.put('/api/security/role/empty_role')
.set('kbn-xsrf', 'xxx')
.send({})
.expect(204);
});
it('should create a role with kibana and elasticsearch privileges', async () => {
await supertest.put('/api/security/role/role_with_privileges')
.set('kbn-xsrf', 'xxx')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
field_security: {
grant: ['*'],
except: [ 'geo.*' ]
},
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
query: `{ "match": { "geo.src": "CN" } }`,
},
],
run_as: ['watcher_user'],
},
kibana: [
{
privileges: ['all'],
},
{
privileges: ['read'],
},
],
})
.expect(204);
const role = await es.shield.getRole({ name: 'role_with_privileges' });
expect(role).to.eql({
role_with_privileges: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
field_security: {
grant: ['*'],
except: [ 'geo.*' ]
},
query: `{ "match": { "geo.src": "CN" } }`,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['all'],
resources: ['*'],
},
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
}
],
run_as: ['watcher_user'],
metadata: {
foo: 'test-metadata',
},
transient_metadata: {
enabled: true,
},
}
});
});
});
describe('Update Role', () => {
it('should update a role with elasticsearch, kibana and other applications privileges', async () => {
await es.shield.putRole({
name: 'role_to_update',
body: {
cluster: ['monitor'],
indices: [
{
names: ['beats-*'],
privileges: ['write'],
field_security: {
grant: [ 'request.*' ],
except: [ 'response.*' ]
},
query: `{ "match": { "host.name": "localhost" } }`,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'logstash-default',
privileges: ['logstash-privilege'],
resources: ['*'],
},
],
run_as: ['reporting_user'],
metadata: {
bar: 'old-metadata',
},
}
});
await supertest.put('/api/security/role/role_to_update')
.set('kbn-xsrf', 'xxx')
.send({
metadata: {
foo: 'test-metadata',
},
elasticsearch: {
cluster: ['manage'],
indices: [
{
field_security: {
grant: ['*'],
except: [ 'geo.*' ]
},
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
query: `{ "match": { "geo.src": "CN" } }`,
},
],
run_as: ['watcher_user'],
},
kibana: [
{
privileges: ['all'],
},
{
privileges: ['read'],
},
],
})
.expect(204);
const role = await es.shield.getRole({ name: 'role_to_update' });
expect(role).to.eql({
role_to_update: {
cluster: ['manage'],
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
field_security: {
grant: ['*'],
except: [ 'geo.*' ]
},
query: `{ "match": { "geo.src": "CN" } }`,
},
],
applications: [
{
application: 'kibana-.kibana',
privileges: ['all'],
resources: ['*'],
},
{
application: 'kibana-.kibana',
privileges: ['read'],
resources: ['*'],
},
{
application: 'logstash-default',
privileges: ['logstash-privilege'],
resources: ['*'],
},
],
run_as: ['watcher_user'],
metadata: {
foo: 'test-metadata',
},
transient_metadata: {
enabled: true,
},
}
});
});
});
describe('Delete Role', () => {
it('should delete the three roles we created', async () => {
await supertest.delete('/api/security/role/empty_role').set('kbn-xsrf', 'xxx').expect(204);
await supertest.delete('/api/security/role/role_with_privileges').set('kbn-xsrf', 'xxx').expect(204);
await supertest.delete('/api/security/role/role_to_update').set('kbn-xsrf', 'xxx').expect(204);
const emptyRole = await es.shield.getRole({ name: 'empty_role', ignore: [404] });
expect(emptyRole).to.eql({});
const roleWithPrivileges = await es.shield.getRole({ name: 'role_with_privileges', ignore: [404] });
expect(roleWithPrivileges).to.eql({});
const roleToUpdate = await es.shield.getRole({ name: 'role_to_update', ignore: [404] });
expect(roleToUpdate).to.eql({});
});
});
});
}

View file

@ -5,6 +5,8 @@
*/
import {
EsProvider,
EsSupertestWithoutAuthProvider,
SupertestWithoutAuthProvider,
UsageAPIProvider,
} from './services';
@ -22,7 +24,8 @@ export default async function ({ readConfigFile }) {
supertest: kibanaAPITestsConfig.get('services.supertest'),
esSupertest: kibanaAPITestsConfig.get('services.esSupertest'),
supertestWithoutAuth: SupertestWithoutAuthProvider,
es: kibanaCommonConfig.get('services.es'),
esSupertestWithoutAuth: EsSupertestWithoutAuthProvider,
es: EsProvider,
esArchiver: kibanaCommonConfig.get('services.esArchiver'),
usageAPI: UsageAPIProvider,
kibanaServer: kibanaCommonConfig.get('services.kibanaServer'),

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { format as formatUrl } from 'url';
import elasticsearch from 'elasticsearch';
import shieldPlugin from '../../../server/lib/esjs_shield_plugin';
export function EsProvider({ getService }) {
const config = getService('config');
return new elasticsearch.Client({
host: formatUrl(config.get('servers.elasticsearch')),
requestTimeout: config.get('timeouts.esRequestTimeout'),
plugins: [shieldPlugin]
});
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { format as formatUrl } from 'url';
import supertestAsPromised from 'supertest-as-promised';
/**
* Supertest provider that doesn't include user credentials into base URL that is passed
* to the supertest.
*/
export function EsSupertestWithoutAuthProvider({ getService }) {
const config = getService('config');
const elasticsearchServerConfig = config.get('servers.elasticsearch');
return supertestAsPromised(formatUrl({
...elasticsearchServerConfig,
auth: false
}));
}

View file

@ -4,5 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { EsProvider } from './es';
export { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth';
export { SupertestWithoutAuthProvider } from './supertest_without_auth';
export { UsageAPIProvider } from './usage_api';

View file

@ -33,6 +33,8 @@ export default function ({ getService, getPageObjects }) {
// start on cluster overview
await PageObjects.monitoring.clickBreadcrumb('breadcrumbClusters');
await PageObjects.header.waitUntilLoadingHasFinished();
// go to nodes listing
await overview.clickEsNodes();
expect(await nodesList.isOnListing()).to.be(true);

View file

@ -12,5 +12,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./users'));
loadTestFile(require.resolve('./secure_roles_perm'));
loadTestFile(require.resolve('./field_level_security'));
loadTestFile(require.resolve('./rbac_phase1'));
});
}

View file

@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { indexBy } from 'lodash';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['security', 'settings', 'common', 'visualize', 'header']);
const log = getService('log');
const esArchiver = getService('esArchiver');
const remote = getService('remote');
const kibanaServer = getService('kibanaServer');
describe.skip('rbac ', async function () {
before(async () => {
await remote.setWindowSize(1600, 1000);
log.debug('users');
await esArchiver.loadIfNeeded('logstash_functional');
log.debug('load kibana index with default index pattern');
await esArchiver.load('discover');
await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC', 'defaultIndex': 'logstash-*' });
await PageObjects.settings.navigateTo();
await PageObjects.security.clickElasticsearchRoles();
await PageObjects.security.addRole('rbac_all', {
"kibana": ["all"],
"indices": [{
"names": [ "logstash-*" ],
"privileges": [ "read", "view_index_metadata" ]
}]
});
await PageObjects.security.clickElasticsearchRoles();
await PageObjects.security.addRole('rbac_read', {
"kibana": ["read"],
"indices": [{
"names": [ "logstash-*" ],
"privileges": [ "read", "view_index_metadata" ]
}]
});
await PageObjects.security.clickElasticsearchUsers();
log.debug('After Add user new: , userObj.userName');
await PageObjects.security.addUser({ username: 'kibanauser', password: 'changeme',
confirmPassword: 'changeme', fullname: 'kibanafirst kibanalast',
email: 'kibanauser@myEmail.com', save: true,
roles: ['rbac_all'] });
log.debug('After Add user: , userObj.userName');
const users = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username');
log.debug('actualUsers = %j', users);
log.debug('roles: ', users.kibanauser.roles);
expect(users.kibanauser.roles).to.eql(['rbac_all']);
expect(users.kibanauser.fullname).to.eql('kibanafirst kibanalast');
expect(users.kibanauser.reserved).to.be(false);
await PageObjects.security.clickElasticsearchUsers();
log.debug('After Add user new: , userObj.userName');
await PageObjects.security.addUser({ username: 'kibanareadonly', password: 'changeme',
confirmPassword: 'changeme', fullname: 'kibanareadonlyFirst kibanareadonlyLast',
email: 'kibanareadonly@myEmail.com', save: true,
roles: ['rbac_read'] });
log.debug('After Add user: , userObj.userName');
const users1 = indexBy(await PageObjects.security.getElasticsearchUsers(), 'username');
const user = users1.kibanareadonly;
log.debug('actualUsers = %j', users1);
log.debug('roles: ', user.roles);
expect(user.roles).to.eql(['rbac_read']);
expect(user.fullname).to.eql('kibanareadonlyFirst kibanareadonlyLast');
expect(user.reserved).to.be(false);
await PageObjects.security.logout();
});
// this is to acertain that all role assigned to the user can perform actions like creating a Visualization
it('rbac all role can save a visualization', async function () {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-23 18:31:44.000';
const vizName1 = 'Visualization VerticalBarChart';
log.debug('navigateToApp visualize');
await PageObjects.security.login('kibanauser', 'changeme');
await PageObjects.common.navigateToUrl('visualize', 'new');
log.debug('clickVerticalBarChart');
await PageObjects.visualize.clickVerticalBarChart();
await PageObjects.visualize.clickNewSearch();
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.visualize.clickGo();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.visualize.waitForVisualization();
const success = await PageObjects.visualize.saveVisualization(vizName1);
expect(success).to.be(true);
await PageObjects.security.logout();
});
it('rbac read only role can not save a visualization', async function () {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-23 18:31:44.000';
const vizName1 = 'Viz VerticalBarChart';
log.debug('navigateToApp visualize');
await PageObjects.security.login('kibanareadonly', 'changeme');
await PageObjects.common.navigateToUrl('visualize', 'new');
log.debug('clickVerticalBarChart');
await PageObjects.visualize.clickVerticalBarChart();
await PageObjects.visualize.clickNewSearch();
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.visualize.clickGo();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.visualize.waitForVisualization();
const success = await PageObjects.visualize.saveVisualization(vizName1);
expect(success).to.be(false);
await PageObjects.security.logout();
});
after(async function () {
await PageObjects.security.logout();
});
});
}

View file

@ -18,7 +18,7 @@ export default function ({ getService, getPageObjects }) {
describe('security', function () {
describe('secure roles and permissions', function () {
before(async () => {
await remote.setWindowSize(1600, 1000);
log.debug('users');

View file

@ -176,18 +176,13 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
const fullnameElement = await user.findByCssSelector('[data-test-subj="userRowFullName"]');
const usernameElement = await user.findByCssSelector('[data-test-subj="userRowUserName"]');
const rolesElement = await user.findByCssSelector('[data-test-subj="userRowRoles"]');
let reserved = false;
try {
reserved = !!(await user.findByCssSelector('[data-test-subj="reservedUser"]'));
} catch(e) {
//ignoring, just means user is not reserved
}
const isReservedElementVisible = await user.findByCssSelector('td:last-child');
return {
username: await usernameElement.getVisibleText(),
fullname: await fullnameElement.getVisibleText(),
roles: (await rolesElement.getVisibleText()).split(',').map(role => role.trim()),
reserved
reserved: (await isReservedElementVisible.getProperty('innerHTML')).includes('reservedUser')
};
});
}
@ -258,6 +253,31 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
return testSubjects.setValue('queryInput0', userObj.indices[0].query);
}
})
//KibanaPriv
.then(function () {
function addKibanaPriv(priv) {
return priv.reduce(function (promise, privName) {
// We have to use non-test-subject selectors because this markup is generated by ui-select.
return promise
.then(function () {
log.debug('priv item = ' + privName);
remote.setFindTimeout(defaultFindTimeout)
.findByCssSelector(`[data-test-subj="kibanaPrivileges-${privName}"]`)
.click();
})
.then(function () {
return PageObjects.common.sleep(500);
});
}, Promise.resolve());
}
return userObj.kibana ? addKibanaPriv(userObj.kibana) : Promise.resolve();
})
.then(function () {
function addPriv(priv) {

View file

@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
const application = 'has_privileges_test';
export default function ({ getService }) {
describe('has_privileges', () => {
before(async () => {
const es = getService('es');
await es.shield.postPrivileges({
body: {
[application]: {
read: {
application,
name: 'read',
actions: ['action:readAction1', 'action:readAction2'],
metadata: {},
}
}
}
});
await es.shield.putRole({
name: 'hp_read_user',
body: {
cluster: [],
index: [],
applications: [{
application,
privileges: ['read'],
resources: ['*']
}]
}
});
await es.shield.putUser({
username: 'testuser',
body: {
password: 'testpassword',
roles: ['hp_read_user'],
full_name: 'a kibana user',
email: 'a_kibana_rbac_user@elastic.co',
}
});
});
function createHasPrivilegesRequest(privileges) {
const supertest = getService('esSupertestWithoutAuth');
return supertest
.post(`/_xpack/security/user/_has_privileges`)
.auth('testuser', 'testpassword')
.send({
applications: [{
application,
privileges,
resources: ['*']
}]
})
.expect(200);
}
it('should return true when user has the requested privilege', async () => {
await createHasPrivilegesRequest(['read'])
.then(response => {
expect(response.body).to.eql({
username: 'testuser',
has_all_requested: true,
cluster: {},
index: {},
application: {
has_privileges_test: {
['*']: {
read: true
}
},
}
});
});
});
it('should return true when user has a newly created privilege', async () => {
// verify user does not have privilege yet
await createHasPrivilegesRequest(['action:a_new_privilege'])
.then(response => {
expect(response.body).to.eql({
username: 'testuser',
has_all_requested: false,
cluster: {},
index: {},
application: {
has_privileges_test: {
['*']: {
'action:a_new_privilege': false
}
},
}
});
});
// Create privilege
const es = getService('es');
await es.shield.postPrivileges({
body: {
[application]: {
read: {
application,
name: 'read',
actions: ['action:readAction1', 'action:readAction2', 'action:a_new_privilege'],
metadata: {},
}
}
}
});
// verify user has new privilege
await createHasPrivilegesRequest(['action:a_new_privilege'])
.then(response => {
expect(response.body).to.eql({
username: 'testuser',
has_all_requested: true,
cluster: {},
index: {},
application: {
has_privileges_test: {
['*']: {
'action:a_new_privilege': true
}
},
}
});
});
});
});
}

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export default function ({ loadTestFile }) {
describe('rbac es', () => {
loadTestFile(require.resolve('./has_privileges'));
loadTestFile(require.resolve('./post_privileges'));
});
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
export default function ({ getService }) {
describe('post_privileges', () => {
it('should allow privileges to be updated', async () => {
const es = getService('es');
const application = 'foo';
const response = await es.shield.postPrivileges({
body: {
[application]: {
all: {
application,
name: 'all',
actions: ['action:*'],
metadata: {},
},
read: {
application,
name: 'read',
actions: ['action:readAction1', 'action:readAction2'],
metadata: {},
}
}
}
});
expect(response).to.eql({
foo: {
all: { created: true },
read: { created: true }
}
});
// Update privileges:
// 1. Not specifying the "all" privilege that we created above
// 2. Specifying a different collection of "read" actions
// 3. Adding a new "other" privilege
const updateResponse = await es.shield.postPrivileges({
body: {
[application]: {
read: {
application,
name: 'read',
actions: ['action:readAction1', 'action:readAction4'],
metadata: {}
},
other: {
application,
name: 'other',
actions: ['action:otherAction1'],
metadata: {},
}
}
}
});
expect(updateResponse).to.eql({
foo: {
other: { created: true },
read: { created: false }
}
});
const retrievedPrivilege = await es.shield.getPrivilege({ privilege: application });
expect(retrievedPrivilege).to.eql({
foo: {
// "all" is maintained even though the subsequent update did not specify this privilege
all: {
application,
name: 'all',
actions: ['action:*'],
metadata: {},
},
read: {
application,
name: 'read',
// actions should only contain what was present in the update. The original actions are not persisted or merged here.
actions: ['action:readAction1', 'action:readAction4'],
metadata: {},
},
other: {
application,
name: 'other',
actions: ['action:otherAction1'],
metadata: {},
}
}
});
});
});
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export default function ({ loadTestFile }) {
describe('apis RBAC', () => {
loadTestFile(require.resolve('./es'));
loadTestFile(require.resolve('./privileges'));
loadTestFile(require.resolve('./saved_objects'));
});
}

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
export default function ({ getService }) {
describe('privileges', () => {
it(`get should return privileges`, async () => {
const supertest = getService('supertest');
const kibanaServer = getService('kibanaServer');
const version = await kibanaServer.version.get();
await supertest
.get(`/api/security/v1/privileges`)
.expect(200)
.then(resp => {
expect(resp.body).to.eql([
{
application: 'kibana-.kibana',
name: 'all',
actions: [`version:${version}`, 'action:*'],
metadata: {},
},
{
application: 'kibana-.kibana',
name: 'read',
actions: [
`version:${version}`,
'action:login',
'action:saved_objects/config/get',
'action:saved_objects/config/bulk_get',
'action:saved_objects/config/find',
'action:saved_objects/timelion-sheet/get',
'action:saved_objects/timelion-sheet/bulk_get',
'action:saved_objects/timelion-sheet/find',
'action:saved_objects/graph-workspace/get',
'action:saved_objects/graph-workspace/bulk_get',
'action:saved_objects/graph-workspace/find',
'action:saved_objects/index-pattern/get',
'action:saved_objects/index-pattern/bulk_get',
'action:saved_objects/index-pattern/find',
'action:saved_objects/visualization/get',
'action:saved_objects/visualization/bulk_get',
'action:saved_objects/visualization/find',
'action:saved_objects/search/get',
'action:saved_objects/search/bulk_get',
'action:saved_objects/search/find',
'action:saved_objects/dashboard/get',
'action:saved_objects/dashboard/bulk_get',
'action:saved_objects/dashboard/find',
'action:saved_objects/url/get',
'action:saved_objects/url/bulk_get',
'action:saved_objects/url/find',
'action:saved_objects/server/get',
'action:saved_objects/server/bulk_get',
'action:saved_objects/server/find',
],
metadata: {},
},
]);
});
});
});
}

View file

@ -0,0 +1,201 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';
export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
const BULK_REQUESTS = [
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
},
{
type: 'dashboard',
id: 'does not exist',
},
{
type: 'config',
id: '7.0.0-alpha1',
},
];
describe('_bulk_get', () => {
const expectResults = resp => {
expect(resp.body).to.eql({
saved_objects: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.saved_objects[0].version,
attributes: {
title: 'Count of requests',
description: '',
version: 1,
// cheat for some of the more complex attributes
visState: resp.body.saved_objects[0].attributes.visState,
uiStateJSON: resp.body.saved_objects[0].attributes.uiStateJSON,
kibanaSavedObjectMeta:
resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta,
},
},
{
id: 'does not exist',
type: 'dashboard',
error: {
statusCode: 404,
message: 'Not found',
},
},
{
id: '7.0.0-alpha1',
type: 'config',
updated_at: '2017-09-21T18:49:16.302Z',
version: resp.body.saved_objects[2].version,
attributes: {
buildNum: 8467,
defaultIndex: '91200a00-9efd-11e7-acb3-3dab96693fab',
},
},
],
});
};
const expectRbacForbidden = resp => {
//eslint-disable-next-line max-len
const missingActions = `action:saved_objects/config/bulk_get,action:saved_objects/dashboard/bulk_get,action:saved_objects/visualization/bulk_get`;
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to bulk_get config,dashboard,visualization, missing ${missingActions}`
});
};
const bulkGetTest = (description, { auth, tests }) => {
describe(description, () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.default.statusCode}`, async () => {
await supertest
.post(`/api/saved_objects/_bulk_get`)
.auth(auth.username, auth.password)
.send(BULK_REQUESTS)
.expect(tests.default.statusCode)
.then(tests.default.response);
});
});
};
bulkGetTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
}
}
});
bulkGetTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
bulkGetTest(`kibana legacy user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
bulkGetTest(`kibana legacy dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
bulkGetTest(`kibana dual-privileges user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
bulkGetTest(`kibana dual-privileges dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
bulkGetTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
bulkGetTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
});
}

View file

@ -0,0 +1,172 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';
export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe('create', () => {
const expectResults = (resp) => {
expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/);
// loose ISO8601 UTC time with milliseconds validation
expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
expect(resp.body).to.eql({
id: resp.body.id,
type: 'visualization',
updated_at: resp.body.updated_at,
version: 1,
attributes: {
title: 'My favorite vis'
}
});
};
const expectRbacForbidden = resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to create visualization, missing action:saved_objects/visualization/create`
});
};
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'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.default.statusCode}`, async () => {
await supertest
.post(`/api/saved_objects/visualization`)
.auth(auth.username, auth.password)
.send({
attributes: {
title: 'My favorite vis'
}
})
.expect(tests.default.statusCode)
.then(tests.default.response);
});
});
};
createTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
createTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
createTest(`kibana legacy user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
createTest(`kibana legacy dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME),
},
}
});
createTest(`kibana dual-privileges user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
createTest(`kibana dual-privileges dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
createTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
default: {
statusCode: 200,
response: expectResults,
},
}
});
createTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
default: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
});
}

View file

@ -0,0 +1,204 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';
export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe('delete', () => {
const expectEmpty = (resp) => {
expect(resp.body).to.eql({});
};
const expectNotFound = (resp) => {
expect(resp.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Saved object [dashboard/not-a-real-id] not found'
});
};
const expectRbacForbidden = resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to delete dashboard, missing action:saved_objects/dashboard/delete`
});
};
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'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.actualId.statusCode} when deleting a doc`, async () => (
await supertest
.delete(`/api/saved_objects/dashboard/be3733a0-9efe-11e7-acb3-3dab96693fab`)
.auth(auth.username, auth.password)
.expect(tests.actualId.statusCode)
.then(tests.actualId.response)
));
it(`should return ${tests.invalidId.statusCode} when deleting an unknown doc`, async () => (
await supertest
.delete(`/api/saved_objects/dashboard/not-a-real-id`)
.auth(auth.username, auth.password)
.expect(tests.invalidId.statusCode)
.then(tests.invalidId.response)
));
});
};
deleteTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 403,
response: expectRbacForbidden,
},
invalidId: {
statusCode: 403,
response: expectRbacForbidden,
}
}
});
deleteTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
actualId: {
statusCode: 200,
response: expectEmpty,
},
invalidId: {
statusCode: 404,
response: expectNotFound,
}
}
});
deleteTest(`kibana legacy user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 200,
response: expectEmpty,
},
invalidId: {
statusCode: 404,
response: expectNotFound,
}
}
});
deleteTest(`kibana legacy dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 403,
response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME),
},
invalidId: {
statusCode: 403,
response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME),
}
}
});
deleteTest(`kibana dual-privileges user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 200,
response: expectEmpty,
},
invalidId: {
statusCode: 404,
response: expectNotFound,
}
}
});
deleteTest(`kibana dual-privileges dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 403,
response: expectRbacForbidden,
},
invalidId: {
statusCode: 403,
response: expectRbacForbidden,
}
}
});
deleteTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 200,
response: expectEmpty,
},
invalidId: {
statusCode: 404,
response: expectNotFound,
}
}
});
deleteTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
actualId: {
statusCode: 403,
response: expectRbacForbidden,
},
invalidId: {
statusCode: 403,
response: expectRbacForbidden,
}
}
});
});
}

View file

@ -0,0 +1,468 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';
export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe('find', () => {
const expectVisualizationResults = (resp) => {
expect(resp.body).to.eql({
page: 1,
per_page: 20,
total: 1,
saved_objects: [
{
type: 'visualization',
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
version: 1,
attributes: {
'title': 'Count of requests'
}
}
]
});
};
const expectResultsWithValidTypes = (resp) => {
expect(resp.body).to.eql({
page: 1,
per_page: 20,
total: 4,
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
},
]
});
};
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,
per_page: perPage,
total: total,
saved_objects: []
});
};
const createExpectRbacForbidden = (type) => resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to find ${type}, missing action:saved_objects/${type}/find`
});
};
const expectForbiddenCantFindAnyTypes = resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Not authorized to find saved_object`
});
};
const findTest = (description, { auth, tests }) => {
describe(description, () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.normal.statusCode} with ${tests.normal.description}`, async () => (
await supertest
.get('/api/saved_objects/_find?type=visualization&fields=title')
.auth(auth.username, auth.password)
.expect(tests.normal.statusCode)
.then(tests.normal.response)
));
describe('unknown type', () => {
it(`should return ${tests.unknownType.statusCode} with ${tests.unknownType.description}`, async () => (
await supertest
.get('/api/saved_objects/_find?type=wigwags')
.auth(auth.username, auth.password)
.expect(tests.unknownType.statusCode)
.then(tests.unknownType.response)
));
});
describe('page beyond total', () => {
it(`should return ${tests.pageBeyondTotal.statusCode} with ${tests.pageBeyondTotal.description}`, async () => (
await supertest
.get('/api/saved_objects/_find?type=visualization&page=100&per_page=100')
.auth(auth.username, auth.password)
.expect(tests.pageBeyondTotal.statusCode)
.then(tests.pageBeyondTotal.response)
));
});
describe('unknown search field', () => {
it(`should return ${tests.unknownSearchField.statusCode} with ${tests.unknownSearchField.description}`, async () => (
await supertest
.get('/api/saved_objects/_find?type=wigwags&search_fields=a')
.auth(auth.username, auth.password)
.expect(tests.unknownSearchField.statusCode)
.then(tests.unknownSearchField.response)
));
});
describe('no type', () => {
it(`should return ${tests.noType.statusCode} with ${tests.noType.description}`, async () => (
await supertest
.get('/api/saved_objects/_find')
.auth(auth.username, auth.password)
.expect(tests.noType.statusCode)
.then(tests.noType.response)
));
});
});
};
findTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
normal: {
description: 'forbidden login and find visualization message',
statusCode: 403,
response: createExpectRbacForbidden('visualization'),
},
unknownType: {
description: 'forbidden login and find wigwags message',
statusCode: 403,
response: createExpectRbacForbidden('wigwags'),
},
pageBeyondTotal: {
description: 'forbidden login and find visualization message',
statusCode: 403,
response: createExpectRbacForbidden('visualization'),
},
unknownSearchField: {
description: 'forbidden login and find wigwags message',
statusCode: 403,
response: createExpectRbacForbidden('wigwags'),
},
noType: {
description: `forbidded can't find any types`,
statusCode: 403,
response: expectForbiddenCantFindAnyTypes,
}
}
});
findTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
},
},
});
findTest(`kibana legacy user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectAllResultsIncludingInvalidTypes,
},
},
});
findTest(`kibana legacy dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectAllResultsIncludingInvalidTypes,
},
}
});
findTest(`kibana dual-privileges user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
},
},
});
findTest(`kibana dual-privileges dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'forbidden find wigwags message',
statusCode: 403,
response: createExpectRbacForbidden('wigwags'),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'forbidden find wigwags message',
statusCode: 403,
response: createExpectRbacForbidden('wigwags'),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
},
}
});
findTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(1, 20, 0),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
},
},
});
findTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
normal: {
description: 'only the visualization',
statusCode: 200,
response: expectVisualizationResults,
},
unknownType: {
description: 'forbidden find wigwags message',
statusCode: 403,
response: createExpectRbacForbidden('wigwags'),
},
pageBeyondTotal: {
description: 'empty result',
statusCode: 200,
response: createExpectEmpty(100, 100, 1),
},
unknownSearchField: {
description: 'forbidden find wigwags message',
statusCode: 403,
response: createExpectRbacForbidden('wigwags'),
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
},
}
});
});
}

View file

@ -0,0 +1,211 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';
export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe('get', () => {
const expectResults = (resp) => {
expect(resp.body).to.eql({
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.version,
attributes: {
title: 'Count of requests',
description: '',
version: 1,
// cheat for some of the more complex attributes
visState: resp.body.attributes.visState,
uiStateJSON: resp.body.attributes.uiStateJSON,
kibanaSavedObjectMeta: resp.body.attributes.kibanaSavedObjectMeta
}
});
};
const expectNotFound = (resp) => {
expect(resp.body).to.eql({
error: 'Not Found',
message: 'Saved object [visualization/foobar] not found',
statusCode: 404,
});
};
const expectRbacForbidden = resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to get visualization, missing action:saved_objects/visualization/get`
});
};
const getTest = (description, { auth, tests }) => {
describe(description, () => {
before(() => esArchiver.load('saved_objects/basic'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.exists.statusCode}`, async () => (
await supertest
.get(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
.auth(auth.username, auth.password)
.expect(tests.exists.statusCode)
.then(tests.exists.response)
));
describe('document does not exist', () => {
it(`should return ${tests.doesntExist.statusCode}`, async () => (
await supertest
.get(`/api/saved_objects/visualization/foobar`)
.auth(auth.username, auth.password)
.expect(tests.doesntExist.statusCode)
.then(tests.doesntExist.response)
));
});
});
};
getTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 403,
response: expectRbacForbidden,
},
doesntExist: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
getTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
getTest(`kibana legacy user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
getTest(`kibana legacy dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
getTest(`kibana dual-privileges user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
getTest(`kibana dual-privileges dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
getTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
getTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
});
}

View file

@ -0,0 +1,160 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AUTHENTICATION } from "./lib/authentication";
export default function ({ loadTestFile, getService }) {
const es = getService('es');
const supertest = getService('supertest');
describe('saved_objects', () => {
before(async () => {
await supertest.put('/api/security/role/kibana_legacy_user')
.send({
elasticsearch: {
indices: [{
names: ['.kibana'],
privileges: ['manage', 'read', 'index', 'delete']
}]
}
});
await supertest.put('/api/security/role/kibana_legacy_dashboard_only_user')
.send({
elasticsearch: {
indices: [{
names: ['.kibana'],
privileges: ['read', 'view_index_metadata']
}]
}
});
await supertest.put('/api/security/role/kibana_dual_privileges_user')
.send({
elasticsearch: {
indices: [{
names: ['.kibana'],
privileges: ['manage', 'read', 'index', 'delete']
}]
},
kibana: [
{
privileges: ['all']
}
]
});
await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user')
.send({
elasticsearch: {
indices: [{
names: ['.kibana'],
privileges: ['read', 'view_index_metadata']
}]
},
kibana: [
{
privileges: ['read']
}
]
});
await supertest.put('/api/security/role/kibana_rbac_user')
.send({
kibana: [
{
privileges: ['all']
}
]
});
await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user')
.send({
kibana: [
{
privileges: ['read']
}
]
});
await es.shield.putUser({
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
body: {
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
roles: [],
full_name: 'not a kibana user',
email: 'not_a_kibana_user@elastic.co',
}
});
await es.shield.putUser({
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
body: {
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
roles: ['kibana_legacy_user'],
full_name: 'a kibana legacy user',
email: 'a_kibana_legacy_user@elastic.co',
}
});
await es.shield.putUser({
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
body: {
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
roles: ["kibana_legacy_dashboard_only_user"],
full_name: 'a kibana legacy dashboard only user',
email: 'a_kibana_legacy_dashboard_only_user@elastic.co',
}
});
await es.shield.putUser({
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
body: {
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
roles: ['kibana_dual_privileges_user'],
full_name: 'a kibana dual_privileges user',
email: 'a_kibana_dual_privileges_user@elastic.co',
}
});
await es.shield.putUser({
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
body: {
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
roles: ["kibana_dual_privileges_dashboard_only_user"],
full_name: 'a kibana dual_privileges dashboard only user',
email: 'a_kibana_dual_privileges_dashboard_only_user@elastic.co',
}
});
await es.shield.putUser({
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
body: {
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
roles: ['kibana_rbac_user'],
full_name: 'a kibana user',
email: 'a_kibana_rbac_user@elastic.co',
}
});
await es.shield.putUser({
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
body: {
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
roles: ["kibana_rbac_dashboard_only_user"],
full_name: 'a kibana dashboard only user',
email: 'a_kibana_rbac_dashboard_only_user@elastic.co',
}
});
});
loadTestFile(require.resolve('./bulk_get'));
loadTestFile(require.resolve('./create'));
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./find'));
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./update'));
});
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const AUTHENTICATION = {
NOT_A_KIBANA_USER: {
USERNAME: 'not_a_kibana_user',
PASSWORD: 'password'
},
SUPERUSER: {
USERNAME: 'elastic',
PASSWORD: 'changeme'
},
KIBANA_LEGACY_USER: {
USERNAME: 'a_kibana_legacy_user',
PASSWORD: 'password'
},
KIBANA_LEGACY_DASHBOARD_ONLY_USER: {
USERNAME: 'a_kibana_legacy_dashboard_only_user',
PASSWORD: 'password'
},
KIBANA_DUAL_PRIVILEGES_USER: {
USERNAME: 'a_kibana_dual_privileges_user',
PASSWORD: 'password'
},
KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER: {
USERNAME: 'a_kibana_dual_privileges_dashboard_only_user',
PASSWORD: 'password'
},
KIBANA_RBAC_USER: {
USERNAME: 'a_kibana_rbac_user',
PASSWORD: 'password'
},
KIBANA_RBAC_DASHBOARD_ONLY_USER: {
USERNAME: 'a_kibana_rbac_dashboard_only_user',
PASSWORD: 'password'
}
};

View file

@ -0,0 +1,229 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from 'expect.js';
import { AUTHENTICATION } from './lib/authentication';
export default function ({ getService }) {
const supertest = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe('update', () => {
const expectResults = resp => {
// loose uuid validation
expect(resp.body).to.have.property('id').match(/^[0-9a-f-]{36}$/);
// loose ISO8601 UTC time with milliseconds validation
expect(resp.body).to.have.property('updated_at').match(/^[\d-]{10}T[\d:\.]{12}Z$/);
expect(resp.body).to.eql({
id: resp.body.id,
type: 'visualization',
updated_at: resp.body.updated_at,
version: 2,
attributes: {
title: 'My second favorite vis'
}
});
};
const expectNotFound = resp => {
expect(resp.body).eql({
statusCode: 404,
error: 'Not Found',
message: 'Saved object [visualization/not an id] not found'
});
};
const expectRbacForbidden = resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Unable to update visualization, missing action:saved_objects/visualization/update`
});
};
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'));
after(() => esArchiver.unload('saved_objects/basic'));
it(`should return ${tests.exists.statusCode}`, async () => {
await supertest
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
.auth(auth.username, auth.password)
.send({
attributes: {
title: 'My second favorite vis'
}
})
.expect(tests.exists.statusCode)
.then(tests.exists.response);
});
describe('unknown id', () => {
it(`should return ${tests.doesntExist.statusCode}`, async () => {
await supertest
.put(`/api/saved_objects/visualization/not an id`)
.auth(auth.username, auth.password)
.send({
attributes: {
title: 'My second favorite vis'
}
})
.expect(tests.doesntExist.statusCode)
.then(tests.doesntExist.response);
});
});
});
};
updateTest(`not a kibana user`, {
auth: {
username: AUTHENTICATION.NOT_A_KIBANA_USER.USERNAME,
password: AUTHENTICATION.NOT_A_KIBANA_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 403,
response: expectRbacForbidden,
},
doesntExist: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
updateTest(`superuser`, {
auth: {
username: AUTHENTICATION.SUPERUSER.USERNAME,
password: AUTHENTICATION.SUPERUSER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
updateTest(`kibana legacy user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
updateTest(`kibana legacy dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 403,
response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME),
},
doesntExist: {
statusCode: 403,
response: createExpectLegacyForbidden(AUTHENTICATION.KIBANA_LEGACY_DASHBOARD_ONLY_USER.USERNAME),
},
}
});
updateTest(`kibana dual-privileges user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
updateTest(`kibana dual-privileges dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 403,
response: expectRbacForbidden,
},
doesntExist: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
updateTest(`kibana rbac user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 200,
response: expectResults,
},
doesntExist: {
statusCode: 404,
response: expectNotFound,
},
}
});
updateTest(`kibana rbac dashboard only user`, {
auth: {
username: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.USERNAME,
password: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER.PASSWORD,
},
tests: {
exists: {
statusCode: 403,
response: expectRbacForbidden,
},
doesntExist: {
statusCode: 403,
response: expectRbacForbidden,
},
}
});
});
}

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import path from 'path';
import { resolveKibanaPath } from '@kbn/plugin-helpers';
import { EsProvider } from './services/es';
export default async function ({ readConfigFile }) {
const config = {
kibana: {
api: await readConfigFile(resolveKibanaPath('test/api_integration/config.js')),
functional: await readConfigFile(require.resolve('../../../test/functional/config.js'))
},
xpack: {
api: await readConfigFile(require.resolve('../api_integration/config.js'))
}
};
return {
testFiles: [require.resolve('./apis')],
servers: config.xpack.api.get('servers'),
services: {
es: EsProvider,
esSupertestWithoutAuth: config.xpack.api.get('services.esSupertestWithoutAuth'),
supertest: config.kibana.api.get('services.supertest'),
supertestWithoutAuth: config.xpack.api.get('services.supertestWithoutAuth'),
esArchiver: config.kibana.functional.get('services.esArchiver'),
kibanaServer: config.kibana.functional.get('services.kibanaServer'),
},
junit: {
reportName: 'X-Pack RBAC API Integration Tests',
},
// The saved_objects/basic archives are almost an exact replica of the ones in OSS
// with the exception of a bogus "not-a-visualization" type that I added to make sure
// the find filtering without a type specified worked correctly. Once we have the ability
// to specify more granular access to the objects via the Kibana privileges, this should
// no longer be necessary, and it's only required as long as we do read/all privileges.
esArchiver: {
directory: path.join(__dirname, 'fixtures', 'es_archiver')
},
esTestCluster: {
...config.xpack.api.get('esTestCluster'),
serverArgs: [
...config.xpack.api.get('esTestCluster.serverArgs'),
],
},
kbnTestServer: {
...config.xpack.api.get('kbnTestServer'),
serverArgs: [
...config.xpack.api.get('kbnTestServer.serverArgs'),
'--optimize.enabled=false',
'--server.xsrf.disableProtection=true',
],
},
};
}

View file

@ -0,0 +1,283 @@
{
"type": "index",
"value": {
"index": ".kibana",
"settings": {
"index": {
"number_of_shards": "1",
"auto_expand_replicas": "0-1",
"number_of_replicas": "0"
}
},
"mappings": {
"doc": {
"dynamic": "strict",
"properties": {
"config": {
"dynamic": "true",
"properties": {
"buildNum": {
"type": "keyword"
},
"defaultIndex": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
"dashboard": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"optionsJSON": {
"type": "text"
},
"panelsJSON": {
"type": "text"
},
"refreshInterval": {
"properties": {
"display": {
"type": "keyword"
},
"pause": {
"type": "boolean"
},
"section": {
"type": "integer"
},
"value": {
"type": "integer"
}
}
},
"timeFrom": {
"type": "keyword"
},
"timeRestore": {
"type": "boolean"
},
"timeTo": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"graph-workspace": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"numLinks": {
"type": "integer"
},
"numVertices": {
"type": "integer"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
},
"wsState": {
"type": "text"
}
}
},
"index-pattern": {
"properties": {
"fieldFormatMap": {
"type": "text"
},
"fields": {
"type": "text"
},
"intervalName": {
"type": "keyword"
},
"notExpandable": {
"type": "boolean"
},
"sourceFilters": {
"type": "text"
},
"timeFieldName": {
"type": "keyword"
},
"title": {
"type": "text"
}
}
},
"search": {
"properties": {
"columns": {
"type": "keyword"
},
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"sort": {
"type": "keyword"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"server": {
"properties": {
"uuid": {
"type": "keyword"
}
}
},
"timelion-sheet": {
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"timelion_chart_height": {
"type": "integer"
},
"timelion_columns": {
"type": "integer"
},
"timelion_interval": {
"type": "keyword"
},
"timelion_other_interval": {
"type": "keyword"
},
"timelion_rows": {
"type": "integer"
},
"timelion_sheet": {
"type": "text"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"type": {
"type": "keyword"
},
"updated_at": {
"type": "date"
},
"url": {
"properties": {
"accessCount": {
"type": "long"
},
"accessDate": {
"type": "date"
},
"createDate": {
"type": "date"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
},
"visualization": {
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"savedSearchId": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
},
"visState": {
"type": "text"
}
}
}
}
}
},
"aliases": {}
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { format as formatUrl } from 'url';
import elasticsearch from 'elasticsearch';
import shieldPlugin from '../../../server/lib/esjs_shield_plugin';
export function EsProvider({ getService }) {
const config = getService('config');
return new elasticsearch.Client({
host: formatUrl(config.get('servers.elasticsearch')),
requestTimeout: config.get('timeouts.esRequestTimeout'),
plugins: [shieldPlugin]
});
}