Allow elastic/fleet-server to call appropriate Fleet APIs (#113932)
This commit is contained in:
parent
f9afe67f1e
commit
5974fcfdb5
|
@ -23,7 +23,17 @@ import type { FleetAppContext } from '../plugin';
|
|||
// Export all mocks from artifacts
|
||||
export * from '../services/artifacts/mocks';
|
||||
|
||||
export const createAppContextStartContractMock = (): FleetAppContext => {
|
||||
export interface MockedFleetAppContext extends FleetAppContext {
|
||||
elasticsearch: ReturnType<typeof elasticsearchServiceMock.createStart>;
|
||||
data: ReturnType<typeof dataPluginMock.createStartContract>;
|
||||
encryptedSavedObjectsStart?: ReturnType<typeof encryptedSavedObjectsMock.createStart>;
|
||||
savedObjects: ReturnType<typeof savedObjectsServiceMock.createStartContract>;
|
||||
securitySetup?: ReturnType<typeof securityMock.createSetup>;
|
||||
securityStart?: ReturnType<typeof securityMock.createStart>;
|
||||
logger: ReturnType<ReturnType<typeof loggingSystemMock.create>['get']>;
|
||||
}
|
||||
|
||||
export const createAppContextStartContractMock = (): MockedFleetAppContext => {
|
||||
const config = {
|
||||
agents: { enabled: true, elasticsearch: {} },
|
||||
enabled: true,
|
||||
|
|
|
@ -80,9 +80,10 @@ import {
|
|||
} from './services/agents';
|
||||
import { registerFleetUsageCollector } from './collectors/register';
|
||||
import { getInstallation, ensureInstalledPackage } from './services/epm/packages';
|
||||
import { makeRouterEnforcingSuperuser } from './routes/security';
|
||||
import { RouterWrappers } from './routes/security';
|
||||
import { startFleetServerSetup } from './services/fleet_server';
|
||||
import { FleetArtifactsClient } from './services/artifacts';
|
||||
import type { FleetRouter } from './types/request_context';
|
||||
|
||||
export interface FleetSetupDeps {
|
||||
licensing: LicensingPluginSetup;
|
||||
|
@ -206,6 +207,24 @@ export class FleetPlugin
|
|||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'],
|
||||
catalogue: ['fleet'],
|
||||
reserved: {
|
||||
description:
|
||||
'Privilege to setup Fleet packages and configured policies. Intended for use by the elastic/fleet-server service account only.',
|
||||
privileges: [
|
||||
{
|
||||
id: 'fleet-setup',
|
||||
privilege: {
|
||||
excludeFromBasePrivileges: true,
|
||||
api: ['fleet-setup'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
privileges: {
|
||||
all: {
|
||||
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`],
|
||||
|
@ -245,7 +264,7 @@ export class FleetPlugin
|
|||
})
|
||||
);
|
||||
|
||||
const router = core.http.createRouter<FleetRequestHandlerContext>();
|
||||
const router: FleetRouter = core.http.createRouter<FleetRequestHandlerContext>();
|
||||
|
||||
// Register usage collection
|
||||
registerFleetUsageCollector(core, config, deps.usageCollection);
|
||||
|
@ -254,24 +273,34 @@ export class FleetPlugin
|
|||
registerAppRoutes(router);
|
||||
// Allow read-only users access to endpoints necessary for Integrations UI
|
||||
// Only some endpoints require superuser so we pass a raw IRouter here
|
||||
registerEPMRoutes(router);
|
||||
|
||||
// For all the routes we enforce the user to have role superuser
|
||||
const routerSuperuserOnly = makeRouterEnforcingSuperuser(router);
|
||||
const superuserRouter = RouterWrappers.require.superuser(router);
|
||||
const fleetSetupRouter = RouterWrappers.require.fleetSetupPrivilege(router);
|
||||
|
||||
// Some EPM routes use regular rbac to support integrations app
|
||||
registerEPMRoutes({ rbac: router, superuser: superuserRouter });
|
||||
|
||||
// Register rest of routes only if security is enabled
|
||||
if (deps.security) {
|
||||
registerSetupRoutes(routerSuperuserOnly, config);
|
||||
registerAgentPolicyRoutes(routerSuperuserOnly);
|
||||
registerPackagePolicyRoutes(routerSuperuserOnly);
|
||||
registerOutputRoutes(routerSuperuserOnly);
|
||||
registerSettingsRoutes(routerSuperuserOnly);
|
||||
registerDataStreamRoutes(routerSuperuserOnly);
|
||||
registerPreconfigurationRoutes(routerSuperuserOnly);
|
||||
registerSetupRoutes(fleetSetupRouter, config);
|
||||
registerAgentPolicyRoutes({
|
||||
fleetSetup: fleetSetupRouter,
|
||||
superuser: superuserRouter,
|
||||
});
|
||||
registerPackagePolicyRoutes(superuserRouter);
|
||||
registerOutputRoutes(superuserRouter);
|
||||
registerSettingsRoutes(superuserRouter);
|
||||
registerDataStreamRoutes(superuserRouter);
|
||||
registerPreconfigurationRoutes(superuserRouter);
|
||||
|
||||
// Conditional config routes
|
||||
if (config.agents.enabled) {
|
||||
registerAgentAPIRoutes(routerSuperuserOnly, config);
|
||||
registerEnrollmentApiKeyRoutes(routerSuperuserOnly);
|
||||
registerAgentAPIRoutes(superuserRouter, config);
|
||||
registerEnrollmentApiKeyRoutes({
|
||||
fleetSetup: fleetSetupRouter,
|
||||
superuser: superuserRouter,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IRouter } from 'src/core/server';
|
||||
|
||||
import { PLUGIN_ID, AGENT_POLICY_API_ROUTES } from '../../constants';
|
||||
import {
|
||||
GetAgentPoliciesRequestSchema,
|
||||
|
@ -17,6 +15,7 @@ import {
|
|||
DeleteAgentPolicyRequestSchema,
|
||||
GetFullAgentPolicyRequestSchema,
|
||||
} from '../../types';
|
||||
import type { FleetRouter } from '../../types/request_context';
|
||||
|
||||
import {
|
||||
getAgentPoliciesHandler,
|
||||
|
@ -29,19 +28,21 @@ import {
|
|||
downloadFullAgentPolicy,
|
||||
} from './handlers';
|
||||
|
||||
export const registerRoutes = (router: IRouter) => {
|
||||
// List
|
||||
router.get(
|
||||
export const registerRoutes = (routers: { superuser: FleetRouter; fleetSetup: FleetRouter }) => {
|
||||
// List - Fleet Server needs access to run setup
|
||||
routers.fleetSetup.get(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.LIST_PATTERN,
|
||||
validate: GetAgentPoliciesRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
// Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0
|
||||
// Required to allow elastic/fleet-server to access this API.
|
||||
// options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
getAgentPoliciesHandler
|
||||
);
|
||||
|
||||
// Get one
|
||||
router.get(
|
||||
routers.superuser.get(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.INFO_PATTERN,
|
||||
validate: GetOneAgentPolicyRequestSchema,
|
||||
|
@ -51,7 +52,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
);
|
||||
|
||||
// Create
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN,
|
||||
validate: CreateAgentPolicyRequestSchema,
|
||||
|
@ -61,7 +62,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
);
|
||||
|
||||
// Update
|
||||
router.put(
|
||||
routers.superuser.put(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.UPDATE_PATTERN,
|
||||
validate: UpdateAgentPolicyRequestSchema,
|
||||
|
@ -71,7 +72,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
);
|
||||
|
||||
// Copy
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.COPY_PATTERN,
|
||||
validate: CopyAgentPolicyRequestSchema,
|
||||
|
@ -81,7 +82,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
);
|
||||
|
||||
// Delete
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.DELETE_PATTERN,
|
||||
validate: DeleteAgentPolicyRequestSchema,
|
||||
|
@ -91,7 +92,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
);
|
||||
|
||||
// Get one full agent policy
|
||||
router.get(
|
||||
routers.superuser.get(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.FULL_INFO_PATTERN,
|
||||
validate: GetFullAgentPolicyRequestSchema,
|
||||
|
@ -101,7 +102,7 @@ export const registerRoutes = (router: IRouter) => {
|
|||
);
|
||||
|
||||
// Download one full agent policy
|
||||
router.get(
|
||||
routers.superuser.get(
|
||||
{
|
||||
path: AGENT_POLICY_API_ROUTES.FULL_INFO_DOWNLOAD_PATTERN,
|
||||
validate: GetFullAgentPolicyRequestSchema,
|
||||
|
|
|
@ -27,7 +27,8 @@ export const getEnrollmentApiKeysHandler: RequestHandler<
|
|||
undefined,
|
||||
TypeOf<typeof GetEnrollmentAPIKeysRequestSchema.query>
|
||||
> = async (context, request, response) => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
// Use kibana_system and depend on authz checks on HTTP layer to prevent abuse
|
||||
const esClient = context.core.elasticsearch.client.asInternalUser;
|
||||
|
||||
try {
|
||||
const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys(esClient, {
|
||||
|
@ -87,7 +88,8 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler<
|
|||
export const getOneEnrollmentApiKeyHandler: RequestHandler<
|
||||
TypeOf<typeof GetOneEnrollmentAPIKeyRequestSchema.params>
|
||||
> = async (context, request, response) => {
|
||||
const esClient = context.core.elasticsearch.client.asCurrentUser;
|
||||
// Use kibana_system and depend on authz checks on HTTP layer to prevent abuse
|
||||
const esClient = context.core.elasticsearch.client.asInternalUser;
|
||||
try {
|
||||
const apiKey = await APIKeyService.getEnrollmentAPIKey(esClient, request.params.keyId);
|
||||
const body: GetOneEnrollmentAPIKeyResponse = { item: apiKey };
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IRouter } from 'src/core/server';
|
||||
|
||||
import { PLUGIN_ID, ENROLLMENT_API_KEY_ROUTES } from '../../constants';
|
||||
import {
|
||||
GetEnrollmentAPIKeysRequestSchema,
|
||||
|
@ -14,6 +12,7 @@ import {
|
|||
DeleteEnrollmentAPIKeyRequestSchema,
|
||||
PostEnrollmentAPIKeyRequestSchema,
|
||||
} from '../../types';
|
||||
import type { FleetRouter } from '../../types/request_context';
|
||||
|
||||
import {
|
||||
getEnrollmentApiKeysHandler,
|
||||
|
@ -22,17 +21,19 @@ import {
|
|||
postEnrollmentApiKeyHandler,
|
||||
} from './handler';
|
||||
|
||||
export const registerRoutes = (router: IRouter) => {
|
||||
router.get(
|
||||
export const registerRoutes = (routers: { superuser: FleetRouter; fleetSetup: FleetRouter }) => {
|
||||
routers.fleetSetup.get(
|
||||
{
|
||||
path: ENROLLMENT_API_KEY_ROUTES.INFO_PATTERN,
|
||||
validate: GetOneEnrollmentAPIKeyRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
// Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0
|
||||
// Required to allow elastic/fleet-server to access this API.
|
||||
// options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
getOneEnrollmentApiKeyHandler
|
||||
);
|
||||
|
||||
router.delete(
|
||||
routers.superuser.delete(
|
||||
{
|
||||
path: ENROLLMENT_API_KEY_ROUTES.DELETE_PATTERN,
|
||||
validate: DeleteEnrollmentAPIKeyRequestSchema,
|
||||
|
@ -41,16 +42,18 @@ export const registerRoutes = (router: IRouter) => {
|
|||
deleteEnrollmentApiKeyHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
routers.fleetSetup.get(
|
||||
{
|
||||
path: ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN,
|
||||
validate: GetEnrollmentAPIKeysRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
// Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0
|
||||
// Required to allow elastic/fleet-server to access this API.
|
||||
// options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
getEnrollmentApiKeysHandler
|
||||
);
|
||||
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: ENROLLMENT_API_KEY_ROUTES.CREATE_PATTERN,
|
||||
validate: PostEnrollmentAPIKeyRequestSchema,
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IRouter } from 'src/core/server';
|
||||
|
||||
import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants';
|
||||
import type { FleetRequestHandlerContext } from '../../types';
|
||||
import {
|
||||
GetCategoriesRequestSchema,
|
||||
GetPackagesRequestSchema,
|
||||
|
@ -21,7 +18,7 @@ import {
|
|||
GetStatsRequestSchema,
|
||||
UpdatePackageRequestSchema,
|
||||
} from '../../types';
|
||||
import { enforceSuperUser } from '../security';
|
||||
import type { FleetRouter } from '../../types/request_context';
|
||||
|
||||
import {
|
||||
getCategoriesHandler,
|
||||
|
@ -39,8 +36,8 @@ import {
|
|||
|
||||
const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
|
||||
|
||||
export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
||||
router.get(
|
||||
export const registerRoutes = (routers: { rbac: FleetRouter; superuser: FleetRouter }) => {
|
||||
routers.rbac.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.CATEGORIES_PATTERN,
|
||||
validate: GetCategoriesRequestSchema,
|
||||
|
@ -49,7 +46,7 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
getCategoriesHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
routers.rbac.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.LIST_PATTERN,
|
||||
validate: GetPackagesRequestSchema,
|
||||
|
@ -58,7 +55,7 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
getListHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
routers.rbac.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.LIMITED_LIST_PATTERN,
|
||||
validate: false,
|
||||
|
@ -67,7 +64,7 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
getLimitedListHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
routers.rbac.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.STATS_PATTERN,
|
||||
validate: GetStatsRequestSchema,
|
||||
|
@ -76,7 +73,7 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
getStatsHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
routers.rbac.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.FILEPATH_PATTERN,
|
||||
validate: GetFileRequestSchema,
|
||||
|
@ -85,7 +82,7 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
getFileHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
routers.rbac.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.INFO_PATTERN,
|
||||
validate: GetInfoRequestSchema,
|
||||
|
@ -94,34 +91,34 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
getInfoHandler
|
||||
);
|
||||
|
||||
router.put(
|
||||
routers.superuser.put(
|
||||
{
|
||||
path: EPM_API_ROUTES.INFO_PATTERN,
|
||||
validate: UpdatePackageRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
enforceSuperUser(updatePackageHandler)
|
||||
updatePackageHandler
|
||||
);
|
||||
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN,
|
||||
validate: InstallPackageFromRegistryRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
enforceSuperUser(installPackageFromRegistryHandler)
|
||||
installPackageFromRegistryHandler
|
||||
);
|
||||
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: EPM_API_ROUTES.BULK_INSTALL_PATTERN,
|
||||
validate: BulkUpgradePackagesFromRegistryRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
enforceSuperUser(bulkInstallPackagesFromRegistryHandler)
|
||||
bulkInstallPackagesFromRegistryHandler
|
||||
);
|
||||
|
||||
router.post(
|
||||
routers.superuser.post(
|
||||
{
|
||||
path: EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN,
|
||||
validate: InstallPackageByUploadRequestSchema,
|
||||
|
@ -134,15 +131,15 @@ export const registerRoutes = (router: IRouter<FleetRequestHandlerContext>) => {
|
|||
},
|
||||
},
|
||||
},
|
||||
enforceSuperUser(installPackageByUploadHandler)
|
||||
installPackageByUploadHandler
|
||||
);
|
||||
|
||||
router.delete(
|
||||
routers.superuser.delete(
|
||||
{
|
||||
path: EPM_API_ROUTES.DELETE_PATTERN,
|
||||
validate: DeletePackageRequestSchema,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
enforceSuperUser(deletePackageHandler)
|
||||
deletePackageHandler
|
||||
);
|
||||
};
|
||||
|
|
175
x-pack/plugins/fleet/server/routes/security.test.ts
Normal file
175
x-pack/plugins/fleet/server/routes/security.test.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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 type { IRouter, RequestHandler, RouteConfig } from '../../../../../src/core/server';
|
||||
import { coreMock } from '../../../../../src/core/server/mocks';
|
||||
import type { AuthenticatedUser } from '../../../security/server';
|
||||
import type { CheckPrivilegesDynamically } from '../../../security/server/authorization/check_privileges_dynamically';
|
||||
import { createAppContextStartContractMock } from '../mocks';
|
||||
import { appContextService } from '../services';
|
||||
|
||||
import type { RouterWrapper } from './security';
|
||||
import { RouterWrappers } from './security';
|
||||
|
||||
describe('RouterWrappers', () => {
|
||||
const runTest = async ({
|
||||
wrapper,
|
||||
security: {
|
||||
roles = [],
|
||||
pluginEnabled = true,
|
||||
licenseEnabled = true,
|
||||
checkPrivilegesDynamically,
|
||||
} = {},
|
||||
}: {
|
||||
wrapper: RouterWrapper;
|
||||
security?: {
|
||||
roles?: string[];
|
||||
pluginEnabled?: boolean;
|
||||
licenseEnabled?: boolean;
|
||||
checkPrivilegesDynamically?: CheckPrivilegesDynamically;
|
||||
};
|
||||
}) => {
|
||||
const fakeRouter = {
|
||||
get: jest.fn(),
|
||||
} as unknown as jest.Mocked<IRouter>;
|
||||
const fakeHandler: RequestHandler = jest.fn((ctx, req, res) => res.ok());
|
||||
|
||||
const mockContext = createAppContextStartContractMock();
|
||||
// @ts-expect-error type doesn't properly respect deeply mocked keys
|
||||
mockContext.securityStart?.authz.actions.api.get.mockImplementation((priv) => `api:${priv}`);
|
||||
|
||||
if (!pluginEnabled) {
|
||||
mockContext.securitySetup = undefined;
|
||||
mockContext.securityStart = undefined;
|
||||
} else {
|
||||
mockContext.securityStart?.authc.getCurrentUser.mockReturnValue({
|
||||
username: 'foo',
|
||||
roles,
|
||||
} as unknown as AuthenticatedUser);
|
||||
|
||||
mockContext.securitySetup?.license.isEnabled.mockReturnValue(licenseEnabled);
|
||||
if (licenseEnabled) {
|
||||
mockContext.securityStart?.authz.mode.useRbacForRequest.mockReturnValue(true);
|
||||
}
|
||||
|
||||
if (checkPrivilegesDynamically) {
|
||||
mockContext.securityStart?.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue(
|
||||
checkPrivilegesDynamically
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
appContextService.start(mockContext);
|
||||
|
||||
const wrappedRouter = wrapper(fakeRouter);
|
||||
wrappedRouter.get({} as RouteConfig<any, any, any, any>, fakeHandler);
|
||||
const wrappedHandler = fakeRouter.get.mock.calls[0][1];
|
||||
const resFactory = { forbidden: jest.fn(() => 'forbidden'), ok: jest.fn(() => 'ok') };
|
||||
const res = await wrappedHandler(
|
||||
{ core: coreMock.createRequestHandlerContext() },
|
||||
{} as any,
|
||||
resFactory as any
|
||||
);
|
||||
|
||||
return res as unknown as 'forbidden' | 'ok';
|
||||
};
|
||||
|
||||
describe('require.superuser', () => {
|
||||
it('allow users with the superuser role', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.superuser,
|
||||
security: { roles: ['superuser'] },
|
||||
})
|
||||
).toEqual('ok');
|
||||
});
|
||||
|
||||
it('does not allow users without the superuser role', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.superuser,
|
||||
security: { roles: ['foo'] },
|
||||
})
|
||||
).toEqual('forbidden');
|
||||
});
|
||||
|
||||
it('does not allow security plugin to be disabled', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.superuser,
|
||||
security: { pluginEnabled: false },
|
||||
})
|
||||
).toEqual('forbidden');
|
||||
});
|
||||
|
||||
it('does not allow security license to be disabled', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.superuser,
|
||||
security: { licenseEnabled: false },
|
||||
})
|
||||
).toEqual('forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('require.fleetSetupPrivilege', () => {
|
||||
const mockCheckPrivileges: jest.Mock<
|
||||
ReturnType<CheckPrivilegesDynamically>,
|
||||
Parameters<CheckPrivilegesDynamically>
|
||||
> = jest.fn().mockResolvedValue({ hasAllRequested: true });
|
||||
|
||||
it('executes custom authz check', async () => {
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.fleetSetupPrivilege,
|
||||
security: { checkPrivilegesDynamically: mockCheckPrivileges },
|
||||
});
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith(
|
||||
{ kibana: ['api:fleet-setup'] },
|
||||
{
|
||||
requireLoginAction: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('allow users with required privileges', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.fleetSetupPrivilege,
|
||||
security: { checkPrivilegesDynamically: mockCheckPrivileges },
|
||||
})
|
||||
).toEqual('ok');
|
||||
});
|
||||
|
||||
it('does not allow users without required privileges', async () => {
|
||||
mockCheckPrivileges.mockResolvedValueOnce({ hasAllRequested: false } as any);
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.fleetSetupPrivilege,
|
||||
security: { checkPrivilegesDynamically: mockCheckPrivileges },
|
||||
})
|
||||
).toEqual('forbidden');
|
||||
});
|
||||
|
||||
it('does not allow security plugin to be disabled', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.fleetSetupPrivilege,
|
||||
security: { pluginEnabled: false },
|
||||
})
|
||||
).toEqual('forbidden');
|
||||
});
|
||||
|
||||
it('does not allow security license to be disabled', async () => {
|
||||
expect(
|
||||
await runTest({
|
||||
wrapper: RouterWrappers.require.fleetSetupPrivilege,
|
||||
security: { licenseEnabled: false },
|
||||
})
|
||||
).toEqual('forbidden');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,56 +5,137 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IRouter, RequestHandler, RequestHandlerContext } from 'src/core/server';
|
||||
import type {
|
||||
IRouter,
|
||||
KibanaRequest,
|
||||
RequestHandler,
|
||||
RequestHandlerContext,
|
||||
} from 'src/core/server';
|
||||
|
||||
import { appContextService } from '../services';
|
||||
|
||||
export function enforceSuperUser<T1, T2, T3, TContext extends RequestHandlerContext>(
|
||||
const SUPERUSER_AUTHZ_MESSAGE =
|
||||
'Access to Fleet API requires the superuser role and for stack security features to be enabled.';
|
||||
|
||||
function checkSecurityEnabled() {
|
||||
return appContextService.hasSecurity() && appContextService.getSecurityLicense().isEnabled();
|
||||
}
|
||||
|
||||
function checkSuperuser(req: KibanaRequest) {
|
||||
if (!checkSecurityEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const security = appContextService.getSecurity();
|
||||
const user = security.authc.getCurrentUser(req);
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userRoles = user.roles || [];
|
||||
if (!userRoles.includes('superuser')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function enforceSuperuser<T1, T2, T3, TContext extends RequestHandlerContext>(
|
||||
handler: RequestHandler<T1, T2, T3, TContext>
|
||||
): RequestHandler<T1, T2, T3, TContext> {
|
||||
return function enforceSuperHandler(context, req, res) {
|
||||
if (!appContextService.hasSecurity() || !appContextService.getSecurityLicense().isEnabled()) {
|
||||
const isSuperuser = checkSuperuser(req);
|
||||
if (!isSuperuser) {
|
||||
return res.forbidden({
|
||||
body: {
|
||||
message: `Access to this API requires that security is enabled`,
|
||||
message: SUPERUSER_AUTHZ_MESSAGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const security = appContextService.getSecurity();
|
||||
const user = security.authc.getCurrentUser(req);
|
||||
if (!user) {
|
||||
return res.forbidden({
|
||||
body: {
|
||||
message:
|
||||
'Access to Fleet API require the superuser role, and for stack security features to be enabled.',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const userRoles = user.roles || [];
|
||||
if (!userRoles.includes('superuser')) {
|
||||
return res.forbidden({
|
||||
body: {
|
||||
message: 'Access to Fleet API require the superuser role.',
|
||||
},
|
||||
});
|
||||
}
|
||||
return handler(context, req, res);
|
||||
};
|
||||
}
|
||||
|
||||
export function makeRouterEnforcingSuperuser<TContext extends RequestHandlerContext>(
|
||||
function makeRouterEnforcingSuperuser<TContext extends RequestHandlerContext>(
|
||||
router: IRouter<TContext>
|
||||
): IRouter<TContext> {
|
||||
return {
|
||||
get: (options, handler) => router.get(options, enforceSuperUser(handler)),
|
||||
delete: (options, handler) => router.delete(options, enforceSuperUser(handler)),
|
||||
post: (options, handler) => router.post(options, enforceSuperUser(handler)),
|
||||
put: (options, handler) => router.put(options, enforceSuperUser(handler)),
|
||||
patch: (options, handler) => router.patch(options, enforceSuperUser(handler)),
|
||||
get: (options, handler) => router.get(options, enforceSuperuser(handler)),
|
||||
delete: (options, handler) => router.delete(options, enforceSuperuser(handler)),
|
||||
post: (options, handler) => router.post(options, enforceSuperuser(handler)),
|
||||
put: (options, handler) => router.put(options, enforceSuperuser(handler)),
|
||||
patch: (options, handler) => router.patch(options, enforceSuperuser(handler)),
|
||||
handleLegacyErrors: (handler) => router.handleLegacyErrors(handler),
|
||||
getRoutes: () => router.getRoutes(),
|
||||
routerPath: router.routerPath,
|
||||
};
|
||||
}
|
||||
|
||||
async function checkFleetSetupPrivilege(req: KibanaRequest) {
|
||||
if (!checkSecurityEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const security = appContextService.getSecurity();
|
||||
|
||||
if (security.authz.mode.useRbacForRequest(req)) {
|
||||
const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req);
|
||||
const { hasAllRequested } = await checkPrivileges(
|
||||
{ kibana: [security.authz.actions.api.get('fleet-setup')] },
|
||||
{ requireLoginAction: false } // exclude login access requirement
|
||||
);
|
||||
|
||||
return !!hasAllRequested;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function enforceFleetSetupPrivilege<P, Q, B, TContext extends RequestHandlerContext>(
|
||||
handler: RequestHandler<P, Q, B, TContext>
|
||||
): RequestHandler<P, Q, B, TContext> {
|
||||
return async (context, req, res) => {
|
||||
const hasFleetSetupPrivilege = await checkFleetSetupPrivilege(req);
|
||||
if (!hasFleetSetupPrivilege) {
|
||||
return res.forbidden({ body: { message: SUPERUSER_AUTHZ_MESSAGE } });
|
||||
}
|
||||
|
||||
return handler(context, req, res);
|
||||
};
|
||||
}
|
||||
|
||||
function makeRouterEnforcingFleetSetupPrivilege<TContext extends RequestHandlerContext>(
|
||||
router: IRouter<TContext>
|
||||
): IRouter<TContext> {
|
||||
return {
|
||||
get: (options, handler) => router.get(options, enforceFleetSetupPrivilege(handler)),
|
||||
delete: (options, handler) => router.delete(options, enforceFleetSetupPrivilege(handler)),
|
||||
post: (options, handler) => router.post(options, enforceFleetSetupPrivilege(handler)),
|
||||
put: (options, handler) => router.put(options, enforceFleetSetupPrivilege(handler)),
|
||||
patch: (options, handler) => router.patch(options, enforceFleetSetupPrivilege(handler)),
|
||||
handleLegacyErrors: (handler) => router.handleLegacyErrors(handler),
|
||||
getRoutes: () => router.getRoutes(),
|
||||
routerPath: router.routerPath,
|
||||
};
|
||||
}
|
||||
|
||||
export type RouterWrapper = <T extends RequestHandlerContext>(route: IRouter<T>) => IRouter<T>;
|
||||
|
||||
interface RouterWrappersSetup {
|
||||
require: {
|
||||
superuser: RouterWrapper;
|
||||
fleetSetupPrivilege: RouterWrapper;
|
||||
};
|
||||
}
|
||||
|
||||
export const RouterWrappers: RouterWrappersSetup = {
|
||||
require: {
|
||||
superuser: (router) => {
|
||||
return makeRouterEnforcingSuperuser(router);
|
||||
},
|
||||
fleetSetupPrivilege: (router) => {
|
||||
return makeRouterEnforcingFleetSetupPrivilege(router);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { RequestHandler } from 'src/core/server';
|
||||
|
||||
import { appContextService } from '../../services';
|
||||
import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common';
|
||||
import { setupFleet } from '../../services/setup';
|
||||
|
@ -14,12 +12,14 @@ import { hasFleetServers } from '../../services/fleet_server';
|
|||
import { defaultIngestErrorHandler } from '../../errors';
|
||||
import type { FleetRequestHandler } from '../../types';
|
||||
|
||||
export const getFleetStatusHandler: RequestHandler = async (context, request, response) => {
|
||||
export const getFleetStatusHandler: FleetRequestHandler = async (context, request, response) => {
|
||||
try {
|
||||
const isApiKeysEnabled = await appContextService
|
||||
.getSecurity()
|
||||
.authc.apiKeys.areAPIKeysEnabled();
|
||||
const isFleetServerSetup = await hasFleetServers(appContextService.getInternalUserESClient());
|
||||
const isFleetServerSetup = await hasFleetServers(
|
||||
context.core.elasticsearch.client.asInternalUser
|
||||
);
|
||||
|
||||
const missingRequirements: GetFleetStatusResponse['missing_requirements'] = [];
|
||||
if (!isApiKeysEnabled) {
|
||||
|
|
|
@ -5,55 +5,48 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IRouter } from 'src/core/server';
|
||||
|
||||
import { PLUGIN_ID, AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants';
|
||||
import type { FleetConfigType } from '../../../common';
|
||||
|
||||
import type { FleetRequestHandlerContext } from '../../types/request_context';
|
||||
import type { FleetRouter } from '../../types/request_context';
|
||||
|
||||
import { getFleetStatusHandler, fleetSetupHandler } from './handlers';
|
||||
|
||||
export const registerFleetSetupRoute = (router: IRouter<FleetRequestHandlerContext>) => {
|
||||
export const registerFleetSetupRoute = (router: FleetRouter) => {
|
||||
router.post(
|
||||
{
|
||||
path: SETUP_API_ROUTE,
|
||||
validate: false,
|
||||
// if this route is set to `-all`, a read-only user get a 404 for this route
|
||||
// and will see `Unable to initialize Ingest Manager` in the UI
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
fleetSetupHandler
|
||||
);
|
||||
};
|
||||
|
||||
// That route is used by agent to setup Fleet
|
||||
export const registerCreateFleetSetupRoute = (router: IRouter<FleetRequestHandlerContext>) => {
|
||||
export const registerCreateFleetSetupRoute = (router: FleetRouter) => {
|
||||
router.post(
|
||||
{
|
||||
path: AGENTS_SETUP_API_ROUTES.CREATE_PATTERN,
|
||||
validate: false,
|
||||
options: { tags: [`access:${PLUGIN_ID}-all`] },
|
||||
},
|
||||
fleetSetupHandler
|
||||
);
|
||||
};
|
||||
|
||||
export const registerGetFleetStatusRoute = (router: IRouter<FleetRequestHandlerContext>) => {
|
||||
export const registerGetFleetStatusRoute = (router: FleetRouter) => {
|
||||
router.get(
|
||||
{
|
||||
path: AGENTS_SETUP_API_ROUTES.INFO_PATTERN,
|
||||
validate: false,
|
||||
// Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0
|
||||
// Required to allow elastic/fleet-server to access this API.
|
||||
options: { tags: [`access:${PLUGIN_ID}-read`] },
|
||||
},
|
||||
getFleetStatusHandler
|
||||
);
|
||||
};
|
||||
|
||||
export const registerRoutes = (
|
||||
router: IRouter<FleetRequestHandlerContext>,
|
||||
config: FleetConfigType
|
||||
) => {
|
||||
export const registerRoutes = (router: FleetRouter, config: FleetConfigType) => {
|
||||
// Ingest manager setup
|
||||
registerFleetSetupRoute(router);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
RequestHandlerContext,
|
||||
RouteMethod,
|
||||
SavedObjectsClientContract,
|
||||
IRouter,
|
||||
} from '../../../../../src/core/server';
|
||||
|
||||
/** @internal */
|
||||
|
@ -37,3 +38,9 @@ export type FleetRequestHandler<
|
|||
Method extends RouteMethod = any,
|
||||
ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory
|
||||
> = RequestHandler<P, Q, B, FleetRequestHandlerContext, Method, ResponseFactory>;
|
||||
|
||||
/**
|
||||
* Convenience type for routers in Fleet that includes the FleetRequestHandlerContext type
|
||||
* @internal
|
||||
*/
|
||||
export type FleetRouter = IRouter<FleetRequestHandlerContext>;
|
||||
|
|
|
@ -878,6 +878,42 @@ describe('#atSpace', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('omits login privilege when requireLoginAction: false', async () => {
|
||||
const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient({
|
||||
has_all_requested: true,
|
||||
username: 'foo-username',
|
||||
index: {},
|
||||
application: {
|
||||
[application]: {
|
||||
'space:space_1': {
|
||||
[mockActions.version]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(
|
||||
mockActions,
|
||||
() => Promise.resolve(mockClusterClient),
|
||||
application
|
||||
);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const checkPrivileges = checkPrivilegesWithRequest(request);
|
||||
await checkPrivileges.atSpace('space_1', {}, { requireLoginAction: false });
|
||||
|
||||
expect(mockScopedClusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
|
||||
body: {
|
||||
index: [],
|
||||
application: [
|
||||
{
|
||||
application,
|
||||
resources: [`space:space_1`],
|
||||
privileges: [mockActions.version],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#atSpaces', () => {
|
||||
|
@ -2083,6 +2119,42 @@ describe('#atSpaces', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('omits login privilege when requireLoginAction: false', async () => {
|
||||
const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient({
|
||||
has_all_requested: true,
|
||||
username: 'foo-username',
|
||||
index: {},
|
||||
application: {
|
||||
[application]: {
|
||||
'space:space_1': {
|
||||
[mockActions.version]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(
|
||||
mockActions,
|
||||
() => Promise.resolve(mockClusterClient),
|
||||
application
|
||||
);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const checkPrivileges = checkPrivilegesWithRequest(request);
|
||||
await checkPrivileges.atSpaces(['space_1'], {}, { requireLoginAction: false });
|
||||
|
||||
expect(mockScopedClusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
|
||||
body: {
|
||||
index: [],
|
||||
application: [
|
||||
{
|
||||
application,
|
||||
resources: [`space:space_1`],
|
||||
privileges: [mockActions.version],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#globally', () => {
|
||||
|
@ -2937,4 +3009,40 @@ describe('#globally', () => {
|
|||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('omits login privilege when requireLoginAction: false', async () => {
|
||||
const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient({
|
||||
has_all_requested: true,
|
||||
username: 'foo-username',
|
||||
index: {},
|
||||
application: {
|
||||
[application]: {
|
||||
[GLOBAL_RESOURCE]: {
|
||||
[mockActions.version]: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(
|
||||
mockActions,
|
||||
() => Promise.resolve(mockClusterClient),
|
||||
application
|
||||
);
|
||||
const request = httpServerMock.createKibanaRequest();
|
||||
const checkPrivileges = checkPrivilegesWithRequest(request);
|
||||
await checkPrivileges.globally({}, { requireLoginAction: false });
|
||||
|
||||
expect(mockScopedClusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
|
||||
body: {
|
||||
index: [],
|
||||
application: [
|
||||
{
|
||||
application,
|
||||
resources: [GLOBAL_RESOURCE],
|
||||
privileges: [mockActions.version],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { GLOBAL_RESOURCE } from '../../common/constants';
|
|||
import { ResourceSerializer } from './resource_serializer';
|
||||
import type {
|
||||
CheckPrivileges,
|
||||
CheckPrivilegesOptions,
|
||||
CheckPrivilegesPayload,
|
||||
CheckPrivilegesResponse,
|
||||
HasPrivilegesResponse,
|
||||
|
@ -41,14 +42,20 @@ export function checkPrivilegesWithRequestFactory(
|
|||
return function checkPrivilegesWithRequest(request: KibanaRequest): CheckPrivileges {
|
||||
const checkPrivilegesAtResources = async (
|
||||
resources: string[],
|
||||
privileges: CheckPrivilegesPayload
|
||||
privileges: CheckPrivilegesPayload,
|
||||
{ requireLoginAction = true }: CheckPrivilegesOptions = {}
|
||||
): Promise<CheckPrivilegesResponse> => {
|
||||
const kibanaPrivileges = Array.isArray(privileges.kibana)
|
||||
? privileges.kibana
|
||||
: privileges.kibana
|
||||
? [privileges.kibana]
|
||||
: [];
|
||||
const allApplicationPrivileges = uniq([actions.version, actions.login, ...kibanaPrivileges]);
|
||||
|
||||
const allApplicationPrivileges = uniq([
|
||||
actions.version,
|
||||
...(requireLoginAction ? [actions.login] : []),
|
||||
...kibanaPrivileges,
|
||||
]);
|
||||
|
||||
const clusterClient = await getClusterClient();
|
||||
const { body } = await clusterClient.asScoped(request).asCurrentUser.security.hasPrivileges({
|
||||
|
@ -135,18 +142,26 @@ export function checkPrivilegesWithRequestFactory(
|
|||
};
|
||||
|
||||
return {
|
||||
async atSpace(spaceId: string, privileges: CheckPrivilegesPayload) {
|
||||
async atSpace(
|
||||
spaceId: string,
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
) {
|
||||
const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId);
|
||||
return await checkPrivilegesAtResources([spaceResource], privileges);
|
||||
return await checkPrivilegesAtResources([spaceResource], privileges, options);
|
||||
},
|
||||
async atSpaces(spaceIds: string[], privileges: CheckPrivilegesPayload) {
|
||||
async atSpaces(
|
||||
spaceIds: string[],
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
) {
|
||||
const spaceResources = spaceIds.map((spaceId) =>
|
||||
ResourceSerializer.serializeSpaceResource(spaceId)
|
||||
);
|
||||
return await checkPrivilegesAtResources(spaceResources, privileges);
|
||||
return await checkPrivilegesAtResources(spaceResources, privileges, options);
|
||||
},
|
||||
async globally(privileges: CheckPrivilegesPayload) {
|
||||
return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privileges);
|
||||
async globally(privileges: CheckPrivilegesPayload, options?: CheckPrivilegesOptions) {
|
||||
return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privileges, options);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { httpServerMock } from 'src/core/server/mocks';
|
||||
|
||||
import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically';
|
||||
import type { CheckPrivilegesOptions } from './types';
|
||||
|
||||
test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
|
||||
const expectedResult = Symbol();
|
||||
|
@ -25,13 +26,18 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
|
|||
namespaceToSpaceId: jest.fn(),
|
||||
})
|
||||
)(request);
|
||||
const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges });
|
||||
const options: CheckPrivilegesOptions = { requireLoginAction: true };
|
||||
const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }, options);
|
||||
|
||||
expect(result).toBe(expectedResult);
|
||||
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, {
|
||||
kibana: privilegeOrPrivileges,
|
||||
});
|
||||
expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(
|
||||
spaceId,
|
||||
{
|
||||
kibana: privilegeOrPrivileges,
|
||||
},
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
test(`checkPrivileges.globally when spaces is disabled`, async () => {
|
||||
|
@ -46,9 +52,13 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => {
|
|||
mockCheckPrivilegesWithRequest,
|
||||
() => undefined
|
||||
)(request);
|
||||
const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges });
|
||||
const options: CheckPrivilegesOptions = { requireLoginAction: true };
|
||||
const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }, options);
|
||||
|
||||
expect(result).toBe(expectedResult);
|
||||
expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request);
|
||||
expect(mockCheckPrivileges.globally).toHaveBeenCalledWith({ kibana: privilegeOrPrivileges });
|
||||
expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(
|
||||
{ kibana: privilegeOrPrivileges },
|
||||
options
|
||||
);
|
||||
});
|
||||
|
|
|
@ -9,13 +9,15 @@ import type { KibanaRequest } from 'src/core/server';
|
|||
|
||||
import type { SpacesService } from '../plugin';
|
||||
import type {
|
||||
CheckPrivilegesOptions,
|
||||
CheckPrivilegesPayload,
|
||||
CheckPrivilegesResponse,
|
||||
CheckPrivilegesWithRequest,
|
||||
} from './types';
|
||||
|
||||
export type CheckPrivilegesDynamically = (
|
||||
privileges: CheckPrivilegesPayload
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
) => Promise<CheckPrivilegesResponse>;
|
||||
|
||||
export type CheckPrivilegesDynamicallyWithRequest = (
|
||||
|
@ -28,11 +30,15 @@ export function checkPrivilegesDynamicallyWithRequestFactory(
|
|||
): CheckPrivilegesDynamicallyWithRequest {
|
||||
return function checkPrivilegesDynamicallyWithRequest(request: KibanaRequest) {
|
||||
const checkPrivileges = checkPrivilegesWithRequest(request);
|
||||
return async function checkPrivilegesDynamically(privileges: CheckPrivilegesPayload) {
|
||||
|
||||
return async function checkPrivilegesDynamically(
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
) {
|
||||
const spacesService = getSpacesService();
|
||||
return spacesService
|
||||
? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privileges)
|
||||
: await checkPrivileges.globally(privileges);
|
||||
? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privileges, options)
|
||||
: await checkPrivileges.globally(privileges, options);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,6 +29,18 @@ export interface HasPrivilegesResponse {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to influce the privilege checks.
|
||||
*/
|
||||
export interface CheckPrivilegesOptions {
|
||||
/**
|
||||
* Whether or not the `login` action should be required (default: true).
|
||||
* Setting this to false is not advised except for special circumstances, when you do not require
|
||||
* the request to belong to a user capable of logging into Kibana.
|
||||
*/
|
||||
requireLoginAction?: boolean;
|
||||
}
|
||||
|
||||
export interface CheckPrivilegesResponse {
|
||||
hasAllRequested: boolean;
|
||||
username: string;
|
||||
|
@ -59,12 +71,20 @@ export interface CheckPrivilegesResponse {
|
|||
export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges;
|
||||
|
||||
export interface CheckPrivileges {
|
||||
atSpace(spaceId: string, privileges: CheckPrivilegesPayload): Promise<CheckPrivilegesResponse>;
|
||||
atSpace(
|
||||
spaceId: string,
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
): Promise<CheckPrivilegesResponse>;
|
||||
atSpaces(
|
||||
spaceIds: string[],
|
||||
privileges: CheckPrivilegesPayload
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
): Promise<CheckPrivilegesResponse>;
|
||||
globally(
|
||||
privileges: CheckPrivilegesPayload,
|
||||
options?: CheckPrivilegesOptions
|
||||
): Promise<CheckPrivilegesResponse>;
|
||||
globally(privileges: CheckPrivilegesPayload): Promise<CheckPrivilegesResponse>;
|
||||
}
|
||||
|
||||
export interface CheckPrivilegesPayload {
|
||||
|
|
|
@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'packs_read',
|
||||
],
|
||||
},
|
||||
reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
};
|
||||
|
||||
await supertest
|
||||
|
|
|
@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
global: ['all', 'read'],
|
||||
space: ['all', 'read'],
|
||||
reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'],
|
||||
};
|
||||
|
||||
await supertest
|
||||
|
|
|
@ -32,12 +32,30 @@ export function getEsClientForAPIKey({ getService }: FtrProviderContext, esApiKe
|
|||
});
|
||||
}
|
||||
|
||||
export function setupFleetAndAgents({ getService }: FtrProviderContext) {
|
||||
export function setupFleetAndAgents(providerContext: FtrProviderContext) {
|
||||
before(async () => {
|
||||
await getService('supertest').post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send();
|
||||
await getService('supertest')
|
||||
// Use elastic/fleet-server service account to execute setup to verify privilege configuration
|
||||
const es = providerContext.getService('es');
|
||||
const {
|
||||
body: { token },
|
||||
// @ts-expect-error SecurityCreateServiceTokenRequest should not require `name`
|
||||
} = await es.security.createServiceToken({
|
||||
namespace: 'elastic',
|
||||
service: 'fleet-server',
|
||||
});
|
||||
const supetestWithoutAuth = getSupertestWithoutAuth(providerContext);
|
||||
|
||||
await supetestWithoutAuth
|
||||
.post(`/api/fleet/setup`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.send()
|
||||
.expect(200);
|
||||
await supetestWithoutAuth
|
||||
.post(`/api/fleet/agents/setup`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.send({ forceRecreate: true });
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.send({ forceRecreate: true })
|
||||
.expect(200);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,7 +14,9 @@ import { setupFleetAndAgents } from '../agents/services';
|
|||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const log = getService('log');
|
||||
const es = getService('es');
|
||||
|
||||
describe('setup api', async () => {
|
||||
skipIfNoDockerRegistry(providerContext);
|
||||
|
@ -47,5 +49,48 @@ export default function (providerContext: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows elastic/fleet-server user to call required APIs', async () => {
|
||||
const {
|
||||
body: { token },
|
||||
// @ts-expect-error SecurityCreateServiceTokenRequest should not require `name`
|
||||
} = await es.security.createServiceToken({
|
||||
namespace: 'elastic',
|
||||
service: 'fleet-server',
|
||||
});
|
||||
|
||||
// elastic/fleet-server needs access to these APIs:
|
||||
// POST /api/fleet/setup
|
||||
// POST /api/fleet/agents/setup
|
||||
// GET /api/fleet/agent_policies
|
||||
// GET /api/fleet/enrollment-api-keys
|
||||
// GET /api/fleet/enrollment-api-keys/<id>
|
||||
await supertestWithoutAuth
|
||||
.post('/api/fleet/setup')
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
await supertestWithoutAuth
|
||||
.post('/api/fleet/agents/setup')
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
await supertestWithoutAuth
|
||||
.get('/api/fleet/agent_policies')
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
const response = await supertestWithoutAuth
|
||||
.get('/api/fleet/enrollment-api-keys')
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
const enrollmentApiKeyId = response.body.list[0].id;
|
||||
await supertestWithoutAuth
|
||||
.get(`/api/fleet/enrollment-api-keys/${enrollmentApiKeyId}`)
|
||||
.set('Authorization', `Bearer ${token.value}`)
|
||||
.set('kbn-xsrf', 'xxx')
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue