Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Controls - Infrastructure and Logging #31843

Merged
merged 43 commits into from
Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a500ded
hide infra/logs apps if disabled via UICapabilities
legrego Feb 11, 2019
2df2771
adds tests
legrego Feb 11, 2019
b1b7c41
adds UICapability tests for infra and log apps
legrego Feb 11, 2019
fe63b39
update expected privilege/action mapping
legrego Feb 11, 2019
bbfc833
adds feature controls security tests for infraHome
legrego Feb 12, 2019
e44b0eb
adds infra spaces feature control tests
legrego Feb 12, 2019
f5db5c8
remove debug code
legrego Feb 12, 2019
4794cbf
Merge branch 'granular-app-privileges' into fc/enable-infra
legrego Feb 14, 2019
a1c3345
a sample readonly implementation, ignoring 'logs' privileges
legrego Feb 15, 2019
0382915
Merge branch 'granular-app-privileges' into fc/enable-infra
legrego Feb 15, 2019
0cb1bf0
ts fixes
legrego Feb 15, 2019
dc61811
fix capability expectations
legrego Feb 15, 2019
7981ad4
Removing RequiresUICapability component, since there are no usages
kobelb Feb 21, 2019
d3bd237
Driving the source configuration seperately for logs/infrastructure
kobelb Feb 21, 2019
19bfb58
Adding infrastructure feature controls security functional tests
kobelb Feb 22, 2019
8eab19a
Adding spaces infrastructure tests
kobelb Feb 22, 2019
b20b6b6
Adding logs functional tests
kobelb Feb 22, 2019
d9dcccf
Reworking the ui capability tests to be more consistent
kobelb Feb 22, 2019
daea6a3
Fixing privileges API
kobelb Feb 22, 2019
f53271c
Forcing logout
kobelb Feb 22, 2019
7b5be17
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Feb 25, 2019
1b3b418
Fixing comma issue introduced by merge
kobelb Feb 25, 2019
a51083d
Fix merge conflicts and loading/unloading esarchives more consistently
kobelb Feb 25, 2019
fc1ea1a
Removing unnecessary !!
kobelb Feb 27, 2019
11e0082
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Feb 28, 2019
c213bc3
Fixing saved object management tests
kobelb Mar 1, 2019
6202363
Fixing more tests
kobelb Mar 1, 2019
4776f1f
Using the new context APIs
kobelb Mar 1, 2019
ab1e930
Revert "Using the new context APIs"
kobelb Mar 1, 2019
28612d0
Adding future version of ui capabilities react provider
kobelb Mar 1, 2019
d4d8538
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Mar 1, 2019
7bdc00d
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Mar 4, 2019
a8d9c41
Switching the order of the HOC's for infra and making the future the
kobelb Mar 4, 2019
35829c1
Applying Felix's PR feedback
kobelb Mar 4, 2019
2cd4a50
Protecting Infra's GraphQL APIs
kobelb Mar 5, 2019
8e49558
Updating privileges list
kobelb Mar 5, 2019
20cd128
Using the introspection query
kobelb Mar 7, 2019
c41d5b3
No longer using apollo context library, rephrasing test descriptions
kobelb Mar 7, 2019
14510b2
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Mar 7, 2019
9206566
Fixing issue introduced by merge conflict, I forgot a }
kobelb Mar 7, 2019
b4b987f
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Mar 7, 2019
6371bb6
Putting back missplaced data test subj
kobelb Mar 7, 2019
a071707
Merge remote-tracking branch 'upstream/granular-app-privileges' into …
kobelb Mar 9, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"abab": "^1.0.4",
"ansi-colors": "^3.0.5",
"ansicolors": "0.3.2",
"apollo-link-context": "^1.0.14",
"axios": "^0.18.0",
"babel-jest": "^23.6.0",
"babel-plugin-inline-react-svg": "^0.5.4",
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/infra/server/kibana.index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
catalogue: ['infraops'],
privileges: {
all: {
api: ['infra/graphql'],
savedObject: {
all: ['infrastructure-ui-source'],
read: ['config'],
},
ui: ['show', 'configureSource'],
},
read: {
api: ['infra/graphql'],
savedObject: {
all: [],
read: ['config', 'infrastructure-ui-source'],
Expand All @@ -61,13 +63,15 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
catalogue: ['infralogging'],
privileges: {
all: {
api: ['infra/graphql'],
savedObject: {
all: ['infrastructure-ui-source'],
read: ['config'],
},
ui: ['show', 'configureSource'],
},
read: {
api: ['infra/graphql'],
savedObject: {
all: [],
read: ['config', 'infrastructure-ui-source'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
schema,
}),
path: routePath,
route: {
tags: ['access:graphql'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be interested to learn how these tags map to the infra/graphql privileges. Are they associated via the overlapping graphql part? What are the semantics of access and are there alternative values for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rather clumsy at the moment and something that I'd like to address in a subsequent PR. The following code parses the access tag to construct the action which we then ensure the user has privileges for:

const { tags = [] } = req.route.settings;
const actionTags = tags.filter(tag => tag.startsWith('access:'));
if (actionTags.length > 0) {
const feature = path.split('/', 3)[2];
const apiActions = actionTags.map(tag => actions.api.get(`${feature}/${tag.split(':', 2)[1]}`));
const checkPrivilegesResponse = await checkPrivileges(apiActions);
if (!checkPrivilegesResponse.hasAllRequested) {
return Boom.notFound();
}
}

It's hard to explain in words or pseudo-code, hence the desire to refactor it :)

},
},
plugin: graphqlHapi,
});
Expand All @@ -67,6 +70,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
passHeader: `'kbn-version': '${this.version}'`,
}),
path: `${routePath}/graphiql`,
route: {
tags: ['access:graphql'],
},
},
plugin: graphiqlHapi,
});
Expand Down
294 changes: 294 additions & 0 deletions x-pack/test/api_integration/apis/infra/feature_controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import expect from 'expect.js';
import { SecurityService, SpacesService } from 'x-pack/test/common/services';
import { metadataQuery } from '../../../../plugins/infra/public/containers/metadata/metadata.gql_query';
import { MetadataQuery } from '../../../../plugins/infra/public/graphql/types';
import { KbnTestProvider } from './types';

