[Fleet] remove superuser requirement in PackageService (#163727)

## Summary

Remove superuser requirement in PackageService and replacing it with the
same privilege requirement as the API uses.

`PackageService` was introduced in
https://github.com/elastic/kibana/pull/121589
@joeypoon Is it okay for security team to change these privileges?

WIP, added only for `ensureInstalledPackage` for now.

### 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
This commit is contained in:
Julia Bardi 2023-08-15 11:33:32 +02:00 committed by GitHub
parent 11e57be842
commit feb72cd69f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 76 additions and 53 deletions

View file

@ -14,6 +14,7 @@ import {
type FleetAuthzRouter,
getRouteRequiredAuthz,
} from '../../services/security';
import type { FleetAuthzRouteConfig } from '../../services/security/types';
import type {
DeletePackageResponse,
@ -68,14 +69,20 @@ import {
const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
export const INSTALL_PACKAGES_AUTHZ: FleetAuthzRouteConfig['fleetAuthz'] = {
integrations: { installPackages: true },
};
export const READ_PACKAGE_INFO_AUTHZ: FleetAuthzRouteConfig['fleetAuthz'] = {
integrations: { readPackageInfo: true },
};
export const registerRoutes = (router: FleetAuthzRouter) => {
router.get(
{
path: EPM_API_ROUTES.CATEGORIES_PATTERN,
validate: GetCategoriesRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getCategoriesHandler
);
@ -84,9 +91,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.LIST_PATTERN,
validate: GetPackagesRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getListHandler
);
@ -95,9 +100,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.INSTALLED_LIST_PATTERN,
validate: GetInstalledPackagesRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getInstalledListHandler
);
@ -106,9 +109,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.LIMITED_LIST_PATTERN,
validate: false,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getLimitedListHandler
);
@ -117,9 +118,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.STATS_PATTERN,
validate: GetStatsRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getStatsHandler
);
@ -128,9 +127,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.FILEPATH_PATTERN,
validate: GetFileRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getFileHandler
);
@ -161,9 +158,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN,
validate: InstallPackageFromRegistryRequestSchema,
fleetAuthz: {
integrations: { installPackages: true },
},
fleetAuthz: INSTALL_PACKAGES_AUTHZ,
},
installPackageFromRegistryHandler
);
@ -202,9 +197,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.CUSTOM_INTEGRATIONS_PATTERN,
validate: CreateCustomIntegrationRequestSchema,
fleetAuthz: {
integrations: { installPackages: true },
},
fleetAuthz: INSTALL_PACKAGES_AUTHZ,
},
createCustomIntegrationHandler
);
@ -224,9 +217,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.VERIFICATION_KEY_ID,
validate: false,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getVerificationKeyIdHandler
);
@ -235,9 +226,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.DATA_STREAMS_PATTERN,
validate: GetDataStreamsRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getDataStreamsHandler
);
@ -246,9 +235,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.BULK_ASSETS_PATTERN,
validate: GetBulkAssetsRequestSchema,
fleetAuthz: {
integrations: { readPackageInfo: true },
},
fleetAuthz: READ_PACKAGE_INFO_AUTHZ,
},
getBulkAssetsHandler
);
@ -305,9 +292,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
{
path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN_DEPRECATED,
validate: InstallPackageFromRegistryRequestSchemaDeprecated,
fleetAuthz: {
integrations: { installPackages: true },
},
fleetAuthz: INSTALL_PACKAGES_AUTHZ,
},
async (context, request, response) => {
const newRequest = {
@ -356,7 +341,7 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
path: EPM_API_ROUTES.REAUTHORIZE_TRANSFORMS,
validate: ReauthorizeTransformRequestSchema,
fleetAuthz: {
integrations: { installPackages: true },
...INSTALL_PACKAGES_AUTHZ,
packagePrivileges: {
transform: {
actions: {

View file

@ -5,7 +5,12 @@
* 2.0.
*/
jest.mock('../security');
jest.mock('../security', () => {
return {
...jest.requireActual('../security'),
getAuthzFromRequest: jest.fn(),
};
});
import type { MockedLogger } from '@kbn/logging-mocks';
@ -20,6 +25,8 @@ import {
import { FleetUnauthorizedError } from '../../errors';
import type { InstallablePackage } from '../../types';
import { getAuthzFromRequest } from '../security';
import type { PackageClient, PackageService } from './package_service';
import { PackageServiceImpl } from './package_service';
import * as epmPackagesGet from './packages/get';
@ -28,6 +35,7 @@ import * as epmRegistry from './registry';
import * as epmTransformsInstall from './elasticsearch/transform/install';
import * as epmArchiveParse from './archive/parse';
const mockGetAuthzFromRequest = getAuthzFromRequest as jest.Mock;
const testKeys = [
'getInstallation',
'ensureInstalledPackage',
@ -206,6 +214,14 @@ describe('PackageService', () => {
const unauthError = new FleetUnauthorizedError(
`User does not have adequate permissions to access Fleet packages.`
);
beforeEach(() => {
mockGetAuthzFromRequest.mockResolvedValueOnce({
integrations: {
installPackages: false,
readPackageInfo: false,
},
});
});
it(`rejects on ${testKey}`, async () => {
const { method, args } = getTest(
@ -217,6 +233,14 @@ describe('PackageService', () => {
});
describe.each(testKeys)('with required privileges', (testKey: string) => {
beforeEach(() => {
mockGetAuthzFromRequest.mockResolvedValueOnce({
integrations: {
installPackages: true,
readPackageInfo: true,
},
});
});
it(`calls ${testKey} and returns results`, async () => {
const mockClients = {
packageClient: mockPackageService.asInternalUser,

View file

@ -27,8 +27,10 @@ import type {
ArchivePackage,
BundledPackage,
} from '../../types';
import { checkSuperuser } from '../security';
import type { FleetAuthzRouteConfig } from '../security/types';
import { checkSuperuser, getAuthzFromRequest, doesNotHaveRequiredFleetAuthz } from '../security';
import { FleetUnauthorizedError } from '../../errors';
import { INSTALL_PACKAGES_AUTHZ, READ_PACKAGE_INFO_AUTHZ } from '../../routes/epm';
import { installTransforms, isTransform } from './elasticsearch/transform/install';
import type { FetchFindLatestPackageOptions } from './registry';
@ -86,8 +88,17 @@ export class PackageServiceImpl implements PackageService {
) {}
public asScoped(request: KibanaRequest) {
const preflightCheck = () => {
if (!checkSuperuser(request)) {
const preflightCheck = async (requiredAuthz?: FleetAuthzRouteConfig['fleetAuthz']) => {
if (requiredAuthz) {
const requestedAuthz = await getAuthzFromRequest(request);
const noRequiredAuthz = doesNotHaveRequiredFleetAuthz(requestedAuthz, requiredAuthz);
if (noRequiredAuthz) {
throw new FleetUnauthorizedError(
`User does not have adequate permissions to access Fleet packages.`
);
}
} else if (!checkSuperuser(request)) {
throw new FleetUnauthorizedError(
`User does not have adequate permissions to access Fleet packages.`
);
@ -115,7 +126,9 @@ class PackageClientImpl implements PackageClient {
private readonly internalEsClient: ElasticsearchClient,
private readonly internalSoClient: SavedObjectsClientContract,
private readonly logger: Logger,
private readonly preflightCheck?: () => void | Promise<void>,
private readonly preflightCheck?: (
requiredAuthz?: FleetAuthzRouteConfig['fleetAuthz']
) => void | Promise<void>,
private readonly request?: KibanaRequest
) {}
@ -127,7 +140,7 @@ class PackageClientImpl implements PackageClient {
}
public async getInstallation(pkgName: string) {
await this.#runPreflight();
await this.#runPreflight(READ_PACKAGE_INFO_AUTHZ);
return getInstallation({
pkgName,
savedObjectsClient: this.internalSoClient,
@ -139,7 +152,7 @@ class PackageClientImpl implements PackageClient {
pkgVersion?: string;
spaceId?: string;
}): Promise<Installation | undefined> {
await this.#runPreflight();
await this.#runPreflight(INSTALL_PACKAGES_AUTHZ);
return ensureInstalledPackage({
...options,
@ -152,12 +165,12 @@ class PackageClientImpl implements PackageClient {
packageName: string,
options?: FetchFindLatestPackageOptions
): Promise<RegistryPackage | BundledPackage> {
await this.#runPreflight();
await this.#runPreflight(READ_PACKAGE_INFO_AUTHZ);
return fetchFindLatestPackageOrThrow(packageName, options);
}
public async readBundledPackage(bundledPackage: BundledPackage) {
await this.#runPreflight();
await this.#runPreflight(READ_PACKAGE_INFO_AUTHZ);
return generatePackageInfoFromArchiveBuffer(bundledPackage.buffer, 'application/zip');
}
@ -166,7 +179,7 @@ class PackageClientImpl implements PackageClient {
packageVersion: string,
options?: Parameters<typeof getPackage>['2']
) {
await this.#runPreflight();
await this.#runPreflight(READ_PACKAGE_INFO_AUTHZ);
return getPackage(packageName, packageVersion, options);
}
@ -176,7 +189,7 @@ class PackageClientImpl implements PackageClient {
prerelease?: false;
}) {
const { excludeInstallStatus, category, prerelease } = params || {};
await this.#runPreflight();
await this.#runPreflight(READ_PACKAGE_INFO_AUTHZ);
return getPackages({
savedObjectsClient: this.internalSoClient,
excludeInstallStatus,
@ -189,7 +202,7 @@ class PackageClientImpl implements PackageClient {
packageInfo: InstallablePackage,
assetPaths: string[]
): Promise<InstalledAssetType[]> {
await this.#runPreflight();
await this.#runPreflight(INSTALL_PACKAGES_AUTHZ);
let installedAssets: InstalledAssetType[] = [];
const transformPaths = assetPaths.filter(isTransform);
@ -207,7 +220,7 @@ class PackageClientImpl implements PackageClient {
}
async #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) {
const authorizationHeader = await this.getAuthorizationHeader();
const authorizationHeader = this.getAuthorizationHeader();
const { installedTransforms } = await installTransforms({
installablePackage: packageInfo,
@ -222,9 +235,9 @@ class PackageClientImpl implements PackageClient {
return installedTransforms;
}
#runPreflight() {
async #runPreflight(requiredAuthz?: FleetAuthzRouteConfig['fleetAuthz']) {
if (this.preflightCheck) {
return this.preflightCheck();
return await this.preflightCheck(requiredAuthz);
}
}
}

View file

@ -61,7 +61,8 @@ describe('Endpoint Policy Response', () => {
login();
});
describe('from Fleet Agent Details page', () => {
// TODO failing test skipped https://github.com/elastic/kibana/issues/162428
describe.skip('from Fleet Agent Details page', () => {
it('should display policy response with errors', () => {
navigateToFleetAgentDetails(endpointMetadata.agent.id);