Skip to content

Commit

Permalink
[Authz] OAS Descriptions for Route Authz (#197001)
Browse files Browse the repository at this point in the history
Closes #191714

## Summary

Update process router to generate authz descriptions based on the new
Route Security objects.


### Checklist

Delete any items that are not applicable to this PR.

- [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

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
3 people authored Oct 28, 2024
1 parent 3791a9b commit a168458
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 5 deletions.
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].'
);
});
});
13 changes: 11 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,10 +64,19 @@ export const processRouter = (
parameters.push(...pathObjects, ...queryObjects);
}

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

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

const hasDeprecations = !!route.options.deprecated;

const operation: CustomOperationObject = {
summary: route.options.summary ?? '',
tags: route.options.tags ? extractTags(route.options.tags) : [],
...(description ? { description } : {}),
...(route.options.description ? { description: route.options.description } : {}),
...(hasDeprecations ? { deprecated: true } : {}),
...(route.options.discontinued ? { 'x-discontinued': route.options.discontinued } : {}),
Expand Down Expand Up @@ -99,7 +109,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 @@ -144,6 +144,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 @@ -155,6 +171,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 @@ -14,6 +14,7 @@ import {
} from '@kbn/core-http-router-server-internal';
import type { RouteMethod, VersionedRouterRoute } 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 @@ -90,6 +91,13 @@ 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);
Expand All @@ -98,6 +106,7 @@ export const processVersionedRouter = (
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 } : {}),
...(hasDeprecations ? { deprecated: true } : {}),
...(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",
]
}

0 comments on commit a168458

Please sign in to comment.