// tslint:disable:no-default-export
const featureControlsTests: KbnTestProvider = ({ getService }) => {
const supertest = getService('supertestWithoutAuth');
const security: SecurityService = getService('security');
const spaces: SpacesService = getService('spaces');
const clientFactory = getService('infraOpsGraphQLClientFactory');

const expectGraphQL404 = (result: any) => {
expect(result.response).to.be(undefined);
expect(result.error).not.to.be(undefined);
expect(result.error).to.have.property('networkError');
expect(result.error.networkError).to.have.property('statusCode', 404);
};

const expectGraphQLResponse = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).to.have.property('data');
expect(result.response.data).to.be.an('object');
};

const expectGraphIQL404 = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 404);
};

const expectGraphIQLResponse = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 200);
};

const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => {
const queryOptions = {
query: metadataQuery,
variables: {
sourceId: 'default',
nodeId: 'demo-stack-mysql-01',
nodeType: 'host',
},
};

const basePath = spaceId ? `/s/${spaceId}` : '';

const client = clientFactory({ username, password, basePath });
let error;
let response;
try {
response = await client.query<MetadataQuery.Query>(queryOptions);
} catch (err) {
error = err;
}
return {
error,
response,
};
};

const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => {
const basePath = spaceId ? `/s/${spaceId}` : '';

return supertest
.get(`${basePath}/api/infra/graphql/graphiql`)
.auth(username, password)
.then((response: any) => ({ error: undefined, response }))
.catch((error: any) => ({ error, response: undefined }));
};

