Create navigation cards for serverless access management (#176761)

Closes #174953

## Summary

Adds "Access" section to serverless management page, with cards for
custom roles, organization members, and API keys. These new cards are
gated by the `roleManagementEnabled` feature flag (see #176200).

<img width="1339" alt="Screenshot 2024-03-11 at 10 17 06 PM"
src="f2bb02f3-4154-4f2a-b07f-4c0013429a0c">

### API keys card
Access to this card is gated by API key privileges - any user with
permission to access the API keys management page will see this card.

### Custom roles card
Access to this card is gated by both the feature flag and role
privileges - any user with permission to access the Roles management
page will see this card if the feature flag is enabled.

### Organization members card
Access to this card is gated by only the feature flag. **Currently there
is no way to query if a user has access to manage the cloud
organization.**

### Implementation Notes:

- Previously, only the serverless search solution offered a link to the
API keys management page from the left navigation bar and the landing
page. This PR will provide access to the API keys management page in all
3 serverless solutions, via the management cards page, given the user
has the minimum API key permissions required.
- In order to check the value of the feature flag from outside of the
security plugin, I have exposed an authz service from the security
plugin (following the paradigm of the authc service). This can be
removed once the feature flag is no longer needed.
- The `Organization members` card is an "extension" navigation card
because it is not tied to an actual application. It provides a link to
the cloud organization. This is implemented in the serverless plugin,
alongside a `getNavigationCards` helper function, to be commonly located
for use in the three serverless solutions plugins. Due to dependency
restrictions, each solution plugin passes the feature flag value from
the security plugin to this function - a complication that will be
removed once the feature flag is no longer needed.

## Manual Testing

1. In the `kibana.dev.yml` file, add the following settings. This
enables the role management feature flag, and provides cloud URLs for
the `Manage organization members` card.
```
xpack.security.roleManagementEnabled: true
xpack.cloud.base_url: 'https://cloud.elastic.co'
xpack.cloud.organization_url: '/account/members'
```

2. Add a test user without access to API keys to the serverless search
`roles.yml` file. Example: Copy the viewer role, and remove the
`manage_own_api_key` cluster privilege.
```
tester:
  cluster: ['read_pipeline']
  indices:
    - names:
        - '*'
      privileges:
        - 'read'
        - 'view_index_metadata'
  applications:
    - application: 'kibana-.kibana'
      privileges:
        - 'read'
      resources:
        - '*'
```

3. Start Elasticsearch and Kibana in serverless mode and SSL enabled (to
access the test user selector). Examples:
```
yarn es --serverless=es --ssl
yarn start --serverless=es --ssl
```
4. Navigate to Kibana (use `https` as SSL is enabled), and log in as the
`Admin` test user.
5. Navigate to the Management page using the side navigation bar. Verify
the three new cards are rendered in a new `Access` section, and that
each functions correctly by navigating the user the appropriate
application, or to the cloud organization page (in the case of the
Manage organization members card).
6. Switch to a user without access to view or update roles (e.g.
`viewer` in the serverless search solution). Verify that the API keys
and Org members cards are present, but not the Custom roles card.
7. Switch to a user without access to the API keys management page (the
test role added in step 2 for the search solution). Verify that the API
keys card is not present.
8. Disable the `xpack.security.roleManagementEnabled` feature flag.
Switch to the `admin` test user, and verify that the `Access` section
contains only the API keys card
9. Switch to a user without access to the API keys management page.
Verify that the `Access` section does not render at all.
10. Repeat testing with other solutions (security, observability). Keep
in mind that you may have to add additional test roles to the
`roles.yml` file if you want to test conditions for steps 6 and 7
independently.

## Automated Testing
See
`x-pack/test_serverless/functional/test_suites/common/platform_security/navigation/management_nav_cards.ts`,
which can be run from
-
`x-pack/test_serverless/functional/test_suites/search/config.feature_flags.ts`
-
`x-pack/test_serverless/functional/test_suites/security/config.feature_flags.ts`
-
`x-pack/test_serverless/functional/test_suites/observability/config.feature_flags.ts`.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jeramy Soucy 2024-03-22 12:50:43 -04:00 committed by GitHub
parent 59b1f8ae94
commit d27ada2e29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 428 additions and 106 deletions

View file

@ -7,6 +7,5 @@
*/
export type { AppId, CardsNavigationComponentProps } from './src';
export { appIds } from './src';
export { CardsNavigation } from './src';
export { appCategories, type CardNavExtensionDefinition } from './src/types';
export { appIds, CardsNavigation } from './src';

View file

@ -109,6 +109,13 @@ const getEnabledAppsByCategory = (
}),
apps: getAppsForCategory(appCategories.DATA, filteredApps),
},
{
id: appCategories.ACCESS,
title: i18n.translate('management.landing.withCardNavigation.accessTitle', {
defaultMessage: 'Access',
}),
apps: getAppsForCategory(appCategories.ACCESS, filteredApps),
},
{
id: appCategories.ALERTS,
title: i18n.translate('management.landing.withCardNavigation.alertsTitle', {

View file

@ -119,14 +119,6 @@ export const appDefinitions: Record<AppId, AppDefinition> = {
icon: 'tag',
},
[AppIds.API_KEYS]: {
category: appCategories.OTHER,
description: i18n.translate('management.landing.withCardNavigation.apiKeysDescription', {
defaultMessage: 'Allow programmatic access to your project data and capabilities.',
}),
icon: 'lockOpen',
},
[AppIds.SERVERLESS_SETTINGS]: {
category: appCategories.OTHER,
description: i18n.translate('management.landing.withCardNavigation.settingsDescription', {
@ -135,10 +127,19 @@ export const appDefinitions: Record<AppId, AppDefinition> = {
icon: 'gear',
},
// Access section
[AppIds.API_KEYS]: {
category: appCategories.ACCESS,
description: i18n.translate('management.landing.withCardNavigation.apiKeysDescription', {
defaultMessage: 'Allow programmatic access to your project data and capabilities.',
}),
icon: 'lockOpen',
},
[AppIds.ROLES]: {
category: appCategories.OTHER,
category: appCategories.ACCESS,
description: i18n.translate('management.landing.withCardNavigation.rolesDescription', {
defaultMessage: 'Allow custom roles to be created for users.',
defaultMessage:
'Create roles unique to this project and combine the exact set of privileges that your users need.',
}),
icon: 'usersRolesApp',
},

View file

@ -20,7 +20,6 @@ export enum AppIds {
SAVED_OBJECTS = 'objects',
TAGS = 'tags',
FILES_MANAGEMENT = 'filesManagement',
API_KEYS = 'api_keys',
DATA_VIEWS = 'dataViews',
REPORTING = 'reporting',
CONNECTORS = 'triggersActionsConnectors',
@ -28,6 +27,7 @@ export enum AppIds {
MAINTENANCE_WINDOWS = 'maintenanceWindows',
SERVERLESS_SETTINGS = 'settings',
ROLES = 'roles',
API_KEYS = 'api_keys',
}
// Create new type that is a union of all the appId values
@ -35,6 +35,7 @@ export type AppId = `${AppIds}`;
export const appCategories = {
DATA: 'data',
ACCESS: 'access',
ALERTS: 'alerts',
CONTENT: 'content',
OTHER: 'other',

View file

@ -6,6 +6,7 @@
*/
export type { AuthenticationServiceStart, AuthenticationServiceSetup } from './src/authentication';
export type { AuthorizationServiceStart, AuthorizationServiceSetup } from './src/authorization';
export type { UserMenuLink, SecurityNavControlServiceStart } from './src/nav_control';
export type { SecurityPluginSetup, SecurityPluginStart } from './src/plugin';
export type {

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
export interface AuthorizationServiceSetup {
/**
* Determines if role management is enabled.
*/
isRoleManagementEnabled: () => boolean | undefined;
}
/**
* Start has the same contract as Setup for now.
*/
export type AuthorizationServiceStart = AuthorizationServiceSetup;

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export type { AuthorizationServiceSetup, AuthorizationServiceStart } from './authorization_service';

View file

@ -7,6 +7,7 @@
import type { SecurityLicense } from '@kbn/security-plugin-types-common';
import type { AuthenticationServiceSetup, AuthenticationServiceStart } from './authentication';
import type { AuthorizationServiceSetup, AuthorizationServiceStart } from './authorization';
import type { SecurityNavControlServiceStart } from './nav_control';
import type { UserProfileAPIClient } from './user_profile';
@ -15,6 +16,10 @@ export interface SecurityPluginSetup {
* Exposes authentication information about the currently logged in user.
*/
authc: AuthenticationServiceSetup;
/**
* Exposes authorization configuration.
*/
authz: AuthorizationServiceSetup;
/**
* Exposes information about the available security features under the current license.
*/
@ -30,6 +35,10 @@ export interface SecurityPluginStart {
* Exposes authentication information about the currently logged in user.
*/
authc: AuthenticationServiceStart;
/**
* Exposes authorization configuration.
*/
authz: AuthorizationServiceStart;
/**
* A set of methods to work with Kibana user profiles.
*/

View file

@ -33,4 +33,4 @@
"remoteClusters"
]
}
}
}

View file

@ -8,6 +8,8 @@
import type {
AuthenticationServiceSetup,
AuthenticationServiceStart,
AuthorizationServiceSetup,
AuthorizationServiceStart,
} from '@kbn/security-plugin-types-public';
export const authenticationMock = {
@ -20,3 +22,12 @@ export const authenticationMock = {
areAPIKeysEnabled: jest.fn(),
}),
};
export const authorizationMock = {
createSetup: (): jest.Mocked<AuthorizationServiceSetup> => ({
isRoleManagementEnabled: jest.fn(),
}),
createStart: (): jest.Mocked<AuthorizationServiceStart> => ({
isRoleManagementEnabled: jest.fn(),
}),
};

View file

@ -0,0 +1,22 @@
/*
* 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 type { AuthorizationServiceSetup } from '@kbn/security-plugin-types-public';
import type { ConfigType } from '../config';
interface SetupParams {
config: ConfigType;
}
export class AuthorizationService {
public setup({ config }: SetupParams): AuthorizationServiceSetup {
const isRoleManagementEnabled = () => config.roleManagementEnabled;
return { isRoleManagementEnabled };
}
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { AuthorizationService } from './authorization_service';

View file

@ -25,6 +25,8 @@ export { ALL_SPACES_ID } from '../common/constants';
export type {
AuthenticationServiceStart,
AuthenticationServiceSetup,
AuthorizationServiceStart,
AuthorizationServiceSetup,
SecurityNavControlServiceStart,
UserMenuLink,
UserProfileBulkGetParams,

View file

@ -51,6 +51,7 @@ describe('ManagementService', () => {
fatalErrors,
authc,
management: managementSetup,
buildFlavor: 'traditional',
});
expect(mockSection.registerApp).toHaveBeenCalledTimes(4);
@ -111,6 +112,7 @@ describe('ManagementService', () => {
fatalErrors,
authc,
management: managementSetup,
buildFlavor: 'traditional',
});
// Only API Keys app should be registered
@ -181,6 +183,7 @@ describe('ManagementService', () => {
fatalErrors,
authc: securityMock.createSetup().authc,
management: managementSetup,
buildFlavor: 'traditional',
});
const getMockedApp = (id: string) => {

View file

@ -7,6 +7,7 @@
import type { Subscription } from 'rxjs';
import type { BuildFlavor } from '@kbn/config';
import type { Capabilities, FatalErrorsSetup, StartServicesAccessor } from '@kbn/core/public';
import type {
ManagementApp,
@ -35,6 +36,7 @@ interface SetupParams {
authc: AuthenticationServiceSetup;
fatalErrors: FatalErrorsSetup;
getStartServices: StartServicesAccessor<PluginStartDependencies>;
buildFlavor: BuildFlavor;
}
interface StartParams {
@ -55,7 +57,7 @@ export class ManagementService {
this.roleMappingManagementEnabled = config.ui?.roleMappingManagementEnabled !== false;
}
setup({ getStartServices, management, authc, license, fatalErrors }: SetupParams) {
setup({ getStartServices, management, authc, license, fatalErrors, buildFlavor }: SetupParams) {
this.license = license;
this.securitySection = management.sections.section.security;
@ -65,7 +67,7 @@ export class ManagementService {
if (this.roleManagementEnabled) {
this.securitySection.registerApp(
rolesManagementApp.create({ fatalErrors, license, getStartServices })
rolesManagementApp.create({ fatalErrors, license, getStartServices, buildFlavor })
);
}

View file

@ -48,6 +48,7 @@ async function mountApp(basePath: string, pathname: string) {
getStartServices: jest
.fn()
.mockResolvedValue([coreStart, { data: {}, features: featuresStart }]),
buildFlavor: 'traditional',
})
.mount({
basePath,
@ -70,6 +71,7 @@ describe('rolesManagementApp', () => {
license: licenseMock.create(),
fatalErrors,
getStartServices: getStartServices as any,
buildFlavor: 'traditional',
})
).toMatchInlineSnapshot(`
Object {

View file

@ -9,6 +9,7 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { useParams } from 'react-router-dom';
import type { BuildFlavor } from '@kbn/config';
import type { FatalErrorsSetup, StartServicesAccessor } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
@ -29,14 +30,20 @@ interface CreateParams {
fatalErrors: FatalErrorsSetup;
license: SecurityLicense;
getStartServices: StartServicesAccessor<PluginStartDependencies>;
buildFlavor: BuildFlavor;
}
export const rolesManagementApp = Object.freeze({
id: 'roles',
create({ license, fatalErrors, getStartServices }: CreateParams) {
const title = i18n.translate('xpack.security.management.rolesTitle', {
defaultMessage: 'Roles',
});
create({ license, fatalErrors, getStartServices, buildFlavor }: CreateParams) {
const title =
buildFlavor === 'serverless'
? i18n.translate('xpack.security.management.rolesTitleServerless', {
defaultMessage: 'Custom Roles',
})
: i18n.translate('xpack.security.management.rolesTitle', {
defaultMessage: 'Roles',
});
return {
id: this.id,
order: 20,

View file

@ -7,7 +7,7 @@
import { of } from 'rxjs';
import { authenticationMock } from './authentication/index.mock';
import { authenticationMock, authorizationMock } from './authentication/index.mock';
import { navControlServiceMock } from './nav_control/index.mock';
import { getUiApiMock } from './ui_api/index.mock';
import { licenseMock } from '../common/licensing/index.mock';
@ -17,12 +17,14 @@ import { mockAuthenticatedUser } from '../common/model/authenticated_user.mock';
function createSetupMock() {
return {
authc: authenticationMock.createSetup(),
authz: authorizationMock.createStart(),
license: licenseMock.create(),
};
}
function createStartMock() {
return {
authc: authenticationMock.createStart(),
authz: authorizationMock.createStart(),
navControlService: navControlServiceMock.createStart(),
userProfiles: {
getCurrent: jest.fn(),

View file

@ -36,6 +36,7 @@ describe('Security Plugin', () => {
)
).toEqual({
authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) },
authz: { isRoleManagementEnabled: expect.any(Function) },
license: {
isLicenseAvailable: expect.any(Function),
isEnabled: expect.any(Function),
@ -75,6 +76,7 @@ describe('Security Plugin', () => {
management: managementSetupMock,
fatalErrors: coreSetupMock.fatalErrors,
getStartServices: coreSetupMock.getStartServices,
buildFlavor: expect.stringMatching(new RegExp('^serverless|traditional$')),
});
});
@ -110,6 +112,9 @@ describe('Security Plugin', () => {
"areAPIKeysEnabled": [Function],
"getCurrentUser": [Function],
},
"authz": Object {
"isRoleManagementEnabled": [Function],
},
"navControlService": Object {
"addUserMenuLinks": [Function],
"getUserMenuLinks$": [Function],

View file

@ -6,6 +6,7 @@
*/
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { BuildFlavor } from '@kbn/config';
import type {
CoreSetup,
CoreStart,
@ -22,6 +23,8 @@ import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/pu
import type {
AuthenticationServiceSetup,
AuthenticationServiceStart,
AuthorizationServiceSetup,
AuthorizationServiceStart,
SecurityPluginSetup,
SecurityPluginStart as SecurityPluginStartWithoutDeprecatedMembers,
} from '@kbn/security-plugin-types-public';
@ -32,6 +35,7 @@ import { accountManagementApp, UserProfileAPIClient } from './account_management
import { AnalyticsService } from './analytics';
import { AnonymousAccessService } from './anonymous_access';
import { AuthenticationService } from './authentication';
import { AuthorizationService } from './authorization';
import { buildSecurityApi } from './build_security_api';
import type { SecurityApiClients } from './components';
import type { ConfigType } from './config';
@ -72,6 +76,7 @@ export class SecurityPlugin
private readonly config: ConfigType;
private sessionTimeout?: SessionTimeout;
private readonly authenticationService = new AuthenticationService();
private readonly authorizationService = new AuthorizationService();
private readonly navControlService;
private readonly securityLicenseService = new SecurityLicenseService();
private readonly managementService: ManagementService;
@ -79,16 +84,17 @@ export class SecurityPlugin
private readonly anonymousAccessService = new AnonymousAccessService();
private readonly analyticsService = new AnalyticsService();
private authc!: AuthenticationServiceSetup;
private authz!: AuthorizationServiceSetup;
private securityApiClients!: SecurityApiClients;
private buildFlavor: BuildFlavor;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.buildFlavor = initializerContext.env.packageInfo.buildFlavor;
this.config = this.initializerContext.config.get<ConfigType>();
this.securityCheckupService = new SecurityCheckupService(this.config, localStorage);
this.navControlService = new SecurityNavControlService(
initializerContext.env.packageInfo.buildFlavor
);
this.managementService = new ManagementService(
this.initializerContext.config.get<ConfigType>()
);
this.navControlService = new SecurityNavControlService(this.buildFlavor);
this.managementService = new ManagementService(this.config);
}
public setup(
@ -107,6 +113,10 @@ export class SecurityPlugin
http: core.http,
});
this.authz = this.authorizationService.setup({
config: this.config,
});
this.securityApiClients = {
userProfiles: new UserProfileAPIClient(core.http),
users: new UserAPIClient(core.http),
@ -142,6 +152,7 @@ export class SecurityPlugin
authc: this.authc,
fatalErrors: core.fatalErrors,
getStartServices: core.getStartServices,
buildFlavor: this.buildFlavor,
});
}
@ -168,6 +179,7 @@ export class SecurityPlugin
return {
authc: this.authc,
authz: this.authz,
license,
};
}
@ -205,6 +217,7 @@ export class SecurityPlugin
uiApi: getUiApi({ core }),
navControlService: this.navControlService.start({ core, authc: this.authc }),
authc: this.authc as AuthenticationServiceStart,
authz: this.authz as AuthorizationServiceStart,
userProfiles: {
getCurrent: this.securityApiClients.userProfiles.getCurrent.bind(
this.securityApiClients.userProfiles

View file

@ -24,27 +24,31 @@ export const enableManagementCardsLanding = (services: Services) => {
const { management, application } = services;
services.getProjectNavLinks$().subscribe((projectNavLinks) => {
const extendCardNavDefinitions = projectNavLinks.reduce<
Record<string, CardNavExtensionDefinition>
>((acc, projectNavLink) => {
if (SecurityManagementCards.has(projectNavLink.id)) {
const { appId, deepLinkId, path } = getNavigationPropsFromId(projectNavLink.id);
const cardNavDefinitions = projectNavLinks.reduce<Record<string, CardNavExtensionDefinition>>(
(acc, projectNavLink) => {
if (SecurityManagementCards.has(projectNavLink.id)) {
const { appId, deepLinkId, path } = getNavigationPropsFromId(projectNavLink.id);
acc[projectNavLink.id] = {
category: SecurityManagementCards.get(projectNavLink.id) ?? 'other',
title: projectNavLink.title,
description: projectNavLink.description ?? '',
icon: projectNavLink.landingIcon ?? '',
href: application.getUrlForApp(appId, { deepLinkId, path }),
skipValidation: true,
};
}
return acc;
}, {});
acc[projectNavLink.id] = {
category: SecurityManagementCards.get(projectNavLink.id) ?? 'other',
title: projectNavLink.title,
description: projectNavLink.description ?? '',
icon: projectNavLink.landingIcon ?? '',
href: application.getUrlForApp(appId, { deepLinkId, path }),
skipValidation: true,
};
}
return acc;
},
{}
);
management.setupCardsNavigation({
enabled: true,
extendCardNavDefinitions,
extendCardNavDefinitions: services.serverless.getNavigationCards(
services.security.authz.isRoleManagementEnabled(),
cardNavDefinitions
),
});
});
};

View file

@ -46,6 +46,6 @@
"@kbn/actions-plugin",
"@kbn/management-cards-navigation",
"@kbn/core-application-browser",
"@kbn/discover-plugin"
"@kbn/discover-plugin",
]
}

View file

@ -12,6 +12,7 @@ const startMock = (): ServerlessPluginStart => ({
setBreadcrumbs: jest.fn(),
setProjectHome: jest.fn(),
setSideNavComponentDeprecated: jest.fn(),
getNavigationCards: jest.fn(),
});
export const serverlessMock = {

View file

@ -16,3 +16,5 @@ export const SideNavComponent: FC<NavigationProps> = (props) => (
<SideNavComponentLazy {...props} />
</Suspense>
);
export { manageOrgMembersNavCardName, generateManageOrgMembersNavCard } from './nav_cards';

View file

@ -0,0 +1,26 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { appCategories, CardNavExtensionDefinition } from '@kbn/management-cards-navigation';
export const manageOrgMembersNavCardName = 'organization_members';
export function generateManageOrgMembersNavCard(cloudOrgUrl?: string): CardNavExtensionDefinition {
return {
category: appCategories.ACCESS,
description: i18n.translate('xpack.serverless.nav.manageOrgMembersDescription', {
defaultMessage: 'Invite team members and assign them roles to access this project.',
}),
icon: 'users',
skipValidation: true,
href: cloudOrgUrl ?? '',
title: i18n.translate('xpack.serverless.nav.manageOrgMembersTitle', {
defaultMessage: 'Manage organization members',
}),
};
}

View file

@ -15,7 +15,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { API_SWITCH_PROJECT as projectChangeAPIUrl } from '../common';
import { ServerlessConfig } from './config';
import { SideNavComponent } from './navigation';
import {
generateManageOrgMembersNavCard,
manageOrgMembersNavCardName,
SideNavComponent,
} from './navigation';
import {
ServerlessPluginSetup,
ServerlessPluginSetupDependencies,
@ -95,6 +99,16 @@ export class ServerlessPlugin
},
setBreadcrumbs: (breadcrumbs, params) => project.setBreadcrumbs(breadcrumbs, params),
setProjectHome: (homeHref: string) => project.setHome(homeHref),
getNavigationCards: (roleManagementEnabled, extendCardNavDefinitions) => {
if (!roleManagementEnabled) return extendCardNavDefinitions;
const manageOrgMembersNavCard = generateManageOrgMembersNavCard(cloud.organizationUrl);
if (extendCardNavDefinitions) {
extendCardNavDefinitions[manageOrgMembersNavCardName] = manageOrgMembersNavCard;
return extendCardNavDefinitions;
}
return { [manageOrgMembersNavCardName]: manageOrgMembersNavCard };
},
};
}

View file

@ -14,6 +14,7 @@ import type {
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { Observable } from 'rxjs';
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import { CardNavExtensionDefinition } from '@kbn/management-cards-navigation';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ServerlessPluginSetup {}
@ -35,6 +36,10 @@ export interface ServerlessPluginStart {
* @deprecated Use {@link ServerlessPluginStart.initNavigation} instead.
*/
setSideNavComponentDeprecated: (navigation: SideNavComponent) => void;
getNavigationCards(
roleManagementEnabled?: boolean,
extendCardNavDefinitions?: Record<string, CardNavExtensionDefinition>
): Record<string, CardNavExtensionDefinition> | undefined;
}
export interface ServerlessPluginSetupDependencies {

View file

@ -26,5 +26,7 @@
"@kbn/cloud-plugin",
"@kbn/serverless-common-settings",
"@kbn/shared-ux-chrome-navigation",
"@kbn/i18n",
"@kbn/management-cards-navigation",
]
}

View file

@ -19,6 +19,7 @@
"observabilityShared",
"management",
"discover",
"security",
],
"optionalPlugins": [],
"requiredBundles": []

