Skip to content

Commit aad46d9

Browse files
kibanamachinelegregoazasypkin
authored
Only add cloud-specific links for superusers (#97870) (#98170)
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Larry Gregory <larry.gregory@elastic.co> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
1 parent 44a174a commit aad46d9

File tree

5 files changed

+251
-17
lines changed

5 files changed

+251
-17
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { nextTick } from '@kbn/test/jest';
9+
import { coreMock } from 'src/core/public/mocks';
10+
import { homePluginMock } from 'src/plugins/home/public/mocks';
11+
import { securityMock } from '../../security/public/mocks';
12+
import { CloudPlugin } from './plugin';
13+
14+
describe('Cloud Plugin', () => {
15+
describe('#start', () => {
16+
function setupPlugin({
17+
roles = [],
18+
simulateUserError = false,
19+
}: { roles?: string[]; simulateUserError?: boolean } = {}) {
20+
const plugin = new CloudPlugin(
21+
coreMock.createPluginInitializerContext({
22+
id: 'cloudId',
23+
base_url: 'https://cloud.elastic.co',
24+
deployment_url: '/abc123',
25+
profile_url: '/profile/alice',
26+
organization_url: '/org/myOrg',
27+
})
28+
);
29+
const coreSetup = coreMock.createSetup();
30+
const homeSetup = homePluginMock.createSetupContract();
31+
const securitySetup = securityMock.createSetup();
32+
if (simulateUserError) {
33+
securitySetup.authc.getCurrentUser.mockRejectedValue(new Error('Something happened'));
34+
} else {
35+
securitySetup.authc.getCurrentUser.mockResolvedValue(
36+
securityMock.createMockAuthenticatedUser({
37+
roles,
38+
})
39+
);
40+
}
41+
42+
plugin.setup(coreSetup, { home: homeSetup, security: securitySetup });
43+
44+
return { coreSetup, securitySetup, plugin };
45+
}
46+
47+
it('registers help support URL', async () => {
48+
const { plugin } = setupPlugin();
49+
50+
const coreStart = coreMock.createStart();
51+
const securityStart = securityMock.createStart();
52+
plugin.start(coreStart, { security: securityStart });
53+
54+
expect(coreStart.chrome.setHelpSupportUrl).toHaveBeenCalledTimes(1);
55+
expect(coreStart.chrome.setHelpSupportUrl.mock.calls[0]).toMatchInlineSnapshot(`
56+
Array [
57+
"https://support.elastic.co/",
58+
]
59+
`);
60+
});
61+
62+
it('registers a custom nav link for superusers', async () => {
63+
const { plugin } = setupPlugin({ roles: ['superuser'] });
64+
65+
const coreStart = coreMock.createStart();
66+
const securityStart = securityMock.createStart();
67+
plugin.start(coreStart, { security: securityStart });
68+
69+
await nextTick();
70+
71+
expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
72+
expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
73+
Array [
74+
Object {
75+
"euiIconType": "arrowLeft",
76+
"href": "https://cloud.elastic.co/abc123",
77+
"title": "Manage this deployment",
78+
},
79+
]
80+
`);
81+
});
82+
83+
it('registers a custom nav link when there is an error retrieving the current user', async () => {
84+
const { plugin } = setupPlugin({ simulateUserError: true });
85+
86+
const coreStart = coreMock.createStart();
87+
const securityStart = securityMock.createStart();
88+
plugin.start(coreStart, { security: securityStart });
89+
90+
await nextTick();
91+
92+
expect(coreStart.chrome.setCustomNavLink).toHaveBeenCalledTimes(1);
93+
expect(coreStart.chrome.setCustomNavLink.mock.calls[0]).toMatchInlineSnapshot(`
94+
Array [
95+
Object {
96+
"euiIconType": "arrowLeft",
97+
"href": "https://cloud.elastic.co/abc123",
98+
"title": "Manage this deployment",
99+
},
100+
]
101+
`);
102+
});
103+
104+
it('does not register a custom nav link for non-superusers', async () => {
105+
const { plugin } = setupPlugin({ roles: ['not-a-superuser'] });
106+
107+
const coreStart = coreMock.createStart();
108+
const securityStart = securityMock.createStart();
109+
plugin.start(coreStart, { security: securityStart });
110+
111+
await nextTick();
112+
113+
expect(coreStart.chrome.setCustomNavLink).not.toHaveBeenCalled();
114+
});
115+
116+
it('registers user profile links for superusers', async () => {
117+
const { plugin } = setupPlugin({ roles: ['superuser'] });
118+
119+
const coreStart = coreMock.createStart();
120+
const securityStart = securityMock.createStart();
121+
plugin.start(coreStart, { security: securityStart });
122+
123+
await nextTick();
124+
125+
expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
126+
expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
127+
Array [
128+
Array [
129+
Object {
130+
"href": "https://cloud.elastic.co/profile/alice",
131+
"iconType": "user",
132+
"label": "Profile",
133+
"order": 100,
134+
"setAsProfile": true,
135+
},
136+
Object {
137+
"href": "https://cloud.elastic.co/org/myOrg",
138+
"iconType": "gear",
139+
"label": "Account & Billing",
140+
"order": 200,
141+
},
142+
],
143+
]
144+
`);
145+
});
146+
147+
it('registers profile links when there is an error retrieving the current user', async () => {
148+
const { plugin } = setupPlugin({ simulateUserError: true });
149+
150+
const coreStart = coreMock.createStart();
151+
const securityStart = securityMock.createStart();
152+
plugin.start(coreStart, { security: securityStart });
153+
154+
await nextTick();
155+
156+
expect(securityStart.navControlService.addUserMenuLinks).toHaveBeenCalledTimes(1);
157+
expect(securityStart.navControlService.addUserMenuLinks.mock.calls[0]).toMatchInlineSnapshot(`
158+
Array [
159+
Array [
160+
Object {
161+
"href": "https://cloud.elastic.co/profile/alice",
162+
"iconType": "user",
163+
"label": "Profile",
164+
"order": 100,
165+
"setAsProfile": true,
166+
},
167+
Object {
168+
"href": "https://cloud.elastic.co/org/myOrg",
169+
"iconType": "gear",
170+
"label": "Account & Billing",
171+
"order": 200,
172+
},
173+
],
174+
]
175+
`);
176+
});
177+
178+
it('does not register profile links for non-superusers', async () => {
179+
const { plugin } = setupPlugin({ roles: ['not-a-superuser'] });
180+
181+
const coreStart = coreMock.createStart();
182+
const securityStart = securityMock.createStart();
183+
plugin.start(coreStart, { security: securityStart });
184+
185+
await nextTick();
186+
187+
expect(securityStart.navControlService.addUserMenuLinks).not.toHaveBeenCalled();
188+
});
189+
});
190+
});

