[7.x] Register privileges in Kibana Platform Security plugin and remove legacy getUser API. (#68344)

# Conflicts:
#	x-pack/README.md
#	x-pack/plugins/uptime/README.md
#	x-pack/scripts/functional_tests.js
This commit is contained in:
Aleh Zasypkin 2020-06-05 14:12:00 +02:00 committed by GitHub
parent 1cf77f7ca9
commit 2890d5b1fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 721 additions and 413 deletions

View file

@ -4,7 +4,6 @@ files:
- 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss'
- 'src/plugins/vis_type_xy/**/*.s+(a|c)ss'
- 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss'
- 'x-pack/plugins/canvas/**/*.s+(a|c)ss'
- 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss'
- 'x-pack/plugins/lens/**/*.s+(a|c)ss'
@ -12,6 +11,7 @@ files:
- 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss'
- 'x-pack/plugins/maps/**/*.s+(a|c)ss'
- 'x-pack/plugins/spaces/**/*.s+(a|c)ss'
- 'x-pack/plugins/security/**/*.s+(a|c)ss'
ignore:
- 'x-pack/plugins/canvas/shareable_runtime/**/*.s+(a|c)ss'
rules:

View file

@ -39,7 +39,7 @@
"xpack.reporting": ["plugins/reporting"],
"xpack.rollupJobs": ["legacy/plugins/rollup", "plugins/rollup"],
"xpack.searchProfiler": "plugins/searchprofiler",
"xpack.security": ["legacy/plugins/security", "plugins/security"],
"xpack.security": "plugins/security",
"xpack.server": "legacy/server",
"xpack.securitySolution": "plugins/security_solution",
"xpack.snapshotRestore": "plugins/snapshot_restore",

View file

@ -25,8 +25,8 @@ Examples:
- Run the jest test case whose description matches 'filtering should skip values of null':
`cd x-pack && yarn test:jest -t 'filtering should skip values of null' plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js`
- Run the x-pack api integration test case whose description matches the given string:
`node scripts/functional_tests_server --config x-pack/test/api_integration/config.js`
`node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'`
`node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts`
`node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='apis Monitoring Beats list with restarted beat instance should load multiple clusters'`
In addition to to providing a regular expression argument, specific tests can also be run by appeding `.only` to an `it` or `describe` function block. E.g. `describe(` to `describe.only(`.
@ -61,7 +61,7 @@ yarn test:mocha
#### Running functional tests
The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.js)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.js)).
The functional UI tests, the API integration tests, and the SAML API integration tests are all run against a live browser, Kibana, and Elasticsearch install. Each set of tests is specified with a unique config that describes how to start the Elasticsearch server, the Kibana server, and what tests to run against them. The sets of tests that exist today are *functional UI tests* ([specified by this config](test/functional/config.js)), *API integration tests* ([specified by this config](test/api_integration/config.ts)), and *SAML API integration tests* ([specified by this config](test/saml_api_integration/config.ts)).
The script runs all sets of tests sequentially like so:
* builds Elasticsearch and X-Pack

View file

@ -15,7 +15,7 @@ In one shell, from **~/kibana/x-pack**:
`node scripts/functional_tests-server.js`
In another shell, from **~kibana/x-pack**:
`node ../scripts/functional_test_runner.js --config test/api_integration/config.js`.
`node ../scripts/functional_test_runner.js --config test/api_integration/config.ts`.
### Manual e2e testing

View file