View file

@ -7,8 +7,7 @@
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { appIds } from '@kbn/management-cards-navigation';
import { appCategories } from '@kbn/management-cards-navigation/src/types';
import { appCategories, appIds } from '@kbn/management-cards-navigation';
import { of } from 'rxjs';
import { navigationTree } from './navigation_tree';
import { createObservabilityDashboardRegistration } from './logs_signal/overview_registration';
@ -52,16 +51,15 @@ export class ServerlessObservabilityPlugin
core: CoreStart,
setupDeps: ServerlessObservabilityPublicStartDependencies
): ServerlessObservabilityPublicStart {
const { serverless, management } = setupDeps;
const { serverless, management, security } = setupDeps;
const navigationTree$ = of(navigationTree);
serverless.setProjectHome('/app/observability/landing');
serverless.initNavigation(navigationTree$, { dataTestSubj: 'svlObservabilitySideNav' });
management.setupCardsNavigation({
enabled: true,
hideLinksTo: [appIds.RULES],
extendCardNavDefinitions: {
const extendCardNavDefinitions = serverless.getNavigationCards(
security.authz.isRoleManagementEnabled(),
{
aiAssistantManagementObservability: {
category: appCategories.OTHER,
title: i18n.translate('xpack.serverlessObservability.aiAssistantManagementTitle', {
@ -75,8 +73,14 @@ export class ServerlessObservabilityPlugin
),
icon: 'sparkles',
},
},
}
);
management.setupCardsNavigation({
enabled: true,
hideLinksTo: [appIds.RULES],
extendCardNavDefinitions,
});
return {};
}

View file

@ -13,6 +13,7 @@ import {
ObservabilitySharedPluginSetup,
ObservabilitySharedPluginStart,
} from '@kbn/observability-shared-plugin/public';
import { SecurityPluginStart } from '@kbn/security-plugin/public';
import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@ -34,4 +35,5 @@ export interface ServerlessObservabilityPublicStartDependencies {
serverless: ServerlessPluginStart;
management: ManagementStart;
data: DataPublicPluginStart;
security: SecurityPluginStart;
}

View file

@ -28,5 +28,6 @@
"@kbn/serverless-observability-settings",
"@kbn/core-chrome-browser",
"@kbn/discover-plugin",
"@kbn/security-plugin",
]
}

View file

@ -121,16 +121,22 @@ export class ServerlessSearchPlugin
core: CoreStart,
services: ServerlessSearchPluginStartDependencies
): ServerlessSearchPluginStart {
const { serverless, management, indexManagement } = services;
const { serverless, management, indexManagement, security } = services;
serverless.setProjectHome('/app/elasticsearch');
const navigationTree$ = of(navigationTree);
serverless.initNavigation(navigationTree$, { dataTestSubj: 'svlSearchSideNav' });
const extendCardNavDefinitions = serverless.getNavigationCards(
security.authz.isRoleManagementEnabled()
);
management.setupCardsNavigation({
enabled: true,
hideLinksTo: [appIds.MAINTENANCE_WINDOWS],
extendCardNavDefinitions,
});
indexManagement?.extensionsService.setIndexMappingsContent(createIndexMappingsContent(core));
indexManagement?.extensionsService.addIndexDetailsTab(
createIndexDocumentsContent(core, services)

View file

@ -11,16 +11,37 @@ export function SvlManagementPageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
// API keys card
async assertApiKeysManagementCardExists() {
await testSubjects.existOrFail('app-card-api_keys');
},
async assertApiKeysManagementCardDoesNotExist() {
await testSubjects.missingOrFail('app-card-api_keys');
},
async clickApiKeysManagementCard() {
await testSubjects.click('app-card-api_keys');
},
// Roles card
async assertRoleManagementCardExists() {
await testSubjects.existOrFail('app-card-roles');
},
async assertRoleManagementCardDoesNotExist() {
await testSubjects.missingOrFail('app-card-roles');
},
async clickRoleManagementCard() {
await testSubjects.click('app-card-roles');
},
// Organization members card
async assertOrgMembersManagementCardExists() {
await testSubjects.existOrFail('app-card-organization_members');
},
async assertOrgMembersManagementCardDoesNotExist() {
await testSubjects.missingOrFail('app-card-organization_members');
},
async clickOrgMembersManagementCard() {
await testSubjects.click('app-card-organization_members');
},
};
}

