Skip to content

Commit

Permalink
[8.x] [Authz] OAS Descriptions for Route Authz (#197001) (#198055)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.x`:
- [[Authz] OAS Descriptions for Route Authz
(#197001)](#197001)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Sid","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-28T14:12:23Z","message":"[Authz]
OAS Descriptions for Route Authz (#197001)\n\nCloses
https://github.com/elastic/kibana/issues/191714\r\n\r\n##
Summary\r\n\r\nUpdate process router to generate authz descriptions
based on the new\r\nRoute Security objects.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"a1684580bc3d6a54dc7e4375384ebaee1410b186","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Security","enhancement","Feature:Security/Authorization","Feature:Hardening","v9.0.0","backport:prev-major"],"number":197001,"url":"https://github.com/elastic/kibana/pull/197001","mergeCommit":{"message":"[Authz]
OAS Descriptions for Route Authz (#197001)\n\nCloses
https://github.com/elastic/kibana/issues/191714\r\n\r\n##
Summary\r\n\r\nUpdate process router to generate authz descriptions
based on the new\r\nRoute Security objects.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"a1684580bc3d6a54dc7e4375384ebaee1410b186"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/197001","number":197001,"mergeCommit":{"message":"[Authz]
OAS Descriptions for Route Authz (#197001)\n\nCloses
https://github.com/elastic/kibana/issues/191714\r\n\r\n##
Summary\r\n\r\nUpdate process router to generate authz descriptions
based on the new\r\nRoute Security objects.\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>\r\nCo-authored-by:
Elastic Machine
<[email protected]>","sha":"a1684580bc3d6a54dc7e4375384ebaee1410b186"}}]}]
BACKPORT-->
  • Loading branch information
SiddharthMantri authored Oct 29, 2024
1 parent c31efcf commit 95d9f14
Show file tree
Hide file tree
Showing 8 changed files with 218 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].'
);
});
});
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",
]
}

0 comments on commit 95d9f14

Please sign in to comment.