@ -8,6 +8,7 @@
import { Lifecycle, ResponseToolkit } from 'hapi';
import * as t from 'io-ts';
import { SecurityPluginSetup } from '../../../../../../../plugins/security/server';
import { LicenseType } from '../../../../common/constants/security';
export const internalAuthData = Symbol('internalAuthData');
@ -39,6 +40,11 @@ export interface BackendFrameworkAdapter {
}
export interface KibanaLegacyServer {
newPlatform: {
setup: {
plugins: { security: SecurityPluginSetup };
};
};
plugins: {
xpack_main: {
status: {
@ -53,9 +59,6 @@ export interface KibanaLegacyServer {
};
};
};
security: {
getUser: (request: KibanaServerRequest) => any;
};
elasticsearch: {
status: {
on: (status: 'green' | 'yellow' | 'red', callback: () => void) => void;

View file

@ -8,6 +8,7 @@ import { ResponseToolkit } from 'hapi';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { get } from 'lodash';
import { isLeft } from 'fp-ts/lib/Either';
import { KibanaRequest, LegacyRequest } from '../../../../../../../../src/core/server';
// @ts-ignore
import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status';
import {
@ -128,13 +129,10 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter {
}
private async getUser(request: KibanaServerRequest): Promise<KibanaUser | null> {
let user;
try {
user = await this.server.plugins.security.getUser(request);
} catch (e) {
return null;
}
if (user === null) {
const user = this.server.newPlatform.setup.plugins.security?.authc.getCurrentUser(
KibanaRequest.from((request as unknown) as LegacyRequest)
);
if (!user) {
return null;
}
const assertKibanaUser = RuntimeKibanaUser.decode(user);

View file

@ -6,64 +6,17 @@
import { Root } from 'joi';
import { resolve } from 'path';
import { Server } from 'src/legacy/server/kbn_server';
import { KibanaRequest, LegacyRequest } from '../../../../src/core/server';
// @ts-ignore
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
import { AuthenticatedUser, SecurityPluginSetup } from '../../../plugins/security/server';
/**
* Public interface of the security plugin.
*/
export interface SecurityPlugin {
getUser: (request: LegacyRequest) => Promise<AuthenticatedUser>;
}
function getSecurityPluginSetup(server: Server) {
const securityPlugin = server.newPlatform.setup.plugins.security as SecurityPluginSetup;
if (!securityPlugin) {
throw new Error('Kibana Platform Security plugin is not available.');
}
return securityPlugin;
}
export const security = (kibana: Record<string, any>) =>
new kibana.Plugin({
id: 'security',
publicDir: resolve(__dirname, 'public'),
require: ['kibana', 'xpack_main'],
require: ['kibana'],
configPrefix: 'xpack.security',
uiExports: {
hacks: ['plugins/security/hacks/legacy'],
injectDefaultVars: (server: Server) => {
return { enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled') };
},
},
config(Joi: Root) {
return Joi.object({
enabled: Joi.boolean().default(true),
})
uiExports: { hacks: ['plugins/security/hacks/legacy'] },
config: (Joi: Root) =>
Joi.object({ enabled: Joi.boolean().default(true) })
.unknown()
.default();
},
async postInit(server: Server) {
watchStatusAndLicenseToInitialize(server.plugins.xpack_main, this, async () => {
const xpackInfo = server.plugins.xpack_main.info;
if (xpackInfo.isAvailable() && xpackInfo.feature('security').isEnabled()) {
await getSecurityPluginSetup(server).__legacyCompat.registerPrivilegesWithCluster();
}
});
},
async init(server: Server) {
const securityPlugin = getSecurityPluginSetup(server);
server.expose({
getUser: async (request: LegacyRequest) =>
securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)),
});
},
.default(),
init() {},
});

View file

@ -83,13 +83,13 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme)
**Start server**
```
node scripts/functional_tests_server --config x-pack/test/api_integration/config.js
node scripts/functional_tests_server --config x-pack/test/api_integration/config.ts
```
**Run tests**
```
node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs'
node scripts/functional_test_runner --config x-pack/test/api_integration/config.ts --grep='APM specs'
```
APM tests are located in `x-pack/test/api_integration/apis/apm`.

View file

@ -52,12 +52,12 @@ This plugin follows the `common`, `server`, `public` structure from the [Archite
1. In one terminal, change to the `x-pack` directory and start the test server with
```
node scripts/functional_tests_server.js --config test/api_integration/config.js
node scripts/functional_tests_server.js --config test/api_integration/config.ts
```
1. in a second terminal, run the tests from the Kibana root directory with
```
node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.js
node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.ts
```
#### EPM

View file

@ -11,4 +11,4 @@ Run all tests from the `x-pack` root directory
- You may want to comment out all imports except for Lens in the config file.
- API Functional tests:
- Run `node scripts/functional_tests_server`
- Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.js --grep=Lens`
- Run `node ../scripts/functional_test_runner.js --config ./test/api_integration/config.ts --grep=Lens`

View file

@ -5,7 +5,12 @@
*/
import { i18n } from '@kbn/i18n';
import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public';
import {
ApplicationSetup,
AppMountParameters,
AppNavLinkStatus,
StartServicesAccessor,
} from '../../../../../src/core/public';
import { AuthenticationServiceSetup } from '../authentication';
interface CreateDeps {
@ -23,8 +28,7 @@ export const accountManagementApp = Object.freeze({
application.register({
id: this.id,
title,
// TODO: switch to proper enum once https://github.com/elastic/kibana/issues/58327 is resolved.
navLinkStatus: 3,
navLinkStatus: AppNavLinkStatus.hidden,
appRoute: '/security/account',
async mount({ element }: AppMountParameters) {
const [

View file

@ -23,9 +23,10 @@
}
&:focus {
@include euiFocusRing;
border-color: transparent;
border-radius: $euiBorderRadius;
@include euiFocusRing;
.secLoginCard__title {
text-decoration: underline;

View file

@ -11,13 +11,15 @@ import { Feature } from '../../../../../features/public';
import { KibanaPrivileges } from '../model';
import { SecurityLicenseFeatures } from '../../..';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { featuresPluginMock } from '../../../../../features/server/mocks';
export const createRawKibanaPrivileges = (
features: Feature[],
{ allowSubFeaturePrivileges = true } = {}
) => {
const featuresService = {
getFeatures: () => features,
};
const featuresService = featuresPluginMock.createSetup();
featuresService.getFeatures.mockReturnValue(features);
const licensingService = {
getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures),

View file

@ -163,7 +163,12 @@ function getProps({
const { http, docLinks, notifications } = coreMock.createStart();
http.get.mockImplementation(async (path: any) => {
if (path === '/api/spaces/space') {
return buildSpaces();
if (spacesEnabled) {
return buildSpaces();
}
const notFoundError = { response: { status: 404 } };
throw notFoundError;
}
});
@ -181,7 +186,6 @@ function getProps({
notifications,
docLinks: new DocumentationLinksService(docLinks),
fatalErrors,
spacesEnabled,
uiCapabilities: buildUICapabilities(canManageSpaces),
history: (scopedHistoryMock.create() as unknown) as ScopedHistory,
};

View file

@ -80,7 +80,6 @@ interface Props {
docLinks: DocumentationLinksService;
http: HttpStart;
license: SecurityLicense;
spacesEnabled: boolean;
uiCapabilities: Capabilities;
notifications: NotificationsStart;
fatalErrors: FatalErrorsSetup;
@ -225,14 +224,21 @@ function useRole(
return [role, setRole] as [Role | null, typeof setRole];
}
function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) {
const [spaces, setSpaces] = useState<Space[] | null>(null);
function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup) {
const [spaces, setSpaces] = useState<{ enabled: boolean; list: Space[] } | null>(null);
useEffect(() => {
(spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then(
(fetchedSpaces) => setSpaces(fetchedSpaces),
(err) => fatalErrors.add(err)
http.get('/api/spaces/space').then(
(fetchedSpaces) => setSpaces({ enabled: true, list: fetchedSpaces }),
(err: IHttpFetchError) => {
// Spaces plugin can be disabled and hence this endpoint can be unavailable.
if (err.response?.status === 404) {
setSpaces({ enabled: false, list: [] });
} else {
fatalErrors.add(err);
}
}
);
}, [http, fatalErrors, spacesEnabled]);
}, [http, fatalErrors]);
return spaces;
}
@ -278,7 +284,6 @@ export const EditRolePage: FunctionComponent<Props> = ({
roleName,
action,
fatalErrors,
spacesEnabled,
license,
docLinks,
uiCapabilities,
@ -295,7 +300,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors);
const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications);
const privileges = usePrivileges(privilegesAPIClient, fatalErrors);
const spaces = useSpaces(http, fatalErrors, spacesEnabled);
const spaces = useSpaces(http, fatalErrors);
const features = useFeatures(getFeatures, fatalErrors);
const [role, setRole] = useRole(
rolesAPIClient,
@ -434,8 +439,8 @@ export const EditRolePage: FunctionComponent<Props> = ({
<EuiSpacer />
<KibanaPrivilegesRegion
kibanaPrivileges={new KibanaPrivileges(kibanaPrivileges, features)}
spaces={spaces}
spacesEnabled={spacesEnabled}
spaces={spaces.list}
spacesEnabled={spaces.enabled}
uiCapabilities={uiCapabilities}
canCustomizeSubFeaturePrivileges={license.getFeatures().allowSubFeaturePrivileges}
editable={!isRoleReadOnly}
@ -519,7 +524,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
setFormError(null);
try {
await rolesAPIClient.saveRole({ role, spacesEnabled });
await rolesAPIClient.saveRole({ role, spacesEnabled: spaces.enabled });
} catch (error) {
notifications.toasts.addDanger(get(error, 'data.message'));
return;
@ -554,7 +559,7 @@ export const EditRolePage: FunctionComponent<Props> = ({
backToRoleList();
};
const description = spacesEnabled ? (
const description = spaces.enabled ? (
<FormattedMessage
id="xpack.security.management.editRole.setPrivilegesToKibanaSpacesDescription"
defaultMessage="Set privileges on your Elasticsearch data and control access to your Kibana spaces."

View file

@ -36,10 +36,7 @@ export const rolesManagementApp = Object.freeze({
];
const [
[
{ application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications },
{ data, features },
],
[{ application, docLinks, http, i18n: i18nStart, notifications }, { data, features }],
{ RolesGridPage },
{ EditRolePage },
{ RolesAPIClient },
@ -86,9 +83,6 @@ export const rolesManagementApp = Object.freeze({
<EditRolePage
action={action}
roleName={roleName}
spacesEnabled={
injectedMetadata.getInjectedVar('enableSpaceAwarePrivileges') as boolean
}
rolesAPIClient={rolesAPIClient}
userAPIClient={new UserAPIClient(http)}
indicesAPIClient={new IndicesAPIClient(http)}

View file

@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, Logger } from '../../../../../src/core/server';
import { Authorization } from '.';
import { HttpServiceSetup, Logger } from '../../../../../src/core/server';
import { AuthorizationServiceSetup } from '.';
export function initAPIAuthorization(
http: CoreSetup['http'],
{ actions, checkPrivilegesDynamicallyWithRequest, mode }: Authorization,
http: HttpServiceSetup,
{ actions, checkPrivilegesDynamicallyWithRequest, mode }: AuthorizationServiceSetup,
logger: Logger
) {
http.registerOnPostAuth(async (request, response, toolkit) => {

View file

@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, Logger } from '../../../../../src/core/server';
import { FeaturesService } from '../plugin';
import { Authorization } from '.';
import { HttpServiceSetup, Logger } from '../../../../../src/core/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../../features/server';
import { AuthorizationServiceSetup } from '.';
class ProtectedApplications {
private applications: Set<string> | null = null;
constructor(private readonly featuresService: FeaturesService) {}
constructor(private readonly featuresService: FeaturesPluginSetup) {}
public shouldProtect(appId: string) {
// Currently, once we get the list of features we essentially "lock" additional
@ -30,14 +30,14 @@ class ProtectedApplications {
}
export function initAppAuthorization(
http: CoreSetup['http'],
http: HttpServiceSetup,
{
actions,
checkPrivilegesDynamicallyWithRequest,
mode,
}: Pick<Authorization, 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'mode'>,
}: Pick<AuthorizationServiceSetup, 'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'mode'>,
logger: Logger,
featuresService: FeaturesService
featuresService: FeaturesPluginSetup
) {
const protectedApplications = new ProtectedApplications(featuresService);

View file

@ -0,0 +1,263 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
mockAuthorizationModeFactory,
mockCheckPrivilegesDynamicallyWithRequestFactory,
mockCheckPrivilegesWithRequestFactory,
mockCheckSavedObjectsPrivilegesWithRequestFactory,
mockPrivilegesFactory,
mockRegisterPrivilegesWithCluster,
} from './service.test.mocks';
import { BehaviorSubject } from 'rxjs';
import { CoreStatus, ServiceStatusLevels } from '../../../../../src/core/server';
import { checkPrivilegesWithRequestFactory } from './check_privileges';
import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically';
import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges';
import { authorizationModeFactory } from './mode';
import { privilegesFactory } from './privileges';
import { AuthorizationService } from '.';
import {
coreMock,
elasticsearchServiceMock,
loggingServiceMock,
} from '../../../../../src/core/server/mocks';
import { featuresPluginMock } from '../../../features/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
import { SecurityLicense, SecurityLicenseFeatures } from '../../common/licensing';
import { nextTick } from 'test_utils/enzyme_helpers';
const kibanaIndexName = '.a-kibana-index';
const application = `kibana-${kibanaIndexName}`;
const mockCheckPrivilegesWithRequest = Symbol();
const mockCheckPrivilegesDynamicallyWithRequest = Symbol();
const mockCheckSavedObjectsPrivilegesWithRequest = Symbol();
const mockPrivilegesService = Symbol();
const mockAuthorizationMode = Symbol();
beforeEach(() => {
mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest);
mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue(
mockCheckPrivilegesDynamicallyWithRequest
);
mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue(
mockCheckSavedObjectsPrivilegesWithRequest
);
mockPrivilegesFactory.mockReturnValue(mockPrivilegesService);
mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode);
});
afterEach(() => {
mockRegisterPrivilegesWithCluster.mockClear();
});
it(`#setup returns exposed services`, () => {
const mockClusterClient = elasticsearchServiceMock.createClusterClient();
const mockGetSpacesService = jest
.fn()
.mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() });
const mockFeaturesSetup = featuresPluginMock.createSetup();
const mockLicense = licenseMock.create();
const mockCoreSetup = coreMock.createSetup();
const authorizationService = new AuthorizationService();
const authz = authorizationService.setup({
http: mockCoreSetup.http,
capabilities: mockCoreSetup.capabilities,
status: mockCoreSetup.status,
clusterClient: mockClusterClient,
license: mockLicense,
loggers: loggingServiceMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
features: mockFeaturesSetup,
getSpacesService: mockGetSpacesService,
});
expect(authz.actions.version).toBe('version:some-version');
expect(authz.applicationName).toBe(application);
expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest);
expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith(
authz.actions,
mockClusterClient,
authz.applicationName
);
expect(authz.checkPrivilegesDynamicallyWithRequest).toBe(
mockCheckPrivilegesDynamicallyWithRequest
);
expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith(
mockCheckPrivilegesWithRequest,
mockGetSpacesService
);
expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe(
mockCheckSavedObjectsPrivilegesWithRequest
);
expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith(
mockCheckPrivilegesWithRequest,
mockGetSpacesService
);
expect(authz.privileges).toBe(mockPrivilegesService);
expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesSetup, mockLicense);
expect(authz.mode).toBe(mockAuthorizationMode);
expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense);
expect(mockCoreSetup.capabilities.registerSwitcher).toHaveBeenCalledTimes(1);
expect(mockCoreSetup.capabilities.registerSwitcher).toHaveBeenCalledWith(expect.any(Function));
});
describe('#start', () => {
let statusSubject: BehaviorSubject<CoreStatus>;
let licenseSubject: BehaviorSubject<SecurityLicenseFeatures>;
let mockLicense: jest.Mocked<SecurityLicense>;
beforeEach(() => {
const mockClusterClient = elasticsearchServiceMock.createClusterClient();
licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures);
mockLicense = licenseMock.create();
mockLicense.isEnabled.mockReturnValue(false);
mockLicense.features$ = licenseSubject;
statusSubject = new BehaviorSubject<CoreStatus>({
elasticsearch: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' },
savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' },
});
const mockCoreSetup = coreMock.createSetup();
mockCoreSetup.status.core$ = statusSubject;
const authorizationService = new AuthorizationService();
authorizationService.setup({
http: mockCoreSetup.http,
capabilities: mockCoreSetup.capabilities,
status: mockCoreSetup.status,
clusterClient: mockClusterClient,
license: mockLicense,
loggers: loggingServiceMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
features: featuresPluginMock.createSetup(),
getSpacesService: jest
.fn()
.mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }),
});
const featuresStart = featuresPluginMock.createStart();
featuresStart.getFeatures.mockReturnValue([]);
authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart });
// ES and license aren't available yet.
expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled();
});
it('registers cluster privileges', async () => {
// ES is available now, but not license.
statusSubject.next({
elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' },
savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' },
});
expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled();
// Both ES and license are available now.
mockLicense.isEnabled.mockReturnValue(true);
licenseSubject.next(({} as unknown) as SecurityLicenseFeatures);
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1);
await nextTick();
// New changes still trigger privileges re-registration.
licenseSubject.next(({} as unknown) as SecurityLicenseFeatures);
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2);
});
it('schedules retries if fails to register cluster privileges', async () => {
jest.useFakeTimers();
mockRegisterPrivilegesWithCluster.mockRejectedValue(new Error('Some error'));
// Both ES and license are available.
mockLicense.isEnabled.mockReturnValue(true);
statusSubject.next({
elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' },
savedObjects: { level: ServiceStatusLevels.unavailable, summary: 'Service is NOT working' },
});
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1);
// Next retry isn't performed immediately, retry happens only after a timeout.
await nextTick();
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(100);
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2);
// Delay between consequent retries is increasing.
await nextTick();
jest.advanceTimersByTime(100);
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(2);
await nextTick();
jest.advanceTimersByTime(100);
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(3);
// When call finally succeeds retries aren't scheduled anymore.
mockRegisterPrivilegesWithCluster.mockResolvedValue(undefined);
await nextTick();
jest.runAllTimers();
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(4);
await nextTick();
jest.runAllTimers();
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(4);
// New changes still trigger privileges re-registration.
licenseSubject.next(({} as unknown) as SecurityLicenseFeatures);
expect(mockRegisterPrivilegesWithCluster).toHaveBeenCalledTimes(5);
});
});
it('#stop unsubscribes from license and ES updates.', () => {
const mockClusterClient = elasticsearchServiceMock.createClusterClient();
const licenseSubject = new BehaviorSubject(({} as unknown) as SecurityLicenseFeatures);
const mockLicense = licenseMock.create();
mockLicense.isEnabled.mockReturnValue(false);
mockLicense.features$ = licenseSubject;
const mockCoreSetup = coreMock.createSetup();
mockCoreSetup.status.core$ = new BehaviorSubject<CoreStatus>({
elasticsearch: { level: ServiceStatusLevels.available, summary: 'Service is working' },
savedObjects: { level: ServiceStatusLevels.available, summary: 'Service is working' },
});
const authorizationService = new AuthorizationService();
authorizationService.setup({
http: mockCoreSetup.http,
capabilities: mockCoreSetup.capabilities,
status: mockCoreSetup.status,
clusterClient: mockClusterClient,
license: mockLicense,
loggers: loggingServiceMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
features: featuresPluginMock.createSetup(),
getSpacesService: jest
.fn()
.mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }),
});
const featuresStart = featuresPluginMock.createStart();
featuresStart.getFeatures.mockReturnValue([]);
authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart });
authorizationService.stop();
// After stop we don't register privileges even if all requirements are met.
mockLicense.isEnabled.mockReturnValue(true);
licenseSubject.next(({} as unknown) as SecurityLicenseFeatures);
expect(mockRegisterPrivilegesWithCluster).not.toHaveBeenCalled();
});