View file

@ -18,6 +18,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
this.tags('smoke');
before(async () => {
await pageObjects.svlCommonPage.loginAsAdmin();
});
beforeEach(async () => {
await pageObjects.common.navigateToApp('management');
});
@ -37,12 +40,29 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});
it('navigates to API keys management by clicking the card', async () => {
await testSubjects.click('app-card-api_keys');
expect(async () => {
await pageObjects.common.waitUntilUrlIncludes('/app/management/security/api_keys');
}).not.to.throwError();
});
describe('Roles management card', () => {
it('should not be displayed by default', async () => {
await pageObjects.common.navigateToApp('management');
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
await pageObjects.svlManagementPage.assertRoleManagementCardDoesNotExist();
});
});
describe('Organization members management card', () => {
it('should not be displayed by default', async () => {
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
await pageObjects.svlManagementPage.assertOrgMembersManagementCardDoesNotExist();
});
});
});
};

View file

@ -0,0 +1,123 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['svlCommonPage', 'common', 'svlManagementPage']);
const browser = getService('browser');
const retry = getService('retry');
describe('Management navigation cards', function () {
this.tags('smoke');
describe('as Admin', function () {
before(async () => {
await pageObjects.svlCommonPage.loginAsAdmin();
});
beforeEach(async () => {
await pageObjects.common.navigateToApp('management');
});
it('renders the page', async () => {
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
const url = await browser.getCurrentUrl();
expect(url).to.contain(`/management`);
});
it('displays the API keys management card, and will navigate to the API keys UI', async () => {
await pageObjects.svlManagementPage.assertApiKeysManagementCardExists();
await pageObjects.svlManagementPage.clickApiKeysManagementCard();
const url = await browser.getCurrentUrl();
expect(url).to.contain('/management/security/api_keys');
});
it('displays the roles management card, and will navigate to the Roles UI', async () => {
await pageObjects.svlManagementPage.assertRoleManagementCardExists();
await pageObjects.svlManagementPage.clickRoleManagementCard();
const url = await browser.getCurrentUrl();
expect(url).to.contain('/management/security/roles');
});
it('displays the Organization members management card, and will navigate to the cloud organization URL', async () => {
await pageObjects.svlManagementPage.assertOrgMembersManagementCardExists();
await pageObjects.svlManagementPage.clickOrgMembersManagementCard();
const url = await browser.getCurrentUrl();
// `--xpack.cloud.organization_url: '/account/members'`,
expect(url).to.contain('/account/members');
});
});
describe('as viewer', function () {
before(async () => {
await pageObjects.svlCommonPage.loginWithRole('viewer');
});
beforeEach(async () => {
await pageObjects.common.navigateToApp('management');
});
it('renders the page', async () => {
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
const url = await browser.getCurrentUrl();
expect(url).to.contain(`/management`);
});
it('should not display the roles manangement card', async () => {
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
await pageObjects.svlManagementPage.assertRoleManagementCardDoesNotExist();
});
it('displays the organization members management card, and will navigate to the cloud organization URL', async () => {
// The org members nav card is always visible because there is no way to check if a user has approprite privileges
await pageObjects.svlManagementPage.assertOrgMembersManagementCardExists();
await pageObjects.svlManagementPage.clickOrgMembersManagementCard();
const url = await browser.getCurrentUrl();
// `--xpack.cloud.organization_url: '/account/members'`,
expect(url).to.contain('/account/members');
});
describe('API keys management card - search solution', function () {
this.tags(['skipSvlOblt', 'skipSvlSec']);
it('displays the API keys management card, and will navigate to the API keys UI (search only)', async () => {
await pageObjects.svlManagementPage.assertApiKeysManagementCardExists();
await pageObjects.svlManagementPage.clickApiKeysManagementCard();
const url = await browser.getCurrentUrl();
expect(url).to.contain('/management/security/api_keys');
});
});
describe('API keys management card - oblt & sec solutions', function () {
this.tags(['skipSvlSearch']);
it('should not display the API keys manangement card (oblt & security only)', async () => {
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
await pageObjects.svlManagementPage.assertApiKeysManagementCardDoesNotExist();
});
});
});
});
};

