mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
108252bd8d
commit
c572ddd780
6 changed files with 382 additions and 23 deletions
267
x-pack/plugins/file_upload/server/capabilities.test.ts
Normal file
267
x-pack/plugins/file_upload/server/capabilities.test.ts
Normal 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();
|
||||
});
|
||||
});
|
47
x-pack/plugins/file_upload/server/capabilities.ts
Normal file
47
x-pack/plugins/file_upload/server/capabilities.ts
Normal 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;
|
||||
});
|
||||
};
|
55
x-pack/plugins/file_upload/server/check_privileges.ts
Normal file
55
x-pack/plugins/file_upload/server/check_privileges.ts
Normal 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 };
|
||||
};
|
|
@ -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', {
|
||||
|
|
|
@ -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 } });
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue