[Fleet][Endpoint][RBAC V2] Update fleet router and config to allow API access via RBAC controls (#145361)

## Summary

> **Note**
> This PR is adding changes only to some of `api/fleet/package_policies`
API routes, there will be subsequent PRs after this to update
`api/fleet/epm/packages`, `api/fleet/agent_policeis` and,
`api/fleet/agent_status`.

This PR introduces the framework needed in fleet in order to be able to
support Package level Privileges - meaning: if a user does not have
authorization granted via Fleet and/or Integration privileges, then
package level privileges are check and API access granted. When access
is granted based on Package Privileges, the data is also validated to
ensure that it is limited to the integration package names that were
given authorization to the API.

The following APIs were updated to leverage this new framework:

- Integration Package Policy list API
- Integration Package Policy get one API
- Integration Package Policy update one API
- Integration Package Policy bulk get API

> ℹ️ these API were updated in support of Endpoint use cases needed for
v8.7.

Example of API error for Package policies api:

```json5
{
    "statusCode": 403,
    "error": "Forbidden",
    "message": "Authorization denied to [package.name=fleet_server]. Allowed package.name's: endpoint"
}
```
___________

To test:
1. Log in as `elastic`/superuser and create some agent policies.
1. Under `Stack Management`, create a role `policy_role` with the
following RBAC settings. **DO NOT** select `Fleet -> All` or toggle
`Integrations`. Leave those RBAC toggles set to `None`
<img width="610" alt="Screenshot 2022-11-16 at 14 45 15"
src="https://user-images.githubusercontent.com/1849116/202196962-9123e380-3b8f-4d52-97f9-8af895fb4c26.png">

2. Create a user e.g. `policy_user` and assign them _only_ the above
role. **NOT** `superuser`.

3. Login with this user and navigate to
`app/security/administration/policy` or curl/postman.
4. Expect to see the following:
- GET `api/fleet/epm/packages?category=security` should return a `403`
status.
- GET
`api/fleet/package_policies?page=1&perPage=10&kuery=ingest-package-policies.package.name%3A%20endpoint`
should return a list of policies.
- GET `/api/fleet/package_policies/<packagePolicyId>` should return a
`200` and a signle item that has the policie's details. Note that the
package name of this item is `endpoint`.
- there should be a POST API request matching
`api/fleet/agent_policies/_bulk_get`, and should return a `403`.
5. With `Policy Management` RBAC set to `All`
- PUT
`http://localhost:5601/api/fleet/package_policies/<packagePolicyId>`
should return a `200` with the updated policy details as response

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

Co-authored-by: Paul Tavares <paul.tavares@elastic.co>
This commit is contained in:
Ashokaditya 2022-12-14 16:24:12 +01:00 committed by GitHub
parent d1e7f50426
commit b1a75ae831
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1407 additions and 485 deletions

View file

@ -257,7 +257,7 @@ export const settingsRoutesService = {
};
export const appRoutesService = {
getCheckPermissionsPath: (fleetServerSetup?: boolean) => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getCheckPermissionsPath: () => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN,
postHealthCheckPath: () => APP_API_ROUTES.HEALTH_CHECK_PATTERN,
};

View file

@ -90,10 +90,9 @@ export const createFleetRequestHandlerContextMock = (): jest.Mocked<
asCurrentUser: createPackagePolicyServiceMock(),
asInternalUser: createPackagePolicyServiceMock(),
},
epm: {
internalSoClient: savedObjectsClientMock.create(),
},
internalSoClient: savedObjectsClientMock.create(),
spaceId: 'default',
limitedToPackages: undefined,
};
};

View file