View file

@ -1,41 +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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['svlCommonPage', 'common', 'svlManagementPage']);
const browser = getService('browser');
const retry = getService('retry');
describe('Roles management card', function () {
this.tags('smoke');
before(async () => {
// Navigate to the index management page
await pageObjects.svlCommonPage.loginAsAdmin();
await pageObjects.common.navigateToApp('management');
});
it('renders the page, displays the Roles card, and will navigate to the Roles UI', async () => {
await retry.waitFor('page to be visible', async () => {
return await testSubjects.exists('cards-navigation-page');
});
let url = await browser.getCurrentUrl();
expect(url).to.contain(`/management`);
await pageObjects.svlManagementPage.assertRoleManagementCardExists();
await pageObjects.svlManagementPage.clickRoleManagementCard();
url = await browser.getCurrentUrl();
expect(url).to.contain('/management/security/roles');
});
});
};

View file

@ -22,6 +22,8 @@ export default createTestConfig({
'--xpack.infra.enabled=true',
'--xpack.infra.featureFlags.customThresholdAlertsEnabled=true',
'--xpack.security.roleManagementEnabled=true',
`--xpack.cloud.base_url='https://cloud.elastic.co'`,
`--xpack.cloud.organization_url='/account/members'`,
],
// load tests in the index file
testFiles: [require.resolve('./index.feature_flags.ts')],

View file

@ -11,6 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless observability UI - feature flags', function () {
// add tests that require feature flags, defined in config.feature_flags.ts
loadTestFile(require.resolve('./infra'));
loadTestFile(require.resolve('../common/platform_security/roles_management_card.ts'));
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
});
}