x-pack/plugins/cloud/public/plugin.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
99
import { i18n } from '@kbn/i18n';
10-
import { SecurityPluginStart } from '../../security/public';
10+
import { AuthenticatedUser, SecurityPluginSetup, SecurityPluginStart } from '../../security/public';
1111
import { getIsCloudEnabled } from '../common/is_cloud_enabled';
1212
import { ELASTIC_SUPPORT_LINK } from '../common/constants';
1313
import { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
@@ -25,6 +25,7 @@ export interface CloudConfigType {
2525

2626
interface CloudSetupDependencies {
2727
home?: HomePublicPluginSetup;
28+
security?: Pick<SecurityPluginSetup, 'authc'>;
2829
}
2930

3031
interface CloudStartDependencies {
@@ -44,13 +45,14 @@ export interface CloudSetup {
4445
export class CloudPlugin implements Plugin<CloudSetup> {
4546
private config!: CloudConfigType;
4647
private isCloudEnabled: boolean;
48+
private authenticatedUserPromise?: Promise<AuthenticatedUser | null>;
4749

4850
constructor(private readonly initializerContext: PluginInitializerContext) {
4951
this.config = this.initializerContext.config.get<CloudConfigType>();
5052
this.isCloudEnabled = false;
5153
}
5254

53-
public setup(core: CoreSetup, { home }: CloudSetupDependencies) {
55+
public setup(core: CoreSetup, { home, security }: CloudSetupDependencies) {
5456
const {
5557
id,
5658
cname,
@@ -68,6 +70,10 @@ export class CloudPlugin implements Plugin<CloudSetup> {
6870
}
6971
}
7072

73+
if (security) {
74+
this.authenticatedUserPromise = security.authc.getCurrentUser().catch(() => null);
75+
}
76+
7177
return {
7278
cloudId: id,
7379
cname,
@@ -82,19 +88,47 @@ export class CloudPlugin implements Plugin<CloudSetup> {
8288
public start(coreStart: CoreStart, { security }: CloudStartDependencies) {
8389
const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config;
8490
coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK);
85-
if (baseUrl && deploymentUrl) {
86-
coreStart.chrome.setCustomNavLink({
87-
title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
88-
defaultMessage: 'Manage this deployment',
89-
}),
90-
euiIconType: 'arrowLeft',
91-
href: getFullCloudUrl(baseUrl, deploymentUrl),
92-
});
93-
}
9491

95-
if (security && this.isCloudEnabled) {
96-
const userMenuLinks = createUserMenuLinks(this.config);
97-
security.navControlService.addUserMenuLinks(userMenuLinks);
98-
}
92+
const setLinks = (authorized: boolean) => {
93+
if (!authorized) return;
94+
95+
if (baseUrl && deploymentUrl) {
96+
coreStart.chrome.setCustomNavLink({
97+
title: i18n.translate('xpack.cloud.deploymentLinkLabel', {
98+
defaultMessage: 'Manage this deployment',
99+
}),
100+
euiIconType: 'arrowLeft',
101+
href: getFullCloudUrl(baseUrl, deploymentUrl),
102+
});
103+
}
104+
105+
if (security && this.isCloudEnabled) {
106+
const userMenuLinks = createUserMenuLinks(this.config);
107+
security.navControlService.addUserMenuLinks(userMenuLinks);
108+
}
109+
};
110+
111+
this.checkIfAuthorizedForLinks()
112+
.then(setLinks)
113+
// In the event of an unexpected error, fail *open*.
114+
// Cloud admin console will always perform the actual authorization checks.
115+
.catch(() => setLinks(true));
116+
}
117+
118+
/**
119+
* Determines if the current user should see links back to Cloud.
120+
* This isn't a true authorization check, but rather a heuristic to
121+
* see if the current user is *likely* a cloud deployment administrator.
122+
*
123+
* At this point, we do not have enough information to reliably make this determination,
124+
* but we do know that all cloud deployment admins are superusers by default.
125+
*/
126+
private async checkIfAuthorizedForLinks() {
127+
// Security plugin is disabled
128+
if (!this.authenticatedUserPromise) return true;
129+
// Otherwise check roles. If user is not defined due to an unexpected error, then fail *open*.
130+
// Cloud admin console will always perform the actual authorization checks.
131+
const user = await this.authenticatedUserPromise;
132+
return user?.roles.includes('superuser') ?? true;
99133
}
100134
}

x-pack/plugins/security/common/model/authenticated_user.mock.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import type { AuthenticatedUser } from './authenticated_user';
99

1010
// We omit `roles` here since the original interface defines this field as `readonly string[]` that makes it hard to use
1111
// in various mocks that expect mutable string array.
12-
type AuthenticatedUserProps = Partial<Omit<AuthenticatedUser, 'roles'> & { roles: string[] }>;
13-
export function mockAuthenticatedUser(user: AuthenticatedUserProps = {}) {
12+
export type MockAuthenticatedUserProps = Partial<
13+
Omit<AuthenticatedUser, 'roles'> & { roles: string[] }
14+
>;
15+
export function mockAuthenticatedUser(user: MockAuthenticatedUserProps = {}) {
1416
return {
1517
username: 'user',
1618
email: 'email',

x-pack/plugins/security/public/mocks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77

88
import { licenseMock } from '../common/licensing/index.mock';
9+
import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock';
10+
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
911
import { authenticationMock } from './authentication/index.mock';
1012
import { navControlServiceMock } from './nav_control/index.mock';
1113
import { createSessionTimeoutMock } from './session/session_timeout.mock';
@@ -26,4 +28,6 @@ function createStartMock() {
2628
export const securityMock = {
2729
createSetup: createSetupMock,
2830
createStart: createStartMock,
31+
createMockAuthenticatedUser: (props: MockAuthenticatedUserProps = {}) =>
32+
mockAuthenticatedUser(props),
2933
};

x-pack/plugins/security/server/mocks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import type { ApiResponse } from '@elastic/elasticsearch';
99

1010
import { licenseMock } from '../common/licensing/index.mock';
11+
import type { MockAuthenticatedUserProps } from '../common/model/authenticated_user.mock';
12+
import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
1113
import { auditServiceMock } from './audit/index.mock';
1214
import { authenticationServiceMock } from './authentication/authentication_service.mock';
1315
import { authorizationMock } from './authorization/index.mock';
@@ -62,4 +64,6 @@ export const securityMock = {
6264
createSetup: createSetupMock,
6365
createStart: createStartMock,
6466
createApiResponse: createApiResponseMock,
67+
createMockAuthenticatedUser: (props: MockAuthenticatedUserProps = {}) =>
68+
mockAuthenticatedUser(props),
6569
};

0 commit comments

Comments
 (0)