mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Only add cloud-specific links for superusers (#97870)
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
485692dbf1
commit
ec8ff3a7fc
5 changed files with 251 additions and 17 deletions
190
x-pack/plugins/cloud/public/plugin.test.ts
Normal file
190
x-pack/plugins/cloud/public/plugin.test.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { homePluginMock } from 'src/plugins/home/public/mocks';
|
||||
import { securityMock } from '../../security/public/mocks';
|
||||
import { CloudPlugin } from './plugin';
|
||||
|
||||
describe('Cloud Plugin', () => {
|
||||
describe('#start', () => {
|
||||
function setupPlugin({
|
||||
roles = [],
|
||||
simulateUserError = false,
|
||||
}: { roles?: string[]; simulateUserError?: boolean } = {}) {
|
||||
const plugin = new CloudPlugin(
|
||||
coreMock.createPluginInitializerContext({
|
||||
id: 'cloudId',
|
||||
base_url: 'https://cloud.elastic.co',
|
||||
deployment_url: '/abc123',
|
||||
profile_url: '/profile/alice',
|
||||
organization_url: '/org/myOrg',
|
||||
})
|
||||
);
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const homeSetup = homePluginMock.createSetupContract();
|
||||
const securitySetup = securityMock.createSetup();
|
||||
if (simulateUserError) {
|
||||
securitySetup.authc.getCurrentUser.mockRejectedValue(new Error('Something happened'));
|
||||
} else {
|
||||
securitySetup.authc.getCurrentUser.mockResolvedValue(
|
||||
securityMock.createMockAuthenticatedUser({
|
||||
roles,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
plugin.setup(coreSetup, { home: homeSetup, security: securitySetup });
|
||||
|
||||
return { coreSetup, securitySetup, plugin };
|
||||
}
|
||||
|
||||
it('registers help support URL', async () => {
|
||||
const { plugin } = setupPlugin();
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"https://support.elastic.co/",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers a custom nav link for superusers', async () => {
|
||||
const { plugin } = setupPlugin({ roles: ['superuser'] });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"euiIconType": "arrowLeft",
|
||||
"href": "https://cloud.elastic.co/abc123",
|
||||
"title": "Manage this deployment",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers a custom nav link when there is an error retrieving the current user', async () => {
|
||||
const { plugin } = setupPlugin({ simulateUserError: true });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"euiIconType": "arrowLeft",
|
||||
"href": "https://cloud.elastic.co/abc123",
|
||||
"title": "Manage this deployment",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not register a custom nav link for non-superusers', async () => {
|
||||
const { plugin } = setupPlugin({ roles: ['not-a-superuser'] });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers user profile links for superusers', async () => {
|
||||
const { plugin } = setupPlugin({ roles: ['superuser'] });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/profile/alice",
|
||||
"iconType": "user",
|
||||
"label": "Profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/org/myOrg",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('registers profile links when there is an error retrieving the current user', async () => {
|
||||
const { plugin } = setupPlugin({ simulateUserError: true });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
|
||||
expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/profile/alice",
|
||||
"iconType": "user",
|
||||
"label": "Profile",
|
||||
"order": 100,
|
||||
"setAsProfile": true,
|
||||
},
|
||||
Object {
|
||||
"href": "https://cloud.elastic.co/org/myOrg",
|
||||
"iconType": "gear",
|
||||
"label": "Account & Billing",
|
||||
"order": 200,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not register profile links for non-superusers', async () => {
|
||||
const { plugin } = setupPlugin({ roles: ['not-a-superuser'] });
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const securityStart = securityMock.createStart();
|
||||
plugin.start(coreStart, { security: securityStart });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SecurityPluginStart } from '../../security/public';
|
||||
import { AuthenticatedUser, SecurityPluginSetup, SecurityPluginStart } from '../../security/public';
|
||||
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
|
||||
import { ELASTIC_SUPPORT_LINK } from '../common/constants';
|
||||
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
|
||||
|
@ -25,6 +25,7 @@ export interface CloudConfigType {
|
|||
|
||||
interface CloudSetupDependencies {
|
||||
home?: HomePublicPluginSetup;
|
||||
security?: Pick<SecurityPluginSetup, 'authc'>;
|
||||
}
|
||||
|
||||
interface CloudStartDependencies {
|
||||
|
@ -44,13 +45,14 @@ export interface CloudSetup {
|
|||
export class CloudPlugin implements Plugin<CloudSetup> {
|
||||
private config!: CloudConfigType;
|
||||
private isCloudEnabled: boolean;
|
||||
private authenticatedUserPromise?: Promise<AuthenticatedUser | null>;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<CloudConfigType>();
|
||||
this.isCloudEnabled = false;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, { home }: CloudSetupDependencies) {
|
||||
public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) {
|
||||
const {
|
||||
id,
|
||||
cname,
|
||||
|
@ -68,6 +70,10 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
}
|
||||
}
|
||||
|
||||
if (security) {
|
||||
this.authenticatedUserPromise = security.authc.getCurrentUser().catch(() => null);
|
||||
}
|
||||
|
||||
return {
|
||||
cloudId: id,
|
||||
cname,
|
||||
|
@ -82,19 +88,47 @@ export class CloudPlugin implements Plugin<CloudSetup> {
|
|||
public start(coreStart: CoreStart, { security }: CloudStartDependencies) {
|
||||
const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config;
|
||||
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
|
||||
if (baseUrl && deploymentUrl) {
|
||||
coreStart.chrome.setCustomNavLink({
|
||||
title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
}),
|
||||
euiIconType: 'arrowLeft',
|
||||
href: getFullCloudUrl(baseUrl, deploymentUrl),
|
||||
});
|
||||
}
|
||||
|
||||
if (security && this.isCloudEnabled) {
|
||||
const userMenuLinks = createUserMenuLinks(this.config);
|
||||
security.navControlService.addUserMenuLinks(userMenuLinks);
|
||||
}
|
||||
const setLinks = (authorized: boolean) => {
|
||||
if (!authorized) return;
|
||||
|
||||
if (baseUrl && deploymentUrl) {
|
||||
coreStart.chrome.setCustomNavLink({
|
||||
title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
|
||||
defaultMessage: 'Manage this deployment',
|
||||
}),
|
||||
euiIconType: 'arrowLeft',
|
||||
href: getFullCloudUrl(baseUrl, deploymentUrl),
|
||||
});
|
||||
}
|
||||
|
||||
if (security && this.isCloudEnabled) {
|
||||
const userMenuLinks = createUserMenuLinks(this.config);
|
||||
security.navControlService.addUserMenuLinks(userMenuLinks);
|
||||
}
|
||||
};
|
||||
|
||||
this.checkIfAuthorizedForLinks()
|
||||
.then(setLinks)
|
||||
// In the event of an unexpected error, fail *open*.
|
||||
// Cloud admin console will always perform the actual authorization checks.
|
||||
.catch(() => setLinks(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current user should see links back to Cloud.
|
||||
* This isn't a true authorization check, but rather a heuristic to
|
||||
* see if the current user is *likely* a cloud deployment administrator.
|
||||
*
|
||||
* At this point, we do not have enough information to reliably make this determination,
|
||||
* but we do know that all cloud deployment admins are superusers by default.
|
||||
*/
|
||||
private async checkIfAuthorizedForLinks() {
|
||||
// Security plugin is disabled
|
||||
if (!this.authenticatedUserPromise) return true;
|
||||
// Otherwise check roles. If user is not defined due to an unexpected error, then fail *open*.
|
||||
// Cloud admin console will always perform the actual authorization checks.
|
||||
const user = await this.authenticatedUserPromise;
|
||||
return user?.roles.includes('superuser') ?? true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import type { AuthenticatedUser } from './authenticated_user';
|
|||
|
||||
// We omit `roles` here since the original interface defines this field as `readonly string[]` that makes it hard to use
|
||||
// in various mocks that expect mutable string array.
|
||||
type AuthenticatedUserProps = Partial<Omit<AuthenticatedUser, 'roles'> & { roles: string[] }>;
|
||||
export function mockAuthenticatedUser(user: AuthenticatedUserProps = {}) {
|
||||
export type MockAuthenticatedUserProps = Partial<
|
||||
Omit<AuthenticatedUser, 'roles'> & { roles: string[] }
|
||||
>;
|
||||
export function mockAuthenticatedUser(user: MockAuthenticatedUserProps = {}) {
|
||||
return {
|
||||
username: 'user',
|
||||
email: 'email',
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import { licenseMock } from '../common/licensing/index.mock';
|
||||
import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock';
|
||||
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
|
||||
import { authenticationMock } from './authentication/index.mock';
|
||||
import { navControlServiceMock } from './nav_control/index.mock';
|
||||
import { createSessionTimeoutMock } from './session/session_timeout.mock';
|
||||
|
@ -26,4 +28,6 @@ function createStartMock() {
|
|||
export const securityMock = {
|
||||
createSetup: createSetupMock,
|
||||
createStart: createStartMock,
|
||||
createMockAuthenticatedUser: (props: MockAuthenticatedUserProps = {}) =>
|
||||
mockAuthenticatedUser(props),
|
||||
};
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import type { ApiResponse } from '@elastic/elasticsearch';
|
||||
|
||||
import { licenseMock } from '../common/licensing/index.mock';
|
||||
import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock';
|
||||
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
|
||||
import { auditServiceMock } from './audit/index.mock';
|
||||
import { authenticationServiceMock } from './authentication/authentication_service.mock';
|
||||
import { authorizationMock } from './authorization/index.mock';
|
||||
|
@ -62,4 +64,6 @@ export const securityMock = {
|
|||
createSetup: createSetupMock,
|
||||
createStart: createStartMock,
|
||||
createApiResponse: createApiResponseMock,
|
||||
createMockAuthenticatedUser: (props: MockAuthenticatedUserProps = {}) =>
|
||||
mockAuthenticatedUser(props),
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue