Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Securing Spaces #21995

Merged
merged 88 commits into from
Sep 12, 2018
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
a25ed8e
Splitting to a client and a wrapper, integation tests are passing
kobelb Aug 2, 2018
e0a83cd
Adding the spaces wrapper back in the mix using a priority collection
kobelb Aug 3, 2018
f937154
Restructuring the secure wrapper as we don't need to switch between
kobelb Aug 3, 2018
591a31b
Checking authorization at the current space
kobelb Aug 3, 2018
dbfe32e
Beginning to make the rbac api integration tests run against spaces
kobelb Aug 6, 2018
d25514a
Adding identical data to the rbac esArchives for two more spaces
kobelb Aug 6, 2018
7ed4332
Adding some space tests for find
kobelb Aug 6, 2018
43322c2
Beginning to work on the spaces client
kobelb Aug 8, 2018
daad804
Fixing find and filtering out unrequested privileges from response
kobelb Aug 8, 2018
d5b9b53
Adding get code and test
kobelb Aug 8, 2018
74335fa
Introducing an RBAC Auth Scope
kobelb Aug 10, 2018
706c505
Exposing the spacesClient a bit better
kobelb Aug 10, 2018
e0a6c6f
Moving the server.expose to the security plugin init
kobelb Aug 13, 2018
5487f0f
Moving checkPrivilegesAtAllResources to it's own thing
kobelb Aug 13, 2018
69b1eb6
No longer using the auth scope for RBAC, dashboard mode didn't work with
kobelb Aug 13, 2018
685b061
Securing the create space method
kobelb Aug 14, 2018
c7e390b
Adding secure update method
kobelb Aug 14, 2018
6d5a15a
Adding secured delete endpoints
kobelb Aug 14, 2018
e355ff0
Restructuring some code in the spaces client
kobelb Aug 14, 2018
67b64e9
Adding tests for the select endpoint
kobelb Aug 14, 2018
52633f0
Spaces can't be managed via the SavedObjectsClient now
kobelb Aug 15, 2018
d2ab5cc
Creating separate space_all and space_read privileges
kobelb Aug 15, 2018
cff7c61
Merge branch 'spaces-phase-1' into spaces/securing
kobelb Aug 17, 2018
b827684
Splitting out the spaces and global privileges
kobelb Aug 20, 2018
ed54020
Merge branch 'spaces-phase-1' into spaces/securing
kobelb Aug 20, 2018
3d75fb3
Fixing edit role screen after API changes
kobelb Aug 20, 2018
151d4ee
Revising comment, there is a Set in JavaScript now, but lodash can't
kobelb Aug 20, 2018
cec82a9
Using authorization mode to log deprecation warning on login
kobelb Aug 21, 2018
5649e10
Changing the signature of checkPrivileges
kobelb Aug 22, 2018
5995cae
Refactoring the way we specify resources when checking privileges
kobelb Aug 22, 2018
b87b506
Exposing the space service more intuitively
kobelb Aug 22, 2018
13fdf1f
Fixing comments
kobelb Aug 22, 2018
588e2e8
Security defines all actions
kobelb Aug 22, 2018
87a8332
Renaming `response` returned by the checkPrivileges function
kobelb Aug 23, 2018
58f037c
Merge remote-tracking branch 'upstream/spaces-phase-1' into spaces/se…
kobelb Aug 28, 2018
e64ec72
Hard-coding the kibana app privileges teporarily
kobelb Aug 28, 2018
daca66b
Adding Authenticator authorization mode tests
kobelb Aug 28, 2018
8ee733f
Adding actions.manageSpaces tests
kobelb Aug 28, 2018
58aa6f1
Adding check privileges tests
kobelb Aug 28, 2018
18e058f
Fixing checkPrivileges test snapshots
kobelb Aug 28, 2018
3813a12
Making sure tests fail until I correct this deficiency
kobelb Aug 28, 2018
a50d51a
Adding stubbed out authorization mode tests
kobelb Aug 28, 2018
4dfb720
Fixing tests for RegisterPrivilegesWithCluster
kobelb Aug 28, 2018
2a632f0
Fixing service AuthorizationService tests
kobelb Aug 28, 2018
e3b23e6
Addinng serializer tests
kobelb Aug 29, 2018
1f5c127
Adding validateEsResponse tests
kobelb Aug 29, 2018
31cc9ff
We don't need the SecureSavedObjectsClient anymore!
kobelb Aug 29, 2018
ac3d143
Adding SecureSavedObjectsClientWrapper tests...
kobelb Aug 29, 2018
aefe2c5
Merge remote-tracking branch 'upstream/spaces-phase-1' into spaces/se…
kobelb Sep 4, 2018
ce171ca
Fixing a few stray tests
kobelb Sep 4, 2018
cef2e78
Fixing issue when user isn't authenticated and check useRbacForRequest
kobelb Sep 4, 2018
7b4575b
Merge remote-tracking branch 'upstream/spaces-phase-1' into spaces/se…
kobelb Sep 5, 2018
5413b47
Validating spaces we're adding to roles
kobelb Sep 7, 2018
c02d0fb
Reusing hasAnyPrivileges from hasAnyResourcePrivileges
kobelb Sep 7, 2018
1436495
Better variable name
kobelb Sep 7, 2018
79884be
toArray -> toPrioritizedArray
kobelb Sep 7, 2018
b7d2aa4
Using Space throughout the SpacesClient
kobelb Sep 7, 2018
14a1d96
GetActiveSpace uses the SpacesClient now
kobelb Sep 10, 2018
d9699f9
Squashed commit of the following:
kobelb Sep 10, 2018
6da2f05
Merge branch 'spaces-phase-1' into spaces/securing
legrego Sep 10, 2018
8e52fc0
update public api to use SpacesClient
legrego Sep 7, 2018
81b785b
fix
legrego Sep 7, 2018
b3b04f3
test and api fixes
legrego Sep 7, 2018
fb40ad9
fix tests
legrego Sep 10, 2018
eb3fde7
Merge branch 'spaces-phase-1' into spaces/securing
legrego Sep 10, 2018
f52e688
Merge pull request #5 from legrego/spaces/securing-update
kobelb Sep 10, 2018
88b03e7
Disallowing use of Spaces with the SpacesSavedObjectsClientWrapper
kobelb Sep 11, 2018
b2e4321
Adding spaces audit logging
kobelb Sep 11, 2018
454dcb1
Updating snapshots
kobelb Sep 11, 2018
d01f9b6
Adding get and getAll tests for the spaces client
kobelb Sep 11, 2018
e3569c8
Adding update tests
kobelb Sep 12, 2018
90b9811
Adding create tests
kobelb Sep 12, 2018
3d0a8f4
Adding SpacesClient delete tests
kobelb Sep 12, 2018
a061555
Fixing authenticate tests
kobelb Sep 12, 2018
b8d9738
Making tests more consistent
kobelb Sep 12, 2018
5e79942
Merge remote-tracking branch 'upstream/spaces-phase-1' into spaces/se…
kobelb Sep 12, 2018
54360a2
Fixing kibana privileges tests
kobelb Sep 12, 2018
ec1eeb7
Fixing a few type issues
kobelb Sep 12, 2018
6685a52
Making typescript ignore the .only test suites
kobelb Sep 12, 2018
dea9859
Switching to beforeEach and afterEach and removing the mocha types
kobelb Sep 12, 2018
d9dc42b
Switching to our own expect.js definitions
kobelb Sep 12, 2018
77538e8
Fixing more linting rules
kobelb Sep 12, 2018
b7fc11e
Removing test stubs and replacing with TODOs. Updating snapshots
kobelb Sep 12, 2018
f59a2ee
No longer shadowing application for the put role API
kobelb Sep 12, 2018
bbc54cb
Relying on the errors thrown by the SpacedClient when determining active
kobelb Sep 12, 2018
aba3f39
Back to after/before... mocha doesn't have beforeAll/afterAll
kobelb Sep 12, 2018
0a531b5
We got them types, thanks Spencer!!!
kobelb Sep 12, 2018
a68f029
Ignoring space type from the secure saved objects client find with no
kobelb Sep 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`1, 1 throws Error 1`] = `"Already have entry with this priority"`;
57 changes: 57 additions & 0 deletions src/server/saved_objects/service/lib/priority_collection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PriorityCollection } from './priority_collection';

