Introduce capabilities provider and switcher to file upload plugin (#96593)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Larry Gregory 2021-05-14 08:31:03 -04:00 committed by GitHub
parent 108252bd8d
commit c572ddd780
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 382 additions and 23 deletions

View file

@ -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();
});
});

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
* 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;
});
};

View file

@ -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 };
};

View file

@ -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;
@ -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', {

View file

@ -22,8 +22,8 @@ import { analyzeFile } from './analyze_file';
import { updateTelemetry } from './telemetry';
import { importFileBodySchema, importFileQuerySchema, analyzeFileQuerySchema } from './schemas';
import { CheckPrivilegesPayload } from '../../security/server';
import { StartDeps } from './types';
import { checkFileUploadPrivileges } from './check_privileges';
function importData(
client: IScopedClusterClient,
@ -60,29 +60,15 @@ export function fileUploadRoutes(coreSetup: CoreSetup<StartDeps, unknown>, logge
const [, pluginsStart] = await coreSetup.getStartServices();
const { indexName, checkCreateIndexPattern, checkHasManagePipeline } = request.query;
const authorizationService = pluginsStart.security?.authz;
const requiresAuthz = authorizationService?.mode.useRbacForRequest(request) ?? false;
const { hasImportPermission } = await checkFileUploadPrivileges({
authorization: pluginsStart.security?.authz,
request,
indexName,
checkCreateIndexPattern,
checkHasManagePipeline,
});
if (!authorizationService || !requiresAuthz) {
return response.ok({ body: { hasImportPermission: true } });
}
const checkPrivilegesPayload: CheckPrivilegesPayload = {
elasticsearch: {
cluster: checkHasManagePipeline ? ['manage_pipeline'] : [],
index: indexName ? { [indexName]: ['create', 'create_index'] } : {},
},
};
if (checkCreateIndexPattern) {
checkPrivilegesPayload.kibana = [
authorizationService.actions.savedObject.get('index-pattern', 'create'),
];
}
const checkPrivileges = authorizationService.checkPrivilegesDynamicallyWithRequest(request);
const checkPrivilegesResp = await checkPrivileges(checkPrivilegesPayload);
return response.ok({ body: { hasImportPermission: checkPrivilegesResp.hasAllRequested } });
return response.ok({ body: { hasImportPermission } });
} catch (e) {
logger.warn(`Unable to check import permission, error: ${e.message}`);
return response.ok({ body: { hasImportPermission: false } });

View file

@ -27,6 +27,7 @@ export type {
GrantAPIKeyResult,
} from './authentication';
export type { CheckPrivilegesPayload } from './authorization';
export type AuthorizationServiceSetup = SecurityPluginStart['authz'];
export { LegacyAuditLogger, AuditLogger, AuditEvent } from './audit';
export type { SecurityPluginSetup, SecurityPluginStart };
export type { AuthenticatedUser } from '../common/model';