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