test(`1, 2, 3`, () => {
const priorityCollection = new PriorityCollection();
priorityCollection.add(1, 1);
priorityCollection.add(2, 2);
priorityCollection.add(3, 3);
expect(priorityCollection.toArray()).toEqual([1, 2, 3]);
});

test(`3, 2, 1`, () => {
const priorityCollection = new PriorityCollection();
priorityCollection.add(3, 3);
priorityCollection.add(2, 2);
priorityCollection.add(1, 1);
expect(priorityCollection.toArray()).toEqual([1, 2, 3]);
});

test(`2, 3, 1`, () => {
const priorityCollection = new PriorityCollection();
priorityCollection.add(2, 2);
priorityCollection.add(3, 3);
priorityCollection.add(1, 1);
expect(priorityCollection.toArray()).toEqual([1, 2, 3]);
});

test(`Number.MAX_VALUE, NUMBER.MIN_VALUE, 1`, () => {
const priorityCollection = new PriorityCollection();
priorityCollection.add(Number.MAX_VALUE, 3);
priorityCollection.add(Number.MIN_VALUE, 1);
priorityCollection.add(1, 2);
expect(priorityCollection.toArray()).toEqual([1, 2, 3]);
});

test(`1, 1 throws Error`, () => {
const priorityCollection = new PriorityCollection();
priorityCollection.add(1, 1);
expect(() => priorityCollection.add(1, 1)).toThrowErrorMatchingSnapshot();
});
47 changes: 47 additions & 0 deletions src/server/saved_objects/service/lib/priority_collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

