mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Authz] Superuser privileges (#196586)](https://github.com/elastic/kibana/pull/196586) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Elena Shostak","email":"165678770+elena-shostak@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-23T07:39:49Z","message":"[Authz] Superuser privileges (#196586)\n\n## Summary\r\n\r\nThis PR adds support for explicit indication whether endpoint is\r\nrestricted to superusers only.\r\nMoved `api/encrypted_saved_objects/_rotate_key` endpoint to the new\r\nconfiguration.\r\n\r\n__Relates: https://github.com/elastic/kibana/issues/196271__\r\n\r\n\r\n### Checklist\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## Release note\r\n\r\nIntroduced explicit configuration for routes that require superuser\r\naccess. Moved `api/encrypted_saved_objects/_rotate_key` endpoint to the\r\nnew configuration.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"598706c7d1d171bf7012e91d1389ade9734e8b35","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Team:Security","Feature:Security/Authorization","v9.0.0","backport:prev-minor"],"title":"[Authz] Superuser privileges","number":196586,"url":"https://github.com/elastic/kibana/pull/196586","mergeCommit":{"message":"[Authz] Superuser privileges (#196586)\n\n## Summary\r\n\r\nThis PR adds support for explicit indication whether endpoint is\r\nrestricted to superusers only.\r\nMoved `api/encrypted_saved_objects/_rotate_key` endpoint to the new\r\nconfiguration.\r\n\r\n__Relates: https://github.com/elastic/kibana/issues/196271__\r\n\r\n\r\n### Checklist\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## Release note\r\n\r\nIntroduced explicit configuration for routes that require superuser\r\naccess. Moved `api/encrypted_saved_objects/_rotate_key` endpoint to the\r\nnew configuration.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"598706c7d1d171bf7012e91d1389ade9734e8b35"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196586","number":196586,"mergeCommit":{"message":"[Authz] Superuser privileges (#196586)\n\n## Summary\r\n\r\nThis PR adds support for explicit indication whether endpoint is\r\nrestricted to superusers only.\r\nMoved `api/encrypted_saved_objects/_rotate_key` endpoint to the new\r\nconfiguration.\r\n\r\n__Relates: https://github.com/elastic/kibana/issues/196271__\r\n\r\n\r\n### Checklist\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## Release note\r\n\r\nIntroduced explicit configuration for routes that require superuser\r\naccess. Moved `api/encrypted_saved_objects/_rotate_key` endpoint to the\r\nnew configuration.\r\n\r\n---------\r\n\r\nCo-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>","sha":"598706c7d1d171bf7012e91d1389ade9734e8b35"}}]}] BACKPORT--> Co-authored-by: Elena Shostak <165678770+elena-shostak@users.noreply.github.com>
This commit is contained in:
parent
7e238131e6
commit
e291c9c704
11 changed files with 244 additions and 63 deletions
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import { validRouteSecurity } from './security_route_config_validator';
|
||||
import { ReservedPrivilegesSet } from '@kbn/core-http-server';
|
||||
|
||||
describe('RouteSecurity validation', () => {
|
||||
it('should pass validation for valid route security with authz enabled and valid required privileges', () => {
|
||||
|
@ -276,4 +277,31 @@ describe('RouteSecurity validation', () => {
|
|||
`"[authz.requiredPrivileges]: anyRequired privileges must contain unique values"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail validation when anyRequired has superuser privileges set', () => {
|
||||
const invalidRouteSecurity = {
|
||||
authz: {
|
||||
requiredPrivileges: [
|
||||
{ anyRequired: ['privilege1', 'privilege1'], allRequired: ['privilege4'] },
|
||||
{ anyRequired: ['privilege5', ReservedPrivilegesSet.superuser] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authz.requiredPrivileges]: Combining superuser with other privileges is redundant, superuser privileges set can be only used as a standalone privilege."`
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail validation when allRequired combines superuser privileges set with other privileges', () => {
|
||||
const invalidRouteSecurity = {
|
||||
authz: {
|
||||
requiredPrivileges: [ReservedPrivilegesSet.superuser, 'privilege1'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => validRouteSecurity(invalidRouteSecurity)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authz.requiredPrivileges]: Combining superuser with other privileges is redundant, superuser privileges set can be only used as a standalone privilege."`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { RouteSecurity, RouteConfigOptions } from '@kbn/core-http-server';
|
||||
import { ReservedPrivilegesSet } from '@kbn/core-http-server';
|
||||
import type { DeepPartial } from '@kbn/utility-types';
|
||||
|
||||
const privilegeSetSchema = schema.object(
|
||||
|
@ -49,6 +50,15 @@ const requiredPrivilegesSchema = schema.arrayOf(
|
|||
}
|
||||
});
|
||||
|
||||
// Combining superuser with other privileges is redundant.
|
||||
// If user is a superuser, they inherently have access to all the privileges that may come with other roles.
|
||||
if (
|
||||
anyRequired.includes(ReservedPrivilegesSet.superuser) ||
|
||||
(allRequired.includes(ReservedPrivilegesSet.superuser) && allRequired.length > 1)
|
||||
) {
|
||||
return 'Combining superuser with other privileges is redundant, superuser privileges set can be only used as a standalone privilege.';
|
||||
}
|
||||
|
||||
if (anyRequired.length && allRequired.length) {
|
||||
for (const privilege of anyRequired) {
|
||||
if (allRequired.includes(privilege)) {
|
||||
|
|
|
@ -127,6 +127,7 @@ export {
|
|||
getResponseValidation,
|
||||
isFullValidatorContainer,
|
||||
isKibanaResponse,
|
||||
ReservedPrivilegesSet,
|
||||
} from './src/router';
|
||||
|
||||
export type { ICspConfig } from './src/csp';
|
||||
|
|
|
@ -66,7 +66,7 @@ export type {
|
|||
PrivilegeSet,
|
||||
} from './route';
|
||||
|
||||
export { validBodyOutput } from './route';
|
||||
export { validBodyOutput, ReservedPrivilegesSet } from './route';
|
||||
export type {
|
||||
RouteValidationFunction,
|
||||
RouteValidationResultFactory,
|
||||
|
|
|
@ -187,6 +187,14 @@ export interface RouteSecurity {
|
|||
authc?: RouteAuthc;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of reserved privileges that can be used to check access to the route.
|
||||
*/
|
||||
export enum ReservedPrivilegesSet {
|
||||
operator = 'operator',
|
||||
superuser = 'superuser',
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional route options.
|
||||
* @public
|
||||
|
|
|
@ -242,7 +242,11 @@ export type {
|
|||
} from '@kbn/core-http-server';
|
||||
export type { IExternalUrlPolicy } from '@kbn/core-http-common';
|
||||
|
||||
export { validBodyOutput, OnPostAuthResultType } from '@kbn/core-http-server';
|
||||
export {
|
||||
validBodyOutput,
|
||||
OnPostAuthResultType,
|
||||
ReservedPrivilegesSet,
|
||||
} from '@kbn/core-http-server';
|
||||
|
||||
export type {
|
||||
HttpResourcesRenderOptions,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Type } from '@kbn/config-schema';
|
||||
import type { IRouter, RequestHandler, RequestHandlerContext, RouteConfig } from '@kbn/core/server';
|
||||
import { kibanaResponseFactory } from '@kbn/core/server';
|
||||
import { kibanaResponseFactory, ReservedPrivilegesSet } from '@kbn/core/server';
|
||||
import { httpServerMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { routeDefinitionParamsMock } from './index.mock';
|
||||
|
@ -43,9 +43,14 @@ describe('Key rotation routes', () => {
|
|||
});
|
||||
|
||||
it('correctly defines route.', () => {
|
||||
expect(routeConfig.security).toEqual({
|
||||
authz: {
|
||||
requiredPrivileges: [ReservedPrivilegesSet.superuser],
|
||||
},
|
||||
});
|
||||
expect(routeConfig.options).toEqual({
|
||||
access: 'public',
|
||||
tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'],
|
||||
tags: ['oas-tag:saved objects'],
|
||||
summary: `Rotate a key for encrypted saved objects`,
|
||||
description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key.
|
||||
NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`,
|
||||
|
@ -96,7 +101,7 @@ describe('Key rotation routes', () => {
|
|||
|
||||
expect(config.options).toEqual({
|
||||
access: 'internal',
|
||||
tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'],
|
||||
tags: ['oas-tag:saved objects'],
|
||||
summary: `Rotate a key for encrypted saved objects`,
|
||||
description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key.
|
||||
NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ReservedPrivilegesSet } from '@kbn/core/server';
|
||||
|
||||
import type { RouteDefinitionParams } from '.';
|
||||
|
||||
|
@ -39,9 +40,14 @@ export function defineKeyRotationRoutes({
|
|||
type: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [ReservedPrivilegesSet.superuser],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
tags: ['access:rotateEncryptionKey', 'oas-tag:saved objects'],
|
||||
access: buildFlavor === 'serverless' ? 'internal' : 'public',
|
||||
tags: ['oas-tag:saved objects'],
|
||||
summary: `Rotate a key for encrypted saved objects`,
|
||||
description: `If a saved object cannot be decrypted using the primary encryption key, Kibana attempts to decrypt it using the specified decryption-only keys. In most of the cases this overhead is negligible, but if you're dealing with a large number of saved objects and experiencing performance issues, you may want to rotate the encryption key.
|
||||
NOTE: Bulk key rotation can consume a considerable amount of resources and hence only user with a superuser role can trigger it.`,
|
||||
|
|
|
@ -127,3 +127,14 @@ export const API_VERSIONS = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Privileges that define the superuser role or the role equivalent to the superuser role.
|
||||
*/
|
||||
export const SUPERUSER_PRIVILEGES = {
|
||||
kibana: ['*'],
|
||||
elasticsearch: {
|
||||
cluster: ['all'],
|
||||
index: { '*': ['all'] },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { RouteSecurity } from '@kbn/core/server';
|
||||
import { ReservedPrivilegesSet } from '@kbn/core/server';
|
||||
import {
|
||||
coreMock,
|
||||
httpServerMock,
|
||||
|
@ -149,7 +150,10 @@ describe('initAPIAuthorization', () => {
|
|||
asserts,
|
||||
}: {
|
||||
security?: RouteSecurity;
|
||||
kibanaPrivilegesResponse?: Array<{ privilege: string; authorized: boolean }>;
|
||||
kibanaPrivilegesResponse?: {
|
||||
privileges: { kibana: Array<{ privilege: string; authorized: boolean }> };
|
||||
hasAllRequested?: boolean;
|
||||
};
|
||||
kibanaPrivilegesRequestActions?: string[];
|
||||
asserts: {
|
||||
forbidden?: boolean;
|
||||
|
@ -180,11 +184,7 @@ describe('initAPIAuthorization', () => {
|
|||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
const mockPostAuthToolkit = httpServiceMock.createOnPostAuthToolkit();
|
||||
|
||||
const mockCheckPrivileges = jest.fn().mockReturnValue({
|
||||
privileges: {
|
||||
kibana: kibanaPrivilegesResponse,
|
||||
},
|
||||
});
|
||||
const mockCheckPrivileges = jest.fn().mockReturnValue(kibanaPrivilegesResponse);
|
||||
mockAuthz.mode.useRbacForRequest.mockReturnValue(true);
|
||||
mockAuthz.checkPrivilegesDynamicallyWithRequest.mockImplementation((request) => {
|
||||
// hapi conceals the actual "request" from us, so we make sure that the headers are passed to
|
||||
|
@ -194,6 +194,12 @@ describe('initAPIAuthorization', () => {
|
|||
return mockCheckPrivileges;
|
||||
});
|
||||
|
||||
mockAuthz.checkPrivilegesWithRequest.mockImplementation((request) => {
|
||||
expect(request.headers).toMatchObject(headers);
|
||||
|
||||
return { globally: () => kibanaPrivilegesResponse };
|
||||
});
|
||||
|
||||
await postAuthHandler(mockRequest, mockResponse, mockPostAuthToolkit);
|
||||
|
||||
expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest);
|
||||
|
@ -207,11 +213,13 @@ describe('initAPIAuthorization', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith({
|
||||
kibana: kibanaPrivilegesRequestActions!.map((action: string) =>
|
||||
mockAuthz.actions.api.get(action)
|
||||
),
|
||||
});
|
||||
if (kibanaPrivilegesRequestActions) {
|
||||
expect(mockCheckPrivileges).toHaveBeenCalledWith({
|
||||
kibana: kibanaPrivilegesRequestActions!.map((action: string) =>
|
||||
mockAuthz.actions.api.get(action)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (asserts.forbidden) {
|
||||
expect(mockResponse.forbidden).toHaveBeenCalled();
|
||||
|
@ -239,11 +247,15 @@ describe('initAPIAuthorization', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3'],
|
||||
asserts: {
|
||||
authzResult: {
|
||||
|
@ -267,10 +279,14 @@ describe('initAPIAuthorization', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2'],
|
||||
asserts: {
|
||||
authzResult: {
|
||||
|
@ -293,11 +309,15 @@ describe('initAPIAuthorization', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: false },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: false },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3'],
|
||||
asserts: {
|
||||
authzResult: {
|
||||
|
@ -317,10 +337,14 @@ describe('initAPIAuthorization', () => {
|
|||
requiredPrivileges: ['privilege1', 'privilege2'],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2'],
|
||||
asserts: {
|
||||
authzResult: {
|
||||
|
@ -344,18 +368,54 @@ describe('initAPIAuthorization', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: false },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
],
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3'],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: false },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
asserts: {
|
||||
forbidden: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
testSecurityConfig(
|
||||
`protected route restricted to only superusers returns forbidden if user not a superuser`,
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [ReservedPrivilegesSet.superuser],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: { privileges: { kibana: [] }, hasAllRequested: false },
|
||||
asserts: {
|
||||
forbidden: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
testSecurityConfig(
|
||||
`protected route allowed only for superuser access returns "authzResult" if user is superuser`,
|
||||
{
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [ReservedPrivilegesSet.superuser],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: { privileges: { kibana: [] }, hasAllRequested: true },
|
||||
asserts: {
|
||||
authzResult: {
|
||||
[ReservedPrivilegesSet.superuser]: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
testSecurityConfig(
|
||||
`protected route returns forbidden if user doesn't have at least one from allRequired privileges requested`,
|
||||
{
|
||||
|
@ -369,12 +429,16 @@ describe('initAPIAuthorization', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: false },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
{ privilege: 'api:privilege4', authorized: true },
|
||||
],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: false },
|
||||
{ privilege: 'api:privilege3', authorized: false },
|
||||
{ privilege: 'api:privilege4', authorized: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2', 'privilege3', 'privilege4'],
|
||||
asserts: {
|
||||
forbidden: true,
|
||||
|
@ -390,10 +454,14 @@ describe('initAPIAuthorization', () => {
|
|||
requiredPrivileges: ['privilege1', 'privilege2'],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesResponse: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: false },
|
||||
],
|
||||
kibanaPrivilegesResponse: {
|
||||
privileges: {
|
||||
kibana: [
|
||||
{ privilege: 'api:privilege1', authorized: true },
|
||||
{ privilege: 'api:privilege2', authorized: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
kibanaPrivilegesRequestActions: ['privilege1', 'privilege2'],
|
||||
asserts: {
|
||||
forbidden: true,
|
||||
|
|
|
@ -14,18 +14,28 @@ import type {
|
|||
PrivilegeSet,
|
||||
RouteAuthz,
|
||||
} from '@kbn/core/server';
|
||||
import { ReservedPrivilegesSet } from '@kbn/core/server';
|
||||
import type { AuthorizationServiceSetup } from '@kbn/security-plugin-types-server';
|
||||
import type { RecursiveReadonly } from '@kbn/utility-types';
|
||||
|
||||
import { API_OPERATION_PREFIX } from '../../common/constants';
|
||||
import { API_OPERATION_PREFIX, SUPERUSER_PRIVILEGES } from '../../common/constants';
|
||||
|
||||
const isAuthzDisabled = (authz?: RecursiveReadonly<RouteAuthz>): authz is AuthzDisabled => {
|
||||
return (authz as AuthzDisabled)?.enabled === false;
|
||||
};
|
||||
|
||||
const isReservedPrivilegeSet = (privilege: string): privilege is ReservedPrivilegesSet => {
|
||||
return Object.hasOwn(ReservedPrivilegesSet, privilege);
|
||||
};
|
||||
|
||||
export function initAPIAuthorization(
|
||||
http: HttpServiceSetup,
|
||||
{ actions, checkPrivilegesDynamicallyWithRequest, mode }: AuthorizationServiceSetup,
|
||||
{
|
||||
actions,
|
||||
checkPrivilegesDynamicallyWithRequest,
|
||||
checkPrivilegesWithRequest,
|
||||
mode,
|
||||
}: AuthorizationServiceSetup,
|
||||
logger: Logger
|
||||
) {
|
||||
http.registerOnPostAuth(async (request, response, toolkit) => {
|
||||
|
@ -47,24 +57,54 @@ export function initAPIAuthorization(
|
|||
|
||||
const authz = security.authz as AuthzEnabled;
|
||||
|
||||
const requestedPrivileges = authz.requiredPrivileges.flatMap((privilegeEntry) => {
|
||||
if (typeof privilegeEntry === 'object') {
|
||||
return [...(privilegeEntry.allRequired ?? []), ...(privilegeEntry.anyRequired ?? [])];
|
||||
const { requestedPrivileges, requestedReservedPrivileges } = authz.requiredPrivileges.reduce(
|
||||
(acc, privilegeEntry) => {
|
||||
const privileges =
|
||||
typeof privilegeEntry === 'object'
|
||||
? [...(privilegeEntry.allRequired ?? []), ...(privilegeEntry.anyRequired ?? [])]
|
||||
: [privilegeEntry];
|
||||
|
||||
for (const privilege of privileges) {
|
||||
if (isReservedPrivilegeSet(privilege)) {
|
||||
acc.requestedReservedPrivileges.push(privilege);
|
||||
} else {
|
||||
acc.requestedPrivileges.push(privilege);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
requestedPrivileges: [] as string[],
|
||||
requestedReservedPrivileges: [] as string[],
|
||||
}
|
||||
);
|
||||
|
||||
return privilegeEntry;
|
||||
});
|
||||
|
||||
const apiActions = requestedPrivileges.map((permission) => actions.api.get(permission));
|
||||
const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request);
|
||||
const checkPrivilegesResponse = await checkPrivileges({ kibana: apiActions });
|
||||
|
||||
const privilegeToApiOperation = (privilege: string) =>
|
||||
privilege.replace(API_OPERATION_PREFIX, '');
|
||||
|
||||
const kibanaPrivileges: Record<string, boolean> = {};
|
||||
|
||||
for (const kbPrivilege of checkPrivilegesResponse.privileges.kibana) {
|
||||
kibanaPrivileges[privilegeToApiOperation(kbPrivilege.privilege)] = kbPrivilege.authorized;
|
||||
if (requestedPrivileges.length > 0) {
|
||||
const checkPrivilegesResponse = await checkPrivileges({
|
||||
kibana: requestedPrivileges.map((permission) => actions.api.get(permission)),
|
||||
});
|
||||
|
||||
for (const kbPrivilege of checkPrivilegesResponse.privileges.kibana) {
|
||||
kibanaPrivileges[privilegeToApiOperation(kbPrivilege.privilege)] = kbPrivilege.authorized;
|
||||
}
|
||||
}
|
||||
|
||||
for (const reservedPrivilege of requestedReservedPrivileges) {
|
||||
if (reservedPrivilege === ReservedPrivilegesSet.superuser) {
|
||||
const checkSuperuserPrivilegesResponse = await checkPrivilegesWithRequest(
|
||||
request
|
||||
).globally(SUPERUSER_PRIVILEGES);
|
||||
|
||||
kibanaPrivileges[ReservedPrivilegesSet.superuser] =
|
||||
checkSuperuserPrivilegesResponse.hasAllRequested;
|
||||
}
|
||||
}
|
||||
|
||||
const hasRequestedPrivilege = (kbPrivilege: Privilege | PrivilegeSet) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue