Skip to content

Commit

Permalink
[Fleet][Endpoint][RBAC V2] Update fleet router and config to allow AP…
Browse files Browse the repository at this point in the history
…I 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 <[email protected]>
  • Loading branch information
ashokaditya and paul-tavares authored Dec 14, 2022
1 parent d1e7f50 commit b1a75ae
Show file tree
Hide file tree
Showing 40 changed files with 1,405 additions and 483 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
5 changes: 2 additions & 3 deletions x-pack/plugins/fleet/server/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,9 @@ export const createFleetRequestHandlerContextMock = (): jest.Mocked<
asCurrentUser: createPackagePolicyServiceMock(),
asInternalUser: createPackagePolicyServiceMock(),
},
epm: {
internalSoClient: savedObjectsClientMock.create(),
},
internalSoClient: savedObjectsClientMock.create(),
spaceId: 'default',
limitedToPackages: undefined,
};
};

Expand Down
46 changes: 34 additions & 12 deletions x-pack/plugins/fleet/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
}
},
};
}
);
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/routes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { FleetAuthzRouter } from '../../services/security';

import { AGENT_API_ROUTES } from '../../constants';
import {
GetAgentsRequestSchema,
Expand All @@ -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';

Expand Down
10 changes: 5 additions & 5 deletions x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/routes/agent_policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { FleetAuthzRouter } from '../../services/security';

import { AGENT_POLICY_API_ROUTES } from '../../constants';
import {
GetAgentPoliciesRequestSchema,
Expand All @@ -17,7 +19,6 @@ import {
GetK8sManifestRequestSchema,
BulkGetAgentPoliciesRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';

import { K8S_API_ROUTES } from '../../../common/constants';

Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/routes/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -90,7 +91,6 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN,
validate: CheckPermissionsRequestSchema,
options: { tags: [] },
},
getCheckPermissionsHandler
);
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/routes/data_streams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/routes/download_source/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { FleetAuthzRouter } from '../../services/security';

import { DOWNLOAD_SOURCE_API_ROUTES } from '../../constants';
import {
getDownloadSourcesRequestSchema,
Expand All @@ -13,7 +15,6 @@ import {
PostDownloadSourcesRequestSchema,
DeleteDownloadSourcesRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';

import {
getDownloadSourcesHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
* 2.0.
*/

import type { FleetAuthzRouter } from '../../services/security';

import { ENROLLMENT_API_KEY_ROUTES } from '../../constants';
import {
GetEnrollmentAPIKeysRequestSchema,
GetOneEnrollmentAPIKeyRequestSchema,
DeleteEnrollmentAPIKeyRequestSchema,
PostEnrollmentAPIKeyRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';

import {
getEnrollmentApiKeysHandler,
Expand Down
20 changes: 10 additions & 10 deletions x-pack/plugins/fleet/server/routes/epm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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 });
Expand All @@ -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 }),
};
Expand All @@ -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;

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/routes/epm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import type { IKibanaResponse } from '@kbn/core/server';

import type { FleetAuthzRouter } from '../../services/security';

import type {
DeletePackageResponse,
GetInfoResponse,
Expand All @@ -32,7 +34,6 @@ import {
UpdatePackageRequestSchema,
UpdatePackageRequestSchemaDeprecated,
} from '../../types';
import type { FleetAuthzRouter } from '../security';

import {
getCategoriesHandler,
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
* 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,
PostFleetProxyRequestSchema,
PutFleetProxyRequestSchema,
} from '../../types';

import type { FleetAuthzRouter } from '../security';

import {
getAllFleetProxyHandler,
postFleetProxyHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import type { FleetAuthzRouter } from '../../services/security';

import { FLEET_SERVER_HOST_API_ROUTES } from '../../../common/constants';
import {
GetAllFleetServerHostRequestSchema,
Expand All @@ -13,8 +15,6 @@ import {
PutFleetServerHostRequestSchema,
} from '../../types';

import type { FleetAuthzRouter } from '../security';

import {
deleteFleetServerPolicyHandler,
getAllFleetServerPolicyHandler,
Expand Down
Loading

0 comments on commit b1a75ae

Please sign in to comment.