[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:
Cristina Amico 2022-02-09 12:33:39 +01:00 committed by GitHub
parent b2b60ff061
commit ceb14e6842
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 847 additions and 53 deletions

View file

@ -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']);

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

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

View file

@ -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."

View file

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

View file

@ -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."

View file

@ -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={() => {