mirror of
https://github.com/valitydev/opendistro-security-kibana-plugin.git
synced 2024-11-06 01:55:16 +00:00
Open Distro for Elasticsearch Security Kibana Plugin initial release
This commit is contained in:
commit
a4d887c01c
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
/package-lock.json
|
||||
build_stage/
|
||||
target/
|
||||
/package.json
|
||||
/releases
|
||||
build/
|
||||
test/
|
||||
/elasticsearch-security-kibana-plugin.iml
|
||||
/.idea
|
||||
/elasticsearch-security-kibana-plugin.iml
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
.DS_Store
|
4
CODE_OF_CONDUCT.md
Normal file
4
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,4 @@
|
||||
## Code of Conduct
|
||||
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
|
||||
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
|
||||
opensource-codeofconduct@amazon.com with any additional questions or comments.
|
61
CONTRIBUTING.md
Normal file
61
CONTRIBUTING.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
|
||||
documentation, we greatly value feedback and contributions from our community.
|
||||
|
||||
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
|
||||
information to effectively respond to your bug report or contribution.
|
||||
|
||||
|
||||
## Reporting Bugs/Feature Requests
|
||||
|
||||
We welcome you to use the GitHub issue tracker to report bugs or suggest features.
|
||||
|
||||
When filing an issue, please check [existing open](https://github.com/OpenDistro/elasticsearch-security-kibana-plugin/issues), or [recently closed](https://github.com/OpenDistro/elasticsearch-security-kibana-plugin/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
|
||||
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
|
||||
|
||||
* A reproducible test case or series of steps
|
||||
* The version of our code being used
|
||||
* Any modifications you've made relevant to the bug
|
||||
* Anything unusual about your environment or deployment
|
||||
|
||||
|
||||
## Contributing via Pull Requests
|
||||
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
|
||||
|
||||
1. You are working against the latest source on the *master* branch.
|
||||
2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
|
||||
3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
|
||||
|
||||
To send us a pull request, please:
|
||||
|
||||
1. Fork the repository.
|
||||
2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
|
||||
3. Ensure local tests pass.
|
||||
4. Commit to your fork using clear commit messages.
|
||||
5. Send us a pull request, answering any default questions in the pull request interface.
|
||||
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
|
||||
|
||||
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
|
||||
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
|
||||
|
||||
|
||||
## Finding contributions to work on
|
||||
Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/OpenDistro/elasticsearch-security-kibana-plugin/labels/help%20wanted) issues is a great place to start.
|
||||
|
||||
|
||||
## Code of Conduct
|
||||
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
|
||||
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
|
||||
opensource-codeofconduct@amazon.com with any additional questions or comments.
|
||||
|
||||
|
||||
## Security issue notifications
|
||||
If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/opendistro_security/vulnerability-reporting/). Please do **not** create a public github issue.
|
||||
|
||||
|
||||
## Licensing
|
||||
|
||||
See the [LICENSE](https://github.com/OpenDistro/elasticsearch-security-kibana-plugin/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
|
||||
|
||||
We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
|
176
LICENSE
Normal file
176
LICENSE
Normal file
@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
2
NOTICE
Normal file
2
NOTICE
Normal file
@ -0,0 +1,2 @@
|
||||
Elasticsearch Security Kibana Plugin
|
||||
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
35
README.md
Normal file
35
README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Open Distro For Elasticsearch Security Kibana Plugin
|
||||
|
||||
<p align="center">
|
||||
<img src="<TO BE REPLACED WITH OUR LOGO>" style="width: 60%" class="md_image"/>
|
||||
</p>
|
||||
|
||||
## About this plugin
|
||||
This plugin for Kibana adds session management and multi-tenancy to your secured cluster.
|
||||
|
||||
For Kibana 6.x it also provides a configuration GUI for the security features.
|
||||
|
||||
## Downloads
|
||||
|
||||
* Kibana 6.x: [Github](https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin)
|
||||
|
||||
## Installation
|
||||
|
||||
Download the release matching your Kibana installation, and install it like any other Kibana plugin:
|
||||
|
||||
```
|
||||
bin/kibana-plugin install file:///path/to/opendistro-for-elasticsearch-security-kibana-plugin-<version>.zip
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Kibana 6.x
|
||||
* [Installation](TBD)
|
||||
* [Authentication](TBD)
|
||||
* [Multi Tenancy](TBD)
|
||||
* [Configuration GUI](TBD)
|
||||
|
||||
## Copyright
|
||||
Open Distro For Elasticsearch Security Copyright 2019- Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
|
||||
|
139
build.sh
Executable file
139
build.sh
Executable file
@ -0,0 +1,139 @@
|
||||
#!/bin/bash
|
||||
KIBANA_VERSION="$1"
|
||||
ELASTICSEARCH_SECURITY_PLUGIN_VERSION="$2"
|
||||
COMMAND="$3"
|
||||
|
||||
# sanity checks for options
|
||||
if [ -z "$KIBANA_VERSION" ] || [ -z "$ELASTICSEARCH_SECURITY_PLUGIN_VERSION" ] || [ -z "$COMMAND" ]; then
|
||||
echo "Usage: ./build.sh <kibana_version> <elasticsearch_security_plugin_version> <install|deploy>"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
if [ "$COMMAND" != "deploy" ] && [ "$COMMAND" != "install" ]; then
|
||||
echo "Usage: ./build.sh <kibana_version> <elasticsearch_security_plugin_version> <install|deploy>"
|
||||
echo "Unknown command: $COMMAND"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# sanity checks for maven
|
||||
if [ -z "$MAVEN_HOME" ]; then
|
||||
echo "MAVEN_HOME not set"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "+++ Checking Maven version +++"
|
||||
$MAVEN_HOME/bin/mvn -version
|
||||
if [ $? != 0 ]; then
|
||||
echo "Checking maven version failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# sanity checks for nvm
|
||||
if [ -z "$NVM_HOME" ]; then
|
||||
echo "NVM_HOME not set"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "+++ Sourcing nvm +++"
|
||||
[ -s "$NVM_HOME/nvm.sh" ] && \. "$NVM_HOME/nvm.sh"
|
||||
|
||||
echo "+++ Checking nvm version +++"
|
||||
nvm version
|
||||
if [ $? != 0 ]; then
|
||||
echo "Checking mvn version failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# check version matches. Do not use jq here, only bash
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd $DIR
|
||||
# while read -r line
|
||||
# do
|
||||
# if [[ "$line" =~ ^\"version\".* ]]; then
|
||||
# if [[ "$line" != "\"version\": \"$1-$2\"," ]]; then
|
||||
# echo "Provided version \"version\": \"$1-$2\" does not match Kibana version: $line"
|
||||
# exit 1;
|
||||
# fi
|
||||
# fi
|
||||
# done < "package.json"
|
||||
|
||||
# cleanup any leftovers
|
||||
./clean.sh
|
||||
if [ $? != 0 ]; then
|
||||
echo "Cleaning leftovers failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
# prepare artefacts
|
||||
PLUGIN_NAME="opendistro_security_kibana_plugin-$ELASTICSEARCH_SECURITY_PLUGIN_VERSION"
|
||||
echo "+++ Building $PLUGIN_NAME.zip +++"
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
mkdir -p build_stage
|
||||
cd build_stage
|
||||
|
||||
echo "+++ Cloning https://github.com/mauve-hedgehog/opendistro-elasticsearch-kibana.git +++"
|
||||
git clone https://github.com/mauve-hedgehog/opendistro-elasticsearch-kibana.git || true > /dev/null 2>&1
|
||||
if [ $? != 0 ]; then
|
||||
echo "got clone Kibana repository failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
cd "opendistro-elasticsearch-kibana"
|
||||
git fetch
|
||||
|
||||
echo "+++ Change to tags/v$KIBANA_VERSION +++"
|
||||
git checkout "tags/v$KIBANA_VERSION"
|
||||
|
||||
if [ $? != 0 ]; then
|
||||
echo "Switching to Kibana tags/v$KIBANA_VERSION failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "+++ Installing node version $(cat .node-version) +++"
|
||||
nvm install "$(cat .node-version)"
|
||||
if [ $? != 0 ]; then
|
||||
echo "Installing node $(cat .node-version) failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
|
||||
cd "$DIR"
|
||||
rm -rf build/
|
||||
rm -rf node_modules/
|
||||
|
||||
echo "+++ Installing node modules +++"
|
||||
npm install
|
||||
if [ $? != 0 ]; then
|
||||
echo "Installing node modules failed";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
|
||||
echo "+++ Copy plugin contents +++"
|
||||
COPYPATH="build/kibana/$PLUGIN_NAME"
|
||||
mkdir -p "$COPYPATH"
|
||||
cp -a "$DIR/index.js" "$COPYPATH"
|
||||
cp -a "$DIR/package.json" "$COPYPATH"
|
||||
cp -a "$DIR/lib" "$COPYPATH"
|
||||
cp -a "$DIR/node_modules" "$COPYPATH"
|
||||
cp -a "$DIR/public" "$COPYPATH"
|
||||
|
||||
if [ "$COMMAND" = "deploy" ] ; then
|
||||
echo "+++ mvn clean deploy -Prelease +++"
|
||||
$MAVEN_HOME/bin/mvn clean deploy -Prelease
|
||||
if [ $? != 0 ]; then
|
||||
echo "$MAVEN_HOME/bin/mvn clean deploy -Prelease failed";
|
||||
exit 1;
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$COMMAND" = "install" ] ; then
|
||||
echo "+++ mvn clean install +++"
|
||||
$MAVEN_HOME/bin/mvn clean install
|
||||
if [ $? != 0 ]; then
|
||||
echo "$MAVEN_HOME/bin/mvn clean install failed";
|
||||
exit 1;
|
||||
fi
|
||||
fi
|
10
clean.sh
Executable file
10
clean.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd $DIR
|
||||
find . -name '.DS_Store' -type f -delete
|
||||
rm -rf ./target
|
||||
rm -rf ./releases
|
||||
rm -rf ./node_modules
|
||||
rm -rf ./build
|
||||
rm -f *.log
|
446
index.js
Normal file
446
index.js
Normal file
@ -0,0 +1,446 @@
|
||||
const pluginRoot = require('requirefrom')('');
|
||||
import { resolve, join, sep } from 'path';
|
||||
import { has } from 'lodash';
|
||||
import indexTemplate from './lib/elasticsearch/setup_index_template';
|
||||
import { migrateTenants } from './lib/multitenancy/migrate_tenants';
|
||||
|
||||
export default function (kibana) {
|
||||
|
||||
let APP_ROOT;
|
||||
let API_ROOT;
|
||||
let authenticationBackend;
|
||||
let securityConfiguration;
|
||||
|
||||
|
||||
return new kibana.Plugin({
|
||||
name: 'opendistro_security',
|
||||
id: 'opendistro_security',
|
||||
require: ['kibana', 'elasticsearch'],
|
||||
|
||||
config: function (Joi) {
|
||||
var obj = Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
allow_client_certificates: Joi.boolean().default(false),
|
||||
readonly_mode: Joi.object().keys({
|
||||
roles: Joi.array().default([]),
|
||||
}).default(),
|
||||
cookie: Joi.object().keys({
|
||||
secure: Joi.boolean().default(false),
|
||||
name: Joi.string().default('security_authentication'),
|
||||
password: Joi.string().min(32).default('security_cookie_default_password'),
|
||||
ttl: Joi.number().integer().min(0).default(60 * 60 * 1000),
|
||||
}).default(),
|
||||
session: Joi.object().keys({
|
||||
ttl: Joi.number().integer().min(0).default(60 * 60 * 1000),
|
||||
keepalive: Joi.boolean().default(true),
|
||||
}).default(),
|
||||
auth: Joi.object().keys({
|
||||
type: Joi.string().valid(['', 'basicauth', 'jwt', 'openid', 'saml', 'proxy', 'kerberos', 'proxycache']).default(''),
|
||||
anonymous_auth_enabled: Joi.boolean().default(false),
|
||||
unauthenticated_routes: Joi.array().default(["/api/status"]),
|
||||
}).default(),
|
||||
basicauth: Joi.object().keys({
|
||||
enabled: Joi.boolean().default(true),
|
||||
unauthenticated_routes: Joi.array().default(["/api/status"]),
|
||||
forbidden_usernames: Joi.array().default([]),
|
||||
header_trumps_session: Joi.boolean().default(false),
|
||||
alternative_login: Joi.object().keys({
|
||||
headers: Joi.array().default([]),
|
||||
show_for_parameter: Joi.string().allow('').default(''),
|
||||
valid_redirects: Joi.array().default([]),
|
||||
button_text: Joi.string().default('Login with provider'),
|
||||
buttonstyle: Joi.string().allow('').default("")
|
||||
}).default(),
|
||||
loadbalancer_url: Joi.string().allow('', null).default(null),
|
||||
login: Joi.object().keys({
|
||||
title: Joi.string().allow('').default('Please login to Kibana'),
|
||||
subtitle: Joi.string().allow('').default('If you have forgotten your username or password, please ask your system administrator'),
|
||||
showbrandimage: Joi.boolean().default(true),
|
||||
brandimage: Joi.string().default("/plugins/opendistro_security/assets/open_distro_for_elasticsearch_logo_h.svg"),
|
||||
buttonstyle: Joi.string().allow('').default("")
|
||||
}).default(),
|
||||
}).default(),
|
||||
multitenancy: Joi.object().keys({
|
||||
enabled: Joi.boolean().default(false),
|
||||
show_roles: Joi.boolean().default(false),
|
||||
enable_filter: Joi.boolean().default(false),
|
||||
debug: Joi.boolean().default(false),
|
||||
tenants: Joi.object().keys({
|
||||
enable_private: Joi.boolean().default(true),
|
||||
enable_global: Joi.boolean().default(true),
|
||||
preferred: Joi.array(),
|
||||
}).default(),
|
||||
}).default(),
|
||||
configuration: Joi.object().keys({
|
||||
enabled: Joi.boolean().default(true)
|
||||
}).default(),
|
||||
accountinfo: Joi.object().keys({
|
||||
enabled: Joi.boolean().default(false)
|
||||
}).default(),
|
||||
openid: Joi.object().keys({
|
||||
connect_url: Joi.string(),
|
||||
header: Joi.string().default('Authorization'),
|
||||
client_id: Joi.string(),
|
||||
client_secret: Joi.string().allow('').default(''),
|
||||
scope: Joi.string().default('openid profile email address phone'),
|
||||
base_redirect_url: Joi.string().allow('').default(''),
|
||||
logout_url: Joi.string().allow('').default(''),
|
||||
root_ca: Joi.string().allow('').default(''),
|
||||
verify_hostnames: Joi.boolean().default(true)
|
||||
}).default().when('auth.type', {
|
||||
is: 'openid',
|
||||
then: Joi.object({
|
||||
client_id: Joi.required(),
|
||||
connect_url: Joi.required()
|
||||
})
|
||||
}),
|
||||
proxycache: Joi.object().keys({
|
||||
user_header: Joi.string(),
|
||||
roles_header: Joi.string(),
|
||||
login_endpoint: Joi.string().allow('', null).default(null),
|
||||
}).default().when('auth.type', {
|
||||
is: 'proxycache',
|
||||
then: Joi.object({
|
||||
user_header: Joi.required(),
|
||||
roles_header: Joi.required()
|
||||
})
|
||||
}),
|
||||
jwt: Joi.object().keys({
|
||||
enabled: Joi.boolean().default(false),
|
||||
login_endpoint: Joi.string(),
|
||||
url_param: Joi.string().default('authorization'),
|
||||
header: Joi.string().default('Authorization')
|
||||
}).default()
|
||||
}).default();
|
||||
return obj;
|
||||
},
|
||||
|
||||
deprecations: function () {
|
||||
return [
|
||||
(settings, log) => {
|
||||
if (has(settings, 'basicauth.enabled')) {
|
||||
log('Config key "opendistro_security.basicauth.enabled" is deprecated. Please use "opendistro_security.auth.type" instead.');
|
||||
}
|
||||
|
||||
if (has(settings, 'jwt.enabled')) {
|
||||
log('Config key "opendistro_security.jwt.enabled" is deprecated. Please use "opendistro_security.auth.type" instead.');
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
uiExports: {
|
||||
hacks: [
|
||||
'plugins/opendistro_security/chrome/readonly/enable_readonly',
|
||||
'plugins/opendistro_security/chrome/multitenancy/enable_multitenancy',
|
||||
'plugins/opendistro_security/chrome/accountinfo/enable_accountinfo',
|
||||
'plugins/opendistro_security/chrome/logout_button',
|
||||
'plugins/opendistro_security/chrome/configuration/enable_configuration',
|
||||
'plugins/opendistro_security/services/access_control',
|
||||
'plugins/opendistro_security/customizations/enable_customizations.js'
|
||||
],
|
||||
replaceInjectedVars: async function(originalInjectedVars, request, server) {
|
||||
const authType = server.config().get('opendistro_security.auth.type');
|
||||
// Make sure securityDynamic is always available to the frontend, no matter what
|
||||
let securityDynamic = {};
|
||||
let userInfo = null;
|
||||
|
||||
try {
|
||||
// If the user is authenticated, just get the regular values
|
||||
if(request.auth.securitySessionStorage.isAuthenticated()) {
|
||||
let sessionCredentials = request.auth.securitySessionStorage.getSessionCredentials();
|
||||
userInfo = {
|
||||
username: sessionCredentials.username,
|
||||
isAnonymousAuth: sessionCredentials.isAnonymousAuth
|
||||
};
|
||||
} else if (['', 'kerberos', 'proxy'].indexOf(authType) > -1) {
|
||||
// We should be able to use this with kerberos and proxy too
|
||||
try {
|
||||
let authInfo = await request.auth.securitySessionStorage.getAuthInfo();
|
||||
userInfo = {
|
||||
username: authInfo.user_name
|
||||
};
|
||||
} catch(error) {
|
||||
// Not authenticated, so don't do anything
|
||||
}
|
||||
}
|
||||
|
||||
if (userInfo) {
|
||||
securityDynamic.user = userInfo;
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't to anything here.
|
||||
// If there's an error, it's probably because x-pack security is enabled.
|
||||
}
|
||||
|
||||
return {
|
||||
...originalInjectedVars,
|
||||
securityDynamic
|
||||
}
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
id: 'security-login',
|
||||
title: 'Login',
|
||||
main: 'plugins/opendistro_security/apps/login/login',
|
||||
hidden: true,
|
||||
auth: false
|
||||
},
|
||||
{
|
||||
id: 'security-customerror',
|
||||
title: 'CustomError',
|
||||
main: 'plugins/opendistro_security/apps/customerror/customerror',
|
||||
hidden: true,
|
||||
auth: false
|
||||
},
|
||||
{
|
||||
id: 'security-multitenancy',
|
||||
title: 'Tenants',
|
||||
main: 'plugins/opendistro_security/apps/multitenancy/multitenancy',
|
||||
hidden: false,
|
||||
auth: true,
|
||||
order: 9010,
|
||||
icon: 'plugins/opendistro_security/assets/networking.svg',
|
||||
linkToLastSubUrl: false,
|
||||
url: '/app/security-multitenancy#/'
|
||||
},
|
||||
{
|
||||
id: 'security-accountinfo',
|
||||
title: 'Account',
|
||||
main: 'plugins/opendistro_security/apps/accountinfo/accountinfo',
|
||||
hidden: false,
|
||||
auth: true,
|
||||
order: 9020,
|
||||
icon: 'plugins/opendistro_security/assets/info.svg',
|
||||
linkToLastSubUrl: false,
|
||||
url: '/app/security-accountinfo#/'
|
||||
},
|
||||
{
|
||||
id: 'security-configuration',
|
||||
title: 'Security',
|
||||
main: 'plugins/opendistro_security/apps/configuration/configuration',
|
||||
order: 9009,
|
||||
auth: true,
|
||||
icon: 'plugins/opendistro_security/assets/opendistro_security.svg',
|
||||
linkToLastSubUrl: false,
|
||||
url: '/app/security-configuration#/'
|
||||
}
|
||||
],
|
||||
chromeNavControls: [
|
||||
'plugins/opendistro_security/chrome/btn_logout/btn_logout.js'
|
||||
]
|
||||
,
|
||||
injectDefaultVars(server, options) {
|
||||
options.multitenancy_enabled = server.config().get('opendistro_security.multitenancy.enabled');
|
||||
options.accountinfo_enabled = server.config().get('opendistro_security.accountinfo.enabled');
|
||||
options.basicauth_enabled = server.config().get('opendistro_security.basicauth.enabled');
|
||||
options.kibana_index = server.config().get('kibana.index');
|
||||
options.kibana_server_user = server.config().get('elasticsearch.username');
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
init(server, options) {
|
||||
|
||||
APP_ROOT = '';
|
||||
API_ROOT = `${APP_ROOT}/api/v1`;
|
||||
const config = server.config();
|
||||
|
||||
// If X-Pack is installed it needs to be disabled for Security to run.
|
||||
try {
|
||||
let xpackInstalled = false;
|
||||
Object.keys(server.plugins).forEach((plugin) => {
|
||||
if (plugin.toLowerCase().indexOf('xpack') > -1) {
|
||||
xpackInstalled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (xpackInstalled && config.get('xpack.opendistro_security.enabled') !== false) {
|
||||
// It seems like X-Pack is installed and enabled, so we show an error message and then exit.
|
||||
this.status.red("X-Pack Security needs to be disabled for Security to work properly. Please set 'xpack.opendistro_security.enabled' to false in your kibana.yml");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
server.log(['error', 'security'], `An error occurred while making sure that X-Pack isn't enabled`);
|
||||
}
|
||||
|
||||
|
||||
// all your routes are belong to us
|
||||
require('./lib/auth/routes_authinfo')(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
|
||||
// provides authentication methods against Security
|
||||
const BackendClass = pluginRoot(`lib/backend/opendistro_security`);
|
||||
const securityBackend = new BackendClass(server, server.config);
|
||||
server.expose('getSecurityBackend', () => securityBackend);
|
||||
|
||||
// provides configuration methods against Security
|
||||
const ConfigurationBackendClass = pluginRoot(`lib/configuration/backend/opendistro_security_configuration_backend`);
|
||||
const securityConfigurationBackend = new ConfigurationBackendClass(server, server.config);
|
||||
server.expose('getSecurityConfigurationBackend', () => securityConfigurationBackend);
|
||||
|
||||
server.register([require('hapi-async-handler')]);
|
||||
|
||||
let authType = config.get('opendistro_security.auth.type');
|
||||
let authClass = null;
|
||||
|
||||
// For legacy code
|
||||
if (! authType) {
|
||||
if (config.get('opendistro_security.basicauth.enabled')) {
|
||||
authType = 'basicauth';
|
||||
} else if(config.get('opendistro_security.jwt.enabled')) {
|
||||
authType = 'jwt';
|
||||
}
|
||||
|
||||
// Dynamically update the auth.type to make it available to the frontend
|
||||
if (authType) {
|
||||
config.set('opendistro_security.auth.type', authType);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the storage cookie
|
||||
server.state('security_storage', {
|
||||
path: '/',
|
||||
ttl: null, // Cookie deleted when the browser is closed
|
||||
password: config.get('opendistro_security.cookie.password'),
|
||||
encoding: 'iron',
|
||||
isSecure: config.get('opendistro_security.cookie.secure'),
|
||||
});
|
||||
|
||||
if (authType && authType !== '' && ['basicauth', 'jwt', 'openid', 'saml', 'proxycache'].indexOf(authType) > -1) {
|
||||
|
||||
server.register([
|
||||
require('hapi-auth-cookie'),
|
||||
], (error) => {
|
||||
|
||||
if (error) {
|
||||
server.log(['error', 'security'], `An error occurred registering server plugins: ${error}`);
|
||||
this.status.red('An error occurred during initialisation, please check the logs.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.status.yellow('Initialising Security authentication plugin.');
|
||||
|
||||
if (config.get("opendistro_security.cookie.password") == 'security_cookie_default_password') {
|
||||
this.status.yellow("Default cookie password detected, please set a password in kibana.yml by setting 'opendistro_security.cookie.password' (min. 32 characters).");
|
||||
}
|
||||
|
||||
if (!config.get("opendistro_security.cookie.secure")) {
|
||||
this.status.yellow("'opendistro_security.cookie.secure' is set to false, cookies are transmitted over unsecure HTTP connection. Consider using HTTPS and set this key to 'true'");
|
||||
}
|
||||
|
||||
if (authType === 'openid') {
|
||||
|
||||
let OpenId = require('./lib/auth/types/openid/OpenId');
|
||||
authClass = new OpenId(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
} else if (authType == 'basicauth') {
|
||||
let BasicAuth = require('./lib/auth/types/basicauth/BasicAuth');
|
||||
authClass = new BasicAuth(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
} else if (authType == 'jwt') {
|
||||
let Jwt = require('./lib/auth/types/jwt/Jwt');
|
||||
authClass = new Jwt(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
this.status.yellow("Security copy JWT params registered.");
|
||||
} else if (authType == 'saml') {
|
||||
let Saml = require('./lib/auth/types/saml/Saml');
|
||||
authClass = new Saml(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
} else if (authType == 'proxycache') {
|
||||
let ProxyCache = require('./lib/auth/types/proxycache/ProxyCache');
|
||||
authClass = new ProxyCache(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
}
|
||||
|
||||
if (authClass) {
|
||||
authClass.init();
|
||||
this.status.yellow('Security session management enabled.');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
// Register the storage plugin for the other auth types
|
||||
server.register({
|
||||
register: pluginRoot('lib/session/sessionPlugin'),
|
||||
options: {
|
||||
authType: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (authType != 'jwt') {
|
||||
this.status.yellow("Security copy JWT params disabled");
|
||||
}
|
||||
|
||||
if (config.get('opendistro_security.multitenancy.enabled')) {
|
||||
|
||||
// sanity check - header whitelisted?
|
||||
var headersWhitelist = config.get('elasticsearch.requestHeadersWhitelist');
|
||||
if (headersWhitelist.indexOf('securitytenant') == -1) {
|
||||
this.status.red('No tenant header found in whitelist. Please add securitytenant to elasticsearch.requestHeadersWhitelist in kibana.yml');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.has('xpack.spaces.enabled') && config.get('xpack.spaces.enabled')) {
|
||||
this.status.red('At the moment it is not possible to have both Spaces and multitenancy enabled. Please set xpack.spaces.enabled to false.');
|
||||
return;
|
||||
}
|
||||
|
||||
require('./lib/multitenancy/routes')(pluginRoot, server, this, APP_ROOT, API_ROOT);
|
||||
require('./lib/multitenancy/headers')(pluginRoot, server, this, APP_ROOT, API_ROOT, authClass);
|
||||
|
||||
server.state('security_preferences', {
|
||||
ttl: 2217100485000,
|
||||
path: '/',
|
||||
isSecure: false,
|
||||
isHttpOnly: false,
|
||||
clearInvalid: true, // remove invalid cookies
|
||||
strictHeader: true, // don't allow violations of RFC 6265
|
||||
encoding: 'iron',
|
||||
password: config.get("opendistro_security.cookie.password")
|
||||
});
|
||||
|
||||
this.status.yellow("Security multitenancy registered.");
|
||||
} else {
|
||||
this.status.yellow("Security multitenancy disabled");
|
||||
}
|
||||
|
||||
// Assign auth header after MT
|
||||
if (authClass) {
|
||||
authClass.registerAssignAuthHeader();
|
||||
}
|
||||
|
||||
if (config.get('opendistro_security.configuration.enabled')) {
|
||||
require('./lib/configuration/routes/routes')(pluginRoot, server, APP_ROOT, API_ROOT);
|
||||
this.status.yellow("Routes for Security configuration GUI registered.");
|
||||
} else {
|
||||
this.status.yellow("Security configuration GUI disabled");
|
||||
}
|
||||
|
||||
// create index template for tenant indices
|
||||
if(config.get('opendistro_security.multitenancy.enabled')) {
|
||||
const { setupIndexTemplate, waitForElasticsearchGreen } = indexTemplate(this, server);
|
||||
//const {migrateTenants} = tenantMigrator(this, server);
|
||||
|
||||
waitForElasticsearchGreen().then( () => {
|
||||
this.status.yellow('Setting up index template.');
|
||||
setupIndexTemplate();
|
||||
|
||||
migrateTenants(server)
|
||||
.then( () => {
|
||||
this.status.green('Tenant indices migrated.');
|
||||
})
|
||||
.catch((error) => {
|
||||
this.status.yellow('Tenant indices migration failed');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
} else {
|
||||
this.status.green('Security plugin initialised.');
|
||||
}
|
||||
|
||||
// Using an admin certificate may lead to unintended consequences
|
||||
if ((typeof config.get('elasticsearch.ssl.certificate') !== 'undefined' && typeof config.get('elasticsearch.ssl.certificate') !== false) && config.get('opendistro_security.allow_client_certificates') !== true) {
|
||||
this.status.red("'elasticsearch.ssl.certificate' can not be used without setting 'opendistro_security.allow_client_certificates' to 'true' in kibana.yml. Please refer to the documentation for more information about the implications of doing so.");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
40
lib/auth/errors/authentication_error.js
Normal file
40
lib/auth/errors/authentication_error.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
export default class AuthenticationError extends Error {
|
||||
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
|
||||
}
|
41
lib/auth/errors/invalid_session_error.js
Normal file
41
lib/auth/errors/invalid_session_error.js
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
export default class InvalidSessionError extends Error {
|
||||
|
||||
constructor(message, inner) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
}
|
40
lib/auth/errors/missing_role_error.js
Normal file
40
lib/auth/errors/missing_role_error.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
export default class MissingRoleError extends Error {
|
||||
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
|
||||
}
|
40
lib/auth/errors/missing_tenant_error.js
Normal file
40
lib/auth/errors/missing_tenant_error.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
export default class MissingTenantError extends Error {
|
||||
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
|
||||
}
|
40
lib/auth/errors/session_expired_error.js
Normal file
40
lib/auth/errors/session_expired_error.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
export default class SessionExpiredError extends Error {
|
||||
|
||||
constructor(message, inner) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
}
|
20
lib/auth/filter_auth_headers.js
Normal file
20
lib/auth/filter_auth_headers.js
Normal file
@ -0,0 +1,20 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function (originalHeaders, headersToKeep) {
|
||||
|
||||
const normalizeHeader = function (header) {
|
||||
if (!header) {
|
||||
return '';
|
||||
}
|
||||
header = header.toString();
|
||||
return header.trim().toLowerCase();
|
||||
};
|
||||
|
||||
const headersToKeepNormalized = headersToKeep.map(normalizeHeader);
|
||||
|
||||
const originalHeadersNormalized = _.mapKeys(originalHeaders, function (headerValue, headerName) {
|
||||
return normalizeHeader(headerName);
|
||||
});
|
||||
|
||||
return _.pick(originalHeaders, headersToKeepNormalized);
|
||||
}
|
58
lib/auth/parseNextUrl.js
Normal file
58
lib/auth/parseNextUrl.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
import { parse } from 'url';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param nextUrl - Only the query parameter value, instead of the complete url. We don't always have the full url.
|
||||
* @param basePath
|
||||
* @returns {*}
|
||||
*/
|
||||
export function parseNextUrl(nextUrl, basePath) {
|
||||
|
||||
// check forgery of protocol, hostname, port, pathname
|
||||
const { protocol, hostname, port, pathname, hash } = parse(decodeURIComponent(nextUrl), true, true);
|
||||
// If we have a relative protocol, hostname is reported as an empty string, so we need to make sure it is null
|
||||
if (protocol !== null || hostname !== null || port !== null) {
|
||||
return `${basePath}/`;
|
||||
}
|
||||
|
||||
// We always need the base path
|
||||
if (!String(pathname).startsWith(basePath)) {
|
||||
return `${basePath}/${nextUrl}`;
|
||||
}
|
||||
|
||||
// All valid
|
||||
return nextUrl
|
||||
|
||||
}
|
141
lib/auth/routes.js
Normal file
141
lib/auth/routes.js
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
const AuthenticationError = pluginRoot('lib/auth/errors/authentication_error');
|
||||
const config = server.config();
|
||||
const sessionTTL = config.get('opendistro_security.session.ttl');
|
||||
const loginApp = server.getHiddenUiAppById('security-login');
|
||||
|
||||
/**
|
||||
* The login page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/login`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(loginApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/login`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
// In order to prevent direct access for certain usernames (e.g. service users like
|
||||
// kibanaserver, logstash etc.) we can add them to basicauth.forbidden_usernames.
|
||||
// If the username in the payload matches an item in the forbidden array, we throw an AuthenticationError
|
||||
const basicAuthConfig = server.config().get('opendistro_security.basicauth');
|
||||
if (basicAuthConfig.forbidden_usernames && basicAuthConfig.forbidden_usernames.length) {
|
||||
if (request.payload && request.payload.username && basicAuthConfig.forbidden_usernames.indexOf(request.payload.username) > -1) {
|
||||
throw new AuthenticationError('Invalid username or password');
|
||||
}
|
||||
}
|
||||
|
||||
let user = await server.plugins.opendistro_security.getSecurityBackend().authenticate(request.payload);
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: user.credentials,
|
||||
proxyCredentials: user.proxyCredentials
|
||||
};
|
||||
if (sessionTTL) {
|
||||
session.expiryTime = Date.now() + sessionTTL;
|
||||
}
|
||||
request.auth.session.set(session);
|
||||
|
||||
// handle tenants if MT is enabled
|
||||
if(server.config().get("opendistro_security.multitenancy.enabled")) {
|
||||
|
||||
// get the preferred tenant of the user
|
||||
let globalTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_global");
|
||||
let privateTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_private");
|
||||
let preferredTenants = server.config().get("opendistro_security.multitenancy.tenants.preferred");
|
||||
|
||||
let finalTenant = server.plugins.opendistro_security.getSecurityBackend().getTenantByPreference(request, user.username, user.tenants, preferredTenants, globalTenantEnabled, privateTenantEnabled);
|
||||
return reply({
|
||||
username: user.username,
|
||||
tenants: user.tenants,
|
||||
roles: user.roles,
|
||||
backendroles: user.backendroles,
|
||||
selectedTenant: user.selectedTenant,
|
||||
}).state('security_tenant', finalTenant);
|
||||
} else {
|
||||
// no MT, nothing more to do
|
||||
return reply({
|
||||
username: user.username,
|
||||
tenants: user.tenants
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply(Boom.unauthorized(error.message));
|
||||
} else {
|
||||
return reply(Boom.badImplementation(error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
config: {
|
||||
validate: {
|
||||
payload: {
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required()
|
||||
}
|
||||
},
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/logout`,
|
||||
handler: (request, reply) => {
|
||||
request.auth.session.clear();
|
||||
reply({}).unstate('security_tenant');
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
53
lib/auth/routes_authinfo.js
Normal file
53
lib/auth/routes_authinfo.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/auth/authinfo`,
|
||||
handler: (request, reply) => {
|
||||
try {
|
||||
let authinfo = server.plugins.opendistro_security.getSecurityBackend().authinfo(request.headers);
|
||||
return reply(authinfo);
|
||||
} catch(error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
308
lib/auth/types/AuthType.js
Normal file
308
lib/auth/types/AuthType.js
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import { assign } from 'lodash';
|
||||
import Boom from 'boom';
|
||||
import InvalidSessionError from "../errors/invalid_session_error";
|
||||
import SessionExpiredError from "../errors/session_expired_error";
|
||||
|
||||
export default class AuthType {
|
||||
|
||||
constructor(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
this.pluginRoot = pluginRoot;
|
||||
this.server = server;
|
||||
this.kbnServer = kbnServer;
|
||||
this.APP_ROOT = APP_ROOT;
|
||||
this.API_ROOT = API_ROOT;
|
||||
this.config = server.config();
|
||||
|
||||
this.basePath = this.config.get('server.basePath');
|
||||
this.unauthenticatedRoutes = this.config.get('opendistro_security.auth.unauthenticated_routes');
|
||||
|
||||
this.sessionTTL = this.config.get('opendistro_security.session.ttl');
|
||||
this.sessionKeepAlive = this.config.get('opendistro_security.session.keepalive');
|
||||
|
||||
/**
|
||||
* The authType is saved in the auth cookie for later reference
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = null;
|
||||
|
||||
/**
|
||||
* Tells the sessionPlugin whether or not to validate the number of tenants when authenticating
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.validateAvailableTenants = true;
|
||||
|
||||
/**
|
||||
* The name of the header were we look for an authorization value.
|
||||
* This should most likely be set in the subclass depending on a config value.
|
||||
* @type {string}
|
||||
*/
|
||||
this.authHeaderName = 'authorization';
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupStorage();
|
||||
this.setupAuthScheme();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
setupStorage() {
|
||||
this.server.register({
|
||||
register: this.pluginRoot('lib/session/sessionPlugin'),
|
||||
options: {
|
||||
authType: this.type,
|
||||
authHeaderName: this.authHeaderName,
|
||||
authenticateFunction: this.authenticate.bind(this),
|
||||
validateAvailableTenants: this.validateAvailableTenants
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getCookieConfig() {
|
||||
const cookieConfig = {
|
||||
password: this.config.get('opendistro_security.cookie.password'),
|
||||
cookie: this.config.get('opendistro_security.cookie.name'),
|
||||
isSecure: this.config.get('opendistro_security.cookie.secure'),
|
||||
validateFunc: this.sessionValidator(this.server),
|
||||
clearInvalid: true,
|
||||
ttl: this.config.get('opendistro_security.cookie.ttl')
|
||||
};
|
||||
|
||||
return cookieConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the auth header needed for the Security backend
|
||||
* @param session
|
||||
* @returns {*}
|
||||
*/
|
||||
getAuthHeader(session) {
|
||||
if (session.credentials && session.credentials.authHeaderValue) {
|
||||
return {
|
||||
[this.authHeaderName]: session.credentials.authHeaderValue
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have an authorization header.
|
||||
*
|
||||
* Pass the existing session credentials to compare with the authorization header.
|
||||
*
|
||||
* @param request
|
||||
* @param sessionCredentials
|
||||
* @returns {object|null} - credentials for the authentication
|
||||
*/
|
||||
detectAuthHeaderCredentials(request, sessionCredentials = null) {
|
||||
|
||||
if (request.headers[this.authHeaderName]) {
|
||||
const authHeaderValue = request.headers[this.authHeaderName];
|
||||
|
||||
// If we have sessionCredentials AND auth headers we need to check if they are the same.
|
||||
if (sessionCredentials !== null && sessionCredentials.authHeaderValue === authHeaderValue) {
|
||||
// The auth header credentials are the same as those in the session,
|
||||
// no need to return new credentials so we're just nulling the token here
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
authHeaderValue: authHeaderValue
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
authenticate(credentials) {
|
||||
throw new Error('The authenticate method must be implemented by the sub class');
|
||||
}
|
||||
|
||||
onUnAuthenticated(request, reply) {
|
||||
throw new Error('The onUnAuthenticated method must be implemented by the sub class');
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
throw new Error('The getAuthHeader method must be implemented by the sub class');
|
||||
}
|
||||
|
||||
setupAuthScheme() {
|
||||
this.server.auth.strategy('security_access_control_cookie', 'cookie', false, this.getCookieConfig());
|
||||
this.server.auth.scheme('security_access_control_scheme', (server, options) => ({
|
||||
authenticate: (request, reply) => {
|
||||
// let configured routes that are not under our control pass,
|
||||
// for example /api/status to check Kibana status without a logged in user
|
||||
if (this.unauthenticatedRoutes.includes(request.path)) {
|
||||
var credentials = this.server.plugins.opendistro_security.getSecurityBackend().getServerUser();
|
||||
reply.continue({credentials});
|
||||
return;
|
||||
};
|
||||
|
||||
this.server.auth.test('security_access_control_cookie', request, async(error, credentials) => {
|
||||
if (error) {
|
||||
let authHeaderCredentials = this.detectAuthHeaderCredentials(request);
|
||||
if (authHeaderCredentials) {
|
||||
try {
|
||||
let {session} = await request.auth.securitySessionStorage.authenticate(authHeaderCredentials);
|
||||
|
||||
// Returning the session equals setting the values with hapi-auth-cookie@set()
|
||||
return reply.continue({
|
||||
// Watch out here - hapi-auth-cookie requires us to send back an object with credentials
|
||||
// as a key. Otherwise other values than the credentials will be overwritten
|
||||
credentials: session
|
||||
});
|
||||
} catch (authError) {
|
||||
return this.onUnAuthenticated(request, reply, authError);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.headers) {
|
||||
// If the session has expired, we may receive ajax requests that can't handle a 302 redirect.
|
||||
// In this case, we trigger a 401 and let the interceptor handle the redirect on the client side.
|
||||
if ((request.headers.accept && request.headers.accept.split(',').indexOf('application/json') > -1)
|
||||
|| (request.headers['content-type'] && request.headers['content-type'].indexOf('application/json') > -1)) {
|
||||
return reply({message: 'Session expired', redirectTo: 'login'}).code(401);
|
||||
}
|
||||
|
||||
// Cookie auth failed, user is not authenticated
|
||||
return this.onUnAuthenticated(request, reply, error);
|
||||
}
|
||||
}
|
||||
// credentials are everything that is in the auth cookie
|
||||
reply.continue(credentials);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
// Activates hapi-auth-cookie for ALL routes, unless
|
||||
// a) the route is listed in "unauthenticatedRoutes" or
|
||||
// b) the auth option in the route definition is explicitly set to false
|
||||
this.server.auth.strategy('security_access_control', 'security_access_control_scheme', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a session auth cookie exists, the sessionValidator is called to validate the content
|
||||
* @param server
|
||||
* @returns {validate}
|
||||
*/
|
||||
sessionValidator(server) {
|
||||
|
||||
let validate = async(request, session, callback) => {
|
||||
|
||||
if (session.authType !== this.type) {
|
||||
return callback(new InvalidSessionError('Invalid session'), false, null);
|
||||
}
|
||||
|
||||
// Check if we have auth header credentials set that are different from the session credentials
|
||||
let differentAuthHeaderCredentials = this.detectAuthHeaderCredentials(request, session.credentials);
|
||||
if (differentAuthHeaderCredentials) {
|
||||
try {
|
||||
let authResponse = await request.auth.securitySessionStorage.authenticate(differentAuthHeaderCredentials);
|
||||
return callback(null, true, {credentials: authResponse.session});
|
||||
} catch(error) {
|
||||
request.auth.securitySessionStorage.clearStorage();
|
||||
return callback(error, false);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are still here, we need to compare the expiration time
|
||||
// JWT's .exp is denoted in seconds, not milliseconds.
|
||||
if (session.exp && session.exp < Math.floor(Date.now() / 1000)) {
|
||||
request.auth.securitySessionStorage.clearStorage();
|
||||
return callback(new SessionExpiredError('Session expired.'), false);
|
||||
} else if (!session.exp && this.sessionTTL) {
|
||||
if (!session.expiryTime || session.expiryTime < Date.now()) {
|
||||
request.auth.securitySessionStorage.clearStorage();
|
||||
return callback(new SessionExpiredError('Session expired.'), false);
|
||||
}
|
||||
|
||||
if (this.sessionKeepAlive) {
|
||||
session.expiryTime = Date.now() + this.sessionTTL;
|
||||
// According to the documentation, returning the session in the cookie
|
||||
// should be equivalent to calling request.auth.session.set(),
|
||||
// but it seems like the cookie's browser lifetime isn't updated.
|
||||
// Hence, we need to set it explicitly.
|
||||
request.auth.session.set(session);
|
||||
}
|
||||
}
|
||||
|
||||
// All good, return the session as it was
|
||||
return callback(null, true, {credentials: session});
|
||||
|
||||
};
|
||||
|
||||
return validate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add credential headers to the passed request.
|
||||
* @param request
|
||||
*/
|
||||
assignAuthHeader(request) {
|
||||
|
||||
if (! request.headers[this.authHeaderName]) {
|
||||
|
||||
const session = request.state[this.config.get('opendistro_security.cookie.name')];
|
||||
if (session && session.credentials) {
|
||||
try {
|
||||
let authHeader = this.getAuthHeader(session);
|
||||
if (authHeader !== false) {
|
||||
assign(request.headers, authHeader);
|
||||
}
|
||||
} catch (error) {
|
||||
this.server.log(['security', 'error'], `An error occurred while computing auth headers, clearing session: ${error}`);
|
||||
request.auth.securitySessionStorage.clear();
|
||||
throw error;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on each authenticated request.
|
||||
* Used to add the credentials header to the request.
|
||||
*/
|
||||
registerAssignAuthHeader() {
|
||||
this.server.ext('onPreAuth', (request, next) => {
|
||||
try {
|
||||
this.assignAuthHeader(request);
|
||||
} catch(error) {
|
||||
return next.redirect(this.basePath + '/customerror?type=authError');
|
||||
}
|
||||
|
||||
return next.continue();
|
||||
});
|
||||
}
|
||||
}
|
150
lib/auth/types/basicauth/BasicAuth.js
Normal file
150
lib/auth/types/basicauth/BasicAuth.js
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import AuthType from "../AuthType";
|
||||
import MissingRoleError from "../../errors/missing_role_error";
|
||||
|
||||
export default class BasicAuth extends AuthType {
|
||||
|
||||
constructor(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
super(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT);
|
||||
|
||||
/**
|
||||
* The authType is saved in the auth cookie for later reference
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = 'basicauth';
|
||||
|
||||
/**
|
||||
* The name of the authorization header to be used
|
||||
* @type {string}
|
||||
*/
|
||||
this.authHeaderName = 'authorization';
|
||||
|
||||
/**
|
||||
* Redirect to a loadbalancer url instead of a relative path when unauthenticated?
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.loadBalancerURL = this.config.get('opendistro_security.basicauth.loadbalancer_url');
|
||||
|
||||
/**
|
||||
* Allow anonymous access?
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.anonymousAuthEnabled = this.config.get('opendistro_security.auth.anonymous_auth_enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have an authorization header.
|
||||
*
|
||||
* Pass the existing session credentials to compare with the authorization header.
|
||||
*
|
||||
* @param request
|
||||
* @param sessionCredentials
|
||||
* @returns {object|null} - credentials for the authentication
|
||||
*/
|
||||
detectAuthHeaderCredentials(request, sessionCredentials = null) {
|
||||
|
||||
if (request.headers[this.authHeaderName]) {
|
||||
|
||||
const authHeaderValue = request.headers[this.authHeaderName];
|
||||
const headerTrumpsSession = this.config.get('opendistro_security.basicauth.header_trumps_session');
|
||||
|
||||
// If we have sessionCredentials AND auth headers we need to check if they are the same.
|
||||
if (sessionCredentials !== null && sessionCredentials.authHeaderValue === authHeaderValue) {
|
||||
// The auth header credentials are the same as those in the session,
|
||||
// no need to return new credentials so we're just nulling the token here
|
||||
return null;
|
||||
}
|
||||
|
||||
// We may have an auth header for a different user than the user saved in the session.
|
||||
// To avoid confusion, we do NOT override the cookie user, unless explicitly configured to do so.
|
||||
if (sessionCredentials !== null && ! headerTrumpsSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
authHeaderValue: authHeaderValue
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async authenticate(credentials, options = {}) {
|
||||
|
||||
// A login can happen via a POST request (login form) or when we have request headers with user credentials.
|
||||
// We also need to re-authenticate if the credentials (headers) don't match what's in the session.
|
||||
try {
|
||||
let user = await this.server.plugins.opendistro_security.getSecurityBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue);
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: credentials,
|
||||
authType: this.type,
|
||||
isAnonymousAuth: (options && options.isAnonymousAuth === true) ? true : false
|
||||
};
|
||||
|
||||
if(this.sessionTTL) {
|
||||
session.expiryTime = Date.now() + this.sessionTTL
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
}
|
||||
} catch(error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onUnAuthenticated(request, reply, error) {
|
||||
if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingRole')
|
||||
}
|
||||
|
||||
const nextUrl = encodeURIComponent(request.url.path);
|
||||
|
||||
if (this.anonymousAuthEnabled) {
|
||||
return reply.redirect(`${this.basePath}${this.APP_ROOT}/auth/anonymous?nextUrl=${nextUrl}`);
|
||||
}
|
||||
|
||||
if (this.loadBalancerURL) {
|
||||
return reply.redirect(`${this.loadBalancerURL}${this.basePath}${this.APP_ROOT}/login?nextUrl=${nextUrl}`);
|
||||
}
|
||||
|
||||
return reply.redirect(`${this.basePath}${this.APP_ROOT}/login?nextUrl=${nextUrl}`);
|
||||
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT);
|
||||
}
|
||||
}
|
238
lib/auth/types/basicauth/routes.js
Normal file
238
lib/auth/types/basicauth/routes.js
Normal file
@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
import MissingRoleError from "../../errors/missing_role_error";
|
||||
import {parseNextUrl} from "../../parseNextUrl";
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
const AuthenticationError = pluginRoot('lib/auth/errors/authentication_error');
|
||||
const loginApp = server.getHiddenUiAppById('security-login');
|
||||
const config = server.config();
|
||||
const customErrorApp = server.getHiddenUiAppById('security-customerror');
|
||||
|
||||
/**
|
||||
* The login page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/login`,
|
||||
handler: {
|
||||
async: async(request, reply) => {
|
||||
try {
|
||||
const basePath = config.get('server.basePath');
|
||||
|
||||
// Check if we have alternative login headers
|
||||
const alternativeHeaders = config.get('opendistro_security.basicauth.alternative_login.headers');
|
||||
if (alternativeHeaders && alternativeHeaders.length) {
|
||||
let requestHeaders = Object.keys(request.headers).map(header => header.toLowerCase());
|
||||
let foundHeaders = alternativeHeaders.filter(header => requestHeaders.indexOf(header.toLowerCase()) > -1);
|
||||
if (foundHeaders.length) {
|
||||
let {session} = await request.auth.securitySessionStorage.authenticateWithHeaders(request.headers);
|
||||
|
||||
let nextUrl = null;
|
||||
if (request.url && request.url.query && request.url.query.nextUrl) {
|
||||
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
|
||||
}
|
||||
|
||||
if (nextUrl) {
|
||||
nextUrl = parseNextUrl(nextUrl, basePath);
|
||||
return reply.redirect(nextUrl);
|
||||
}
|
||||
|
||||
return reply.redirect(basePath + '/app/kibana');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingRole');
|
||||
} else if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingTenant');
|
||||
}
|
||||
// Let normal authentication errors through(?) and just go to the regular login page?
|
||||
}
|
||||
|
||||
return reply.renderAppWithDefaultConfig(loginApp);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/login`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
// In order to prevent direct access for certain usernames (e.g. service users like
|
||||
// kibanaserver, logstash etc.) we can add them to basicauth.forbidden_usernames.
|
||||
// If the username in the payload matches an item in the forbidden array, we throw an AuthenticationError
|
||||
const basicAuthConfig = server.config().get('opendistro_security.basicauth');
|
||||
if (basicAuthConfig.forbidden_usernames && basicAuthConfig.forbidden_usernames.length) {
|
||||
if (request.payload && request.payload.username && basicAuthConfig.forbidden_usernames.indexOf(request.payload.username) > -1) {
|
||||
throw new AuthenticationError('Invalid username or password');
|
||||
}
|
||||
}
|
||||
|
||||
const authHeaderValue = new Buffer(`${request.payload.username}:${request.payload.password}`).toString('base64');
|
||||
let {user} = await request.auth.securitySessionStorage.authenticate({
|
||||
authHeaderValue: 'Basic ' + authHeaderValue
|
||||
});
|
||||
|
||||
|
||||
// handle tenants if MT is enabled
|
||||
if(server.config().get("opendistro_security.multitenancy.enabled")) {
|
||||
|
||||
// get the preferred tenant of the user
|
||||
let globalTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_global");
|
||||
let privateTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_private");
|
||||
let preferredTenants = server.config().get("opendistro_security.multitenancy.tenants.preferred");
|
||||
|
||||
let finalTenant = server.plugins.opendistro_security.getSecurityBackend().getTenantByPreference(request, user.username, user.tenants, preferredTenants, globalTenantEnabled, privateTenantEnabled);
|
||||
|
||||
request.auth.securitySessionStorage.putStorage('tenant', {
|
||||
selected: finalTenant
|
||||
});
|
||||
|
||||
return reply({
|
||||
username: user.username,
|
||||
tenants: user.tenants,
|
||||
roles: user.roles,
|
||||
backendroles: user.backendroles,
|
||||
selectedTenant: user.selectedTenant,
|
||||
});
|
||||
} else {
|
||||
// no MT, nothing more to do
|
||||
return reply({
|
||||
username: user.username,
|
||||
tenants: user.tenants
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply(Boom.unauthorized(error.message));
|
||||
} else if (error instanceof MissingTenantError) {
|
||||
return reply(Boom.notFound(error.message));
|
||||
} else if (error instanceof MissingRoleError) {
|
||||
return reply(Boom.notFound(error.message));
|
||||
} else {
|
||||
return reply(Boom.badImplementation(error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
config: {
|
||||
validate: {
|
||||
payload: {
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required()
|
||||
}
|
||||
},
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/logout`,
|
||||
handler: (request, reply) => {
|
||||
request.auth.securitySessionStorage.clear();
|
||||
reply({});
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/auth/anonymous`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
|
||||
if (server.config().get('opendistro_security.auth.anonymous_auth_enabled')) {
|
||||
const basePath = server.config().get('server.basePath');
|
||||
try {
|
||||
let {session} = await request.auth.securitySessionStorage.authenticate({}, {isAnonymousAuth: true});
|
||||
|
||||
let nextUrl = null;
|
||||
if (request.url && request.url.query && request.url.query.nextUrl) {
|
||||
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
|
||||
}
|
||||
|
||||
if (nextUrl) {
|
||||
nextUrl = parseNextUrl(nextUrl, basePath);
|
||||
return reply.redirect(nextUrl);
|
||||
}
|
||||
|
||||
return reply.redirect(basePath + '/app/kibana');
|
||||
|
||||
} catch (error) {
|
||||
|
||||
if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingRole');
|
||||
} else if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingTenant');
|
||||
} else {
|
||||
return reply.redirect(basePath + '/customerror?type=anonymousAuthError');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return reply.redirect(`${APP_ROOT}/login`);
|
||||
}
|
||||
}
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The error page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/customerror`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
185
lib/auth/types/jwt/Jwt.js
Normal file
185
lib/auth/types/jwt/Jwt.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import AuthType from "../AuthType";
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
import SessionExpiredError from "../../errors/session_expired_error";
|
||||
import {parse, format} from 'url';
|
||||
import MissingRoleError from "../../errors/missing_role_error";
|
||||
|
||||
export default class Jwt extends AuthType {
|
||||
|
||||
constructor(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
super(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT);
|
||||
|
||||
/**
|
||||
* The authType is saved in the auth cookie for later reference
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = 'jwt';
|
||||
|
||||
try {
|
||||
this.authHeaderName = this.config.get('opendistro_security.jwt.header').toLowerCase();
|
||||
} catch(error) {
|
||||
this.kbnServer.status.yellow('No authorization header name defined for JWT, using "authorization"');
|
||||
this.authHeaderName = 'authorization'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect authorization header value, either as an http header or as a query parameter
|
||||
* @param request
|
||||
* @param sessionCredentials
|
||||
* @returns {*}
|
||||
*/
|
||||
detectAuthHeaderCredentials(request, sessionCredentials = null) {
|
||||
|
||||
let authHeaderValue = null;
|
||||
const urlparamname = this.config.get('opendistro_security.jwt.url_param').toLowerCase();
|
||||
|
||||
// Go through all given query parameters and make them lowercase
|
||||
// to avoid confusion when using uppercase or perhaps mixed caps
|
||||
let lowerCaseQueryParameters = {};
|
||||
Object.keys(request.query).forEach((query) => {
|
||||
lowerCaseQueryParameters[query.toLowerCase()] = request.query[query];
|
||||
});
|
||||
|
||||
let jwtAuthParam = lowerCaseQueryParameters[urlparamname] || null;
|
||||
|
||||
// The token may be passed via a query parameter
|
||||
if (jwtAuthParam != null) {
|
||||
authHeaderValue = 'Bearer ' + jwtAuthParam;
|
||||
request.headers[this.authHeaderName] = authHeaderValue;
|
||||
} else if (request.headers[this.authHeaderName]) {
|
||||
try {
|
||||
authHeaderValue = request.headers[this.authHeaderName];
|
||||
} catch (error) {
|
||||
console.log('Something went wrong when getting the JWT bearer from the header', request.headers)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have sessionCredentials AND auth headers we need to check if they are the same.
|
||||
if (authHeaderValue !== null && sessionCredentials !== null && sessionCredentials.authHeaderValue === authHeaderValue) {
|
||||
// The auth header credentials are the same as those in the session,
|
||||
// no need to return new credentials so we're just nulling the token here
|
||||
return null
|
||||
}
|
||||
|
||||
if (authHeaderValue !== null) {
|
||||
return {
|
||||
authHeaderValue: authHeaderValue
|
||||
}
|
||||
}
|
||||
|
||||
return authHeaderValue;
|
||||
}
|
||||
|
||||
async authenticate(credentials) {
|
||||
// A "login" can happen when we have a token (as header or as URL parameter but no session,
|
||||
// or when we have an existing session, but the passed token does not match what's in the session.
|
||||
try {
|
||||
let user = await this.server.plugins.opendistro_security.getSecurityBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue);
|
||||
let tokenPayload = {};
|
||||
try {
|
||||
tokenPayload = JSON.parse(Buffer.from(credentials.authHeaderValue.split('.')[1], 'base64').toString());
|
||||
} catch (error) {
|
||||
// Something went wrong while parsing the payload, but the user was authenticated correctly.
|
||||
}
|
||||
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: credentials,
|
||||
authType: this.type
|
||||
};
|
||||
|
||||
if (tokenPayload.exp) {
|
||||
// The token's exp value trumps the config setting
|
||||
this.sessionKeepAlive = false;
|
||||
session.exp = parseInt(tokenPayload.exp, 10);
|
||||
} else if(this.sessionTTL) {
|
||||
session.expiryTime = Date.now() + this.sessionTTL
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onUnAuthenticated(request, reply, error) {
|
||||
|
||||
if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingTenant');
|
||||
} else if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingRole');
|
||||
} else {
|
||||
// The customer may use a login endpoint, to which we can redirect
|
||||
// if the user isn't authenticated.
|
||||
let loginEndpoint = this.config.get('opendistro_security.jwt.login_endpoint');
|
||||
if (loginEndpoint) {
|
||||
try {
|
||||
// Parse the login endpoint so that we can append our nextUrl
|
||||
// if the customer has defined query parameters in the endpoint
|
||||
let loginEndpointURLObject = parse(loginEndpoint, true);
|
||||
|
||||
// Make sure we don't overwrite an existing "nextUrl" parameter,
|
||||
// just in case the customer is using that name for something else
|
||||
if (typeof loginEndpointURLObject.query['nextUrl'] === 'undefined') {
|
||||
const nextUrl = encodeURIComponent(request.url.path);
|
||||
// Delete the search parameter - otherwise format() will use its value instead of the .query property
|
||||
delete loginEndpointURLObject.search;
|
||||
loginEndpointURLObject.query['nextUrl'] = nextUrl;
|
||||
}
|
||||
// Format the parsed endpoint object into a URL and redirect
|
||||
return reply.redirect(format(loginEndpointURLObject));
|
||||
} catch(error) {
|
||||
this.server.log(['error', 'security'], 'An error occured while parsing the opendistro_security.jwt.login_endpoint value');
|
||||
return reply.redirect(this.basePath + '/customerror?type=authError');
|
||||
}
|
||||
|
||||
} else if (error instanceof SessionExpiredError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=sessionExpired');
|
||||
} else {
|
||||
return reply.redirect(this.basePath + '/customerror?type=authError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT);
|
||||
}
|
||||
|
||||
}
|
76
lib/auth/types/jwt/routes.js
Normal file
76
lib/auth/types/jwt/routes.js
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
const customErrorApp = server.getHiddenUiAppById('security-customerror');
|
||||
|
||||
/**
|
||||
* After a logout we are redirected to a login page
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/login`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The error page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/customerror`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/logout`,
|
||||
handler: (request, reply) => {
|
||||
request.auth.securitySessionStorage.clear();
|
||||
reply({});
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
150
lib/auth/types/openid/OpenId.js
Normal file
150
lib/auth/types/openid/OpenId.js
Normal file
@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import AuthType from "../AuthType";
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
import AuthenticationError from "../../errors/authentication_error";
|
||||
import MissingRoleError from "../../errors/missing_role_error";
|
||||
const Wreck = require('wreck');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
export default class OpenId extends AuthType {
|
||||
|
||||
constructor(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
super(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT);
|
||||
|
||||
/**
|
||||
* The authType is saved in the auth cookie for later reference
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = 'openid';
|
||||
|
||||
// support for self signed certificates: root ca and verify hostname
|
||||
const options = {};
|
||||
|
||||
if (this.config.get('opendistro_security.openid.root_ca')) {
|
||||
options.ca = [ fs.readFileSync(this.config.get('opendistro_security.openid.root_ca')) ]
|
||||
}
|
||||
|
||||
if (this.config.get('opendistro_security.openid.verify_hostnames') == false) {
|
||||
// do not check identity
|
||||
options.checkServerIdentity = function(host, cert) {}
|
||||
}
|
||||
|
||||
if (options.ca || options.checkServerIdentity) {
|
||||
Wreck.agents.https = new https.Agent(options);
|
||||
}
|
||||
|
||||
try {
|
||||
this.authHeaderName = this.config.get('opendistro_security.openid.header').toLowerCase();
|
||||
} catch(error) {
|
||||
this.kbnServer.status.yellow('No authorization header name defined for OpenId, using "authorization"');
|
||||
this.authHeaderName = 'authorization'
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate(credentials) {
|
||||
// A "login" can happen when we have a token (as header or as URL parameter but no session,
|
||||
// or when we have an existing session, but the passed token does not match what's in the session.
|
||||
try {
|
||||
|
||||
let user = await this.server.plugins.opendistro_security.getSecurityBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue);
|
||||
let tokenPayload = {};
|
||||
try {
|
||||
tokenPayload = JSON.parse(Buffer.from(credentials.authHeaderValue.split('.')[1], 'base64').toString());
|
||||
} catch (error) {
|
||||
// Something went wrong while parsing the payload, but the user was authenticated correctly.
|
||||
}
|
||||
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: credentials,
|
||||
authType: this.type
|
||||
};
|
||||
|
||||
if (tokenPayload.exp) {
|
||||
// The token's exp value trumps the config setting
|
||||
this.sessionKeepAlive = false;
|
||||
session.exp = parseInt(tokenPayload.exp, 10);
|
||||
} else if(this.sessionTTL) {
|
||||
session.expiryTime = Date.now() + this.sessionTTL
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
onUnAuthenticated(request, reply, error) {
|
||||
|
||||
// If we don't have any tenant we need to show the custom error page
|
||||
if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingTenant')
|
||||
} else if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingRole')
|
||||
} else if (error instanceof AuthenticationError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=authError')
|
||||
}
|
||||
|
||||
const nextUrl = encodeURIComponent(request.url.path);
|
||||
return reply.redirect(`${this.basePath}/auth/openid/login?nextUrl=${nextUrl}`);
|
||||
}
|
||||
|
||||
async setupRoutes() {
|
||||
Wreck.get(this.config.get('opendistro_security.openid.connect_url'), (err, response, payload) => {
|
||||
if (err ||
|
||||
response.statusCode < 200 ||
|
||||
response.statusCode > 299) {
|
||||
this.server.log(["error", "openid"], err);
|
||||
throw new Error('Failed when trying to obtain the endpoints from your IdP');
|
||||
}
|
||||
|
||||
const parsedPayload = JSON.parse(payload.toString());
|
||||
|
||||
let endPoints = {
|
||||
authorization_endpoint: parsedPayload.authorization_endpoint,
|
||||
token_endpoint: parsedPayload.token_endpoint,
|
||||
end_session_endpoint: parsedPayload.end_session_endpoint || null
|
||||
};
|
||||
|
||||
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT, endPoints);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
211
lib/auth/types/openid/routes.js
Normal file
211
lib/auth/types/openid/routes.js
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import {parseNextUrl} from '../../parseNextUrl'
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, openIdEndPoints) {
|
||||
|
||||
const AuthenticationError = pluginRoot('lib/auth/errors/authentication_error');
|
||||
const config = server.config();
|
||||
const basePath = config.get('server.basePath');
|
||||
const customErrorApp = server.getHiddenUiAppById('security-customerror');
|
||||
|
||||
const routesPath = '/auth/openid/';
|
||||
|
||||
// OpenId config
|
||||
const clientId = config.get('opendistro_security.openid.client_id');
|
||||
const clientSecret = config.get('opendistro_security.openid.client_secret');
|
||||
|
||||
// Scope must include "openid"
|
||||
// Other available scopes as per the spec: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
|
||||
let scope = config.get('opendistro_security.openid.scope').split(' ');
|
||||
if (scope.indexOf('openid') === -1) {
|
||||
scope.push('openid');
|
||||
}
|
||||
|
||||
/**
|
||||
* The redirect uri can't always be resolved automatically.
|
||||
* Instead, we have the opendistro_security.openid.base_redirect_uri config option.
|
||||
* @returns {*}
|
||||
*/
|
||||
function getBaseRedirectUrl() {
|
||||
const configuredBaseRedirectUrl = config.get('opendistro_security.openid.base_redirect_url');
|
||||
if (configuredBaseRedirectUrl) {
|
||||
return (configuredBaseRedirectUrl.endsWith('/')) ? configuredBaseRedirectUrl.slice(0, -1) : configuredBaseRedirectUrl;
|
||||
}
|
||||
|
||||
// Config option not used, try to get the correct protocol and host
|
||||
let host = config.get('server.host');
|
||||
let port = config.get('server.port');
|
||||
if (port) {
|
||||
host = host + ':' + port;
|
||||
}
|
||||
|
||||
return `${server.info.protocol}://${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handler for the cases where we can't catch errors while obtaining the token.
|
||||
* Mainly happens when Wreck within Bell
|
||||
*/
|
||||
server.ext('onPreResponse', function(request, reply) {
|
||||
// Make sure we only handle errors for the login route
|
||||
if (request.response.isBoom && request.path.indexOf(`${APP_ROOT}${routesPath}login`) > -1 && request.response.output.statusCode === 500) {
|
||||
return reply.redirect(basePath + '/customerror?type=authError');
|
||||
}
|
||||
|
||||
reply.continue();
|
||||
});
|
||||
|
||||
// Register bell with the server
|
||||
server.register(require('bell'), function (err) {
|
||||
let baseRedirectUrl = getBaseRedirectUrl();
|
||||
let location = `${baseRedirectUrl}${basePath}`;
|
||||
|
||||
server.auth.strategy('customOAuth', 'bell', {
|
||||
provider: {
|
||||
auth: openIdEndPoints.authorization_endpoint,
|
||||
token: openIdEndPoints.token_endpoint,
|
||||
scope: scope,
|
||||
protocol: 'oauth2',
|
||||
useParamsAuth: true,
|
||||
},
|
||||
skipProfile: true,
|
||||
location: encodeURI(location),
|
||||
password: config.get('opendistro_security.cookie.password'),
|
||||
clientId: clientId,
|
||||
clientSecret: clientSecret,
|
||||
isSecure: config.get('opendistro_security.cookie.secure'),
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The login page.
|
||||
*/
|
||||
server.route({
|
||||
method: ['GET', 'POST'],
|
||||
path: `${APP_ROOT}${routesPath}login`,
|
||||
config: {
|
||||
auth: 'customOAuth'
|
||||
},
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
if (!request.auth.isAuthenticated) {
|
||||
return reply.redirect(basePath + '/customerror?type=authError');
|
||||
}
|
||||
|
||||
let credentials = request.auth.credentials;
|
||||
|
||||
let nextUrl = (credentials.query && credentials.query.nextUrl) ? credentials.query.nextUrl : null;
|
||||
|
||||
try {
|
||||
// Bell gives us the access token to identify with here,
|
||||
// but we want the id_token returned from the IDP
|
||||
let {user} = await request.auth.securitySessionStorage.authenticate({
|
||||
authHeaderValue: 'Bearer ' + request.auth.artifacts['id_token']
|
||||
});
|
||||
|
||||
if (nextUrl) {
|
||||
nextUrl = parseNextUrl(nextUrl, basePath);
|
||||
return reply.redirect(nextUrl);
|
||||
}
|
||||
|
||||
return reply.redirect(basePath + '/app/kibana');
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply.redirect(basePath + '/customerror?type=authError');
|
||||
} else if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingTenant');
|
||||
} else {
|
||||
return reply.redirect(basePath + '/customerror?type=authError');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The error page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/customerror`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clears the session and logs the user out from the IdP (if we have an endpoint available)
|
||||
* @url http://openid.net/specs/openid-connect-session-1_0.html#RPLogout
|
||||
*/
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/logout`,
|
||||
handler: (request, reply) => {
|
||||
request.auth.securitySessionStorage.clear();
|
||||
|
||||
// Build the redirect uri needed by the provider
|
||||
let baseRedirectUrl = getBaseRedirectUrl();
|
||||
// Unfortunately, it seems like the cookie plugin isn't available yet,
|
||||
// which means that we can't use the new plugin
|
||||
const cookieName = config.get('opendistro_security.cookie.name');
|
||||
// Get the session credentials and remove "Bearer " from the value
|
||||
const token = request.state[cookieName].credentials.authHeaderValue.split(' ')[1];
|
||||
let requestQueryParameters = `?post_logout_redirect_uri=${baseRedirectUrl}${basePath}/app/kibana`;
|
||||
|
||||
// If we don't have an "end_session_endpoint" in the .well-known list,
|
||||
// we may have a custom logout_url defined in the config.
|
||||
// The custom url trumps the .well-known endpoint if both are available.
|
||||
let customLogoutUrl = config.get('opendistro_security.openid.logout_url');
|
||||
let endSessionUrl = null
|
||||
if (customLogoutUrl) {
|
||||
// Pass the post_logout_uri just in case, but not the token
|
||||
endSessionUrl = customLogoutUrl + requestQueryParameters;
|
||||
} else if (openIdEndPoints.end_session_endpoint) {
|
||||
endSessionUrl = openIdEndPoints.end_session_endpoint + requestQueryParameters + '&id_token_hint=' + token;
|
||||
}
|
||||
|
||||
reply({redirectURL: endSessionUrl});
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
176
lib/auth/types/proxycache/ProxyCache.js
Normal file
176
lib/auth/types/proxycache/ProxyCache.js
Normal file
@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import AuthType from "../AuthType";
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
import SessionExpiredError from "../../errors/session_expired_error";
|
||||
import {parse, format} from 'url';
|
||||
import MissingRoleError from "../../errors/missing_role_error";
|
||||
import {parseLoginEndpoint} from "./parse_login_endpoint";
|
||||
|
||||
export default class ProxyCache extends AuthType {
|
||||
|
||||
constructor(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
super(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT);
|
||||
|
||||
/**
|
||||
* The authType is saved in the auth cookie for later reference
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = 'proxycache';
|
||||
|
||||
/**
|
||||
* The header that identifies the user
|
||||
*/
|
||||
this.userHeaderName = this.config.get('opendistro_security.proxycache.user_header').toLowerCase();
|
||||
|
||||
/**
|
||||
* The header that identifies the user's role(s). Optional.
|
||||
*/
|
||||
this.rolesHeaderName = this.config.get('opendistro_security.proxycache.roles_header').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect authorization header value, either as an http header or as a query parameter
|
||||
* @param request
|
||||
* @param sessionCredentials
|
||||
* @returns {*}
|
||||
*/
|
||||
detectAuthHeaderCredentials(request, sessionCredentials = null) {
|
||||
|
||||
// The point of ProxyCache is that we only have headers on the first request.
|
||||
// In other words, if we already have a session, we don't need to check the headers.
|
||||
if (sessionCredentials !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (request.headers[this.userHeaderName]) {
|
||||
const authHeaderValues = {
|
||||
[this.userHeaderName]: request.headers[this.userHeaderName],
|
||||
'x-forwarded-for': request.headers['x-forwarded-for']
|
||||
};
|
||||
|
||||
// The roles header is optional
|
||||
if (request.headers[this.rolesHeaderName]) {
|
||||
authHeaderValues[this.rolesHeaderName] = request.headers[this.rolesHeaderName];
|
||||
}
|
||||
|
||||
return authHeaderValues;
|
||||
|
||||
} else if (request.headers[this.authHeaderName]) {
|
||||
return {
|
||||
[this.authHeaderName]: request.headers[this.authHeaderName]
|
||||
}
|
||||
}
|
||||
|
||||
// We still need to support basic auth for Curl etc.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the auth header(s) needed for the Security backend
|
||||
* @param session
|
||||
* @returns {*}
|
||||
*/
|
||||
getAuthHeader(session) {
|
||||
if (! session.credentials) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (session.credentials[this.userHeaderName]) {
|
||||
return {
|
||||
[this.userHeaderName]: session.credentials[this.userHeaderName],
|
||||
[this.rolesHeaderName]: session.credentials[this.rolesHeaderName]
|
||||
}
|
||||
} else if (session.credentials[this.authHeaderName]) {
|
||||
return {
|
||||
[this.authHeaderName]: session.credentials[this.authHeaderName]
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async authenticate(credentialHeaders) {
|
||||
try {
|
||||
let user = await this.server.plugins.opendistro_security.getSecurityBackend().authenticateWithHeaders(credentialHeaders, credentialHeaders);
|
||||
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: credentialHeaders,
|
||||
authType: this.type
|
||||
};
|
||||
|
||||
if(this.sessionTTL) {
|
||||
session.expiryTime = Date.now() + this.sessionTTL
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onUnAuthenticated(request, reply, error) {
|
||||
if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingTenant');
|
||||
} else if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingRole');
|
||||
} else {
|
||||
// The customer may use a login endpoint, to which we can redirect
|
||||
// if the user isn't authenticated.
|
||||
let loginEndpoint = this.config.get('opendistro_security.proxycache.login_endpoint');
|
||||
if (loginEndpoint) {
|
||||
try {
|
||||
const redirectUrl = parseLoginEndpoint(loginEndpoint, request);
|
||||
return reply.redirect(redirectUrl);
|
||||
} catch(error) {
|
||||
this.server.log(['error', 'security'], 'An error occured while parsing the opendistro_security.proxycache.login_endpoint value');
|
||||
return reply.redirect(this.basePath + '/customerror?type=proxycacheAuthError');
|
||||
}
|
||||
} else if (error instanceof SessionExpiredError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=sessionExpired');
|
||||
} else {
|
||||
return reply.redirect(this.basePath + '/customerror?type=proxycacheAuthError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT);
|
||||
}
|
||||
|
||||
}
|
55
lib/auth/types/proxycache/parse_login_endpoint.js
Normal file
55
lib/auth/types/proxycache/parse_login_endpoint.js
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {parse, format} from 'url';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param loginEndpoint
|
||||
* @param request - Optional, only needed if we should append a "nextUrl" query parameter
|
||||
* @returns {*}
|
||||
*/
|
||||
export function parseLoginEndpoint(loginEndpoint, request = null) {
|
||||
// Parse the login endpoint so that we can append our nextUrl
|
||||
// if the customer has defined query parameters in the endpoint
|
||||
let loginEndpointURLObject = parse(loginEndpoint, true);
|
||||
|
||||
// Make sure we don't overwrite an existing "nextUrl" parameter,
|
||||
// just in case the customer is using that name for something else
|
||||
if (typeof loginEndpointURLObject.query['nextUrl'] === 'undefined' && request) {
|
||||
const nextUrl = encodeURIComponent(request.url.path);
|
||||
// Delete the search parameter - otherwise format() will use its value instead of the .query property
|
||||
delete loginEndpointURLObject.search;
|
||||
loginEndpointURLObject.query['nextUrl'] = nextUrl;
|
||||
}
|
||||
// Format the parsed endpoint object into a URL and return
|
||||
return format(loginEndpointURLObject);
|
||||
}
|
90
lib/auth/types/proxycache/routes.js
Normal file
90
lib/auth/types/proxycache/routes.js
Normal file
@ -0,0 +1,90 @@
|
||||
import {parseLoginEndpoint} from "./parse_login_endpoint";
|
||||
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
const customErrorApp = server.getHiddenUiAppById('security-customerror');
|
||||
|
||||
/**
|
||||
* After a logout we are redirected to a login page
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/login`,
|
||||
handler(request, reply) {
|
||||
// The customer may use a login endpoint, to which we can redirect
|
||||
// if the user isn't authenticated.
|
||||
let loginEndpoint = server.config().get('opendistro_security.proxycache.login_endpoint');
|
||||
if (loginEndpoint) {
|
||||
try {
|
||||
const redirectUrl = parseLoginEndpoint(loginEndpoint);
|
||||
return reply.redirect(redirectUrl);
|
||||
} catch(error) {
|
||||
this.server.log(['error', 'security'], 'An error occured while parsing the opendistro_security.proxycache.login_endpoint value');
|
||||
}
|
||||
} else {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
}
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The error page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/customerror`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/logout`,
|
||||
handler: (request, reply) => {
|
||||
request.auth.securitySessionStorage.clear();
|
||||
reply({});
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
107
lib/auth/types/saml/Saml.js
Normal file
107
lib/auth/types/saml/Saml.js
Normal file
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import AuthType from "../AuthType";
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
import AuthenticationError from "../../errors/authentication_error";
|
||||
import MissingRoleError from "../../errors/missing_role_error";
|
||||
|
||||
export default class Saml extends AuthType {
|
||||
|
||||
constructor(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
super(pluginRoot, server, kbnServer, APP_ROOT, API_ROOT);
|
||||
|
||||
/**
|
||||
* The authType is saved in the auth cookie for later reference
|
||||
* @type {string}
|
||||
*/
|
||||
this.type = 'saml';
|
||||
}
|
||||
|
||||
async authenticate(credentials) {
|
||||
// A "login" can happen when we have a token (as header or as URL parameter but no session,
|
||||
// or when we have an existing session, but the passed token does not match what's in the session.
|
||||
try {
|
||||
|
||||
let user = await this.server.plugins.opendistro_security.getSecurityBackend().authenticateWithHeader(this.authHeaderName, credentials.authHeaderValue);
|
||||
|
||||
let tokenPayload = {};
|
||||
try {
|
||||
tokenPayload = JSON.parse(Buffer.from(credentials.authHeaderValue.split('.')[1], 'base64').toString());
|
||||
} catch (error) {
|
||||
// Something went wrong while parsing the payload, but the user was authenticated correctly.
|
||||
}
|
||||
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: credentials,
|
||||
authType: this.type
|
||||
};
|
||||
|
||||
if (tokenPayload.exp) {
|
||||
// The token's exp value trumps the config setting
|
||||
this.sessionKeepAlive = false;
|
||||
session.exp = parseInt(tokenPayload.exp, 10);
|
||||
} else if(this.sessionTTL) {
|
||||
session.expiryTime = Date.now() + this.sessionTTL
|
||||
}
|
||||
|
||||
return {
|
||||
session,
|
||||
user
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
onUnAuthenticated(request, reply, error) {
|
||||
|
||||
// If we don't have any tenant we need to show the custom error page
|
||||
if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingTenant')
|
||||
} else if (error instanceof MissingRoleError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=missingRole')
|
||||
} else if (error instanceof AuthenticationError) {
|
||||
return reply.redirect(this.basePath + '/customerror?type=samlAuthError')
|
||||
}
|
||||
|
||||
const nextUrl = encodeURIComponent(request.url.path);
|
||||
return reply.redirect(`${this.basePath}/auth/saml/login?nextUrl=${nextUrl}`);
|
||||
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
require('./routes')(this.pluginRoot, this.server, this.kbnServer, this.APP_ROOT, this.API_ROOT);
|
||||
}
|
||||
}
|
234
lib/auth/types/saml/routes.js
Normal file
234
lib/auth/types/saml/routes.js
Normal file
@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import {parseNextUrl} from '../../parseNextUrl'
|
||||
import MissingTenantError from "../../errors/missing_tenant_error";
|
||||
import AuthenticationError from "../../errors/authentication_error";
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
const AuthenticationError = pluginRoot('lib/auth/errors/authentication_error');
|
||||
const config = server.config();
|
||||
const basePath = config.get('server.basePath');
|
||||
const customErrorApp = server.getHiddenUiAppById('security-customerror');
|
||||
|
||||
const routesPath = '/auth/saml/';
|
||||
|
||||
/**
|
||||
* The login page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}${routesPath}login`,
|
||||
config: {
|
||||
auth: false
|
||||
},
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
if (request.auth.isAuthenticated) {
|
||||
return reply.redirect(basePath + '/app/kibana');
|
||||
}
|
||||
|
||||
let nextUrl = null;
|
||||
if (request.url && request.url.query && request.url.query.nextUrl) {
|
||||
nextUrl = parseNextUrl(request.url.query.nextUrl, basePath);
|
||||
}
|
||||
|
||||
try {
|
||||
// Grab the request for SAML
|
||||
server.plugins.opendistro_security.getSecurityBackend().getSamlHeader()
|
||||
.then((samlHeader) => {
|
||||
request.auth.securitySessionStorage.putStorage('temp-saml', {
|
||||
requestId: samlHeader.requestId,
|
||||
nextUrl: nextUrl
|
||||
});
|
||||
|
||||
return reply.redirect(samlHeader.location);
|
||||
})
|
||||
.catch(() => {
|
||||
return reply.redirect(basePath + '/customerror?type=samlConfigError');
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
return reply.redirect(basePath + '/customerror?type=samlConfigError');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The page that the IdP redirects to after a successful SP-initiated login
|
||||
*/
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${APP_ROOT}/_opendistro/_security/saml/acs`,
|
||||
config: {
|
||||
auth: false
|
||||
},
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
|
||||
let storedRequestInfo = request.auth.securitySessionStorage.getStorage('temp-saml', {});
|
||||
request.auth.securitySessionStorage.clearStorage('temp-saml');
|
||||
|
||||
if (! storedRequestInfo.requestId) {
|
||||
return reply.redirect(basePath + '/customerror?type=samlAuthError');
|
||||
}
|
||||
|
||||
try {
|
||||
let credentials = await server.plugins.opendistro_security.getSecurityBackend().authtoken(storedRequestInfo.requestId || null, request.payload.SAMLResponse);
|
||||
|
||||
let {user} = await request.auth.securitySessionStorage.authenticate({
|
||||
authHeaderValue: credentials.authorization
|
||||
});
|
||||
|
||||
let nextUrl = storedRequestInfo.nextUrl;
|
||||
|
||||
if (nextUrl) {
|
||||
nextUrl = parseNextUrl(nextUrl, basePath);
|
||||
return reply.redirect(nextUrl);
|
||||
}
|
||||
|
||||
return reply.redirect(basePath + '/app/kibana');
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply.redirect(basePath + '/customerror?type=samlAuthError');
|
||||
} else if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingTenant');
|
||||
} else {
|
||||
return reply.redirect(basePath + '/customerror?type=samlAuthError');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The page that the IdP redirects to after a successful IdP-initiated login
|
||||
*/
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${APP_ROOT}/_opendistro/_security/saml/acs/idpinitiated`,
|
||||
config: {
|
||||
auth: false
|
||||
},
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
|
||||
try {
|
||||
const acsEndpoint = `${APP_ROOT}/_opendistro/_security/saml/acs/idpinitiated`;
|
||||
let credentials = await server.plugins.opendistro_security.getSecurityBackend().authtoken(null, request.payload.SAMLResponse, acsEndpoint);
|
||||
|
||||
let {user} = await request.auth.securitySessionStorage.authenticate({
|
||||
authHeaderValue: credentials.authorization
|
||||
});
|
||||
|
||||
return reply.redirect(basePath + '/app/kibana');
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return reply.redirect(basePath + '/customerror?type=samlAuthError');
|
||||
} else if (error instanceof MissingTenantError) {
|
||||
return reply.redirect(basePath + '/customerror?type=missingTenant');
|
||||
} else {
|
||||
return reply.redirect(basePath + '/customerror?type=samlAuthError');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The custom error page.
|
||||
*/
|
||||
server.route({
|
||||
method: ['GET', 'POST'],
|
||||
path: `${APP_ROOT}/opendistro_security/saml/logout`,
|
||||
handler(request, reply) {
|
||||
return reply.redirect(`${APP_ROOT}/customerror?type=samlLogoutSuccess`);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The custom error page.
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${APP_ROOT}/customerror`,
|
||||
handler(request, reply) {
|
||||
return reply.renderAppWithDefaultConfig(customErrorApp);
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/auth/logout`,
|
||||
handler: {
|
||||
async: async(request, reply) => {
|
||||
|
||||
const cookieName = config.get('opendistro_security.cookie.name');
|
||||
let authInfo = null;
|
||||
|
||||
try {
|
||||
let authHeader = {
|
||||
[request.auth.securitySessionStorage.getAuthHeaderName()]: request.state[cookieName].credentials.authHeaderValue
|
||||
};
|
||||
authInfo = await server.plugins.opendistro_security.getSecurityBackend().authinfo(authHeader);
|
||||
} catch(error) {
|
||||
// Not much we can do here, so we'll just fall back to the login page if we don't get an sso_logout_url
|
||||
}
|
||||
|
||||
request.auth.securitySessionStorage.clear();
|
||||
const redirectURL = (authInfo && authInfo.sso_logout_url) ? authInfo.sso_logout_url : `${APP_ROOT}/customerror?type=samlLogoutSuccess`;
|
||||
|
||||
reply({redirectURL});
|
||||
}
|
||||
},
|
||||
config: {
|
||||
auth: false
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
96
lib/auth/user.js
Normal file
96
lib/auth/user.js
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a Security user
|
||||
*/
|
||||
export default class User {
|
||||
|
||||
/**
|
||||
* @property {string} username - The username.
|
||||
*/
|
||||
get username() {
|
||||
return this._username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {Array} roles - The user roles.
|
||||
*/
|
||||
get roles() {
|
||||
return this._roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {Array} roles - The users unmapped backend roles.
|
||||
*/
|
||||
get backendroles() {
|
||||
return this._backendroles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {Array} tenants - The user tenants.
|
||||
*/
|
||||
get tenants() {
|
||||
return this._tenants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {Array} tenants - The user tenants.
|
||||
*/
|
||||
get selectedTenant() {
|
||||
return this._selectedTenant;
|
||||
}
|
||||
/**
|
||||
* @property {object} credentials - The credentials that were used to authenticate the user.
|
||||
*/
|
||||
get credentials() {
|
||||
return this._credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* @property {object} proxyCredentials - User credentials to be used in requests to Elasticsearch performed by either the transport client
|
||||
* or the query engine.
|
||||
*/
|
||||
get proxyCredentials() {
|
||||
return this._proxyCredentials;
|
||||
}
|
||||
|
||||
constructor(username, credentials, proxyCredentials, roles, backendroles, tenants, selectedTenant) {
|
||||
this._username = username;
|
||||
this._credentials = credentials;
|
||||
this._proxyCredentials = proxyCredentials;
|
||||
this._roles = roles;
|
||||
this._selectedTenant = selectedTenant;
|
||||
this._backendroles = backendroles;
|
||||
this._tenants = tenants;
|
||||
}
|
||||
|
||||
}
|
17
lib/backend/errors/conflict.js
Normal file
17
lib/backend/errors/conflict.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Thrown when an existing object with the same identifier already exists.
|
||||
*/
|
||||
export default class ConflictError {
|
||||
/**
|
||||
* Creates a new ConflictError.
|
||||
*
|
||||
* @param {string} message - The error message.
|
||||
* @param {Error} inner - An optional error that caused the ConflictError.
|
||||
*/
|
||||
constructor(message, inner) {
|
||||
this.name = 'ConflictError';
|
||||
this.message = message;
|
||||
this.inner = inner;
|
||||
this.stack = new Error().stack;
|
||||
}
|
||||
}
|
17
lib/backend/errors/not_found.js
Normal file
17
lib/backend/errors/not_found.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Thrown when an object is not found.
|
||||
*/
|
||||
export default class NotFoundError {
|
||||
/**
|
||||
* Creates a new NotFoundError.
|
||||
*
|
||||
* @param {string} message - The error message.
|
||||
* @param {Error} inner - An optional error that caused the NotFoundError.
|
||||
*/
|
||||
constructor(message, inner) {
|
||||
this.name = 'NotFoundError';
|
||||
this.message = message;
|
||||
this.inner = inner;
|
||||
this.stack = new Error().stack;
|
||||
}
|
||||
}
|
41
lib/backend/errors/wrap_elasticsearch_error.js
Normal file
41
lib/backend/errors/wrap_elasticsearch_error.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { get } from 'lodash';
|
||||
import Boom from 'boom';
|
||||
import AuthenticationError from '../../auth/errors/authentication_error';
|
||||
|
||||
/**
|
||||
* Wraps an Elasticsearch client error into a backend error.
|
||||
*
|
||||
* @param {Error} error - An Elasticsearch client error.
|
||||
*/
|
||||
export default function wrapElasticsearchError(error) {
|
||||
|
||||
let statusCode = error.statusCode;
|
||||
|
||||
if (error.status) {
|
||||
statusCode = error.status;
|
||||
}
|
||||
|
||||
if (!statusCode) {
|
||||
statusCode = 500;
|
||||
}
|
||||
|
||||
let message = get(error, 'body.message');
|
||||
if (!message) {
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
const wwwAuthHeader = get(error, 'body.error.header[WWW-Authenticate]');
|
||||
|
||||
if (wwwAuthHeader) {
|
||||
const boomError = Boom.boomify(error, { statusCode: statusCode, message: message });
|
||||
boomError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"';
|
||||
return boomError;
|
||||
}
|
||||
|
||||
if (statusCode == 401) {
|
||||
return new AuthenticationError();
|
||||
}
|
||||
|
||||
return Boom.boomify(error, { statusCode: statusCode, message: message });
|
||||
|
||||
}
|
366
lib/backend/opendistro_security.js
Normal file
366
lib/backend/opendistro_security.js
Normal file
@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { get } from 'lodash';
|
||||
import filterAuthHeaders from '../auth/filter_auth_headers';
|
||||
import SecurityPlugin from './opendistro_security_plugin';
|
||||
import AuthenticationError from '../auth/errors/authentication_error';
|
||||
import wrapElasticsearchError from './errors/wrap_elasticsearch_error';
|
||||
import User from '../auth/user';
|
||||
|
||||
/**
|
||||
* The Security backend.
|
||||
*/
|
||||
export default class SecurityBackend {
|
||||
|
||||
constructor(server) {
|
||||
// client for authentication and authorization
|
||||
const config = Object.assign({ plugins: [SecurityPlugin], auth: true }, server.config().get('elasticsearch'));
|
||||
this._cluster = server.plugins.elasticsearch.createCluster('security', config
|
||||
);
|
||||
|
||||
this._client = this._cluster._noAuthClient;
|
||||
|
||||
// the es config for later use
|
||||
this._esconfig = server.config().get('elasticsearch');
|
||||
}
|
||||
|
||||
async authenticate(credentials) {
|
||||
const authHeader = new Buffer(`${credentials.username}:${credentials.password}`).toString('base64');
|
||||
try {
|
||||
const response = await this._client.opendistro_security.authinfo({
|
||||
headers: {
|
||||
authorization: `Basic ${authHeader}`
|
||||
}
|
||||
});
|
||||
return new User(credentials.username, credentials, credentials, response.roles, response.backend_roles, response.tenants, response.user_requested_tenant);
|
||||
} catch(error) {
|
||||
if (error.status == 401) {
|
||||
throw new AuthenticationError("Invalid username or password");
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateWithHeader(headerName, headerValue) {
|
||||
|
||||
try {
|
||||
const credentials = {
|
||||
headerName: headerName,
|
||||
headerValue: headerValue
|
||||
};
|
||||
|
||||
let headers = {};
|
||||
|
||||
// For anonymous auth, we wouldn't have any value here
|
||||
if (headerValue) {
|
||||
headers[headerName] = headerValue
|
||||
}
|
||||
|
||||
const response = await this._client.opendistro_security.authinfo({
|
||||
headers: headers
|
||||
});
|
||||
return new User(response.user_name, credentials, null, response.roles, response.backend_roles, response.tenants, response.user_requested_tenant);
|
||||
} catch(error) {
|
||||
if (error.status == 401) {
|
||||
throw new AuthenticationError("Invalid username or password");
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for authinfo() when we expect a response to be used for a cookie
|
||||
* @param headers
|
||||
* @param credentials
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
async authenticateWithHeaders(headers, credentials = {}) {
|
||||
|
||||
try {
|
||||
const response = await this._client.opendistro_security.authinfo({
|
||||
headers: headers
|
||||
});
|
||||
return new User(response.user_name, credentials, null, response.roles, response.backend_roles, response.tenants, response.user_requested_tenant);
|
||||
} catch(error) {
|
||||
if (error.status == 401) {
|
||||
throw new AuthenticationError("Invalid username or password");
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buildSessionResponse(credentials, authInfoResponse) {
|
||||
return new User(authInfoResponse.user_name, credentials, null, authInfoResponse.roles, authInfoResponse.backend_roles, authInfoResponse.tenants, authInfoResponse.user_requested_tenant);
|
||||
}
|
||||
|
||||
async authinfo(headers) {
|
||||
try {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
const response = await this._client.opendistro_security.authinfo({
|
||||
headers: authHeaders
|
||||
});
|
||||
return response
|
||||
} catch(error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
getSamlHeader() {
|
||||
|
||||
return this._client.opendistro_security.authinfo({})
|
||||
.then(() => {})
|
||||
.catch((error) => {
|
||||
|
||||
if (! error.wwwAuthenticateDirective) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
let locationRegExp = /location="(.*?)"/;
|
||||
let requestIdRegExp = /requestId="(.*?)"/;
|
||||
|
||||
return {
|
||||
location: locationRegExp.exec(error.wwwAuthenticateDirective)[1],
|
||||
requestId: requestIdRegExp.exec(error.wwwAuthenticateDirective)[1]
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges a SAMLResponse from the IdP against a token for internal use
|
||||
* @param RequestId
|
||||
* @param SAMLResponse
|
||||
* @param acsEndpoint
|
||||
* @returns {Promise<Promise<*>|*>}
|
||||
*/
|
||||
async authtoken(RequestId, SAMLResponse, acsEndpoint = null) {
|
||||
const body = {
|
||||
RequestId,
|
||||
SAMLResponse,
|
||||
acsEndpoint
|
||||
};
|
||||
|
||||
return this._client.opendistro_security.authtoken({
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
|
||||
async multitenancyinfo(headers) {
|
||||
try {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
const response = await this._client.opendistro_security.multitenancyinfo({
|
||||
headers: authHeaders
|
||||
});
|
||||
return response
|
||||
} catch(error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getTenantInfo(headers) {
|
||||
try {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
const response = await this._client.opendistro_security.tenantinfo({
|
||||
headers: authHeaders
|
||||
});
|
||||
return response
|
||||
} catch(error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated, use the sessionPlugin instead
|
||||
* @param user
|
||||
* @returns {Promise<{authorization: string}>}
|
||||
*/
|
||||
async getAuthHeaders(user) {
|
||||
const credentials = user.credentials;
|
||||
const authHeader = new Buffer(`${credentials.username}:${credentials.password}`).toString('base64');
|
||||
return {
|
||||
'authorization': `Basic ${authHeader}`
|
||||
};
|
||||
}
|
||||
|
||||
getAuthHeaders(username, password) {
|
||||
const authHeader = new Buffer(`${username}:${password}`).toString('base64');
|
||||
return {
|
||||
'authorization': `Basic ${authHeader}`
|
||||
};
|
||||
}
|
||||
|
||||
getUser(username, password) {
|
||||
var credentials = {"username": username, "password": password};
|
||||
var user = new User(credentials.username, credentials, credentials, [], {});
|
||||
return user;
|
||||
}
|
||||
|
||||
getServerUser() {
|
||||
return this.getUser(this._esconfig.username, this._esconfig.password);
|
||||
}
|
||||
|
||||
getServerUserAuthHeader() {
|
||||
return this.getAuthHeaders(this._esconfig.username, this._esconfig.password);
|
||||
}
|
||||
|
||||
updateAndGetTenantPreferences(request, user, tenant) {
|
||||
|
||||
var prefs = request.state.security_preferences;
|
||||
// no prefs cookie present
|
||||
if (!prefs) {
|
||||
var newPrefs = {};
|
||||
newPrefs[user] = tenant;
|
||||
return newPrefs;
|
||||
}
|
||||
prefs[user] = tenant;
|
||||
return prefs;
|
||||
}
|
||||
|
||||
getTenantByPreference(request, username, tenants, preferredTenants, globalEnabled, privateEnabled) {
|
||||
// delete user from tenants first to check if we have a tenant to choose from at all
|
||||
// keep original preferences untouched, we need the original values again
|
||||
// http://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object
|
||||
var tenantsCopy = JSON.parse(JSON.stringify(tenants));
|
||||
delete tenantsCopy[username];
|
||||
|
||||
// sanity check
|
||||
if (!globalEnabled && !privateEnabled && _.isEmpty(tenantsCopy)) {
|
||||
return null;
|
||||
}
|
||||
// get users preferred tenant
|
||||
var prefs = request.state.security_preferences;
|
||||
|
||||
if (prefs) {
|
||||
var preferredTenant = prefs[username];
|
||||
|
||||
// user has a preferred tenant, check if it is accessible
|
||||
if (preferredTenant && tenants[preferredTenant] != undefined) {
|
||||
return preferredTenant;
|
||||
}
|
||||
|
||||
// special case: in tenants returned from SECURITY, the private tenant is
|
||||
// the username of the logged in user, but the header value is __user__
|
||||
if ((preferredTenant == "__user__" || preferredTenant == "private") && tenants[username] != undefined && privateEnabled) {
|
||||
return "__user__";
|
||||
}
|
||||
|
||||
if ((preferredTenant == "global" || preferredTenant === '' ) && globalEnabled) {
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// no preference in cookie, or tenant no accessible anymore, evaluate preferredTenants from kibana config
|
||||
if (preferredTenants && !_.isEmpty(preferredTenants)) {
|
||||
for (var i = 0; i < preferredTenants.length; i++) {
|
||||
var check = preferredTenants[i].toLowerCase();
|
||||
|
||||
if (globalEnabled && (check === 'global' || check === '__global__')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (privateEnabled && (check === 'private' || check === '__user__') && tenants[username] != undefined) {
|
||||
return '__user__';
|
||||
}
|
||||
|
||||
if (tenants[check] != undefined) {
|
||||
return check;
|
||||
}
|
||||
if (check.toLowerCase() == "private" && privateEnabled) {
|
||||
return "__user__";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no pref in cookie, no preferred tenant in kibana, use GLOBAL, Private or the first tenant in the list
|
||||
if (globalEnabled) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (privateEnabled) {
|
||||
return "__user__";
|
||||
} else {
|
||||
delete tenants[username];
|
||||
}
|
||||
|
||||
// sort tenants by putting the keys in an array first
|
||||
var tenantkeys = [];
|
||||
var k;
|
||||
|
||||
for (k in tenants) {
|
||||
tenantkeys.push(k);
|
||||
}
|
||||
tenantkeys.sort();
|
||||
return tenantkeys[0];
|
||||
}
|
||||
|
||||
validateTenant(username, requestedTenant, tenants, globalEnabled, privateEnabled) {
|
||||
// delete user from tenants first to check if we have a tenant to choose from at all
|
||||
// keep original preferences untouched, we need the original values again
|
||||
// http://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object
|
||||
var tenantsCopy = JSON.parse(JSON.stringify(tenants));
|
||||
delete tenantsCopy[username];
|
||||
|
||||
// sanity check: no global, no private, no other tenants -> no tenant available
|
||||
if (!globalEnabled && !privateEnabled && _.isEmpty(tenantsCopy)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// requested tenant accessible for user
|
||||
if (tenants[requestedTenant] != undefined) {
|
||||
return requestedTenant;
|
||||
}
|
||||
|
||||
if ((requestedTenant == "__user__" || requestedTenant == "private") && tenants[username] && privateEnabled) {
|
||||
return "__user__";
|
||||
}
|
||||
|
||||
if ((requestedTenant == "global" || requestedTenant === '') && globalEnabled) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
71
lib/backend/opendistro_security_plugin.js
Normal file
71
lib/backend/opendistro_security_plugin.js
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Security plugin extension for the Elasticsearch Javascript client.
|
||||
*/
|
||||
|
||||
import util from 'util';
|
||||
|
||||
export default function (Client, config, components) {
|
||||
|
||||
const ca = components.clientAction.factory;
|
||||
|
||||
Client.prototype.opendistro_security = components.clientAction.namespaceFactory();
|
||||
|
||||
Client.prototype.opendistro_security.prototype.authinfo = ca({
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/authinfo'
|
||||
}
|
||||
});
|
||||
|
||||
Client.prototype.opendistro_security.prototype.multitenancyinfo = ca({
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/kibanainfo'
|
||||
}
|
||||
});
|
||||
|
||||
Client.prototype.opendistro_security.prototype.tenantinfo = ca({
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/tenantinfo'
|
||||
}
|
||||
});
|
||||
|
||||
Client.prototype.opendistro_security.prototype.authtoken = ca({
|
||||
method: 'POST',
|
||||
needBody: true,
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/authtoken'
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import Boom from 'boom';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
import SecurityConfigurationPlugin from './opendistro_security_configuration_plugin';
|
||||
import wrapElasticsearchError from '../../backend/errors/wrap_elasticsearch_error';
|
||||
import NotFoundError from '../../backend/errors/not_found';
|
||||
import filterAuthHeaders from '../../auth/filter_auth_headers';
|
||||
import Joi from 'joi'
|
||||
import internalusers_schema from '../validation/internalusers'
|
||||
import actiongroups_schema from '../validation/actiongroups'
|
||||
import roles_schema from '../validation/roles'
|
||||
import rolesmapping_schema from '../validation/rolesmapping'
|
||||
/**
|
||||
* The Security backend.
|
||||
*/
|
||||
export default class SecurityConfigurationBackend {
|
||||
|
||||
constructor(server) {
|
||||
const config = Object.assign({ plugins: [SecurityConfigurationPlugin], auth: true }, server.config().get('elasticsearch'));
|
||||
this._cluster = server.plugins.elasticsearch.createCluster('configuration',
|
||||
config
|
||||
);
|
||||
|
||||
this._client = this._cluster._noAuthClient;
|
||||
|
||||
// the es config for later use
|
||||
this._esconfig = server.config().get('elasticsearch');
|
||||
|
||||
this.getValidator = (resourceName) => {
|
||||
switch (resourceName) {
|
||||
case 'internalusers':
|
||||
return internalusers_schema;
|
||||
case 'actiongroups':
|
||||
return actiongroups_schema;
|
||||
case 'rolesmapping':
|
||||
return rolesmapping_schema;
|
||||
case 'roles':
|
||||
return roles_schema;
|
||||
default:
|
||||
throw new Error('Unknown resource');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async restapiinfo(headers) {
|
||||
try {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
const response = await this._client.opendistro_security.restapiinfo({
|
||||
headers: authHeaders
|
||||
});
|
||||
return response
|
||||
} catch(error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async indices(headers) {
|
||||
try {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
const response = await this._client.opendistro_security.indices({
|
||||
headers: authHeaders
|
||||
});
|
||||
return response;
|
||||
} catch(error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async list(headers, resourceName) {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
try {
|
||||
const response = await this._client.opendistro_security.listResource({
|
||||
resourceName: resourceName,
|
||||
headers: authHeaders
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async get(headers, resourceName, id) {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
try {
|
||||
const response = await this._client.opendistro_security.getResource({
|
||||
resourceName: resourceName,
|
||||
id,
|
||||
headers: authHeaders
|
||||
});
|
||||
var jsonRes = JSON.parse(response)
|
||||
return jsonRes[id];
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async save(headers, resourceName, id, body) {
|
||||
const result = Joi.validate(body, this.getValidator(resourceName));
|
||||
if (result.error) {
|
||||
throw Boom.create(500, "Resource not valid");
|
||||
}
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
try {
|
||||
const response = await this._client.opendistro_security.saveResource({
|
||||
resourceName: resourceName,
|
||||
id,
|
||||
body: body,
|
||||
headers: authHeaders
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(headers, resourceName, id) {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
try {
|
||||
return await this._client.opendistro_security.deleteResource({
|
||||
resourceName: resourceName,
|
||||
id,
|
||||
headers: authHeaders
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async clearCache(headers, certificates) {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
try {
|
||||
const response = await this._client.opendistro_security.clearCache({
|
||||
headers: authHeaders
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async validateDls(headers, indexname, body) {
|
||||
const authHeaders = filterAuthHeaders(headers, this._esconfig.requestHeadersWhitelist);
|
||||
try {
|
||||
const response = await this._client.opendistro_security.validateDls({
|
||||
body: body,
|
||||
headers: authHeaders
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import util from 'util';
|
||||
import Joi from 'joi';
|
||||
|
||||
export default function (Client, config, components) {
|
||||
|
||||
const ca = components.clientAction.factory;
|
||||
|
||||
Client.prototype.opendistro_security = components.clientAction.namespaceFactory();
|
||||
|
||||
Client.prototype.opendistro_security.prototype.restapiinfo = ca({
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/permissionsinfo'
|
||||
}
|
||||
});
|
||||
|
||||
Client.prototype.opendistro_security.prototype.indices = ca({
|
||||
url: {
|
||||
fmt: '/_all/_mapping/field/*'
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Returns a Security resource configuration.
|
||||
*
|
||||
* Sample response:
|
||||
*
|
||||
* {
|
||||
* "user": {
|
||||
* "hash": "#123123"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
Client.prototype.opendistro_security.prototype.listResource = ca({
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/<%=resourceName%>',
|
||||
req: {
|
||||
resourceName: {
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a Security resource instance.
|
||||
*
|
||||
* At the moment Security does not support conflict detection,
|
||||
* so this method can be effectively used to both create and update resource.
|
||||
*
|
||||
* Sample response:
|
||||
*
|
||||
* {
|
||||
* "status": "CREATED",
|
||||
* "message": "User username created"
|
||||
* }
|
||||
*/
|
||||
Client.prototype.opendistro_security.prototype.saveResource = ca({
|
||||
method: 'PUT',
|
||||
needBody: true,
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/<%=resourceName%>/<%=id%>',
|
||||
req: {
|
||||
resourceName: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a Security resource instance.
|
||||
*
|
||||
* Sample response:
|
||||
*
|
||||
* {
|
||||
* "user": {
|
||||
* "hash": '#123123'
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
Client.prototype.opendistro_security.prototype.getResource = ca({
|
||||
method: 'GET',
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/<%=resourceName%>/<%=id%>',
|
||||
req: {
|
||||
resourceName: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a Security resource instance.
|
||||
*/
|
||||
Client.prototype.opendistro_security.prototype.deleteResource = ca({
|
||||
method: 'DELETE',
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/<%=resourceName%>/<%=id%>',
|
||||
req: {
|
||||
resourceName: {
|
||||
type: 'string',
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a Security resource instance.
|
||||
*/
|
||||
Client.prototype.opendistro_security.prototype.clearCache = ca({
|
||||
method: 'DELETE',
|
||||
url: {
|
||||
fmt: '/_opendistro/_security/api/cache',
|
||||
}
|
||||
});
|
||||
|
||||
Client.prototype.opendistro_security.prototype.validateDls = ca({
|
||||
method: 'POST',
|
||||
needBody: true,
|
||||
url: {
|
||||
fmt: '/_validate/query?explain=true'
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
11
lib/configuration/resources.js
Normal file
11
lib/configuration/resources.js
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Authentication backend resource identifiers.
|
||||
*/
|
||||
// TODO: get rid of resources mapping, or do it once
|
||||
export default {
|
||||
CONFIG: 'config',
|
||||
INTERNAL_USER: 'InternalUser',
|
||||
ROLE: 'Role',
|
||||
ROLEMAPPING: 'RoleMapping',
|
||||
ACTIONGROUP: 'ActionGroup'
|
||||
};
|
229
lib/configuration/routes/routes.js
Normal file
229
lib/configuration/routes/routes.js
Normal file
@ -0,0 +1,229 @@
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
import Resources from './../resources';
|
||||
|
||||
/**
|
||||
* The backend API allows to manage the backend configuration.
|
||||
*
|
||||
* NOTE: All routes are POST since the REST API requires an admin certificat to access
|
||||
*
|
||||
*/
|
||||
export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
|
||||
|
||||
const config = server.config();
|
||||
const backend = server.plugins.opendistro_security.getSecurityConfigurationBackend();
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of resource instances.
|
||||
*
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/configuration/{resourceName}`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const results = await backend.list(request.headers, request.params.resourceName);
|
||||
return reply({
|
||||
total: Object.keys(results).length,
|
||||
data: results
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
config: {
|
||||
validate: {
|
||||
params: {
|
||||
resourceName: Joi.string().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a resource instance.
|
||||
*
|
||||
* Response sample:
|
||||
*
|
||||
* {
|
||||
* "id": "kibiuser",
|
||||
* }
|
||||
*/
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/configuration/{resourceName}/{id}`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const result = await backend.get(request.headers, request.params.resourceName, request.params.id);
|
||||
return reply(result);
|
||||
} catch (error) {
|
||||
if (error.name === 'NotFoundError') {
|
||||
return reply(Boom.notFound(`${request.params.id} not found.`));
|
||||
} else {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
config: {
|
||||
validate: {
|
||||
params: {
|
||||
resourceName: Joi.string().required(),
|
||||
id: Joi.string().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a resource instance.
|
||||
*
|
||||
* Response sample:
|
||||
*
|
||||
* {
|
||||
* "message": "Deleted user username"
|
||||
* }
|
||||
*/
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${API_ROOT}/configuration/{resourceName}/{id}`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const response = await backend.delete(request.headers, request.params.resourceName, request.params.id);
|
||||
return reply({
|
||||
message: response.message
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
config: {
|
||||
validate: {
|
||||
params: {
|
||||
resourceName: Joi.string().required(),
|
||||
id: Joi.string().required()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Updates or creates a resource
|
||||
*
|
||||
* Request sample:
|
||||
*
|
||||
* {
|
||||
* "password": "password"
|
||||
* }
|
||||
*/
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/configuration/{resourceName}/{id}`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const response = await backend.save(request.headers, request.params.resourceName, request.params.id, request.payload);
|
||||
return reply({
|
||||
message: response.message
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: `${API_ROOT}/configuration/cache`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const response = await backend.clearCache(request.headers);
|
||||
return reply({
|
||||
message: response.message
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/restapiinfo`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const response = await backend.restapiinfo(request.headers);
|
||||
return reply(response);
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/configuration/indices`,
|
||||
handler: (request, reply) => {
|
||||
try {
|
||||
let response = backend.indices(request.headers);
|
||||
return reply(response);
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/configuration/validatedls/{indexName}`,
|
||||
handler: {
|
||||
async: async (request, reply) => {
|
||||
try {
|
||||
const response = await backend.validateDls(request.headers, request.params.indexName, request.payload);
|
||||
return reply(response);
|
||||
} catch (error) {
|
||||
if (error.isBoom) {
|
||||
return reply(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
5
lib/configuration/validation/actiongroups.js
Normal file
5
lib/configuration/validation/actiongroups.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
export default Joi.object().keys({
|
||||
permissions: Joi.array().items(Joi.string())
|
||||
});
|
7
lib/configuration/validation/internalusers.js
Normal file
7
lib/configuration/validation/internalusers.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
export default Joi.object().keys({
|
||||
password: Joi.string().allow(''),
|
||||
roles: Joi.array().items(Joi.string()),
|
||||
attributes: Joi.object()
|
||||
});
|
16
lib/configuration/validation/roles.js
Normal file
16
lib/configuration/validation/roles.js
Normal file
@ -0,0 +1,16 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
export default Joi.object().keys({
|
||||
cluster: Joi.array().items(Joi.string()),
|
||||
tenants: Joi.object().pattern(/.*/, Joi.alternatives().try("RO", "RW")),
|
||||
indices: Joi.object()
|
||||
// indices, everything allowed
|
||||
.pattern(/.*/, Joi.object({
|
||||
// dls and fls keys, fixed
|
||||
_dls_: Joi.string(),
|
||||
_fls_: Joi.array().items(Joi.string())
|
||||
})
|
||||
// doctypes, everything allowed
|
||||
.pattern(/.*/, Joi.array().items(Joi.string())
|
||||
))
|
||||
});
|
7
lib/configuration/validation/rolesmapping.js
Normal file
7
lib/configuration/validation/rolesmapping.js
Normal file
@ -0,0 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
export default Joi.object().keys({
|
||||
backendroles: Joi.array().items(Joi.string()),
|
||||
hosts: Joi.array().items(Joi.string()),
|
||||
users: Joi.array().items(Joi.string())
|
||||
});
|
51
lib/elasticsearch/setup_index_template.js
Normal file
51
lib/elasticsearch/setup_index_template.js
Normal file
@ -0,0 +1,51 @@
|
||||
import Promise from 'bluebird';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
|
||||
export default function (plugin, server) {
|
||||
|
||||
const callAdminAsKibanaUser = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;
|
||||
const index = server.config().get('kibana.index');
|
||||
const mappings = server.getKibanaIndexMappingsDsl();
|
||||
|
||||
function waitForElasticsearchGreen() {
|
||||
return new Promise((resolve) => {
|
||||
server.plugins.elasticsearch.status.once('green', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async function setupIndexTemplate() {
|
||||
const adminCluster = server.plugins.elasticsearch.getCluster('admin');
|
||||
|
||||
try {
|
||||
await callAdminAsKibanaUser('indices.putTemplate', {
|
||||
name: `kibana_index_template:${index}_*`,
|
||||
body: {
|
||||
template: index+"_*",
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: server.getKibanaIndexMappingsDsl(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
server.log(['debug', 'setupIndexTemplate'], {
|
||||
tmpl: 'Error setting up indexTemplate for SavedObjects: <%= err.message %>',
|
||||
es: {
|
||||
resp: error.body,
|
||||
status: error.status,
|
||||
},
|
||||
err: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
throw new adminCluster.errors.ServiceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
setupIndexTemplate: setupIndexTemplate,
|
||||
waitForElasticsearchGreen: waitForElasticsearchGreen
|
||||
};
|
||||
|
||||
}
|
115
lib/hapi/auth.js
Normal file
115
lib/hapi/auth.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import {assign} from 'lodash';
|
||||
import User from '../auth/user';
|
||||
const querystring = require('querystring')
|
||||
|
||||
export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
|
||||
const config = server.config();
|
||||
const basePath = config.get('server.basePath');
|
||||
const unauthenticatedRoutes = config.get('opendistro_security.basicauth.unauthenticated_routes');
|
||||
// START add default unauthenticated routes
|
||||
// END add default unauthenticated routes
|
||||
const cookieConfig = {
|
||||
password: config.get('opendistro_security.cookie.password'),
|
||||
cookie: config.get('opendistro_security.cookie.name'),
|
||||
isSecure: config.get('opendistro_security.cookie.secure'),
|
||||
validateFunc: pluginRoot('lib/session/validate')(server),
|
||||
ttl: config.get('opendistro_security.cookie.ttl')
|
||||
};
|
||||
|
||||
server.auth.strategy('security_access_control_cookie', 'cookie', false, cookieConfig);
|
||||
|
||||
server.auth.scheme('security_access_control_scheme', (server, options) => ({
|
||||
authenticate: (request, reply) => {
|
||||
if (request.headers.authorization) {
|
||||
var tmp = request.headers.authorization.split(' ');
|
||||
var creds = new Buffer(tmp[1], 'base64').toString().split(':');
|
||||
var username = creds[0];
|
||||
var password = creds[1];
|
||||
var credentials = server.plugins.opendistro_security.getSecurityBackend().getUser(username, password);
|
||||
reply.continue({credentials});
|
||||
return;
|
||||
}
|
||||
// let configured routes that are not under our control pass,
|
||||
// for example /api/status to check Kibana status without a logged in user
|
||||
if (unauthenticatedRoutes.includes(request.path)) {
|
||||
var credentials = server.plugins.opendistro_security.getSecurityBackend().getServerUser();
|
||||
reply.continue({credentials});
|
||||
return;
|
||||
};
|
||||
|
||||
server.auth.test('security_access_control_cookie', request, (error, credentials) => {
|
||||
if (error) {
|
||||
if (request.url.path.indexOf(API_ROOT) === 0 || request.method !== 'get') {
|
||||
return reply(Boom.forbidden(error));
|
||||
} else {
|
||||
// If the session has expired, we may receive ajax requests that can't handle a 302 redirect.
|
||||
// In this case, we trigger a 401 and let the interceptor handle the redirect on the client side.
|
||||
if (request.headers && request.headers.accept && request.headers.accept.split(',').indexOf('application/json') > -1) {
|
||||
// The redirectTo property in the payload tells the interceptor to handle this error.
|
||||
return reply({message: 'Session expired', redirectTo: 'login'}).code(401);
|
||||
}
|
||||
|
||||
const nextUrl = encodeURIComponent(request.url.path);
|
||||
|
||||
return reply.redirect(`${basePath}${APP_ROOT}/login?nextUrl=${nextUrl}`);
|
||||
}
|
||||
}
|
||||
reply.continue({credentials});
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
server.auth.strategy('security_access_control', 'security_access_control_scheme', true);
|
||||
|
||||
server.ext('onPostAuth', function (request, next) {
|
||||
|
||||
if (request.auth && request.auth.isAuthenticated) {
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
return backend.getAuthHeaders(request.auth.credentials)
|
||||
.then((headers) => {
|
||||
assign(request.headers, headers);
|
||||
return next.continue();
|
||||
})
|
||||
.catch((error) => {
|
||||
server.log(['security', 'error'], `An error occurred while computing auth headers, clearing session: ${error}`);
|
||||
request.auth.session.clear();
|
||||
// redirect to login somehow?
|
||||
return next.continue();
|
||||
});
|
||||
}
|
||||
return next.continue();
|
||||
});
|
||||
|
||||
}
|
60
lib/jwt/headers.js
Normal file
60
lib/jwt/headers.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {assign} from 'lodash';
|
||||
|
||||
export default function (pluginRoot, server, APP_ROOT, API_ROOT) {
|
||||
|
||||
const config = server.config();
|
||||
const basePath = config.get('server.basePath');
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
const urlparamname = server.config().get('opendistro_security.jwt.url_param');
|
||||
const headername = server.config().get('opendistro_security.jwt.header');
|
||||
|
||||
server.ext('onPostAuth', async function (request, next) {
|
||||
|
||||
var jwtBearer = request.state.security_jwt;
|
||||
var jwtAuthParam = request.query[urlparamname];
|
||||
|
||||
if(jwtAuthParam != null) {
|
||||
jwtBearer = jwtAuthParam;
|
||||
next.state('security_jwt', jwtBearer);
|
||||
}
|
||||
|
||||
if (jwtBearer != null) {
|
||||
var headerValue = "Bearer " + jwtBearer;
|
||||
var headers = {};
|
||||
headers[headername] = headerValue;
|
||||
assign(request.headers, headers);
|
||||
}
|
||||
return next.continue();
|
||||
});
|
||||
}
|
157
lib/multitenancy/headers.js
Normal file
157
lib/multitenancy/headers.js
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import {assign} from 'lodash';
|
||||
|
||||
export default function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT, authClass) {
|
||||
|
||||
const config = server.config();
|
||||
const basePath = config.get('server.basePath');
|
||||
const global_enabled = config.get('opendistro_security.multitenancy.tenants.enable_global');
|
||||
const private_enabled = config.get('opendistro_security.multitenancy.tenants.enable_private');
|
||||
const preferredTenants = config.get('opendistro_security.multitenancy.tenants.preferred');
|
||||
const debugEnabled = config.get('opendistro_security.multitenancy.debug');
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
|
||||
const defaultSpaceId = 'default';
|
||||
|
||||
server.ext('onPreAuth', async function (request, next) {
|
||||
|
||||
// default is the tenant stored in the tenants cookie
|
||||
const storedSelectedTenant = request.auth.securitySessionStorage.getStorage('tenant', {}).selected;
|
||||
let selectedTenant = storedSelectedTenant;
|
||||
|
||||
if (debugEnabled) {
|
||||
request.log(['info', 'security', 'tenant_storagecookie'], selectedTenant);
|
||||
}
|
||||
|
||||
// check for tenant information in HTTP header. E.g. when using the saved objects API
|
||||
if(request.headers.securitytenant || request.headers.security_tenant) {
|
||||
selectedTenant = request.headers.securitytenant? request.headers.securitytenant : request.headers.security_tenant;
|
||||
|
||||
if (debugEnabled) {
|
||||
request.log(['info', 'security', 'tenant_http_header'], selectedTenant);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check for tenant information in GET parameter. E.g. when using a share link. Overwrites the HTTP header.
|
||||
if (request.query && (request.query.security_tenant || request.query.securitytenant)) {
|
||||
selectedTenant = request.query.security_tenant? request.query.security_tenant : request.query.securitytenant;
|
||||
|
||||
if (debugEnabled) {
|
||||
request.log(['info', 'security', 'tenant_url_param'], selectedTenant);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MT is only relevant for these paths
|
||||
if (!request.path.startsWith("/elasticsearch") && !request.path.startsWith("/api") && !request.path.startsWith("/app") && request.path != "/" && !selectedTenant) {
|
||||
return next.continue();
|
||||
}
|
||||
|
||||
var response;
|
||||
|
||||
try {
|
||||
if (authClass) {
|
||||
authClass.assignAuthHeader(request);
|
||||
}
|
||||
|
||||
response = await request.auth.securitySessionStorage.getAuthInfo(request.headers);
|
||||
} catch(error) {
|
||||
return next.continue();
|
||||
}
|
||||
|
||||
// if we have a tenant, check validity and set it
|
||||
if (typeof selectedTenant !== 'undefined' && selectedTenant !== null) {
|
||||
selectedTenant = backend.validateTenant(response.user_name, selectedTenant, response.tenants, global_enabled, private_enabled);
|
||||
} else {
|
||||
// no tenant, choose configured, preferred tenant
|
||||
try {
|
||||
selectedTenant = backend.getTenantByPreference(request, response.user_name, response.tenants, preferredTenants, global_enabled, private_enabled);
|
||||
} catch(error) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedTenant != storedSelectedTenant) {
|
||||
// save validated tenant as preference
|
||||
let prefcookie = backend.updateAndGetTenantPreferences(request, response.user_name, selectedTenant);
|
||||
request.auth.securitySessionStorage.putStorage('tenant', {
|
||||
selected: selectedTenant
|
||||
});
|
||||
next.state('security_preferences', prefcookie);
|
||||
}
|
||||
|
||||
if (debugEnabled) {
|
||||
request.log(['info', 'security', 'tenant_assigned'], selectedTenant);
|
||||
}
|
||||
|
||||
if (selectedTenant != null) {
|
||||
assign(request.headers, {'securitytenant' : selectedTenant});
|
||||
}
|
||||
|
||||
|
||||
// Test for default space?
|
||||
if (config.has('xpack.spaces.enabled') && config.get('xpack.spaces.enabled') && (request.path === '/' || request.path.startsWith('/app'))) {
|
||||
|
||||
// We can't add a default space for RO tenants at the moment
|
||||
if (selectedTenant && response.tenants[selectedTenant] === false) {
|
||||
return next.continue();
|
||||
}
|
||||
|
||||
const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request);
|
||||
let defaultSpace = null;
|
||||
|
||||
try {
|
||||
defaultSpace = await spacesClient.get(defaultSpaceId)
|
||||
} catch(error) {
|
||||
// Most likely not really an error, default space just not found
|
||||
}
|
||||
|
||||
if (defaultSpace === null) {
|
||||
try {
|
||||
await spacesClient.create({
|
||||
id: defaultSpaceId,
|
||||
name: 'Default',
|
||||
description: 'This is your default space!',
|
||||
color: '#00bfb3',
|
||||
_reserved: true
|
||||
});
|
||||
} catch(error) {
|
||||
server.log(['security', 'error'], `An error occurred while creating a default space`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next.continue();
|
||||
});
|
||||
}
|
117
lib/multitenancy/migrate_tenants.js
Normal file
117
lib/multitenancy/migrate_tenants.js
Normal file
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import Boom from 'boom';
|
||||
import elasticsearch from 'elasticsearch';
|
||||
import wrapElasticsearchError from './../backend/errors/wrap_elasticsearch_error';
|
||||
// import { KibanaMigrator } from '../../../../src/server/saved_objects/migrations';
|
||||
|
||||
async function migrateTenants (server) {
|
||||
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
|
||||
try {
|
||||
let tenantInfo = await backend.getTenantInfo(backend.getServerUserAuthHeader());
|
||||
|
||||
if (tenantInfo) {
|
||||
let indexNames = Object.keys(tenantInfo);
|
||||
for (var index = 0; index < indexNames.length; ++index) {
|
||||
await migrateTenantIndex(indexNames[index], server);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
server.log(['error', 'migration'], error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateTenantIndex(tenantIndexName, server) {
|
||||
const {kbnServer} = mockKbnServer(server.kibanaMigrator.kbnServer, server, tenantIndexName);
|
||||
const migrator = new KibanaMigrator({kbnServer});
|
||||
await migrator.awaitMigration();
|
||||
}
|
||||
|
||||
async function migrateTenant(tenantIndexName, force, server) {
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
try {
|
||||
let tenantInfo = await backend.getTenantInfo(backend.getServerUserAuthHeader());
|
||||
if (tenantInfo) {
|
||||
if (tenantInfo[tenantIndexName] || (force == true)) {
|
||||
await migrateTenantIndex(tenantIndexName, server);
|
||||
return {statusCode:200, message: tenantIndexName + " migrated."}
|
||||
} else {
|
||||
return Boom.badRequest('Index ' + tenantIndexName + ' not found or not a tenand index. Force migration: ' + force);
|
||||
}
|
||||
} else {
|
||||
return Boom.badImplementation("Could not fetch tenant info.");
|
||||
}
|
||||
} catch (error) {
|
||||
server.log(['error', 'migration'], error);
|
||||
return wrapElasticsearchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function mockKbnServer(originalKbnServer, server, indexname) {
|
||||
|
||||
const kbnServer = {
|
||||
version: originalKbnServer.version,
|
||||
ready: originalKbnServer.ready,
|
||||
uiExports: originalKbnServer.uiExports,
|
||||
server: {
|
||||
config: () => ({
|
||||
get: ((name) => {
|
||||
switch (name) {
|
||||
case 'kibana.index':
|
||||
return indexname;
|
||||
case 'migrations.batchSize':
|
||||
return originalKbnServer.server.config().get("migrations.batchSize");
|
||||
case 'migrations.pollInterval':
|
||||
return originalKbnServer.server.config().get("migrations.pollInterval");
|
||||
case 'migrations.scrollDuration':
|
||||
return originalKbnServer.server.config().get("migrations.scrollDuration");
|
||||
default:
|
||||
throw new Error(`Unexpected config ${name}`);
|
||||
}
|
||||
})
|
||||
}),
|
||||
log: function (tags, data, timestamp, _internal) {
|
||||
server.log(tags, data, timestamp, _internal);
|
||||
},
|
||||
plugins: originalKbnServer.server.plugins
|
||||
}
|
||||
};
|
||||
|
||||
return { kbnServer };
|
||||
}
|
||||
|
||||
module.exports.migrateTenants=migrateTenants;
|
||||
module.exports.migrateTenant=migrateTenant;
|
103
lib/multitenancy/routes.js
Normal file
103
lib/multitenancy/routes.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import Joi from 'joi';
|
||||
import indexTemplate from '../elasticsearch/setup_index_template';
|
||||
import { migrateTenant } from './migrate_tenants';
|
||||
|
||||
module.exports = function (pluginRoot, server, kbnServer, APP_ROOT, API_ROOT) {
|
||||
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
const { setupIndexTemplate } = indexTemplate(this, server);
|
||||
const debugEnabled = server.config().get("opendistro_security.multitenancy.debug");
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/multitenancy/tenant`,
|
||||
handler: (request, reply) => {
|
||||
|
||||
var username = request.payload.username;
|
||||
var selectedTenant = request.payload.tenant;
|
||||
var prefs = backend.updateAndGetTenantPreferences(request, username, selectedTenant);
|
||||
|
||||
request.auth.securitySessionStorage.putStorage('tenant', {
|
||||
selected: selectedTenant,
|
||||
});
|
||||
|
||||
if (debugEnabled) {
|
||||
request.log(['info', 'security', 'tenant_POST'], selectedTenant);
|
||||
}
|
||||
|
||||
return reply(request.payload.tenant).state('security_preferences', prefs);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/multitenancy/tenant`,
|
||||
handler: (request, reply) => {
|
||||
let selectedTenant = request.auth.securitySessionStorage.getStorage('tenant', {}).selected;
|
||||
|
||||
if (debugEnabled) {
|
||||
request.log(['info', 'security', 'tenant_GET'], selectedTenant);
|
||||
}
|
||||
|
||||
return reply(selectedTenant);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: `${API_ROOT}/multitenancy/info`,
|
||||
handler: (request, reply) => {
|
||||
let mtinfo = server.plugins.opendistro_security.getSecurityBackend().multitenancyinfo(request.headers);
|
||||
return reply(mtinfo);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: `${API_ROOT}/multitenancy/migrate/{tenantindex}`,
|
||||
handler: async (request, reply) => {
|
||||
if (!request.params.tenantindex) {
|
||||
return reply(Boom.badRequest, "Please provide a tenant index name.")
|
||||
}
|
||||
let forceMigration = false;
|
||||
if (request.query.force && request.query.force == "true") {
|
||||
forceMigration = true
|
||||
}
|
||||
let result = await migrateTenant(request.params.tenantindex, forceMigration, server);
|
||||
reply (result);
|
||||
}
|
||||
});
|
||||
|
||||
}; //end module
|
332
lib/session/sessionPlugin.js
Executable file
332
lib/session/sessionPlugin.js
Executable file
@ -0,0 +1,332 @@
|
||||
import MissingTenantError from "../auth/errors/missing_tenant_error";
|
||||
import MissingRoleError from "../auth/errors/missing_role_error";
|
||||
|
||||
var Hoek = require('hoek');
|
||||
var Joi = require('joi');
|
||||
|
||||
/**
|
||||
* Name of the cookie where we store additional session information, such as authInfo
|
||||
* @type {string}
|
||||
*/
|
||||
const storageCookieName = 'security_storage';
|
||||
|
||||
let internals = {};
|
||||
|
||||
internals.config = Joi.object({
|
||||
authType: Joi.string().allow(null),
|
||||
authHeaderName: Joi.string(),
|
||||
authenticateFunction: Joi.func(),
|
||||
validateAvailableTenants: Joi.boolean().default(true),
|
||||
validateAvailableRoles: Joi.boolean().default(true)
|
||||
}).required();
|
||||
|
||||
|
||||
exports.register = async function (server, options, next) {
|
||||
let results = Joi.validate(options, internals.config);
|
||||
Hoek.assert(!results.error, results.error);
|
||||
|
||||
let settings = results.value;
|
||||
|
||||
// @todo Don't register e.g. authenticate() when we have Kerberos or Proxy-Auth?
|
||||
server.ext('onPreAuth', function (request, reply) {
|
||||
request.auth.securitySessionStorage = {
|
||||
|
||||
/**
|
||||
* Tries to authenticate a user. If multitenancy is enabled, we also try to validate that the
|
||||
* user has at least one valid tenant
|
||||
* @param {object} credentials
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
authenticate: async function(credentials, options = {}) {
|
||||
try {
|
||||
// authResponse is an object with .session and .user
|
||||
const authResponse = await settings.authenticateFunction(credentials, options);
|
||||
|
||||
return this._handleAuthResponse(credentials, authResponse);
|
||||
} catch(error) {
|
||||
// Make sure we clear any existing cookies if something went wrong
|
||||
this.clear();
|
||||
throw error;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
authenticateWithHeaders: async function(headers, credentials = {}, options = {}) {
|
||||
try {
|
||||
let user = await server.plugins.opendistro_security.getSecurityBackend().authenticateWithHeaders(headers);
|
||||
let session = {
|
||||
username: user.username,
|
||||
credentials: credentials,
|
||||
authType: settings.authType,
|
||||
/**
|
||||
* Used later to signal that we should not assign any specific auth header in AuthType
|
||||
*/
|
||||
assignAuthHeader: false
|
||||
};
|
||||
|
||||
let sessionTTL = server.config().get('opendistro_security.session.ttl')
|
||||
|
||||
if(sessionTTL) {
|
||||
session.expiryTime = Date.now() + sessionTTL
|
||||
}
|
||||
|
||||
const authResponse = {
|
||||
session,
|
||||
user
|
||||
};
|
||||
|
||||
return this._handleAuthResponse(credentials, authResponse)
|
||||
} catch(error) {
|
||||
// Make sure we clear any existing cookies if something went wrong
|
||||
this.clear();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Normalized response after an authentication
|
||||
* @param credentials
|
||||
* @param authResponse
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_handleAuthResponse: function(credentials, authResponse) {
|
||||
// Make sure the user has a tenant that they can use
|
||||
if(settings.validateAvailableTenants && server.config().get("opendistro_security.multitenancy.enabled") && ! server.config().get("opendistro_security.multitenancy.tenants.enable_global")) {
|
||||
let privateTenantEnabled = server.config().get("opendistro_security.multitenancy.tenants.enable_private");
|
||||
|
||||
let allTenants = authResponse.user.tenants;
|
||||
if (allTenants != null && ! privateTenantEnabled) {
|
||||
delete allTenants[authResponse.user.username]
|
||||
}
|
||||
|
||||
if (allTenants == null || Object.keys(allTenants).length === 0) {
|
||||
throw new MissingTenantError('No tenant available for this user, please contact your system administrator.')
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that the user has at least one valid role
|
||||
if (settings.validateAvailableRoles && (!authResponse.user.roles || authResponse.user.roles.length === 0)) {
|
||||
throw new MissingRoleError('No roles available for this user, please contact your system administrator.');
|
||||
}
|
||||
|
||||
request.auth.session.set(authResponse.session);
|
||||
|
||||
this.setAuthInfo(authResponse.user.username, authResponse.user.backendroles, authResponse.user.roles, authResponse.user.tenants, authResponse.user.selectedTenant);
|
||||
|
||||
return authResponse;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current auth type
|
||||
* @returns {void | null}
|
||||
*/
|
||||
getAuthType: function() {
|
||||
return settings.authType;
|
||||
},
|
||||
|
||||
getAuthHeaderName: function() {
|
||||
return settings.authHeaderName;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remember to call this in the correct lifecycle step. Calling this in onPreAuth will most likely return false because auth is not set up yet.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAuthenticated: function() {
|
||||
if (request.auth && request.auth.isAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the session credentials
|
||||
* @returns {*}
|
||||
*/
|
||||
getSessionCredentials: function() {
|
||||
if (this.isAuthenticated()) {
|
||||
return request.auth.credentials;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the cookies associated with the authenticated user
|
||||
*/
|
||||
clear: function() {
|
||||
request.auth.session.clear();
|
||||
reply.unstate(storageCookieName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the content of the storage cookie or, when key is defined, a part of it
|
||||
* @param key
|
||||
* @param whenMissing - Allows for a default value when the given key is not in the cookie
|
||||
* @returns {*}
|
||||
*/
|
||||
getStorage: function(key, whenMissing = null) {
|
||||
let storage = request.state[storageCookieName];
|
||||
|
||||
if (! storage) {
|
||||
return whenMissing;
|
||||
}
|
||||
|
||||
if (! key) {
|
||||
return storage;
|
||||
}
|
||||
|
||||
if (key && storage[key]) {
|
||||
return storage[key];
|
||||
}
|
||||
|
||||
return whenMissing;
|
||||
},
|
||||
|
||||
/**
|
||||
* Store a value in the cookie
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
putStorage: function(key, value) {
|
||||
let storage = request.state[storageCookieName] || {};
|
||||
|
||||
if (! key) {
|
||||
// Bail if we don't have a key, the cookie should contain an object
|
||||
return;
|
||||
}
|
||||
|
||||
storage[key] = value;
|
||||
|
||||
reply.state(storageCookieName, storage);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the extra storage cookie only.
|
||||
* Use .clear to remove both the auth and the storage cookies
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Clears the extra storage cookie only.
|
||||
* Use .clear to remove both the auth and the storage cookies
|
||||
*
|
||||
* @param key - Pass a key to only delete a part of the storage cookie.
|
||||
*/
|
||||
clearStorage: function(key = null) {
|
||||
if (key === null) {
|
||||
reply.unstate(storageCookieName);
|
||||
return;
|
||||
}
|
||||
|
||||
let storage = this.getStorage();
|
||||
|
||||
if (storage && storage[key]) {
|
||||
delete storage[key];
|
||||
reply.state(storageCookieName, storage);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the result from the authinfo endpoint in the cookie.
|
||||
* We don't store everything at the moment.
|
||||
* @todo ask Jochen - custom_attribute_names could be too large for a cookie?
|
||||
*
|
||||
* @param user_name
|
||||
* @param backend_roles
|
||||
* @param roles
|
||||
* @param tenants
|
||||
* @param user_requested_tenant
|
||||
*/
|
||||
setAuthInfo: function(user_name, backend_roles, roles, tenants, user_requested_tenant) {
|
||||
|
||||
const authInfo = {
|
||||
user_name,
|
||||
backend_roles,
|
||||
roles,
|
||||
tenants,
|
||||
user_requested_tenant
|
||||
};
|
||||
|
||||
this.putStorage('authInfo', authInfo)
|
||||
},
|
||||
|
||||
/**
|
||||
* The storage cookie is coupled to the auth cookie, so we try to validate it similar to
|
||||
* how we would validate the auth cookie
|
||||
* @returns {*}
|
||||
*/
|
||||
validateStorageCookie: function() {
|
||||
let sessionStorage = this.getStorage();
|
||||
let authSession = this.getSessionCredentials();
|
||||
|
||||
// If we have an existing storage session and an existing auth session,
|
||||
// we can assume that they are connected. We should validate that
|
||||
// the auth session hasn't expired
|
||||
// @todo Is this really necessary? We write the authInfo every time
|
||||
// that we login, and we may need to provide the authInfo even
|
||||
// if we don't have an auth session (Kerberos?) @Jochen
|
||||
if (sessionStorage && authSession) {
|
||||
if (authSession.exp && authSession.exp < Math.floor(Date.now() / 1000)) {
|
||||
sessionStorage = null;
|
||||
}
|
||||
|
||||
if (authSession.expiryTime && authSession.expiryTime < Date.now()) {
|
||||
sessionStorage = null;
|
||||
}
|
||||
}
|
||||
|
||||
return sessionStorage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the authinfo from the storage cookie, if available.
|
||||
* If not available, we pass the request headers to the backend
|
||||
* and get the authinfo directly from there
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
getAuthInfo: async function() {
|
||||
|
||||
// See if we have the value in the cookie
|
||||
if (this.authType !== null) {
|
||||
let sessionStorage = this.validateStorageCookie();
|
||||
if (sessionStorage && sessionStorage.authInfo) {
|
||||
return sessionStorage.authInfo;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let authInfo = await server.plugins.opendistro_security.getSecurityBackend().authinfo(request.headers);
|
||||
// Don't save the authInfo in the cookie for e.g. Kerberos and Proxy-Auth
|
||||
if (this.authType !== null && this.isAuthenticated()) {
|
||||
this.setAuthInfo(authInfo.user_name, authInfo.backend_roles, authInfo.roles, authInfo.tenants, authInfo.user_requested_tenant);
|
||||
}
|
||||
|
||||
return authInfo;
|
||||
|
||||
} catch (error) {
|
||||
// Remove the storage cookie if something went wrong
|
||||
if (this.authType !== null) {
|
||||
reply.unstate(storageCookieName);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return reply.continue();
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
exports.register.attributes = {
|
||||
name: 'security-session-storage'
|
||||
};
|
65
lib/session/validate.js
Normal file
65
lib/session/validate.js
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import { assign } from 'lodash';
|
||||
import InvalidSessionError from '../auth/errors/invalid_session_error';
|
||||
|
||||
export default function (server) {
|
||||
|
||||
const config = server.config();
|
||||
const sessionTTL = config.get('opendistro_security.session.ttl');
|
||||
const sessionKeepAlive = config.get('opendistro_security.session.keepalive');
|
||||
|
||||
return function validate(request, session, callback) {
|
||||
try {
|
||||
const backend = server.plugins.opendistro_security.getSecurityBackend();
|
||||
if (sessionTTL) {
|
||||
if (!session.expiryTime || session.expiryTime < Date.now()) {
|
||||
return callback(new InvalidSessionError('Session expired.'), false);
|
||||
}
|
||||
}
|
||||
backend.authenticate(session.credentials).then((user) => {
|
||||
if (sessionTTL && sessionKeepAlive) {
|
||||
let extendedSession = {};
|
||||
assign(extendedSession, session);
|
||||
extendedSession.expiryTime = Date.now() + sessionTTL;
|
||||
request.auth.session.set(extendedSession);
|
||||
}
|
||||
return callback(null, true, user);
|
||||
}).catch((error) => {
|
||||
return callback(new InvalidSessionError('Invalid session.', error), false);
|
||||
});
|
||||
} catch (error) {
|
||||
return callback(new InvalidSessionError('Invalid session', error), false);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "opendistro_security",
|
||||
"version": "6.5.4",
|
||||
"description": "Security features for kibana",
|
||||
"main": "index.js",
|
||||
"homepage": "https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/kibana-ui-framework": "0.0.11",
|
||||
"bell": "^8.8.0",
|
||||
"boom": "5.2.0",
|
||||
"cookie": "^0.3.1",
|
||||
"hapi": "^16.0.1",
|
||||
"hapi-async-handler": "^1.0.3",
|
||||
"hapi-auth-cookie": "^3.1.0",
|
||||
"hapi-authorization": "^3.0.2",
|
||||
"joi": "10.6.0",
|
||||
"js-yaml": "^3.7.0",
|
||||
"requirefrom": "^0.2.0",
|
||||
"wreck": "10.x.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ui-select": "^0.19.8"
|
||||
}
|
||||
}
|
19
plugin.xml
Normal file
19
plugin.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0"?>
|
||||
<assembly>
|
||||
<id>plugin</id>
|
||||
<formats>
|
||||
<format>zip</format>
|
||||
</formats>
|
||||
<includeBaseDirectory>false</includeBaseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>${project.basedir}/build</directory>
|
||||
<outputDirectory>${file.separator}</outputDirectory>
|
||||
<useDefaultExcludes>false</useDefaultExcludes>
|
||||
<includes>
|
||||
<include>kibana/**</include>
|
||||
</includes>
|
||||
<fileMode>0755</fileMode>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
90
pom.xml
Normal file
90
pom.xml
Normal file
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!-- Copyright 2015-2017 floragunn GmbH
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
use this file except in compliance with the License. You may obtain a copy
|
||||
of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required
|
||||
by applicable law or agreed to in writing, software distributed under the
|
||||
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
OF ANY KIND, either express or implied. See the License for the specific
|
||||
language governing permissions and limitations under the License. -->
|
||||
<!--
|
||||
~ Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License").
|
||||
~ You may not use this file except in compliance with the License.
|
||||
~ A copy of the License is located at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ or in the "license" file accompanying this file. This file is distributed
|
||||
~ on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
~ express or implied. See the License for the specific language governing
|
||||
~ permissions and limitations under the License.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.amazon.opendistroforelasticsearch</groupId>
|
||||
<artifactId>opendistro_security_parent</artifactId>
|
||||
<version>0.7.0.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.amazon.opendistroforelasticsearch</groupId>
|
||||
<artifactId>opendistro_security_kibana_plugin</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>0.7.0.0</version>
|
||||
<name>Open Distro Security for Elasticsearch Kibana Plugin</name>
|
||||
<description>Elasticsearch Security Kibana Plugin for Elasticsearch 6</description>
|
||||
<url>https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin</url>
|
||||
<inceptionYear>2016</inceptionYear>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<properties>
|
||||
<plugin.descriptor>${basedir}/plugin.xml</plugin.descriptor>
|
||||
</properties>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin</url>
|
||||
<connection>scm:git:git@github.com:mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin.git</connection>
|
||||
<developerConnection>scm:git:git@github.com:mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin.git</developerConnection>
|
||||
<tag>v0.7.0.0</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
<system>GitHub</system>
|
||||
<url>https://github.com/mauve-hedgehog/opendistro-elasticsearch-security-kibana-plugin/issues</url>
|
||||
</issueManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>plugin</id>
|
||||
<phase>package</phase>
|
||||
<configuration>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<outputDirectory>${project.build.directory}/releases/</outputDirectory>
|
||||
<descriptors>
|
||||
<descriptor>${plugin.descriptor}</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
51
public/apps/accountinfo/accountinfo.html
Normal file
51
public/apps/accountinfo/accountinfo.html
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="security app-container ng-scope">
|
||||
|
||||
<div class="kuiLocalNav">
|
||||
<div class="kuiLocalNavRow">
|
||||
<div class="kuiLocalNavRow__section">
|
||||
<div class="kuiLocalTitle">
|
||||
Account Information
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
|
||||
<div class="kuiHeaderBar" style="margin-bottom: 20px; margin-top: 30px;">
|
||||
<div class="kuiHeaderBarSection">
|
||||
<h2 class="kuiSubTitle">Username</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li>{{ctrl.security_user.user_name}}</li>
|
||||
</ul>
|
||||
|
||||
<div class="kuiHeaderBar" style="margin-bottom: 20px; margin-top: 30px;">
|
||||
<div class="kuiHeaderBarSection">
|
||||
<h2 class="kuiSubTitle">Roles</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p data-ng-repeat="role in ctrl.security_user.roles" style="margin-bottom: 10px;">
|
||||
{{role}}
|
||||
</p>
|
||||
<div ng-show="ctrl.security_user.roles == 0" style="font-style: italic">No roles found, please check the role mapping for this user.</div>
|
||||
|
||||
<div class="kuiHeaderBar" style="margin-bottom: 20px; margin-top: 30px;">
|
||||
<div class="kuiHeaderBarSection">
|
||||
<h2 class="kuiSubTitle">Backend Roles</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p data-ng-repeat="backendrole in ctrl.security_user.backend_roles" style="margin-bottom: 10px;">
|
||||
{{backendrole}}
|
||||
</p>
|
||||
<div ng-show="ctrl.security_user.backend_roles.length == 0" style="font-style: italic">No backend roles found.</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
67
public/apps/accountinfo/accountinfo.js
Normal file
67
public/apps/accountinfo/accountinfo.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import uiRoutes from 'ui/routes';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import 'ui/autoload/styles';
|
||||
import infoTemplate from './accountinfo.html';
|
||||
|
||||
uiRoutes.enable();
|
||||
|
||||
uiRoutes
|
||||
.when('/', {
|
||||
template: infoTemplate,
|
||||
controller: 'accountInfoNavController',
|
||||
controllerAs: 'ctrl'
|
||||
});
|
||||
|
||||
uiModules
|
||||
.get('app/security-accountinfo')
|
||||
.controller('accountInfoNavController', function ($http, $window, Private, security_resolvedInfo) {
|
||||
|
||||
var APP_ROOT = `${chrome.getBasePath()}`;
|
||||
var API_ROOT = `${APP_ROOT}/api/v1`;
|
||||
|
||||
$http.get(`${API_ROOT}/auth/authinfo`)
|
||||
.then(
|
||||
(response) => {
|
||||
|
||||
this.security_user = response.data;
|
||||
},
|
||||
(error) => {
|
||||
toastNotifications.addDanger({
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
94
public/apps/configuration/backend_api/actiongroups.js
Normal file
94
public/apps/configuration/backend_api/actiongroups.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { merge } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import client from './client';
|
||||
|
||||
/**
|
||||
* Action groups API client service.
|
||||
*/
|
||||
uiModules.get('apps/opendistro_security/configuration', [])
|
||||
.service('backendActionGroups', function (backendAPI, Promise, $http, kbnUrl) {
|
||||
|
||||
const RESOURCE = 'actiongroups';
|
||||
|
||||
this.title = {
|
||||
singular: 'action group',
|
||||
plural: 'action groups'
|
||||
};
|
||||
|
||||
this.newLabel = "Action Group name";
|
||||
|
||||
this.list = () => {
|
||||
return backendAPI.list(RESOURCE);
|
||||
};
|
||||
|
||||
this.listSilent = () => {
|
||||
return backendAPI.listSilent(RESOURCE);
|
||||
};
|
||||
|
||||
this.get = (id) => {
|
||||
return backendAPI.get(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.save = (actiongroupname, data) => {
|
||||
sessionStorage.removeItem("actiongroupsautocomplete");
|
||||
sessionStorage.removeItem("actiongroupnames");
|
||||
var data = this.preSave(data);
|
||||
return backendAPI.save(RESOURCE, actiongroupname, data);
|
||||
};
|
||||
|
||||
this.delete = (id) => {
|
||||
sessionStorage.removeItem("actiongroupsautocomplete");
|
||||
sessionStorage.removeItem("actiongroupnames");
|
||||
return backendAPI.delete(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.listAutocomplete = (names) => {
|
||||
return backendAPI.listAutocomplete(names);
|
||||
};
|
||||
|
||||
this.emptyModel = () => {
|
||||
var actiongroup = {};
|
||||
actiongroup.permissions = [];
|
||||
actiongroup.actiongroups = [];
|
||||
return actiongroup;
|
||||
};
|
||||
|
||||
this.preSave = (actiongroup) => {
|
||||
var result = {};
|
||||
var all = [];
|
||||
all = all.concat(actiongroup.actiongroups);
|
||||
all = all.concat(actiongroup.permissions);
|
||||
// remove empty roles
|
||||
all = all.filter(e => String(e).trim());
|
||||
// remove duplicate roles
|
||||
all = uniq(all);
|
||||
result["permissions"] = all;
|
||||
return result;
|
||||
};
|
||||
|
||||
this.postFetch = (actiongroup) => {
|
||||
// we need to support old and new format of actiongroups,
|
||||
// normalize both formats to common representation
|
||||
|
||||
var permissionsArray = actiongroup;
|
||||
|
||||
// new SECURITY6 format, explicit permissions entry
|
||||
if (actiongroup.permissions) {
|
||||
permissionsArray = actiongroup.permissions;
|
||||
}
|
||||
// determine which format to use
|
||||
permissionsArray = backendAPI.cleanArraysFromDuplicates(permissionsArray);
|
||||
|
||||
var permissions = backendAPI.sortPermissions(permissionsArray);
|
||||
|
||||
// if readonly flag is set for SECURITY6 format, add as well
|
||||
if (actiongroup.readonly) {
|
||||
permissions["readonly"] = actiongroup.readonly;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
};
|
||||
|
||||
});
|
||||
|
245
public/apps/configuration/backend_api/client.js
Normal file
245
public/apps/configuration/backend_api/client.js
Normal file
@ -0,0 +1,245 @@
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { merge } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import { isPlainObject } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
/**
|
||||
* Backend API client service.
|
||||
*/
|
||||
uiModules.get('apps/opendistro_security/configuration', [])
|
||||
.service('backendAPI', function (Promise, $http, $window, kbnUrl, securityAccessControl) {
|
||||
|
||||
// Take the basePath configuration value into account
|
||||
// @url https://www.elastic.co/guide/en/kibana/current/development-basepath.html
|
||||
const AUTH_BACKEND_API_ROOT = chrome.addBasePath("/api/v1");
|
||||
|
||||
this.testConnection = () => {
|
||||
|
||||
return $http.post(`${AUTH_BACKEND_API_ROOT}/get/config`)
|
||||
.then((response) => {
|
||||
return 200;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status) {
|
||||
return error.status;
|
||||
} else {
|
||||
return 500;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.get = function(resourceName, id) {
|
||||
return $http.get(`${AUTH_BACKEND_API_ROOT}/configuration/${resourceName}/${id}`)
|
||||
.then((response) => {
|
||||
return response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status == 403) {
|
||||
securityAccessControl.logout();
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: 'Unable to load data.',
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
this.getSilent = function(resourceName, id, showError) {
|
||||
showError = typeof showError !== 'undefined' ? showError : true;
|
||||
return $http.get(`${AUTH_BACKEND_API_ROOT}/configuration/${resourceName}/${id}`)
|
||||
.then((response) => {
|
||||
return response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
// nothing
|
||||
});
|
||||
};
|
||||
|
||||
this.save = (resourceName, id, data) => {
|
||||
let url = `${AUTH_BACKEND_API_ROOT}/configuration/${resourceName}/${id}`;
|
||||
return $http.post(url, data)
|
||||
.then((response) => {
|
||||
toastNotifications.addSuccess({
|
||||
title: `'${id}' saved.`
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status == 403) {
|
||||
securityAccessControl.logout();
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
text: error.message
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
this.delete = (resourceName, id) => {
|
||||
return $http.delete(`${AUTH_BACKEND_API_ROOT}/configuration/${resourceName}/${id}`)
|
||||
.then((response) => {
|
||||
toastNotifications.addSuccess({
|
||||
title: `'${id}' deleted.`
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status == 403) {
|
||||
securityAccessControl.logout();
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: 'Unable to delete data.',
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
this.list = (resourceName) => {
|
||||
return $http.get(`${AUTH_BACKEND_API_ROOT}/configuration/${resourceName}`)
|
||||
.then((response) => {
|
||||
return response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status == 403) {
|
||||
securityAccessControl.logout();
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
text: error.message
|
||||
});
|
||||
}
|
||||
toastNotifications.addDanger({
|
||||
title: 'Unable to load data.',
|
||||
text: error.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.listSilent = (resourceName) => {
|
||||
return $http.get(`${AUTH_BACKEND_API_ROOT}/configuration/${resourceName}`)
|
||||
.then((response) => {
|
||||
return response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
// nothing
|
||||
});
|
||||
};
|
||||
|
||||
this.listAutocomplete = (names) => {
|
||||
var completeList = [];
|
||||
names.sort().forEach( function(name) {
|
||||
var autocomplete = {};
|
||||
autocomplete["name"] = name;
|
||||
completeList.push(autocomplete);
|
||||
} );
|
||||
return completeList;
|
||||
};
|
||||
|
||||
this.clearCache = () => {
|
||||
return $http.delete(`${AUTH_BACKEND_API_ROOT}/configuration/cache`)
|
||||
.then((response) => {
|
||||
toastNotifications.addSuccess({
|
||||
title: response.data.message
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.status == 403) {
|
||||
securityAccessControl.logout();
|
||||
} else {
|
||||
toastNotifications.addDanger({
|
||||
title: 'Unable to clear cache.',
|
||||
text: error.message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
this.cleanArraysFromDuplicates = function(theobject) {
|
||||
|
||||
// We assume we don't have any mixed arrays,
|
||||
// i.e. only arrays of one type
|
||||
if (Array.isArray(theobject) && !isEmpty(theobject)) {
|
||||
|
||||
var firstEntry = theobject[0];
|
||||
|
||||
// string arrays, clean it
|
||||
if (isString(firstEntry)) {
|
||||
return this.cleanArray(theobject);
|
||||
}
|
||||
|
||||
// object array, traverse down
|
||||
if (isPlainObject(firstEntry)) {
|
||||
for(var i = 0; i<theobject.length; i++) {
|
||||
theobject[i] = this.cleanArraysFromDuplicates(theobject[i]);
|
||||
}
|
||||
}
|
||||
// something else ...
|
||||
return theobject;
|
||||
}
|
||||
|
||||
// Object, traverse keys
|
||||
if (isPlainObject(theobject)) {
|
||||
var keys = Object.keys(theobject);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
theobject[keys[i]] = this.cleanArraysFromDuplicates(theobject[keys[i]])
|
||||
}
|
||||
}
|
||||
return theobject;
|
||||
}
|
||||
|
||||
this.mergeCleanArray = (array1, array2) => {
|
||||
var merged = [];
|
||||
if (array1){
|
||||
merged = merged.concat(array1);
|
||||
}
|
||||
if (array2) {
|
||||
merged = merged.concat(array2);
|
||||
}
|
||||
merged = this.cleanArray(merged);
|
||||
return merged;
|
||||
};
|
||||
|
||||
|
||||
this.cleanArray = (thearray) => {
|
||||
if (!thearray) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(thearray)) {
|
||||
return;
|
||||
}
|
||||
// remove empty entries
|
||||
thearray = thearray.filter(e => String(e).trim());
|
||||
// remove duplicate entries
|
||||
thearray = uniq(thearray);
|
||||
return thearray;
|
||||
};
|
||||
|
||||
this.sortPermissions = (permissionsArray) => {
|
||||
var actiongroups = [];
|
||||
var permissions = [];
|
||||
if (permissionsArray && Array.isArray(permissionsArray)) {
|
||||
permissionsArray.forEach(function (entry) {
|
||||
if (entry.startsWith("cluster:") || entry.startsWith("indices:")) {
|
||||
permissions.push(entry);
|
||||
} else {
|
||||
actiongroups.push(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
actiongroups: actiongroups,
|
||||
permissions: permissions
|
||||
}
|
||||
};
|
||||
|
||||
// taken from lodash, not provided by Kibana
|
||||
var isString = function(val) {
|
||||
return typeof val === 'string' || ((!!val && typeof val === 'object') && Object.prototype.toString.call(val) === '[object String]');
|
||||
}
|
||||
});
|
91
public/apps/configuration/backend_api/internalusers.js
Normal file
91
public/apps/configuration/backend_api/internalusers.js
Normal file
@ -0,0 +1,91 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { merge } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import client from './client';
|
||||
|
||||
/**
|
||||
* Internal users API client service.
|
||||
*/
|
||||
uiModules.get('apps/opendistro_security/configuration', [])
|
||||
.service('backendInternalUsers', function (backendAPI, Promise, $http) {
|
||||
|
||||
const RESOURCE = 'internalusers';
|
||||
|
||||
this.title = {
|
||||
singular: 'internal user',
|
||||
plural: 'internal users'
|
||||
};
|
||||
|
||||
this.newLabel = "Username";
|
||||
|
||||
this.list = () => {
|
||||
return backendAPI.list(RESOURCE);
|
||||
};
|
||||
|
||||
this.get = (username) => {
|
||||
return backendAPI.get(RESOURCE, username);
|
||||
};
|
||||
|
||||
this.save = (username, data) => {
|
||||
data = this.preSave(data);
|
||||
return backendAPI.save(RESOURCE, username, data);
|
||||
};
|
||||
|
||||
this.delete = (username) => {
|
||||
return backendAPI.delete(RESOURCE, username);
|
||||
};
|
||||
|
||||
this.emptyModel = () => {
|
||||
var user = {};
|
||||
user["password"] = "";
|
||||
user["passwordConfirmation"] = "";
|
||||
user.roles = [];
|
||||
user.attributesArray = [];
|
||||
return user;
|
||||
};
|
||||
|
||||
this.preSave = (user) => {
|
||||
delete user["passwordConfirmation"];
|
||||
// remove empty roles
|
||||
user.roles = user.roles.filter(e => String(e).trim());
|
||||
// remove duplicate roles
|
||||
user.roles = uniq(user.roles);
|
||||
|
||||
// attribiutes
|
||||
user["attributes"] = {};
|
||||
for (var i = 0, l = user.attributesArray.length; i < l; i++) {
|
||||
var entry = user.attributesArray[i];
|
||||
if (entry && entry.key != "") {
|
||||
user.attributes[entry.key] = entry.value;
|
||||
}
|
||||
}
|
||||
delete user["attributesArray"];
|
||||
return user;
|
||||
};
|
||||
|
||||
this.postFetch = (user) => {
|
||||
user = backendAPI.cleanArraysFromDuplicates(user);
|
||||
delete user["hash"];
|
||||
user["password"] = "";
|
||||
user["passwordConfirmation"] = "";
|
||||
if (!user.roles) {
|
||||
user.roles = [];
|
||||
}
|
||||
// transform user attributes to object
|
||||
user["attributesArray"] = [];
|
||||
if (user.attributes) {
|
||||
var attributeNames = Object.keys(user.attributes).sort();
|
||||
attributeNames.forEach(function(attributeName){
|
||||
|
||||
user.attributesArray.push(
|
||||
{
|
||||
key: attributeName,
|
||||
value: user.attributes[attributeName]
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
});
|
322
public/apps/configuration/backend_api/roles.js
Normal file
322
public/apps/configuration/backend_api/roles.js
Normal file
@ -0,0 +1,322 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { isEmpty } from 'lodash';
|
||||
import client from './client';
|
||||
|
||||
/**
|
||||
* Role mappings API client service.
|
||||
*/
|
||||
uiModules.get('apps/opendistro_security/configuration', [])
|
||||
.service('backendRoles', function (backendAPI, Promise, $http) {
|
||||
|
||||
const RESOURCE = 'roles';
|
||||
|
||||
this.title = {
|
||||
singular: 'role',
|
||||
plural: 'roles'
|
||||
};
|
||||
|
||||
this.newLabel = "Role name";
|
||||
|
||||
this.list = () => {
|
||||
return backendAPI.list(RESOURCE);
|
||||
};
|
||||
|
||||
this.listSilent = () => {
|
||||
return backendAPI.listSilent(RESOURCE);
|
||||
};
|
||||
|
||||
this.listAutocomplete = (names) => {
|
||||
return backendAPI.listAutocomplete(names);
|
||||
};
|
||||
|
||||
this.get = (id) => {
|
||||
return backendAPI.get(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.save = (rolename, data) => {
|
||||
sessionStorage.removeItem("rolesautocomplete");
|
||||
sessionStorage.removeItem("rolenames");
|
||||
var resourceCopy = JSON.parse(JSON.stringify(data));
|
||||
var data = this.preSave(resourceCopy);
|
||||
return backendAPI.save(RESOURCE, rolename, data);
|
||||
};
|
||||
|
||||
this.delete = (id) => {
|
||||
sessionStorage.removeItem("rolesautocomplete");
|
||||
sessionStorage.removeItem("rolenames");
|
||||
return backendAPI.delete(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.emptyModel = () => {
|
||||
var role = {};
|
||||
role["cluster"] = [""];
|
||||
role["indices"] = {};
|
||||
role["tenants"] = {};
|
||||
return role;
|
||||
};
|
||||
|
||||
this.addEmptyIndex = (role, indexname, doctypename) => {
|
||||
if (!role.indices) {
|
||||
role["indices"] = {};
|
||||
}
|
||||
if (!role.indices[indexname]) {
|
||||
role.indices[indexname] = {};
|
||||
role.dlsfls[indexname] = {
|
||||
_dls_: "",
|
||||
_fls_: [],
|
||||
_masked_fields_: [],
|
||||
_flsmode_: "whitelist"
|
||||
};
|
||||
}
|
||||
role.indices[indexname][doctypename] = {
|
||||
"actiongroups": [""],
|
||||
"permissions": []
|
||||
};
|
||||
}
|
||||
|
||||
this.preSave = (role) => {
|
||||
delete role["indexnames"];
|
||||
// merge cluster permissions
|
||||
var clusterpermissions = backendAPI.mergeCleanArray(role.cluster.actiongroups, role.cluster.permissions);
|
||||
// delete tmp permissions
|
||||
delete role.cluster["actiongroups"];
|
||||
delete role.cluster["permissions"];
|
||||
role.cluster = clusterpermissions;
|
||||
|
||||
// same for each index and each doctype
|
||||
for (var indexname in role.indices) {
|
||||
var index = role.indices[indexname];
|
||||
|
||||
for (var doctypename in index) {
|
||||
var doctype = index[doctypename];
|
||||
var doctypepermissions = backendAPI.mergeCleanArray(doctype.actiongroups, doctype.permissions);
|
||||
delete doctype["actiongroups"];
|
||||
delete doctype["permissions"];
|
||||
index[doctypename] = doctypepermissions;
|
||||
}
|
||||
|
||||
// set field prefixes according to FLS mode
|
||||
this.setFlsModeToFields(role.dlsfls[indexname]);
|
||||
|
||||
// move back dls and fls
|
||||
var dlsfls = role.dlsfls[indexname];
|
||||
if(dlsfls) {
|
||||
if (dlsfls["_dls_"].length > 0) {
|
||||
// remove any formatting
|
||||
var dls = dlsfls["_dls_"];
|
||||
try {
|
||||
var dlsJsonObject = JSON.parse(dls);
|
||||
dls = JSON.stringify(dlsJsonObject);
|
||||
} catch (exception) {
|
||||
// no valid json, keep as is.
|
||||
}
|
||||
index["_dls_"] = dls.replace(/(\r\n|\n|\r|\t)/gm,"");;
|
||||
}
|
||||
if (dlsfls["_fls_"].length > 0) {
|
||||
index["_fls_"] = dlsfls["_fls_"];
|
||||
}
|
||||
if (dlsfls["_masked_fields_"].length > 0) {
|
||||
index["_masked_fields_"] = dlsfls["_masked_fields_"];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
delete role["dlsfls"];
|
||||
|
||||
// tenants
|
||||
role["tenants"] = {};
|
||||
for (var i = 0, l = role.tenantsArray.length; i < l; i++) {
|
||||
var tenant = role.tenantsArray[i];
|
||||
if (tenant && tenant.name != "") {
|
||||
role.tenants[tenant.name] = tenant.permissions;
|
||||
}
|
||||
}
|
||||
|
||||
delete role["tenantsArray"];
|
||||
return role;
|
||||
};
|
||||
|
||||
this.postFetch = (role) => {
|
||||
|
||||
role = backendAPI.cleanArraysFromDuplicates(role);
|
||||
|
||||
// separate action groups and single permissions on cluster level
|
||||
var clusterpermissions = backendAPI.sortPermissions(role.cluster);
|
||||
role["cluster"] = {};
|
||||
role.cluster["actiongroups"] = clusterpermissions.actiongroups;
|
||||
role.cluster["permissions"] = clusterpermissions.permissions;
|
||||
|
||||
// move dls and fls to separate section on top level
|
||||
// otherwise its on the same level as the document types
|
||||
// and it is hard to separate them in the views. We
|
||||
// should think about restructuring the config here, but
|
||||
// for the moment we're fiddling with the model directly
|
||||
role.dlsfls = {};
|
||||
|
||||
if (role.indices) {
|
||||
|
||||
// flat list of indexnames, can't be done in view
|
||||
role["indexnames"] = Object.keys(role.indices).sort();
|
||||
|
||||
for (var indexname in role.indices) {
|
||||
|
||||
var index = role.indices[indexname];
|
||||
|
||||
var dlsfls = {
|
||||
_dls_: "",
|
||||
_fls_: [],
|
||||
_masked_fields_: [],
|
||||
_flsmode_: "whitelist"
|
||||
};
|
||||
|
||||
if (index["_dls_"]) {
|
||||
dlsfls._dls_ = index["_dls_"];
|
||||
}
|
||||
if (index["_fls_"]) {
|
||||
dlsfls._fls_ = index["_fls_"];
|
||||
}
|
||||
if (index["_masked_fields_"]) {
|
||||
dlsfls._masked_fields_ = index["_masked_fields_"];
|
||||
}
|
||||
|
||||
delete role.indices[indexname]["_fls_"];
|
||||
delete role.indices[indexname]["_dls_"];
|
||||
delete role.indices[indexname]["_masked_fields_"];
|
||||
role.dlsfls[indexname] = dlsfls;
|
||||
|
||||
// determine the fls mode and strip any prefixes
|
||||
this.determineFlsMode(role.dlsfls[indexname]);
|
||||
|
||||
// sort permissions into actiongroups and single permissions
|
||||
for (var doctypename in index) {
|
||||
var doctype = index[doctypename];
|
||||
var doctypepermissions = backendAPI.sortPermissions(doctype);
|
||||
doctype = {
|
||||
actiongroups: doctypepermissions.actiongroups,
|
||||
permissions: doctypepermissions.permissions
|
||||
}
|
||||
index[doctypename] = doctype;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
role.indices = {};
|
||||
}
|
||||
|
||||
// transform tenants to object
|
||||
role["tenantsArray"] = [];
|
||||
if (role.tenants) {
|
||||
var tenantNames = Object.keys(role.tenants).sort();
|
||||
tenantNames.forEach(function(tenantName){
|
||||
|
||||
role.tenantsArray.push(
|
||||
{
|
||||
name: tenantName,
|
||||
permissions: role.tenants[tenantName]
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
delete role["tenants"];
|
||||
return role;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine the FLS mode (exclude/include) and
|
||||
* strip the prefixes from the fields for
|
||||
* display purposes. Rule here is that if one field
|
||||
* is excluded, i.e. prefixed with a tilde, we
|
||||
* assume exclude (blacklist) mode.
|
||||
* @param dlsfls
|
||||
*/
|
||||
this.determineFlsMode = function (dlsfls) {
|
||||
// default is whitelisting
|
||||
dlsfls["_flsmode_"] = "whitelist";
|
||||
// any fields to set?
|
||||
var flsFields = dlsfls["_fls_"];
|
||||
if (isEmpty(flsFields) || !Array.isArray(flsFields)) {
|
||||
return;
|
||||
}
|
||||
for (var index = 0; index < flsFields.length; ++index) {
|
||||
var field = flsFields[index];
|
||||
if (field.startsWith("~")) {
|
||||
// clean multiple tildes at the beginning, just in case
|
||||
flsFields[index] = field.replace(/^\~+/, '');
|
||||
dlsfls["_flsmode_"] = "blacklist";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all fields are either prefixed with
|
||||
* a tilde, or no field is prefixed with a tilde, based
|
||||
* on the exclude/include mode of FLS.
|
||||
* @param dlsfls
|
||||
*/
|
||||
this.setFlsModeToFields = function(dlsfls) {
|
||||
if (!dlsfls) {
|
||||
return;
|
||||
}
|
||||
// any fields to set?
|
||||
var flsFields = dlsfls["_fls_"];
|
||||
if (isEmpty(flsFields) || !Array.isArray(flsFields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var index = 0; index < flsFields.length; ++index) {
|
||||
var field = flsFields[index];
|
||||
// remove any tilde from beginning of string, in case
|
||||
// the user has added it in addition to setting mode to blacklist
|
||||
// We need just a single tilde here.
|
||||
field = field.replace(/^\~+/, '');
|
||||
if (!field.startsWith("~") && dlsfls["_flsmode_"] == "blacklist") {
|
||||
flsFields[index] = "~" + field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a role definition is empty. Empty
|
||||
* roles are not supported and cannot be saved. We need
|
||||
* at least some index or clusterpermissions
|
||||
* @param role
|
||||
*/
|
||||
this.isRoleEmpty = function (role) {
|
||||
// clean duplicates and remove empty arrays
|
||||
role.cluster.actiongroups = backendAPI.cleanArray(role.cluster.actiongroups);
|
||||
role.cluster.permissions = backendAPI.cleanArray(role.cluster.permissions);
|
||||
var clusterPermsEmpty = role.cluster.actiongroups.length == 0 && role.cluster.permissions.length == 0;
|
||||
var indicesEmpty = this.checkIndicesStatus(role).allEmpty;
|
||||
return clusterPermsEmpty && indicesEmpty;
|
||||
}
|
||||
|
||||
this.checkIndicesStatus = function (role) {
|
||||
|
||||
// index, we need at least one index with one document type with one permissions
|
||||
var indicesStatus = {
|
||||
allEmpty: true,
|
||||
faultyIndices: []
|
||||
};
|
||||
if (role.indices) {
|
||||
|
||||
var indexNames = Object.keys(role.indices);
|
||||
indexNames.forEach(function(indexName) {
|
||||
var docTypeNames = Object.keys(role.indices[indexName]);
|
||||
docTypeNames.forEach(function(docTypeName) {
|
||||
var doctype = role.indices[indexName][docTypeName];
|
||||
// doctype with at least one permission
|
||||
doctype.actiongroups = backendAPI.cleanArray(doctype.actiongroups);
|
||||
doctype.permissions = backendAPI.cleanArray(doctype.permissions);
|
||||
if ((doctype.actiongroups && doctype.actiongroups.length > 0) || (doctype.permissions && doctype.permissions.length > 0)) {
|
||||
indicesStatus.allEmpty = false;
|
||||
} else {
|
||||
// empty doctype
|
||||
indicesStatus.faultyIndices.push(indexName + " / " + docTypeName);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return indicesStatus;
|
||||
}
|
||||
|
||||
});
|
72
public/apps/configuration/backend_api/rolesmapping.js
Normal file
72
public/apps/configuration/backend_api/rolesmapping.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { merge } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import client from './client';
|
||||
|
||||
/**
|
||||
* Role mappings API client service.
|
||||
*/
|
||||
uiModules.get('apps/opendistro_security/configuration', [])
|
||||
.service('backendrolesmapping', function (backendAPI, Promise, $http) {
|
||||
|
||||
const RESOURCE = 'rolesmapping';
|
||||
|
||||
this.title = {
|
||||
singular: 'role mapping',
|
||||
plural: 'role mappings'
|
||||
};
|
||||
|
||||
this.newLabel = "Role";
|
||||
|
||||
this.list = () => {
|
||||
return backendAPI.list(RESOURCE);
|
||||
};
|
||||
|
||||
this.get = (id) => {
|
||||
return backendAPI.get(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.getSilent = (id) => {
|
||||
return backendAPI.getSilent(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.save = (actiongroupname, data) => {
|
||||
var data = this.preSave(data);
|
||||
return backendAPI.save(RESOURCE, actiongroupname, data);
|
||||
};
|
||||
|
||||
this.delete = (id) => {
|
||||
return backendAPI.delete(RESOURCE, id);
|
||||
};
|
||||
|
||||
this.emptyModel = () => {
|
||||
var rolemapping = {};
|
||||
rolemapping.users = [];
|
||||
rolemapping.hosts = [];
|
||||
rolemapping.backendroles = [];
|
||||
return rolemapping;
|
||||
};
|
||||
|
||||
this.preSave = (rolemapping) => {
|
||||
rolemapping.users = this.cleanArray(rolemapping.users);
|
||||
rolemapping.backendroles = this.cleanArray(rolemapping.backendroles);
|
||||
rolemapping.hosts = this.cleanArray(rolemapping.hosts);
|
||||
return rolemapping;
|
||||
};
|
||||
|
||||
this.postFetch = (rolemapping) => {
|
||||
rolemapping = backendAPI.cleanArraysFromDuplicates(rolemapping);
|
||||
return rolemapping;
|
||||
};
|
||||
|
||||
this.cleanArray = (thearray) => {
|
||||
if (thearray && Array.isArray(thearray)) {
|
||||
// remove empty entries
|
||||
thearray = thearray.filter(e => String(e).trim());
|
||||
// remove duplicate entries
|
||||
thearray = uniq(thearray);
|
||||
return thearray;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import client from './client';
|
||||
|
||||
/**
|
||||
* security configuration client service.
|
||||
*/
|
||||
uiModules.get('apps/opendistro_security/configuration', [])
|
||||
.service('securityConfiguration', function (backendAPI, Promise, $http) {
|
||||
|
||||
const RESOURCE = 'config';
|
||||
|
||||
this.title = {
|
||||
singular: 'Authentication / Authorization configuration',
|
||||
plural: 'Authentication / Authorization configuration'
|
||||
};
|
||||
|
||||
this.list = () => {
|
||||
return backendAPI.list(RESOURCE);
|
||||
};
|
||||
|
||||
|
||||
this.postFetch = (securityconfig) => {
|
||||
return securityconfig;
|
||||
};
|
||||
|
||||
});
|
419
public/apps/configuration/base_controller.js
Normal file
419
public/apps/configuration/base_controller.js
Normal file
@ -0,0 +1,419 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import chrome from 'ui/chrome';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { get } from 'lodash';
|
||||
import './directives/directives';
|
||||
import client from './backend_api/client';
|
||||
import { uniq } from 'lodash';
|
||||
import { orderBy } from 'lodash';
|
||||
|
||||
import clusterpermissions from './permissions/clusterpermissions';
|
||||
import indexpermissions from './permissions/indexpermissions';
|
||||
|
||||
import 'ui-select';
|
||||
import 'ui-select/dist/select.css';
|
||||
|
||||
require ('./backend_api/actiongroups');
|
||||
require ('./systemstate/systemstate');
|
||||
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', ['ui.ace', 'ui.select']);
|
||||
|
||||
app.controller('securityBaseController', function ($scope, $element, $route, $window, $http, backendAPI, backendActionGroups, backendRoles, kbnUrl, systemstate) {
|
||||
|
||||
var APP_ROOT = `${chrome.getBasePath()}`;
|
||||
var API_ROOT = `${APP_ROOT}/api/v1`;
|
||||
|
||||
// props of the child controller
|
||||
$scope.service = null;
|
||||
$scope.endpoint = null;
|
||||
|
||||
// loading state and loaded resources
|
||||
$scope.numresources = "0";
|
||||
$scope.loaded = false;
|
||||
|
||||
$scope.title = "Security Base Controller";
|
||||
$scope.errorMessage = "";
|
||||
$scope.query = "";
|
||||
$scope.resource = {};
|
||||
$scope.showEditor = false;
|
||||
$scope.toggleEditorLabel = "Show JSON";
|
||||
$scope.resourceAsJson = null;
|
||||
$scope.accessState = "pending";
|
||||
|
||||
$scope.actiongroupNames = [];
|
||||
$scope.roleNames = [];
|
||||
|
||||
// objects for autocomplete
|
||||
$scope.actiongroupsAutoComplete = {};
|
||||
$scope.rolesAutoComplete = {};
|
||||
$scope.clusterpermissionsAutoComplete = clusterpermissions;
|
||||
$scope.indexpermissionsAutoComplete = indexpermissions;
|
||||
$scope.allpermissionsAutoComplete = indexpermissions.concat(clusterpermissions);
|
||||
$scope.currentuser = "";
|
||||
|
||||
// modal delete dialogue
|
||||
$scope.displayModal = false;
|
||||
$scope.deleteModalResourceName = "";
|
||||
|
||||
// edit views modal delete dialogue
|
||||
$scope.deleteFromEditModal = {
|
||||
displayModal: false,
|
||||
params: {},
|
||||
header: 'Confirm Delete',
|
||||
body: '',
|
||||
onConfirm: null,
|
||||
onClose: null
|
||||
};
|
||||
|
||||
// img assets routes
|
||||
$scope.roleMappingsSvgURL = APP_ROOT + "/plugins/opendistro_security/assets/role_mappings.svg";
|
||||
$scope.rolesSvgURL = APP_ROOT + "/plugins/opendistro_security/assets/roles.svg";
|
||||
$scope.actionGroupsSvgURL = APP_ROOT + "/plugins/opendistro_security/assets/action_groups.svg";
|
||||
$scope.internalUserDatabaseSvgURL = APP_ROOT + "/plugins/opendistro_security/assets/internal_user_database.svg";
|
||||
$scope.authenticationSvgURL = APP_ROOT + "/plugins/opendistro_security/assets/authentication.svg";
|
||||
$scope.purgeCacheSvgURL = APP_ROOT + "/plugins/opendistro_security/assets/purge_cache.svg";
|
||||
|
||||
$scope.title = "Security Configuration";
|
||||
|
||||
$scope.initialiseStates = () => {
|
||||
$scope.complianceFeaturesEnabled = systemstate.complianceFeaturesEnabled();
|
||||
systemstate.loadRestInfo().then(function(){
|
||||
$scope.accessState = "ok";
|
||||
$scope.loadActionGroups();
|
||||
$scope.loadRoles();
|
||||
$scope.currentuser = systemstate.getRestApiInfo().user_name;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.loadActionGroups = () => {
|
||||
var cachedActionGroups = sessionStorage.getItem("actiongroupsautocomplete");
|
||||
var cachedActionGroupNames = sessionStorage.getItem("actiongroupnames");
|
||||
|
||||
if (cachedActionGroups) {
|
||||
$scope.actiongroupsAutoComplete = JSON.parse(cachedActionGroups);
|
||||
}
|
||||
|
||||
if (cachedActionGroupNames) {
|
||||
$scope.actiongroupNames = JSON.parse(cachedActionGroupNames);
|
||||
}
|
||||
|
||||
if (cachedActionGroupNames && cachedActionGroups) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(systemstate.endpointAndMethodEnabled("ACTIONGROUPS","GET")) {
|
||||
backendActionGroups.listSilent().then((response) => {
|
||||
$scope.actiongroupNames = Object.keys(response.data);
|
||||
sessionStorage.setItem("actiongroupnames", JSON.stringify($scope.actiongroupNames));
|
||||
$scope.actiongroupsAutoComplete = backendActionGroups.listAutocomplete($scope.actiongroupNames);
|
||||
sessionStorage.setItem("actiongroupsautocomplete", JSON.stringify($scope.actiongroupsAutoComplete));
|
||||
}, (error) => {
|
||||
toastNotifications.addDanger({
|
||||
title: 'Unable to load action groups',
|
||||
text: error.message,
|
||||
});
|
||||
$scope.accessState = "forbidden";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$scope.loadRoles = () => {
|
||||
var cachedRoles = sessionStorage.getItem("rolesautocomplete");
|
||||
var cachedRoleNames = sessionStorage.getItem("rolenames");
|
||||
|
||||
if (cachedRoles) {
|
||||
$scope.rolesAutoComplete = JSON.parse(cachedRoles);
|
||||
}
|
||||
|
||||
if (cachedRoleNames) {
|
||||
$scope.roleNames = JSON.parse(cachedRoleNames);
|
||||
}
|
||||
|
||||
if (cachedRoles && cachedRoleNames) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(systemstate.endpointAndMethodEnabled("ROLES","GET")) {
|
||||
backendRoles.listSilent().then((response) => {
|
||||
$scope.rolesAutoComplete = backendRoles.listAutocomplete(Object.keys(response.data));
|
||||
$scope.roleNames = Object.keys(response.data);
|
||||
sessionStorage.setItem("rolesautocomplete", JSON.stringify($scope.rolesAutoComplete));
|
||||
sessionStorage.setItem("rolenames", JSON.stringify(Object.keys(response.data)));
|
||||
}, (error) => {
|
||||
toastNotifications.addDanger({
|
||||
text: error.message,
|
||||
});
|
||||
$scope.accessState = "forbidden";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$scope.clearCache = function() {
|
||||
backendAPI.clearCache();
|
||||
}
|
||||
|
||||
$scope.getDocTypeAutocomplete = () => {
|
||||
$scope.indexAutoComplete = backendAPI.indexAutocomplete();
|
||||
}
|
||||
|
||||
$scope.endpointAndMethodEnabled = (endpoint, method) => {
|
||||
return systemstate.endpointAndMethodEnabled(endpoint, method);
|
||||
}
|
||||
|
||||
// +++ START common functions for all controllers +++
|
||||
|
||||
// --- Start navigation
|
||||
$scope.edit = function(resourcename) {
|
||||
kbnUrl.change('/' +$scope.endpoint.toLowerCase() + '/edit/' + resourcename );
|
||||
}
|
||||
|
||||
$scope.new = function(query) {
|
||||
kbnUrl.change('/' +$scope.endpoint.toLowerCase() + '/new?name='+query);
|
||||
}
|
||||
|
||||
$scope.clone = function(resourcename) {
|
||||
kbnUrl.change('/' +$scope.endpoint.toLowerCase() + '/clone/' + resourcename);
|
||||
}
|
||||
|
||||
$scope.cancel = function () {
|
||||
kbnUrl.change('/' +$scope.endpoint.toLowerCase() );
|
||||
}
|
||||
// --- End navigation
|
||||
|
||||
$scope.delete = function() {
|
||||
$scope.displayModal = false;
|
||||
var name = $scope.deleteModalResourceName;
|
||||
$scope.deleteModalResourceName = "";
|
||||
$scope.service.delete(name)
|
||||
.then(() => $scope.cancel());
|
||||
}
|
||||
|
||||
$scope.confirmDelete = function(resourcename) {
|
||||
$scope.deleteModalResourceName = resourcename;
|
||||
$scope.displayModal = true;
|
||||
}
|
||||
|
||||
$scope.closeDeleteModal = () => {
|
||||
$scope.deleteModalResourceName = "";
|
||||
$scope.displayModal = false;
|
||||
};
|
||||
|
||||
|
||||
$scope.aceLoaded = (editor) => {
|
||||
editor.session.setOptions({
|
||||
tabSize: 2,
|
||||
useSoftTabs: false
|
||||
});
|
||||
editor.$blockScrolling = Infinity;
|
||||
editor.setShowPrintMargin(false);
|
||||
};
|
||||
|
||||
$scope.aceRwLoaded = (editor) => {
|
||||
editor.session.setOptions({
|
||||
tabSize: 2,
|
||||
useSoftTabs: false
|
||||
});
|
||||
editor.$blockScrolling = Infinity;
|
||||
editor.setShowPrintMargin(false);
|
||||
};
|
||||
|
||||
$scope.toggleEditor = (resource) => {
|
||||
if ($scope.resourceAsJson == null) {
|
||||
$scope.loadJSON(resource)
|
||||
}
|
||||
$scope.showEditor = !$scope.showEditor;
|
||||
$scope.toggleEditorLabel = $scope.showEditor? "Hide JSON" : "Show JSON";
|
||||
};
|
||||
|
||||
$scope.loadJSON = function(resource) {
|
||||
// copy resource, we don't want to modify current edit session
|
||||
var resourceCopy = JSON.parse(JSON.stringify(resource));
|
||||
$scope.resourceAsJson = JSON.stringify($scope.service.preSave(resourceCopy), null, 2);
|
||||
}
|
||||
|
||||
$scope.checkActionGroupExists = function (array, index, item) {
|
||||
if($scope.actiongroupNames.indexOf(item) == -1) {
|
||||
array[index] = "";
|
||||
}
|
||||
}
|
||||
|
||||
$scope.addArrayEntry = function (resource, fieldname, value) {
|
||||
if(!resource[fieldname] || !Array.isArray(resource[fieldname])) {
|
||||
resource[fieldname] = [];
|
||||
}
|
||||
resource[fieldname].push(value);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an array entry after user confirmation, or when the user removes an empty entry
|
||||
* @param {array} array
|
||||
* @param {object} item
|
||||
*/
|
||||
let removeArrayEntry = function (array, item) {
|
||||
var index = array.indexOf(item);
|
||||
if (index > -1) {
|
||||
array.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Ask for confirmation before deleting an entry
|
||||
* @param {array} array
|
||||
* @param {string} item
|
||||
*/
|
||||
$scope.confirmRemoveArrayEntry = function (array, item) {
|
||||
if(!Array.isArray(array) || array.indexOf(item) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item && item.length > 0) {
|
||||
$scope.deleteFromEditModal = {
|
||||
displayModal: true,
|
||||
header: 'Confirm Delete',
|
||||
body: `Are you sure you want to delete '${item}'?`,
|
||||
onConfirm: function() {
|
||||
removeArrayEntry(array, item);
|
||||
$scope.closeDeleteFromEditModal()
|
||||
},
|
||||
onClose: $scope.closeDeleteFromEditModal
|
||||
};
|
||||
} else {
|
||||
removeArrayEntry(array, item);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the confirmation dialog
|
||||
* @param {string} reason
|
||||
*/
|
||||
$scope.closeDeleteFromEditModal = function () {
|
||||
$scope.deleteFromEditModal = {
|
||||
displayModal: false,
|
||||
params: null
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Remove when we use the new confirmation dialog everywhere
|
||||
* @deprecated
|
||||
* @param array
|
||||
* @param item
|
||||
*/
|
||||
|
||||
$scope.removeArrayEntry = function (array, item) {
|
||||
if(!Array.isArray(array)) {
|
||||
return;
|
||||
}
|
||||
if (item && item.length > 0) {
|
||||
if (!confirm(`Are you sure you want to delete '${item}'?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
var index = array.indexOf(item);
|
||||
array.splice(index, 1);
|
||||
}
|
||||
|
||||
$scope.lastArrayEntryEmpty = function (array) {
|
||||
|
||||
if (!array || typeof array == 'undefined' || array.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var entry = array[array.length - 1];
|
||||
|
||||
if (typeof entry === 'undefined' || entry.length == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$scope.removeObjectKey = function (theobject, key) {
|
||||
if (theobject[key]) {
|
||||
if (confirm(`Are you sure you want to delete '${key}'?`)) {
|
||||
delete theobject[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.addObjectKey = function (theobject, key, value) {
|
||||
theobject[key] = value;
|
||||
}
|
||||
|
||||
$scope.sortObjectArray = function (objectArray, sortProperty) {
|
||||
//return orderBy(objectArray, [sortProperty], ["asc"]);
|
||||
return objectArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for confirmation before deleting an entry
|
||||
* @param {array} thearray
|
||||
* @param {int} index
|
||||
* @param {string} value
|
||||
*/
|
||||
$scope.confirmRemoveFromObjectArray = function(thearray, index, value) {
|
||||
// We're not checking the value here, so we may need to adjust the body
|
||||
let body = (value === '') ? 'Are you sure you want to delete this?' : `Are you sure you want to delete '${value}'?`;
|
||||
$scope.deleteFromEditModal = {
|
||||
displayModal: true,
|
||||
header: 'Confirm Delete',
|
||||
body: body,
|
||||
onConfirm: function() {
|
||||
thearray.splice(index, 1);
|
||||
$scope.closeDeleteFromEditModal()
|
||||
},
|
||||
onClose: $scope.closeDeleteFromEditModal
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove when we've tested all dialogs
|
||||
* @deprecated
|
||||
* @param thearray
|
||||
* @param index
|
||||
* @param value
|
||||
*/
|
||||
$scope.removeFromObjectArray = function (thearray, index, value) {
|
||||
if (confirm(`Are you sure you want to delete '${value}'?`)) {
|
||||
thearray.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.addToObjectArray = function (thearray, value) {
|
||||
return thearray.push(value);
|
||||
}
|
||||
|
||||
// helper function to use Object.keys in templates
|
||||
$scope.keys = function (object) {
|
||||
if (object) {
|
||||
return Object.keys(object).sort();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.flatten = function (list, textAttribute) {
|
||||
return uniq(list.reduce((result, item) => {
|
||||
const text = item[textAttribute];
|
||||
if (text) {
|
||||
result.push(text);
|
||||
}
|
||||
return result;
|
||||
}, [])).sort();
|
||||
}
|
||||
|
||||
|
||||
// --- init ---
|
||||
$scope.initialiseStates();
|
||||
|
||||
});
|
||||
|
||||
app.filter('escape', function() {
|
||||
return window.encodeURIComponent;
|
||||
});
|
||||
|
||||
app.filter('unsafe', function($sce) {
|
||||
return $sce.trustAsHtml;
|
||||
});
|
131
public/apps/configuration/configuration.html
Normal file
131
public/apps/configuration/configuration.html
Normal file
@ -0,0 +1,131 @@
|
||||
<div ng-controller="securityBaseController">
|
||||
|
||||
<div class="security app-container" ng-controller="securityConfigurationController">
|
||||
|
||||
<securityc-header></securityc-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12" style="text-align: center;">
|
||||
|
||||
<h3 ng-if="accessState=='pending'" style="margin-top:0px;">Please wait ...</h3>
|
||||
|
||||
<p id="opendistro_security.label.errormessage" class="error-message" ng-show="errorMessage">{{ errorMessage }}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row" ng-if="accessState=='forbidden'">
|
||||
<div class="col-xs-12" style="text-align: center;">
|
||||
<h3>You do not have permission to access the Security configuration. Please contact your System
|
||||
Administrator.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="accessState=='notenabled'">
|
||||
<div class="col-xs-12" style="text-align: center;">
|
||||
<h3>The REST API module is not installed. Please contact your System Administrator.</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="accessState=='ok'">
|
||||
|
||||
<div class="kuiViewContent kuiViewContent--constrainedWidth">
|
||||
|
||||
<div class="kuiViewContentItem">
|
||||
|
||||
<div class="kuiVerticalRhythm kuiVerticalRhythm--medium ">
|
||||
|
||||
<h2 class="kuiSubTitle" style="margin-bottom:10px;">Permissions and Roles</h2>
|
||||
|
||||
<div class="kuiGallery">
|
||||
|
||||
<a id="opendistro_security.link.rolesmapping" ng-if="endpointAndMethodEnabled('ROLESMAPPING', 'GET')" class="kuiGalleryItem ng-scope"
|
||||
ng-href="#/rolesmapping"
|
||||
tooltip="Map users, backend roles and hostnames to roles."
|
||||
tooltip-placement="bottom" href="#/rolesmapping">
|
||||
<div class="kuiGalleryButton__image">
|
||||
<img src="{{roleMappingsSvgURL}}" width="56" />
|
||||
</div>
|
||||
<div class="kuiGalleryButton__label">
|
||||
Role Mappings
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a id="opendistro_security.link.roles" ng-if="endpointAndMethodEnabled('ROLES', 'GET')" class="kuiGalleryItem ng-scope"
|
||||
ng-href="#/roles" tooltip="Configure Roles and their permissions."
|
||||
tooltip-placement="bottom" href="#/roles">
|
||||
<div class="kuiGalleryButton__image">
|
||||
<img src="{{rolesSvgURL}}" width="56" />
|
||||
</div>
|
||||
<div class="kuiGalleryButton__label">
|
||||
Roles
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a id="opendistro_security.link.actiongroups" ng-if="endpointAndMethodEnabled('ACTIONGROUPS', 'GET')" class="kuiGalleryItem ng-scope"
|
||||
ng-href="#/actiongroups"
|
||||
tooltip="Configure named groups of permissions that can be applied to roles."
|
||||
tooltip-placement="bottom" href="#/actiongroups">
|
||||
<div class="kuiGalleryButton__image">
|
||||
<img src="{{actionGroupsSvgURL}}" width="56" />
|
||||
</div>
|
||||
<div class="kuiGalleryButton__label">
|
||||
Action Groups
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiVerticalRhythm kuiVerticalRhythm--medium ">
|
||||
<h2 class="kuiSubTitle" style="margin-bottom:10px;">Authentication Backends</h2>
|
||||
|
||||
<div class="kuiGallery">
|
||||
|
||||
<a id="opendistro_security.link.internalusers" ng-if="endpointAndMethodEnabled('INTERNALUSERS', 'GET')" class="kuiGalleryItem"
|
||||
ng-href="#/internalusers"
|
||||
tooltip="Use the Internal Users Database if you don't have any external authentication systems in place."
|
||||
tooltip-placement="bottom" href="#/internalusers">
|
||||
<div class="kuiGalleryButton__image">
|
||||
<img src="{{internalUserDatabaseSvgURL}}" width="56" />
|
||||
</div>
|
||||
<div class="kuiGalleryButton__label">
|
||||
Internal User Database
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="kuiVerticalRhythm kuiVerticalRhythm--medium ">
|
||||
<h2 class="kuiSubTitle" style="margin-bottom:10px;">System</h2>
|
||||
|
||||
<div class="kuiGallery">
|
||||
<a id="opendistro_security.link.securityconfig" ng-if="endpointAndMethodEnabled('SECURITYCONFIG', 'GET')" class="kuiGalleryItem ng-scope"
|
||||
ng-href="#/securityconfiguration"
|
||||
tooltip="View the configured authentication and authorization modules."
|
||||
href="#/securityconfiguration">
|
||||
<div class="kuiGalleryButton__image">
|
||||
<img src="{{authenticationSvgURL}}" width="56" />
|
||||
</div>
|
||||
<div class="kuiGalleryButton__label">
|
||||
Authentication & Authorization
|
||||
</div>
|
||||
</a>
|
||||
<a id="opendistro_security.link.cache" ng-if="endpointAndMethodEnabled('CACHE', 'DELETE')" class="kuiGalleryItem ng-scope"
|
||||
ng-click="clearCache()" tooltip="Purge all Security caches"
|
||||
tooltip-placement="bottom">
|
||||
<div class="kuiGalleryButton__image">
|
||||
<img src="{{purgeCacheSvgURL}}" width="56" />
|
||||
</div>
|
||||
<div class="kuiGalleryButton__label">
|
||||
Purge Cache
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
58
public/apps/configuration/configuration.js
Normal file
58
public/apps/configuration/configuration.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2015-2018 _floragunn_ GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
/*
|
||||
* Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this file except in compliance with the License.
|
||||
* A copy of the License is located at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* or in the "license" file accompanying this file. This file is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
* express or implied. See the License for the specific language governing
|
||||
* permissions and limitations under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import uiRoutes from 'ui/routes';
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
import internalusers from './sections/internalusers';
|
||||
import actiongroups from './sections/actiongroups';
|
||||
import rolesmapping from './sections/rolesmapping';
|
||||
import roles from './sections/roles';
|
||||
import securityconfiguration from './sections/securityconfiguration';
|
||||
|
||||
import 'ui/autoload/styles';
|
||||
|
||||
import 'plugins/opendistro_security/apps/configuration/configuration.less';
|
||||
|
||||
import 'ace';
|
||||
|
||||
import securityConfigurationController from './configuration_controller';
|
||||
import template from './configuration.html';
|
||||
|
||||
require('ui/tooltip');
|
||||
|
||||
uiRoutes.enable();
|
||||
|
||||
uiRoutes
|
||||
.when('/', {
|
||||
template: template
|
||||
});
|
289
public/apps/configuration/configuration.less
Normal file
289
public/apps/configuration/configuration.less
Normal file
@ -0,0 +1,289 @@
|
||||
@import "~ui/styles/variables/colors.less";
|
||||
@import "~ui/styles/variables/bootstrap-mods.less";
|
||||
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.help {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.error-message a {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-right: auto;
|
||||
margin-left: 0px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
width: 970px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
width: 1170px;
|
||||
}
|
||||
}
|
||||
.container-fluid {
|
||||
margin-right: auto;
|
||||
margin-left: 0px;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ng-submitted input.ng-invalid {
|
||||
border-color: #A30000 !important;
|
||||
}
|
||||
|
||||
.kuiTextInput:invalid {
|
||||
border-color: #DEDEDE;
|
||||
}
|
||||
|
||||
.kuiBadge {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.topBottomMargin {
|
||||
margin-bottom:20px;
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.editResourceForm {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.brand-image {
|
||||
max-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.indexheading {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.resourcename {
|
||||
margin-bottom: 10px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.noresourcename {
|
||||
font-style: italic;
|
||||
color:#aaa;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.labelhint {
|
||||
font-style: italic;
|
||||
color:#aaa;
|
||||
font-weight: normal;
|
||||
}
|
||||
.marginbottom--small {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tab-inactive {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.headerHeadline {
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.kuiViewContent--constrainedWidth {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin-left: 0px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.kuiPanel--noborder {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.kuiEmptyTablePrompt--nopadding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.kuiGalleryButton {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.kuiGalleryButton__label {
|
||||
font-size: 14px;
|
||||
color: #191E23;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
white-space: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.kuiTableRowCell {
|
||||
padding: 7px 8px 8px;
|
||||
}
|
||||
|
||||
.tableHeaderCellIndexGroups {
|
||||
width:70%;
|
||||
color:#000;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.tableHeaderBold {
|
||||
color:#000;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.tableIndexGroups {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cellAlignTop {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.twowidth {
|
||||
width:20%;
|
||||
}
|
||||
|
||||
|
||||
.fourwidth {
|
||||
width:40%;
|
||||
}
|
||||
|
||||
.fivewidth {
|
||||
width:50%;
|
||||
}
|
||||
.sevenwidth {
|
||||
width:70%;
|
||||
}
|
||||
|
||||
.fullwidth {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.formsubmit {
|
||||
margin-bottom: 30px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.kuiLocalNav {
|
||||
min-height: 60px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.kuiLocalNavMain {
|
||||
min-height: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #73b626;
|
||||
border-bottom-color: #73b626;
|
||||
border-left-color: #73b626;
|
||||
border-right-color: #73b626;
|
||||
border-top-color: #73b626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
background-color: #5e9b24;
|
||||
border-bottom-color: #5e9b24;
|
||||
border-left-color: #5e9b24;
|
||||
border-right-color: #5e9b24;
|
||||
border-top-color: #5e9b24;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
outline: 0;
|
||||
border-color: #ECECEC;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
padding: 6px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ace_editor {
|
||||
min-height: 300px;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.authc-disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular UI-Select
|
||||
*/
|
||||
// Since we have Bootstrap classes through the component,
|
||||
// we need to remove some styles
|
||||
.ui-select-container {
|
||||
.ui-select-toggle.form-control {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
// Helps mimic the other fields that we're using
|
||||
.ui-select-search,
|
||||
.ui-select-toggle {
|
||||
padding-left: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// Neutralize default styles
|
||||
.ui-select-match .btn:hover {
|
||||
background-color: #fff;
|
||||
color: #2D2D2D;
|
||||
border: 1px solid #D9D9D9;
|
||||
}
|
18
public/apps/configuration/configuration_controller.js
Normal file
18
public/apps/configuration/configuration_controller.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { get } from 'lodash';
|
||||
import client from './backend_api/client';
|
||||
import './directives/directives';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', ['ui.ace']);
|
||||
|
||||
app.controller('securityConfigurationController', function ($scope, $element, $route, $window, $http, createNotifier, backendAPI) {
|
||||
|
||||
$scope.errorMessage = "";
|
||||
|
||||
$scope.title = "Security";
|
||||
|
||||
$scope.clearCache = function() {
|
||||
backendAPI.clearCache();
|
||||
}
|
||||
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<div class="kuiTogglePanelHeader">
|
||||
<accordeon-header additional-class="{{accordeon.additionalClass}}" title="{{ accordeon.title }}" is-collapsed="accordeon.isCollapsed" on-click="accordeon.toggle">
|
||||
</accordeon-header>
|
||||
</div>
|
||||
<div
|
||||
ng-hide="accordeon.isCollapsed"
|
||||
ng-transclude
|
||||
></div>
|
||||
</div>
|
28
public/apps/configuration/directives/accordeon/accordeon.js
Normal file
28
public/apps/configuration/directives/accordeon/accordeon.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './accordeon.html';
|
||||
import './accordeon_header';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('accordeon', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
template: template,
|
||||
scope: {
|
||||
accordeonId: '@',
|
||||
title: '@',
|
||||
isCollapsed: '=',
|
||||
additionalClass: '@'
|
||||
},
|
||||
controllerAs: 'accordeon',
|
||||
bindToController: true,
|
||||
controller: class AccordeonController {
|
||||
toggle = () => {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
//this.onToggle(this.togglePanelId);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
<button type="button" class="kuiToggleButton" ng-click="accordeonHeader.onClick()">
|
||||
<h5>
|
||||
<span style="margin-right:10px;" ng-class="{'kuiIcon fa-caret-down': !accordeonHeader.isCollapsed, 'kuiIcon fa-caret-right': accordeonHeader.isCollapsed}" class="fa {{accordeonHeader.additionalClass}}"></span>
|
||||
<span class="{{accordeonHeader.additionalClass}}">{{ accordeonHeader.title }}</span>
|
||||
</h5>
|
||||
</button>
|
@ -0,0 +1,22 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
import template from './accordeon_header.html';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('accordeonHeader', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
template: template,
|
||||
scope: {
|
||||
title: '@',
|
||||
isCollapsed: '=',
|
||||
onClick: '=',
|
||||
additionalClass: '@'
|
||||
},
|
||||
controllerAs: 'accordeonHeader',
|
||||
bindToController: true,
|
||||
controller: class ToggleButtonController {
|
||||
}
|
||||
};
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
<div class="angucomplete-holder">
|
||||
<input id="{{id}}_value" ng-required="{{isrequired}}" ng-model="selectedObject" type="text" placeholder="{{placeholder}}" class="{{inputClass}}" pattern="{{pattern}}" onmouseup="this.select();" ng-focus="resetHideResults()" ng-blur="hideResults()" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
||||
<div id="{{id}}_dropdown" class="angucomplete-dropdown" ng-if="showDropdown">
|
||||
<div class="angucomplete-searching" ng-show="searching">Searching...</div>
|
||||
<div class="angucomplete-searching" ng-show="!searching && (!results || results.length == 0)">No results found
|
||||
</div>
|
||||
<div class="angucomplete-row" ng-repeat="result in results" ng-mousedown="selectResult(result)" ng-mouseover="hoverRow()" ng-class="{'angucomplete-selected-row': $index == currentIndex}">
|
||||
<div ng-if="imageField" class="angucomplete-image-holder">
|
||||
<img ng-if="result.image && result.image != ''" ng-src="{{result.image}}" class="angucomplete-image"/>
|
||||
<div ng-if="!result.image && result.image != ''" class="angucomplete-image-default"></div>
|
||||
</div>
|
||||
<div class="angucomplete-title" ng-if="matchClass" ng-bind-html="result.title"></div>
|
||||
<div class="angucomplete-title" ng-if="!matchClass">{{ result.title }}</div>
|
||||
<div ng-if="result.description && result.description != ''" class="angucomplete-description">
|
||||
{{result.description}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Angucomplete
|
||||
* Autocomplete directive for AngularJS
|
||||
* By Daryl Rowland
|
||||
*/
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('angucomplete', function ($parse, $http, $sce, $timeout) {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
scope: {
|
||||
"id": "@id",
|
||||
"placeholder": "@placeholder",
|
||||
"selectedObject": "=selectedobject",
|
||||
"modelArray": "=modelarray",
|
||||
"url": "@url",
|
||||
"dataField": "@datafield",
|
||||
"titleField": "@titlefield",
|
||||
"descriptionField": "@descriptionfield",
|
||||
"imageField": "@imagefield",
|
||||
"imageUri": "@imageuri",
|
||||
"inputClass": "@inputclass",
|
||||
"userPause": "@pause",
|
||||
"localData": "=localdata",
|
||||
"searchFields": "@searchfields",
|
||||
"minLengthUser": "@minlength",
|
||||
"matchClass": "@matchclass",
|
||||
"isrequired": "@isrequired"
|
||||
},
|
||||
template: require('./angucomplete.html'),
|
||||
|
||||
link: function($scope, elem, attrs) {
|
||||
$scope.lastSearchTerm = null;
|
||||
$scope.currentIndex = null;
|
||||
$scope.justChanged = false;
|
||||
$scope.searchTimer = null;
|
||||
$scope.hideTimer = null;
|
||||
$scope.searching = false;
|
||||
$scope.pause = 500;
|
||||
$scope.minLength = 3;
|
||||
$scope.searchStr = $scope.selectedObject;
|
||||
|
||||
if ($scope.minLengthUser && $scope.minLengthUser != "") {
|
||||
$scope.minLength = $scope.minLengthUser;
|
||||
}
|
||||
|
||||
if ($scope.userPause) {
|
||||
$scope.pause = $scope.userPause;
|
||||
}
|
||||
|
||||
$scope.remove = function () {
|
||||
if(!Array.isArray($scope.modelArray)) {
|
||||
return;
|
||||
}
|
||||
if (confirm(`Are you sure you want to delete '${$scope.selectedObject}'?`)) {
|
||||
var index = $scope.modelArray.indexOf($scope.selectedObject);
|
||||
$scope.modelArray.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.isNewSearchNeeded = function(newTerm, oldTerm) {
|
||||
return newTerm.length >= $scope.minLength && newTerm != oldTerm
|
||||
}
|
||||
|
||||
$scope.processResults = function(responseData, str) {
|
||||
if (responseData && responseData.length > 0) {
|
||||
$scope.results = [];
|
||||
|
||||
var titleFields = [];
|
||||
if ($scope.titleField && $scope.titleField != "") {
|
||||
titleFields = $scope.titleField.split(",");
|
||||
}
|
||||
|
||||
for (var i = 0; i < responseData.length; i++) {
|
||||
// Get title variables
|
||||
var titleCode = [];
|
||||
|
||||
for (var t = 0; t < titleFields.length; t++) {
|
||||
titleCode.push(responseData[i][titleFields[t]]);
|
||||
}
|
||||
|
||||
var description = "";
|
||||
if ($scope.descriptionField) {
|
||||
description = responseData[i][$scope.descriptionField];
|
||||
}
|
||||
|
||||
var imageUri = "";
|
||||
if ($scope.imageUri) {
|
||||
imageUri = $scope.imageUri;
|
||||
}
|
||||
|
||||
var image = "";
|
||||
if ($scope.imageField) {
|
||||
image = imageUri + responseData[i][$scope.imageField];
|
||||
}
|
||||
|
||||
var text = titleCode.join(' ');
|
||||
if ($scope.matchClass) {
|
||||
var re = new RegExp(str, 'i');
|
||||
var strPart = text.match(re)[0];
|
||||
text = $sce.trustAsHtml(text.replace(re, '<span class="'+ $scope.matchClass +'">'+ strPart +'</span>'));
|
||||
}
|
||||
|
||||
var resultRow = {
|
||||
title: text,
|
||||
description: description,
|
||||
image: image,
|
||||
originalObject: responseData[i]
|
||||
}
|
||||
|
||||
$scope.results[$scope.results.length] = resultRow;
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
$scope.results = [];
|
||||
}
|
||||
}
|
||||
|
||||
$scope.searchTimerComplete = function(str) {
|
||||
// Begin the search
|
||||
|
||||
if (str.length >= $scope.minLength) {
|
||||
if ($scope.localData) {
|
||||
var searchFields = $scope.searchFields.split(",");
|
||||
|
||||
var matches = [];
|
||||
|
||||
for (var i = 0; i < $scope.localData.length; i++) {
|
||||
var match = false;
|
||||
|
||||
for (var s = 0; s < searchFields.length; s++) {
|
||||
match = match || (typeof $scope.localData[i][searchFields[s]] === 'string' && typeof str === 'string' && $scope.localData[i][searchFields[s]].toLowerCase().indexOf(str.toLowerCase()) >= 0);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
matches[matches.length] = $scope.localData[i];
|
||||
}
|
||||
}
|
||||
|
||||
$scope.searching = false;
|
||||
$scope.processResults(matches, str);
|
||||
|
||||
} else {
|
||||
$http.get($scope.url + str, {}).
|
||||
success(function(responseData, status, headers, config) {
|
||||
$scope.searching = false;
|
||||
$scope.processResults((($scope.dataField) ? responseData[$scope.dataField] : responseData ), str);
|
||||
}).
|
||||
error(function(data, status, headers, config) {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.hideResults = function() {
|
||||
$scope.hideTimer = $timeout(function() {
|
||||
$scope.showDropdown = false;
|
||||
}, $scope.pause);
|
||||
};
|
||||
|
||||
$scope.resetHideResults = function() {
|
||||
if($scope.hideTimer) {
|
||||
$timeout.cancel($scope.hideTimer);
|
||||
};
|
||||
};
|
||||
|
||||
$scope.hoverRow = function(index) {
|
||||
$scope.currentIndex = index;
|
||||
}
|
||||
|
||||
$scope.keyPressed = function(event) {
|
||||
if (!(event.which == 38 || event.which == 40 || event.which == 13)) {
|
||||
if (!$scope.selectedObject || $scope.selectedObject == "") {
|
||||
$scope.showDropdown = false;
|
||||
$scope.lastSearchTerm = null
|
||||
} else if ($scope.isNewSearchNeeded($scope.selectedObject, $scope.lastSearchTerm)) {
|
||||
$scope.lastSearchTerm = $scope.selectedObject
|
||||
$scope.showDropdown = true;
|
||||
$scope.currentIndex = -1;
|
||||
$scope.results = [];
|
||||
|
||||
if ($scope.searchTimer) {
|
||||
$timeout.cancel($scope.searchTimer);
|
||||
}
|
||||
|
||||
$scope.searching = true;
|
||||
|
||||
$scope.searchTimer = $timeout(function() {
|
||||
$scope.searchTimerComplete($scope.selectedObject);
|
||||
}, $scope.pause);
|
||||
}
|
||||
} else {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.selectResult = function(result) {
|
||||
if ($scope.matchClass) {
|
||||
result.title = result.title.toString().replace(/(<([^>]+)>)/ig, '');
|
||||
}
|
||||
$scope.selectedObject = $scope.lastSearchTerm = result.title;
|
||||
$scope.selectedObject = result.originalObject[$scope.dataField];
|
||||
$scope.showDropdown = false;
|
||||
$scope.results = [];
|
||||
//$scope.$apply();
|
||||
}
|
||||
|
||||
var inputField = elem.find('input');
|
||||
|
||||
inputField.on('keyup', $scope.keyPressed);
|
||||
|
||||
elem.on("keyup", function (event) {
|
||||
if(event.which === 40) {
|
||||
if ($scope.results && ($scope.currentIndex + 1) < $scope.results.length) {
|
||||
$scope.currentIndex ++;
|
||||
$scope.$apply();
|
||||
event.preventDefault;
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
$scope.$apply();
|
||||
} else if(event.which == 38) {
|
||||
if ($scope.currentIndex >= 1) {
|
||||
$scope.currentIndex --;
|
||||
$scope.$apply();
|
||||
event.preventDefault;
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
} else if (event.which == 13) {
|
||||
if ($scope.results && $scope.currentIndex >= 0 && $scope.currentIndex < $scope.results.length) {
|
||||
$scope.selectResult($scope.results[$scope.currentIndex]);
|
||||
$scope.$apply();
|
||||
event.preventDefault;
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
$scope.results = [];
|
||||
$scope.$apply();
|
||||
event.preventDefault;
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
} else if (event.which == 27) {
|
||||
$scope.results = [];
|
||||
$scope.showDropdown = false;
|
||||
$scope.$apply();
|
||||
} else if (event.which == 8) {
|
||||
$scope.selectedObject = "";
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -0,0 +1,19 @@
|
||||
<div class="kuiModalOverlay js-confirmOverlay" ng-click="vm.closeOnOverlay($event)">
|
||||
<div class="kuiModal" style="width: 450px;">
|
||||
<div class="kuiModalHeader">
|
||||
<div class="kuiModalHeader__title">
|
||||
{{::vm.header}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="kuiModalBody">
|
||||
<div class="kuiModalBodyText">
|
||||
{{::vm.body}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="kuiModalFooter">
|
||||
<button type="button" ng-click="vm.closeModal('cancel')" class="kuiButton {{::vm.config.buttons.cancel.classes}}"><span
|
||||
class="kuiButton__inner"><span>Cancel</span></span></button>
|
||||
<button type="button" ng-click="vm.confirm()" class="kuiButton {{::vm.config.buttons.confirm.classes}}" id="opendistro_security.button.deletemodal.confirm"><span>Confirm</span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,133 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
//import { merge } from 'lodash';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycConfirmationModal', function () {
|
||||
return {
|
||||
template: require('./confirmationmodal.html'),
|
||||
restrict: 'E',
|
||||
scope: {},
|
||||
controllerAs: 'vm',
|
||||
bindToController: {
|
||||
/**
|
||||
* Dialog title
|
||||
* @param {string}
|
||||
*/
|
||||
header: '@',
|
||||
/**
|
||||
* Dialog body text
|
||||
* @param {string}
|
||||
*/
|
||||
body: '@',
|
||||
/**
|
||||
* Optional params object that would be passed
|
||||
* to the on-confirm function
|
||||
* @param {object}
|
||||
*/
|
||||
params: '=',
|
||||
/**
|
||||
* Confirm handler
|
||||
* @param {function}
|
||||
*/
|
||||
onConfirm: '&',
|
||||
/**
|
||||
* Close handler
|
||||
* @param {function}
|
||||
*/
|
||||
onClose: '&',
|
||||
/**
|
||||
* Optional object that extends the default config
|
||||
* @todo See if the angular version supports one way binding
|
||||
*/
|
||||
extendConfig: '=',
|
||||
},
|
||||
controller: function($scope, $timeout) {
|
||||
// Default config
|
||||
// @todo Annoying to override?
|
||||
this.config = {
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
classes: 'kuiButton--primary'
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
classes: 'kuiButton--hollow'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function () {
|
||||
|
||||
if (this.extendConfig) {
|
||||
this.config = angular.merge({}, this.config, this.extendConfig);
|
||||
}
|
||||
|
||||
// We want to be able to close the modal with the escape key
|
||||
document.addEventListener('keydown', handleCloseOnEsc);
|
||||
};
|
||||
|
||||
/**
|
||||
* User confirmed the action
|
||||
*/
|
||||
this.confirm = function () {
|
||||
if (this.onConfirm()) {
|
||||
this.onConfirm()({
|
||||
params: this.params
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the confirmation.
|
||||
* If available, a "reason" is passed to the handler.
|
||||
* Reasons:
|
||||
* - cancel (user clicked the cancel button)
|
||||
* - overlay (user clicked the overlay)
|
||||
* - esc (user hit the escape key)
|
||||
* @param {String} reason
|
||||
*/
|
||||
this.closeModal = function (reason) {
|
||||
if (this.onClose()) {
|
||||
this.onClose()(reason);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle keydown events and close the modal with the escape key
|
||||
* @param {Event} event
|
||||
*/
|
||||
let handleCloseOnEsc = (event) => {
|
||||
if (event.keyCode === 27) {
|
||||
// Make sure angular is notified
|
||||
$timeout(() => {
|
||||
this.closeModal('esc');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle clicks on the overlay and check whether to close the modal
|
||||
* @param {Event} event
|
||||
*/
|
||||
this.closeOnOverlay = function (event) {
|
||||
// Make sure we only close on clicks that happened
|
||||
// on the overlay itself, and not on its content
|
||||
if (!angular.element(event.target).hasClass('js-confirmOverlay')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeModal('overlay');
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up
|
||||
*/
|
||||
this.$onDestroy = function () {
|
||||
document.removeEventListener('keydown', handleCloseOnEsc);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
<div class="kuiModalOverlay" ng-show="displayModal">
|
||||
<div class="kuiModal" style="width: 450px;">
|
||||
<div class="kuiModalHeader">
|
||||
<div class="kuiModalHeader__title">Confirm Delete</div>
|
||||
</div>
|
||||
<div class="kuiModalBody">
|
||||
<div class="kuiModalBodyText">Do you really want to delete '{{deleteModalResourceName}}'?
|
||||
</div>
|
||||
</div>
|
||||
<div class="kuiModalFooter">
|
||||
<button type="button" ng-click="closeDeleteModal()" class="kuiButton kuiButton--hollow"><span
|
||||
class="kuiButton__inner"><span>Cancel</span></span></button>
|
||||
<button type="button" ng-click="delete()" class="kuiButton kuiButton--primary"><span>Confirm</span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycDeleteModal', function () {
|
||||
return {
|
||||
template: require('./deletemodal.html'),
|
||||
restrict: 'E',
|
||||
scope: false
|
||||
};
|
||||
});
|
13
public/apps/configuration/directives/directives.js
Normal file
13
public/apps/configuration/directives/directives.js
Normal file
@ -0,0 +1,13 @@
|
||||
import './edit_header/edit_header';
|
||||
import './filterbar/filterbar';
|
||||
import './form_resourcename/form_resourcename';
|
||||
import './form_newresourcefield/form_newresourcefield';
|
||||
import './list_header/list_header';
|
||||
import './accordeon/accordeon';
|
||||
import './permissions/permissions';
|
||||
import './fileupload/fileupload';
|
||||
import './header/header';
|
||||
import './errormessage/errormessage';
|
||||
import './confirmationmodal/confirmationmodal';
|
||||
import './form_focusfield/form_focusaddedfield';
|
||||
import './form_focusfield/form_focusfield';
|
@ -0,0 +1,9 @@
|
||||
<div class="kuiLocalNav">
|
||||
<div class="kuiLocalNavRow">
|
||||
<div class="kuiLocalNavRow__section">
|
||||
<div class="kuiLocalTitle">
|
||||
{{title()}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycEditHeader', function () {
|
||||
return {
|
||||
template: require('./edit_header.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
<div class="kuiInfoPanel kuiInfoPanel--error" ng-show="errorMessage" style="margin-bottom:10px;">
|
||||
<div class="kuiInfoPanelHeader">
|
||||
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
|
||||
<span class="kuiInfoPanelHeader__title" ng-bind-html="errorMessage" id="opendistro_security.label.form.errormessage"></span>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycErrorMessage', function () {
|
||||
return {
|
||||
template: require('./errormessage.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('fileModel', ['$parse', function ($parse) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
//var fileReader = new $window.FileReader();
|
||||
var reader = new FileReader();
|
||||
var model = $parse(attrs.fileModel);
|
||||
var modelSetter = model.assign;
|
||||
|
||||
element.bind('change', function(){
|
||||
scope.$apply(function(){
|
||||
var file = element[0].files[0];
|
||||
reader.readAsText(file);
|
||||
reader.onload = function (evt) {
|
||||
modelSetter(scope, reader.result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
@ -0,0 +1,51 @@
|
||||
<div>
|
||||
<div class="kuiToolBar">
|
||||
<div class="kuiToolBarSearch">
|
||||
<div class="kuiToolBarSearchBox">
|
||||
<div class="kuiToolBarSearchBox__icon kuiIcon fa-search"></div>
|
||||
<input class="kuiToolBarSearchBox__input ng-pristine ng-untouched ng-valid" type="text" placeholder="Search..." ng-model="query">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuiToolBarSection">
|
||||
<button id="opendistro_security.button.toolbar.new" type="button" tooltip="Add a new {{service.title.singular}}" ng-click="new(query)" class="kuiButton kuiButton--primary kuiButton--iconText">
|
||||
<span class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
</button>
|
||||
|
||||
<a ng-href="#/" class="kuiButton kuiButton--basic kuiButton--iconText" tooltip="Back to overview">
|
||||
<span class="kuiButton__icon kuiIcon fa-chevron-left" id="opendistro_security.button.toolbar.back"></span>
|
||||
Back
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="kuiToolBarSection">
|
||||
<!-- We need an empty section for the buttons to be positioned consistently. -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Results -->
|
||||
<div class="kuiPanel kuiPanel--centered kuiPanel--withToolBar" ng-show="!(resourcenames | filter:query).length && loaded">
|
||||
<div class="kuiEmptyTablePrompt">
|
||||
<div class="kuiEmptyTablePrompt__message">No {{service.title.plural}} found. Would you like to add one?</div>
|
||||
<div class="kuiEmptyTablePrompt__actions">
|
||||
<div class="kuiButtonGroup">
|
||||
<button id="opendistro_security.button.tableprompt.new" type="button" ng-click="new(query)" class="kuiButton kuiButton--primary kuiButton--iconText" aria-label="Add a new {{service.title.singular}}">
|
||||
<span class="kuiButton__inner">
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
<span>Add a new {{service.title.singular}}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="kuiPanel kuiPanel--centered kuiPanel--withToolBar" ng-show="!loaded">
|
||||
<div class="kuiEmptyTablePrompt">
|
||||
<div class="kuiEmptyTablePrompt__message">Loading ...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
11
public/apps/configuration/directives/filterbar/filterbar.js
Normal file
11
public/apps/configuration/directives/filterbar/filterbar.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycFilterBar', function () {
|
||||
return {
|
||||
template: require('./filterbar.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
/**
|
||||
* Attaches a mutation observer and checks for
|
||||
* dynamically added input type="text" elements
|
||||
*/
|
||||
app.directive('securitycFormFocusAddedField', function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, el) {
|
||||
|
||||
/**
|
||||
* The MutationObserver
|
||||
* @type {null|MutationObserver}
|
||||
*/
|
||||
let observer = null;
|
||||
|
||||
/**
|
||||
* Options for the MutationObserver
|
||||
* @type {{childList: boolean, subtree: boolean}}
|
||||
*/
|
||||
let config = {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
};
|
||||
|
||||
// Callback function to execute when mutations are observed
|
||||
let observerCallback = function(mutationsList) {
|
||||
|
||||
// We only want mutations with added nodes
|
||||
let mutationsWithAdditions = mutationsList.filter((mutation) => {
|
||||
return (mutation.addedNodes.length);
|
||||
});
|
||||
|
||||
if (mutationsWithAdditions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, only bother with the first mutation
|
||||
let mutation = mutationsWithAdditions[0];
|
||||
|
||||
// Check the first added node and make sure it's an ELEMENT_NODE (nodeType === 1)
|
||||
if (mutation.addedNodes[0].nodeType === 1) {
|
||||
|
||||
let focusable = mutation.addedNodes[0].querySelector("input[type='text']");
|
||||
// Only focus on elements without a value for now
|
||||
if (focusable && focusable.value === '') {
|
||||
focusable.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if ('MutationObserver' in window) {
|
||||
// Use a $timeout to avoid listening for mutations on the first render
|
||||
$timeout(function() {
|
||||
observer = new MutationObserver(observerCallback);
|
||||
observer.observe(el[0], config);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up
|
||||
*/
|
||||
el.on('$destroy', function() {
|
||||
if (observer !== null) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
/**
|
||||
* Adds focus to a single input field.
|
||||
* Since we may use this in conjunction with UI-Select, we support
|
||||
* adding the directive both directly to an input field, but also
|
||||
* to a container, in which the element can be found.
|
||||
*/
|
||||
app.directive('securitycFormFocusField', function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
focusWhen: '=',
|
||||
},
|
||||
link: function(scope, el) {
|
||||
scope.$watch('focusWhen', function(current, previous) {
|
||||
if (current === true && ! previous) {
|
||||
$timeout(function() {
|
||||
// We use several UI-Select instances, so in order to support those we
|
||||
// can also attach this directive to a parent container
|
||||
if (el[0].nodeName.toLowerCase() !== 'input') {
|
||||
let focusable = el[0].querySelector("input[type='text']");
|
||||
if (focusable) {
|
||||
focusable.focus();
|
||||
}
|
||||
} else {
|
||||
// Directive seems to be attached to an input element
|
||||
el[0].focus();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
<div>
|
||||
<div ng-show="isNew" style="margin-bottom:20px;">
|
||||
<h3 style="margin-top:0px;">{{service.newLabel}}:</h3>
|
||||
<input
|
||||
id="object-form-id-new"
|
||||
name="objectId"
|
||||
type="text"
|
||||
ng-model="resourcename"
|
||||
class="form-control kuiTextInput"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
required
|
||||
ng-required="isNew"/>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycFormNewResourceField', function () {
|
||||
return {
|
||||
template: require('./form_newresourcefield.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
<div>
|
||||
|
||||
<table class="fullwidth" ng-show="!isNew" style="margin-bottom:10px;">
|
||||
<tr>
|
||||
<td>
|
||||
<h3 class="resourcename" id="opendistro_security.label.resource.resourcename">
|
||||
{{resourcelabel}}: {{resourcename}}
|
||||
<div class="kuiBadge kuiBadge--default" ng-if="resource.readonly">
|
||||
<span class="kuiIcon fa-lock" id="opendistro_security.label.resource.reserved"></span>
|
||||
reserved
|
||||
</div>
|
||||
</h3>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button id="opendistro_security.button.resource.json.refresh" ng-if="showEditor" type="button" class="kuiButton kuiButton--secondary kuiButton--iconText" ng-click="loadJSON(resource)">
|
||||
<span class="kuiButton__icon kuiIcon fa-refresh" ng-bind></span>
|
||||
Refresh
|
||||
</button>
|
||||
<button id="opendistro_security.button.resource.json.toggle" type="button" class="kuiButton kuiButton--secondary kuiButton--iconText" ng-click="toggleEditor(resource)">
|
||||
<span class="kuiButton__icon kuiIcon fa-code" ng-bind></span>
|
||||
{{toggleEditorLabel}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="margin-bottom:10px;"
|
||||
id="object-form-json-raw"
|
||||
name="jsonRaw"
|
||||
ui-ace="{ onLoad: aceLoaded, mode: 'json' }"
|
||||
readonly
|
||||
ng-model="resourceAsJson"
|
||||
required
|
||||
ng-if="showEditor"
|
||||
>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycFormResourceName', function () {
|
||||
return {
|
||||
template: require('./form_resourcename.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
9
public/apps/configuration/directives/header/header.html
Normal file
9
public/apps/configuration/directives/header/header.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="kuiLocalNav kuiLocalNavMain">
|
||||
<div class="kuiLocalNavRow">
|
||||
<div class="kuiLocalNavRow__section">
|
||||
<div class="headerHeadline" id="opendistro_security.label.header.title">
|
||||
{{title}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
11
public/apps/configuration/directives/header/header.js
Normal file
11
public/apps/configuration/directives/header/header.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycHeader', function () {
|
||||
return {
|
||||
template: require('./header.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
<div class="kuiLocalNav">
|
||||
<div class="kuiLocalNavRow">
|
||||
<div class="kuiLocalNavRow__section">
|
||||
<div class="headerHeadline">
|
||||
<span id="opendistro_security.label.header.title">{{title}}</span><br/>
|
||||
<span id="opendistro_security.label.header.numresources">{{numresources}} entries found.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycListHeader', function () {
|
||||
return {
|
||||
template: require('./list_header.html'),
|
||||
replace: true,
|
||||
restrict: 'E'
|
||||
};
|
||||
});
|
@ -0,0 +1,115 @@
|
||||
<div>
|
||||
<table class="kuiTable tableIndexGroups">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kuiTableHeaderCell tableHeaderCellIndexGroups">
|
||||
<span class="kuiTableHeaderCell__liner">
|
||||
Permissions: Action Groups
|
||||
</span>
|
||||
</th>
|
||||
|
||||
<th class="kuiTableHeaderCell tableHeaderCellIndexGroups actions">
|
||||
<span class="kuiTableHeaderCell__liner">
|
||||
<input type="checkbox" data-ng-model="showAdvanced"> Show Advanced</td>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="kuiTableRow" ng-show="!permissionsResource.actiongroups.length">
|
||||
<td colspan="2" class="kuiTableRowCell">
|
||||
<div class="kuiPanel kuiPanel--centered kuiPanel--noborder">
|
||||
<div class="kuiEmptyTablePrompt kuiEmptyTablePrompt--nopadding">
|
||||
<div class="kuiEmptyTablePrompt__message ng-binding">No Action Groups found.</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="kuiTableRow" data-ng-repeat="actiongroup in permissionsResource.actiongroups track by $index" >
|
||||
<td class="kuiTableRowCell cellAlignTop">
|
||||
<fieldset class="marginbottom--small" id="object-form-actiongroups">
|
||||
<ui-select ng-model="permissionsResource.actiongroups[$index]">
|
||||
<ui-select-match placeholder="Action Group name">
|
||||
{{permissionsResource.actiongroups[$index]}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="item in (getActiongroupItems() | filter: $select.search) track by $index">
|
||||
<span ng-bind="item"></span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</fieldset>
|
||||
</td>
|
||||
<td class="kuiTableRowCell cellAlignTop actions">
|
||||
<button style="display:inline-block" type="button" ng-click="confirmDeletePermission(permissionsResource.actiongroups, actiongroup)" class="kuiButton kuiButton--danger kuiButton--iconText">
|
||||
<span class="kuiButton__icon kuiIcon fa-trash-o"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="kuiTableRow">
|
||||
<td class="kuiTableRowCell cellAlignTop ">
|
||||
|
||||
</td>
|
||||
<td class="kuiTableRowCell cellAlignTop actions">
|
||||
<button type="button" ng-click="addArrayEntry(permissionsResource, 'actiongroups', '')" ng-disabled="lastArrayEntryEmpty(permissionsResource.actiongroups)" class="kuiButton kuiButton--primary kuiButton--iconText">
|
||||
<span class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
Add Action Group
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Permissions Table -->
|
||||
<table class="kuiTable tableIndexGroups" ng-if="showAdvanced">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kuiTableHeaderCell tableHeaderCellIndexGroups">
|
||||
<span class="kuiTableHeaderCell__liner">
|
||||
Permissions: Single Permissions
|
||||
</span>
|
||||
</th>
|
||||
|
||||
<th class="kuiTableHeaderCell">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="kuiTableRow" ng-show="!permissionsResource.permissions.length">
|
||||
<td colspan="2" class="kuiTableRowCell">
|
||||
<div class="kuiPanel kuiPanel--centered kuiPanel--noborder">
|
||||
<div class="kuiEmptyTablePrompt kuiEmptyTablePrompt--nopadding">
|
||||
<div class="kuiEmptyTablePrompt__message ng-binding">No Single Permissions found.</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="kuiTableRow" data-ng-repeat="permission in permissionsResource.permissions track by $index">
|
||||
<td class="kuiTableRowCell cellAlignTop">
|
||||
<fieldset class="marginbottom--small" id="object-form-actiongroups">
|
||||
<ui-select ng-model="permissionsResource.permissions[$index]">
|
||||
<ui-select-match placeholder="Start with cluster: or indices:">
|
||||
{{permission}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="item in (permissionItems | filter: $select.search) track by $index">
|
||||
<span ng-bind="item"></span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</fieldset>
|
||||
</td>
|
||||
<td class="kuiTableRowCell cellAlignTop actions">
|
||||
<a ng-click="confirmDeletePermission(permissionsResource.permissions, permission)" class="kuiButton kuiButton--danger kuiButton--iconText">
|
||||
<span class="kuiButton__icon kuiIcon fa-trash-o"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="kuiTableRow">
|
||||
<td class="kuiTableRowCell cellAlignTop actions"></td>
|
||||
<td class="kuiTableRowCell cellAlignTop actions">
|
||||
<button type="button" ng-click="addArrayEntry(permissionsResource, 'permissions', '')" ng-disabled="lastArrayEntryEmpty(permissionsResource.permissions)" class="kuiButton kuiButton--primary kuiButton--iconText">
|
||||
<span class="kuiButton__icon kuiIcon fa-plus"></span>
|
||||
Add Single Permission
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
@ -0,0 +1,95 @@
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
const app = uiModules.get('apps/opendistro_security/configuration', []);
|
||||
|
||||
app.directive('securitycPermissions', function () {
|
||||
return {
|
||||
template: require('./permissions.html'),
|
||||
restrict: 'EA',
|
||||
scope: {
|
||||
"permissionsResource": "=permissionsresource",
|
||||
'onShouldConfirm': '&',
|
||||
},
|
||||
controller: 'securityBaseController',
|
||||
link: function(scope, elem, attr) {
|
||||
|
||||
scope.showAdvanced = null;
|
||||
scope.actiongroupItems = [];
|
||||
|
||||
scope.$watch('permissionsResource', function(newValue, oldValue){
|
||||
if(newValue && (scope.showAdvanced == null)) {
|
||||
if (scope.permissionsResource.permissions && scope.permissionsResource.permissions.length > 0) {
|
||||
scope.showAdvanced = true;
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
|
||||
/**
|
||||
* Prepare values for the actiongroupsAutoComplete
|
||||
* We could probably change the data source to avoid
|
||||
* having to convert the data twice
|
||||
* @returns {Array|*}
|
||||
*/
|
||||
scope.getActiongroupItems = function() {
|
||||
if (scope.actiongroupItems.length) {
|
||||
return scope.actiongroupItems;
|
||||
}
|
||||
|
||||
if (scope.actiongroupsAutoComplete) {
|
||||
scope.actiongroupItems = scope.actiongroupsAutoComplete.map((item) => {
|
||||
return item.name;
|
||||
});
|
||||
}
|
||||
|
||||
return scope.actiongroupItems;
|
||||
|
||||
};
|
||||
|
||||
// UI-Select seems to work best with a plain array in this case
|
||||
scope.permissionItems = scope.allpermissionsAutoComplete.map((item) => {
|
||||
return item.name;
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a weird workaround for the autocomplete where
|
||||
* we have can't or don't want to use the model item
|
||||
* directly in the view. Instead, we use the on-select
|
||||
* event to set the target value
|
||||
* @type {{}}
|
||||
*/
|
||||
/*
|
||||
scope.onSelectedActionGroup = function(event) {
|
||||
scope.permissionsResource.actiongroups[event.index] = event.item.name;
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a weird workaround for the autocomplete where
|
||||
* we have can't or don't want to use the model item
|
||||
* directly in the view. Instead, we use the on-select
|
||||
* event to set the target value
|
||||
* @type {{}}
|
||||
*/
|
||||
/*
|
||||
scope.onSelectedPermission = function(event) {
|
||||
scope.permissionsResource.permissions[event.index] = event.item.name;
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Since we have an isolated scope, we can't modify the parent scope without breaking
|
||||
* the binding. Hence, we pass the parent scope's handler to this directive.
|
||||
*
|
||||
* An alternative could be to encapsulate the delete logic in a service.
|
||||
*
|
||||
* @param {array} source
|
||||
* @param {string} item
|
||||
*/
|
||||
scope.confirmDeletePermission = function(source, item) {
|
||||
scope.onShouldConfirm()(source, item);
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
});
|
40
public/apps/configuration/permissions/clusterpermissions.js
Normal file
40
public/apps/configuration/permissions/clusterpermissions.js
Normal file
@ -0,0 +1,40 @@
|
||||
export default [
|
||||
{"name": "cluster:admin/ingest/pipeline/delete"},
|
||||
{"name": "cluster:admin/ingest/pipeline/get"},
|
||||
{"name": "cluster:admin/ingest/pipeline/put"},
|
||||
{"name": "cluster:admin/ingest/pipeline/simulate"},
|
||||
{"name": "cluster:admin/ingest/processor/grok/get"},
|
||||
{"name": "cluster:admin/reindex/rethrottle"},
|
||||
{"name": "cluster:admin/repository/delete"},
|
||||
{"name": "cluster:admin/repository/get"},
|
||||
{"name": "cluster:admin/repository/put"},
|
||||
{"name": "cluster:admin/repository/verify"},
|
||||
{"name": "cluster:admin/reroute"},
|
||||
{"name": "cluster:admin/script/delete"},
|
||||
{"name": "cluster:admin/script/get"},
|
||||
{"name": "cluster:admin/script/put"},
|
||||
{"name": "cluster:admin/settings/update"},
|
||||
{"name": "cluster:admin/snapshot/create"},
|
||||
{"name": "cluster:admin/snapshot/delete"},
|
||||
{"name": "cluster:admin/snapshot/get"},
|
||||
{"name": "cluster:admin/snapshot/restore"},
|
||||
{"name": "cluster:admin/snapshot/status"},
|
||||
{"name": "cluster:admin/snapshot/status*"},
|
||||
{"name": "cluster:admin/tasks/cancel"},
|
||||
{"name": "cluster:admin/tasks/test"},
|
||||
{"name": "cluster:admin/tasks/testunblock"},
|
||||
{"name": "cluster:monitor/allocation/explain"},
|
||||
{"name": "cluster:monitor/health"},
|
||||
{"name": "cluster:monitor/main"},
|
||||
{"name": "cluster:monitor/nodes/hot_threads"},
|
||||
{"name": "cluster:monitor/nodes/info"},
|
||||
{"name": "cluster:monitor/nodes/liveness"},
|
||||
{"name": "cluster:monitor/nodes/stats"},
|
||||
{"name": "cluster:monitor/nodes/usage"},
|
||||
{"name": "cluster:monitor/remote/info"},
|
||||
{"name": "cluster:monitor/state"},
|
||||
{"name": "cluster:monitor/stats"},
|
||||
{"name": "cluster:monitor/task"},
|
||||
{"name": "cluster:monitor/task/get"},
|
||||
{"name": "cluster:monitor/tasks/list"}
|
||||
];
|
64
public/apps/configuration/permissions/indexpermissions.js
Normal file
64
public/apps/configuration/permissions/indexpermissions.js
Normal file
@ -0,0 +1,64 @@
|
||||
export default [
|
||||
{"name": "indices:admin/aliases"},
|
||||
{"name": "indices:admin/aliases/exists"},
|
||||
{"name": "indices:admin/aliases/get"},
|
||||
{"name": "indices:admin/analyze"},
|
||||
{"name": "indices:admin/cache/clear"},
|
||||
{"name": "indices:admin/close"},
|
||||
{"name": "indices:admin/create"},
|
||||
{"name": "indices:admin/delete"},
|
||||
{"name": "indices:admin/exists"},
|
||||
{"name": "indices:admin/flush"},
|
||||
{"name": "indices:admin/flush*"},
|
||||
{"name": "indices:admin/forcemerge"},
|
||||
{"name": "indices:admin/get"},
|
||||
{"name": "indices:admin/mapping/put"},
|
||||
{"name": "indices:admin/mappings/fields/get"},
|
||||
{"name": "indices:admin/mappings/fields/get*"},
|
||||
{"name": "indices:admin/mappings/get"},
|
||||
{"name": "indices:admin/open"},
|
||||
{"name": "indices:admin/refresh"},
|
||||
{"name": "indices:admin/refresh*"},
|
||||
{"name": "indices:admin/rollover"},
|
||||
{"name": "indices:admin/seq_no/global_checkpoint_sync"},
|
||||
{"name": "indices:admin/settings/update"},
|
||||
{"name": "indices:admin/shards/search_shards"},
|
||||
{"name": "indices:admin/shrink"},
|
||||
{"name": "indices:admin/synced_flush"},
|
||||
{"name": "indices:admin/template/delete"},
|
||||
{"name": "indices:admin/template/get"},
|
||||
{"name": "indices:admin/template/put"},
|
||||
{"name": "indices:admin/types/exists"},
|
||||
{"name": "indices:admin/upgrade"},
|
||||
{"name": "indices:admin/validate/query"},
|
||||
{"name": "indices:data/read/explain"},
|
||||
{"name": "indices:data/read/field_caps"},
|
||||
{"name": "indices:data/read/field_caps*"},
|
||||
{"name": "indices:data/read/get"},
|
||||
{"name": "indices:data/read/mget"},
|
||||
{"name": "indices:data/read/mget*"},
|
||||
{"name": "indices:data/read/msearch"},
|
||||
{"name": "indices:data/read/msearch/template"},
|
||||
{"name": "indices:data/read/mtv"},
|
||||
{"name": "indices:data/read/mtv*"},
|
||||
{"name": "indices:data/read/scroll"},
|
||||
{"name": "indices:data/read/scroll/clear"},
|
||||
{"name": "indices:data/read/search"},
|
||||
{"name": "indices:data/read/search*"},
|
||||
{"name": "indices:data/read/search/template"},
|
||||
{"name": "indices:data/read/tv"},
|
||||
{"name": "indices:data/write/bulk"},
|
||||
{"name": "indices:data/write/bulk*"},
|
||||
{"name": "indices:data/write/delete"},
|
||||
{"name": "indices:data/write/delete/byquery"},
|
||||
{"name": "indices:data/write/index"},
|
||||
{"name": "indices:data/write/reindex"},
|
||||
{"name": "indices:data/write/update"},
|
||||
{"name": "indices:data/write/update/byquery"},
|
||||
{"name": "indices:monitor/recovery"},
|
||||
{"name": "indices:monitor/segments"},
|
||||
{"name": "indices:monitor/settings/get"},
|
||||
{"name": "indices:monitor/shard_stores"},
|
||||
{"name": "indices:monitor/stats"},
|
||||
{"name": "indices:monitor/upgrade"}
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user