Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [Authz] OAS Descriptions for Route Authz (#197001) #198055

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { schema } from '@kbn/config-schema';
import { extractAuthzDescription } from './extract_authz_description';
import { InternalRouterRoute } from './type';
import { RouteSecurity } from '@kbn/core-http-server';

describe('extractAuthzDescription', () => {
it('should return empty if route does not require privileges', () => {
const route: InternalRouterRoute = {
path: '/foo',
options: { access: 'internal' },
handler: jest.fn(),
validationSchemas: { request: { body: schema.object({}) } },
method: 'get',
isVersioned: false,
};
const description = extractAuthzDescription(route.security);
expect(description).toBe('');
});

it('should return route authz description for simple privileges', () => {
const routeSecurity: RouteSecurity = {
authz: {
requiredPrivileges: ['manage_spaces'],
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe('[Authz] Route required privileges: ALL of [manage_spaces].');
});

it('should return route authz description for privilege groups', () => {
{
const routeSecurity: RouteSecurity = {
authz: {
requiredPrivileges: [{ allRequired: ['console'] }],
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe('[Authz] Route required privileges: ALL of [console].');
}
{
const routeSecurity: RouteSecurity = {
authz: {
requiredPrivileges: [
{
anyRequired: ['manage_spaces', 'taskmanager'],
},
],
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Authz] Route required privileges: ANY of [manage_spaces OR taskmanager].'
);
}
{
const routeSecurity: RouteSecurity = {
authz: {
requiredPrivileges: [
{
allRequired: ['console', 'filesManagement'],
anyRequired: ['manage_spaces', 'taskmanager'],
},
],
},
};
const description = extractAuthzDescription(routeSecurity);
expect(description).toBe(
'[Authz] Route required privileges: ALL of [console, filesManagement] AND ANY of [manage_spaces OR taskmanager].'
);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { AuthzEnabled, AuthzDisabled, InternalRouteSecurity } from '@kbn/core-http-server';

interface PrivilegeGroupValue {
allRequired: string[];
anyRequired: string[];
}

export const extractAuthzDescription = (routeSecurity: InternalRouteSecurity | undefined) => {
if (!routeSecurity) {
return '';
}
if (!('authz' in routeSecurity) || (routeSecurity.authz as AuthzDisabled).enabled === false) {
return '';
}

const privileges = (routeSecurity.authz as AuthzEnabled).requiredPrivileges;

const groupedPrivileges = privileges.reduce<PrivilegeGroupValue>(
(groups, privilege) => {
if (typeof privilege === 'string') {
groups.allRequired.push(privilege);

return groups;
}
groups.allRequired.push(...(privilege.allRequired ?? []));
groups.anyRequired.push(...(privilege.anyRequired ?? []));

return groups;
},
{
anyRequired: [],
allRequired: [],
}
);

const getPrivilegesDescription = (allRequired: string[], anyRequired: string[]) => {
const allDescription = allRequired.length ? `ALL of [${allRequired.join(', ')}]` : '';
const anyDescription = anyRequired.length ? `ANY of [${anyRequired.join(' OR ')}]` : '';

return `${allDescription}${allDescription && anyDescription ? ' AND ' : ''}${anyDescription}`;
};

const getDescriptionForRoute = () => {
const allRequired = [...groupedPrivileges.allRequired];
const anyRequired = [...groupedPrivileges.anyRequired];

return `Route required privileges: ${getPrivilegesDescription(allRequired, anyRequired)}.`;
};

return `[Authz] ${getDescriptionForRoute()}`;
};
35 changes: 33 additions & 2 deletions packages/kbn-router-to-openapispec/src/process_router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { schema } from '@kbn/config-schema';
import { Router } from '@kbn/core-http-router-server-internal';
import { OasConverter } from './oas_converter';
import { createOperationIdCounter } from './operation_id_counter';
import { extractResponses, processRouter, type InternalRouterRoute } from './process_router';
import { extractResponses, processRouter } from './process_router';
import { type InternalRouterRoute } from './type';

describe('extractResponses', () => {
let oasConverter: OasConverter;
Expand Down Expand Up @@ -102,6 +103,24 @@ describe('processRouter', () => {
handler: jest.fn(),
validationSchemas: { request: { body: schema.object({}) } },
},
{
path: '/qux',
method: 'post',
options: {},
handler: jest.fn(),
validationSchemas: { request: { body: schema.object({}) } },
security: {
authz: {
requiredPrivileges: [
'manage_spaces',
{
allRequired: ['taskmanager'],
anyRequired: ['console'],
},
],
},
},
},
],
} as unknown as Router;

Expand All @@ -110,11 +129,23 @@ describe('processRouter', () => {
version: '2023-10-31',
});

expect(Object.keys(result1.paths!)).toHaveLength(3);
expect(Object.keys(result1.paths!)).toHaveLength(4);

const result2 = processRouter(testRouter, new OasConverter(), createOperationIdCounter(), {
version: '2024-10-31',
});
expect(Object.keys(result2.paths!)).toHaveLength(0);
});

it('updates description with privileges required', () => {
const result = processRouter(testRouter, new OasConverter(), createOperationIdCounter(), {
version: '2023-10-31',
});

expect(result.paths['/qux']?.post).toBeDefined();

expect(result.paths['/qux']?.post?.description).toEqual(
'[Authz] Route required privileges: ALL of [manage_spaces, taskmanager] AND ANY of [console].'
);
});
});
12 changes: 10 additions & 2 deletions packages/kbn-router-to-openapispec/src/process_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
} from './util';
import type { OperationIdCounter } from './operation_id_counter';
import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas';
import type { CustomOperationObject } from './type';
import type { CustomOperationObject, InternalRouterRoute } from './type';
import { extractAuthzDescription } from './extract_authz_description';

export const processRouter = (
appRouter: Router,
Expand Down Expand Up @@ -63,9 +64,17 @@ export const processRouter = (
parameters.push(...pathObjects, ...queryObjects);
}

let description = '';
if (route.security) {
const authzDescription = extractAuthzDescription(route.security);

description = `${route.options.description ?? ''}${authzDescription ?? ''}`;
}

const operation: CustomOperationObject = {
summary: route.options.summary ?? '',
tags: route.options.tags ? extractTags(route.options.tags) : [],
...(description ? { description } : {}),
...(route.options.description ? { description: route.options.description } : {}),
...(route.options.deprecated ? { deprecated: route.options.deprecated } : {}),
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
Expand Down Expand Up @@ -98,7 +107,6 @@ export const processRouter = (
return { paths };
};

export type InternalRouterRoute = ReturnType<Router['getRoutes']>[0];
export const extractResponses = (route: InternalRouterRoute, converter: OasConverter) => {
const responses: OpenAPIV3.ResponsesObject = {};
if (!route.validationSchemas) return responses;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ describe('processVersionedRouter', () => {
'application/test+json; Elastic-Api-Version=2023-10-31',
]);
});

it('correctly updates the authz description for routes that require privileges', () => {
const results = processVersionedRouter(
{ getRoutes: () => [createTestRoute()] } as unknown as CoreVersionedRouter,
new OasConverter(),
createOperationIdCounter(),
{}
);
expect(results.paths['/foo']).toBeDefined();

expect(results.paths['/foo']!.get).toBeDefined();

expect(results.paths['/foo']!.get!.description).toBe(
'[Authz] Route required privileges: ALL of [manage_spaces].'
);
});
});

const createTestRoute: () => VersionedRouterRoute = () => ({
Expand All @@ -156,6 +172,11 @@ const createTestRoute: () => VersionedRouterRoute = () => ({
deprecated: true,
discontinued: 'discontinued versioned router',
options: { body: { access: ['application/test+json'] } as any },
security: {
authz: {
requiredPrivileges: ['manage_spaces'],
},
},
},
handlers: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@kbn/core-http-router-server-internal';
import type { RouteMethod } from '@kbn/core-http-server';
import type { OpenAPIV3 } from 'openapi-types';
import { extractAuthzDescription } from './extract_authz_description';
import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas';
import type { OasConverter } from './oas_converter';
import { isReferenceObject } from './oas_converter/common';
Expand Down Expand Up @@ -91,12 +92,20 @@ export const processVersionedRouter = (
];
}

let description = '';
if (route.options.security) {
const authzDescription = extractAuthzDescription(route.options.security);

description = `${route.options.description ?? ''}${authzDescription ?? ''}`;
}

const hasBody = Boolean(extractValidationSchemaFromVersionedHandler(handler)?.request?.body);
const contentType = extractContentType(route.options.options?.body);
const hasVersionFilter = Boolean(filters?.version);
const operation: OpenAPIV3.OperationObject = {
summary: route.options.summary ?? '',
tags: route.options.options?.tags ? extractTags(route.options.options.tags) : [],
...(description ? { description } : {}),
...(route.options.description ? { description: route.options.description } : {}),
...(route.options.deprecated ? { deprecated: route.options.deprecated } : {}),
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
Expand Down
3 changes: 3 additions & 0 deletions packages/kbn-router-to-openapispec/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { Router } from '@kbn/core-http-router-server-internal';
import type { OpenAPIV3 } from '../openapi-types';
export type { OpenAPIV3 } from '../openapi-types';
export interface KnownParameters {
Expand Down Expand Up @@ -39,3 +40,5 @@ export type CustomOperationObject = OpenAPIV3.OperationObject<{
// Custom OpenAPI from ES API spec based on @availability
'x-state'?: 'Technical Preview' | 'Beta';
}>;

export type InternalRouterRoute = ReturnType<Router['getRoutes']>[0];
2 changes: 1 addition & 1 deletion packages/kbn-router-to-openapispec/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"@kbn/core-http-router-server-internal",
"@kbn/core-http-server",
"@kbn/config-schema",
"@kbn/zod"
"@kbn/zod",
]
}