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] [SecuritySolution][ProductFeatures] Add support for `security.authz.requiredPrivileges` for the API auth control (#198312) #198566

Merged
merged 1 commit into from
Oct 31, 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
Expand Up @@ -23,6 +23,7 @@ import type {
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
import { httpServiceMock } from '@kbn/core-http-server-mocks';
import type {
AuthzEnabled,
KibanaRequest,
LifecycleResponseFactory,
OnPostAuthHandler,
Expand Down Expand Up @@ -181,11 +182,6 @@ describe('ProductFeaturesService', () => {
lastRegisteredFn = fn;
});

const getReq = (tags: string[] = []) =>
({
route: { options: { tags } },
url: { pathname: '', search: '' },
} as unknown as KibanaRequest);
const res = { notFound: jest.fn() } as unknown as LifecycleResponseFactory;
const toolkit = httpServiceMock.createOnPostAuthToolkit();

Expand All @@ -204,93 +200,281 @@ describe('ProductFeaturesService', () => {
expect(mockHttpSetup.registerOnPostAuth).toHaveBeenCalledTimes(1);
});

it('should authorize when no tag matches', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

await lastRegisteredFn(getReq(['access:something', 'access:securitySolution']), res, toolkit);

expect(MockedProductFeatures.mock.instances[0].isActionRegistered).not.toHaveBeenCalled();
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should check when tag matches and return not found when not action registered', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

(MockedProductFeatures.mock.instances[0].isActionRegistered as jest.Mock).mockReturnValueOnce(
false
);
await lastRegisteredFn(getReq(['access:securitySolution-foo']), res, toolkit);

expect(MockedProductFeatures.mock.instances[0].isActionRegistered).toHaveBeenCalledWith(
'api:securitySolution-foo'
);
expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

it('should check when tag matches and continue when action registered', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

(MockedProductFeatures.mock.instances[0].isActionRegistered as jest.Mock).mockReturnValueOnce(
true
);
await lastRegisteredFn(getReq(['access:securitySolution-foo']), res, toolkit);

expect(MockedProductFeatures.mock.instances[0].isActionRegistered).toHaveBeenCalledWith(
'api:securitySolution-foo'
);
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should check when productFeature tag when it matches and return not found when not enabled', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(false);

await lastRegisteredFn(getReq(['securitySolutionProductFeature:foo']), res, toolkit);

expect(productFeaturesService.isEnabled).toHaveBeenCalledWith('foo');
expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
describe('when using productFeature tag', () => {
const getReq = (tags: string[] = []) =>
({
route: { options: { tags } },
url: { pathname: '', search: '' },
} as unknown as KibanaRequest);

it('should check when productFeature tag when it matches and return not found when not enabled', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(false);

await lastRegisteredFn(getReq(['securitySolutionProductFeature:foo']), res, toolkit);

expect(productFeaturesService.isEnabled).toHaveBeenCalledWith('foo');
expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

it('should check when productFeature tag when it matches and continue when enabled', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(true);

await lastRegisteredFn(getReq(['securitySolutionProductFeature:foo']), res, toolkit);

expect(productFeaturesService.isEnabled).toHaveBeenCalledWith('foo');
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});
});

it('should check when productFeature tag when it matches and continue when enabled', async () => {
const experimentalFeatures = {} as ExperimentalFeatures;
const productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);

productFeaturesService.isEnabled = jest.fn().mockReturnValueOnce(true);

await lastRegisteredFn(getReq(['securitySolutionProductFeature:foo']), res, toolkit);

expect(productFeaturesService.isEnabled).toHaveBeenCalledWith('foo');
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
// Documentation: https://docs.elastic.dev/kibana-dev-docs/key-concepts/security-api-authorization
describe('when using authorization', () => {
let productFeaturesService: ProductFeaturesService;
let mockIsActionRegistered: jest.Mock;

beforeEach(() => {
const experimentalFeatures = {} as ExperimentalFeatures;
productFeaturesService = new ProductFeaturesService(
loggerMock.create(),
experimentalFeatures
);
productFeaturesService.registerApiAccessControl(mockHttpSetup);
mockIsActionRegistered = MockedProductFeatures.mock.instances[0]
.isActionRegistered as jest.Mock;
});

describe('when using access tag', () => {
const getReq = (tags: string[] = []) =>
({
route: { options: { tags } },
url: { pathname: '', search: '' },
} as unknown as KibanaRequest);

it('should authorize when no tag matches', async () => {
await lastRegisteredFn(
getReq(['access:something', 'access:securitySolution']),
res,
toolkit
);

expect(mockIsActionRegistered).not.toHaveBeenCalled();
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should check when tag matches and return not found when not action registered', async () => {
mockIsActionRegistered.mockReturnValueOnce(false);
await lastRegisteredFn(getReq(['access:securitySolution-foo']), res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-foo');
expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

it('should check when tag matches and continue when action registered', async () => {
mockIsActionRegistered.mockReturnValueOnce(true);
await lastRegisteredFn(getReq(['access:securitySolution-foo']), res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-foo');
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});
});

describe('when using security authz', () => {
beforeEach(() => {
mockIsActionRegistered.mockImplementation((action: string) => action.includes('enabled'));
});

const getReq = (requiredPrivileges?: AuthzEnabled['requiredPrivileges']) =>
({
route: { options: { security: { authz: { requiredPrivileges } } } },
url: { pathname: '', search: '' },
} as unknown as KibanaRequest);

it('should authorize when no privilege matches', async () => {
await lastRegisteredFn(getReq(['something', 'securitySolution']), res, toolkit);

expect(mockIsActionRegistered).not.toHaveBeenCalled();
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should check when privilege matches and return not found when not action registered', async () => {
await lastRegisteredFn(getReq(['securitySolution-disabled']), res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled');
expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

it('should check when privilege matches and continue when action registered', async () => {
mockIsActionRegistered.mockReturnValueOnce(true);
await lastRegisteredFn(getReq(['securitySolution-enabled']), res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled');
expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should restrict access when one action is not registered', async () => {
mockIsActionRegistered.mockReturnValueOnce(true);
await lastRegisteredFn(
getReq([
'securitySolution-enabled',
'securitySolution-disabled',
'securitySolution-enabled2',
]),
res,
toolkit
);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(2);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled');
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled');

expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

describe('when using nested requiredPrivileges', () => {
describe('when using allRequired', () => {
it('should allow access when all actions are registered', async () => {
const req = getReq([
{
allRequired: [
'securitySolution-enabled',
'securitySolution-enabled2',
'securitySolution-enabled3',
],
},
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(3);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled');
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled2');
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled3');

expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should restrict access if one action is not registered', async () => {
const req = getReq([
{
allRequired: [
'securitySolution-enabled',
'securitySolution-disabled',
'securitySolution-notCalled',
],
},
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(2);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled');
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled');

expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

it('should allow only based on security privileges and ignore non-security', async () => {
const req = getReq([
{ allRequired: ['notSecurityPrivilege', 'securitySolution-enabled'] },
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(1);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled');

expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should restrict only based on security privileges and ignore non-security', async () => {
const req = getReq([
{ allRequired: ['notSecurityPrivilege', 'securitySolution-disabled'] },
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(1);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled');

expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});
});

describe('when using anyRequired', () => {
it('should allow access when one action is registered', async () => {
const req = getReq([
{
anyRequired: [
'securitySolution-disabled',
'securitySolution-enabled',
'securitySolution-notCalled',
],
},
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(2);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled');
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-enabled');

expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});

it('should restrict access when no action is registered', async () => {
const req = getReq([
{
anyRequired: ['securitySolution-disabled', 'securitySolution-disabled2'],
},
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).toHaveBeenCalledTimes(2);
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled');
expect(mockIsActionRegistered).toHaveBeenCalledWith('api:securitySolution-disabled2');

expect(res.notFound).toHaveBeenCalledTimes(1);
expect(toolkit.next).not.toHaveBeenCalled();
});

it('should restrict only based on security privileges and allow when non-security privilege is present', async () => {
const req = getReq([
{
anyRequired: ['notSecurityPrivilege', 'securitySolution-disabled'],
},
]);
await lastRegisteredFn(req, res, toolkit);

expect(mockIsActionRegistered).not.toHaveBeenCalled();

expect(res.notFound).not.toHaveBeenCalled();
expect(toolkit.next).toHaveBeenCalledTimes(1);
});
});
});
});
});
});
});
Loading