Skip to content

Commit

Permalink
Introduce capabilities provider and switcher to file upload plugin (#…
Browse files Browse the repository at this point in the history
…96593)

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
legrego and kibanamachine authored May 14, 2021
1 parent 108252b commit c572ddd
Show file tree
Hide file tree
Showing 6 changed files with 382 additions and 23 deletions.
267 changes: 267 additions & 0 deletions x-pack/plugins/file_upload/server/capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { setupCapabilities } from './capabilities';
import { coreMock, httpServerMock } from '../../../../src/core/server/mocks';
import { Capabilities, CoreStart } from 'kibana/server';
import { securityMock } from '../../security/server/mocks';

describe('setupCapabilities', () => {
it('registers a capabilities provider for the file upload feature', () => {
const coreSetup = coreMock.createSetup();
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1);
const [provider] = coreSetup.capabilities.registerProvider.mock.calls[0];
expect(provider()).toMatchInlineSnapshot(`
Object {
"fileUpload": Object {
"show": true,
},
}
`);
});

it('registers a capabilities switcher that returns unaltered capabilities when security is disabled', async () => {
const coreSetup = coreMock.createSetup();
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0];

const capabilities = {
navLinks: {},
management: {},
catalogue: {},
fileUpload: {
show: true,
},
} as Capabilities;

const request = httpServerMock.createKibanaRequest();

await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(`
Object {
"catalogue": Object {},
"fileUpload": Object {
"show": true,
},
"management": Object {},
"navLinks": Object {},
}
`);
});

it('registers a capabilities switcher that returns unaltered capabilities when default capabilities are requested', async () => {
const coreSetup = coreMock.createSetup();
const security = securityMock.createStart();
security.authz.mode.useRbacForRequest.mockReturnValue(true);
coreSetup.getStartServices.mockResolvedValue([
(undefined as unknown) as CoreStart,
{ security },
undefined,
]);
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0];

const capabilities = {
navLinks: {},
management: {},
catalogue: {},
fileUpload: {
show: true,
},
} as Capabilities;

const request = httpServerMock.createKibanaRequest();

await expect(switcher(request, capabilities, true)).resolves.toMatchInlineSnapshot(`
Object {
"catalogue": Object {},
"fileUpload": Object {
"show": true,
},
"management": Object {},
"navLinks": Object {},
}
`);

expect(security.authz.mode.useRbacForRequest).not.toHaveBeenCalled();
expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled();
});

it('registers a capabilities switcher that disables capabilities for underprivileged users', async () => {
const coreSetup = coreMock.createSetup();
const security = securityMock.createStart();
security.authz.mode.useRbacForRequest.mockReturnValue(true);

const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: false });
security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges);
coreSetup.getStartServices.mockResolvedValue([
(undefined as unknown) as CoreStart,
{ security },
undefined,
]);
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0];

const capabilities = {
navLinks: {},
management: {},
catalogue: {},
fileUpload: {
show: true,
},
} as Capabilities;

const request = httpServerMock.createKibanaRequest();

await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(`
Object {
"fileUpload": Object {
"show": false,
},
}
`);

expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1);
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request);
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1);
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request);
});

it('registers a capabilities switcher that enables capabilities for privileged users', async () => {
const coreSetup = coreMock.createSetup();
const security = securityMock.createStart();
security.authz.mode.useRbacForRequest.mockReturnValue(true);

const mockCheckPrivileges = jest.fn().mockResolvedValue({ hasAllRequested: true });
security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges);
coreSetup.getStartServices.mockResolvedValue([
(undefined as unknown) as CoreStart,
{ security },
undefined,
]);
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0];

const capabilities = {
navLinks: {},
management: {},
catalogue: {},
fileUpload: {
show: true,
},
} as Capabilities;

const request = httpServerMock.createKibanaRequest();

await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(`
Object {
"catalogue": Object {},
"fileUpload": Object {
"show": true,
},
"management": Object {},
"navLinks": Object {},
}
`);

expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1);
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request);
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledTimes(1);
expect(security.authz.checkPrivilegesDynamicallyWithRequest).toHaveBeenCalledWith(request);
});

it('registers a capabilities switcher that disables capabilities for unauthenticated requests', async () => {
const coreSetup = coreMock.createSetup();
const security = securityMock.createStart();
security.authz.mode.useRbacForRequest.mockReturnValue(true);
const mockCheckPrivileges = jest
.fn()
.mockRejectedValue(new Error('this should not have been called'));
security.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(mockCheckPrivileges);
coreSetup.getStartServices.mockResolvedValue([
(undefined as unknown) as CoreStart,
{ security },
undefined,
]);
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0];

const capabilities = {
navLinks: {},
management: {},
catalogue: {},
fileUpload: {
show: true,
},
} as Capabilities;

const request = httpServerMock.createKibanaRequest({ auth: { isAuthenticated: false } });

await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(`
Object {
"fileUpload": Object {
"show": false,
},
}
`);

expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1);
expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled();
});