interface PriorityCollectionEntry<T> {
priority: number;
value: T;
}

export class PriorityCollection<T> {
private readonly array: Array<PriorityCollectionEntry<T>> = [];

public add(priority: number, value: T) {
let i = 0;
while (i < this.array.length) {
const current = this.array[i];
if (priority === current.priority) {
throw new Error('Already have entry with this priority');
}

if (priority < current.priority) {
break;
}
++i;
}
this.array.splice(i, 0, { priority, value });
}

public toArray(): T[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about naming this toPrioritizedArray(), or something similar?

return this.array.map(entry => entry.value);
}
}
21 changes: 12 additions & 9 deletions src/server/saved_objects/service/lib/scoped_client_provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { PriorityCollection } from './priority_collection';

/**
* Provider for the Scoped Saved Object Client.
*/
export class ScopedSavedObjectsClientProvider {

_wrapperFactories = [];
_wrapperFactories = new PriorityCollection();

constructor({
defaultClientFactory
Expand All @@ -38,8 +39,8 @@ export class ScopedSavedObjectsClientProvider {
// dependency on plugin b, that means that plugin b's client wrapper would want
// to be able to run first when the SavedObjectClient methods are invoked to
// provide additional context to plugin a's client wrapper.
addClientWrapperFactory(wrapperFactory) {
this._wrapperFactories.unshift(wrapperFactory);
addClientWrapperFactory(priority, wrapperFactory) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preceding comment is outdated now that this accepts a priority

this._wrapperFactories.add(priority, wrapperFactory);
}

setClientFactory(customClientFactory) {
Expand All @@ -55,11 +56,13 @@ export class ScopedSavedObjectsClientProvider {
request,
});

return this._wrapperFactories.reduce((clientToWrap, wrapperFactory) => {
return wrapperFactory({
request,
client: clientToWrap,
});
}, client);
return this._wrapperFactories
.toArray()
.reduceRight((clientToWrap, wrapperFactory) => {
return wrapperFactory({
request,
client: clientToWrap,
});
}, client);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,40 +64,20 @@ test(`throws error when more than one scoped saved objects client factory is set
}).toThrowErrorMatchingSnapshot();
});

test(`invokes and uses instance from single added wrapper factory`, () => {
test(`invokes and uses wrappers in specified order`, () => {
const defaultClient = Symbol();
const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient);
const clientProvider = new ScopedSavedObjectsClientProvider({
defaultClientFactory: defaultClientFactoryMock
});
const wrappedClient = Symbol();
const clientWrapperFactoryMock = jest.fn().mockReturnValue(wrappedClient);
const request = Symbol();

clientProvider.addClientWrapperFactory(clientWrapperFactoryMock);
const actualClient = clientProvider.getClient(request);

expect(actualClient).toBe(wrappedClient);
expect(clientWrapperFactoryMock).toHaveBeenCalledWith({
request,
client: defaultClient
});
});

test(`invokes and uses wrappers in LIFO order`, () => {
const defaultClient = Symbol();
const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient);
const clientProvider = new ScopedSavedObjectsClientProvider({
defaultClientFactory: defaultClientFactoryMock
});
const firstWrappedClient = Symbol();
const firstWrappedClient = Symbol('first client');
const firstClientWrapperFactoryMock = jest.fn().mockReturnValue(firstWrappedClient);
const secondWrapperClient = Symbol();
const secondWrapperClient = Symbol('second client');
const secondClientWrapperFactoryMock = jest.fn().mockReturnValue(secondWrapperClient);
const request = Symbol();

clientProvider.addClientWrapperFactory(firstClientWrapperFactoryMock);
clientProvider.addClientWrapperFactory(secondClientWrapperFactoryMock);
clientProvider.addClientWrapperFactory(1, secondClientWrapperFactoryMock);
clientProvider.addClientWrapperFactory(0, firstClientWrapperFactoryMock);
const actualClient = clientProvider.getClient(request);

expect(actualClient).toBe(firstWrappedClient);
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/

export const ALL_RESOURCE = '*';
export const GLOBAL_RESOURCE = '*';
47 changes: 28 additions & 19 deletions x-pack/plugins/security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { initAuthenticator } from './server/lib/authentication/authenticator';
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 { createAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization';
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper';
import { deepFreeze } from './server/lib/deep_freeze';

export const security = (kibana) => new kibana.Plugin({
id: 'security',
Expand Down Expand Up @@ -106,7 +107,8 @@ export const security = (kibana) => new kibana.Plugin({
server.auth.strategy('session', 'login', 'required');

// exposes server.plugins.security.authorization
initAuthorizationService(server);
const authorization = createAuthorizationService(server, xpackInfoFeature);
server.expose('authorization', deepFreeze(authorization));

watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => {
if (license.allowRbac) {
Expand All @@ -124,30 +126,37 @@ export const security = (kibana) => new kibana.Plugin({
const { callWithRequest, callWithInternalUser } = adminCluster;
const callCluster = (...args) => callWithRequest(request, ...args);

if (authorization.mode.useRbacForRequest(request)) {
const internalRepository = savedObjects.getSavedObjectsRepository(callWithInternalUser);
return new savedObjects.SavedObjectsClient(internalRepository);
}

const callWithRequestRepository = savedObjects.getSavedObjectsRepository(callCluster);
return new savedObjects.SavedObjectsClient(callWithRequestRepository);
});

if (!xpackInfoFeature.getLicenseCheckResults().allowRbac) {
return new savedObjects.SavedObjectsClient(callWithRequestRepository);
savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_VALUE, ({ client, request }) => {
if (authorization.mode.useRbacForRequest(request)) {
const { spaces } = server.plugins;

return new SecureSavedObjectsClientWrapper({
actions: authorization.actions,
auditLogger,
baseClient: client,
checkPrivilegesWithRequest: authorization.checkPrivilegesWithRequest,
errors: savedObjects.SavedObjectsClient.errors,
request,
savedObjectTypes: savedObjects.types,
spaces,
});
}

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,
});
return client;
});

getUserProvider(server);

await initAuthenticator(server);
await initAuthenticator(server, authorization.mode);
initAuthenticateApi(server);
initUsersApi(server);
initPublicRolesApi(server);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@ 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);
return $resource(baseUrl, null, {
query: {
isArray: false,
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {

return role.then(res => res.toJSON());
},
kibanaApplicationPrivilege(ApplicationPrivileges, kbnUrl, Promise, Private) {
return ApplicationPrivileges.query().$promise
.then(privileges => privileges.map(p => p.toJSON()))
.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
Expand All @@ -93,7 +88,6 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {

const Notifier = $injector.get('Notifier');

const kibanaApplicationPrivilege = $route.current.locals.kibanaApplicationPrivilege;
const role = $route.current.locals.role;

const xpackInfo = Private(XPackInfoProvider);
Expand Down Expand Up @@ -126,6 +120,9 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
spaces,
} = $route.current.locals;

// todo: don't hard-code this...
const kibanaApplicationPrivilege = [{ name: 'all' }, { name: 'read' } ];

$scope.$$postDigest(() => {
const domNode = document.getElementById('editRoleReactRoot');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Authenticator', () => {
let server;
let session;
let cluster;
let authorizationMode;
beforeEach(() => {
server = serverFixture();
session = sinon.createStubInstance(Session);
Expand All @@ -34,6 +35,8 @@ describe('Authenticator', () => {
cluster = sinon.stub({ callWithRequest() {} });
sandbox.stub(ClientShield, 'getClient').returns(cluster);

authorizationMode = { initialize: sinon.stub() };

server.config.returns(config);
server.register.yields();

Expand Down Expand Up @@ -83,7 +86,7 @@ describe('Authenticator', () => {
server.plugins.kibana.systemApi.isSystemApiRequest.returns(true);
session.clear.throws(new Error('`Session.clear` is not supposed to be called!'));

await initAuthenticator(server);
await initAuthenticator(server, authorizationMode);

// Second argument will be a method we'd like to test.
authenticate = server.expose.withArgs('authenticate').firstCall.args[1];
Expand Down Expand Up @@ -112,6 +115,18 @@ describe('Authenticator', () => {
expect(authenticationResult.error).to.be(failureReason);
});

it(`doesn't initialize authorizationMode when authentication fails.`, async () => {
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
session.get.withArgs(request).returns(Promise.resolve(null));

const failureReason = new Error('Not Authorized');
cluster.callWithRequest.withArgs(request).returns(Promise.reject(failureReason));

await authenticate(request);

sinon.assert.notCalled(authorizationMode.initialize);
});

it('returns user that authentication provider returns.', async () => {
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
const user = { username: 'user' };
Expand All @@ -125,6 +140,15 @@ describe('Authenticator', () => {
});
});

it('initiliazes authorizationMode when authentication succeeds.', async () => {
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
const user = { username: 'user' };
cluster.callWithRequest.withArgs(request).returns(Promise.resolve(user));

await authenticate(request);
sinon.assert.calledWith(authorizationMode.initialize, request);
});

it('creates session whenever authentication provider returns state to store.', async () => {
const user = { username: 'user' };
const systemAPIRequest = requestFixture({ headers: { authorization: 'Basic xxx' } });
Expand Down
Loading