mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Fleet] Test new privileges system via cypress (#124797)
* Tests new roles introduced with superuser removal * Use login and roles utilities from security-solution cypress library * Add some more tests * expand tests * Fix failing test * Fix linter check
This commit is contained in:
parent
b2b60ff061
commit
ceb14e6842
17 changed files with 847 additions and 53 deletions
|
@ -5,47 +5,17 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AGENTS_TAB, AGENT_POLICIES_TAB, ENROLLMENT_TOKENS_TAB } from '../screens/fleet';
|
||||
import {
|
||||
AGENTS_TAB,
|
||||
ADD_AGENT_BUTTON_TOP,
|
||||
AGENT_FLYOUT_CLOSE_BUTTON,
|
||||
STANDALONE_TAB,
|
||||
} from '../screens/fleet';
|
||||
import { cleanupAgentPolicies, unenrollAgent } from '../tasks/cleanup';
|
||||
import { verifyPolicy, verifyAgentPackage, navigateToTab } from '../tasks/fleet';
|
||||
import { FLEET, navigateTo } from '../tasks/navigation';
|
||||
|
||||
describe('Fleet startup', () => {
|
||||
function navigateToTab(tab: string) {
|
||||
cy.getBySel(tab).click();
|
||||
cy.get('.euiBasicTable-loading').should('not.exist');
|
||||
}
|
||||
|
||||
function navigateToAgentPolicy(name: string) {
|
||||
cy.get('.euiLink').contains(name).click();
|
||||
cy.get('.euiLoadingSpinner').should('not.exist');
|
||||
}
|
||||
|
||||
function navigateToEnrollmentTokens() {
|
||||
cy.getBySel(ENROLLMENT_TOKENS_TAB).click();
|
||||
cy.get('.euiBasicTable-loading').should('not.exist');
|
||||
cy.get('.euiButtonIcon--danger'); // wait for trash icon
|
||||
}
|
||||
|
||||
function verifyPolicy(name: string, integrations: string[]) {
|
||||
navigateToTab(AGENT_POLICIES_TAB);
|
||||
|
||||
navigateToAgentPolicy(name);
|
||||
integrations.forEach((integration) => {
|
||||
cy.get('.euiLink').contains(integration);
|
||||
});
|
||||
|
||||
cy.get('.euiButtonEmpty').contains('View all agent policies').click();
|
||||
|
||||
navigateToEnrollmentTokens();
|
||||
|
||||
cy.get('.euiTableCellContent').contains(name);
|
||||
}
|
||||
|
||||
function verifyAgentPackage() {
|
||||
cy.visit('/app/integrations/installed');
|
||||
cy.getBySel('integration-card:epr:elastic_agent');
|
||||
}
|
||||
|
||||
// skipping Fleet Server enroll, to enable, comment out runner.ts line 23
|
||||
describe.skip('Fleet Server', () => {
|
||||
it('should display Add agent button and Healthy agent once Fleet Agent page loaded', () => {
|
||||
|
@ -77,8 +47,8 @@ describe('Fleet startup', () => {
|
|||
});
|
||||
|
||||
it('should create agent policy', () => {
|
||||
cy.getBySel('addAgentBtnTop').click();
|
||||
cy.getBySel('standaloneTab').click();
|
||||
cy.getBySel(ADD_AGENT_BUTTON_TOP).click();
|
||||
cy.getBySel(STANDALONE_TAB).click();
|
||||
|
||||
cy.intercept('POST', '/api/fleet/agent_policies?sys_monitoring=true').as('createAgentPolicy');
|
||||
|
||||
|
@ -97,7 +67,7 @@ describe('Fleet startup', () => {
|
|||
// verify agent.yml code block has new policy id
|
||||
cy.get('.euiCodeBlock__code').contains(`id: ${agentPolicyId}`);
|
||||
|
||||
cy.getBySel('euiFlyoutCloseButton').click();
|
||||
cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click();
|
||||
|
||||
// verify policy is created and has system package
|
||||
verifyPolicy('Agent policy 1', ['System']);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { navigateTo } from '../tasks/navigation';
|
||||
import { UPDATE_PACKAGE_BTN } from '../screens/integrations';
|
||||
import { AGENT_POLICY_SAVE_INTEGRATION } from '../screens/fleet';
|
||||
|
||||
describe('Add Integration - Mock API', () => {
|
||||
describe('upgrade package and upgrade package policy', () => {
|
||||
|
@ -141,7 +142,7 @@ describe('Add Integration - Mock API', () => {
|
|||
);
|
||||
|
||||
cy.getBySel('toastCloseButton').click();
|
||||
cy.getBySel('saveIntegration').click();
|
||||
cy.getBySel(AGENT_POLICY_SAVE_INTEGRATION).click();
|
||||
|
||||
cy.wait('@updateApachePolicy').then((interception) => {
|
||||
expect(interception.request.body.package.version).to.equal(newVersion);
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
SETTINGS_TAB,
|
||||
UPDATE_PACKAGE_BTN,
|
||||
} from '../screens/integrations';
|
||||
import { ADD_PACKAGE_POLICY_BTN } from '../screens/fleet';
|
||||
import { cleanupAgentPolicies } from '../tasks/cleanup';
|
||||
|
||||
describe('Add Integration - Real API', () => {
|
||||
|
@ -75,7 +76,7 @@ describe('Add Integration - Real API', () => {
|
|||
|
||||
cy.visit(`/app/fleet/policies/${agentPolicyId}`);
|
||||
cy.intercept('GET', '/api/fleet/epm/packages?*').as('packages');
|
||||
cy.getBySel('addPackagePolicyButton').click();
|
||||
cy.getBySel(ADD_PACKAGE_POLICY_BTN).click();
|
||||
cy.wait('@packages');
|
||||
cy.get('.euiLoadingSpinner').should('not.exist');
|
||||
cy.get('input[placeholder="Search for integrations"]').type('Apache');
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { FLEET } from '../tasks/navigation';
|
||||
import {
|
||||
createUsersAndRoles,
|
||||
FleetAllIntegrNoneRole,
|
||||
FleetAllIntegrNoneUser,
|
||||
deleteUsersAndRoles,
|
||||
} from '../tasks/privileges';
|
||||
import { loginWithUserAndWaitForPage, logout } from '../tasks/login';
|
||||
|
||||
import { MISSING_PRIVILEGES_TITLE, MISSING_PRIVILEGES_MESSAGE } from '../screens/fleet';
|
||||
const rolesToCreate = [FleetAllIntegrNoneRole];
|
||||
const usersToCreate = [FleetAllIntegrNoneUser];
|
||||
|
||||
describe('When the user has All privilege for Fleet but None for integrations', () => {
|
||||
before(() => {
|
||||
createUsersAndRoles(usersToCreate, rolesToCreate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logout();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
deleteUsersAndRoles(usersToCreate, rolesToCreate);
|
||||
});
|
||||
|
||||
it('Fleet access is blocked with a callout', () => {
|
||||
loginWithUserAndWaitForPage(FLEET, FleetAllIntegrNoneUser);
|
||||
cy.getBySel(MISSING_PRIVILEGES_TITLE).should('have.text', 'Permission denied');
|
||||
cy.getBySel(MISSING_PRIVILEGES_MESSAGE).should(
|
||||
'contain',
|
||||
'You are not authorized to access Fleet.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 { FLEET, INTEGRATIONS, navigateTo } from '../tasks/navigation';
|
||||
import {
|
||||
createUsersAndRoles,
|
||||
FleetAllIntegrReadRole,
|
||||
FleetAllIntegrReadUser,
|
||||
deleteUsersAndRoles,
|
||||
} from '../tasks/privileges';
|
||||
import { loginWithUserAndWaitForPage, logout } from '../tasks/login';
|
||||
import { navigateToTab, createAgentPolicy } from '../tasks/fleet';
|
||||
import { cleanupAgentPolicies, unenrollAgent } from '../tasks/cleanup';
|
||||
|
||||
import {
|
||||
FLEET_SERVER_MISSING_PRIVILEGES_TITLE,
|
||||
FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE,
|
||||
ADD_AGENT_BUTTON_TOP,
|
||||
AGENT_POLICIES_TAB,
|
||||
AGENT_POLICY_SAVE_INTEGRATION,
|
||||
ADD_PACKAGE_POLICY_BTN,
|
||||
} from '../screens/fleet';
|
||||
import { ADD_POLICY_BTN, AGENT_POLICY_NAME_LINK } from '../screens/integrations';
|
||||
|
||||
const rolesToCreate = [FleetAllIntegrReadRole];
|
||||
const usersToCreate = [FleetAllIntegrReadUser];
|
||||
|
||||
describe('When the user has All privilege for Fleet but Read for integrations', () => {
|
||||
before(() => {
|
||||
createUsersAndRoles(usersToCreate, rolesToCreate);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
deleteUsersAndRoles(usersToCreate, rolesToCreate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logout();
|
||||
});
|
||||
|
||||
describe('When there are agent policies', () => {
|
||||
before(() => {
|
||||
navigateTo(FLEET);
|
||||
createAgentPolicy();
|
||||
});
|
||||
|
||||
it('Some elements in the UI are not enabled', () => {
|
||||
logout();
|
||||
loginWithUserAndWaitForPage(FLEET, FleetAllIntegrReadUser);
|
||||
navigateToTab(AGENT_POLICIES_TAB);
|
||||
|
||||
cy.getBySel(AGENT_POLICY_NAME_LINK).click();
|
||||
cy.getBySel(ADD_PACKAGE_POLICY_BTN).should('be.disabled');
|
||||
|
||||
cy.get('a[title="system-1"]').click();
|
||||
cy.getBySel(AGENT_POLICY_SAVE_INTEGRATION).should('be.disabled');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
unenrollAgent();
|
||||
cleanupAgentPolicies();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When there are no agent policies', () => {
|
||||
it('If fleet server is not set up, Fleet shows a callout', () => {
|
||||
loginWithUserAndWaitForPage(FLEET, FleetAllIntegrReadUser);
|
||||
cy.getBySel(FLEET_SERVER_MISSING_PRIVILEGES_TITLE).should('have.text', 'Permission denied');
|
||||
cy.getBySel(FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE).should(
|
||||
'contain',
|
||||
'Fleet Server needs to be set up.'
|
||||
);
|
||||
cy.getBySel(ADD_AGENT_BUTTON_TOP).should('not.be.disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integrations', () => {
|
||||
it('are visible but cannot be added', () => {
|
||||
loginWithUserAndWaitForPage(INTEGRATIONS, FleetAllIntegrReadUser);
|
||||
cy.getBySel('integration-card:epr:apache').click();
|
||||
cy.getBySel(ADD_POLICY_BTN).should('be.disabled');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { INTEGRATIONS } from '../tasks/navigation';
|
||||
import {
|
||||
createUsersAndRoles,
|
||||
FleetNoneIntegrAllRole,
|
||||
FleetNoneIntegrAllUser,
|
||||
deleteUsersAndRoles,
|
||||
} from '../tasks/privileges';
|
||||
import { loginWithUserAndWaitForPage, logout } from '../tasks/login';
|
||||
|
||||
import { ADD_POLICY_BTN } from '../screens/integrations';
|
||||
|
||||
const rolesToCreate = [FleetNoneIntegrAllRole];
|
||||
const usersToCreate = [FleetNoneIntegrAllUser];
|
||||
|
||||
describe('When the user has All privileges for Integrations but None for for Fleet', () => {
|
||||
before(() => {
|
||||
createUsersAndRoles(usersToCreate, rolesToCreate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
logout();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
deleteUsersAndRoles(usersToCreate, rolesToCreate);
|
||||
});
|
||||
|
||||
it('Integrations are visible but cannot be added', () => {
|
||||
loginWithUserAndWaitForPage(INTEGRATIONS, FleetNoneIntegrAllUser);
|
||||
cy.getBySel('integration-card:epr:apache').click();
|
||||
cy.getBySel(ADD_POLICY_BTN).should('be.disabled');
|
||||
});
|
||||
});
|
|
@ -6,8 +6,19 @@
|
|||
*/
|
||||
|
||||
export const ADD_AGENT_BUTTON = 'addAgentButton';
|
||||
export const ADD_AGENT_BUTTON_TOP = 'addAgentBtnTop';
|
||||
export const CREATE_POLICY_BUTTON = 'createPolicyBtn';
|
||||
export const AGENT_FLYOUT_CLOSE_BUTTON = 'euiFlyoutCloseButton';
|
||||
|
||||
export const AGENTS_TAB = 'fleet-agents-tab';
|
||||
export const AGENT_POLICIES_TAB = 'fleet-agent-policies-tab';
|
||||
export const ENROLLMENT_TOKENS_TAB = 'fleet-enrollment-tokens-tab';
|
||||
export const SETTINGS_TAB = 'fleet-settings-tab';
|
||||
export const STANDALONE_TAB = 'standaloneTab';
|
||||
export const MISSING_PRIVILEGES_TITLE = 'missingPrivilegesPromptTitle';
|
||||
export const MISSING_PRIVILEGES_MESSAGE = 'missingPrivilegesPromptMessage';
|
||||
export const FLEET_SERVER_MISSING_PRIVILEGES_MESSAGE = 'fleetServerMissingPrivilegesMessage';
|
||||
export const FLEET_SERVER_MISSING_PRIVILEGES_TITLE = 'fleetServerMissingPrivilegesTitle';
|
||||
export const AGENT_POLICY_SAVE_INTEGRATION = 'saveIntegration';
|
||||
export const PACKAGE_POLICY_TABLE_LINK = 'PackagePoliciesTableLink';
|
||||
export const ADD_PACKAGE_POLICY_BTN = 'addPackagePolicyButton';
|
||||
|
|
|
@ -11,6 +11,7 @@ export const INTEGRATIONS_CARD = '.euiCard__titleAnchor';
|
|||
|
||||
export const INTEGRATION_NAME_LINK = 'integrationNameLink';
|
||||
export const AGENT_POLICY_NAME_LINK = 'agentPolicyNameLink';
|
||||
export const AGENT_ACTIONS_BTN = 'agentActionsBtn';
|
||||
|
||||
export const CONFIRM_MODAL_BTN = 'confirmModalConfirmButton';
|
||||
export const CONFIRM_MODAL_BTN_SEL = `[data-test-subj=${CONFIRM_MODAL_BTN}]`;
|
||||
|
@ -19,6 +20,7 @@ export const FLYOUT_CLOSE_BTN_SEL = '[data-test-subj="euiFlyoutCloseButton"]';
|
|||
|
||||
export const SETTINGS_TAB = 'tab-settings';
|
||||
export const POLICIES_TAB = 'tab-policies';
|
||||
export const ADVANCED_TAB = 'tab-custom';
|
||||
|
||||
export const UPDATE_PACKAGE_BTN = 'updatePackageBtn';
|
||||
export const LATEST_VERSION = 'latestVersion';
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const TOGGLE_NAVIGATION_BTN = '[data-test-subj="toggleNavButton"]';
|
||||
export const TOGGLE_NAVIGATION_BTN = 'toggleNavButton';
|
||||
export const NAV_APP_LINK = 'collapsibleNavAppLink';
|
||||
|
|
59
x-pack/plugins/fleet/cypress/tasks/fleet.ts
Normal file
59
x-pack/plugins/fleet/cypress/tasks/fleet.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 {
|
||||
AGENT_POLICIES_TAB,
|
||||
ENROLLMENT_TOKENS_TAB,
|
||||
ADD_AGENT_BUTTON_TOP,
|
||||
CREATE_POLICY_BUTTON,
|
||||
AGENT_FLYOUT_CLOSE_BUTTON,
|
||||
STANDALONE_TAB,
|
||||
} from '../screens/fleet';
|
||||
|
||||
export function createAgentPolicy() {
|
||||
cy.getBySel(ADD_AGENT_BUTTON_TOP).click();
|
||||
cy.getBySel(STANDALONE_TAB).click();
|
||||
cy.getBySel(CREATE_POLICY_BUTTON).click();
|
||||
cy.getBySel('agentPolicyCreateStatusCallOut').contains('Agent policy created');
|
||||
cy.getBySel(AGENT_FLYOUT_CLOSE_BUTTON).click();
|
||||
}
|
||||
|
||||
export function navigateToTab(tab: string) {
|
||||
cy.getBySel(tab).click();
|
||||
cy.get('.euiBasicTable-loading').should('not.exist');
|
||||
}
|
||||
|
||||
export function navigateToAgentPolicy(name: string) {
|
||||
cy.get('.euiLink').contains(name).click();
|
||||
cy.get('.euiLoadingSpinner').should('not.exist');
|
||||
}
|
||||
|
||||
export function navigateToEnrollmentTokens() {
|
||||
cy.getBySel(ENROLLMENT_TOKENS_TAB).click();
|
||||
cy.get('.euiBasicTable-loading').should('not.exist');
|
||||
cy.get('.euiButtonIcon--danger'); // wait for trash icon
|
||||
}
|
||||
|
||||
export function verifyPolicy(name: string, integrations: string[]) {
|
||||
navigateToTab(AGENT_POLICIES_TAB);
|
||||
|
||||
navigateToAgentPolicy(name);
|
||||
integrations.forEach((integration) => {
|
||||
cy.get('.euiLink').contains(integration);
|
||||
});
|
||||
|
||||
cy.get('.euiButtonEmpty').contains('View all agent policies').click();
|
||||
|
||||
navigateToEnrollmentTokens();
|
||||
|
||||
cy.get('.euiTableCellContent').contains(name);
|
||||
}
|
||||
|
||||
export function verifyAgentPackage() {
|
||||
cy.visit('/app/integrations/installed');
|
||||
cy.getBySel('integration-card:epr:elastic_agent');
|
||||
}
|
341
x-pack/plugins/fleet/cypress/tasks/login.ts
Normal file
341
x-pack/plugins/fleet/cypress/tasks/login.ts
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
* 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 Url from 'url';
|
||||
import type { UrlObject } from 'url';
|
||||
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
import type { ROLES } from './privileges';
|
||||
import { hostDetailsUrl, LOGOUT_URL } from './navigation';
|
||||
|
||||
/**
|
||||
* Credentials in the `kibana.dev.yml` config file will be used to authenticate
|
||||
* with Kibana when credentials are not provided via environment variables
|
||||
*/
|
||||
const KIBANA_DEV_YML_PATH = '../../../config/kibana.dev.yml';
|
||||
|
||||
/**
|
||||
* The configuration path in `kibana.dev.yml` to the username to be used when
|
||||
* authenticating with Kibana.
|
||||
*/
|
||||
const ELASTICSEARCH_USERNAME_CONFIG_PATH = 'config.elasticsearch.username';
|
||||
|
||||
/**
|
||||
* The configuration path in `kibana.dev.yml` to the password to be used when
|
||||
* authenticating with Kibana.
|
||||
*/
|
||||
const ELASTICSEARCH_PASSWORD_CONFIG_PATH = 'config.elasticsearch.password';
|
||||
|
||||
/**
|
||||
* The `CYPRESS_ELASTICSEARCH_USERNAME` environment variable specifies the
|
||||
* username to be used when authenticating with Kibana
|
||||
*/
|
||||
const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME';
|
||||
|
||||
/**
|
||||
* The `CYPRESS_ELASTICSEARCH_PASSWORD` environment variable specifies the
|
||||
* username to be used when authenticating with Kibana
|
||||
*/
|
||||
const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD';
|
||||
|
||||
/**
|
||||
* The Kibana server endpoint used for authentication
|
||||
*/
|
||||
const LOGIN_API_ENDPOINT = '/internal/security/login';
|
||||
|
||||
/**
|
||||
* cy.visit will default to the baseUrl which uses the default kibana test user
|
||||
* This function will override that functionality in cy.visit by building the baseUrl
|
||||
* directly from the environment variables set up in x-pack/test/security_solution_cypress/runner.ts
|
||||
*
|
||||
* @param role string role/user to log in with
|
||||
* @param route string route to visit
|
||||
*/
|
||||
export const getUrlWithRoute = (role: ROLES, route: string) => {
|
||||
const url = Cypress.config().baseUrl;
|
||||
const kibana = new URL(String(url));
|
||||
const theUrl = `${Url.format({
|
||||
auth: `${role}:changeme`,
|
||||
username: role,
|
||||
password: 'changeme',
|
||||
protocol: kibana.protocol.replace(':', ''),
|
||||
hostname: kibana.hostname,
|
||||
port: kibana.port,
|
||||
} as UrlObject)}${route.startsWith('/') ? '' : '/'}${route}`;
|
||||
cy.log(`origin: ${theUrl}`);
|
||||
return theUrl;
|
||||
};
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URL with basic auth using the passed in user.
|
||||
*
|
||||
* @param user the user information to build the basic auth with
|
||||
* @param route string route to visit
|
||||
*/
|
||||
export const constructUrlWithUser = (user: User, route: string) => {
|
||||
const url = Cypress.config().baseUrl;
|
||||
const kibana = new URL(String(url));
|
||||
const hostname = kibana.hostname;
|
||||
const username = user.username;
|
||||
const password = user.password;
|
||||
const protocol = kibana.protocol.replace(':', '');
|
||||
const port = kibana.port;
|
||||
|
||||
const path = `${route.startsWith('/') ? '' : '/'}${route}`;
|
||||
const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`;
|
||||
const builtUrl = new URL(strUrl);
|
||||
|
||||
cy.log(`origin: ${builtUrl.href}`);
|
||||
return builtUrl.href;
|
||||
};
|
||||
|
||||
export const getCurlScriptEnvVars = () => ({
|
||||
ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'),
|
||||
ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'),
|
||||
ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'),
|
||||
KIBANA_URL: Cypress.config().baseUrl,
|
||||
});
|
||||
|
||||
export const postRoleAndUser = (role: ROLES) => {
|
||||
const env = getCurlScriptEnvVars();
|
||||
const detectionsRoleScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_role.sh`;
|
||||
const detectionsRoleJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_role.json`;
|
||||
const detectionsUserScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/post_detections_user.sh`;
|
||||
const detectionsUserJsonPath = `./server/lib/detection_engine/scripts/roles_users/${role}/detections_user.json`;
|
||||
|
||||
// post the role
|
||||
cy.exec(`bash ${detectionsRoleScriptPath} ${detectionsRoleJsonPath}`, {
|
||||
env,
|
||||
});
|
||||
|
||||
// post the user associated with the role to elasticsearch
|
||||
cy.exec(`bash ${detectionsUserScriptPath} ${detectionsUserJsonPath}`, {
|
||||
env,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRoleAndUser = (role: ROLES) => {
|
||||
const env = getCurlScriptEnvVars();
|
||||
const detectionsUserDeleteScriptPath = `./server/lib/detection_engine/scripts/roles_users/${role}/delete_detections_user.sh`;
|
||||
|
||||
// delete the role
|
||||
cy.exec(`bash ${detectionsUserDeleteScriptPath}`, {
|
||||
env,
|
||||
});
|
||||
};
|
||||
|
||||
export const loginWithUser = (user: User) => {
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: constructUrlWithUser(user, LOGIN_API_ENDPOINT),
|
||||
});
|
||||
};
|
||||
|
||||
export const loginWithRole = async (role: ROLES) => {
|
||||
postRoleAndUser(role);
|
||||
const theUrl = Url.format({
|
||||
auth: `${role}:changeme`,
|
||||
username: role,
|
||||
password: 'changeme',
|
||||
protocol: Cypress.env('protocol'),
|
||||
hostname: Cypress.env('hostname'),
|
||||
port: Cypress.env('configport'),
|
||||
} as UrlObject);
|
||||
cy.log(`origin: ${theUrl}`);
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: role,
|
||||
password: 'changeme',
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: getUrlWithRoute(role, LOGIN_API_ENDPOINT),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana using, if specified, credentials specified by
|
||||
* environment variables. The credentials in `kibana.dev.yml` will be used
|
||||
* for authentication when the environment variables are unset.
|
||||
*
|
||||
* To speed the execution of tests, prefer this non-interactive authentication,
|
||||
* which is faster than authentication via Kibana's interactive login page.
|
||||
*/
|
||||
export const login = (role?: ROLES) => {
|
||||
if (role != null) {
|
||||
loginWithRole(role);
|
||||
} else if (credentialsProvidedByEnvironment()) {
|
||||
loginViaEnvironmentCredentials();
|
||||
} else {
|
||||
loginViaConfig();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns `true` if the credentials used to login to Kibana are provided
|
||||
* via environment variables
|
||||
*/
|
||||
const credentialsProvidedByEnvironment = (): boolean =>
|
||||
Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null;
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana by reading credentials from the
|
||||
* `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD`
|
||||
* environment variables, and POSTing the username and password directly to
|
||||
* Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
|
||||
*/
|
||||
const loginViaEnvironmentCredentials = () => {
|
||||
cy.log(
|
||||
`Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables`
|
||||
);
|
||||
|
||||
// programmatically authenticate without interacting with the Kibana login page
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: Cypress.env(ELASTICSEARCH_USERNAME),
|
||||
password: Cypress.env(ELASTICSEARCH_PASSWORD),
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-env' },
|
||||
method: 'POST',
|
||||
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana by reading credentials from the
|
||||
* `kibana.dev.yml` file and POSTing the username and password directly to
|
||||
* Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
|
||||
*/
|
||||
const loginViaConfig = () => {
|
||||
cy.log(
|
||||
`Authenticating via config credentials \`${ELASTICSEARCH_USERNAME_CONFIG_PATH}\` and \`${ELASTICSEARCH_PASSWORD_CONFIG_PATH}\` from \`${KIBANA_DEV_YML_PATH}\``
|
||||
);
|
||||
|
||||
// read the login details from `kibana.dev.yaml`
|
||||
cy.readFile(KIBANA_DEV_YML_PATH).then((kibanaDevYml) => {
|
||||
const config = yaml.safeLoad(kibanaDevYml);
|
||||
|
||||
// programmatically authenticate without interacting with the Kibana login page
|
||||
cy.request({
|
||||
body: {
|
||||
providerType: 'basic',
|
||||
providerName: 'basic',
|
||||
currentURL: '/',
|
||||
params: {
|
||||
username: config.elasticsearch.username,
|
||||
password: config.elasticsearch.password,
|
||||
},
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the configured auth details that were used to spawn cypress
|
||||
*
|
||||
* @returns the default Elasticsearch username and password for this environment
|
||||
*/
|
||||
export const getEnvAuth = (): User => {
|
||||
if (credentialsProvidedByEnvironment()) {
|
||||
return {
|
||||
username: Cypress.env(ELASTICSEARCH_USERNAME),
|
||||
password: Cypress.env(ELASTICSEARCH_PASSWORD),
|
||||
};
|
||||
} else {
|
||||
let user: User = { username: '', password: '' };
|
||||
cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => {
|
||||
const config = yaml.safeLoad(devYml);
|
||||
user = { username: config.elasticsearch.username, password: config.elasticsearch.password };
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates with Kibana, visits the specified `url`, and waits for the
|
||||
* Kibana global nav to be displayed before continuing
|
||||
*/
|
||||
export const loginAndWaitForPage = (
|
||||
url: string,
|
||||
role?: ROLES,
|
||||
onBeforeLoadCallback?: (win: Cypress.AUTWindow) => void
|
||||
) => {
|
||||
login(role);
|
||||
cy.visit(
|
||||
`${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`,
|
||||
{
|
||||
onBeforeLoad(win) {
|
||||
if (onBeforeLoadCallback) {
|
||||
onBeforeLoadCallback(win);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]');
|
||||
};
|
||||
export const waitForPage = (url: string) => {
|
||||
cy.visit(
|
||||
`${url}?timerange=(global:(linkTo:!(timeline),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1547914976217,fromStr:'2019-01-19T16:22:56.217Z',kind:relative,to:1579537385745,toStr:now)))`
|
||||
);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]');
|
||||
};
|
||||
|
||||
export const loginAndWaitForPageWithoutDateRange = (url: string, role?: ROLES) => {
|
||||
login(role);
|
||||
cy.visit(role ? getUrlWithRoute(role, url) : url);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
|
||||
};
|
||||
|
||||
export const loginWithUserAndWaitForPage = (url: string, user: User) => {
|
||||
loginWithUser(user);
|
||||
cy.visit(constructUrlWithUser(user, url));
|
||||
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
|
||||
};
|
||||
|
||||
export const loginAndWaitForHostDetailsPage = (hostName = 'suricata-iowa') => {
|
||||
loginAndWaitForPage(hostDetailsUrl(hostName));
|
||||
cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist');
|
||||
};
|
||||
|
||||
export const waitForPageWithoutDateRange = (url: string, role?: ROLES) => {
|
||||
cy.visit(role ? getUrlWithRoute(role, url) : url);
|
||||
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
cy.visit(LOGOUT_URL);
|
||||
};
|
|
@ -5,15 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TOGGLE_NAVIGATION_BTN } from '../screens/navigation';
|
||||
|
||||
export const INTEGRATIONS = 'app/integrations#/';
|
||||
export const FLEET = 'app/fleet/';
|
||||
export const LOGIN_API_ENDPOINT = '/internal/security/login';
|
||||
export const LOGOUT_API_ENDPOINT = '/api/security/logout';
|
||||
export const LOGIN_URL = '/login';
|
||||
export const LOGOUT_URL = '/logout';
|
||||
|
||||
export const hostDetailsUrl = (hostName: string) =>
|
||||
`/app/security/hosts/${hostName}/authentications`;
|
||||
|
||||
export const navigateTo = (page: string) => {
|
||||
cy.visit(page);
|
||||
};
|
||||
|
||||
export const openNavigationFlyout = () => {
|
||||
cy.get(TOGGLE_NAVIGATION_BTN).click();
|
||||
};
|
||||
|
|
232
x-pack/plugins/fleet/cypress/tasks/privileges.ts
Normal file
232
x-pack/plugins/fleet/cypress/tasks/privileges.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
/*
|
||||
* 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 { constructUrlWithUser, getEnvAuth } from './login';
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
description?: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface FeaturesPrivileges {
|
||||
[featureId: string]: string[];
|
||||
}
|
||||
|
||||
interface ElasticsearchIndices {
|
||||
names: string[];
|
||||
privileges: string[];
|
||||
}
|
||||
|
||||
interface ElasticSearchPrivilege {
|
||||
cluster?: string[];
|
||||
indices?: ElasticsearchIndices[];
|
||||
}
|
||||
|
||||
interface KibanaPrivilege {
|
||||
spaces: string[];
|
||||
base?: string[];
|
||||
feature?: FeaturesPrivileges;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
name: string;
|
||||
privileges: {
|
||||
elasticsearch?: ElasticSearchPrivilege;
|
||||
kibana?: KibanaPrivilege[];
|
||||
};
|
||||
}
|
||||
|
||||
// Create roles with allowed combinations of Fleet and Integrations
|
||||
export const FleetAllIntegrAllRole: Role = {
|
||||
name: 'fleet_all_int_all_role',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['all'],
|
||||
fleet: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const FleetAllIntegrAllUser: User = {
|
||||
username: 'fleet_all_int_all_user',
|
||||
password: 'password',
|
||||
roles: [FleetAllIntegrAllRole.name],
|
||||
};
|
||||
|
||||
export const FleetAllIntegrReadRole: Role = {
|
||||
name: 'fleet_all_int_read_user',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['all'],
|
||||
fleet: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const FleetAllIntegrReadUser: User = {
|
||||
username: 'fleet_all_int_read_user',
|
||||
password: 'password',
|
||||
roles: [FleetAllIntegrReadRole.name],
|
||||
};
|
||||
export const FleetAllIntegrNoneRole: Role = {
|
||||
name: 'fleet_all_int_none_role',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['all'],
|
||||
fleet: ['none'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const FleetAllIntegrNoneUser: User = {
|
||||
username: 'fleet_all_int_none_user',
|
||||
password: 'password',
|
||||
roles: [FleetAllIntegrNoneRole.name],
|
||||
};
|
||||
export const FleetNoneIntegrAllRole: Role = {
|
||||
name: 'fleet_none_int_all_role',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
fleetv2: ['none'],
|
||||
fleet: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
export const FleetNoneIntegrAllUser: User = {
|
||||
username: 'fleet_none_int_all_user',
|
||||
password: 'password',
|
||||
roles: [FleetNoneIntegrAllRole.name],
|
||||
};
|
||||
|
||||
const getUserInfo = (user: User): UserInfo => ({
|
||||
username: user.username,
|
||||
full_name: user.username.replace('_', ' '),
|
||||
email: `${user.username}@elastic.co`,
|
||||
});
|
||||
|
||||
export enum ROLES {
|
||||
elastic = 'elastic',
|
||||
}
|
||||
|
||||
export const createUsersAndRoles = (users: User[], roles: Role[]) => {
|
||||
const envUser = getEnvAuth();
|
||||
for (const role of roles) {
|
||||
cy.log(`Creating role: ${JSON.stringify(role)}`);
|
||||
cy.request({
|
||||
body: role.privileges,
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'PUT',
|
||||
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
|
||||
})
|
||||
.its('status')
|
||||
.should('eql', 204);
|
||||
}
|
||||
|
||||
for (const user of users) {
|
||||
const userInfo = getUserInfo(user);
|
||||
cy.log(`Creating user: ${JSON.stringify(user)}`);
|
||||
cy.request({
|
||||
body: {
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
roles: user.roles,
|
||||
full_name: userInfo.full_name,
|
||||
email: userInfo.email,
|
||||
},
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'POST',
|
||||
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
|
||||
})
|
||||
.its('status')
|
||||
.should('eql', 200);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteUsersAndRoles = (users: User[], roles: Role[]) => {
|
||||
const envUser = getEnvAuth();
|
||||
for (const user of users) {
|
||||
cy.log(`Deleting user: ${JSON.stringify(user)}`);
|
||||
cy.request({
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'DELETE',
|
||||
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.its('status')
|
||||
.should('oneOf', [204, 404]);
|
||||
}
|
||||
|
||||
for (const role of roles) {
|
||||
cy.log(`Deleting role: ${JSON.stringify(role)}`);
|
||||
cy.request({
|
||||
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
|
||||
method: 'DELETE',
|
||||
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
|
||||
failOnStatusCode: false,
|
||||
})
|
||||
.its('status')
|
||||
.should('oneOf', [204, 404]);
|
||||
}
|
||||
};
|
|
@ -88,7 +88,7 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err
|
|||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h2>
|
||||
<h2 data-test-subj="missingPrivilegesPromptTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorTitle"
|
||||
defaultMessage="Permission denied"
|
||||
|
@ -96,7 +96,7 @@ const PermissionsError: React.FunctionComponent<{ error: string }> = memo(({ err
|
|||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<p data-test-subj="missingPrivilegesPromptMessage">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.permissionDeniedErrorMessage"
|
||||
defaultMessage="You are not authorized to access Fleet. It requires the {roleName1} Kibana privilege for Fleet, and the {roleName2} or {roleName1} privilege for Integrations."
|
||||
|
|
|
@ -189,6 +189,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="PackagePoliciesTableUpgradeButton"
|
||||
size="s"
|
||||
minWidth="0"
|
||||
isDisabled={!canWriteIntegrationPolicies}
|
||||
|
|
|
@ -22,7 +22,7 @@ export const FleetServerMissingPrivileges = () => {
|
|||
<EuiEmptyPrompt
|
||||
iconType="securityApp"
|
||||
title={
|
||||
<h2>
|
||||
<h2 data-test-subj="fleetServerMissingPrivilegesTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetupPermissionDeniedErrorTitle"
|
||||
defaultMessage="Permission denied"
|
||||
|
@ -30,7 +30,7 @@ export const FleetServerMissingPrivileges = () => {
|
|||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<p data-test-subj="fleetServerMissingPrivilegesMessage">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.fleetServerSetupPermissionDeniedErrorMessage"
|
||||
defaultMessage="Fleet Server needs to be set up. This requires the {roleName} cluster privilege. Contact your administrator."
|
||||
|
|
|
@ -60,6 +60,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
|
|||
...(showAddAgent && !agentPolicy.is_managed
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="PackagePolicyActionsAddAgentItem"
|
||||
icon="plusInCircle"
|
||||
onClick={() => {
|
||||
setIsActionsMenuOpen(false);
|
||||
|
@ -75,6 +76,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
|
|||
]
|
||||
: []),
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="PackagePolicyActionsEditItem"
|
||||
disabled={!canWriteIntegrationPolicies}
|
||||
icon="pencil"
|
||||
href={getHref('integration_policy_edit', {
|
||||
|
@ -88,6 +90,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
|
|||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
data-test-subj="PackagePolicyActionsUpgradeItem"
|
||||
disabled={!packagePolicy.hasUpgrade || !canWriteIntegrationPolicies}
|
||||
icon="refresh"
|
||||
href={upgradePackagePolicyHref}
|
||||
|
@ -113,6 +116,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
|
|||
{(deletePackagePoliciesPrompt) => {
|
||||
return (
|
||||
<DangerEuiContextMenuItem
|
||||
data-test-subj="PackagePolicyActionsDeleteItem"
|
||||
disabled={!canWriteIntegrationPolicies}
|
||||
icon="trash"
|
||||
onClick={() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue