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:
Larry Gregory 2021-04-23 11:59:11 -04:00 committed by GitHub
parent 485692dbf1
commit ec8ff3a7fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 17 deletions

View 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();
});
});
});

View file

@ -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;
}
}

View file

@ -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',

View file

@ -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),
};

View file

@ -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),
};