diff --git a/shield/kibana/.gitignore b/shield/kibana/.gitignore new file mode 100644 index 00000000000..b73454cfeb7 --- /dev/null +++ b/shield/kibana/.gitignore @@ -0,0 +1,20 @@ +work +.idea +agent/logs +agent/data +agent/target +agent/.project +agent/.classpath +agent/.settings +agent/config +agent/lib +agent/.local-execution-hints.log +.DS_Store +*.iml +*.log +node_modules +esvm +build +.aws-config.json +html_docs +target diff --git a/shield/kibana/index.js b/shield/kibana/index.js new file mode 100644 index 00000000000..7d7dc789544 --- /dev/null +++ b/shield/kibana/index.js @@ -0,0 +1,63 @@ +const _ = require('lodash'); +const hapiAuthCookie = require('hapi-auth-cookie'); +const getAuthHeader = require('./server/lib/get_auth_header'); + +module.exports = (kibana) => new kibana.Plugin({ + name: 'security', + require: ['elasticsearch'], + + config(Joi) { + return Joi.object({ + enabled: Joi.boolean().default(true), + encryptionKey: Joi.string().default('secret'), + sessionTimeout: Joi.number().default(30 * 60 * 1000) + }).default() + }, + + uiExports: { + apps: [{ + id: 'login', + title: 'Login', + main: 'plugins/security/login', + hidden: true, + autoload: kibana.autoload.styles + }, { + id: 'logout', + title: 'Logout', + main: 'plugins/security/login/logout', + hidden: false, + autoload: kibana.autoload.styles + }] + }, + + init(server, options) { + const isValidUser = require('./server/lib/is_valid_user')(server.plugins.elasticsearch.client); + const config = server.config(); + + server.register(hapiAuthCookie, (error) => { + if (error != null) throw error; + + server.auth.strategy('session', 'cookie', 'required', { + cookie: 'sid', + password: config.get('security.encryptionKey'), + ttl: config.get('security.sessionTimeout'), + clearInvalid: true, + keepAlive: true, + isSecure: false, // TODO: Remove this + redirectTo: '/login', + validateFunc(request, session, callback) { + const {username, password} = session; + + return isValidUser(username, password).then(() => { + _.assign(request.headers, getAuthHeader(username, password)); + return callback(null, true); + }, (error) => { + return callback(error, false); + }); + } + }); + }); + + require('./server/routes/authentication')(server, this); + } +}); \ No newline at end of file diff --git a/shield/kibana/package.json b/shield/kibana/package.json new file mode 100644 index 00000000000..95314d96f04 --- /dev/null +++ b/shield/kibana/package.json @@ -0,0 +1,25 @@ +{ + "author": { + "name": "Elasticsearch", + "company": "Elasticsearch BV" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "precommit": "gulp lint" + }, + "name": "security", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "http://github.com/elastic/x-plugins" + }, + "devDependencies": {}, + "dependencies": { + "bluebird": "^3.0.0", + "boom": "^2.10.0", + "hapi": "^11.0.2", + "hapi-auth-cookie": "^3.1.0", + "joi": "^6.9.1", + "lodash": "^3.10.1" + } +} diff --git a/shield/kibana/public/login/login.html b/shield/kibana/public/login/login.html new file mode 100644 index 00000000000..762eb6fce9d --- /dev/null +++ b/shield/kibana/public/login/login.html @@ -0,0 +1,17 @@ +<div class="container"> + <h1><img src="{{login.kibanaLogoUrl}}" /></h1> + + <form id="login-form" ng-submit="login.submit(username, password)" class="animated infinite bounce"> + <div class="form-group inner-addon left-addon"> + <i class="fa fa-user fa-lg fa-fw"></i> + <input type="text" ng-model="username" class="form-control" id="username" name="username" placeholder="Username" /> + </div> + <div class="form-group inner-addon left-addon"> + <i class="fa fa-lock fa-lg fa-fw"></i> + <input type="password" ng-model="password" class="form-control" id="password" name="password" placeholder="Password" /> + </div> + <div ng-show="login.error" class="form-group has-error">Oops! That is an invalid username/password combination.</div> + <button type="submit" ng-disabled="!username || !password" class="btn btn-default login">LOG IN</button> + <!--<span ng-show="login.loading" class="fa fa-spinner fa-spin"></span>--> + </form> +</div> \ No newline at end of file diff --git a/shield/kibana/public/login/login.js b/shield/kibana/public/login/login.js new file mode 100644 index 00000000000..f91dd5bf3c5 --- /dev/null +++ b/shield/kibana/public/login/login.js @@ -0,0 +1,27 @@ +require('plugins/security/login/login.less'); + +const kibanaLogoUrl = require('ui/images/kibana-transparent-white.svg'); + +require('ui/chrome') + .setVisible(false) + .setRootTemplate(require('plugins/security/login/login.html')) + .setRootController('login', ($http) => { + var login = { + loading: false, + kibanaLogoUrl + }; + + login.submit = (username, password) => { + login.loading = true; + + $http.post('/login', { + username: username, + password: password + }).then( + (response) => window.location.href = '/', // TODO: Redirect more intelligently + (error) => login.error = true + ).finally(() => login.loading = false); + }; + + return login; + }); \ No newline at end of file diff --git a/shield/kibana/public/login/login.less b/shield/kibana/public/login/login.less new file mode 100644 index 00000000000..7031df40d8d --- /dev/null +++ b/shield/kibana/public/login/login.less @@ -0,0 +1,54 @@ +body { + background: #222222; +} + +.inner-addon { + position: relative; +} + +.inner-addon .fa { + position: absolute; + padding: 10px; + pointer-events: none; + color: #A2A4AC; +} + +.left-addon .fa { left: 0px;} +.right-addon .fa { right: 0px;} + +.left-addon input { padding-left: 30px !important; } +.right-addon input { padding-right: 30px !important; } + +.container { + width: 350px; + margin: auto; + text-align: center; + + h1, button { + margin: 1em; + } +} + +.has-error { + color: red; +} + +.login { + background-color: #94C63D; + color: white; + width: 200px; + font-size: 1.5em; + border: none; + + &:disabled { + background-color: #44532A; + color: #636464; + } +} + +input.form-control { + border: none; + font-size: 1.25em; + height: auto; + padding: 0.5em; +} \ No newline at end of file diff --git a/shield/kibana/public/login/logout.js b/shield/kibana/public/login/logout.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/shield/kibana/server/lib/get_auth_header.js b/shield/kibana/server/lib/get_auth_header.js new file mode 100644 index 00000000000..7a396218803 --- /dev/null +++ b/shield/kibana/server/lib/get_auth_header.js @@ -0,0 +1,4 @@ +module.exports = (username, password) => { + const auth = new Buffer(`${username}:${password}`, 'utf8').toString('base64'); + return {'Authorization': `Basic ${auth}`}; +}; \ No newline at end of file diff --git a/shield/kibana/server/lib/is_valid_user.js b/shield/kibana/server/lib/is_valid_user.js new file mode 100644 index 00000000000..b521f73b94c --- /dev/null +++ b/shield/kibana/server/lib/is_valid_user.js @@ -0,0 +1,5 @@ +const getAuthHeader = require('./get_auth_header'); + +module.exports = (client) => (username, password) => client.info({ + headers: getAuthHeader(username, password) +}); \ No newline at end of file diff --git a/shield/kibana/server/routes/authentication.js b/shield/kibana/server/routes/authentication.js new file mode 100644 index 00000000000..b2b9f1fa069 --- /dev/null +++ b/shield/kibana/server/routes/authentication.js @@ -0,0 +1,53 @@ +const Boom = require('boom'); +const Joi = require('joi'); + +module.exports = (server, uiExports) => { + const login = uiExports.apps.byId.login; + const isValidUser = require('../lib/is_valid_user')(server.plugins.elasticsearch.client); + + server.route({ + method: 'GET', + path: '/login', + handler(request, reply) { + return reply.renderApp(login); + }, + config: { + auth: false + } + }); + + server.route({ + method: 'POST', + path: '/login', + handler(request, reply) { + return isValidUser(request.payload.username, request.payload.password).then(() => { + request.auth.session.set({username: request.payload.username, password: request.payload.password}); + return reply({ + statusCode: 200, + payload: 'success' + }); + }, (error) => { + request.auth.session.clear(); + return reply(Boom.unauthorized(error)); + }) + }, + config: { + auth: false, + validate: { + payload: { + username: Joi.string().required(), + password: Joi.string().required() + } + } + } + }); + + server.route({ + method: 'GET', + path: '/app/logout', // TODO: Change to /logout + handler(request, reply) { + request.auth.session.clear(); + return reply.redirect('/'); + } + }); +}; \ No newline at end of file