View file

@ -0,0 +1,221 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { combineLatest, BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, filter } from 'rxjs/operators';
import { UICapabilities } from 'ui/capabilities';
import {
LoggerFactory,
KibanaRequest,
IClusterClient,
ServiceStatusLevels,
Logger,
StatusServiceSetup,
HttpServiceSetup,
CapabilitiesSetup,
} from '../../../../../src/core/server';
import {
PluginSetupContract as FeaturesPluginSetup,
PluginStartContract as FeaturesPluginStart,
} from '../../../features/server';
import { SpacesService } from '../plugin';
import { Actions } from './actions';
import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges';
import {
CheckPrivilegesDynamicallyWithRequest,
checkPrivilegesDynamicallyWithRequestFactory,
} from './check_privileges_dynamically';
import {
CheckSavedObjectsPrivilegesWithRequest,
checkSavedObjectsPrivilegesWithRequestFactory,
} from './check_saved_objects_privileges';
import { AuthorizationMode, authorizationModeFactory } from './mode';
import { privilegesFactory, PrivilegesService } from './privileges';
import { initAppAuthorization } from './app_authorization';
import { initAPIAuthorization } from './api_authorization';
import { disableUICapabilitiesFactory } from './disable_ui_capabilities';
import { validateFeaturePrivileges } from './validate_feature_privileges';
import { validateReservedPrivileges } from './validate_reserved_privileges';
import { registerPrivilegesWithCluster } from './register_privileges_with_cluster';
import { APPLICATION_PREFIX } from '../../common/constants';
import { SecurityLicense } from '../../common/licensing';
export { Actions } from './actions';
export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges';
export { featurePrivilegeIterator } from './privileges';
interface AuthorizationServiceSetupParams {
packageVersion: string;
http: HttpServiceSetup;
status: StatusServiceSetup;
capabilities: CapabilitiesSetup;
clusterClient: IClusterClient;
license: SecurityLicense;
loggers: LoggerFactory;
features: FeaturesPluginSetup;
kibanaIndexName: string;
getSpacesService(): SpacesService | undefined;
}
interface AuthorizationServiceStartParams {
features: FeaturesPluginStart;
clusterClient: IClusterClient;
}
export interface AuthorizationServiceSetup {
actions: Actions;
checkPrivilegesWithRequest: CheckPrivilegesWithRequest;
checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest;
checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest;
applicationName: string;
mode: AuthorizationMode;
privileges: PrivilegesService;
}
export class AuthorizationService {
private logger!: Logger;
private license!: SecurityLicense;
private status!: StatusServiceSetup;
private applicationName!: string;
private privileges!: PrivilegesService;
private statusSubscription?: Subscription;
setup({
http,
capabilities,
status,
packageVersion,
clusterClient,
license,
loggers,
features,
kibanaIndexName,
getSpacesService,
}: AuthorizationServiceSetupParams): AuthorizationServiceSetup {
this.logger = loggers.get('authorization');
this.license = license;
this.status = status;
this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`;
const mode = authorizationModeFactory(license);
const actions = new Actions(packageVersion);
this.privileges = privilegesFactory(actions, features, license);
const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(
actions,
clusterClient,
this.applicationName
);
const authz = {
actions,
applicationName: this.applicationName,
mode,
privileges: this.privileges,
checkPrivilegesWithRequest,
checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory(
checkPrivilegesWithRequest,
getSpacesService
),
checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory(
checkPrivilegesWithRequest,
getSpacesService
),
};
capabilities.registerSwitcher(
async (request: KibanaRequest, uiCapabilities: UICapabilities) => {
// If we have a license which doesn't enable security, or we're a legacy user we shouldn't
// disable any ui capabilities
if (!mode.useRbacForRequest(request)) {
return uiCapabilities;
}
const disableUICapabilities = disableUICapabilitiesFactory(
request,
features.getFeatures(),
this.logger,
authz
);
if (!request.auth.isAuthenticated) {
return disableUICapabilities.all(uiCapabilities);
}
return await disableUICapabilities.usingPrivileges(uiCapabilities);
}
);
initAPIAuthorization(http, authz, loggers.get('api-authorization'));
initAppAuthorization(http, authz, loggers.get('app-authorization'), features);
return authz;
}
start({ clusterClient, features }: AuthorizationServiceStartParams) {
const allFeatures = features.getFeatures();
validateFeaturePrivileges(allFeatures);
validateReservedPrivileges(allFeatures);
this.registerPrivileges(clusterClient);
}
stop() {
if (this.statusSubscription !== undefined) {
this.statusSubscription.unsubscribe();
this.statusSubscription = undefined;
}
}
private registerPrivileges(clusterClient: IClusterClient) {
const RETRY_SCALE_DURATION = 100;
const RETRY_TIMEOUT_MAX = 10000;
const retries$ = new BehaviorSubject(0);
let retryTimeout: NodeJS.Timeout;
// Register cluster privileges once Elasticsearch is available and Security plugin is enabled.
this.statusSubscription = combineLatest([
this.status.core$,
this.license.features$,
retries$.asObservable().pipe(
// We shouldn't emit new value if retry counter is reset. This comparator isn't called for
// the initial value.
distinctUntilChanged((prev, curr) => prev === curr || curr === 0)
),
])
.pipe(
filter(
([status]) =>
this.license.isEnabled() && status.elasticsearch.level === ServiceStatusLevels.available
)
)
.subscribe(async () => {
// If status or license change occurred before retry timeout we should cancel it.
if (retryTimeout) {
clearTimeout(retryTimeout);
}
try {
await registerPrivilegesWithCluster(
this.logger,
this.privileges,
this.applicationName,
clusterClient
);
retries$.next(0);
} catch (err) {
const retriesElapsed = retries$.getValue() + 1;
retryTimeout = setTimeout(
() => retries$.next(retriesElapsed),
Math.min(retriesElapsed * RETRY_SCALE_DURATION, RETRY_TIMEOUT_MAX)
);
}
});
}
}

View file

@ -10,13 +10,13 @@ import { KibanaRequest, Logger } from '../../../../../src/core/server';
import { Feature } from '../../../features/server';
import { CheckPrivilegesResponse } from './check_privileges';
import { Authorization } from './index';
import { AuthorizationServiceSetup } from '.';
export function disableUICapabilitiesFactory(
request: KibanaRequest,
features: Feature[],
logger: Logger,
authz: Authorization
authz: AuthorizationServiceSetup
) {
const featureNavLinkIds = features
.map((feature) => feature.navLinkId)

View file

@ -1,100 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
mockAuthorizationModeFactory,
mockCheckPrivilegesDynamicallyWithRequestFactory,
mockCheckPrivilegesWithRequestFactory,
mockCheckSavedObjectsPrivilegesWithRequestFactory,
mockPrivilegesFactory,
} from './service.test.mocks';
import { checkPrivilegesWithRequestFactory } from './check_privileges';
import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically';
import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges';
import { authorizationModeFactory } from './mode';
import { privilegesFactory } from './privileges';
import { setupAuthorization } from '.';
import {
coreMock,
elasticsearchServiceMock,
loggingServiceMock,
} from '../../../../../src/core/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
test(`returns exposed services`, () => {
const kibanaIndexName = '.a-kibana-index';
const application = `kibana-${kibanaIndexName}`;
const mockCheckPrivilegesWithRequest = Symbol();
mockCheckPrivilegesWithRequestFactory.mockReturnValue(mockCheckPrivilegesWithRequest);
const mockCheckPrivilegesDynamicallyWithRequest = Symbol();
mockCheckPrivilegesDynamicallyWithRequestFactory.mockReturnValue(
mockCheckPrivilegesDynamicallyWithRequest
);
const mockCheckSavedObjectsPrivilegesWithRequest = Symbol();
mockCheckSavedObjectsPrivilegesWithRequestFactory.mockReturnValue(
mockCheckSavedObjectsPrivilegesWithRequest
);
const mockPrivilegesService = Symbol();
mockPrivilegesFactory.mockReturnValue(mockPrivilegesService);
const mockAuthorizationMode = Symbol();
mockAuthorizationModeFactory.mockReturnValue(mockAuthorizationMode);
const mockClusterClient = elasticsearchServiceMock.createClusterClient();
const mockGetSpacesService = jest
.fn()
.mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() });
const mockFeaturesService = { getFeatures: () => [] };
const mockLicense = licenseMock.create();
const authz = setupAuthorization({
http: coreMock.createSetup().http,
clusterClient: mockClusterClient,
license: mockLicense,
loggers: loggingServiceMock.create(),
kibanaIndexName,
packageVersion: 'some-version',
featuresService: mockFeaturesService,
getSpacesService: mockGetSpacesService,
});
expect(authz.actions.version).toBe('version:some-version');
expect(authz.applicationName).toBe(application);
expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest);
expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith(
authz.actions,
mockClusterClient,
authz.applicationName
);
expect(authz.checkPrivilegesDynamicallyWithRequest).toBe(
mockCheckPrivilegesDynamicallyWithRequest
);
expect(checkPrivilegesDynamicallyWithRequestFactory).toHaveBeenCalledWith(
mockCheckPrivilegesWithRequest,
mockGetSpacesService
);
expect(authz.checkSavedObjectsPrivilegesWithRequest).toBe(
mockCheckSavedObjectsPrivilegesWithRequest
);
expect(checkSavedObjectsPrivilegesWithRequestFactory).toHaveBeenCalledWith(
mockCheckPrivilegesWithRequest,
mockGetSpacesService
);
expect(authz.privileges).toBe(mockPrivilegesService);
expect(privilegesFactory).toHaveBeenCalledWith(authz.actions, mockFeaturesService, mockLicense);
expect(authz.mode).toBe(mockAuthorizationMode);
expect(authorizationModeFactory).toHaveBeenCalledWith(mockLicense);
});

View file

@ -4,134 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { UICapabilities } from 'ui/capabilities';
import {
CoreSetup,
LoggerFactory,
KibanaRequest,
IClusterClient,
} from '../../../../../src/core/server';
import { FeaturesService, SpacesService } from '../plugin';
import { Actions } from './actions';
import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges';
import {
CheckPrivilegesDynamicallyWithRequest,
checkPrivilegesDynamicallyWithRequestFactory,
} from './check_privileges_dynamically';
import {
CheckSavedObjectsPrivilegesWithRequest,
checkSavedObjectsPrivilegesWithRequestFactory,
} from './check_saved_objects_privileges';
import { AuthorizationMode, authorizationModeFactory } from './mode';
import { privilegesFactory, PrivilegesService } from './privileges';
import { initAppAuthorization } from './app_authorization';
import { initAPIAuthorization } from './api_authorization';
import { disableUICapabilitiesFactory } from './disable_ui_capabilities';
import { validateFeaturePrivileges } from './validate_feature_privileges';
import { validateReservedPrivileges } from './validate_reserved_privileges';
import { registerPrivilegesWithCluster } from './register_privileges_with_cluster';
import { APPLICATION_PREFIX } from '../../common/constants';
import { SecurityLicense } from '../../common/licensing';
export { Actions } from './actions';
export { AuthorizationService, AuthorizationServiceSetup } from './authorization_service';
export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges';
export { featurePrivilegeIterator } from './privileges';
interface SetupAuthorizationParams {
packageVersion: string;
http: CoreSetup['http'];
clusterClient: IClusterClient;
license: SecurityLicense;
loggers: LoggerFactory;
featuresService: FeaturesService;
kibanaIndexName: string;
getSpacesService(): SpacesService | undefined;
}
export interface Authorization {
actions: Actions;
checkPrivilegesWithRequest: CheckPrivilegesWithRequest;
checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest;
checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest;
applicationName: string;
mode: AuthorizationMode;
privileges: PrivilegesService;
disableUnauthorizedCapabilities: (
request: KibanaRequest,
capabilities: UICapabilities
) => Promise<UICapabilities>;
registerPrivilegesWithCluster: () => Promise<void>;
}
export function setupAuthorization({
http,
packageVersion,
clusterClient,
license,
loggers,
featuresService,
kibanaIndexName,
getSpacesService,
}: SetupAuthorizationParams): Authorization {
const actions = new Actions(packageVersion);
const mode = authorizationModeFactory(license);
const applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`;
const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory(
actions,
clusterClient,
applicationName
);
const privileges = privilegesFactory(actions, featuresService, license);
const logger = loggers.get('authorization');
const authz = {
actions,
applicationName,
checkPrivilegesWithRequest,
checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory(
checkPrivilegesWithRequest,
getSpacesService
),
checkSavedObjectsPrivilegesWithRequest: checkSavedObjectsPrivilegesWithRequestFactory(
checkPrivilegesWithRequest,
getSpacesService
),
mode,
privileges,
async disableUnauthorizedCapabilities(request: KibanaRequest, capabilities: UICapabilities) {
// If we have a license which doesn't enable security, or we're a legacy user we shouldn't
// disable any ui capabilities
if (!mode.useRbacForRequest(request)) {
return capabilities;
}
const disableUICapabilities = disableUICapabilitiesFactory(
request,
featuresService.getFeatures(),
logger,
authz
);
if (!request.auth.isAuthenticated) {
return disableUICapabilities.all(capabilities);
}
return await disableUICapabilities.usingPrivileges(capabilities);
},
registerPrivilegesWithCluster: async () => {
const features = featuresService.getFeatures();
validateFeaturePrivileges(features);
validateReservedPrivileges(features);
await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient);
},
};
initAPIAuthorization(http, authz, loggers.get('api-authorization'));
initAppAuthorization(http, authz, loggers.get('app-authorization'), featuresService);
return authz;
}