describe('feature controls', () => {
it(`APIs can't be accessed by logstash-* read privileges role`, async () => {
const username = 'logstash_read';
const roleName = 'logstash_read';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
});

await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});

const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);

const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});

it('APIs can be accessed global all with logstash-* read privileges role', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a hard time parsing this name and forming a model of what the test case is supposed to do. 🤔 Could it be rephrased somehow?

const username = 'global_all';
const roleName = 'global_all';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
base: ['all'],
spaces: ['*'],
},
],
});

await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});

const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQLResponse(graphQLResult);

const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQLResponse(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});

// this could be any role which doesn't have access to the infra feature
it(`APIs can't be accessed by dashboard all with logstash-* read privileges role`, async () => {
const username = 'dashboard_all';
const roleName = 'dashboard_all';
const password = `${username}-password`;
try {
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
dashboard: ['all'],
},
spaces: ['*'],
},
],
});

await security.user.create(username, {
password,
roles: [roleName],
full_name: 'a kibana user',
});

const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);

const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
}
});

describe('spaces', () => {
// the following tests create a user_1 which has infrastructure read access to space_1, logs read access to space_2 and dashboard all access to space_3
const space1Id = 'space_1';
const space2Id = 'space_2';
const space3Id = 'space_3';

const roleName = 'user_1';
const username = 'user_1';
const password = 'user_1-password';

before(async () => {
await spaces.create({
id: space1Id,
name: space1Id,
disabledFeatures: [],
});
await spaces.create({
id: space2Id,
name: space2Id,
disabledFeatures: [],
});
await spaces.create({
id: space3Id,
name: space3Id,
disabledFeatures: [],
});
await security.role.create(roleName, {
elasticsearch: {
indices: [
{
names: ['logstash-*'],
privileges: ['read', 'view_index_metadata'],
},
],
},
kibana: [
{
feature: {
infrastructure: ['read'],
},
spaces: [space1Id],
},
{
feature: {
logs: ['read'],
},
spaces: [space2Id],
},
{
feature: {
dashboard: ['all'],
},
spaces: [space3Id],
},
],
});
await security.user.create(username, {
password,
roles: [roleName],
});
});

after(async () => {
await spaces.delete(space1Id);
await spaces.delete(space2Id);
await spaces.delete(space3Id);
await security.role.delete(roleName);
await security.user.delete(username);
});

it('user_1 can access APIs in space_1', async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space1Id);
expectGraphQLResponse(graphQLResult);

const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id);
expectGraphIQLResponse(graphQLIResult);
});

it(`user_1 can access APIs in space_2`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space2Id);
expectGraphQLResponse(graphQLResult);

const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id);
expectGraphIQLResponse(graphQLIResult);
});

it(`user_1 can't access APIs in space_3`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space3Id);
expectGraphQL404(graphQLResult);

const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id);
expectGraphIQL404(graphQLIResult);
});
});
});
};

// tslint:disable-next-line no-default-export
export default featureControlsTests;
1 change: 1 addition & 0 deletions x-pack/test/api_integration/apis/infra/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./sources'));
loadTestFile(require.resolve('./waffle'));
loadTestFile(require.resolve('./log_item'));
loadTestFile(require.resolve('./feature_controls'));
});
}
9 changes: 9 additions & 0 deletions x-pack/test/api_integration/apis/infra/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ export interface EsArchiver {
unload(name: string): void;
}

interface InfraOpsGraphQLClientFactoryOptions {
username: string;
password: string;
basePath: string;
}

export interface KbnTestProviderOptions {
getService(name: string): any;
getService(name: 'esArchiver'): EsArchiver;
getService(name: 'infraOpsGraphQLClient'): ApolloClient<InMemoryCache>;
getService(
name: 'infraOpsGraphQLClientFactory'
): (options: InfraOpsGraphQLClientFactoryOptions) => ApolloClient<InMemoryCache>;
}

export type KbnTestProvider = (options: KbnTestProviderOptions) => void;
Loading