it('registers a capabilities switcher that skips privilege check for requests not using rbac', async () => {
const coreSetup = coreMock.createSetup();
const security = securityMock.createStart();
security.authz.mode.useRbacForRequest.mockReturnValue(false);
coreSetup.getStartServices.mockResolvedValue([
(undefined as unknown) as CoreStart,
{ security },
undefined,
]);
setupCapabilities(coreSetup);

expect(coreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
const [switcher] = coreSetup.capabilities.registerSwitcher.mock.calls[0];

const capabilities = {
navLinks: {},
management: {},
catalogue: {},
fileUpload: {
show: true,
},
} as Capabilities;

const request = httpServerMock.createKibanaRequest();

await expect(switcher(request, capabilities, false)).resolves.toMatchInlineSnapshot(`
Object {
"catalogue": Object {},
"fileUpload": Object {
"show": true,
},
"management": Object {},
"navLinks": Object {},
}
`);

expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledTimes(1);
expect(security.authz.mode.useRbacForRequest).toHaveBeenCalledWith(request);
expect(security.authz.checkPrivilegesDynamicallyWithRequest).not.toHaveBeenCalled();
});
});
47 changes: 47 additions & 0 deletions x-pack/plugins/file_upload/server/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { CoreSetup } from 'kibana/server';
import { checkFileUploadPrivileges } from './check_privileges';
import { StartDeps } from './types';

export const setupCapabilities = (
core: Pick<CoreSetup<StartDeps>, 'capabilities' | 'getStartServices'>
) => {
core.capabilities.registerProvider(() => {
return {
fileUpload: {
show: true,
},
};
});

core.capabilities.registerSwitcher(async (request, capabilities, useDefaultCapabilities) => {
if (useDefaultCapabilities) {
return capabilities;
}
const [, { security }] = await core.getStartServices();

// Check the bare minimum set of privileges required to get some utility out of this feature
const { hasImportPermission } = await checkFileUploadPrivileges({
authorization: security?.authz,
request,
checkCreateIndexPattern: true,
checkHasManagePipeline: false,
});

if (!hasImportPermission) {
return {
fileUpload: {
show: false,
},
};
}

return capabilities;
});
};
55 changes: 55 additions & 0 deletions x-pack/plugins/file_upload/server/check_privileges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { KibanaRequest } from 'kibana/server';
import { AuthorizationServiceSetup, CheckPrivilegesPayload } from '../../security/server';

interface Deps {
request: KibanaRequest;
authorization?: Pick<
AuthorizationServiceSetup,
'mode' | 'actions' | 'checkPrivilegesDynamicallyWithRequest'
>;
checkHasManagePipeline: boolean;
checkCreateIndexPattern: boolean;
indexName?: string;
}

export const checkFileUploadPrivileges = async ({
request,
authorization,
checkHasManagePipeline,
checkCreateIndexPattern,
indexName,
}: Deps) => {
const requiresAuthz = authorization?.mode.useRbacForRequest(request) ?? false;

if (!authorization || !requiresAuthz) {
return { hasImportPermission: true };
}

if (!request.auth.isAuthenticated) {
return { hasImportPermission: false };
}

const checkPrivilegesPayload: CheckPrivilegesPayload = {
elasticsearch: {
cluster: checkHasManagePipeline ? ['manage_pipeline'] : [],
index: indexName ? { [indexName]: ['create', 'create_index'] } : {},
},
};
if (checkCreateIndexPattern) {
checkPrivilegesPayload.kibana = [
authorization.actions.savedObject.get('index-pattern', 'create'),
];
}

const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(request);
const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload);

return { hasImportPermission: checkPrivilegesResp.hasAllRequested };
};
3 changes: 3 additions & 0 deletions x-pack/plugins/file_upload/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { initFileUploadTelemetry } from './telemetry';
import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { UI_SETTING_MAX_FILE_SIZE, MAX_FILE_SIZE } from '../common';
import { StartDeps } from './types';
import { setupCapabilities } from './capabilities';

interface SetupDeps {
usageCollection: UsageCollectionSetup;
Expand All @@ -28,6 +29,8 @@ export class FileUploadPlugin implements Plugin {
async setup(coreSetup: CoreSetup<StartDeps, unknown>, plugins: SetupDeps) {
fileUploadRoutes(coreSetup, this._logger);

setupCapabilities(coreSetup);

coreSetup.uiSettings.register({
[UI_SETTING_MAX_FILE_SIZE]: {
name: i18n.translate('xpack.fileUpload.maxFileSizeUiSetting.name', {
Expand Down
Loading

0 comments on commit c572ddd

Please sign in to comment.