View file

@ -8,6 +8,8 @@ import { Feature } from '../../../../features/server';
import { Actions } from '../actions';
import { privilegesFactory } from './privileges';
import { featuresPluginMock } from '../../../../features/server/mocks';
const actions = new Actions('1.0.0-zeta1');
describe('features', () => {
@ -42,7 +44,9 @@ describe('features', () => {
}),
];
const mockFeaturesService = { getFeatures: jest.fn().mockReturnValue(features) };
const mockFeaturesService = featuresPluginMock.createSetup();
mockFeaturesService.getFeatures.mockReturnValue(features);
const mockLicenseService = {
getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }),
};

View file

@ -6,11 +6,10 @@
import { uniq } from 'lodash';
import { SecurityLicense } from '../../../common/licensing';
import { Feature } from '../../../../features/server';
import { Feature, PluginSetupContract as FeaturesPluginSetup } from '../../../../features/server';
import { RawKibanaPrivileges } from '../../../common/model';
import { Actions } from '../actions';
import { featurePrivilegeBuilderFactory } from './feature_privilege_builder';
import { FeaturesService } from '../../plugin';
import {
featurePrivilegeIterator,
subFeaturePrivilegeIterator,
@ -22,7 +21,7 @@ export interface PrivilegesService {
export function privilegesFactory(
actions: Actions,
featuresService: FeaturesService,
featuresService: FeaturesPluginSetup,
licenseService: Pick<SecurityLicense, 'getFeatures'>
) {
const featurePrivilegeBuilder = featurePrivilegeBuilderFactory(actions);

View file

@ -49,7 +49,7 @@ const registerPrivilegesWithClusterTest = (
});
for (const deletedPrivilege of deletedPrivileges) {
expect(mockLogger.debug).toHaveBeenCalledWith(
`Deleting Kibana Privilege ${deletedPrivilege} from Elasticearch for ${application}`
`Deleting Kibana Privilege ${deletedPrivilege} from Elasticsearch for ${application}`
);
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith(
'shield.deletePrivilege',
@ -82,7 +82,7 @@ const registerPrivilegesWithClusterTest = (
`Registering Kibana Privileges with Elasticsearch for ${application}`
);
expect(mockLogger.debug).toHaveBeenCalledWith(
`Kibana Privileges already registered with Elasticearch for ${application}`
`Kibana Privileges already registered with Elasticsearch for ${application}`
);
};
};

View file

@ -61,14 +61,14 @@ export async function registerPrivilegesWithCluster(
privilege: application,
});
if (arePrivilegesEqual(existingPrivileges, expectedPrivileges)) {
logger.debug(`Kibana Privileges already registered with Elasticearch for ${application}`);
logger.debug(`Kibana Privileges already registered with Elasticsearch for ${application}`);
return;
}
const privilegesToDelete = getPrivilegesToDelete(existingPrivileges, expectedPrivileges);
for (const privilegeToDelete of privilegesToDelete) {
logger.debug(
`Deleting Kibana Privilege ${privilegeToDelete} from Elasticearch for ${application}`
`Deleting Kibana Privilege ${privilegeToDelete} from Elasticsearch for ${application}`
);
try {
await clusterClient.callAsInternalUser('shield.deletePrivilege', {

View file

@ -28,3 +28,8 @@ export const mockAuthorizationModeFactory = jest.fn();
jest.mock('./mode', () => ({
authorizationModeFactory: mockAuthorizationModeFactory,
}));
export const mockRegisterPrivilegesWithCluster = jest.fn();
jest.mock('./register_privileges_with_cluster', () => ({
registerPrivilegesWithCluster: mockRegisterPrivilegesWithCluster,
}));

View file

@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SecurityPluginSetup } from './plugin';
import { authenticationMock } from './authentication/index.mock';
import { authorizationMock } from './authorization/index.mock';
import { licenseMock } from '../common/licensing/index.mock';
@ -23,7 +21,6 @@ function createSetupMock() {
},
registerSpacesService: jest.fn(),
license: licenseMock.create(),
__legacyCompat: {} as SecurityPluginSetup['__legacyCompat'],
};
}

View file

@ -50,9 +50,6 @@ describe('Security Plugin', () => {
it('exposes proper contract', async () => {
await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(`
Object {
"__legacyCompat": Object {
"registerPrivilegesWithCluster": [Function],
},
"audit": Object {
"getLogger": [Function],
},

View file

@ -8,22 +8,22 @@ import { combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { TypeOf } from '@kbn/config-schema';
import {
deepFreeze,
ICustomClusterClient,
CoreSetup,
CoreStart,
Logger,
PluginInitializerContext,
CoreStart,
} from '../../../../src/core/server';
import { deepFreeze } from '../../../../src/core/server';
import { SpacesPluginSetup } from '../../spaces/server';
import {
PluginSetupContract as FeaturesSetupContract,
PluginStartContract as FeaturesStartContract,
PluginSetupContract as FeaturesPluginSetup,
PluginStartContract as FeaturesPluginStart,
} from '../../features/server';
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { Authentication, setupAuthentication } from './authentication';
import { Authorization, setupAuthorization } from './authorization';
import { AuthorizationService, AuthorizationServiceSetup } from './authorization';
import { ConfigSchema, createConfig } from './config';
import { defineRoutes } from './routes';
import { SecurityLicenseService, SecurityLicense } from '../common/licensing';
@ -37,8 +37,6 @@ export type SpacesService = Pick<
'getSpaceId' | 'namespaceToSpaceId'
>;
export type FeaturesService = Pick<FeaturesSetupContract, 'getFeatures'>;
/**
* Describes public Security plugin contract returned at the `setup` stage.
*/
@ -53,7 +51,7 @@ export interface SecurityPluginSetup {
| 'grantAPIKeyAsInternalUser'
| 'invalidateAPIKeyAsInternalUser'
>;
authz: Pick<Authorization, 'actions' | 'checkPrivilegesWithRequest' | 'mode'>;
authz: Pick<AuthorizationServiceSetup, 'actions' | 'checkPrivilegesWithRequest' | 'mode'>;
license: SecurityLicense;
audit: Pick<AuditServiceSetup, 'getLogger'>;
@ -66,19 +64,15 @@ export interface SecurityPluginSetup {
* @param service Spaces service exposed by the Spaces plugin.
*/
registerSpacesService: (service: SpacesService) => void;
__legacyCompat: {
registerPrivilegesWithCluster: () => void;
};
}
export interface PluginSetupDependencies {
features: FeaturesService;
features: FeaturesPluginSetup;
licensing: LicensingPluginSetup;
}
export interface PluginStartDependencies {
features: FeaturesStartContract;
features: FeaturesPluginStart;
licensing: LicensingPluginStart;
}
@ -101,6 +95,7 @@ export class Plugin {
};
private readonly auditService = new AuditService(this.initializerContext.logger.get('audit'));
private readonly authorizationService = new AuthorizationService();
private readonly getSpacesService = () => {
// Changing property value from Symbol to undefined denotes the fact that property was accessed.
@ -156,15 +151,17 @@ export class Plugin {
loggers: this.initializerContext.logger,
});
const authz = await setupAuthorization({
const authz = this.authorizationService.setup({
http: core.http,
capabilities: core.capabilities,
status: core.status,
clusterClient: this.clusterClient,
license,
loggers: this.initializerContext.logger,
kibanaIndexName: legacyConfig.kibana.index,
packageVersion: this.initializerContext.env.packageInfo.version,
getSpacesService: this.getSpacesService,
featuresService: features,
features,
});
setupSavedObjects({
@ -174,8 +171,6 @@ export class Plugin {
getSpacesService: this.getSpacesService,
});
core.capabilities.registerSwitcher(authz.disableUnauthorizedCapabilities);
defineRoutes({
router: core.http.createRouter(),
basePath: core.http.basePath,
@ -223,18 +218,15 @@ export class Plugin {
this.spacesService = service;
},
__legacyCompat: {
registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(),
},
});
}
public start(core: CoreStart, { licensing }: PluginStartDependencies) {
public start(core: CoreStart, { features, licensing }: PluginStartDependencies) {
this.logger.debug('Starting plugin');
this.featureUsageServiceStart = this.featureUsageService.start({
featureUsage: licensing.featureUsage,
});
this.authorizationService.start({ features, clusterClient: this.clusterClient! });
}
public stop() {
@ -254,6 +246,7 @@ export class Plugin {
this.featureUsageServiceStart = undefined;
}
this.auditService.stop();
this.authorizationService.stop();
}
private wasSpacesServiceAccessed() {

View file

@ -14,7 +14,7 @@ import {
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { Authentication } from '../authentication';
import { Authorization } from '../authorization';
import { AuthorizationServiceSetup } from '../authorization';
import { ConfigType } from '../config';
import { defineAuthenticationRoutes } from './authentication';
@ -37,7 +37,7 @@ export interface RouteDefinitionParams {
clusterClient: IClusterClient;
config: ConfigType;
authc: Authentication;
authz: Authorization;
authz: AuthorizationServiceSetup;
license: SecurityLicense;
getFeatures: () => Promise<Feature[]>;
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;

View file

@ -11,13 +11,16 @@ import {
SavedObjectsClient,
} from '../../../../../src/core/server';
import { SecureSavedObjectsClientWrapper } from './secure_saved_objects_client_wrapper';
import { Authorization } from '../authorization';
import { AuthorizationServiceSetup } from '../authorization';
import { SecurityAuditLogger } from '../audit';
import { SpacesService } from '../plugin';
interface SetupSavedObjectsParams {
auditLogger: SecurityAuditLogger;
authz: Pick<Authorization, 'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest'>;
authz: Pick<
AuthorizationServiceSetup,
'mode' | 'actions' | 'checkSavedObjectsPrivilegesWithRequest'
>;
savedObjects: CoreSetup['savedObjects'];
getSpacesService(): SpacesService | undefined;
}

View file

@ -56,7 +56,7 @@ In another shell, from **~kibana/x-pack**:
If instead you need to run API tests, start up the test server and then in another shell, from **~kibana/x-pack**:
`node ../scripts/functional_test_runner.js --config test/api_integration/config.js --grep="{TEST_NAME}"`.
`node ../scripts/functional_test_runner.js --config test/api_integration/config.ts --grep="{TEST_NAME}"`.
You can update snapshots by prefixing the runner command with `env UPDATE_UPTIME_FIXTURES=1`

View file

@ -11,8 +11,9 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/functional_with_es_ssl/config.ts'),
require.resolve('../test/functional/config_security_basic.ts'),
require.resolve('../test/functional/config_security_trial.ts'),
require.resolve('../test/api_integration/config_security_basic.js'),
require.resolve('../test/api_integration/config.js'),
require.resolve('../test/api_integration/config_security_basic.ts'),
require.resolve('../test/api_integration/config_security_trial.ts'),
require.resolve('../test/api_integration/config.ts'),
require.resolve('../test/alerting_api_integration/basic/config.ts'),
require.resolve('../test/alerting_api_integration/spaces_only/config.ts'),
require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'),

View file

@ -40,7 +40,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackApiIntegrationTestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.js')
require.resolve('../../api_integration/config.ts')
);
const servers = {
...xPackApiIntegrationTestsConfig.get('servers'),

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect/expect.js';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('Privileges registration', function () {
this.tags(['skipCloud']);
it('privileges are re-registered on license downgrade', async () => {
// Verify currently registered privileges for TRIAL license.
// If you're adding a privilege to the following, that's great!
// If you're removing a privilege, this breaks backwards compatibility
// Roles are associated with these privileges, and we shouldn't be removing them in a minor version.
const expectedTrialLicenseDiscoverPrivileges = [
'all',
'read',
'minimal_all',
'minimal_read',
'url_create',
];
const trialPrivileges = await supertest
.get('/api/security/privileges')
.set('kbn-xsrf', 'xxx')
.send()
.expect(200);
expect(trialPrivileges.body.features.discover).to.eql(expectedTrialLicenseDiscoverPrivileges);
// Revert license to basic.
await supertest
.post('/api/license/start_basic?acknowledge=true')
.set('kbn-xsrf', 'xxx')
.expect(200, {
basic_was_started: true,
acknowledged: true,
});
// Verify that privileges were re-registered.
const expectedBasicLicenseDiscoverPrivileges = ['all', 'read'];
const basicPrivileges = await supertest
.get('/api/security/privileges')
.set('kbn-xsrf', 'xxx')
.send()
.expect(200);
expect(basicPrivileges.body.features.discover).to.eql(expectedBasicLicenseDiscoverPrivileges);
});
});
}

View file

@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('security (trial license)', function () {
this.tags('ciGroup6');
// THIS TEST NEEDS TO BE LAST. IT IS DESTRUCTIVE! IT REMOVES TRIAL LICENSE!!!
loadTestFile(require.resolve('./license_downgrade'));
});
}

View file

@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export async function getApiIntegrationConfig({ readConfigFile }) {
export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProviderContext) {
const xPackFunctionalTestsConfig = await readConfigFile(
require.resolve('../functional/config.js')
);

View file

@ -4,11 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable import/no-default-export */
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { default as createTestConfig } from './config';
export default async function ({ readConfigFile }) {
//security APIs should function the same under a basic or trial license
return createTestConfig({ readConfigFile }).then((config) => {
export default async function (context: FtrConfigProviderContext) {
// security APIs should function the same under a basic or trial license
return createTestConfig(context).then((config) => {
config.esTestCluster.license = 'basic';
config.esTestCluster.serverArgs = [
'xpack.license.self_generated.type=basic',

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable import/no-default-export */
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { default as createTestConfig } from './config';
export default async function (context: FtrConfigProviderContext) {
return createTestConfig(context).then((config) => {
config.testFiles = [require.resolve('./apis/security/security_trial')];
return config;
});
}

View file

@ -17,7 +17,7 @@ export function createTestConfig(settings: Settings) {
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackAPITestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.js')
require.resolve('../../api_integration/config.ts')
);
return {

View file

@ -39,7 +39,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackApiIntegrationTestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.js')
require.resolve('../../api_integration/config.ts')
);
const servers = {

View file

@ -42,7 +42,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackApiIntegrationTestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.js')
require.resolve('../../api_integration/config.ts')
);
const servers = {
...xPackApiIntegrationTestsConfig.get('servers'),

View file

@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
return {
testFiles: [require.resolve('./tests')],

View file

@ -7,7 +7,7 @@
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
return {
...xPackAPITestsConfig.getAll(),

View file

@ -7,7 +7,7 @@
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
return {
testFiles: [require.resolve('./apis')],

View file

@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const kerberosKeytabPath = resolve(
__dirname,

View file

@ -12,7 +12,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const kibanaAPITestsConfig = await readConfigFile(
require.resolve('../../../test/api_integration/config.js')
);
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
const kerberosKeytabPath = resolve(__dirname, '../kerberos_api_integration/fixtures/krb5.keytab');

View file

@ -17,7 +17,7 @@ export function createTestConfig(settings: Settings) {
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackAPITestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.js')
require.resolve('../../api_integration/config.ts')
);
return {

View file

@ -9,7 +9,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const plugin = resolve(__dirname, './fixtures/oidc_provider');
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
const jwksPath = resolve(__dirname, './fixtures/jwks.json');

View file

@ -10,7 +10,7 @@ import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils';
import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const servers = {
...xPackAPITestsConfig.get('servers'),

View file

@ -11,7 +11,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const kibanaAPITestsConfig = await readConfigFile(
require.resolve('../../../test/api_integration/config.js')
);
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
const idpPath = resolve(__dirname, '../../test/saml_api_integration/fixtures/idp_metadata.xml');

View file

@ -24,7 +24,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')),
},
xpack: {
api: await readConfigFile(require.resolve('../../api_integration/config.js')),
api: await readConfigFile(require.resolve('../../api_integration/config.ts')),
},
};

View file

@ -25,7 +25,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
functional: await readConfigFile(require.resolve('../../../../test/functional/config.js')),
},
xpack: {
api: await readConfigFile(require.resolve('../../api_integration/config.js')),
api: await readConfigFile(require.resolve('../../api_integration/config.ts')),
},
};

View file

@ -5,7 +5,7 @@
*/
export default async function ({ readConfigFile }) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
return {
testFiles: [require.resolve('./auth')],

View file

@ -27,9 +27,6 @@
"plugins/xpack_main/*": [
"x-pack/legacy/plugins/xpack_main/public/*"
],
"plugins/security/*": [
"x-pack/legacy/plugins/security/public/*"
],
"plugins/spaces/*": [
"x-pack/legacy/plugins/spaces/public/*"
],

View file

@ -7,7 +7,6 @@
import 'hapi';
import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main';
import { SecurityPlugin } from '../legacy/plugins/security';
import { ActionsPlugin, ActionsClient } from '../plugins/actions/server';
import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server';
import { TaskManager } from '../plugins/task_manager/server';
@ -19,7 +18,6 @@ declare module 'hapi' {
}
interface PluginProperties {
xpack_main: XPackMainPlugin;
security?: SecurityPlugin;
actions?: ActionsPlugin;
alerts?: AlertingPlugin;
task_manager?: TaskManager;