Using HapiJS's scopes to perform authorization on api endpoints

This commit is contained in:
kobelb 2019-03-11 11:21:38 -07:00
parent e79d63b5d1
commit f73810c22d
7 changed files with 61 additions and 38 deletions

View file

@ -65,12 +65,17 @@ export const createProxyRoute = ({
path: '/api/console/proxy',
method: 'POST',
config: {
tags: ['access:execute'],
payload: {
output: 'stream',
parse: false
},
auth: {
access: {
scope: 'console'
}
},
validate: {
query: Joi.object().keys({
method: Joi.string()

View file

@ -34,7 +34,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
catalogue: ['infraops'],
privileges: {
all: {
api: ['infra/graphql'],
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
read: ['config'],
@ -42,7 +42,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
ui: ['show', 'configureSource'],
},
read: {
api: ['infra/graphql'],
api: ['infra'],
savedObject: {
all: [],
read: ['config', 'infrastructure-ui-source'],
@ -63,7 +63,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
catalogue: ['infralogging'],
privileges: {
all: {
api: ['infra/graphql'],
api: ['infra'],
savedObject: {
all: ['infrastructure-ui-source'],
read: ['config'],
@ -71,7 +71,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => {
ui: ['show', 'configureSource'],
},
read: {
api: ['infra/graphql'],
api: ['infra'],
savedObject: {
all: [],
read: ['config', 'infrastructure-ui-source'],

View file

@ -57,7 +57,11 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
}),
path: routePath,
route: {
tags: ['access:graphql'],
auth: {
access: {
scope: 'infra',
},
},
},
},
plugin: graphqlHapi,
@ -71,7 +75,11 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework
}),
path: `${routePath}/graphiql`,
route: {
tags: ['access:graphql'],
auth: {
access: {
scope: 'infra',
},
},
},
},
plugin: graphiqlHapi,

View file

@ -5,6 +5,7 @@
*/
import Boom from 'boom';
import { flatten, get, uniq } from 'lodash';
import { resolve } from 'path';
import { getUserProvider } from './server/lib/get_user';
import { initAuthenticateApi } from './server/routes/api/v1/authenticate';
@ -219,6 +220,32 @@ export const security = (kibana) => new kibana.Plugin({
};
});
server.plugins.security.registerAuthScopeGetter(async (request) => {
if (!request.path.startsWith('/api/')) {
return;
}
const { actions, checkPrivilegesDynamicallyWithRequest } = server.plugins.security.authorization;
const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request);
const access = get(request, 'route.settings.auth.access');
if (!access) {
return;
}
const scopes = uniq(access.reduce((acc, entry) => ([...acc, ...flatten(Object.values(entry.scope))]), []));
if (scopes.length === 0) {
return;
}
const actionsToScopeMap = new Map(scopes.map(scope => ([actions.api.get(scope), scope])));
const checkPrivilegesResponse = await checkPrivileges(Array.from(actionsToScopeMap.keys()));
const hasScopes = Object.entries(checkPrivilegesResponse.privileges)
.filter(([, value]) => value)
.map(([action]) => actionsToScopeMap.get(action));
return hasScopes;
});
server.ext('onPostAuth', async function (req, h) {
const path = req.path;
@ -243,23 +270,6 @@ export const security = (kibana) => new kibana.Plugin({
}
}
// Enforce API restrictions for associated applications
if (path.startsWith('/api/')) {
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();
}
}
}
return h.continue;
});
}

View file

@ -111,7 +111,7 @@ const kibanaFeatures: Feature[] = [
catalogue: ['console', 'searchprofiler', 'grokdebugger'],
privileges: {
all: {
api: ['console/execute'],
api: ['console'],
savedObject: {
all: [],
read: ['config'],
@ -119,7 +119,7 @@ const kibanaFeatures: Feature[] = [
ui: ['show'],
},
read: {
api: ['console/execute'],
api: ['console'],
savedObject: {
all: [],
read: ['config'],

View file

@ -137,7 +137,7 @@ export default function securityTests({ getService }: KibanaFunctionalTestDefaul
.auth(username, password)
.set('kbn-xsrf', 'xxx')
.send()
.expect(404);
.expect(403);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
@ -211,7 +211,7 @@ export default function securityTests({ getService }: KibanaFunctionalTestDefaul
.auth(user1.username, user1.password)
.set('kbn-xsrf', 'xxx')
.send()
.expect(404);
.expect(403);
});
});
});

View file

@ -26,11 +26,11 @@ const featureControlsTests: KbnTestProvider = ({ getService }) => {
const spaces: SpacesService = getService('spaces');
const clientFactory = getService('infraOpsGraphQLClientFactory');
const expectGraphQL404 = (result: any) => {
const expectGraphQL403 = (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);
expect(result.error.networkError).to.have.property('statusCode', 403);
};
const expectGraphQLResponse = (result: any) => {
@ -39,10 +39,10 @@ const featureControlsTests: KbnTestProvider = ({ getService }) => {
expect(result.response.data).to.be.an('object');
};
const expectGraphIQL404 = (result: any) => {
const expectGraphIQL403 = (result: any) => {
expect(result.error).to.be(undefined);
expect(result.response).not.to.be(undefined);
expect(result.response).to.have.property('statusCode', 404);
expect(result.response).to.have.property('statusCode', 403);
};
const expectGraphIQLResponse = (result: any) => {
@ -106,10 +106,10 @@ const featureControlsTests: KbnTestProvider = ({ getService }) => {
});
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);
expectGraphQL403(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
expectGraphIQL403(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
@ -187,10 +187,10 @@ const featureControlsTests: KbnTestProvider = ({ getService }) => {
});
const graphQLResult = await executeGraphQLQuery(username, password);
expectGraphQL404(graphQLResult);
expectGraphQL403(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password);
expectGraphIQL404(graphQLIResult);
expectGraphIQL403(graphQLIResult);
} finally {
await security.role.delete(roleName);
await security.user.delete(username);
@ -285,10 +285,10 @@ const featureControlsTests: KbnTestProvider = ({ getService }) => {
it(`user_1 can't access APIs in space_3`, async () => {
const graphQLResult = await executeGraphQLQuery(username, password, space3Id);
expectGraphQL404(graphQLResult);
expectGraphQL403(graphQLResult);
const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id);
expectGraphIQL404(graphQLIResult);
expectGraphIQL403(graphQLIResult);
});
});
});