From fc24a532a36e406ec8ee346a80746367ad39ae05 Mon Sep 17 00:00:00 2001
From: Yan Zeng <46499415+zengyan-amazon@users.noreply.github.com>
Date: Thu, 9 Apr 2020 14:54:43 -0700
Subject: [PATCH] Initial PR for new security kibana plugin (#162)
Initial commit of Security Kibana plugin
---
.eslintrc.js | 8 +
.gitignore | 5 +
CODE_OF_CONDUCT.md | 2 +
LICENSE | 176 ++++++++++++
NOTICE | 2 +
README.md | 1 +
common/index.ts | 17 ++
kibana.json | 9 +
public/application.tsx | 44 +++
public/index.scss | 0
public/index.ts | 26 ++
public/plugin.ts | 51 ++++
public/types.ts | 24 ++
server/auth/types/basic/basic_auth.ts | 115 ++++++++
server/auth/types/basic/routes.ts | 271 ++++++++++++++++++
server/auth/user.ts | 36 +++
server/backend/opendistro_security_client.ts | 70 +++++
...pendistro_security_configuration_plugin.ts | 219 ++++++++++++++
server/backend/opendistro_security_plugin.ts | 48 ++++
server/index.ts | 146 ++++++++++
server/plugin.ts | 96 +++++++
server/routes/index.ts | 267 +++++++++++++++++
server/routes/test_routes.ts | 70 +++++
server/session/security_cookie.ts | 52 ++++
server/types.ts | 19 ++
server/utils/filter_auth_headers.ts | 32 +++
26 files changed, 1806 insertions(+)
create mode 100644 .eslintrc.js
create mode 100644 CODE_OF_CONDUCT.md
create mode 100644 LICENSE
create mode 100644 NOTICE
create mode 100644 README.md
create mode 100644 common/index.ts
create mode 100644 kibana.json
create mode 100644 public/application.tsx
create mode 100644 public/index.scss
create mode 100644 public/index.ts
create mode 100644 public/plugin.ts
create mode 100644 public/types.ts
create mode 100644 server/auth/types/basic/basic_auth.ts
create mode 100644 server/auth/types/basic/routes.ts
create mode 100644 server/auth/user.ts
create mode 100644 server/backend/opendistro_security_client.ts
create mode 100644 server/backend/opendistro_security_configuration_plugin.ts
create mode 100644 server/backend/opendistro_security_plugin.ts
create mode 100644 server/index.ts
create mode 100644 server/plugin.ts
create mode 100644 server/routes/index.ts
create mode 100644 server/routes/test_routes.ts
create mode 100644 server/session/security_cookie.ts
create mode 100644 server/types.ts
create mode 100644 server/utils/filter_auth_headers.ts
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 000000000..d97d3568e
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,8 @@
+
+module.exports = {
+ root: true,
+ extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'],
+ rules: {
+ "@kbn/eslint/require-license-header": "off"
+ }
+};
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e69de29bb..8b8cc022b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,5 @@
+npm-debug.log*
+node_modules
+/build/
+/public/app.css
+target
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..4c45bccef
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,2 @@
+## Code of Conduct
+This project has adopted an [Open Source Code of Conduct](https://opendistro.github.io/for-elasticsearch/codeofconduct.html).
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..2bb9ad240
--- /dev/null
+++ b/LICENSE
@@ -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
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 000000000..448b392c8
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,2 @@
+ Open Distro for Elasticsearch Security Kibana Plugin
+Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..80e80b193
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# security-kibana-plugin
diff --git a/common/index.ts b/common/index.ts
new file mode 100644
index 000000000..48edde340
--- /dev/null
+++ b/common/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 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 const PLUGIN_ID = 'opendistroSecurity';
+export const PLUGIN_NAME = 'opendistro_security';
diff --git a/kibana.json b/kibana.json
new file mode 100644
index 000000000..53f266c19
--- /dev/null
+++ b/kibana.json
@@ -0,0 +1,9 @@
+{
+ "id": "security",
+ "version": "0.0.1",
+ "kibanaVersion": "8.0.0",
+ "configPath": ["opendistro_security"],
+ "requiredPlugins": ["navigation"],
+ "server": true,
+ "ui": true
+}
diff --git a/public/application.tsx b/public/application.tsx
new file mode 100644
index 000000000..92ea36a5e
--- /dev/null
+++ b/public/application.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 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 React from 'react';
+import ReactDOM from 'react-dom';
+import { AppMountContext, AppMountParameters, CoreStart } from '../../../src/core/public';
+import { AppPluginStartDependencies } from './types';
+
+export function renderApp(
+ { notifications, http }: CoreStart,
+ { navigation }: AppPluginStartDependencies,
+ appMountContext: AppMountContext,
+ params: AppMountParameters
+ // basePath: string
+) {
+
+ ReactDOM.render(
+ // security application
+ (
+
),
+ params.element);
+ return () => ReactDOM.unmountComponentAtNode(params.element);
+}
+
+function setBreadcrumbs(appMountContext: AppMountContext) {
+ appMountContext.core.chrome.setBreadcrumbs([
+ {
+ text: "Security",
+ href: '',
+ }
+ ]);
+}
\ No newline at end of file
diff --git a/public/index.scss b/public/index.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/public/index.ts b/public/index.ts
new file mode 100644
index 000000000..a85c45b60
--- /dev/null
+++ b/public/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 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 './index.scss';
+
+import { OpendistroSecurityPlugin } from './plugin';
+import { PluginInitializerContext } from '../../../src/core/public';
+
+// This exports static code and TypeScript types,
+// as well as, Kibana Platform `plugin()` initializer.
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new OpendistroSecurityPlugin(initializerContext);
+}
+export { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types';
diff --git a/public/plugin.ts b/public/plugin.ts
new file mode 100644
index 000000000..835f6e4de
--- /dev/null
+++ b/public/plugin.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 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 { i18n } from '@kbn/i18n';
+import { AppMountParameters, CoreSetup, CoreStart, Plugin, PluginInitializerContext, AppMountContext } from '../../../src/core/public';
+import {
+ OpendistroSecurityPluginSetup,
+ OpendistroSecurityPluginStart,
+ AppPluginStartDependencies,
+} from './types';
+
+export class OpendistroSecurityPlugin
+ implements Plugin {
+
+ constructor(private readonly initializerContext: PluginInitializerContext) {}
+
+ public setup(core: CoreSetup): OpendistroSecurityPluginSetup {
+ core.application.register({
+ id: "opendistro_security",
+ title: "Security",
+ order: 1,
+ mount: async (context: AppMountContext, params: AppMountParameters) => {
+ const { renderApp } = await import('./application');
+ const [coreStart, depsStart] = await core.getStartServices();
+ return renderApp(coreStart, depsStart as AppPluginStartDependencies, context, params);
+ }
+ });
+
+ // Return methods that should be available to other plugins
+ return {
+ };
+ }
+
+ public start(core: CoreStart): OpendistroSecurityPluginStart {
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/public/types.ts b/public/types.ts
new file mode 100644
index 000000000..e3c123c61
--- /dev/null
+++ b/public/types.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2020 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 { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public';
+
+export interface OpendistroSecurityPluginSetup {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface OpendistroSecurityPluginStart {}
+
+export interface AppPluginStartDependencies {
+ navigation: NavigationPublicPluginStart;
+}
diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts
new file mode 100644
index 000000000..717847ee1
--- /dev/null
+++ b/server/auth/types/basic/basic_auth.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2020 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 { AuthenticationHandler, SessionStorageFactory, IRouter, IClusterClient } from "../../../../../../src/core/server";
+import { SecurityPluginConfigType } from "../../..";
+import { SecuritySessionCookie } from "../../../session/security_cookie";
+import { CoreSetup } from "../../../../../../src/core/server";
+import _ from 'lodash';
+import { SecurityClient } from "../../../backend/opendistro_security_client";
+import { BasicAuthRoutes } from "./routes";
+
+export class AuthConfig {
+ constructor(
+ public readonly authType: string,
+ public readonly authHeaderName: string,
+ public readonly allowedAdditionalAuthHeaders: string[],
+ public readonly authenticateFunction: () => void,
+ public readonly validateAvailableTenants: boolean,
+ public readonly validateAvailableRoles: boolean) {
+ }
+}
+
+export class BasicAuthentication {
+ private static readonly AUTH_HEADER_NAME: string = 'authorization';
+ private static readonly ALLOWED_ADDITIONAL_AUTH_HEADERS: string[] = ['security_impersonate_as'];
+ private static readonly ROUTES_TO_IGNORE: string[] = [
+ '/bundles/app/security-login/bootstrap.js',
+ '/bundles/app/security-customerror/bootstrap.js',
+ '/',
+ '/app/login',
+ '/app/opendistro_login',
+ '/api/core/capabilities',
+ ];
+
+ // private readonly unauthenticatedRoutes: string[];
+ private readonly securityClient: SecurityClient;
+ private readonly authConfig: AuthConfig;
+
+ constructor(private readonly config: SecurityPluginConfigType,
+ private readonly sessionStorageFactory: SessionStorageFactory,
+ private readonly router: IRouter,
+ private readonly esClient: IClusterClient,
+ private readonly coreSetup: CoreSetup) {
+
+ const multitenantEnabled = config.multitenancy.enabled;
+
+ this.securityClient = new SecurityClient(this.esClient);
+ this.authConfig = new AuthConfig('basicauth',
+ BasicAuthentication.AUTH_HEADER_NAME,
+ BasicAuthentication.ALLOWED_ADDITIONAL_AUTH_HEADERS,
+ async () => { },
+ multitenantEnabled,
+ true);
+ // this.unauthenticatedRoutes = this.config.auth.unauthenticated_routes;
+
+ this.init();
+ }
+
+ private async init() {
+ const routes = new BasicAuthRoutes(this.router, this.config, this.sessionStorageFactory, this.securityClient, this.authConfig, this.coreSetup);
+ routes.setupRoutes();
+ }
+
+ /**
+ * Basic Authentication auth handler. Registered to core.http if basic authentication is enabled.
+ */
+ authHandler: AuthenticationHandler = async (request, response, toolkit) => {
+
+ if (BasicAuthentication.ROUTES_TO_IGNORE.includes(request.url.path)) {
+ return toolkit.authenticated();
+ }
+
+ if (this.config.auth.unauthenticated_routes.indexOf(request.url.path) > -1) {
+ // TODO: user kibana server user
+ return toolkit.authenticated();
+ }
+
+ let cookie: SecuritySessionCookie = undefined;
+ try {
+ cookie = await this.sessionStorageFactory.asScoped(request).get();
+ // TODO: need to do auth for each all?
+ if (!cookie) {
+ return response.unauthorized();
+ }
+ // set cookie to extend ttl
+ cookie.expiryTime = Date.now() + this.config.cookie.ttl;
+ this.sessionStorageFactory.asScoped(request).set(cookie);
+
+ // pass credentials to request to Elasticsearch
+ const credentials = cookie.credentials;
+ return toolkit.authenticated({
+ // state: credentials,
+ requestHeaders: {
+ 'authorization': credentials.authHeaderValue,
+ },
+ });
+ } catch (error) {
+ // TODO: switch to logger
+ console.log(`error: ${error}`);
+ // TODO: redirect using response?
+ }
+ }
+}
diff --git a/server/auth/types/basic/routes.ts b/server/auth/types/basic/routes.ts
new file mode 100644
index 000000000..d43a9c5ca
--- /dev/null
+++ b/server/auth/types/basic/routes.ts
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2020 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 { IRouter, SessionStorageFactory, KibanaRequest } from "../../../../../../src/core/server";
+import { SecuritySessionCookie } from "../../../session/security_cookie";
+import { SecurityPluginConfigType } from "../../..";
+import { AuthConfig } from "./basic_auth";
+import { filterAuthHeaders } from "../../../utils/filter_auth_headers";
+import { User } from "../../user";
+import { SecurityClient } from "../../../backend/opendistro_security_client";
+import { schema } from '@kbn/config-schema';
+import { CoreSetup } from "../../../../../../src/core/server";
+
+export class BasicAuthRoutes {
+ constructor(private readonly router: IRouter,
+ private readonly config: SecurityPluginConfigType,
+ private readonly sessionStorageFactory: SessionStorageFactory,
+ private readonly securityClient: SecurityClient,
+ private readonly authConfig: AuthConfig,
+ private readonly coreSetup: CoreSetup) {
+ }
+
+ public async setupRoutes() {
+ const PREFIX = '';
+
+ // if the user can be authenticated using auth headers, redirect to the next url, otherwise, render login page
+ this.router.get(
+ {
+ path: `${PREFIX}/login`,
+ validate: false,
+ options: {
+ authRequired: false,
+ }
+ },
+ async (context, request, response) => {
+ try {
+ const alternativeHeaders = this.config.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) {
+ await this.authenticateWithHeaders(request);
+
+ let nextUrl = undefined;
+ if (request.url.query) {
+ // TODO: extract nextUrl from query string
+ response.redirected({
+ headers: {
+ location: nextUrl,
+ }
+ });
+ }
+ }
+ }
+ } catch (error) {
+ return response.redirected({
+ headers: {
+ location: `/customerror`,
+ }
+ })
+ }
+
+ return response.ok({
+ body: await context.core.rendering.render(),
+ headers: {
+ 'content-security-policy': this.coreSetup.http.csp.header,
+ }
+ }); // render login page here
+ }
+ );
+
+ // login using username and password
+ this.router.post(
+ {
+ path: `${PREFIX}/auth/login`,
+ validate: {
+ body: schema.object({
+ username: schema.string(),
+ password: schema.string(),
+ }),
+ },
+ options: {
+ authRequired: false,
+ }
+ },
+ async (context, request, response) => {
+ const forbidden_usernames = this.config.auth.forbidden_usernames;
+ if (forbidden_usernames.indexOf(request.body.username) > -1) {
+ throw new Error('Invalid username or password'); // Cannot login using forbidden user name.
+ }
+
+ // const authHeaderValue = Buffer.from(`${request.body.username}:${request.body.password}`).toString('base64');
+ let user: User;
+ try {
+ user = await this.securityClient.authenticate(request, { username: request.body.username, password: request.body.password});
+ } catch (error) {
+ return response.unauthorized({
+ headers: {
+ "www-authenticate": error.message,
+ }
+ })
+ }
+
+ const encodedCredentials = Buffer.from(`${request.body.username}:${request.body.password}`).toString('base64');
+ const sessionStorage: SecuritySessionCookie = {
+ username: user.username,
+ credentials: {
+ authHeaderValue: `Basic ${encodedCredentials}`,
+ },
+ authType: 'basicauth',
+ isAnonymousAuth: false,
+ expiryTime: Date.now() + this.config.cookie.ttl,
+ }
+ this.sessionStorageFactory.asScoped(request).set(sessionStorage);
+
+ if (this.config.multitenancy.enabled) {
+ let globalTenantEnabled = this.config.multitenancy.tenants.enable_global;
+ let privateTentantEnabled = this.config.multitenancy.tenants.enable_private;
+ let preferredTenants = this.config.multitenancy.tenants.preferred;
+
+ // TODO: figureout selected tenant here and set it in the cookie
+
+ return response.ok({
+ body: {
+ username: user.username,
+ tenants: user.tenants,
+ roles: user.roles,
+ backendroles: user.backendRoles,
+ selectedTenants: '', // TODO: determine selected tenants
+ }
+ })
+ }
+ return response.ok({
+ body: {
+ username: user.username,
+ tenants: user.tenants,
+ }
+ });
+ },
+ );
+
+ // logout
+ this.router.post({
+ path: `${PREFIX}/auth/logout`,
+ validate: false,
+ options: {
+ authRequired: false,
+ }
+ },
+ async (context, request, response) => {
+ this.sessionStorageFactory.asScoped(request).clear();
+ return response.ok(); // TODO: redirect to login?
+ });
+
+ // anonymous auth
+ this.router.get({
+ path: `${PREFIX}/auth/anonymous`,
+ validate: false,
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ if (this.config.auth.anonymous_auth_enabled) {
+ // TODO: implement anonymous auth for basic authentication
+ } else {
+ return response.redirected({
+ headers: {
+ location: `${PREFIX}/login`,
+ }
+ })
+ }
+ });
+
+ // renders custom error page
+ this.router.get({
+ path: `${PREFIX}/customerror`,
+ validate: false,
+ options: {
+ authRequired: false,
+ }
+ },
+ async (context, request, response) => {
+ return response.ok({
+ body: '',
+ })
+ });
+ }
+
+ // session storage plugin's authenticateWithHeaders() function
+ private async authenticateWithHeaders(request: KibanaRequest, credentials: any = {}, options: any = {}) {
+ try {
+ const additionalAuthHeaders = filterAuthHeaders(request.headers, this.authConfig.allowedAdditionalAuthHeaders);
+ let user = await this.securityClient.authenticateWithHeaders(request, credentials, additionalAuthHeaders);
+
+ let session: SecuritySessionCookie = {
+ username: user.username,
+ credentials: credentials,
+ authType: this.authConfig.authType,
+ assignAuthHeader: false,
+ };
+ let sessionTtl = this.config.session.ttl;
+ if (sessionTtl) {
+ session.expiryTime = Date.now() + sessionTtl;
+ }
+ const authResponse: AuthResponse = {
+ session,
+ user,
+ };
+
+ return this._handleAuthResponse(request, authResponse, additionalAuthHeaders);
+ } catch (error) {
+ this.sessionStorageFactory.asScoped(request).clear();
+ throw error;
+ }
+ }
+
+ private _handleAuthResponse(request: KibanaRequest, authResponse: AuthResponse, additionalAuthHeaders: any = {}) {
+ // Validate the user has at least one tenant
+ if (this.authConfig.validateAvailableTenants && this.config.multitenancy.enabled &&
+ !this.config.multitenancy.tenants.enable_global) {
+ let privateTentantEnabled = this.config.multitenancy.tenants.enable_private;
+ let allTenants = authResponse.user.tenants;
+
+ if (!this._hasAtLastOneTenant(authResponse.user, allTenants, privateTentantEnabled)) {
+ throw new Error('No tenant available for this user, please contact your system administrator.');
+ }
+ }
+
+ if (this.authConfig.validateAvailableRoles && (!authResponse.user.roles || authResponse.user.roles.length === 0)) {
+ throw new Error('No roles available for this user, please contact your system administrator.');
+ }
+
+ if (Object.keys(additionalAuthHeaders).length > 0) {
+ authResponse.session.additionalAuthHeaders = additionalAuthHeaders;
+ }
+
+ this.sessionStorageFactory.asScoped(request).set(authResponse.session);
+
+ return authResponse;
+ }
+
+ private _hasAtLastOneTenant(user: User, allTenant: any, privateTentantEnabled: boolean): boolean {
+ if (privateTentantEnabled) {
+ return true;
+ }
+
+ if (!allTenant || Object.keys(allTenant).length === 0 ||
+ (Object.keys(allTenant).length === 1 && Object.keys(allTenant)[0] === user.username)) {
+ return false;
+ }
+ return true;
+ }
+}
+
+class AuthResponse {
+ session: SecuritySessionCookie;
+ user: User;
+}
\ No newline at end of file
diff --git a/server/auth/user.ts b/server/auth/user.ts
new file mode 100644
index 000000000..fb51a3d65
--- /dev/null
+++ b/server/auth/user.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 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 class User {
+ readonly username: string;
+ readonly roles: Array;
+ readonly backendRoles: Array;
+ readonly tenants: Array;
+ readonly selectedTenant: string;
+ readonly credentials: any;
+ readonly proxyCredentials: any;
+
+ constructor(username: string, roles: Array, backendRoles: Array, tenants: Array,
+ selectedTenant: string, credentials: any = undefined, proxyCredentials: any = undefined) {
+ this.username = username;
+ this.roles = roles;
+ this.backendRoles = backendRoles;
+ this.tenants = tenants;
+ this.selectedTenant = selectedTenant;
+ this.credentials = credentials;
+ this.proxyCredentials = proxyCredentials;
+ }
+}
+
diff --git a/server/backend/opendistro_security_client.ts b/server/backend/opendistro_security_client.ts
new file mode 100644
index 000000000..5c4c51ce3
--- /dev/null
+++ b/server/backend/opendistro_security_client.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 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 { IClusterClient, KibanaRequest } from "../../../../src/core/server";
+import { User} from "../auth/user";
+
+export class SecurityClient {
+ constructor(private readonly esClient: IClusterClient) {
+ }
+
+ public async authenticate(request: KibanaRequest, credentials: any): Promise {
+ const authHeader = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
+ try {
+ let esResponse = await this.esClient.asScoped(request).callAsCurrentUser('opendistro_security.authinfo', {
+ headers: {
+ authorization: `Basic ${authHeader}`,
+ }
+ });
+ return new User(credentials.username, esResponse.roles, esResponse.backend_roles, esResponse.teanats, esResponse.user_requested_tenant, credentials, credentials);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async authenticateWithHeader(request: KibanaRequest, headerName: string, headerValue: string, whitelistedHeadersAndValues: any, additionalAuthHeaders: any = {}): Promise {
+ try {
+ const credentials: any = {
+ headerName,
+ headerValue,
+ };
+ let headers = {};
+ if (headerValue) {
+ headers[headerName] = headerValue;
+ }
+
+ // cannot get config elasticsearch.requestHeadersWhitelist from kibana.yml file in new platfrom
+ // meanwhile, do we really need to save all headers in cookie?
+ const esResponse = await this.esClient.asScoped(request).callAsCurrentUser('opendistro_security.authinfo', {
+ headers: headers
+ });
+ return new User(esResponse.user_name, esResponse.roles, esResponse.backend_roles, esResponse.teanats, esResponse.user_requested_tenant, credentials, null);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async authenticateWithHeaders(request: KibanaRequest, headerscredentials: any = {}, additionalAuthHeaders: any = {}) {
+ try {
+ const esResponse = await this.esClient.asScoped(request).callAsCurrentUser('opendistro_security.authinfo', {
+ headers: additionalAuthHeaders,
+ });
+ return new User(esResponse.user_name, esResponse.roles, esResponse.backend_roles, esResponse.tenants, esResponse.user_requested_tenant);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ }
+
+}
diff --git a/server/backend/opendistro_security_configuration_plugin.ts b/server/backend/opendistro_security_configuration_plugin.ts
new file mode 100644
index 000000000..1b3f059d3
--- /dev/null
+++ b/server/backend/opendistro_security_configuration_plugin.ts
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2020 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 function (Client: any, config: any, components: any) {
+
+ 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
+ }
+ }
+ }
+ });
+
+ /**
+ * Updates a resource.
+ * Resource identification is expected to computed from headers. Eg: auth headers.
+ *
+ * Sample response:
+ * {
+ * "status": "OK",
+ * "message": "Username updated."
+ * }
+ */
+ Client.prototype.opendistro_security.prototype.saveResourceWithoutId = ca({
+ method: 'PUT',
+ needBody: true,
+ url: {
+ fmt: '/_opendistro/_security/api/<%=resourceName%>',
+ req: {
+ resourceName: {
+ 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'
+ }
+ });
+
+ Client.prototype.opendistro_security.prototype.getIndexMappings = ca({
+ method: 'GET',
+ needBody: true,
+ url: {
+ fmt: '/<%=index%>/_mapping',
+ req: {
+ index: {
+ type: 'string',
+ required: true
+ }
+ }
+ }
+ });
+
+
+ /////
+ 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'
+ }
+ });
+}
\ No newline at end of file
diff --git a/server/backend/opendistro_security_plugin.ts b/server/backend/opendistro_security_plugin.ts
new file mode 100644
index 000000000..1163c98d1
--- /dev/null
+++ b/server/backend/opendistro_security_plugin.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 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 function (Client: any, config: any, components: any) {
+
+ 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'
+ }
+ });
+
+};
\ No newline at end of file
diff --git a/server/index.ts b/server/index.ts
new file mode 100644
index 000000000..033a49b93
--- /dev/null
+++ b/server/index.ts
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2020 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 { schema, TypeOf } from '@kbn/config-schema';
+import { PluginInitializerContext, PluginConfigDescriptor } from '../../../src/core/server';
+import { OpendistroSecurityPlugin } from './plugin';
+
+export const configSchema = schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ allow_client_certificates: schema.boolean({ defaultValue: false }),
+ readonly_mode: schema.object({
+ roles: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ cookie: schema.object({
+ secure: schema.boolean({ defaultValue: true }),
+ name: schema.string({ defaultValue: 'security_authentication' }),
+ password: schema.string({ defaultValue: 'security_cookie_default_password', minLength: 32 }),
+ ttl: schema.number({ defaultValue: 60 * 60 * 1000 }),
+ domain: schema.nullable(schema.string()),
+ isSameSite: schema.oneOf(
+ [
+ schema.string({
+ validate(value) {
+ if (value === 'Strict' || value === 'Lax') {
+ return `Allowed values of 'isSameSite' are ['Strict, 'Lax', true, false]`;
+ }
+ }
+ }),
+ schema.boolean()
+ ], { defaultValue: true }),
+ }),
+ session: schema.object({
+ ttl: schema.number({ defaultValue: 60 * 60 * 1000 }),
+ keepalive: schema.boolean({ defaultValue: true }),
+ }),
+ auth: schema.object({
+ type: schema.string({
+ defaultValue: '',
+ validate(value) {
+ if (!['', 'basicauth', 'jwt', 'openid', 'saml', 'proxy', 'kerberos', 'proxycache'].includes(value)) {
+ return `allowed auth.type are ['', 'basicauth', 'jwt', 'openid', 'saml', 'proxy', 'kerberos', 'proxycache']`;
+ }
+ }
+ }),
+ anonymous_auth_enabled: schema.boolean({ defaultValue: false }),
+ unauthenticated_routes: schema.arrayOf(schema.string(), { defaultValue: ["/api/status"] }),
+ forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ logout_url: schema.string({ defaultValue: '' }),
+ }),
+ basicauth: schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ unauthenticated_routes: schema.arrayOf(schema.string(), { defaultValue: ["/api/status"] }),
+ forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ header_trumps_session: schema.boolean({ defaultValue: false }),
+ alternative_login: schema.object({
+ headers: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ show_for_parameter: schema.string({ defaultValue: '' }),
+ valid_redirects: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ button_text: schema.string({ defaultValue: 'Login with provider' }),
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ loadbalancer_url: schema.maybe(schema.string()),
+ login: schema.object({
+ title: schema.string({ defaultValue: 'Please login to Kibana' }),
+ subtitle: schema.string({ defaultValue: 'If you have forgotten your username or password, please ask your system administrator' }),
+ showbrandimage: schema.boolean({ defaultValue: true }),
+ brandimage: schema.string({ defaultValue: '' }), // TODO: update brand image
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ }),
+ multitenancy: schema.maybe(schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ show_roles: schema.boolean({ defaultValue: false }),
+ enable_filter: schema.boolean({ defaultValue: false }),
+ debug: schema.boolean({ defaultValue: false }),
+ tenants: schema.object({
+ enable_private: schema.boolean({ defaultValue: true }),
+ enable_global: schema.boolean({ defaultValue: true }),
+ preferred: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ })),
+ configuration: schema.maybe(schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ })),
+ accountinfo: schema.maybe(schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ })),
+ openid: schema.maybe(schema.object({
+ connect_url: schema.maybe(schema.string()),
+ header: schema.string({ defaultValue: 'Authorization' }),
+ // TODO: test if siblingRef() works here
+ // client_id is required when auth.type is openid
+ client_id: schema.conditional(schema.siblingRef('auth.type'), 'openid', schema.string(), schema.maybe(schema.string())),
+ client_secret: schema.string({ defaultValue: '' }),
+ scope: schema.string({ defaultValue: 'openid profile email address phone' }),
+ base_redirect_url: schema.string({ defaultValue: '' }),
+ logout_url: schema.string({ defaultValue: '' }),
+ root_ca: schema.string({ defaultValue: '' }),
+ verify_hostnames: schema.boolean({ defaultValue: true }),
+ })),
+ proxycache: schema.maybe(schema.object({
+ // when auth.type is proxycache, user_header, roles_header and proxy_header_ip are required
+ user_header: schema.conditional(schema.siblingRef('auth.type'), 'proxycache', schema.string(), schema.maybe(schema.string())),
+ roles_header: schema.conditional(schema.siblingRef('auth.type'), 'proxycache', schema.string(), schema.maybe(schema.string())),
+ proxy_header: schema.maybe(schema.string({ defaultValue: 'x-forwarded-for' })),
+ proxy_header_ip: schema.conditional(schema.siblingRef('auth.type'), 'proxycache', schema.string(), schema.maybe(schema.string())),
+ login_endpoint: schema.maybe(schema.string({ defaultValue: '' })),
+ })),
+ jwt: schema.maybe(schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ login_endpoint: schema.maybe(schema.string()),
+ url_param: schema.string({ defaultValue: 'authorization' }),
+ header: schema.string({ defaultValue: 'Authorization' }),
+ })),
+});
+
+export type SecurityPluginConfigType = TypeOf;
+
+export const config: PluginConfigDescriptor = {
+ exposeToBrowser: {
+ cookie: true,
+ auth: true,
+ },
+ schema: configSchema,
+};
+
+// This exports static code and TypeScript types,
+// as well as, Kibana Platform `plugin()` initializer.
+
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new OpendistroSecurityPlugin(initializerContext);
+}
+
+export { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types';
diff --git a/server/plugin.ts b/server/plugin.ts
new file mode 100644
index 000000000..02ddb1cee
--- /dev/null
+++ b/server/plugin.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2020 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 {
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ Logger,
+ IClusterClient,
+ SessionStorageFactory,
+} from '../../../src/core/server';
+
+import { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types';
+import { defineRoutes } from './routes';
+import { SecurityPluginConfigType } from '.';
+import opendistro_security_configuratoin_plugin from './backend/opendistro_security_configuration_plugin';
+import opendistro_security_plugin from './backend/opendistro_security_plugin';
+import { first } from 'rxjs/operators';
+import { SecuritySessionCookie, getSecurityCookieOptions } from './session/security_cookie';
+import { BasicAuthentication } from './auth/types/basic/basic_auth';
+import { defineTestRoutes } from './routes/test_routes'; // TODO: remove this later
+
+export class OpendistroSecurityPlugin
+ implements Plugin {
+ private readonly logger: Logger;
+
+ constructor(private readonly initializerContext: PluginInitializerContext) {
+ this.logger = initializerContext.logger.get();
+ }
+
+ public async setup(core: CoreSetup) {
+ this.logger.debug('opendistro_security: Setup');
+
+ const config$ = this.initializerContext.config.create();
+ const config: SecurityPluginConfigType = await config$.pipe(first()).toPromise();
+
+ const router = core.http.createRouter();
+
+ const securityClient: IClusterClient = core.elasticsearch.createClient(
+ 'opendistro_security',
+ {
+ plugins: [
+ opendistro_security_configuratoin_plugin,
+ // TODO need to add other endpoints such as multitenanct and other
+ // FIXME: having multiple plugins caused the extended endpoints not working, currently
+ // added all endpoints to opendistro_security_configuratoin_plugin as a workaround
+ // opendistro_security_plugin,
+ ],
+ }
+ );
+
+ const securitySessionStorageFactory: SessionStorageFactory
+ = await core.http.createCookieSessionStorageFactory(getSecurityCookieOptions(config));
+
+
+ // Register server side APIs
+ defineRoutes(router, securityClient);
+
+ // test routes
+ defineTestRoutes(router, securityClient, securitySessionStorageFactory, core);
+
+
+ // setup auth
+ if (config.auth.type === undefined || config.auth.type === '' || config.auth.type === 'basicauth') {
+ // TODO: switch implementation according to configurations
+ const auth = new BasicAuthentication(config, securitySessionStorageFactory, router, securityClient, core);
+ core.http.registerAuth(auth.authHandler);
+ }
+
+ return {
+ config$,
+ securityConfigClient: securityClient,
+ };
+ }
+
+ public start(core: CoreStart) {
+ this.logger.debug('opendistro_security: Started');
+ return {};
+ }
+
+ public stop() { }
+
+}
diff --git a/server/routes/index.ts b/server/routes/index.ts
new file mode 100644
index 000000000..fb6604b4c
--- /dev/null
+++ b/server/routes/index.ts
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2020 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 { IRouter, IClusterClient } from '../../../../src/core/server';
+import { schema } from '@kbn/config-schema';
+
+export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient) {
+ const API_PREFIX: string = '/api/v1/opendistro_security';
+
+ const internalUserSchema = schema.object({
+ description: schema.string(),
+ password: schema.string(),
+ backend_roles: schema.arrayOf(schema.string()),
+ // opendistro_security_roles: schema.nullable(schema.arrayOf(schema.string())),
+ attributes: schema.any(),
+ });
+
+ const actionGroupSchema = schema.object({
+ description: schema.string(),
+ allowed_actions: schema.arrayOf(schema.string()),
+ type: schema.oneOf([schema.literal('cluster'), schema.literal('index'), schema.literal('kibana')]),
+ });
+
+ const roleMappingSchema = schema.object({
+ description: schema.string(),
+ backend_roles: schema.arrayOf(schema.string()),
+ hosts: schema.arrayOf(schema.string()),
+ users: schema.arrayOf(schema.string()),
+ });
+
+ const roleSchema = schema.object({
+ description: schema.string(),
+ cluster_permissions: schema.nullable(schema.arrayOf(schema.string())),
+ tenant_permissions: schema.arrayOf(schema.any()),
+ index_permissions: schema.arrayOf(schema.any()),
+ });
+
+ const tenantSchema = schema.object({
+ description: schema.string(),
+ });
+
+ const accountSchema = schema.object({
+ password: schema.string(),
+ current_password: schema.string(),
+ });
+
+ function validateRequestBody(resourceName: string, requestBody: any): any {
+ let inputSchema;
+ switch (resourceName) {
+ case 'internalusers': inputSchema = internalUserSchema; break;
+ case 'actiongroups': inputSchema = accountSchema; break;
+ case 'rolesmapping': inputSchema = roleMappingSchema; break;
+ case 'roles': inputSchema = roleSchema; break;
+ case 'tenants': inputSchema = tenantSchema; break;
+ case 'account': inputSchema = accountSchema; break;
+ default: throw new Error(`Unknown resource ${resourceName}`);
+ }
+ inputSchema.validate(requestBody); // throws error if validation fail
+ }
+
+ router.get(
+ {
+ path: '/api/test/security_config',
+ validate: false,
+ },
+ async (context, request, response) => {
+ const esResponse = await securityConfigClient.asScoped(request).callAsCurrentUser('opendistro_security.restapiinfo', { format: 'json' });
+ return response.ok({
+ body: esResponse,
+ });
+ }
+ );
+
+ // list resources by resource name
+ router.get(
+ {
+ path: `${API_PREFIX}/configuration/{resourceName}`,
+ validate: {
+ params: schema.object({
+ resourceName: schema.string(),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ const client = securityConfigClient.asScoped(request);
+ let esResp;
+ try {
+ esResp = await client.callAsCurrentUser('opendistro_security.listResource', { resourceName: request.params.resourceName });
+ return response.ok({
+ body: {
+ total: Object.keys(esResp).length,
+ data: esResp,
+ }
+ });
+ } catch (error) {
+ return response.custom({
+ statusCode: error.statusCode,
+ body: error.message,
+ });
+ }
+ },
+ );
+
+ // get resource by resource name and id
+ router.get(
+ {
+ path: `${API_PREFIX}/configuration/{resourceName}/{id}`,
+ validate: {
+ params: schema.object({
+ resourceName: schema.string(),
+ id: schema.string(),
+ }),
+ }
+ },
+ async (context, request, response) => {
+ const client = securityConfigClient.asScoped(request);
+ let esResp;
+ try {
+ esResp = await client.callAsCurrentUser('opendistro_security.getResource', { resourceName: request.params.resourceName, id: request.params.id });
+ return response.ok({ body: esResp[request.params.id] });
+ } catch (error) {
+ return response.custom({
+ statusCode: error.statusCode,
+ body: error.message,
+ });
+ }
+ },
+ );
+
+ // delete resource by resource name and id
+ router.delete(
+ {
+ path: `${API_PREFIX}/configuration/{resourceName}/{id}`,
+ validate: {
+ params: schema.object({
+ resourceName: schema.string(),
+ id: schema.string(),
+ }),
+ }
+ },
+ async (context, request, response) => {
+ const client = securityConfigClient.asScoped(request);
+ let esResp;
+ try {
+ esResp = await client.callAsCurrentUser('opendistro_security.deleteResource', { resourceName: request.params.resourceName, id: request.params.id });
+ return response.ok({
+ body: {
+ message: esResp.message,
+ }
+ })
+ } catch (error) {
+ return response.custom({
+ statusCode: error.statusCode,
+ body: error.message,
+ });
+ }
+ }
+ );
+
+ // create new resource
+ router.post(
+ {
+ path: `${API_PREFIX}/configuration/{resourceName}`,
+ validate: {
+ params: schema.object({
+ resourceName: schema.string(),
+ }),
+ body: schema.any(),
+ },
+ },
+ async (context, request, response) => {
+ try {
+ validateRequestBody(request.params.resourceName, request.body)
+ } catch (error) {
+ return response.badRequest({ body: error });
+ }
+ const client = securityConfigClient.asScoped(request);
+ let esResp;
+ try {
+ esResp = await client.callAsCurrentUser('opendistro_security.saveResourceWithoutId', { resourceName: request.params.resourceName, body: request.body });
+ return response.ok({
+ body: {
+ message: esResp.message,
+ }
+ });
+ } catch (error) {
+ return response.custom({
+ statusCode: error.statusCode,
+ body: error.message,
+ });
+ }
+ }
+ );
+
+ // update resource by Id
+ router.post(
+ {
+ path: `${API_PREFIX}/configuration/{resourceName}/{id}`,
+ validate: {
+ params: schema.object({
+ resourceName: schema.string(),
+ id: schema.string(),
+ }),
+ body: schema.any(),
+ },
+ },
+ async (context, request, response) => {
+ try {
+ validateRequestBody(request.params.resourceName, request.body)
+ } catch (error) {
+ return response.badRequest({ body: error });
+ }
+ const client = securityConfigClient.asScoped(request);
+ let esResp;
+ try {
+ esResp = await client.callAsCurrentUser('opendistro_security.saveResource', { resourceName: request.params.resourceName, id: request.params.id, body: request.body });
+ return response.ok({
+ body: {
+ message: esResp.message,
+ }
+ });
+ } catch (error) {
+ return response.custom({
+ statusCode: error.statusCode,
+ body: error.message,
+ });
+ }
+ }
+ );
+
+ router.get(
+ {
+ path: `${API_PREFIX}/auth/authinfo`,
+ validate: false,
+ },
+ async (context, request, response) => {
+ const client = securityConfigClient.asScoped(request);
+ let esResp;
+ try {
+ esResp = await client.callAsCurrentUser('opendistro_security.authinfo');
+
+ return response.ok({
+ body: {
+ message: esResp.message,
+ }
+ });
+ } catch (error) {
+ return response.custom({
+ statusCode: error.statusCode,
+ body: error.message,
+ });
+ }
+ }
+ );
+}
diff --git a/server/routes/test_routes.ts b/server/routes/test_routes.ts
new file mode 100644
index 000000000..493efd43e
--- /dev/null
+++ b/server/routes/test_routes.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020 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 { IRouter, IClusterClient, SessionStorageFactory } from "../../../../src/core/server";
+import { SecuritySessionCookie } from "../session/security_cookie";
+import { schema } from '@kbn/config-schema';
+import { User } from "../auth/user";
+import { SecurityClient } from "../backend/opendistro_security_client";
+import { CoreSetup } from "../../../../src/core/server";
+
+export function defineTestRoutes(router: IRouter,
+ securityConfigClient: IClusterClient,
+ sessionStorageFactory: SessionStorageFactory,
+ core: CoreSetup) {
+
+ router.get({
+ path: `/test/login`,
+ validate: {
+ query: schema.object({
+ username: schema.string(),
+ password: schema.string(),
+ }),
+ },
+ options: {
+ authRequired: false,
+ }
+ },
+ async (context, request, response) => {
+ sessionStorageFactory.asScoped(request).clear();
+ const username = request.query.username;
+ const password = request.query.password;
+ let user: User;
+ try {
+ const securityClient = new SecurityClient(securityConfigClient);
+ user = await securityClient.authenticate(request, { username, password});
+ const encodedCredentials = Buffer.from(`${username}:${password}`).toString('base64');
+ const sessionStorage: SecuritySessionCookie = {
+ username: user.username,
+ credentials: {
+ authHeaderValue: `Basic ${encodedCredentials}`,
+ },
+ authType: 'basicauth',
+ isAnonymousAuth: false,
+ expiryTime: Date.now() + 3600000000,
+ }
+ sessionStorageFactory.asScoped(request).set(sessionStorage);
+ return response.redirected({
+ headers: {
+ location: `${core.http.basePath.serverBasePath}/app/kibana`,
+ }
+ });
+ } catch (error) {
+ return response.unauthorized({
+ body: `Failed to authenticate with username: '${username}' and password: '${password}'`,
+ })
+ }
+ });
+}
\ No newline at end of file
diff --git a/server/session/security_cookie.ts b/server/session/security_cookie.ts
new file mode 100644
index 000000000..22bb76648
--- /dev/null
+++ b/server/session/security_cookie.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 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 { SessionStorageCookieOptions, Logger } from "../../../../src/core/server";
+import { SecurityPluginConfigType } from "..";
+
+export class SecuritySessionCookie {
+ // security_authentication
+ username: string;
+ credentials?: any;
+ authType?: string;
+ assignAuthHeader?: boolean;
+ isAnonymousAuth?: boolean;
+ expiryTime?: number;
+ additionalAuthHeaders?: any;
+
+ // security_storage
+ tentent?: any;
+}
+
+export function getSecurityCookieOptions(config: SecurityPluginConfigType): SessionStorageCookieOptions {
+ return {
+ name: config.cookie.name,
+ encryptionKey: config.cookie.password,
+ validate: (sessionStorage: SecuritySessionCookie) => {
+ if (sessionStorage === undefined
+ || sessionStorage.username === undefined
+ || sessionStorage.credentials === undefined) {
+ return { isValid: false };
+ }
+
+ if (sessionStorage.expiryTime === undefined
+ || new Date(sessionStorage.expiryTime) < new Date()) {
+ return { isValid: false };
+ }
+ return { isValid: true, path: '/' };
+ },
+ isSecure: false, // config.cookie.secure,
+ }
+}
diff --git a/server/types.ts b/server/types.ts
new file mode 100644
index 000000000..4522ca856
--- /dev/null
+++ b/server/types.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 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.
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface OpendistroSecurityPluginSetup {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface OpendistroSecurityPluginStart {}
diff --git a/server/utils/filter_auth_headers.ts b/server/utils/filter_auth_headers.ts
new file mode 100644
index 000000000..fd2dfb565
--- /dev/null
+++ b/server/utils/filter_auth_headers.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 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 { Headers } from '../../../../src/core/server/http/router/headers';
+
+export function filterAuthHeaders(originalHeaders: Headers, headersToKeep: string[]) {
+ const normalizeHeader = function (header: string) {
+ if (!header) {
+ return '';
+ }
+ return header.trim().toLowerCase();
+ };
+
+ const headersToKeepNormalized = headersToKeep.map(normalizeHeader);
+ const originalHeadersNormalized = _.mapKeys(originalHeaders, function (headerValue, headerName) {
+ return normalizeHeader(headerName);
+ });
+ return _.pick(originalHeadersNormalized, headersToKeepNormalized);
+}