@ -57,6 +57,13 @@ import type { ExperimentalFeatures } from '../common/experimental_features';
import { INTEGRATIONS_PLUGIN_ID } from '../common';
import { parseExperimentalConfigValue } from '../common/experimental_features';
import {
getRouteRequiredAuthz,
makeRouterWithFleetAuthz,
calculateRouteAuthz,
getAuthzFromRequest,
} from './services/security';
import {
PLUGIN_ID,
OUTPUT_SAVED_OBJECT_TYPE,
@ -93,7 +100,6 @@ import {
fetchAgentsUsage,
fetchFleetUsage,
} from './collectors/register';
import { getAuthzFromRequest, makeRouterWithFleetAuthz } from './routes/security';
import { FleetArtifactsClient } from './services/artifacts';
import type { FleetRouter } from './types/request_context';
import { TelemetryEventsSender } from './telemetry/sender';
@ -337,7 +343,19 @@ export class FleetPlugin
PLUGIN_ID,
async (context, request) => {
const plugin = this;
const esClient = (await context.core).elasticsearch.client;
const coreContext = await context.core;
const authz = await getAuthzFromRequest(request);
const esClient = coreContext.elasticsearch.client;
const routeRequiredAuthz = getRouteRequiredAuthz(request.route.method, request.route.path);
const routeAuthz = routeRequiredAuthz
? calculateRouteAuthz(authz, routeRequiredAuthz)
: undefined;
const getInternalSoClient = (): SavedObjectsClientContract =>
appContextService
.getSavedObjects()
.getScopedClient(request, { excludedExtensions: [SECURITY_EXTENSION_ID] });
return {
get agentClient() {
@ -356,18 +374,21 @@ export class FleetPlugin
asInternalUser: service.asInternalUser,
};
},
authz: await getAuthzFromRequest(request),
epm: {
authz,
get internalSoClient() {
// Use a lazy getter to avoid constructing this client when not used by a request handler
get internalSoClient() {
return appContextService
.getSavedObjects()
.getScopedClient(request, { excludedExtensions: [SECURITY_EXTENSION_ID] });
},
return getInternalSoClient();
},
get spaceId() {
return deps.spaces.spacesService.getSpaceId(request);
},
get limitedToPackages() {
if (routeAuthz && routeAuthz.granted) {
return routeAuthz.scopeDataToPackages;
}
},
};
}
);
@ -384,10 +405,11 @@ export class FleetPlugin
// Only some endpoints require superuser so we pass a raw IRouter here
// For all the routes we enforce the user to have role superuser
const { router: fleetAuthzRouter, onPostAuthHandler: fleetAuthzOnPostAuthHandler } =
makeRouterWithFleetAuthz(router);
const fleetAuthzRouter = makeRouterWithFleetAuthz(
router,
this.initializerContext.logger.get('fleet_authz_router')
);
core.http.registerOnPostAuth(fleetAuthzOnPostAuthHandler);
registerRoutes(fleetAuthzRouter, config);
this.telemetryEventsSender.setup(deps.telemetry);

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { AGENT_API_ROUTES } from '../../constants';
import {
GetAgentsRequestSchema,
@ -30,7 +32,6 @@ import {
} from '../../types';
import * as AgentService from '../../services/agents';
import type { FleetConfigType } from '../..';
import type { FleetAuthzRouter } from '../security';
import { PostBulkUpdateAgentTagsRequestSchema } from '../../types/rest_spec/agent';

View file

@ -67,7 +67,7 @@ export const getAgentPoliciesHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const soClient = fleetContext.epm.internalSoClient;
const soClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const { full: withPackagePolicies = false, ...restOfQuery } = request.query;
try {
@ -98,7 +98,7 @@ export const bulkGetAgentPoliciesHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const soClient = fleetContext.epm.internalSoClient;
const soClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const { full: withPackagePolicies = false, ignoreMissing = false, ids } = request.body;
try {
@ -158,7 +158,7 @@ export const createAgentPolicyHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const soClient = fleetContext.epm.internalSoClient;
const soClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
const withSysMonitoring = request.query.sys_monitoring ?? false;
@ -276,7 +276,7 @@ export const getFullAgentPolicy: FleetRequestHandler<
TypeOf<typeof GetFullAgentPolicyRequestSchema.query>
> = async (context, request, response) => {
const fleetContext = await context.fleet;
const soClient = fleetContext.epm.internalSoClient;
const soClient = fleetContext.internalSoClient;
if (request.query.kubernetes === true) {
try {
@ -332,7 +332,7 @@ export const downloadFullAgentPolicy: FleetRequestHandler<
TypeOf<typeof GetFullAgentPolicyRequestSchema.query>
> = async (context, request, response) => {
const fleetContext = await context.fleet;
const soClient = fleetContext.epm.internalSoClient;
const soClient = fleetContext.internalSoClient;
const {
params: { agentPolicyId },
} = request;

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { AGENT_POLICY_API_ROUTES } from '../../constants';
import {
GetAgentPoliciesRequestSchema,
@ -17,7 +19,6 @@ import {
GetK8sManifestRequestSchema,
BulkGetAgentPoliciesRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import { K8S_API_ROUTES } from '../../../common/constants';

View file

@ -8,11 +8,12 @@
import type { RequestHandler } from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';
import type { FleetAuthzRouter } from '../../services/security';
import { APP_API_ROUTES } from '../../constants';
import { appContextService } from '../../services';
import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../../common/types';
import { defaultFleetErrorHandler, GenerateServiceTokenError } from '../../errors';
import type { FleetAuthzRouter } from '../security';
import type { FleetRequestHandler } from '../../types';
import { CheckPermissionsRequestSchema } from '../../types';
@ -90,7 +91,6 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
validate: CheckPermissionsRequestSchema,
options: { tags: [] },
},
getCheckPermissionsHandler
);

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { DATA_STREAM_API_ROUTES } from '../../constants';
import type { FleetAuthzRouter } from '../security';
import { getListHandler } from './handlers';

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { DOWNLOAD_SOURCE_API_ROUTES } from '../../constants';
import {
getDownloadSourcesRequestSchema,
@ -13,7 +15,6 @@ import {
PostDownloadSourcesRequestSchema,
DeleteDownloadSourcesRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
getDownloadSourcesHandler,

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { ENROLLMENT_API_KEY_ROUTES } from '../../constants';
import {
GetEnrollmentAPIKeysRequestSchema,
@ -12,7 +14,6 @@ import {
DeleteEnrollmentAPIKeyRequestSchema,
PostEnrollmentAPIKeyRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
getEnrollmentApiKeysHandler,

View file

@ -86,7 +86,7 @@ export const getListHandler: FleetRequestHandler<
TypeOf<typeof GetPackagesRequestSchema.query>
> = async (context, request, response) => {
try {
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const savedObjectsClient = (await context.fleet).internalSoClient;
const res = await getPackages({
savedObjectsClient,
...request.query,
@ -112,7 +112,7 @@ export const getLimitedListHandler: FleetRequestHandler<
undefined
> = async (context, request, response) => {
try {
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const savedObjectsClient = (await context.fleet).internalSoClient;
const res = await getLimitedPackages({
savedObjectsClient,
prerelease: request.query.prerelease,
@ -134,7 +134,7 @@ export const getFileHandler: FleetRequestHandler<
> = async (context, request, response) => {
try {
const { pkgName, pkgVersion, filePath } = request.params;
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const savedObjectsClient = (await context.fleet).internalSoClient;
const installation = await getInstallation({ savedObjectsClient, pkgName });
const useLocalFile = pkgVersion === installation?.version;
@ -208,7 +208,7 @@ export const getInfoHandler: FleetRequestHandler<
TypeOf<typeof GetInfoRequestSchema.query>
> = async (context, request, response) => {
try {
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const savedObjectsClient = (await context.fleet).internalSoClient;
const { pkgName, pkgVersion } = request.params;
const { ignoreUnverified = false, full = false, prerelease } = request.query;
if (pkgVersion && !semverValid(pkgVersion)) {
@ -237,7 +237,7 @@ export const updatePackageHandler: FleetRequestHandler<
TypeOf<typeof UpdatePackageRequestSchema.body>
> = async (context, request, response) => {
try {
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const savedObjectsClient = (await context.fleet).internalSoClient;
const { pkgName } = request.params;
const res = await updatePackage({ savedObjectsClient, pkgName, ...request.body });
@ -256,7 +256,7 @@ export const getStatsHandler: FleetRequestHandler<
> = async (context, request, response) => {
try {
const { pkgName } = request.params;
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const savedObjectsClient = (await context.fleet).internalSoClient;
const body: GetStatsResponse = {
response: await getPackageUsageStats({ savedObjectsClient, pkgName }),
};
@ -273,7 +273,7 @@ export const installPackageFromRegistryHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const savedObjectsClient = fleetContext.epm.internalSoClient;
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const { pkgName, pkgVersion } = request.params;
@ -323,7 +323,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const savedObjectsClient = fleetContext.epm.internalSoClient;
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const spaceId = fleetContext.spaceId;
const bulkInstalledResponses = await bulkInstallPackages({
@ -354,7 +354,7 @@ export const installPackageByUploadHandler: FleetRequestHandler<
}
const coreContext = await context.core;
const fleetContext = await context.fleet;
const savedObjectsClient = fleetContext.epm.internalSoClient;
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later
const archiveBuffer = Buffer.from(request.body);
@ -390,7 +390,7 @@ export const deletePackageHandler: FleetRequestHandler<
const { pkgName, pkgVersion } = request.params;
const coreContext = await context.core;
const fleetContext = await context.fleet;
const savedObjectsClient = fleetContext.epm.internalSoClient;
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const res = await removeInstallation({
savedObjectsClient,

View file

@ -7,6 +7,8 @@
import type { IKibanaResponse } from '@kbn/core/server';
import type { FleetAuthzRouter } from '../../services/security';
import type {
DeletePackageResponse,
GetInfoResponse,
@ -32,7 +34,6 @@ import {
UpdatePackageRequestSchema,
UpdatePackageRequestSchemaDeprecated,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
getCategoriesHandler,

View file

@ -4,6 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { FLEET_PROXY_API_ROUTES } from '../../../common/constants';
import {
GetOneFleetProxyRequestSchema,
@ -11,8 +13,6 @@ import {
PutFleetProxyRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
getAllFleetProxyHandler,
postFleetProxyHandler,

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { FLEET_SERVER_HOST_API_ROUTES } from '../../../common/constants';
import {
GetAllFleetServerHostRequestSchema,
@ -13,8 +15,6 @@ import {
PutFleetServerHostRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
deleteFleetServerPolicyHandler,
getAllFleetServerPolicyHandler,

View file

@ -9,9 +9,10 @@ import https from 'https';
import type { TypeOf } from '@kbn/config-schema';
import fetch from 'node-fetch';
import type { FleetAuthzRouter } from '../../services/security';
import { APP_API_ROUTES } from '../../constants';
import type { FleetRequestHandler } from '../../types';
import type { FleetAuthzRouter } from '../security';
import { defaultFleetErrorHandler } from '../../errors';
import { PostHealthCheckRequestSchema } from '../../types';

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../services/security';
import type { FleetConfigType } from '../config';
import { registerRoutes as registerAgentPolicyRoutes } from './agent_policy';
@ -22,7 +24,6 @@ import { registerRoutes as registerDownloadSourcesRoutes } from './download_sour
import { registerRoutes as registerHealthCheckRoutes } from './health_check';
import { registerRoutes as registerFleetServerHostRoutes } from './fleet_server_policy_config';
import { registerRoutes as registerFleetProxiesRoutes } from './fleet_proxies';
import type { FleetAuthzRouter } from './security';
export async function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: FleetConfigType) {
// Always register app routes for permissions checking

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { OUTPUT_API_ROUTES } from '../../constants';
import {
DeleteOutputRequestSchema,
@ -13,7 +15,6 @@ import {
PostOutputRequestSchema,
PutOutputRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
deleteOutputHandler,

View file

@ -9,6 +9,8 @@ import { httpServerMock, httpServiceMock } from '@kbn/core/server/mocks';
import type { KibanaRequest } from '@kbn/core/server';
import type { RouteConfig } from '@kbn/core/server';
import type { FleetAuthzRouter } from '../../services/security';
import { PACKAGE_POLICY_API_ROUTES } from '../../../common/constants';
import { appContextService, packagePolicyService } from '../../services';
import { createAppContextStartContractMock, xpackMocks } from '../../mocks';
@ -22,7 +24,6 @@ import type {
CreatePackagePolicyRequestSchema,
UpdatePackagePolicyRequestSchema,
} from '../../types/rest_spec';
import type { FleetAuthzRouter } from '../security';
import type { FleetRequestHandler } from '../../types';
import type { PackagePolicy } from '../../types';

View file

@ -43,16 +43,72 @@ import { simplifiedPackagePolicytoNewPackagePolicy } from '../../../common/servi
import type { SimplifiedPackagePolicy } from '../../../common/services/simplified_package_policy_helper';
export const getPackagePoliciesHandler: RequestHandler<
const getAllowedPackageNamesMessage = (allowedPackageNames: string[]): string => {
return `Allowed package.name's: ${allowedPackageNames.join(', ')}`;
};
/**
* Validates that Package Policy data only includes `package.name`'s that are in the list of
* `allowedPackageNames`. If an error is encountered, then a message is return, otherwise, undefined.
*
* @param data
* @param allowedPackageNames
*/
const validatePackagePolicyDataIsScopedToAllowedPackageNames = (
data: PackagePolicy[],
allowedPackageNames: string[] | undefined
): string | undefined => {
if (!data.length || typeof allowedPackageNames === 'undefined') {
return;
}
if (!allowedPackageNames.length) {
return 'Authorization denied due to lack of integration package privileges';
}
// Because List type of APIs have an un-bounded `perPage` query param, we only validate the
// data up to the first package.name that we find is not authorized.
for (const packagePolicy of data) {
if (!packagePolicy.package) {
return `Authorization denied. ${getAllowedPackageNamesMessage(allowedPackageNames)}`;
}
if (!allowedPackageNames.includes(packagePolicy.package.name)) {
return `Authorization denied to [package.name=${
packagePolicy.package.name
}]. ${getAllowedPackageNamesMessage(allowedPackageNames)}`;
}
}
};
export const getPackagePoliciesHandler: FleetRequestHandler<
undefined,
TypeOf<typeof GetPackagePoliciesRequestSchema.query>
> = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
const fleetContext = await context.fleet;
const soClient = fleetContext.internalSoClient;
const limitedToPackages = fleetContext.limitedToPackages;
try {
const { items, total, page, perPage } = await packagePolicyService.list(
soClient,
request.query
);
// specific to package-level RBAC
const validationResult = validatePackagePolicyDataIsScopedToAllowedPackageNames(
items,
limitedToPackages
);
if (validationResult) {
return response.forbidden({
body: {
message: validationResult,
},
});
}
// agnostic to package-level RBAC
return response.ok({
body: {
items,
@ -66,13 +122,16 @@ export const getPackagePoliciesHandler: RequestHandler<
}
};
export const bulkGetPackagePoliciesHandler: RequestHandler<
export const bulkGetPackagePoliciesHandler: FleetRequestHandler<
undefined,
undefined,
TypeOf<typeof BulkGetPackagePoliciesRequestSchema.body>
> = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
const fleetContext = await context.fleet;
const soClient = fleetContext.internalSoClient;
const limitedToPackages = fleetContext.limitedToPackages;
const { ids, ignoreMissing } = request.body;
try {
const items = await packagePolicyService.getByIDs(soClient, ids, {
ignoreMissing,
@ -80,6 +139,18 @@ export const bulkGetPackagePoliciesHandler: RequestHandler<
const body: BulkGetPackagePoliciesResponse = { items: items ?? [] };
const validationResult = validatePackagePolicyDataIsScopedToAllowedPackageNames(
body.items,
limitedToPackages
);
if (validationResult) {
return response.forbidden({
body: {
message: validationResult,
},
});
}
return response.ok({
body,
});
@ -94,17 +165,32 @@ export const bulkGetPackagePoliciesHandler: RequestHandler<
}
};
export const getOnePackagePolicyHandler: RequestHandler<
export const getOnePackagePolicyHandler: FleetRequestHandler<
TypeOf<typeof GetOnePackagePolicyRequestSchema.params>
> = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
const fleetContext = await context.fleet;
const soClient = fleetContext.internalSoClient;
const limitedToPackages = fleetContext.limitedToPackages;
const { packagePolicyId } = request.params;
const notFoundResponse = () =>
response.notFound({ body: { message: `Package policy ${packagePolicyId} not found` } });
try {
const packagePolicy = await packagePolicyService.get(soClient, packagePolicyId);
if (packagePolicy) {
const validationResult = validatePackagePolicyDataIsScopedToAllowedPackageNames(
[packagePolicy],
limitedToPackages
);
if (validationResult) {
return response.forbidden({
body: {
message: validationResult,
},
});
}
return response.ok({
body: {
item: packagePolicy,
@ -184,7 +270,7 @@ export const createPackagePolicyHandler: FleetRequestHandler<
> = async (context, request, response) => {
const coreContext = await context.core;
const fleetContext = await context.fleet;
const soClient = fleetContext.epm.internalSoClient;
const soClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined;
const { force, package: pkg, ...newPolicy } = request.body;
@ -258,13 +344,15 @@ export const createPackagePolicyHandler: FleetRequestHandler<
}
};
export const updatePackagePolicyHandler: RequestHandler<
export const updatePackagePolicyHandler: FleetRequestHandler<
TypeOf<typeof UpdatePackagePolicyRequestSchema.params>,
unknown,
TypeOf<typeof UpdatePackagePolicyRequestSchema.body>
> = async (context, request, response) => {
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
const fleetContext = await context.fleet;
const soClient = fleetContext.internalSoClient;
const limitedToPackages = fleetContext.limitedToPackages;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined;
const packagePolicy = await packagePolicyService.get(soClient, request.params.packagePolicyId);
@ -273,6 +361,15 @@ export const updatePackagePolicyHandler: RequestHandler<
throw Boom.notFound('Package policy not found');
}
if (limitedToPackages && limitedToPackages.length) {
const packageName = packagePolicy?.package?.name;
if (packageName && !limitedToPackages.includes(packageName)) {
return response.forbidden({
body: { message: `Update for package name ${packageName} is not authorized.` },
});
}
}
try {
const { force, package: pkg, ...body } = request.body;
// TODO Remove deprecated APIs https://github.com/elastic/kibana/issues/121485

View file

@ -5,6 +5,11 @@
* 2.0.
*/
import { getRouteRequiredAuthz } from '../../services/security';
import type { FleetAuthzRouter } from '../../services/security';
import type { FleetAuthz } from '../../../common';
import { PACKAGE_POLICY_API_ROUTES } from '../../constants';
import {
GetPackagePoliciesRequestSchema,
@ -17,7 +22,7 @@ import {
DeleteOnePackagePolicyRequestSchema,
BulkGetPackagePoliciesRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import { calculateRouteAuthz } from '../../services/security/security';
import {
getPackagePoliciesHandler,
@ -38,20 +43,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN,
validate: GetPackagePoliciesRequestSchema,
fleetAuthz: {
integrations: { readIntegrationPolicies: true },
},
fleetAuthz: (fleetAuthz: FleetAuthz): boolean =>
calculateRouteAuthz(
fleetAuthz,
getRouteRequiredAuthz('get', PACKAGE_POLICY_API_ROUTES.LIST_PATTERN)
).granted,
},
getPackagePoliciesHandler
);
// Get bulk
router.post(
{
path: PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN,
validate: BulkGetPackagePoliciesRequestSchema,
fleetAuthz: {
integrations: { readIntegrationPolicies: true },
},
fleetAuthz: (fleetAuthz: FleetAuthz): boolean =>
calculateRouteAuthz(
fleetAuthz,
getRouteRequiredAuthz('post', PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN)
).granted,
},
bulkGetPackagePoliciesHandler
);
@ -61,9 +71,11 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: PACKAGE_POLICY_API_ROUTES.INFO_PATTERN,
validate: GetOnePackagePolicyRequestSchema,
fleetAuthz: {
integrations: { readIntegrationPolicies: true },
},
fleetAuthz: (fleetAuthz: FleetAuthz): boolean =>
calculateRouteAuthz(
fleetAuthz,
getRouteRequiredAuthz('get', PACKAGE_POLICY_API_ROUTES.INFO_PATTERN)
).granted,
},
getOnePackagePolicyHandler
);
@ -93,14 +105,16 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN,
validate: UpdatePackagePolicyRequestSchema,
fleetAuthz: {
integrations: { writeIntegrationPolicies: true },
},
fleetAuthz: (fleetAuthz: FleetAuthz): boolean =>
calculateRouteAuthz(
fleetAuthz,
getRouteRequiredAuthz('put', PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN)
).granted,
},
updatePackagePolicyHandler
);
// Delete
// Delete (bulk)
router.post(
{
path: PACKAGE_POLICY_API_ROUTES.DELETE_PATTERN,

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { PRECONFIGURATION_API_ROUTES } from '../../constants';
import { PostResetOnePreconfiguredAgentPoliciesSchema } from '../../types';
import type { FleetAuthzRouter } from '../security';
import { resetPreconfigurationHandler, resetOnePreconfigurationHandler } from './handler';

View file

@ -1,297 +0,0 @@
/*
* 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 { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
import type {
IRouter,
RouteConfig,
RouteMethod,
KibanaRequest,
RequestHandler,
RequestHandlerContext,
OnPostAuthHandler,
} from '@kbn/core/server';
import type { FleetAuthz } from '../../common';
import { INTEGRATIONS_PLUGIN_ID } from '../../common';
import { calculateAuthz, calculatePackagePrivilegesFromKibanaPrivileges } from '../../common/authz';
import { appContextService } from '../services';
import type { FleetRequestHandlerContext } from '../types';
import { PLUGIN_ID, ENDPOINT_PRIVILEGES } from '../constants';
function checkSecurityEnabled() {
return appContextService.getSecurityLicense().isEnabled();
}
export 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 getAuthorizationFromPrivileges(
kibanaPrivileges: Array<{
resource?: string;
privilege: string;
authorized: boolean;
}>,
searchPrivilege: string
) {
const privilege = kibanaPrivileges.find((p) => p.privilege.includes(searchPrivilege));
return privilege ? privilege.authorized : false;
}
export async function getAuthzFromRequest(req: KibanaRequest): Promise<FleetAuthz> {
const security = appContextService.getSecurity();
if (security.authz.mode.useRbacForRequest(req)) {
const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req);
const endpointPrivileges = ENDPOINT_PRIVILEGES.map((privilege) =>
security.authz.actions.api.get(`${DEFAULT_APP_CATEGORIES.security.id}-${privilege}`)
);
const { privileges } = await checkPrivileges({
kibana: [
security.authz.actions.api.get(`${PLUGIN_ID}-all`),
security.authz.actions.api.get(`${PLUGIN_ID}-setup`),
security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-all`),
security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-read`),
...endpointPrivileges,
],
});
const fleetAllAuth = getAuthorizationFromPrivileges(privileges.kibana, `${PLUGIN_ID}-all`);
const intAllAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-all`
);
const intReadAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-read`
);
const fleetSetupAuth = getAuthorizationFromPrivileges(privileges.kibana, 'fleet-setup');
return {
...calculateAuthz({
fleet: { all: fleetAllAuth, setup: fleetSetupAuth },
integrations: {
all: intAllAuth,
read: intReadAuth,
},
isSuperuser: checkSuperuser(req),
}),
packagePrivileges: calculatePackagePrivilegesFromKibanaPrivileges(privileges.kibana),
};
}
return calculateAuthz({
fleet: { all: false, setup: false },
integrations: {
all: false,
read: false,
},
isSuperuser: false,
});
}
interface Authz {
[k: string]: Authz | boolean;
}
function containsRequirement(authz: Authz, requirements: DeepPartialTruthy<Authz>) {
if (!authz) {
return false;
}
for (const key of Object.keys(requirements)) {
if (typeof requirements[key] !== 'undefined' && typeof requirements[key] === 'boolean') {
if (!authz[key]) {
return false;
}
} else if (
!containsRequirement(authz[key] as Authz, requirements[key] as DeepPartialTruthy<Authz>)
) {
return false;
}
}
return true;
}
export function hasRequiredFleetAuthzPrivilege(
authz: FleetAuthz,
{ fleetAuthz }: { fleetAuthz?: FleetAuthzRequirements }
): boolean {
if (!checkSecurityEnabled()) {
return false;
}
if (fleetAuthz && !containsRequirement(authz as unknown as Authz, fleetAuthz)) {
return false;
}
return true;
}
type DeepPartialTruthy<T> = {
[P in keyof T]?: T[P] extends boolean ? true : DeepPartialTruthy<T[P]>;
};
type FleetAuthzRequirements = DeepPartialTruthy<FleetAuthz>;
type FleetAuthzRouteRegistrar<
Method extends RouteMethod,
Context extends RequestHandlerContext = RequestHandlerContext
> = <P, Q, B>(
route: FleetRouteConfig<P, Q, B, Method>,
handler: RequestHandler<P, Q, B, Context, Method>
) => void;
export interface FleetAuthzRouteConfig {
fleetAuthz?: FleetAuthzRequirements;
}
type FleetRouteConfig<P, Q, B, Method extends RouteMethod> = RouteConfig<P, Q, B, Method> &
FleetAuthzRouteConfig;
// Fleet router that allow to add required access when registering route
export interface FleetAuthzRouter<
TContext extends FleetRequestHandlerContext = FleetRequestHandlerContext
> extends IRouter<TContext> {
get: FleetAuthzRouteRegistrar<'get', TContext>;
delete: FleetAuthzRouteRegistrar<'delete', TContext>;
post: FleetAuthzRouteRegistrar<'post', TContext>;
put: FleetAuthzRouteRegistrar<'put', TContext>;
patch: FleetAuthzRouteRegistrar<'patch', TContext>;
}
function shouldHandlePostAuthRequest(req: KibanaRequest) {
if (req?.route?.options?.tags) {
return req.route.options.tags.some((tag) => tag.match(/^fleet:authz/));
}
return false;
}
// Exported for test only
export function deserializeAuthzConfig(tags: readonly string[]): FleetAuthzRouteConfig {
let fleetAuthz: FleetAuthzRequirements | undefined;
for (const tag of tags) {
if (!tag.match(/^fleet:authz/)) {
continue;
}
if (!fleetAuthz) {
fleetAuthz = {};
}
tag
.replace(/^fleet:authz:/, '')
.split(':')
.reduce((acc: any, key, idx, keys) => {
if (idx === keys.length - 1) {
acc[key] = true;
return acc;
}
if (!acc[key]) {
acc[key] = {};
}
return acc[key];
}, fleetAuthz);
}
return { fleetAuthz };
}
// Exported for test only
export function serializeAuthzConfig(config: FleetAuthzRouteConfig): string[] {
const tags: string[] = [];
if (config.fleetAuthz) {
function fleetAuthzToTags(requirements: DeepPartialTruthy<Authz>, prefix: string = '') {
for (const key of Object.keys(requirements)) {
if (typeof requirements[key] === 'boolean') {
tags.push(`fleet:authz:${prefix}${key}`);
} else if (typeof requirements[key] !== 'undefined') {
fleetAuthzToTags(requirements[key] as DeepPartialTruthy<Authz>, `${prefix}${key}:`);
}
}
}
fleetAuthzToTags(config.fleetAuthz);
}
return tags;
}
export function makeRouterWithFleetAuthz<TContext extends FleetRequestHandlerContext>(
router: IRouter<TContext>
): { router: FleetAuthzRouter<TContext>; onPostAuthHandler: OnPostAuthHandler } {
function buildFleetAuthzRouteConfig<P, Q, B, Method extends RouteMethod>({
fleetAuthz,
...routeConfig
}: FleetRouteConfig<P, Q, B, Method>) {
return {
...routeConfig,
options: {
...routeConfig.options,
tags: [
...(routeConfig?.options?.tags ?? []),
...serializeAuthzConfig({
fleetAuthz,
}),
],
},
};
}
const fleetAuthzOnPostAuthHandler: OnPostAuthHandler = async (req, res, toolkit) => {
if (!shouldHandlePostAuthRequest(req)) {
return toolkit.next();
}
if (!checkSecurityEnabled()) {
return res.forbidden();
}
const fleetAuthzConfig = deserializeAuthzConfig(req.route.options.tags);
if (!fleetAuthzConfig) {
return toolkit.next();
}
const authz = await getAuthzFromRequest(req);
if (!hasRequiredFleetAuthzPrivilege(authz, fleetAuthzConfig)) {
return res.forbidden();
}
return toolkit.next();
};
const fleetAuthzRouter: FleetAuthzRouter<TContext> = {
get: (routeConfig, handler) => router.get(buildFleetAuthzRouteConfig(routeConfig), handler),
delete: (routeConfig, handler) =>
router.delete(buildFleetAuthzRouteConfig(routeConfig), handler),
post: (routeConfig, handler) => router.post(buildFleetAuthzRouteConfig(routeConfig), handler),
put: (routeConfig, handler) => router.put(buildFleetAuthzRouteConfig(routeConfig), handler),
patch: (routeConfig, handler) => router.patch(buildFleetAuthzRouteConfig(routeConfig), handler),
handleLegacyErrors: (handler) => router.handleLegacyErrors(handler),
getRoutes: () => router.getRoutes(),
routerPath: router.routerPath,
};
return { router: fleetAuthzRouter, onPostAuthHandler: fleetAuthzOnPostAuthHandler };
}

View file

@ -7,15 +7,16 @@
import type { TypeOf } from '@kbn/config-schema';
import type { FleetAuthzRouter } from '../../services/security';
import { SETTINGS_API_ROUTES } from '../../constants';
import type { FleetRequestHandler } from '../../types';
import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types';
import { defaultFleetErrorHandler } from '../../errors';
import { settingsService, agentPolicyService, appContextService } from '../../services';
import type { FleetAuthzRouter } from '../security';
export const getSettingsHandler: FleetRequestHandler = async (context, request, response) => {
const soClient = (await context.fleet).epm.internalSoClient;
const soClient = (await context.fleet).internalSoClient;
try {
const settings = await settingsService.getSettings(soClient);
@ -39,7 +40,7 @@ export const putSettingsHandler: FleetRequestHandler<
undefined,
TypeOf<typeof PutSettingsRequestSchema.body>
> = async (context, request, response) => {
const soClient = (await context.fleet).epm.internalSoClient;
const soClient = (await context.fleet).internalSoClient;
const esClient = (await context.core).elasticsearch.client.asInternalUser;
const user = await appContextService.getSecurity()?.authc.getCurrentUser(request);

View file

@ -51,10 +51,9 @@ describe('FleetSetupHandler', () => {
asCurrentUser: createPackagePolicyServiceMock(),
asInternalUser: createPackagePolicyServiceMock(),
},
epm: {
internalSoClient: savedObjectsClientMock.create(),
},
internalSoClient: savedObjectsClientMock.create(),
spaceId: 'default',
limitedToPackages: undefined,
},
};
response = httpServerMock.createResponseFactory();

View file

@ -60,7 +60,7 @@ export const getFleetStatusHandler: FleetRequestHandler = async (context, reques
export const fleetSetupHandler: FleetRequestHandler = async (context, request, response) => {
try {
const soClient = (await context.fleet).epm.internalSoClient;
const soClient = (await context.fleet).internalSoClient;
const esClient = (await context.core).elasticsearch.client.asInternalUser;
const setupStatus = await setupFleet(soClient, esClient);
const body: PostFleetSetupResponse = {

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import type { FleetAuthzRouter } from '../../services/security';
import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants';
import type { FleetConfigType } from '../../../common/types';
import type { FleetAuthzRouter } from '../security';
import { getFleetStatusHandler, fleetSetupHandler } from './handlers';
export const registerFleetSetupRoute = (router: FleetAuthzRouter) => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
jest.mock('../../routes/security');
jest.mock('../security');
jest.mock('./crud');
jest.mock('./status');
@ -14,7 +14,7 @@ import { elasticsearchServiceMock, httpServerMock } from '@kbn/core/server/mocks
import { FleetUnauthorizedError } from '../../errors';
import { getAuthzFromRequest } from '../../routes/security';
import { getAuthzFromRequest } from '../security';
import type { FleetAuthz } from '../../../common';
import type { AgentClient } from './agent_service';

View file

@ -12,7 +12,7 @@ import type { ElasticsearchClient, KibanaRequest } from '@kbn/core/server';
import type { AgentStatus, ListWithKuery } from '../../types';
import type { Agent, GetAgentStatusResponse } from '../../../common/types';
import { getAuthzFromRequest } from '../../routes/security';
import { getAuthzFromRequest } from '../security';
import { FleetUnauthorizedError } from '../../errors';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
jest.mock('../../routes/security');
jest.mock('../security');
import type { MockedLogger } from '@kbn/logging-mocks';

View file

@ -22,7 +22,7 @@ import type {
ArchivePackage,
BundledPackage,
} from '../../types';
import { checkSuperuser } from '../../routes/security';
import { checkSuperuser } from '../security';
import { FleetUnauthorizedError } from '../../errors';
import { installTransforms, isTransform } from './elasticsearch/transform/install';

View file

@ -82,8 +82,9 @@ import type {
} from '../types';
import type { ExternalCallback } from '..';
import type { FleetAuthzRouteConfig } from '../routes/security';
import { getAuthzFromRequest, hasRequiredFleetAuthzPrivilege } from '../routes/security';
import type { FleetAuthzRouteConfig } from './security';
import { getAuthzFromRequest, doesNotHaveRequiredFleetAuthz } from './security';
import { storedPackagePolicyToAgentInputs } from './agent_policies';
import { agentPolicyService } from './agent_policy';
@ -1294,12 +1295,14 @@ export class PackagePolicyServiceImpl
implements PackagePolicyService
{
public asScoped(request: KibanaRequest): PackagePolicyClient {
const preflightCheck = async (fleetAuthzConfig: FleetAuthzRouteConfig) => {
const preflightCheck = async ({ fleetAuthz: fleetRequiredAuthz }: FleetAuthzRouteConfig) => {
const authz = await getAuthzFromRequest(request);
if (!hasRequiredFleetAuthzPrivilege(authz, fleetAuthzConfig)) {
if (doesNotHaveRequiredFleetAuthz(authz, fleetRequiredAuthz)) {
throw new FleetUnauthorizedError('Not authorized to this action on integration policies');
}
};
return new PackagePolicyClientWithAuthz(preflightCheck);
}

View file

@ -5,17 +5,25 @@
* 2.0.
*/
import type { IRouter, RequestHandler, RouteConfig } from '@kbn/core/server';
import { coreMock } from '@kbn/core/server/mocks';
import type { AuthenticatedUser, CheckPrivilegesPayload } from '@kbn/security-plugin/server';
import type { CheckPrivilegesResponse } from '@kbn/security-plugin/server/authorization/types';
import type { CheckPrivilegesDynamically } from '@kbn/security-plugin/server/authorization/check_privileges_dynamically';
import type { IRouter, RequestHandler, RouteConfig } from '@kbn/core/server';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createAppContextStartContractMock } from '../mocks';
import { appContextService } from '../services';
import type { FleetRequestHandlerContext } from '../types';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { deserializeAuthzConfig, makeRouterWithFleetAuthz, serializeAuthzConfig } from './security';
import { coreMock } from '@kbn/core/server/mocks';
import type { CheckPrivilegesPayload } from '@kbn/security-plugin/server';
import type { CheckPrivilegesResponse } from '@kbn/security-plugin/server/authorization/types';
import type { FleetRequestHandlerContext } from '../..';
import { createAppContextStartContractMock } from '../../mocks';
import { appContextService } from '..';
import { makeRouterWithFleetAuthz } from './fleet_router';
const mockLogger = loggingSystemMock.createLogger();
function getCheckPrivilegesMockedImplementation(kibanaRoles: string[]) {
return (checkPrivileges: CheckPrivilegesPayload) => {
@ -82,12 +90,11 @@ describe('FleetAuthzRouter', () => {
appContextService.start(mockContext);
const { router: wrappedRouter, onPostAuthHandler } = makeRouterWithFleetAuthz(fakeRouter);
wrappedRouter.get({ ...routeConfig } as RouteConfig<any, any, any, any>, fakeHandler);
const fleetAuthzRouter = makeRouterWithFleetAuthz(fakeRouter, mockLogger);
fleetAuthzRouter.get({ ...routeConfig } as RouteConfig<any, any, any, any>, fakeHandler);
const wrappedHandler = fakeRouter.get.mock.calls[0][1];
const wrappedRouteConfig = fakeRouter.get.mock.calls[0][0];
const resFactory = { forbidden: jest.fn(() => 'forbidden'), ok: jest.fn(() => 'ok') };
const fakeToolkit = { next: jest.fn(() => 'next') };
const fakeReq = {
route: {
@ -96,11 +103,6 @@ describe('FleetAuthzRouter', () => {
options: wrappedRouteConfig.options,
},
} as any;
const onPostRes = await onPostAuthHandler(fakeReq, resFactory as any, fakeToolkit as any);
if ((onPostRes as unknown) !== 'next') {
return onPostRes;
}
const res = await wrappedHandler(
{
@ -198,79 +200,3 @@ describe('FleetAuthzRouter', () => {
});
});
});
describe('serializeAuthzConfig', () => {
it('should serialize authz to tags', () => {
const res = serializeAuthzConfig({
fleetAuthz: {
fleet: {
readEnrollmentTokens: true,
setup: true,
},
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
});
expect(res).toEqual([
'fleet:authz:fleet:readEnrollmentTokens',
'fleet:authz:fleet:setup',
'fleet:authz:integrations:readPackageInfo',
'fleet:authz:integrations:removePackages',
'fleet:authz:packagePrivileges:endpoint:actions:readPolicyManagement:executePackageAction',
'fleet:authz:packagePrivileges:endpoint:actions:readBlocklist:executePackageAction',
]);
});
});
describe('deserializeAuthzConfig', () => {
it('should deserialize tags to fleet authz', () => {
const res = deserializeAuthzConfig([
'fleet:authz:fleet:readEnrollmentTokens',
'fleet:authz:fleet:setup',
'fleet:authz:integrations:readPackageInfo',
'fleet:authz:integrations:removePackages',
'fleet:authz:packagePrivileges:endpoint:actions:readPolicyManagement:executePackageAction',
'fleet:authz:packagePrivileges:endpoint:actions:readBlocklist:executePackageAction',
]);
expect(res).toEqual({
fleetAuthz: {
fleet: {
readEnrollmentTokens: true,
setup: true,
},
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
});
});
});

View file

@ -0,0 +1,95 @@
/*
* 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 {
IKibanaResponse,
IRouter,
KibanaRequest,
KibanaResponseFactory,
Logger,
RequestHandler,
RouteMethod,
} from '@kbn/core/server';
import type { FleetRequestHandlerContext } from '../..';
import type { FleetAuthzRouteConfig, FleetAuthzRouter } from './types';
import {
checkSecurityEnabled,
getAuthzFromRequest,
doesNotHaveRequiredFleetAuthz,
} from './security';
export function makeRouterWithFleetAuthz<TContext extends FleetRequestHandlerContext>(
router: IRouter<TContext>,
logger: Logger
): FleetAuthzRouter<TContext> {
const routerAuthzWrapper = async <R extends RouteMethod>({
context,
request,
response,
handler,
hasRequiredAuthz,
}: {
context: TContext;
request: KibanaRequest;
response: KibanaResponseFactory;
handler: RequestHandler<any, any, any, TContext, R, KibanaResponseFactory>;
hasRequiredAuthz?: FleetAuthzRouteConfig['fleetAuthz'];
}): Promise<IKibanaResponse<any>> => {
if (!checkSecurityEnabled()) {
const securityEnabledInfo = 'Kibana security must be enabled to use Fleet';
logger.info(securityEnabledInfo);
return response.forbidden({
body: {
message: securityEnabledInfo,
},
});
}
const requestedAuthz = await getAuthzFromRequest(request);
if (doesNotHaveRequiredFleetAuthz(requestedAuthz, hasRequiredAuthz)) {
logger.info(`User does not have required fleet authz to access path: ${request.route.path}`);
return response.forbidden();
}
return handler(context, request, response);
};
const fleetAuthzRouter: FleetAuthzRouter<TContext> = {
get: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => {
router.get(options, async (context, request, response) =>
routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz })
);
},
delete: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => {
router.delete(options, async (context, request, response) =>
routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz })
);
},
post: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => {
router.post(options, async (context, request, response) =>
routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz })
);
},
put: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => {
router.put(options, async (context, request, response) =>
routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz })
);
},
patch: ({ fleetAuthz: hasRequiredAuthz, ...options }, handler) => {
router.patch(options, async (context, request, response) =>
routerAuthzWrapper({ context, request, response, handler, hasRequiredAuthz })
);
},
handleLegacyErrors: (handler) => router.handleLegacyErrors(handler),
getRoutes: () => router.getRoutes(),
routerPath: router.routerPath,
};
return fleetAuthzRouter;
}

View file

@ -0,0 +1,17 @@
/*
* 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.
*/
export * from './types';
export { makeRouterWithFleetAuthz } from './fleet_router';
export { getRouteRequiredAuthz } from './route_required_authz';
export {
checkSecurityEnabled,
checkSuperuser,
calculateRouteAuthz,
getAuthzFromRequest,
doesNotHaveRequiredFleetAuthz,
} from './security';

View 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 { deepFreeze } from '@kbn/std';
import type { RouteMethod } from '@kbn/core-http-server';
import { PACKAGE_POLICY_API_ROUTES } from '../../../common';
import type { FleetRouteRequiredAuthz } from './types';
/**
* The authorization requirements needed for an API route. Route authorization requirements are
* defined either via an `all` object, where all values must be `true` in order for access to be granted,
* or, by an `any` object, where any value defined that is set to `true` will grant access to the API.
*
* The `all` conditions are checked first and if those evaluate to `false`, then `any` conditions are evaluated.
*/
const ROUTE_AUTHZ_REQUIREMENTS = deepFreeze<Record<string, FleetRouteRequiredAuthz>>({
// Package Policy Update API
[`put:${PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN}`]: {
any: {
integrations: { writeIntegrationPolicies: true },
packagePrivileges: {
endpoint: {
actions: {
writePolicyManagement: {
executePackageAction: true,
},
},
},
},
},
},
// Package Policy GET one API
[`get:${PACKAGE_POLICY_API_ROUTES.INFO_PATTERN}`]: {
any: {
integrations: {
readIntegrationPolicies: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readTrustedApplications: {
executePackageAction: true,
},
readEventFilters: {
executePackageAction: true,
},
readHostIsolationExceptions: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
},
// Package Policy Bulk GET API
[`post:${PACKAGE_POLICY_API_ROUTES.BULK_GET_PATTERN}`]: {
any: {
integrations: {
readIntegrationPolicies: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readTrustedApplications: {
executePackageAction: true,
},
readEventFilters: {
executePackageAction: true,
},
readHostIsolationExceptions: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
},
// Package Policy List API
[`get:${PACKAGE_POLICY_API_ROUTES.LIST_PATTERN}`]: {
any: {
integrations: {
readIntegrationPolicies: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readTrustedApplications: {
executePackageAction: true,
},
readEventFilters: {
executePackageAction: true,
},
readHostIsolationExceptions: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
},
});
/**
* Retrieves the required fleet route authz
* in order to grant access to the given api route
* @param routeMethod
* @param routePath
*/
export const getRouteRequiredAuthz = (
routeMethod: RouteMethod,
routePath: string
): FleetRouteRequiredAuthz | undefined => {
const key = `${routeMethod}:${routePath}`;
if (typeof ROUTE_AUTHZ_REQUIREMENTS[key] !== 'undefined') {
return ROUTE_AUTHZ_REQUIREMENTS[key];
}
for (const k of Object.keys(ROUTE_AUTHZ_REQUIREMENTS)) {
if (pathMatchesPattern(k, key)) {
return ROUTE_AUTHZ_REQUIREMENTS[k];
}
}
};
const pathMatchesPattern = (pathPattern: string, path: string): boolean => {
// No path params - pattern is single path
if (pathPattern === path) {
return true;
}
// If pathPattern has params (`{value}`), then see if `path` matches it
if (/{.*?}/.test(pathPattern)) {
const pathParts = path.split(/\//);
const patternParts = pathPattern.split(/\//);
if (pathParts.length !== patternParts.length) {
return false;
}
return pathParts.every((part, index) => {
return part === patternParts[index] || /{.*?}/.test(patternParts[index]);
});
}
return false;
};

View file

@ -0,0 +1,540 @@
/*
* 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 { deepFreeze } from '@kbn/std';
import type { FleetAuthz } from '../../../common';
import { calculateRouteAuthz } from './security';
describe('When using calculateRouteAuthz()', () => {
const fleetAuthz = deepFreeze({
fleet: {
all: false,
setup: false,
readEnrollmentTokens: false,
readAgentPolicies: false,
},
integrations: {
readPackageInfo: false,
readInstalledPackages: false,
installPackages: false,
upgradePackages: false,
removePackages: false,
uploadPackages: false,
readPackageSettings: false,
writePackageSettings: false,
readIntegrationPolicies: false,
writeIntegrationPolicies: false,
},
packagePrivileges: {
endpoint: {
actions: {
writeEndpointList: {
executePackageAction: false,
},
readEndpointList: {
executePackageAction: false,
},
writeTrustedApplications: {
executePackageAction: false,
},
readTrustedApplications: {
executePackageAction: false,
},
writeHostIsolationExceptions: {
executePackageAction: false,
},
readHostIsolationExceptions: {
executePackageAction: false,
},
writeBlocklist: {
executePackageAction: false,
},
readBlocklist: {
executePackageAction: false,
},
writeEventFilters: {
executePackageAction: false,
},
readEventFilters: {
executePackageAction: false,
},
writePolicyManagement: {
executePackageAction: false,
},
readPolicyManagement: {
executePackageAction: false,
},
writeActionsLogManagement: {
executePackageAction: false,
},
readActionsLogManagement: {
executePackageAction: false,
},
writeHostIsolation: {
executePackageAction: false,
},
writeProcessOperations: {
executePackageAction: false,
},
writeFileOperations: {
executePackageAction: false,
},
},
},
someOtherPackage: {
actions: {
readSomeThing: {
executePackageAction: false,
},
},
},
},
});
const getFleetAuthzMock = (authz: FleetAuthz = fleetAuthz) => authz;
describe('with ANY object defined', () => {
it('should grant access if `any` are true', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
},
},
},
}),
{
any: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({
granted: true,
grantedByFleetPrivileges: false,
scopeDataToPackages: ['endpoint'],
});
});
it('should deny access if `any` are false', () => {
expect(
calculateRouteAuthz(getFleetAuthzMock(), {
any: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
})
).toEqual({
granted: false,
grantedByFleetPrivileges: false,
scopeDataToPackages: undefined,
});
});
});
describe('with ALL object defined', () => {
it('should grant access if `all` are true', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
integrations: {
...fleetAuthz.integrations,
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined });
});
it('should deny access if not `all` are true', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
},
},
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({
granted: false,
grantedByFleetPrivileges: false,
scopeDataToPackages: undefined,
});
});
});
describe('with ALL and ANY', () => {
it('should grant access if `all` are true', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
integrations: {
...fleetAuthz.integrations,
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined });
});
it('should grant access if all OR any are true', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
integrations: {
...fleetAuthz.integrations,
readPackageInfo: true,
removePackages: true,
},
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
},
},
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
},
any: {
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined });
});
it('should grant access if `all` are not true but `any` are true ', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
integrations: {
...fleetAuthz.integrations,
readPackageInfo: true,
},
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
},
},
someOtherPackage: {
actions: {
readSomeThing: {
executePackageAction: true,
},
},
},
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
},
any: {
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
someOtherPackage: {
actions: {
readSomeThing: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({
granted: true,
grantedByFleetPrivileges: false,
scopeDataToPackages: ['endpoint', 'someOtherPackage'],
});
});
it('should grant access if `all` are true but `any` are not true ', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
integrations: {
...fleetAuthz.integrations,
readPackageInfo: true,
removePackages: true,
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
},
any: {
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({ granted: true, grantedByFleetPrivileges: true, scopeDataToPackages: undefined });
});
});
describe('and access is granted based on package privileges', () => {
it('should exclude package names for which there is no access allowed', () => {
expect(
calculateRouteAuthz(
getFleetAuthzMock({
...fleetAuthz,
packagePrivileges: {
...fleetAuthz.packagePrivileges,
endpoint: {
...fleetAuthz.packagePrivileges.endpoint,
actions: {
...fleetAuthz.packagePrivileges.endpoint.actions,
readPolicyManagement: {
executePackageAction: true,
},
},
},
},
}),
{
all: {
integrations: {
readPackageInfo: true,
removePackages: true,
},
},
any: {
packagePrivileges: {
endpoint: {
actions: {
readPolicyManagement: {
executePackageAction: true,
},
readBlocklist: {
executePackageAction: true,
},
},
},
// This package Authz is not allowed, so it should not be listed in `scopeDataToPackages`
someOtherPackage: {
actions: {
readSomeThing: {
executePackageAction: true,
},
},
},
},
},
}
)
).toEqual({
granted: true,
grantedByFleetPrivileges: false,
scopeDataToPackages: ['endpoint'],
});
});
});
});

View file

@ -0,0 +1,252 @@
/*
* 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 { pick } from 'lodash';
import type { KibanaRequest } from '@kbn/core/server';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
import type { FleetAuthz } from '../../../common';
import { INTEGRATIONS_PLUGIN_ID } from '../../../common';
import {
calculateAuthz,
calculatePackagePrivilegesFromKibanaPrivileges,
} from '../../../common/authz';
import { appContextService } from '..';
import { ENDPOINT_PRIVILEGES, PLUGIN_ID } from '../../constants';
import type {
FleetAuthzRequirements,
FleetRouteRequiredAuthz,
FleetAuthzRouteConfig,
} from './types';
export function checkSecurityEnabled() {
return appContextService.getSecurityLicense().isEnabled();
}
export 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 getAuthorizationFromPrivileges(
kibanaPrivileges: Array<{
resource?: string;
privilege: string;
authorized: boolean;
}>,
searchPrivilege: string
) {
const privilege = kibanaPrivileges.find((p) => p.privilege.includes(searchPrivilege));
return privilege ? privilege.authorized : false;
}
export async function getAuthzFromRequest(req: KibanaRequest): Promise<FleetAuthz> {
const security = appContextService.getSecurity();
if (security.authz.mode.useRbacForRequest(req)) {
const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req);
const endpointPrivileges = ENDPOINT_PRIVILEGES.map((privilege) =>
security.authz.actions.api.get(`${DEFAULT_APP_CATEGORIES.security.id}-${privilege}`)
);
const { privileges } = await checkPrivileges({
kibana: [
security.authz.actions.api.get(`${PLUGIN_ID}-all`),
security.authz.actions.api.get(`${PLUGIN_ID}-setup`),
security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-all`),
security.authz.actions.api.get(`${INTEGRATIONS_PLUGIN_ID}-read`),
...endpointPrivileges,
],
});
const fleetAllAuth = getAuthorizationFromPrivileges(privileges.kibana, `${PLUGIN_ID}-all`);
const intAllAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-all`
);
const intReadAuth = getAuthorizationFromPrivileges(
privileges.kibana,
`${INTEGRATIONS_PLUGIN_ID}-read`
);
const fleetSetupAuth = getAuthorizationFromPrivileges(privileges.kibana, 'fleet-setup');
return {
...calculateAuthz({
fleet: { all: fleetAllAuth, setup: fleetSetupAuth },
integrations: {
all: intAllAuth,
read: intReadAuth,
},
isSuperuser: checkSuperuser(req),
}),
packagePrivileges: calculatePackagePrivilegesFromKibanaPrivileges(privileges.kibana),
};
}
return calculateAuthz({
fleet: { all: false, setup: false },
integrations: {
all: false,
read: false,
},
isSuperuser: false,
});
}
interface RouteAuthz {
/** Is route access granted (based on authz) */
granted: boolean;
/** Was authorization to the api a result of Fleet (and Integrations) Privileges (as oposed to Package privileges) */
grantedByFleetPrivileges: boolean;
/**
* Set when `grantedByFleetPrivileges` is `false` and `granted` is true, which indicate access was granted
* via a Package Privileges. Array will hold the list of Package names that are allowed
*/
scopeDataToPackages: string[] | undefined;
}
/**
* Calculates Authz information for a Route, including:
* 1. Is access granted
* 2. was access granted based on Fleet and/or Integration privileges, and
* 3. a list of package names for which access was granted (only set if access was granted by package privileges)
*
* @param fleetAuthz
* @param requiredAuthz
*/
export const calculateRouteAuthz = (
fleetAuthz: FleetAuthz,
requiredAuthz: FleetRouteRequiredAuthz | undefined
): RouteAuthz => {
const response: RouteAuthz = {
granted: false,
grantedByFleetPrivileges: false,
scopeDataToPackages: undefined,
};
const fleetAuthzFlatten = flatten(fleetAuthz);
const isPrivilegeGranted = (flattenPrivilegeKey: string): boolean =>
fleetAuthzFlatten[flattenPrivilegeKey] === true;
if (typeof requiredAuthz === 'undefined') {
return response;
}
if (requiredAuthz.all) {
response.granted = Object.keys(flatten(requiredAuthz.all)).every(isPrivilegeGranted);
if (response.granted) {
if (requiredAuthz.all.fleet || requiredAuthz.all.integrations) {
response.grantedByFleetPrivileges = true;
}
return response;
}
}
if (requiredAuthz.any) {
response.granted = Object.keys(flatten(requiredAuthz.any)).some(isPrivilegeGranted);
if (response.granted) {
// Figure out if authz was granted via Fleet privileges
if (requiredAuthz.any.fleet || requiredAuthz.any.integrations) {
const fleetAnyPrivileges = pick(requiredAuthz.any, ['fleet', 'integrations']);
response.grantedByFleetPrivileges = Object.keys(flatten(fleetAnyPrivileges)).some(
isPrivilegeGranted
);
}
// If access was NOT granted via Fleet Authz, then retrieve a list of Package names that were
// granted access to their respective data.
if (!response.grantedByFleetPrivileges && requiredAuthz.any.packagePrivileges) {
for (const [packageName, packageRequiredAuthz] of Object.entries(
requiredAuthz.any.packagePrivileges
)) {
const packageRequiredAuthzKeys = Object.keys(
flatten({ packagePrivileges: { [packageName]: packageRequiredAuthz } })
);
if (packageRequiredAuthzKeys.some(isPrivilegeGranted)) {
if (!response.scopeDataToPackages) {
response.scopeDataToPackages = [];
}
response.scopeDataToPackages.push(packageName);
}
}
}
return response;
}
}
return response;
};
/**
* Utility to flatten an object's key all the way down to the last value.
* @param source
*/
function flatten(source: FleetAuthzRequirements | FleetAuthz): Record<string, boolean> {
const response: Record<string, boolean> = {};
const processKeys = (prefix: string, value: unknown) => {
if (typeof value === 'object' && value !== null) {
const objectKeys = Object.keys(value);
for (const key of objectKeys) {
processKeys(`${prefix}${prefix ? '.' : ''}${key}`, (value as Record<string, boolean>)[key]);
}
} else if (Array.isArray(value)) {
value.forEach((subValue, key) => {
processKeys(`${prefix}${prefix ? '.' : ''}${key}`, subValue);
});
} else {
response[prefix] = value as boolean;
}
};
processKeys('', source);
return response;
}
/**
* Utility to determine if a user has the required Fleet Authz based on user privileges
* and route required authz structure.
* @param authz
* @param fleetRequiredAuthz
* @returns boolean
*/
export const doesNotHaveRequiredFleetAuthz = (
authz: FleetAuthz,
fleetRequiredAuthz: FleetAuthzRouteConfig['fleetAuthz']
): boolean => {
return (
!!fleetRequiredAuthz &&
((typeof fleetRequiredAuthz === 'function' && !fleetRequiredAuthz(authz)) ||
(typeof fleetRequiredAuthz !== 'function' &&
!calculateRouteAuthz(authz, { all: fleetRequiredAuthz }).granted))
);
};

View file

@ -0,0 +1,62 @@
/*
* 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 { RouteConfig, RouteMethod } from '@kbn/core-http-server';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { IRouter, RequestHandler } from '@kbn/core/server';
import type { FleetRequestHandlerContext } from '../..';
import type { FleetAuthz } from '../../../common';
/** The values allowed for the `fleetAuthz` property of the Fleet Router registration interface. */
type FleetAuthzRouterConfigParam = FleetAuthzRequirements | ((userAuthz: FleetAuthz) => boolean);
type FleetAuthzRouteRegistrar<
Method extends RouteMethod,
Context extends RequestHandlerContext = RequestHandlerContext
> = <P, Q, B>(
route: FleetRouteConfig<P, Q, B, Method>,
handler: RequestHandler<P, Q, B, Context, Method>
) => void;
export interface FleetAuthzRouteConfig<
T extends FleetAuthzRouterConfigParam = FleetAuthzRouterConfigParam
> {
fleetAuthz?: T;
}
export type FleetRouteConfig<P, Q, B, Method extends RouteMethod> = RouteConfig<P, Q, B, Method> &
FleetAuthzRouteConfig;
// Fleet router that allow to add required access when registering route
export interface FleetAuthzRouter<
TContext extends FleetRequestHandlerContext = FleetRequestHandlerContext
> extends IRouter<TContext> {
get: FleetAuthzRouteRegistrar<'get', TContext>;
delete: FleetAuthzRouteRegistrar<'delete', TContext>;
post: FleetAuthzRouteRegistrar<'post', TContext>;
put: FleetAuthzRouteRegistrar<'put', TContext>;
patch: FleetAuthzRouteRegistrar<'patch', TContext>;
}
type DeepPartialTruthy<T> = {
[P in keyof T]?: T[P] extends boolean ? true : DeepPartialTruthy<T[P]>;
};
/**
* The set of authz properties required to be granted access to an API route
*/
export type FleetAuthzRequirements = DeepPartialTruthy<FleetAuthz>;
/**
* Interface used for registering and calculating authorization for a Fleet API routes
*/
export type FleetRouteRequiredAuthz = Partial<{
any: FleetAuthzRequirements;
all: FleetAuthzRequirements;
}>;

View file

@ -32,14 +32,18 @@ export type FleetRequestHandlerContext = CustomRequestHandlerContext<{
asCurrentUser: PackagePolicyClient;
asInternalUser: PackagePolicyClient;
};
epm: {
/**
* Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be
* used by routes that have additional privilege checks for authorization (such as requiring superuser).
*/
readonly internalSoClient: SavedObjectsClientContract;
};
/**
* Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be
* used by routes that have additional privilege checks for authorization (such as requiring superuser).
*/
readonly internalSoClient: SavedObjectsClientContract;
spaceId: string;
/**
* If data is to be limited to the list of integration package names. This will be set when
* authz to the API was granted only based on Package Privileges.
*/
limitedToPackages: string[] | undefined;
};
}>;

View file

@ -51,6 +51,7 @@ export default function (providerContext: FtrProviderContext) {
.post(`/api/fleet/epm/packages/_bulk`)
.auth(testUsers.fleet_all_int_read.username, testUsers.fleet_all_int_read.password)
.set('kbn-xsrf', 'xxxx')
.send({ packages: ['multiple_versions', 'overrides'] })
.expect(403);
});
it('should return 403 if user without fleet access requests upgrade', async function () {
@ -58,6 +59,7 @@ export default function (providerContext: FtrProviderContext) {
.post(`/api/fleet/epm/packages/_bulk`)
.auth(testUsers.integr_all_only.username, testUsers.integr_all_only.password)
.set('kbn-xsrf', 'xxxx')
.send({ packages: ['multiple_versions', 'overrides'] })
.expect(403);
});
it('should return 200 and an array for upgrading a package', async function () {