View file

@ -18,7 +18,11 @@ export default createTestConfig({
},
suiteTags: { exclude: ['skipSvlSearch'] },
// add feature flags
kbnServerArgs: ['--xpack.security.roleManagementEnabled=true'],
kbnServerArgs: [
`--xpack.security.roleManagementEnabled=true`,
`--xpack.cloud.base_url='https://cloud.elastic.co'`,
`--xpack.cloud.organization_url='/account/members'`,
],
// load tests in the index file
testFiles: [require.resolve('./index.feature_flags.ts')],

View file

@ -10,6 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless search UI - feature flags', function () {
// add tests that require feature flags, defined in config.feature_flags.ts
loadTestFile(require.resolve('../common/platform_security/roles_management_card.ts'));
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
});
}

View file

@ -18,7 +18,11 @@ export default createTestConfig({
},
suiteTags: { exclude: ['skipSvlSec'] },
// add feature flags
kbnServerArgs: ['--xpack.security.roleManagementEnabled=true'],
kbnServerArgs: [
`--xpack.security.roleManagementEnabled=true`,
`--xpack.cloud.base_url='https://cloud.elastic.co'`,
`--xpack.cloud.organization_url='/account/members'`,
],
// load tests in the index file
testFiles: [require.resolve('./index.feature_flags.ts')],

View file

@ -10,6 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless security UI - feature flags', function () {
// add tests that require feature flags, defined in config.feature_flags.ts
loadTestFile(require.resolve('../common/platform_security/roles_management_card.ts'));
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
});
}