diff --git a/x-pack/legacy/plugins/fleet/index.ts b/x-pack/legacy/plugins/fleet/index.ts index ff009f5b5934e..0b91aadf1c5aa 100644 --- a/x-pack/legacy/plugins/fleet/index.ts +++ b/x-pack/legacy/plugins/fleet/index.ts @@ -61,6 +61,30 @@ export function fleet(kibana: any) { attributesToEncrypt: new Set(['token']), attributesToExcludeFromAAD: new Set(['enrollment_rules']), }); + server.plugins.xpack_main.registerFeature({ + id: 'fleet', + name: 'Fleet', + app: ['fleet', 'kibana'], + excludeFromBasePrivileges: true, + privileges: { + all: { + savedObject: { + all: ['agents', 'events', 'tokens'], + read: [], + }, + ui: ['read', 'write'], + api: ['fleet-read', 'fleet-all'], + }, + read: { + savedObject: { + all: [], + read: ['agents', 'events', 'tokens'], + }, + ui: ['read'], + api: ['fleet-read'], + }, + }, + }); initServerWithKibana(server); }, }); diff --git a/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/adapter_types.ts index 27307d3153890..a9fc9be2aaa97 100644 --- a/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/adapter_types.ts @@ -13,6 +13,7 @@ export interface FrameworkAdapter { // Instance vars info: FrameworkInfo; version: string; + capabilities: { read: boolean; write: boolean }; currentUser: FrameworkUser; // Methods waitUntilFrameworkReady(): Promise; diff --git a/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/kibana_framework_adapter.ts index f127359b887ce..9088d503a888f 100644 --- a/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/fleet/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -11,6 +11,7 @@ import { isLeft } from 'fp-ts/lib/Either'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { UIRoutes } from 'ui/routes'; +import { capabilities } from 'ui/capabilities'; import { BufferedKibanaServiceCall, KibanaAdapterServiceRefs, KibanaUIConfig } from '../../types'; import { FrameworkAdapter, @@ -36,6 +37,10 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { public get currentUser() { return this.shieldUser!; } + public get capabilities(): Readonly<{ read: boolean; write: boolean }> { + return capabilities.get().fleet as { read: boolean; write: boolean }; + } + private xpackInfo: FrameworkInfo | null = null; private adapterService: KibanaAdapterServiceProvider; private shieldUser: FrameworkUser | null = null; diff --git a/x-pack/legacy/plugins/fleet/public/lib/framework.ts b/x-pack/legacy/plugins/fleet/public/lib/framework.ts index ff07beaf558cc..f6b9ec46d0a2a 100644 --- a/x-pack/legacy/plugins/fleet/public/lib/framework.ts +++ b/x-pack/legacy/plugins/fleet/public/lib/framework.ts @@ -21,6 +21,10 @@ export class FrameworkLib { return this.adapter.currentUser; } + public get capabilities(): { read: boolean; write: boolean } { + return this.adapter.capabilities; + } + public get info() { return this.adapter.info; } diff --git a/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx b/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx index 6dec90a2f332b..245533f7797bb 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/agent_list/index.tsx @@ -154,12 +154,16 @@ export const AgentListPage: React.SFC = ({ libs }) => { } actions={ - setIsEnrollmentFlyoutOpen(true)}> - - + libs.framework.capabilities.write ? ( + setIsEnrollmentFlyoutOpen(true)}> + + + ) : ( + null + ) } /> ); @@ -191,14 +195,16 @@ export const AgentListPage: React.SFC = ({ libs }) => { - - setIsEnrollmentFlyoutOpen(true)}> - - - + {libs.framework.capabilities.write && ( + + setIsEnrollmentFlyoutOpen(true)}> + + + + )} diff --git a/x-pack/legacy/plugins/fleet/public/pages/error/no_access.tsx b/x-pack/legacy/plugins/fleet/public/pages/error/no_access.tsx index a468616052b09..3b18032059b85 100644 --- a/x-pack/legacy/plugins/fleet/public/pages/error/no_access.tsx +++ b/x-pack/legacy/plugins/fleet/public/pages/error/no_access.tsx @@ -20,7 +20,7 @@ export const NoAccessPage = injectI18n(({ intl }) => (

diff --git a/x-pack/legacy/plugins/fleet/public/routes.tsx b/x-pack/legacy/plugins/fleet/public/routes.tsx index 1de8dd1e311f9..2daedc5e39e71 100644 --- a/x-pack/legacy/plugins/fleet/public/routes.tsx +++ b/x-pack/legacy/plugins/fleet/public/routes.tsx @@ -63,6 +63,16 @@ export class AppRoutes extends Component { /> )} + {!this.props.libs.framework.capabilities.read && ( + + !props.location.pathname.includes('/error') ? ( + + ) : null + } + /> + )} + {/* Ensure security is eanabled for elastic and kibana */} {/* TODO: Disabled for now as we don't have this info set up on backend yet */} {/* {!get(this.props.libs.framework.info, 'security.enabled', true) && ( diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/actions.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/actions.ts index 1dd367508e6c9..05db16ca1a520 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/agents/actions.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/actions.ts @@ -17,7 +17,8 @@ import { AgentAction } from '../../../common/types/domain_data'; export const createAgentsAddActionRoute = (libs: FleetServerLib) => ({ method: 'POST', path: '/api/fleet/agents/{agentId}/actions', - config: { + options: { + tags: ['access:fleet-all'], validate: { payload: Joi.object(), }, diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/delete.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/delete.ts index f4a1052a788f0..0b030df057d5d 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/agents/delete.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/delete.ts @@ -14,8 +14,10 @@ import { FleetServerLib } from '../../libs/types'; export const createDeleteAgentsRoute = (libs: FleetServerLib) => ({ method: 'DELETE', - config: {}, path: '/api/fleet/agents/{id}', + options: { + tags: ['access:fleet-all'], + }, handler: async ( request: FrameworkRequest<{ params: { id: string } }>, h: FrameworkResponseToolkit diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/enroll.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/enroll.ts index c7cb50657b53c..bf9419f1ef121 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/agents/enroll.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/enroll.ts @@ -13,7 +13,7 @@ import { Agent } from '../../../common/types/domain_data'; export const createEnrollAgentsRoute = (libs: FleetServerLib) => ({ method: 'POST', path: '/api/fleet/agents/enroll', - config: { + options: { auth: false, validate: { headers: Joi.object({ diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/events.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/events.ts index 69e2714a9b896..70824f6cd9800 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/agents/events.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/events.ts @@ -13,7 +13,8 @@ import { AgentEvent } from '../../../common/types/domain_data'; export const createGETAgentEventsRoute = (libs: FleetServerLib) => ({ method: 'GET', path: '/api/fleet/agents/{agentId}/events', - config: { + options: { + tags: ['access:fleet-read'], validate: { query: Joi.object({ kuery: Joi.string() diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/get.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/get.ts index 596307ef53072..9f13f315008c8 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/agents/get.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/get.ts @@ -13,7 +13,8 @@ import { Agent } from '../../../common/types/domain_data'; export const createGETAgentsRoute = (libs: FleetServerLib) => ({ method: 'GET', path: '/api/fleet/agents/{agentId}', - config: { + options: { + tags: ['access:fleet-read'], validate: {}, }, handler: async ( diff --git a/x-pack/legacy/plugins/fleet/server/routes/agents/list.ts b/x-pack/legacy/plugins/fleet/server/routes/agents/list.ts index bd3df1dea4ce1..0b55cedd4c1aa 100644 --- a/x-pack/legacy/plugins/fleet/server/routes/agents/list.ts +++ b/x-pack/legacy/plugins/fleet/server/routes/agents/list.ts @@ -14,7 +14,8 @@ import { DEFAULT_AGENTS_PAGE_SIZE } from '../../../common/constants'; export const createListAgentsRoute = (libs: FleetServerLib) => ({ method: 'GET', path: '/api/fleet/agents', - config: { + options: { + tags: ['access:fleet-read'], validate: { query: { page: Joi.number().default(1), diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 469c32541c23d..fd88395cd6d2e 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -98,6 +98,7 @@ export default function({ getService }: FtrProviderContext) { expect(featureIds.sort()).to.eql( [ 'discover', + 'fleet', 'visualize', 'dashboard', 'dev_tools', diff --git a/x-pack/test/api_integration/apis/fleet/agent_actions.ts b/x-pack/test/api_integration/apis/fleet/agent_actions.ts index 739237c3c4219..e34369172eb63 100644 --- a/x-pack/test/api_integration/apis/fleet/agent_actions.ts +++ b/x-pack/test/api_integration/apis/fleet/agent_actions.ts @@ -7,13 +7,55 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { SecurityService } from '../../../common/services'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const users: { [rollName: string]: { username: string; password: string; permissions?: any } } = { + fleet_user: { + permissions: { + feature: { + fleet: ['read'], + }, + spaces: ['*'], + }, + username: 'fleet_user', + password: 'changeme', + }, + fleet_admin: { + permissions: { + feature: { + fleet: ['all'], + }, + spaces: ['*'], + }, + username: 'fleet_admin', + password: 'changeme', + }, + }; describe('fleet_agent_actions', () => { before(async () => { + for (const roleName in users) { + if (users.hasOwnProperty(roleName)) { + const user = users[roleName]; + + if (user.permissions) { + await security.role.create(roleName, { + kibana: [user.permissions], + }); + } + + // Import a repository first + await security.user.create(user.username, { + password: user.password, + roles: [roleName], + full_name: user.username, + }); + } + } + await esArchiver.loadIfNeeded('fleet/agents'); }); after(async () => { @@ -23,6 +65,8 @@ export default function({ getService }: FtrProviderContext) { it('should return a 404 if the agent do not exists', async () => { await supertest .post(`/api/fleet/agents/i-do-not-exist/actions`) + .auth(users.fleet_admin.username, users.fleet_admin.password) + .send({ type: 'PAUSE', }) @@ -33,6 +77,8 @@ export default function({ getService }: FtrProviderContext) { it('should return a 400 if the action is not invalid', async () => { await supertest .post(`/api/fleet/agents/agent1/actions`) + .auth(users.fleet_admin.username, users.fleet_admin.password) + .send({ type: 'INVALID_ACTION', }) @@ -43,6 +89,8 @@ export default function({ getService }: FtrProviderContext) { it('should return a 200 if the action is not invalid', async () => { const { body: apiResponse } = await supertest .post(`/api/fleet/agents/agent1/actions`) + .auth(users.fleet_admin.username, users.fleet_admin.password) + .send({ type: 'PAUSE', }) @@ -52,6 +100,17 @@ export default function({ getService }: FtrProviderContext) { expect(apiResponse.item).to.have.keys(['id', 'type', 'created_at']); }); + it('should return a 404 if called by a user without permissions', async () => { + await supertest + .post(`/api/fleet/agents/agent1/actions`) + .auth(users.fleet_user.username, users.fleet_user.password) + .send({ + type: 'PAUSE', + }) + .set('kbn-xsrf', 'xx') + .expect(404); + }); + // it('should return a 200 after deleting an agent', async () => { // const { body: apiResponse } = await supertest // .delete(`/api/fleet/agents/agent1`) diff --git a/x-pack/test/api_integration/apis/fleet/delete_agent.ts b/x-pack/test/api_integration/apis/fleet/delete_agent.ts index f2440e2deab51..13581d8327a7c 100644 --- a/x-pack/test/api_integration/apis/fleet/delete_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/delete_agent.ts @@ -5,24 +5,79 @@ */ import expect from '@kbn/expect'; - import { FtrProviderContext } from '../../ftr_provider_context'; +import { SecurityService } from '../../../common/services'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const users: { [rollName: string]: { username: string; password: string; permissions?: any } } = { + fleet_user: { + permissions: { + feature: { + fleet: ['read'], + }, + spaces: ['*'], + }, + username: 'fleet_user', + password: 'changeme', + }, + fleet_admin: { + permissions: { + feature: { + fleet: ['all'], + }, + spaces: ['*'], + }, + username: 'fleet_admin', + password: 'changeme', + }, + }; describe('fleet_delete_agent', () => { before(async () => { + for (const roleName in users) { + if (users.hasOwnProperty(roleName)) { + const user = users[roleName]; + + if (user.permissions) { + await security.role.create(roleName, { + kibana: [user.permissions], + }); + } + + // Import a repository first + await security.user.create(user.username, { + password: user.password, + roles: [roleName], + full_name: user.username, + }); + } + } + await esArchiver.loadIfNeeded('fleet/agents'); }); after(async () => { await esArchiver.unload('fleet/agents'); }); + it('should return a 404 if user lacks fleet-write permissions', async () => { + const { body: apiResponse } = await supertest + .delete(`/api/fleet/agents/agent1`) + .auth(users.fleet_user.username, users.fleet_user.password) + .set('kbn-xsrf', 'xx') + .expect(404); + + expect(apiResponse).not.to.eql({ + success: true, + action: 'deleted', + }); + }); + it('should return a 404 if there is no agent to delete', async () => { await supertest .delete(`/api/fleet/agents/i-do-not-exist`) + .auth(users.fleet_admin.username, users.fleet_admin.password) .set('kbn-xsrf', 'xx') .expect(404); }); @@ -30,6 +85,7 @@ export default function({ getService }: FtrProviderContext) { it('should return a 200 after deleting an agent', async () => { const { body: apiResponse } = await supertest .delete(`/api/fleet/agents/agent1`) + .auth(users.fleet_admin.username, users.fleet_admin.password) .set('kbn-xsrf', 'xx') .expect(200); expect(apiResponse).to.eql({ diff --git a/x-pack/test/api_integration/apis/fleet/list_agent.ts b/x-pack/test/api_integration/apis/fleet/list_agent.ts index 8b5d8a2f04d3d..4c3e85b2924ad 100644 --- a/x-pack/test/api_integration/apis/fleet/list_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/list_agent.ts @@ -7,24 +7,95 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { SecurityService } from '../../../common/services'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const users: { [rollName: string]: { username: string; password: string; permissions?: any } } = { + kibana_basic_user: { + permissions: { + feature: { + dashboards: ['read'], + }, + spaces: ['*'], + }, + username: 'kibana_basic_user', + password: 'changeme', + }, + fleet_user: { + permissions: { + feature: { + fleet: ['read'], + }, + spaces: ['*'], + }, + username: 'fleet_user', + password: 'changeme', + }, + fleet_admin: { + permissions: { + feature: { + fleet: ['all'], + }, + spaces: ['*'], + }, + username: 'fleet_admin', + password: 'changeme', + }, + }; describe('fleet_list_agent', () => { before(async () => { + for (const roleName in users) { + if (users.hasOwnProperty(roleName)) { + const user = users[roleName]; + + if (user.permissions) { + await security.role.create(roleName, { + kibana: [user.permissions], + }); + } + + // Import a repository first + await security.user.create(user.username, { + password: user.password, + roles: [roleName], + full_name: user.username, + }); + } + } + await esArchiver.loadIfNeeded('fleet/agents'); }); after(async () => { await esArchiver.unload('fleet/agents'); }); - it('should return the list of agents', async () => { - const { body: apiResponse } = await supertest.get(`/api/fleet/agents`).expect(200); + it('should return the list of agents when requesting as a user with fleet write permissions', async () => { + const { body: apiResponse } = await supertest + .get(`/api/fleet/agents`) + .auth(users.fleet_admin.username, users.fleet_admin.password) + .expect(200); + expect(apiResponse).to.have.keys('success', 'page', 'total', 'list'); + expect(apiResponse.success).to.eql(true); + expect(apiResponse.total).to.eql(4); + }); + it('should return the list of agents when requesting as a user with fleet read permissions', async () => { + const { body: apiResponse } = await supertest + .get(`/api/fleet/agents`) + .auth(users.fleet_user.username, users.fleet_user.password) + .expect(200); expect(apiResponse).to.have.keys('success', 'page', 'total', 'list'); expect(apiResponse.success).to.eql(true); expect(apiResponse.total).to.eql(4); }); + it('should not return the list of agents when requesting as a user without fleet permissions', async () => { + await supertest + .get(`/api/fleet/agents`) + .auth(users.kibana_basic_user.username, users.kibana_basic_user.password) + .expect(404); + }); }); }