From f2996ba76e83f676b31f71fc583a2f9510c08b35 Mon Sep 17 00:00:00 2001 From: Yan Zeng <46499415+zengyan-amazon@users.noreply.github.com> Date: Tue, 14 Jan 2020 14:02:55 -0800 Subject: [PATCH 01/21] Initial commit --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 203 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 000000000..61658c6d6 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# security-kibana-plugin +private repo for Elasticsearch security kibana plugin development From 2be9bddd6de2ddac32541a0f806066be7f707325 Mon Sep 17 00:00:00 2001 From: Zeng Date: Tue, 28 Jan 2020 11:04:17 -0800 Subject: [PATCH 02/21] Add UI skeleton --- .gitignore | 4 ++ kibana.json | 8 +++ public/index.ts | 6 ++ public/plugin.ts | 39 ++++++++++++ .../roles/role-management-app.tsx | 10 +++ .../security-management-app.tsx | 63 +++++++++++++++++++ .../security-management.tsx | 30 +++++++++ 7 files changed, 160 insertions(+) create mode 100644 .gitignore create mode 100644 kibana.json create mode 100644 public/index.ts create mode 100644 public/plugin.ts create mode 100644 public/security-management/roles/role-management-app.tsx create mode 100644 public/security-management/security-management-app.tsx create mode 100644 public/security-management/security-management.tsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..521e1c729 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +npm-debug.log* +node_modules +/build/ +/public/app.css diff --git a/kibana.json b/kibana.json new file mode 100644 index 000000000..da96bf1de --- /dev/null +++ b/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "security", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["opendistro_security"], + "server": false, + "ui": true +} diff --git a/public/index.ts b/public/index.ts new file mode 100644 index 000000000..dd3408be7 --- /dev/null +++ b/public/index.ts @@ -0,0 +1,6 @@ +import { PluginInitializer, PluginInitializerContext} from 'kibana/public'; +import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new SecurityPlugin(initializerContext); diff --git a/public/plugin.ts b/public/plugin.ts new file mode 100644 index 000000000..b477cf7cf --- /dev/null +++ b/public/plugin.ts @@ -0,0 +1,39 @@ +import { + Plugin, + CoreSetup, + CoreStart, + PluginInitializerContext, + AppMountContext, + AppMountParameters +} from 'kibana/public'; + + +export class SecurityPlugin implements Plugin { + + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + + core.application.register({ + id: "security_management_app", + title: "Security", + order: 1, + mount: async (context: AppMountContext, params: AppMountParameters) => { + const { renderApp } = await import('./security-management/security-management'); + return renderApp(params.element, context, params.appBasePath); + } + }); + + return {}; + } + + public start(core: CoreStart) { + // eslint-disable-next-line no-console + console.log(`Security plugin started`); + } + + public stop() {} +} + +export type SecurityPluginSetup = ReturnType; +export type SecurityPluginStart = ReturnType; diff --git a/public/security-management/roles/role-management-app.tsx b/public/security-management/roles/role-management-app.tsx new file mode 100644 index 000000000..585183194 --- /dev/null +++ b/public/security-management/roles/role-management-app.tsx @@ -0,0 +1,10 @@ +import React, { Component } from 'react'; + +export class RoleManagement extends Component { + render() { + return (
+ RoleManagement page +
); + } +} + diff --git a/public/security-management/security-management-app.tsx b/public/security-management/security-management-app.tsx new file mode 100644 index 000000000..542c85315 --- /dev/null +++ b/public/security-management/security-management-app.tsx @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; +import { AppMountContext } from 'kibana/public'; +import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import { RoleManagement } from './roles/role-management-app'; + +class SecurityManagementAppProps { + element!: HTMLElement; + appMountContext!: AppMountContext; + basePath!: string; +} +class SecurityManagementAppState { + selectedTab?: EuiTabbedContentTab; +} + +class SecurityManagementApp extends Component { + private tabs: EuiTabbedContentTab[] = [ + { + id: 'roles', + name: 'Roles', + content: () + }, + { + id: 'action_groups', + name: 'Action Groups', + content: (
bbb
) + }, + { + id: 'internal_user_database', + name: 'Internal User Database', + content: (
ccc
) + } + ]; + + constructor(props: SecurityManagementAppProps) { + super(props); + this.state = {}; + } + + componentDidMount() { + this.setState( { + selectedTab: this.tabs[0], + } ); + } + + render() { + return
+ + + +
+ } + + onTabClick = (tab: EuiTabbedContentTab) => { + this.setState({ selectedTab: tab }); + }; +} + +export default SecurityManagementApp; \ No newline at end of file diff --git a/public/security-management/security-management.tsx b/public/security-management/security-management.tsx new file mode 100644 index 000000000..75a3f20dd --- /dev/null +++ b/public/security-management/security-management.tsx @@ -0,0 +1,30 @@ +import { AppMountContext } from 'kibana/public'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import SecurityManagementApp from './security-management-app' + +export function renderApp(element: HTMLElement, + appMountContext: AppMountContext, + basePath: string +) { + console.log("in renderApp"); + setBreadcrumbs(appMountContext); + + ReactDOM.render( + , + element); + return () => ReactDOM.unmountComponentAtNode(element); +} + +function setBreadcrumbs(appMountContext: AppMountContext) { + appMountContext.core.chrome.setBreadcrumbs([ + { + text: "Security Management", + href: '', + } + ]); +} From 477575cd2772f0addafe5d78d58bca3e91481aa1 Mon Sep 17 00:00:00 2001 From: Zeng Date: Tue, 28 Jan 2020 11:05:22 -0800 Subject: [PATCH 03/21] Add server plugin --- lib/auth/types/AuthType.ts | 10 +++++++++ lib/auth/user.ts | 30 +++++++++++++++++++++++++++ server/index.ts | 41 +++++++++++++++++++++++++++++++++++++ server/plugin.ts | 42 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 lib/auth/types/AuthType.ts create mode 100644 lib/auth/user.ts create mode 100644 server/index.ts create mode 100644 server/plugin.ts diff --git a/lib/auth/types/AuthType.ts b/lib/auth/types/AuthType.ts new file mode 100644 index 000000000..7b241a92f --- /dev/null +++ b/lib/auth/types/AuthType.ts @@ -0,0 +1,10 @@ +import { SecurityPluginConfigType } from 'src/plugins/security-kibana-plugin/server'; + +export default class AuthType { + private const APP_ROOT: string; + private const API_ROOT: string; + private pluginRoot: string; + private config: SecurityPluginConfigType; + private basePath: string; + private unauthenticatedRoutes: Array; +} \ No newline at end of file diff --git a/lib/auth/user.ts b/lib/auth/user.ts new file mode 100644 index 000000000..ea80387ea --- /dev/null +++ b/lib/auth/user.ts @@ -0,0 +1,30 @@ +export class User { + readonly username: string; + readonly roles: Array; + readonly backendRoles: Array; + readonly tenants: Array; + readonly selectedTenant: string; + readonly credentials: Credentials; + readonly proxyCredentials: Credentials; + + constructor(username: string, roles: Array, backendRoles: Array, tenants: Array, + selectedTenant: string, credentials: Credentials, proxyCredentials: Credentials) { + this.username = username; + this.roles = roles; + this.backendRoles = backendRoles; + this.tenants = tenants; + this.selectedTenant = selectedTenant; + this.credentials = credentials; + this.proxyCredentials = proxyCredentials; + } +} + +export class Credentials { + readonly username: string; + readonly password: string; + + constructor(username: string, password: string) { + this.username = username; + this.password = password; + } +} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 000000000..e72cee8cb --- /dev/null +++ b/server/index.ts @@ -0,0 +1,41 @@ +import { schema, TypeOf } from '@kbn/config-schema'; + +import { + PluginInitializerContext, + PluginConfigDescriptor, +} from 'kibana/server'; +import SecurityPlugin from './plugin'; + +export const configSchema = schema.object({ + cookie: schema.object({ + secure: schema.boolean({ defaultValue: false }), + ttl: schema.number({ defaultValue: 3600 }), + password: schema.string(), + }), + multitenancy: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + tenants: schema.object({ + preferred: schema.arrayOf(schema.string(), {defaultValue: [] }), + }), + }), + readonly_mode: schema.object({ + roles: schema.arrayOf(schema.string(), { defaultValue: [] }), + }), + auth: schema.object({ + unauthenticated_routes: schema.arrayOf(schema.string()), + }), +}); + +export type SecurityPluginConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + cookie: true, + }, + schema: configSchema, +}; + + + +export const plugin = (initializerContext: PluginInitializerContext) => + new SecurityPlugin(initializerContext); diff --git a/server/plugin.ts b/server/plugin.ts new file mode 100644 index 000000000..66af5a056 --- /dev/null +++ b/server/plugin.ts @@ -0,0 +1,42 @@ +import { map } from 'rxjs/operators'; +import { + CoreSetup, + CoreStart, + Logger, + PluginInitializerContext, + PluginName, +} from 'kibana/server'; +import { SecurityPluginConfigType } from './'; + +export default class SecurityPlugin { + private readonly log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = this.initializerContext.logger.get(); + } + + public setup(core: CoreSetup, deps: Record) { + const config$ = this.initializerContext.config.create(); + + const router = core.http.createRouter(); + router.get( + { path: '/test/authenticate', validate: false }, + async (context, req, res) => { + const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); + return res.ok({ body: `Elasticsearch: ${response}` }); + } + ); + + return { + config$, + }; + } + + public start(core: CoreStart, deps: Record) { + this.log.debug(`Starting security plugin`); + } + + public stop() { + this.log.debug(`Stopping security plugin`); + } +} From 30465d9e9997448c090784bcb962559f87624659 Mon Sep 17 00:00:00 2001 From: Zeng Date: Mon, 10 Feb 2020 14:13:44 -0800 Subject: [PATCH 04/21] Merge in files geneated from plugin generator --- .eslintrc.js | 7 +++ common/index.ts | 2 + kibana.json | 4 +- lib/auth/types/AuthType.ts | 4 +- ...ecurity-management.tsx => application.tsx} | 11 ++--- .../security-management-app.tsx | 4 +- .../roles/role-management-app.tsx | 0 public/index.scss | 0 public/index.ts | 15 ++++-- public/plugin.ts | 38 ++++++++------- public/types.ts | 9 ++++ server/index.ts | 46 ++++--------------- server/plugin.ts | 42 ++++++++--------- server/routes/index.ts | 17 +++++++ server/types.ts | 4 ++ 15 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 .eslintrc.js create mode 100644 common/index.ts rename public/{security-management/security-management.tsx => application.tsx} (75%) rename public/{security-management => components}/security-management-app.tsx (90%) rename public/{ => components}/security-management/roles/role-management-app.tsx (100%) create mode 100644 public/index.scss create mode 100644 public/types.ts create mode 100644 server/routes/index.ts create mode 100644 server/types.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..ff4759cf3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,7 @@ +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/common/index.ts b/common/index.ts new file mode 100644 index 000000000..c5f3b0ce3 --- /dev/null +++ b/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'opendistroSecurity'; +export const PLUGIN_NAME = 'opendistro_security'; diff --git a/kibana.json b/kibana.json index da96bf1de..43727a98e 100644 --- a/kibana.json +++ b/kibana.json @@ -1,8 +1,8 @@ { "id": "security", "version": "0.0.1", - "kibanaVersion": "kibana", + "kibanaVersion": "8.0.0", "configPath": ["opendistro_security"], - "server": false, + "server": true, "ui": true } diff --git a/lib/auth/types/AuthType.ts b/lib/auth/types/AuthType.ts index 7b241a92f..121417c36 100644 --- a/lib/auth/types/AuthType.ts +++ b/lib/auth/types/AuthType.ts @@ -1,10 +1,10 @@ -import { SecurityPluginConfigType } from 'src/plugins/security-kibana-plugin/server'; - export default class AuthType { + /* private const APP_ROOT: string; private const API_ROOT: string; private pluginRoot: string; private config: SecurityPluginConfigType; private basePath: string; private unauthenticatedRoutes: Array; + */ } \ No newline at end of file diff --git a/public/security-management/security-management.tsx b/public/application.tsx similarity index 75% rename from public/security-management/security-management.tsx rename to public/application.tsx index 75a3f20dd..8f2912e0a 100644 --- a/public/security-management/security-management.tsx +++ b/public/application.tsx @@ -1,13 +1,12 @@ -import { AppMountContext } from 'kibana/public'; -import ReactDOM from 'react-dom'; import React from 'react'; -import SecurityManagementApp from './security-management-app' +import ReactDOM from 'react-dom'; +import { AppMountContext } from '../../../src/core/public'; +import SecurityManagementApp from './components/security-management-app'; export function renderApp(element: HTMLElement, appMountContext: AppMountContext, basePath: string ) { - console.log("in renderApp"); setBreadcrumbs(appMountContext); ReactDOM.render( @@ -23,8 +22,8 @@ export function renderApp(element: HTMLElement, function setBreadcrumbs(appMountContext: AppMountContext) { appMountContext.core.chrome.setBreadcrumbs([ { - text: "Security Management", + text: "Security", href: '', } ]); -} +} \ No newline at end of file diff --git a/public/security-management/security-management-app.tsx b/public/components/security-management-app.tsx similarity index 90% rename from public/security-management/security-management-app.tsx rename to public/components/security-management-app.tsx index 542c85315..c4a9fb226 100644 --- a/public/security-management/security-management-app.tsx +++ b/public/components/security-management-app.tsx @@ -1,8 +1,8 @@ import React, { Component } from 'react'; -import { AppMountContext } from 'kibana/public'; import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import { EuiPanel } from '@elastic/eui'; -import { RoleManagement } from './roles/role-management-app'; +import { RoleManagement } from './security-management/roles/role-management-app'; +import { AppMountContext } from '../../../../src/core/public'; class SecurityManagementAppProps { element!: HTMLElement; diff --git a/public/security-management/roles/role-management-app.tsx b/public/components/security-management/roles/role-management-app.tsx similarity index 100% rename from public/security-management/roles/role-management-app.tsx rename to public/components/security-management/roles/role-management-app.tsx 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 index dd3408be7..d44c8a7cd 100644 --- a/public/index.ts +++ b/public/index.ts @@ -1,6 +1,11 @@ -import { PluginInitializer, PluginInitializerContext} from 'kibana/public'; -import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; +import './index.scss'; -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => new SecurityPlugin(initializerContext); +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 index b477cf7cf..991ae5ba4 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -1,39 +1,37 @@ +import { i18n } from '@kbn/i18n'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin, PluginInitializerContext, AppMountContext } from '../../../src/core/public'; import { - Plugin, - CoreSetup, - CoreStart, - PluginInitializerContext, - AppMountContext, - AppMountParameters -} from 'kibana/public'; + OpendistroSecurityPluginSetup, + OpendistroSecurityPluginStart, + AppPluginStartDependencies, +} from './types'; +import { PLUGIN_NAME } from '../common'; - -export class SecurityPlugin implements Plugin { +export class OpendistroSecurityPlugin + implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup): OpendistroSecurityPluginSetup { - public async setup(core: CoreSetup, deps: {}) { - core.application.register({ - id: "security_management_app", + id: "opendistro_security", title: "Security", order: 1, mount: async (context: AppMountContext, params: AppMountParameters) => { - const { renderApp } = await import('./security-management/security-management'); + const { renderApp } = await import('./application'); return renderApp(params.element, context, params.appBasePath); } }); - return {}; + // Return methods that should be available to other plugins + return { + }; } - public start(core: CoreStart) { - // eslint-disable-next-line no-console - console.log(`Security plugin started`); + public start(core: CoreStart): OpendistroSecurityPluginStart { + return {}; } public stop() {} } - -export type SecurityPluginSetup = ReturnType; -export type SecurityPluginStart = ReturnType; diff --git a/public/types.ts b/public/types.ts new file mode 100644 index 000000000..3fae38f0d --- /dev/null +++ b/public/types.ts @@ -0,0 +1,9 @@ +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/index.ts b/server/index.ts index e72cee8cb..33e1777ed 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,41 +1,11 @@ -import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '../../../src/core/server'; +import { OpendistroSecurityPlugin } from './plugin'; -import { - PluginInitializerContext, - PluginConfigDescriptor, -} from 'kibana/server'; -import SecurityPlugin from './plugin'; +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. -export const configSchema = schema.object({ - cookie: schema.object({ - secure: schema.boolean({ defaultValue: false }), - ttl: schema.number({ defaultValue: 3600 }), - password: schema.string(), - }), - multitenancy: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - tenants: schema.object({ - preferred: schema.arrayOf(schema.string(), {defaultValue: [] }), - }), - }), - readonly_mode: schema.object({ - roles: schema.arrayOf(schema.string(), { defaultValue: [] }), - }), - auth: schema.object({ - unauthenticated_routes: schema.arrayOf(schema.string()), - }), -}); +export function plugin(initializerContext: PluginInitializerContext) { + return new OpendistroSecurityPlugin(initializerContext); +} -export type SecurityPluginConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - cookie: true, - }, - schema: configSchema, -}; - - - -export const plugin = (initializerContext: PluginInitializerContext) => - new SecurityPlugin(initializerContext); +export { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types'; diff --git a/server/plugin.ts b/server/plugin.ts index 66af5a056..1aeac94d5 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -1,42 +1,42 @@ -import { map } from 'rxjs/operators'; import { + PluginInitializerContext, CoreSetup, CoreStart, + Plugin, Logger, - PluginInitializerContext, - PluginName, -} from 'kibana/server'; -import { SecurityPluginConfigType } from './'; +} from '../../../src/core/server'; + +import { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types'; +import { defineRoutes } from './routes'; +import { SecurityPluginConfigType } from '.'; -export default class SecurityPlugin { - private readonly log: Logger; +export class OpendistroSecurityPlugin + implements Plugin { + private readonly logger: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); + this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup) { + this.logger.debug('opendistro_security: Setup'); + const config$ = this.initializerContext.config.create(); const router = core.http.createRouter(); - router.get( - { path: '/test/authenticate', validate: false }, - async (context, req, res) => { - const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); - return res.ok({ body: `Elasticsearch: ${response}` }); - } - ); + + // Register server side APIs + defineRoutes(router); return { config$, }; } - public start(core: CoreStart, deps: Record) { - this.log.debug(`Starting security plugin`); + public start(core: CoreStart) { + this.logger.debug('opendistro_security: Started'); + return {}; } - public stop() { - this.log.debug(`Stopping security plugin`); - } + public stop() {} } diff --git a/server/routes/index.ts b/server/routes/index.ts new file mode 100644 index 000000000..2e8d66ac6 --- /dev/null +++ b/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/opendistro_security/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 000000000..6e1091692 --- /dev/null +++ b/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OpendistroSecurityPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OpendistroSecurityPluginStart {} From 2ba32c5cfe8a85facab2117bd3cb6f08b3d16be9 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Tue, 11 Feb 2020 12:04:24 -0800 Subject: [PATCH 05/21] add configs --- kibana.json | 1 + public/application.tsx | 19 ++++--- public/plugin.ts | 6 +- server/index.ts | 121 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/kibana.json b/kibana.json index 43727a98e..53f266c19 100644 --- a/kibana.json +++ b/kibana.json @@ -3,6 +3,7 @@ "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 index 8f2912e0a..67bab1599 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -1,22 +1,27 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountContext } from '../../../src/core/public'; +import { AppMountContext, AppMountParameters, CoreStart } from '../../../src/core/public'; import SecurityManagementApp from './components/security-management-app'; +import { AppPluginStartDependencies } from '../../np_demo/public/types'; -export function renderApp(element: HTMLElement, +export function renderApp( + // element: HTMLElement, + { notifications, http }: CoreStart, + { navigation }: AppPluginStartDependencies, appMountContext: AppMountContext, - basePath: string + params: AppMountParameters + // basePath: string ) { setBreadcrumbs(appMountContext); ReactDOM.render( , - element); - return () => ReactDOM.unmountComponentAtNode(element); + params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); } function setBreadcrumbs(appMountContext: AppMountContext) { diff --git a/public/plugin.ts b/public/plugin.ts index 991ae5ba4..af5405ad5 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -11,16 +11,16 @@ export class OpendistroSecurityPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} - - public setup(core: CoreSetup): OpendistroSecurityPluginSetup { + 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'); - return renderApp(params.element, context, params.appBasePath); + const [coreStart, depsStart] = await core.getStartServices(); + return renderApp(coreStart, depsStart as AppPluginStartDependencies, context, params); } }); diff --git a/server/index.ts b/server/index.ts index 33e1777ed..253f7f645 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,125 @@ -import { PluginInitializerContext } from '../../../src/core/server'; +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.maybe(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, + }, + schema: configSchema, +}; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. From 3dd8b9f31c5f55480f1cb19bcd6cdcdee7f2294d Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Wed, 12 Feb 2020 10:49:25 -0800 Subject: [PATCH 06/21] try expose config to browser side --- public/plugin.ts | 1 + server/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/public/plugin.ts b/public/plugin.ts index af5405ad5..133db1308 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -13,6 +13,7 @@ export class OpendistroSecurityPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): OpendistroSecurityPluginSetup { + console.log(this.initializerContext.config.get()); core.application.register({ id: "opendistro_security", title: "Security", diff --git a/server/index.ts b/server/index.ts index 253f7f645..6e242ef9e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -116,6 +116,7 @@ export type SecurityPluginConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { cookie: true, + auth: true, }, schema: configSchema, }; From bbda3119c2309296fba951f091b1f36b84dfe066 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Wed, 12 Feb 2020 11:00:51 -0800 Subject: [PATCH 07/21] commit missing change --- public/application.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/application.tsx b/public/application.tsx index 67bab1599..6ebbc9e20 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -5,7 +5,6 @@ import SecurityManagementApp from './components/security-management-app'; import { AppPluginStartDependencies } from '../../np_demo/public/types'; export function renderApp( - // element: HTMLElement, { notifications, http }: CoreStart, { navigation }: AppPluginStartDependencies, appMountContext: AppMountContext, From 0c403fbc29c60a61c3ddb5502cbb272679f59092 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Tue, 25 Feb 2020 12:49:39 -0800 Subject: [PATCH 08/21] Added entity CRUD APIs --- lib/auth/types/AuthType.ts | 10 - public/plugin.ts | 1 - server/auth/dummy_authentication_handler.ts | 6 + server/auth/errors/authentication_error.ts | 6 + server/auth/errors/invalid_session_error.ts | 6 + server/auth/errors/missing_role_error.ts | 8 + server/auth/errors/missing_tenant_error.ts | 8 + server/auth/errors/session_expired_error.ts | 7 + server/auth/types/AuthType.ts | 37 +++ {lib => server}/auth/user.ts | 0 .../errors/wrap_elasticsearch_error.ts | 36 +++ ...pendistro_security_configuration_plugin.ts | 176 ++++++++++ server/plugin.ts | 37 ++- server/routes/index.ts | 303 +++++++++++++++++- 14 files changed, 623 insertions(+), 18 deletions(-) delete mode 100644 lib/auth/types/AuthType.ts create mode 100644 server/auth/dummy_authentication_handler.ts create mode 100644 server/auth/errors/authentication_error.ts create mode 100644 server/auth/errors/invalid_session_error.ts create mode 100644 server/auth/errors/missing_role_error.ts create mode 100644 server/auth/errors/missing_tenant_error.ts create mode 100644 server/auth/errors/session_expired_error.ts create mode 100644 server/auth/types/AuthType.ts rename {lib => server}/auth/user.ts (100%) create mode 100644 server/backend/errors/wrap_elasticsearch_error.ts create mode 100644 server/backend/opendistro_security_configuration_plugin.ts diff --git a/lib/auth/types/AuthType.ts b/lib/auth/types/AuthType.ts deleted file mode 100644 index 121417c36..000000000 --- a/lib/auth/types/AuthType.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default class AuthType { - /* - private const APP_ROOT: string; - private const API_ROOT: string; - private pluginRoot: string; - private config: SecurityPluginConfigType; - private basePath: string; - private unauthenticatedRoutes: Array; - */ -} \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 133db1308..13bebf1ad 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -5,7 +5,6 @@ import { OpendistroSecurityPluginStart, AppPluginStartDependencies, } from './types'; -import { PLUGIN_NAME } from '../common'; export class OpendistroSecurityPlugin implements Plugin { diff --git a/server/auth/dummy_authentication_handler.ts b/server/auth/dummy_authentication_handler.ts new file mode 100644 index 000000000..ae3e3f67b --- /dev/null +++ b/server/auth/dummy_authentication_handler.ts @@ -0,0 +1,6 @@ +import { AuthenticationHandler, KibanaRequest, LifecycleResponseFactory, AuthToolkit } from "../../../../src/core/server"; + +export function dummyAuthHandler(request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit): AuthenticationHandler { + + return ; +}; diff --git a/server/auth/errors/authentication_error.ts b/server/auth/errors/authentication_error.ts new file mode 100644 index 000000000..84ecb43d6 --- /dev/null +++ b/server/auth/errors/authentication_error.ts @@ -0,0 +1,6 @@ +export default class AuthenticationError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/server/auth/errors/invalid_session_error.ts b/server/auth/errors/invalid_session_error.ts new file mode 100644 index 000000000..67cbec865 --- /dev/null +++ b/server/auth/errors/invalid_session_error.ts @@ -0,0 +1,6 @@ +export default class InvalidSessionError extends Error { + constructor(message: string, public inner: Error) { // TODO: should inner be public? + super(message); + this.name = this.constructor.name; + } +} \ No newline at end of file diff --git a/server/auth/errors/missing_role_error.ts b/server/auth/errors/missing_role_error.ts new file mode 100644 index 000000000..ca21abd52 --- /dev/null +++ b/server/auth/errors/missing_role_error.ts @@ -0,0 +1,8 @@ +export default class MissingRoleError extends Error { + + constructor(message) { + super(message); + this.name = this.constructor.name; + } + +} \ No newline at end of file diff --git a/server/auth/errors/missing_tenant_error.ts b/server/auth/errors/missing_tenant_error.ts new file mode 100644 index 000000000..61e020812 --- /dev/null +++ b/server/auth/errors/missing_tenant_error.ts @@ -0,0 +1,8 @@ +export default class MissingTenantError extends Error { + + constructor(message) { + super(message); + this.name = this.constructor.name; + } + +} \ No newline at end of file diff --git a/server/auth/errors/session_expired_error.ts b/server/auth/errors/session_expired_error.ts new file mode 100644 index 000000000..57dde3273 --- /dev/null +++ b/server/auth/errors/session_expired_error.ts @@ -0,0 +1,7 @@ +export default class SessionExpiredError extends Error { + + constructor(message, public inner: Error) { + super(message); + this.name = this.constructor.name; + } +} \ No newline at end of file diff --git a/server/auth/types/AuthType.ts b/server/auth/types/AuthType.ts new file mode 100644 index 000000000..2bcbb05fd --- /dev/null +++ b/server/auth/types/AuthType.ts @@ -0,0 +1,37 @@ +import { CoreSetup } from "../../../../../src/core/server"; +import { SecurityPluginConfigType } from "../.."; + +export default class AuthType { // TODO maybe replace the logic here with AuthenticationHandler + /* + private const APP_ROOT: string; + private const API_ROOT: string; + private pluginRoot: string; + private basePath: string; + */ + private unauthenticatedRoutes: Array; + private routesToIgnore: Array + private sessionTTL: number; + private sessionKeepAlive: boolean; + private type: string; + private validateAvailableTenantes: boolean = true; + private authHeaderNaem: string = 'authorization'; + private allowedAdditionalAuthHeaders: Array = ['security_impersonate_as']; + + constructor(private core: CoreSetup, private config: SecurityPluginConfigType) { + this.routesToIgnore = [ + '/bundles/app/security-login/bootstrap.js', + '/bundles/app/security-customerror/bootstrap.js' + ]; + this.sessionTTL = this.config.session.ttl; + this.sessionKeepAlive = this.config.session.keepalive; + this.unauthenticatedRoutes = this.config.auth.unauthenticated_routes; + } + + async init() { + this.setupStorage(); + } + + private setupStorage = () => void { + + } +} \ No newline at end of file diff --git a/lib/auth/user.ts b/server/auth/user.ts similarity index 100% rename from lib/auth/user.ts rename to server/auth/user.ts diff --git a/server/backend/errors/wrap_elasticsearch_error.ts b/server/backend/errors/wrap_elasticsearch_error.ts new file mode 100644 index 000000000..a03d85674 --- /dev/null +++ b/server/backend/errors/wrap_elasticsearch_error.ts @@ -0,0 +1,36 @@ +import { get } from 'lodash'; +import Boom from 'boom'; +import AuthenticationError from "../../../auth/errors/authentication_error"; + +export default function wrapElasticsearchError(error) { + + let statusCode = error.statusCode; + + if (error.status) { + statusCode = error.status; + } + + if (!statusCode) { + statusCode = 500; + } + + let message: string = get(error, 'body.message'); + if (!message) { + message = error.message; + } + + const wwwAuthHeader: string = 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(`${message}`); + } + + return Boom.boomify(error, { statusCode: statusCode, message: 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..40cfe078d --- /dev/null +++ b/server/backend/opendistro_security_configuration_plugin.ts @@ -0,0 +1,176 @@ +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 + } + } + } + }); +} \ No newline at end of file diff --git a/server/plugin.ts b/server/plugin.ts index 1aeac94d5..826b9a86c 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -4,32 +4,58 @@ import { CoreStart, Plugin, Logger, + IClusterClient, + KibanaRequest, + LifecycleResponseFactory, + OnPreAuthToolkit, + 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'; export class OpendistroSecurityPlugin - implements Plugin { + implements Plugin { private readonly logger: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public async setup(core: CoreSetup) { this.logger.debug('opendistro_security: Setup'); - + const config$ = this.initializerContext.config.create(); + const securityConfigClient: IClusterClient = core.elasticsearch.createClient( + 'opendistro_security', + { + plugins: [opendistro_security_configuratoin_plugin] + } + ); + + // cookie session handling + const dummyCookieSessionStorageFactory: SessionStorageFactory = await core.http.createCookieSessionStorageFactory({ + name: 'cookie_name', + encryptionKey: 'abcdefghijklmnopqrstuvwxyz0123456789', + validate: (sessionValue) => { + // console.log(`sessionValue: ${sessionValue}`); + return { isValid: true, path: '/' }; + // return { isValid: false }; + }, + isSecure: false, + }); + const router = core.http.createRouter(); // Register server side APIs - defineRoutes(router); + defineRoutes(router, securityConfigClient, dummyCookieSessionStorageFactory); return { config$, + securityConfigClient, }; } @@ -38,5 +64,6 @@ export class OpendistroSecurityPlugin return {}; } - public stop() {} + public stop() { } + } diff --git a/server/routes/index.ts b/server/routes/index.ts index 2e8d66ac6..b268ee5a2 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,6 +1,60 @@ -import { IRouter } from '../../../../src/core/server'; +import { IRouter, IClusterClient, SessionStorageFactory } from '../../../../src/core/server'; +import { schema } from '@kbn/config-schema'; + +export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient, dummyCookieSessionStorageFactory: SessionStorageFactory) { + 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 + } -export function defineRoutes(router: IRouter) { router.get( { path: '/api/opendistro_security/example', @@ -14,4 +68,249 @@ export function defineRoutes(router: IRouter) { }); } ); + 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, + }); + } + ); + router.get( + { + path: '/api/test_call_as_internal_user', + validate: false, + }, + async (context, request, response) => { + let catNodeResponse = {}; + try { + catNodeResponse = await context.core.elasticsearch.adminClient.callAsInternalUser('cat.nodes', { format: 'json' }); + } catch (err) { + catNodeResponse = err; + } + return response.ok({ + body: { + time: new Date().toISOString(), + nodes: catNodeResponse + }, + }); + } + ); + router.get( + { + path: '/api/test_call_as_current_user', + validate: false, + }, + async (context, request, response) => { + let esResponse = {}; + try { + esResponse = await context.core.elasticsearch.dataClient.callAsCurrentUser('search'); + } catch (err) { + esResponse = err + } + return response.ok({ + body: { + data: esResponse, + } + }); + }, + ); + router.get( + { + path: '/api/test_params/{param1}', + validate: { + params: schema.object({ + param1: schema.string({ defaultValue: undefined }), + }), + } + }, + async (context, request, response) => { + request.params.param1 + return response.ok({ + body: { + data: request.params.param1, + }, + }); + }, + ); + router.get( + { + path: `${API_PREFIX}/configuration/{resourceName}`, + validate: { + params: schema.object({ + resourceName: schema.string(), + }), + }, + }, + async (context, request, response) => { + /* + let cookieValue = await dummyCookieSessionStorageFactory.asScoped(request).get(); + console.log(`cookie value: ${cookieValue}`); + dummyCookieSessionStorageFactory.asScoped(request).clear(); + dummyCookieSessionStorageFactory.asScoped(request).set({dummyKey: 'dummy_value'}); + */ + 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, + }); + } + }, + ); + + 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, + }); + } + }, + ); + + 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, + }); + } + } + ); + + 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, + }); + } + } + ); + + 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.post( + { + path: `${API_PREFIX}/configuration/validate`, + validate: { + body: schema.any(), + // body: internalUserSchema, + }, + }, + async (context, request, response) => { + let validateOutput; + try { + validateOutput = internalUserSchema.validate(request.body); + } catch (error) { + console.log(`${error}`); + return response.badRequest({ body: error }); + } + return response.ok({ body: validateOutput }); + }, + ); } From b7d7cb547f47de27d0e545db27c14a594b6f9d43 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Wed, 1 Apr 2020 16:38:54 -0700 Subject: [PATCH 09/21] preliminary basic auth --- .gitignore | 1 + public/login.tsx | 15 + public/plugin.ts | 12 +- server/auth/{types => }/AuthType.ts | 4 +- server/auth/dummy_authentication_handler.ts | 6 - server/auth/types/basic/basic_auth.ts | 101 +++++++ server/auth/types/basic/routes.ts | 256 ++++++++++++++++++ server/auth/user.ts | 15 +- .../errors/wrap_elasticsearch_error.ts | 2 +- server/backend/opendistro_security_client.ts | 55 ++++ ...pendistro_security_configuration_plugin.ts | 28 ++ server/backend/opendistro_security_plugin.ts | 33 +++ server/index.ts | 4 +- server/plugin.ts | 53 ++-- server/routes/index.ts | 37 ++- server/routes/test_routes.ts | 55 ++++ server/session/security_cookie.ts | 37 +++ server/utils/filter_auth_headers.ts | 18 ++ 18 files changed, 681 insertions(+), 51 deletions(-) create mode 100644 public/login.tsx rename server/auth/{types => }/AuthType.ts (90%) delete mode 100644 server/auth/dummy_authentication_handler.ts create mode 100644 server/auth/types/basic/basic_auth.ts create mode 100644 server/auth/types/basic/routes.ts create mode 100644 server/backend/opendistro_security_client.ts create mode 100644 server/backend/opendistro_security_plugin.ts create mode 100644 server/routes/test_routes.ts create mode 100644 server/session/security_cookie.ts create mode 100644 server/utils/filter_auth_headers.ts diff --git a/.gitignore b/.gitignore index 521e1c729..8b8cc022b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ npm-debug.log* node_modules /build/ /public/app.css +target diff --git a/public/login.tsx b/public/login.tsx new file mode 100644 index 000000000..7a695cf7b --- /dev/null +++ b/public/login.tsx @@ -0,0 +1,15 @@ +import { AppMountParameters } from "../../../src/core/public"; +import ReactDOM from "react-dom"; + +export function renderApp( + params: AppMountParameters + // basePath: string +) { + + ReactDOM.render( +
+ login page +
, + params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); +} \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 13bebf1ad..db46af732 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -12,7 +12,6 @@ export class OpendistroSecurityPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): OpendistroSecurityPluginSetup { - console.log(this.initializerContext.config.get()); core.application.register({ id: "opendistro_security", title: "Security", @@ -24,6 +23,17 @@ export class OpendistroSecurityPlugin } }); + core.application.register({ + id: 'opendistro_login', + title: 'Login', + chromeless: true, + // appRoute: `/app/login`, + mount: async (params: AppMountParameters) => { + const { renderApp } = await import('./login'); + return renderApp(params); + }, + }); + // Return methods that should be available to other plugins return { }; diff --git a/server/auth/types/AuthType.ts b/server/auth/AuthType.ts similarity index 90% rename from server/auth/types/AuthType.ts rename to server/auth/AuthType.ts index 2bcbb05fd..7cd050e40 100644 --- a/server/auth/types/AuthType.ts +++ b/server/auth/AuthType.ts @@ -1,5 +1,5 @@ -import { CoreSetup } from "../../../../../src/core/server"; -import { SecurityPluginConfigType } from "../.."; +import { CoreSetup } from "../../../../src/core/server"; +import { SecurityPluginConfigType } from ".."; export default class AuthType { // TODO maybe replace the logic here with AuthenticationHandler /* diff --git a/server/auth/dummy_authentication_handler.ts b/server/auth/dummy_authentication_handler.ts deleted file mode 100644 index ae3e3f67b..000000000 --- a/server/auth/dummy_authentication_handler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AuthenticationHandler, KibanaRequest, LifecycleResponseFactory, AuthToolkit } from "../../../../src/core/server"; - -export function dummyAuthHandler(request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit): AuthenticationHandler { - - return ; -}; diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts new file mode 100644 index 000000000..2b34cbcff --- /dev/null +++ b/server/auth/types/basic/basic_auth.ts @@ -0,0 +1,101 @@ +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"; +import { LoggerAdapter } from "../../../../../../src/core/server/logging/logger_adapter"; + +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..9c8c6a21b --- /dev/null +++ b/server/auth/types/basic/routes.ts @@ -0,0 +1,256 @@ +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 index ea80387ea..b8c3c83bb 100644 --- a/server/auth/user.ts +++ b/server/auth/user.ts @@ -4,11 +4,11 @@ export class User { readonly backendRoles: Array; readonly tenants: Array; readonly selectedTenant: string; - readonly credentials: Credentials; - readonly proxyCredentials: Credentials; + readonly credentials: any; + readonly proxyCredentials: any; constructor(username: string, roles: Array, backendRoles: Array, tenants: Array, - selectedTenant: string, credentials: Credentials, proxyCredentials: Credentials) { + selectedTenant: string, credentials: any = undefined, proxyCredentials: any = undefined) { this.username = username; this.roles = roles; this.backendRoles = backendRoles; @@ -19,12 +19,3 @@ export class User { } } -export class Credentials { - readonly username: string; - readonly password: string; - - constructor(username: string, password: string) { - this.username = username; - this.password = password; - } -} diff --git a/server/backend/errors/wrap_elasticsearch_error.ts b/server/backend/errors/wrap_elasticsearch_error.ts index a03d85674..627acad52 100644 --- a/server/backend/errors/wrap_elasticsearch_error.ts +++ b/server/backend/errors/wrap_elasticsearch_error.ts @@ -1,6 +1,6 @@ import { get } from 'lodash'; import Boom from 'boom'; -import AuthenticationError from "../../../auth/errors/authentication_error"; +import AuthenticationError from "../../auth/errors/authentication_error"; export default function wrapElasticsearchError(error) { diff --git a/server/backend/opendistro_security_client.ts b/server/backend/opendistro_security_client.ts new file mode 100644 index 000000000..fcf522b15 --- /dev/null +++ b/server/backend/opendistro_security_client.ts @@ -0,0 +1,55 @@ +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 index 40cfe078d..0b4a10d4e 100644 --- a/server/backend/opendistro_security_configuration_plugin.ts +++ b/server/backend/opendistro_security_configuration_plugin.ts @@ -173,4 +173,32 @@ export default function (Client: any, config: any, components: any) { } } }); + + + ///// + 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..00a603769 --- /dev/null +++ b/server/backend/opendistro_security_plugin.ts @@ -0,0 +1,33 @@ +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 index 6e242ef9e..1455436c0 100644 --- a/server/index.ts +++ b/server/index.ts @@ -44,7 +44,7 @@ export const configSchema = schema.object({ forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }), logout_url: schema.string({ defaultValue: '' }), }), - basicauth: schema.maybe(schema.object({ + basicauth: schema.object({ enabled: schema.boolean({ defaultValue: true }), unauthenticated_routes: schema.arrayOf(schema.string(), { defaultValue: ["/api/status"] }), forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }), @@ -64,7 +64,7 @@ export const configSchema = schema.object({ 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 }), diff --git a/server/plugin.ts b/server/plugin.ts index 826b9a86c..fd6c2f65a 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -5,16 +5,23 @@ import { Plugin, Logger, IClusterClient, + SessionStorageFactory, KibanaRequest, LifecycleResponseFactory, OnPreAuthToolkit, - 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'; +import { r } from 'tar'; + export class OpendistroSecurityPlugin implements Plugin { @@ -28,34 +35,44 @@ export class OpendistroSecurityPlugin 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 securityConfigClient: IClusterClient = core.elasticsearch.createClient( + const securityClient: IClusterClient = core.elasticsearch.createClient( 'opendistro_security', { - plugins: [opendistro_security_configuratoin_plugin] + 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, + ], } ); - // cookie session handling - const dummyCookieSessionStorageFactory: SessionStorageFactory = await core.http.createCookieSessionStorageFactory({ - name: 'cookie_name', - encryptionKey: 'abcdefghijklmnopqrstuvwxyz0123456789', - validate: (sessionValue) => { - // console.log(`sessionValue: ${sessionValue}`); - return { isValid: true, path: '/' }; - // return { isValid: false }; - }, - isSecure: false, - }); - - const router = core.http.createRouter(); + const securitySessionStorageFactory: SessionStorageFactory + = await core.http.createCookieSessionStorageFactory(getSecurityCookieOptions(config)); + // Register server side APIs - defineRoutes(router, securityConfigClient, dummyCookieSessionStorageFactory); + 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, + securityConfigClient: securityClient, }; } diff --git a/server/routes/index.ts b/server/routes/index.ts index b268ee5a2..c54b4f9f6 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,7 +1,7 @@ -import { IRouter, IClusterClient, SessionStorageFactory } from '../../../../src/core/server'; +import { IRouter, IClusterClient } from '../../../../src/core/server'; import { schema } from '@kbn/config-schema'; -export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient, dummyCookieSessionStorageFactory: SessionStorageFactory) { +export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient) { const API_PREFIX: string = '/api/v1/opendistro_security'; const internalUserSchema = schema.object({ @@ -147,12 +147,6 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie }, }, async (context, request, response) => { - /* - let cookieValue = await dummyCookieSessionStorageFactory.asScoped(request).get(); - console.log(`cookie value: ${cookieValue}`); - dummyCookieSessionStorageFactory.asScoped(request).clear(); - dummyCookieSessionStorageFactory.asScoped(request).set({dummyKey: 'dummy_value'}); - */ const client = securityConfigClient.asScoped(request); let esResp; try { @@ -294,6 +288,7 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie } } ); + // test endpoint, DELETE it router.post( { path: `${API_PREFIX}/configuration/validate`, @@ -307,10 +302,34 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie try { validateOutput = internalUserSchema.validate(request.body); } catch (error) { - console.log(`${error}`); return response.badRequest({ body: error }); } return response.ok({ body: validateOutput }); }, ); + + 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..c0c5c5c8b --- /dev/null +++ b/server/routes/test_routes.ts @@ -0,0 +1,55 @@ +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..c9d074bf2 --- /dev/null +++ b/server/session/security_cookie.ts @@ -0,0 +1,37 @@ +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/utils/filter_auth_headers.ts b/server/utils/filter_auth_headers.ts new file mode 100644 index 000000000..01255c201 --- /dev/null +++ b/server/utils/filter_auth_headers.ts @@ -0,0 +1,18 @@ + +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); +} From a9dd35e69abf7cf15fceb9db11306c5de52b64a6 Mon Sep 17 00:00:00 2001 From: Yan Zeng <46499415+zengyan-amazon@users.noreply.github.com> Date: Tue, 14 Jan 2020 14:02:55 -0800 Subject: [PATCH 10/21] Initial commit --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 2 files changed, 203 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 000000000..61658c6d6 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# security-kibana-plugin +private repo for Elasticsearch security kibana plugin development From 1f5f3a1901c266e347e65c608ccde6bb595ab7d4 Mon Sep 17 00:00:00 2001 From: Zeng Date: Tue, 28 Jan 2020 11:04:17 -0800 Subject: [PATCH 11/21] Add UI skeleton --- .gitignore | 4 ++ kibana.json | 8 +++ public/index.ts | 6 ++ public/plugin.ts | 39 ++++++++++++ .../roles/role-management-app.tsx | 10 +++ .../security-management-app.tsx | 63 +++++++++++++++++++ .../security-management.tsx | 30 +++++++++ 7 files changed, 160 insertions(+) create mode 100644 kibana.json create mode 100644 public/index.ts create mode 100644 public/plugin.ts create mode 100644 public/security-management/roles/role-management-app.tsx create mode 100644 public/security-management/security-management-app.tsx create mode 100644 public/security-management/security-management.tsx diff --git a/.gitignore b/.gitignore index e69de29bb..521e1c729 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,4 @@ +npm-debug.log* +node_modules +/build/ +/public/app.css diff --git a/kibana.json b/kibana.json new file mode 100644 index 000000000..da96bf1de --- /dev/null +++ b/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "security", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["opendistro_security"], + "server": false, + "ui": true +} diff --git a/public/index.ts b/public/index.ts new file mode 100644 index 000000000..dd3408be7 --- /dev/null +++ b/public/index.ts @@ -0,0 +1,6 @@ +import { PluginInitializer, PluginInitializerContext} from 'kibana/public'; +import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; + +export const plugin: PluginInitializer = ( + initializerContext: PluginInitializerContext +) => new SecurityPlugin(initializerContext); diff --git a/public/plugin.ts b/public/plugin.ts new file mode 100644 index 000000000..b477cf7cf --- /dev/null +++ b/public/plugin.ts @@ -0,0 +1,39 @@ +import { + Plugin, + CoreSetup, + CoreStart, + PluginInitializerContext, + AppMountContext, + AppMountParameters +} from 'kibana/public'; + + +export class SecurityPlugin implements Plugin { + + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup, deps: {}) { + + core.application.register({ + id: "security_management_app", + title: "Security", + order: 1, + mount: async (context: AppMountContext, params: AppMountParameters) => { + const { renderApp } = await import('./security-management/security-management'); + return renderApp(params.element, context, params.appBasePath); + } + }); + + return {}; + } + + public start(core: CoreStart) { + // eslint-disable-next-line no-console + console.log(`Security plugin started`); + } + + public stop() {} +} + +export type SecurityPluginSetup = ReturnType; +export type SecurityPluginStart = ReturnType; diff --git a/public/security-management/roles/role-management-app.tsx b/public/security-management/roles/role-management-app.tsx new file mode 100644 index 000000000..585183194 --- /dev/null +++ b/public/security-management/roles/role-management-app.tsx @@ -0,0 +1,10 @@ +import React, { Component } from 'react'; + +export class RoleManagement extends Component { + render() { + return (
+ RoleManagement page +
); + } +} + diff --git a/public/security-management/security-management-app.tsx b/public/security-management/security-management-app.tsx new file mode 100644 index 000000000..542c85315 --- /dev/null +++ b/public/security-management/security-management-app.tsx @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; +import { AppMountContext } from 'kibana/public'; +import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import { RoleManagement } from './roles/role-management-app'; + +class SecurityManagementAppProps { + element!: HTMLElement; + appMountContext!: AppMountContext; + basePath!: string; +} +class SecurityManagementAppState { + selectedTab?: EuiTabbedContentTab; +} + +class SecurityManagementApp extends Component { + private tabs: EuiTabbedContentTab[] = [ + { + id: 'roles', + name: 'Roles', + content: () + }, + { + id: 'action_groups', + name: 'Action Groups', + content: (
bbb
) + }, + { + id: 'internal_user_database', + name: 'Internal User Database', + content: (
ccc
) + } + ]; + + constructor(props: SecurityManagementAppProps) { + super(props); + this.state = {}; + } + + componentDidMount() { + this.setState( { + selectedTab: this.tabs[0], + } ); + } + + render() { + return
+ + + +
+ } + + onTabClick = (tab: EuiTabbedContentTab) => { + this.setState({ selectedTab: tab }); + }; +} + +export default SecurityManagementApp; \ No newline at end of file diff --git a/public/security-management/security-management.tsx b/public/security-management/security-management.tsx new file mode 100644 index 000000000..75a3f20dd --- /dev/null +++ b/public/security-management/security-management.tsx @@ -0,0 +1,30 @@ +import { AppMountContext } from 'kibana/public'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import SecurityManagementApp from './security-management-app' + +export function renderApp(element: HTMLElement, + appMountContext: AppMountContext, + basePath: string +) { + console.log("in renderApp"); + setBreadcrumbs(appMountContext); + + ReactDOM.render( + , + element); + return () => ReactDOM.unmountComponentAtNode(element); +} + +function setBreadcrumbs(appMountContext: AppMountContext) { + appMountContext.core.chrome.setBreadcrumbs([ + { + text: "Security Management", + href: '', + } + ]); +} From d59fde4ece7c79f8e6cb700ab244bd3f21f7c969 Mon Sep 17 00:00:00 2001 From: Zeng Date: Tue, 28 Jan 2020 11:05:22 -0800 Subject: [PATCH 12/21] Add server plugin --- lib/auth/types/AuthType.ts | 10 +++++++++ lib/auth/user.ts | 30 +++++++++++++++++++++++++++ server/index.ts | 41 +++++++++++++++++++++++++++++++++++++ server/plugin.ts | 42 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 lib/auth/types/AuthType.ts create mode 100644 lib/auth/user.ts create mode 100644 server/index.ts create mode 100644 server/plugin.ts diff --git a/lib/auth/types/AuthType.ts b/lib/auth/types/AuthType.ts new file mode 100644 index 000000000..7b241a92f --- /dev/null +++ b/lib/auth/types/AuthType.ts @@ -0,0 +1,10 @@ +import { SecurityPluginConfigType } from 'src/plugins/security-kibana-plugin/server'; + +export default class AuthType { + private const APP_ROOT: string; + private const API_ROOT: string; + private pluginRoot: string; + private config: SecurityPluginConfigType; + private basePath: string; + private unauthenticatedRoutes: Array; +} \ No newline at end of file diff --git a/lib/auth/user.ts b/lib/auth/user.ts new file mode 100644 index 000000000..ea80387ea --- /dev/null +++ b/lib/auth/user.ts @@ -0,0 +1,30 @@ +export class User { + readonly username: string; + readonly roles: Array; + readonly backendRoles: Array; + readonly tenants: Array; + readonly selectedTenant: string; + readonly credentials: Credentials; + readonly proxyCredentials: Credentials; + + constructor(username: string, roles: Array, backendRoles: Array, tenants: Array, + selectedTenant: string, credentials: Credentials, proxyCredentials: Credentials) { + this.username = username; + this.roles = roles; + this.backendRoles = backendRoles; + this.tenants = tenants; + this.selectedTenant = selectedTenant; + this.credentials = credentials; + this.proxyCredentials = proxyCredentials; + } +} + +export class Credentials { + readonly username: string; + readonly password: string; + + constructor(username: string, password: string) { + this.username = username; + this.password = password; + } +} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 000000000..e72cee8cb --- /dev/null +++ b/server/index.ts @@ -0,0 +1,41 @@ +import { schema, TypeOf } from '@kbn/config-schema'; + +import { + PluginInitializerContext, + PluginConfigDescriptor, +} from 'kibana/server'; +import SecurityPlugin from './plugin'; + +export const configSchema = schema.object({ + cookie: schema.object({ + secure: schema.boolean({ defaultValue: false }), + ttl: schema.number({ defaultValue: 3600 }), + password: schema.string(), + }), + multitenancy: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + tenants: schema.object({ + preferred: schema.arrayOf(schema.string(), {defaultValue: [] }), + }), + }), + readonly_mode: schema.object({ + roles: schema.arrayOf(schema.string(), { defaultValue: [] }), + }), + auth: schema.object({ + unauthenticated_routes: schema.arrayOf(schema.string()), + }), +}); + +export type SecurityPluginConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + cookie: true, + }, + schema: configSchema, +}; + + + +export const plugin = (initializerContext: PluginInitializerContext) => + new SecurityPlugin(initializerContext); diff --git a/server/plugin.ts b/server/plugin.ts new file mode 100644 index 000000000..66af5a056 --- /dev/null +++ b/server/plugin.ts @@ -0,0 +1,42 @@ +import { map } from 'rxjs/operators'; +import { + CoreSetup, + CoreStart, + Logger, + PluginInitializerContext, + PluginName, +} from 'kibana/server'; +import { SecurityPluginConfigType } from './'; + +export default class SecurityPlugin { + private readonly log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = this.initializerContext.logger.get(); + } + + public setup(core: CoreSetup, deps: Record) { + const config$ = this.initializerContext.config.create(); + + const router = core.http.createRouter(); + router.get( + { path: '/test/authenticate', validate: false }, + async (context, req, res) => { + const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); + return res.ok({ body: `Elasticsearch: ${response}` }); + } + ); + + return { + config$, + }; + } + + public start(core: CoreStart, deps: Record) { + this.log.debug(`Starting security plugin`); + } + + public stop() { + this.log.debug(`Stopping security plugin`); + } +} From 9e39d93e95ea05acc70aa6209039bca81d9dd419 Mon Sep 17 00:00:00 2001 From: Zeng Date: Mon, 10 Feb 2020 14:13:44 -0800 Subject: [PATCH 13/21] Merge in files geneated from plugin generator --- .eslintrc.js | 7 +++ common/index.ts | 2 + kibana.json | 4 +- lib/auth/types/AuthType.ts | 4 +- ...ecurity-management.tsx => application.tsx} | 11 ++--- .../security-management-app.tsx | 4 +- .../roles/role-management-app.tsx | 0 public/index.scss | 0 public/index.ts | 15 ++++-- public/plugin.ts | 38 ++++++++------- public/types.ts | 9 ++++ server/index.ts | 46 ++++--------------- server/plugin.ts | 42 ++++++++--------- server/routes/index.ts | 17 +++++++ server/types.ts | 4 ++ 15 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 .eslintrc.js create mode 100644 common/index.ts rename public/{security-management/security-management.tsx => application.tsx} (75%) rename public/{security-management => components}/security-management-app.tsx (90%) rename public/{ => components}/security-management/roles/role-management-app.tsx (100%) create mode 100644 public/index.scss create mode 100644 public/types.ts create mode 100644 server/routes/index.ts create mode 100644 server/types.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..ff4759cf3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,7 @@ +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/common/index.ts b/common/index.ts new file mode 100644 index 000000000..c5f3b0ce3 --- /dev/null +++ b/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'opendistroSecurity'; +export const PLUGIN_NAME = 'opendistro_security'; diff --git a/kibana.json b/kibana.json index da96bf1de..43727a98e 100644 --- a/kibana.json +++ b/kibana.json @@ -1,8 +1,8 @@ { "id": "security", "version": "0.0.1", - "kibanaVersion": "kibana", + "kibanaVersion": "8.0.0", "configPath": ["opendistro_security"], - "server": false, + "server": true, "ui": true } diff --git a/lib/auth/types/AuthType.ts b/lib/auth/types/AuthType.ts index 7b241a92f..121417c36 100644 --- a/lib/auth/types/AuthType.ts +++ b/lib/auth/types/AuthType.ts @@ -1,10 +1,10 @@ -import { SecurityPluginConfigType } from 'src/plugins/security-kibana-plugin/server'; - export default class AuthType { + /* private const APP_ROOT: string; private const API_ROOT: string; private pluginRoot: string; private config: SecurityPluginConfigType; private basePath: string; private unauthenticatedRoutes: Array; + */ } \ No newline at end of file diff --git a/public/security-management/security-management.tsx b/public/application.tsx similarity index 75% rename from public/security-management/security-management.tsx rename to public/application.tsx index 75a3f20dd..8f2912e0a 100644 --- a/public/security-management/security-management.tsx +++ b/public/application.tsx @@ -1,13 +1,12 @@ -import { AppMountContext } from 'kibana/public'; -import ReactDOM from 'react-dom'; import React from 'react'; -import SecurityManagementApp from './security-management-app' +import ReactDOM from 'react-dom'; +import { AppMountContext } from '../../../src/core/public'; +import SecurityManagementApp from './components/security-management-app'; export function renderApp(element: HTMLElement, appMountContext: AppMountContext, basePath: string ) { - console.log("in renderApp"); setBreadcrumbs(appMountContext); ReactDOM.render( @@ -23,8 +22,8 @@ export function renderApp(element: HTMLElement, function setBreadcrumbs(appMountContext: AppMountContext) { appMountContext.core.chrome.setBreadcrumbs([ { - text: "Security Management", + text: "Security", href: '', } ]); -} +} \ No newline at end of file diff --git a/public/security-management/security-management-app.tsx b/public/components/security-management-app.tsx similarity index 90% rename from public/security-management/security-management-app.tsx rename to public/components/security-management-app.tsx index 542c85315..c4a9fb226 100644 --- a/public/security-management/security-management-app.tsx +++ b/public/components/security-management-app.tsx @@ -1,8 +1,8 @@ import React, { Component } from 'react'; -import { AppMountContext } from 'kibana/public'; import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import { EuiPanel } from '@elastic/eui'; -import { RoleManagement } from './roles/role-management-app'; +import { RoleManagement } from './security-management/roles/role-management-app'; +import { AppMountContext } from '../../../../src/core/public'; class SecurityManagementAppProps { element!: HTMLElement; diff --git a/public/security-management/roles/role-management-app.tsx b/public/components/security-management/roles/role-management-app.tsx similarity index 100% rename from public/security-management/roles/role-management-app.tsx rename to public/components/security-management/roles/role-management-app.tsx 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 index dd3408be7..d44c8a7cd 100644 --- a/public/index.ts +++ b/public/index.ts @@ -1,6 +1,11 @@ -import { PluginInitializer, PluginInitializerContext} from 'kibana/public'; -import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; +import './index.scss'; -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => new SecurityPlugin(initializerContext); +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 index b477cf7cf..991ae5ba4 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -1,39 +1,37 @@ +import { i18n } from '@kbn/i18n'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin, PluginInitializerContext, AppMountContext } from '../../../src/core/public'; import { - Plugin, - CoreSetup, - CoreStart, - PluginInitializerContext, - AppMountContext, - AppMountParameters -} from 'kibana/public'; + OpendistroSecurityPluginSetup, + OpendistroSecurityPluginStart, + AppPluginStartDependencies, +} from './types'; +import { PLUGIN_NAME } from '../common'; - -export class SecurityPlugin implements Plugin { +export class OpendistroSecurityPlugin + implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup): OpendistroSecurityPluginSetup { - public async setup(core: CoreSetup, deps: {}) { - core.application.register({ - id: "security_management_app", + id: "opendistro_security", title: "Security", order: 1, mount: async (context: AppMountContext, params: AppMountParameters) => { - const { renderApp } = await import('./security-management/security-management'); + const { renderApp } = await import('./application'); return renderApp(params.element, context, params.appBasePath); } }); - return {}; + // Return methods that should be available to other plugins + return { + }; } - public start(core: CoreStart) { - // eslint-disable-next-line no-console - console.log(`Security plugin started`); + public start(core: CoreStart): OpendistroSecurityPluginStart { + return {}; } public stop() {} } - -export type SecurityPluginSetup = ReturnType; -export type SecurityPluginStart = ReturnType; diff --git a/public/types.ts b/public/types.ts new file mode 100644 index 000000000..3fae38f0d --- /dev/null +++ b/public/types.ts @@ -0,0 +1,9 @@ +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/index.ts b/server/index.ts index e72cee8cb..33e1777ed 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,41 +1,11 @@ -import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from '../../../src/core/server'; +import { OpendistroSecurityPlugin } from './plugin'; -import { - PluginInitializerContext, - PluginConfigDescriptor, -} from 'kibana/server'; -import SecurityPlugin from './plugin'; +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. -export const configSchema = schema.object({ - cookie: schema.object({ - secure: schema.boolean({ defaultValue: false }), - ttl: schema.number({ defaultValue: 3600 }), - password: schema.string(), - }), - multitenancy: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - tenants: schema.object({ - preferred: schema.arrayOf(schema.string(), {defaultValue: [] }), - }), - }), - readonly_mode: schema.object({ - roles: schema.arrayOf(schema.string(), { defaultValue: [] }), - }), - auth: schema.object({ - unauthenticated_routes: schema.arrayOf(schema.string()), - }), -}); +export function plugin(initializerContext: PluginInitializerContext) { + return new OpendistroSecurityPlugin(initializerContext); +} -export type SecurityPluginConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - cookie: true, - }, - schema: configSchema, -}; - - - -export const plugin = (initializerContext: PluginInitializerContext) => - new SecurityPlugin(initializerContext); +export { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types'; diff --git a/server/plugin.ts b/server/plugin.ts index 66af5a056..1aeac94d5 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -1,42 +1,42 @@ -import { map } from 'rxjs/operators'; import { + PluginInitializerContext, CoreSetup, CoreStart, + Plugin, Logger, - PluginInitializerContext, - PluginName, -} from 'kibana/server'; -import { SecurityPluginConfigType } from './'; +} from '../../../src/core/server'; + +import { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types'; +import { defineRoutes } from './routes'; +import { SecurityPluginConfigType } from '.'; -export default class SecurityPlugin { - private readonly log: Logger; +export class OpendistroSecurityPlugin + implements Plugin { + private readonly logger: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); + this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup, deps: Record) { + public setup(core: CoreSetup) { + this.logger.debug('opendistro_security: Setup'); + const config$ = this.initializerContext.config.create(); const router = core.http.createRouter(); - router.get( - { path: '/test/authenticate', validate: false }, - async (context, req, res) => { - const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); - return res.ok({ body: `Elasticsearch: ${response}` }); - } - ); + + // Register server side APIs + defineRoutes(router); return { config$, }; } - public start(core: CoreStart, deps: Record) { - this.log.debug(`Starting security plugin`); + public start(core: CoreStart) { + this.logger.debug('opendistro_security: Started'); + return {}; } - public stop() { - this.log.debug(`Stopping security plugin`); - } + public stop() {} } diff --git a/server/routes/index.ts b/server/routes/index.ts new file mode 100644 index 000000000..2e8d66ac6 --- /dev/null +++ b/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/opendistro_security/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 000000000..6e1091692 --- /dev/null +++ b/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OpendistroSecurityPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface OpendistroSecurityPluginStart {} From 07e130f76c2e64951836d307fdd586fab2ab9045 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Tue, 11 Feb 2020 12:04:24 -0800 Subject: [PATCH 14/21] add configs --- kibana.json | 1 + public/application.tsx | 19 ++++--- public/plugin.ts | 6 +- server/index.ts | 121 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/kibana.json b/kibana.json index 43727a98e..53f266c19 100644 --- a/kibana.json +++ b/kibana.json @@ -3,6 +3,7 @@ "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 index 8f2912e0a..67bab1599 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -1,22 +1,27 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { AppMountContext } from '../../../src/core/public'; +import { AppMountContext, AppMountParameters, CoreStart } from '../../../src/core/public'; import SecurityManagementApp from './components/security-management-app'; +import { AppPluginStartDependencies } from '../../np_demo/public/types'; -export function renderApp(element: HTMLElement, +export function renderApp( + // element: HTMLElement, + { notifications, http }: CoreStart, + { navigation }: AppPluginStartDependencies, appMountContext: AppMountContext, - basePath: string + params: AppMountParameters + // basePath: string ) { setBreadcrumbs(appMountContext); ReactDOM.render( , - element); - return () => ReactDOM.unmountComponentAtNode(element); + params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); } function setBreadcrumbs(appMountContext: AppMountContext) { diff --git a/public/plugin.ts b/public/plugin.ts index 991ae5ba4..af5405ad5 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -11,16 +11,16 @@ export class OpendistroSecurityPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} - - public setup(core: CoreSetup): OpendistroSecurityPluginSetup { + 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'); - return renderApp(params.element, context, params.appBasePath); + const [coreStart, depsStart] = await core.getStartServices(); + return renderApp(coreStart, depsStart as AppPluginStartDependencies, context, params); } }); diff --git a/server/index.ts b/server/index.ts index 33e1777ed..253f7f645 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,125 @@ -import { PluginInitializerContext } from '../../../src/core/server'; +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.maybe(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, + }, + schema: configSchema, +}; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. From aeaeb4c8b75bf3b4d00631e8544794f96e474f75 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Wed, 12 Feb 2020 10:49:25 -0800 Subject: [PATCH 15/21] try expose config to browser side --- public/plugin.ts | 1 + server/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/public/plugin.ts b/public/plugin.ts index af5405ad5..133db1308 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -13,6 +13,7 @@ export class OpendistroSecurityPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): OpendistroSecurityPluginSetup { + console.log(this.initializerContext.config.get()); core.application.register({ id: "opendistro_security", title: "Security", diff --git a/server/index.ts b/server/index.ts index 253f7f645..6e242ef9e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -116,6 +116,7 @@ export type SecurityPluginConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { cookie: true, + auth: true, }, schema: configSchema, }; From c584f47ceae6f9f3f07e418f6b9c09c4c47c1ea2 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Wed, 12 Feb 2020 11:00:51 -0800 Subject: [PATCH 16/21] commit missing change --- public/application.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/application.tsx b/public/application.tsx index 67bab1599..6ebbc9e20 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -5,7 +5,6 @@ import SecurityManagementApp from './components/security-management-app'; import { AppPluginStartDependencies } from '../../np_demo/public/types'; export function renderApp( - // element: HTMLElement, { notifications, http }: CoreStart, { navigation }: AppPluginStartDependencies, appMountContext: AppMountContext, From bea4939ccb91f6c2ca8988e8de3dd50bc14b2b81 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Tue, 25 Feb 2020 12:49:39 -0800 Subject: [PATCH 17/21] Added entity CRUD APIs --- lib/auth/types/AuthType.ts | 10 - public/plugin.ts | 1 - server/auth/dummy_authentication_handler.ts | 6 + server/auth/errors/authentication_error.ts | 6 + server/auth/errors/invalid_session_error.ts | 6 + server/auth/errors/missing_role_error.ts | 8 + server/auth/errors/missing_tenant_error.ts | 8 + server/auth/errors/session_expired_error.ts | 7 + server/auth/types/AuthType.ts | 37 +++ {lib => server}/auth/user.ts | 0 .../errors/wrap_elasticsearch_error.ts | 36 +++ ...pendistro_security_configuration_plugin.ts | 176 ++++++++++ server/plugin.ts | 37 ++- server/routes/index.ts | 303 +++++++++++++++++- 14 files changed, 623 insertions(+), 18 deletions(-) delete mode 100644 lib/auth/types/AuthType.ts create mode 100644 server/auth/dummy_authentication_handler.ts create mode 100644 server/auth/errors/authentication_error.ts create mode 100644 server/auth/errors/invalid_session_error.ts create mode 100644 server/auth/errors/missing_role_error.ts create mode 100644 server/auth/errors/missing_tenant_error.ts create mode 100644 server/auth/errors/session_expired_error.ts create mode 100644 server/auth/types/AuthType.ts rename {lib => server}/auth/user.ts (100%) create mode 100644 server/backend/errors/wrap_elasticsearch_error.ts create mode 100644 server/backend/opendistro_security_configuration_plugin.ts diff --git a/lib/auth/types/AuthType.ts b/lib/auth/types/AuthType.ts deleted file mode 100644 index 121417c36..000000000 --- a/lib/auth/types/AuthType.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default class AuthType { - /* - private const APP_ROOT: string; - private const API_ROOT: string; - private pluginRoot: string; - private config: SecurityPluginConfigType; - private basePath: string; - private unauthenticatedRoutes: Array; - */ -} \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 133db1308..13bebf1ad 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -5,7 +5,6 @@ import { OpendistroSecurityPluginStart, AppPluginStartDependencies, } from './types'; -import { PLUGIN_NAME } from '../common'; export class OpendistroSecurityPlugin implements Plugin { diff --git a/server/auth/dummy_authentication_handler.ts b/server/auth/dummy_authentication_handler.ts new file mode 100644 index 000000000..ae3e3f67b --- /dev/null +++ b/server/auth/dummy_authentication_handler.ts @@ -0,0 +1,6 @@ +import { AuthenticationHandler, KibanaRequest, LifecycleResponseFactory, AuthToolkit } from "../../../../src/core/server"; + +export function dummyAuthHandler(request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit): AuthenticationHandler { + + return ; +}; diff --git a/server/auth/errors/authentication_error.ts b/server/auth/errors/authentication_error.ts new file mode 100644 index 000000000..84ecb43d6 --- /dev/null +++ b/server/auth/errors/authentication_error.ts @@ -0,0 +1,6 @@ +export default class AuthenticationError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/server/auth/errors/invalid_session_error.ts b/server/auth/errors/invalid_session_error.ts new file mode 100644 index 000000000..67cbec865 --- /dev/null +++ b/server/auth/errors/invalid_session_error.ts @@ -0,0 +1,6 @@ +export default class InvalidSessionError extends Error { + constructor(message: string, public inner: Error) { // TODO: should inner be public? + super(message); + this.name = this.constructor.name; + } +} \ No newline at end of file diff --git a/server/auth/errors/missing_role_error.ts b/server/auth/errors/missing_role_error.ts new file mode 100644 index 000000000..ca21abd52 --- /dev/null +++ b/server/auth/errors/missing_role_error.ts @@ -0,0 +1,8 @@ +export default class MissingRoleError extends Error { + + constructor(message) { + super(message); + this.name = this.constructor.name; + } + +} \ No newline at end of file diff --git a/server/auth/errors/missing_tenant_error.ts b/server/auth/errors/missing_tenant_error.ts new file mode 100644 index 000000000..61e020812 --- /dev/null +++ b/server/auth/errors/missing_tenant_error.ts @@ -0,0 +1,8 @@ +export default class MissingTenantError extends Error { + + constructor(message) { + super(message); + this.name = this.constructor.name; + } + +} \ No newline at end of file diff --git a/server/auth/errors/session_expired_error.ts b/server/auth/errors/session_expired_error.ts new file mode 100644 index 000000000..57dde3273 --- /dev/null +++ b/server/auth/errors/session_expired_error.ts @@ -0,0 +1,7 @@ +export default class SessionExpiredError extends Error { + + constructor(message, public inner: Error) { + super(message); + this.name = this.constructor.name; + } +} \ No newline at end of file diff --git a/server/auth/types/AuthType.ts b/server/auth/types/AuthType.ts new file mode 100644 index 000000000..2bcbb05fd --- /dev/null +++ b/server/auth/types/AuthType.ts @@ -0,0 +1,37 @@ +import { CoreSetup } from "../../../../../src/core/server"; +import { SecurityPluginConfigType } from "../.."; + +export default class AuthType { // TODO maybe replace the logic here with AuthenticationHandler + /* + private const APP_ROOT: string; + private const API_ROOT: string; + private pluginRoot: string; + private basePath: string; + */ + private unauthenticatedRoutes: Array; + private routesToIgnore: Array + private sessionTTL: number; + private sessionKeepAlive: boolean; + private type: string; + private validateAvailableTenantes: boolean = true; + private authHeaderNaem: string = 'authorization'; + private allowedAdditionalAuthHeaders: Array = ['security_impersonate_as']; + + constructor(private core: CoreSetup, private config: SecurityPluginConfigType) { + this.routesToIgnore = [ + '/bundles/app/security-login/bootstrap.js', + '/bundles/app/security-customerror/bootstrap.js' + ]; + this.sessionTTL = this.config.session.ttl; + this.sessionKeepAlive = this.config.session.keepalive; + this.unauthenticatedRoutes = this.config.auth.unauthenticated_routes; + } + + async init() { + this.setupStorage(); + } + + private setupStorage = () => void { + + } +} \ No newline at end of file diff --git a/lib/auth/user.ts b/server/auth/user.ts similarity index 100% rename from lib/auth/user.ts rename to server/auth/user.ts diff --git a/server/backend/errors/wrap_elasticsearch_error.ts b/server/backend/errors/wrap_elasticsearch_error.ts new file mode 100644 index 000000000..a03d85674 --- /dev/null +++ b/server/backend/errors/wrap_elasticsearch_error.ts @@ -0,0 +1,36 @@ +import { get } from 'lodash'; +import Boom from 'boom'; +import AuthenticationError from "../../../auth/errors/authentication_error"; + +export default function wrapElasticsearchError(error) { + + let statusCode = error.statusCode; + + if (error.status) { + statusCode = error.status; + } + + if (!statusCode) { + statusCode = 500; + } + + let message: string = get(error, 'body.message'); + if (!message) { + message = error.message; + } + + const wwwAuthHeader: string = 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(`${message}`); + } + + return Boom.boomify(error, { statusCode: statusCode, message: 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..40cfe078d --- /dev/null +++ b/server/backend/opendistro_security_configuration_plugin.ts @@ -0,0 +1,176 @@ +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 + } + } + } + }); +} \ No newline at end of file diff --git a/server/plugin.ts b/server/plugin.ts index 1aeac94d5..826b9a86c 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -4,32 +4,58 @@ import { CoreStart, Plugin, Logger, + IClusterClient, + KibanaRequest, + LifecycleResponseFactory, + OnPreAuthToolkit, + 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'; export class OpendistroSecurityPlugin - implements Plugin { + implements Plugin { private readonly logger: Logger; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public async setup(core: CoreSetup) { this.logger.debug('opendistro_security: Setup'); - + const config$ = this.initializerContext.config.create(); + const securityConfigClient: IClusterClient = core.elasticsearch.createClient( + 'opendistro_security', + { + plugins: [opendistro_security_configuratoin_plugin] + } + ); + + // cookie session handling + const dummyCookieSessionStorageFactory: SessionStorageFactory = await core.http.createCookieSessionStorageFactory({ + name: 'cookie_name', + encryptionKey: 'abcdefghijklmnopqrstuvwxyz0123456789', + validate: (sessionValue) => { + // console.log(`sessionValue: ${sessionValue}`); + return { isValid: true, path: '/' }; + // return { isValid: false }; + }, + isSecure: false, + }); + const router = core.http.createRouter(); // Register server side APIs - defineRoutes(router); + defineRoutes(router, securityConfigClient, dummyCookieSessionStorageFactory); return { config$, + securityConfigClient, }; } @@ -38,5 +64,6 @@ export class OpendistroSecurityPlugin return {}; } - public stop() {} + public stop() { } + } diff --git a/server/routes/index.ts b/server/routes/index.ts index 2e8d66ac6..b268ee5a2 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,6 +1,60 @@ -import { IRouter } from '../../../../src/core/server'; +import { IRouter, IClusterClient, SessionStorageFactory } from '../../../../src/core/server'; +import { schema } from '@kbn/config-schema'; + +export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient, dummyCookieSessionStorageFactory: SessionStorageFactory) { + 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 + } -export function defineRoutes(router: IRouter) { router.get( { path: '/api/opendistro_security/example', @@ -14,4 +68,249 @@ export function defineRoutes(router: IRouter) { }); } ); + 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, + }); + } + ); + router.get( + { + path: '/api/test_call_as_internal_user', + validate: false, + }, + async (context, request, response) => { + let catNodeResponse = {}; + try { + catNodeResponse = await context.core.elasticsearch.adminClient.callAsInternalUser('cat.nodes', { format: 'json' }); + } catch (err) { + catNodeResponse = err; + } + return response.ok({ + body: { + time: new Date().toISOString(), + nodes: catNodeResponse + }, + }); + } + ); + router.get( + { + path: '/api/test_call_as_current_user', + validate: false, + }, + async (context, request, response) => { + let esResponse = {}; + try { + esResponse = await context.core.elasticsearch.dataClient.callAsCurrentUser('search'); + } catch (err) { + esResponse = err + } + return response.ok({ + body: { + data: esResponse, + } + }); + }, + ); + router.get( + { + path: '/api/test_params/{param1}', + validate: { + params: schema.object({ + param1: schema.string({ defaultValue: undefined }), + }), + } + }, + async (context, request, response) => { + request.params.param1 + return response.ok({ + body: { + data: request.params.param1, + }, + }); + }, + ); + router.get( + { + path: `${API_PREFIX}/configuration/{resourceName}`, + validate: { + params: schema.object({ + resourceName: schema.string(), + }), + }, + }, + async (context, request, response) => { + /* + let cookieValue = await dummyCookieSessionStorageFactory.asScoped(request).get(); + console.log(`cookie value: ${cookieValue}`); + dummyCookieSessionStorageFactory.asScoped(request).clear(); + dummyCookieSessionStorageFactory.asScoped(request).set({dummyKey: 'dummy_value'}); + */ + 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, + }); + } + }, + ); + + 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, + }); + } + }, + ); + + 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, + }); + } + } + ); + + 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, + }); + } + } + ); + + 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.post( + { + path: `${API_PREFIX}/configuration/validate`, + validate: { + body: schema.any(), + // body: internalUserSchema, + }, + }, + async (context, request, response) => { + let validateOutput; + try { + validateOutput = internalUserSchema.validate(request.body); + } catch (error) { + console.log(`${error}`); + return response.badRequest({ body: error }); + } + return response.ok({ body: validateOutput }); + }, + ); } From 96dcd3af4a3cb063514ddb01ab2c0c05a0b882a5 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Wed, 1 Apr 2020 16:38:54 -0700 Subject: [PATCH 18/21] preliminary basic auth --- .gitignore | 1 + public/login.tsx | 15 + public/plugin.ts | 12 +- server/auth/{types => }/AuthType.ts | 4 +- server/auth/dummy_authentication_handler.ts | 6 - server/auth/types/basic/basic_auth.ts | 101 +++++++ server/auth/types/basic/routes.ts | 256 ++++++++++++++++++ server/auth/user.ts | 15 +- .../errors/wrap_elasticsearch_error.ts | 2 +- server/backend/opendistro_security_client.ts | 55 ++++ ...pendistro_security_configuration_plugin.ts | 28 ++ server/backend/opendistro_security_plugin.ts | 33 +++ server/index.ts | 4 +- server/plugin.ts | 53 ++-- server/routes/index.ts | 37 ++- server/routes/test_routes.ts | 55 ++++ server/session/security_cookie.ts | 37 +++ server/utils/filter_auth_headers.ts | 18 ++ 18 files changed, 681 insertions(+), 51 deletions(-) create mode 100644 public/login.tsx rename server/auth/{types => }/AuthType.ts (90%) delete mode 100644 server/auth/dummy_authentication_handler.ts create mode 100644 server/auth/types/basic/basic_auth.ts create mode 100644 server/auth/types/basic/routes.ts create mode 100644 server/backend/opendistro_security_client.ts create mode 100644 server/backend/opendistro_security_plugin.ts create mode 100644 server/routes/test_routes.ts create mode 100644 server/session/security_cookie.ts create mode 100644 server/utils/filter_auth_headers.ts diff --git a/.gitignore b/.gitignore index 521e1c729..8b8cc022b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ npm-debug.log* node_modules /build/ /public/app.css +target diff --git a/public/login.tsx b/public/login.tsx new file mode 100644 index 000000000..7a695cf7b --- /dev/null +++ b/public/login.tsx @@ -0,0 +1,15 @@ +import { AppMountParameters } from "../../../src/core/public"; +import ReactDOM from "react-dom"; + +export function renderApp( + params: AppMountParameters + // basePath: string +) { + + ReactDOM.render( +
+ login page +
, + params.element); + return () => ReactDOM.unmountComponentAtNode(params.element); +} \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 13bebf1ad..db46af732 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -12,7 +12,6 @@ export class OpendistroSecurityPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup): OpendistroSecurityPluginSetup { - console.log(this.initializerContext.config.get()); core.application.register({ id: "opendistro_security", title: "Security", @@ -24,6 +23,17 @@ export class OpendistroSecurityPlugin } }); + core.application.register({ + id: 'opendistro_login', + title: 'Login', + chromeless: true, + // appRoute: `/app/login`, + mount: async (params: AppMountParameters) => { + const { renderApp } = await import('./login'); + return renderApp(params); + }, + }); + // Return methods that should be available to other plugins return { }; diff --git a/server/auth/types/AuthType.ts b/server/auth/AuthType.ts similarity index 90% rename from server/auth/types/AuthType.ts rename to server/auth/AuthType.ts index 2bcbb05fd..7cd050e40 100644 --- a/server/auth/types/AuthType.ts +++ b/server/auth/AuthType.ts @@ -1,5 +1,5 @@ -import { CoreSetup } from "../../../../../src/core/server"; -import { SecurityPluginConfigType } from "../.."; +import { CoreSetup } from "../../../../src/core/server"; +import { SecurityPluginConfigType } from ".."; export default class AuthType { // TODO maybe replace the logic here with AuthenticationHandler /* diff --git a/server/auth/dummy_authentication_handler.ts b/server/auth/dummy_authentication_handler.ts deleted file mode 100644 index ae3e3f67b..000000000 --- a/server/auth/dummy_authentication_handler.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AuthenticationHandler, KibanaRequest, LifecycleResponseFactory, AuthToolkit } from "../../../../src/core/server"; - -export function dummyAuthHandler(request: KibanaRequest, response: LifecycleResponseFactory, toolkit: AuthToolkit): AuthenticationHandler { - - return ; -}; diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts new file mode 100644 index 000000000..2b34cbcff --- /dev/null +++ b/server/auth/types/basic/basic_auth.ts @@ -0,0 +1,101 @@ +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"; +import { LoggerAdapter } from "../../../../../../src/core/server/logging/logger_adapter"; + +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..9c8c6a21b --- /dev/null +++ b/server/auth/types/basic/routes.ts @@ -0,0 +1,256 @@ +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 index ea80387ea..b8c3c83bb 100644 --- a/server/auth/user.ts +++ b/server/auth/user.ts @@ -4,11 +4,11 @@ export class User { readonly backendRoles: Array; readonly tenants: Array; readonly selectedTenant: string; - readonly credentials: Credentials; - readonly proxyCredentials: Credentials; + readonly credentials: any; + readonly proxyCredentials: any; constructor(username: string, roles: Array, backendRoles: Array, tenants: Array, - selectedTenant: string, credentials: Credentials, proxyCredentials: Credentials) { + selectedTenant: string, credentials: any = undefined, proxyCredentials: any = undefined) { this.username = username; this.roles = roles; this.backendRoles = backendRoles; @@ -19,12 +19,3 @@ export class User { } } -export class Credentials { - readonly username: string; - readonly password: string; - - constructor(username: string, password: string) { - this.username = username; - this.password = password; - } -} diff --git a/server/backend/errors/wrap_elasticsearch_error.ts b/server/backend/errors/wrap_elasticsearch_error.ts index a03d85674..627acad52 100644 --- a/server/backend/errors/wrap_elasticsearch_error.ts +++ b/server/backend/errors/wrap_elasticsearch_error.ts @@ -1,6 +1,6 @@ import { get } from 'lodash'; import Boom from 'boom'; -import AuthenticationError from "../../../auth/errors/authentication_error"; +import AuthenticationError from "../../auth/errors/authentication_error"; export default function wrapElasticsearchError(error) { diff --git a/server/backend/opendistro_security_client.ts b/server/backend/opendistro_security_client.ts new file mode 100644 index 000000000..fcf522b15 --- /dev/null +++ b/server/backend/opendistro_security_client.ts @@ -0,0 +1,55 @@ +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 index 40cfe078d..0b4a10d4e 100644 --- a/server/backend/opendistro_security_configuration_plugin.ts +++ b/server/backend/opendistro_security_configuration_plugin.ts @@ -173,4 +173,32 @@ export default function (Client: any, config: any, components: any) { } } }); + + + ///// + 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..00a603769 --- /dev/null +++ b/server/backend/opendistro_security_plugin.ts @@ -0,0 +1,33 @@ +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 index 6e242ef9e..1455436c0 100644 --- a/server/index.ts +++ b/server/index.ts @@ -44,7 +44,7 @@ export const configSchema = schema.object({ forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }), logout_url: schema.string({ defaultValue: '' }), }), - basicauth: schema.maybe(schema.object({ + basicauth: schema.object({ enabled: schema.boolean({ defaultValue: true }), unauthenticated_routes: schema.arrayOf(schema.string(), { defaultValue: ["/api/status"] }), forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }), @@ -64,7 +64,7 @@ export const configSchema = schema.object({ 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 }), diff --git a/server/plugin.ts b/server/plugin.ts index 826b9a86c..fd6c2f65a 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -5,16 +5,23 @@ import { Plugin, Logger, IClusterClient, + SessionStorageFactory, KibanaRequest, LifecycleResponseFactory, OnPreAuthToolkit, - 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'; +import { r } from 'tar'; + export class OpendistroSecurityPlugin implements Plugin { @@ -28,34 +35,44 @@ export class OpendistroSecurityPlugin 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 securityConfigClient: IClusterClient = core.elasticsearch.createClient( + const securityClient: IClusterClient = core.elasticsearch.createClient( 'opendistro_security', { - plugins: [opendistro_security_configuratoin_plugin] + 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, + ], } ); - // cookie session handling - const dummyCookieSessionStorageFactory: SessionStorageFactory = await core.http.createCookieSessionStorageFactory({ - name: 'cookie_name', - encryptionKey: 'abcdefghijklmnopqrstuvwxyz0123456789', - validate: (sessionValue) => { - // console.log(`sessionValue: ${sessionValue}`); - return { isValid: true, path: '/' }; - // return { isValid: false }; - }, - isSecure: false, - }); - - const router = core.http.createRouter(); + const securitySessionStorageFactory: SessionStorageFactory + = await core.http.createCookieSessionStorageFactory(getSecurityCookieOptions(config)); + // Register server side APIs - defineRoutes(router, securityConfigClient, dummyCookieSessionStorageFactory); + 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, + securityConfigClient: securityClient, }; } diff --git a/server/routes/index.ts b/server/routes/index.ts index b268ee5a2..c54b4f9f6 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,7 +1,7 @@ -import { IRouter, IClusterClient, SessionStorageFactory } from '../../../../src/core/server'; +import { IRouter, IClusterClient } from '../../../../src/core/server'; import { schema } from '@kbn/config-schema'; -export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient, dummyCookieSessionStorageFactory: SessionStorageFactory) { +export function defineRoutes(router: IRouter, securityConfigClient: IClusterClient) { const API_PREFIX: string = '/api/v1/opendistro_security'; const internalUserSchema = schema.object({ @@ -147,12 +147,6 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie }, }, async (context, request, response) => { - /* - let cookieValue = await dummyCookieSessionStorageFactory.asScoped(request).get(); - console.log(`cookie value: ${cookieValue}`); - dummyCookieSessionStorageFactory.asScoped(request).clear(); - dummyCookieSessionStorageFactory.asScoped(request).set({dummyKey: 'dummy_value'}); - */ const client = securityConfigClient.asScoped(request); let esResp; try { @@ -294,6 +288,7 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie } } ); + // test endpoint, DELETE it router.post( { path: `${API_PREFIX}/configuration/validate`, @@ -307,10 +302,34 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie try { validateOutput = internalUserSchema.validate(request.body); } catch (error) { - console.log(`${error}`); return response.badRequest({ body: error }); } return response.ok({ body: validateOutput }); }, ); + + 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..c0c5c5c8b --- /dev/null +++ b/server/routes/test_routes.ts @@ -0,0 +1,55 @@ +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..c9d074bf2 --- /dev/null +++ b/server/session/security_cookie.ts @@ -0,0 +1,37 @@ +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/utils/filter_auth_headers.ts b/server/utils/filter_auth_headers.ts new file mode 100644 index 000000000..01255c201 --- /dev/null +++ b/server/utils/filter_auth_headers.ts @@ -0,0 +1,18 @@ + +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); +} From 3080b766a92e8bc9dfcb4c595574859899f3d5c4 Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Thu, 2 Apr 2020 21:51:16 -0700 Subject: [PATCH 19/21] clean up code --- server/auth/types/basic/basic_auth.ts | 1 - server/plugin.ts | 7 +- server/routes/index.ts | 95 ++------------------------- 3 files changed, 7 insertions(+), 96 deletions(-) diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts index 2b34cbcff..7768deeba 100644 --- a/server/auth/types/basic/basic_auth.ts +++ b/server/auth/types/basic/basic_auth.ts @@ -5,7 +5,6 @@ import { CoreSetup } from "../../../../../../src/core/server"; import _ from 'lodash'; import { SecurityClient } from "../../../backend/opendistro_security_client"; import { BasicAuthRoutes } from "./routes"; -import { LoggerAdapter } from "../../../../../../src/core/server/logging/logger_adapter"; export class AuthConfig { constructor( diff --git a/server/plugin.ts b/server/plugin.ts index fd6c2f65a..fdf5ab79f 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -6,9 +6,6 @@ import { Logger, IClusterClient, SessionStorageFactory, - KibanaRequest, - LifecycleResponseFactory, - OnPreAuthToolkit, } from '../../../src/core/server'; import { OpendistroSecurityPluginSetup, OpendistroSecurityPluginStart } from './types'; @@ -19,9 +16,7 @@ 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'; -import { r } from 'tar'; - +import { defineTestRoutes } from './routes/test_routes'; // TODO: remove this later export class OpendistroSecurityPlugin implements Plugin { diff --git a/server/routes/index.ts b/server/routes/index.ts index c54b4f9f6..7c9402114 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -55,19 +55,6 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie inputSchema.validate(requestBody); // throws error if validation fail } - router.get( - { - path: '/api/opendistro_security/example', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - body: { - time: new Date().toISOString(), - }, - }); - } - ); router.get( { path: '/api/test/security_config', @@ -80,63 +67,8 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie }); } ); - router.get( - { - path: '/api/test_call_as_internal_user', - validate: false, - }, - async (context, request, response) => { - let catNodeResponse = {}; - try { - catNodeResponse = await context.core.elasticsearch.adminClient.callAsInternalUser('cat.nodes', { format: 'json' }); - } catch (err) { - catNodeResponse = err; - } - return response.ok({ - body: { - time: new Date().toISOString(), - nodes: catNodeResponse - }, - }); - } - ); - router.get( - { - path: '/api/test_call_as_current_user', - validate: false, - }, - async (context, request, response) => { - let esResponse = {}; - try { - esResponse = await context.core.elasticsearch.dataClient.callAsCurrentUser('search'); - } catch (err) { - esResponse = err - } - return response.ok({ - body: { - data: esResponse, - } - }); - }, - ); - router.get( - { - path: '/api/test_params/{param1}', - validate: { - params: schema.object({ - param1: schema.string({ defaultValue: undefined }), - }), - } - }, - async (context, request, response) => { - request.params.param1 - return response.ok({ - body: { - data: request.params.param1, - }, - }); - }, - ); + + // list resources by resource name router.get( { path: `${API_PREFIX}/configuration/{resourceName}`, @@ -166,6 +98,7 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie }, ); + // get resource by resource name and id router.get( { path: `${API_PREFIX}/configuration/{resourceName}/{id}`, @@ -191,6 +124,7 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie }, ); + // delete resource by resource name and id router.delete( { path: `${API_PREFIX}/configuration/{resourceName}/{id}`, @@ -220,6 +154,7 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie } ); + // create new resource router.post( { path: `${API_PREFIX}/configuration/{resourceName}`, @@ -254,6 +189,7 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie } ); + // update resource by Id router.post( { path: `${API_PREFIX}/configuration/{resourceName}/{id}`, @@ -288,25 +224,6 @@ export function defineRoutes(router: IRouter, securityConfigClient: IClusterClie } } ); - // test endpoint, DELETE it - router.post( - { - path: `${API_PREFIX}/configuration/validate`, - validate: { - body: schema.any(), - // body: internalUserSchema, - }, - }, - async (context, request, response) => { - let validateOutput; - try { - validateOutput = internalUserSchema.validate(request.body); - } catch (error) { - return response.badRequest({ body: error }); - } - return response.ok({ body: validateOutput }); - }, - ); router.get( { From 5c7400f5514c64c711a13831359242e3de8a10ac Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Thu, 9 Apr 2020 10:05:34 -0700 Subject: [PATCH 20/21] Fix minor problems --- README.md | 1 - public/application.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 61658c6d6..80e80b193 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ # security-kibana-plugin -private repo for Elasticsearch security kibana plugin development diff --git a/public/application.tsx b/public/application.tsx index 6ebbc9e20..d2bb7f4e1 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { AppMountContext, AppMountParameters, CoreStart } from '../../../src/core/public'; import SecurityManagementApp from './components/security-management-app'; -import { AppPluginStartDependencies } from '../../np_demo/public/types'; +import { AppPluginStartDependencies } from './types'; export function renderApp( { notifications, http }: CoreStart, From 289b52a273f9fc548dba8ce24116c451266c8f4c Mon Sep 17 00:00:00 2001 From: Yan Zeng Date: Thu, 9 Apr 2020 10:39:55 -0700 Subject: [PATCH 21/21] clean up browser app code, add license header --- .eslintrc.js | 1 + CODE_OF_CONDUCT.md | 2 + LICENSE | 27 +------- NOTICE | 2 + common/index.ts | 15 +++++ public/application.tsx | 25 +++++--- public/components/security-management-app.tsx | 63 ------------------- .../roles/role-management-app.tsx | 10 --- public/index.ts | 15 +++++ public/login.tsx | 15 ----- public/plugin.ts | 26 ++++---- public/types.ts | 15 +++++ server/auth/AuthType.ts | 37 ----------- server/auth/errors/authentication_error.ts | 6 -- server/auth/errors/invalid_session_error.ts | 6 -- server/auth/errors/missing_role_error.ts | 8 --- server/auth/errors/missing_tenant_error.ts | 8 --- server/auth/errors/session_expired_error.ts | 7 --- server/auth/types/basic/basic_auth.ts | 15 +++++ server/auth/types/basic/routes.ts | 15 +++++ server/auth/user.ts | 15 +++++ .../errors/wrap_elasticsearch_error.ts | 36 ----------- server/backend/opendistro_security_client.ts | 15 +++++ ...pendistro_security_configuration_plugin.ts | 15 +++++ server/backend/opendistro_security_plugin.ts | 15 +++++ server/index.ts | 15 +++++ server/plugin.ts | 15 +++++ server/routes/index.ts | 15 +++++ server/routes/test_routes.ts | 15 +++++ server/session/security_cookie.ts | 15 +++++ server/types.ts | 15 +++++ server/utils/filter_auth_headers.ts | 14 +++++ 32 files changed, 278 insertions(+), 240 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 NOTICE delete mode 100644 public/components/security-management-app.tsx delete mode 100644 public/components/security-management/roles/role-management-app.tsx delete mode 100644 public/login.tsx delete mode 100644 server/auth/AuthType.ts delete mode 100644 server/auth/errors/authentication_error.ts delete mode 100644 server/auth/errors/invalid_session_error.ts delete mode 100644 server/auth/errors/missing_role_error.ts delete mode 100644 server/auth/errors/missing_tenant_error.ts delete mode 100644 server/auth/errors/session_expired_error.ts delete mode 100644 server/backend/errors/wrap_elasticsearch_error.ts diff --git a/.eslintrc.js b/.eslintrc.js index ff4759cf3..d97d3568e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,4 @@ + module.exports = { root: true, extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], 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 index 261eeb9e9..2bb9ad240 100644 --- a/LICENSE +++ b/LICENSE @@ -173,29 +173,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. + 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/common/index.ts b/common/index.ts index c5f3b0ce3..48edde340 100644 --- a/common/index.ts +++ b/common/index.ts @@ -1,2 +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/public/application.tsx b/public/application.tsx index d2bb7f4e1..92ea36a5e 100644 --- a/public/application.tsx +++ b/public/application.tsx @@ -1,7 +1,21 @@ +/* + * 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 SecurityManagementApp from './components/security-management-app'; import { AppPluginStartDependencies } from './types'; export function renderApp( @@ -11,14 +25,11 @@ export function renderApp( params: AppMountParameters // basePath: string ) { - setBreadcrumbs(appMountContext); ReactDOM.render( - , + // security application + (
+
), params.element); return () => ReactDOM.unmountComponentAtNode(params.element); } diff --git a/public/components/security-management-app.tsx b/public/components/security-management-app.tsx deleted file mode 100644 index c4a9fb226..000000000 --- a/public/components/security-management-app.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react'; -import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; -import { EuiPanel } from '@elastic/eui'; -import { RoleManagement } from './security-management/roles/role-management-app'; -import { AppMountContext } from '../../../../src/core/public'; - -class SecurityManagementAppProps { - element!: HTMLElement; - appMountContext!: AppMountContext; - basePath!: string; -} -class SecurityManagementAppState { - selectedTab?: EuiTabbedContentTab; -} - -class SecurityManagementApp extends Component { - private tabs: EuiTabbedContentTab[] = [ - { - id: 'roles', - name: 'Roles', - content: () - }, - { - id: 'action_groups', - name: 'Action Groups', - content: (
bbb
) - }, - { - id: 'internal_user_database', - name: 'Internal User Database', - content: (
ccc
) - } - ]; - - constructor(props: SecurityManagementAppProps) { - super(props); - this.state = {}; - } - - componentDidMount() { - this.setState( { - selectedTab: this.tabs[0], - } ); - } - - render() { - return
- - - -
- } - - onTabClick = (tab: EuiTabbedContentTab) => { - this.setState({ selectedTab: tab }); - }; -} - -export default SecurityManagementApp; \ No newline at end of file diff --git a/public/components/security-management/roles/role-management-app.tsx b/public/components/security-management/roles/role-management-app.tsx deleted file mode 100644 index 585183194..000000000 --- a/public/components/security-management/roles/role-management-app.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { Component } from 'react'; - -export class RoleManagement extends Component { - render() { - return (
- RoleManagement page -
); - } -} - diff --git a/public/index.ts b/public/index.ts index d44c8a7cd..a85c45b60 100644 --- a/public/index.ts +++ b/public/index.ts @@ -1,3 +1,18 @@ +/* + * 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'; diff --git a/public/login.tsx b/public/login.tsx deleted file mode 100644 index 7a695cf7b..000000000 --- a/public/login.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { AppMountParameters } from "../../../src/core/public"; -import ReactDOM from "react-dom"; - -export function renderApp( - params: AppMountParameters - // basePath: string -) { - - ReactDOM.render( -
- login page -
, - params.element); - return () => ReactDOM.unmountComponentAtNode(params.element); -} \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index db46af732..835f6e4de 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -1,3 +1,18 @@ +/* + * 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 { @@ -23,17 +38,6 @@ export class OpendistroSecurityPlugin } }); - core.application.register({ - id: 'opendistro_login', - title: 'Login', - chromeless: true, - // appRoute: `/app/login`, - mount: async (params: AppMountParameters) => { - const { renderApp } = await import('./login'); - return renderApp(params); - }, - }); - // Return methods that should be available to other plugins return { }; diff --git a/public/types.ts b/public/types.ts index 3fae38f0d..e3c123c61 100644 --- a/public/types.ts +++ b/public/types.ts @@ -1,3 +1,18 @@ +/* + * 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 {} diff --git a/server/auth/AuthType.ts b/server/auth/AuthType.ts deleted file mode 100644 index 7cd050e40..000000000 --- a/server/auth/AuthType.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { CoreSetup } from "../../../../src/core/server"; -import { SecurityPluginConfigType } from ".."; - -export default class AuthType { // TODO maybe replace the logic here with AuthenticationHandler - /* - private const APP_ROOT: string; - private const API_ROOT: string; - private pluginRoot: string; - private basePath: string; - */ - private unauthenticatedRoutes: Array; - private routesToIgnore: Array - private sessionTTL: number; - private sessionKeepAlive: boolean; - private type: string; - private validateAvailableTenantes: boolean = true; - private authHeaderNaem: string = 'authorization'; - private allowedAdditionalAuthHeaders: Array = ['security_impersonate_as']; - - constructor(private core: CoreSetup, private config: SecurityPluginConfigType) { - this.routesToIgnore = [ - '/bundles/app/security-login/bootstrap.js', - '/bundles/app/security-customerror/bootstrap.js' - ]; - this.sessionTTL = this.config.session.ttl; - this.sessionKeepAlive = this.config.session.keepalive; - this.unauthenticatedRoutes = this.config.auth.unauthenticated_routes; - } - - async init() { - this.setupStorage(); - } - - private setupStorage = () => void { - - } -} \ No newline at end of file diff --git a/server/auth/errors/authentication_error.ts b/server/auth/errors/authentication_error.ts deleted file mode 100644 index 84ecb43d6..000000000 --- a/server/auth/errors/authentication_error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default class AuthenticationError extends Error { - constructor(message) { - super(message); - this.name = this.constructor.name; - } -} diff --git a/server/auth/errors/invalid_session_error.ts b/server/auth/errors/invalid_session_error.ts deleted file mode 100644 index 67cbec865..000000000 --- a/server/auth/errors/invalid_session_error.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default class InvalidSessionError extends Error { - constructor(message: string, public inner: Error) { // TODO: should inner be public? - super(message); - this.name = this.constructor.name; - } -} \ No newline at end of file diff --git a/server/auth/errors/missing_role_error.ts b/server/auth/errors/missing_role_error.ts deleted file mode 100644 index ca21abd52..000000000 --- a/server/auth/errors/missing_role_error.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default class MissingRoleError extends Error { - - constructor(message) { - super(message); - this.name = this.constructor.name; - } - -} \ No newline at end of file diff --git a/server/auth/errors/missing_tenant_error.ts b/server/auth/errors/missing_tenant_error.ts deleted file mode 100644 index 61e020812..000000000 --- a/server/auth/errors/missing_tenant_error.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default class MissingTenantError extends Error { - - constructor(message) { - super(message); - this.name = this.constructor.name; - } - -} \ No newline at end of file diff --git a/server/auth/errors/session_expired_error.ts b/server/auth/errors/session_expired_error.ts deleted file mode 100644 index 57dde3273..000000000 --- a/server/auth/errors/session_expired_error.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default class SessionExpiredError extends Error { - - constructor(message, public inner: Error) { - super(message); - this.name = this.constructor.name; - } -} \ No newline at end of file diff --git a/server/auth/types/basic/basic_auth.ts b/server/auth/types/basic/basic_auth.ts index 7768deeba..717847ee1 100644 --- a/server/auth/types/basic/basic_auth.ts +++ b/server/auth/types/basic/basic_auth.ts @@ -1,3 +1,18 @@ +/* + * 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"; diff --git a/server/auth/types/basic/routes.ts b/server/auth/types/basic/routes.ts index 9c8c6a21b..d43a9c5ca 100644 --- a/server/auth/types/basic/routes.ts +++ b/server/auth/types/basic/routes.ts @@ -1,3 +1,18 @@ +/* + * 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 "../../.."; diff --git a/server/auth/user.ts b/server/auth/user.ts index b8c3c83bb..fb51a3d65 100644 --- a/server/auth/user.ts +++ b/server/auth/user.ts @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/server/backend/errors/wrap_elasticsearch_error.ts b/server/backend/errors/wrap_elasticsearch_error.ts deleted file mode 100644 index 627acad52..000000000 --- a/server/backend/errors/wrap_elasticsearch_error.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { get } from 'lodash'; -import Boom from 'boom'; -import AuthenticationError from "../../auth/errors/authentication_error"; - -export default function wrapElasticsearchError(error) { - - let statusCode = error.statusCode; - - if (error.status) { - statusCode = error.status; - } - - if (!statusCode) { - statusCode = 500; - } - - let message: string = get(error, 'body.message'); - if (!message) { - message = error.message; - } - - const wwwAuthHeader: string = 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(`${message}`); - } - - return Boom.boomify(error, { statusCode: statusCode, message: message }); - -} diff --git a/server/backend/opendistro_security_client.ts b/server/backend/opendistro_security_client.ts index fcf522b15..5c4c51ce3 100644 --- a/server/backend/opendistro_security_client.ts +++ b/server/backend/opendistro_security_client.ts @@ -1,3 +1,18 @@ +/* + * 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"; diff --git a/server/backend/opendistro_security_configuration_plugin.ts b/server/backend/opendistro_security_configuration_plugin.ts index 0b4a10d4e..1b3f059d3 100644 --- a/server/backend/opendistro_security_configuration_plugin.ts +++ b/server/backend/opendistro_security_configuration_plugin.ts @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/server/backend/opendistro_security_plugin.ts b/server/backend/opendistro_security_plugin.ts index 00a603769..1163c98d1 100644 --- a/server/backend/opendistro_security_plugin.ts +++ b/server/backend/opendistro_security_plugin.ts @@ -1,3 +1,18 @@ +/* + * 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; diff --git a/server/index.ts b/server/index.ts index 1455436c0..033a49b93 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,3 +1,18 @@ +/* + * 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'; diff --git a/server/plugin.ts b/server/plugin.ts index fdf5ab79f..02ddb1cee 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -1,3 +1,18 @@ +/* + * 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, diff --git a/server/routes/index.ts b/server/routes/index.ts index 7c9402114..fb6604b4c 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,3 +1,18 @@ +/* + * 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'; diff --git a/server/routes/test_routes.ts b/server/routes/test_routes.ts index c0c5c5c8b..493efd43e 100644 --- a/server/routes/test_routes.ts +++ b/server/routes/test_routes.ts @@ -1,3 +1,18 @@ +/* + * 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'; diff --git a/server/session/security_cookie.ts b/server/session/security_cookie.ts index c9d074bf2..22bb76648 100644 --- a/server/session/security_cookie.ts +++ b/server/session/security_cookie.ts @@ -1,3 +1,18 @@ +/* + * 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 ".."; diff --git a/server/types.ts b/server/types.ts index 6e1091692..4522ca856 100644 --- a/server/types.ts +++ b/server/types.ts @@ -1,3 +1,18 @@ +/* + * 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 diff --git a/server/utils/filter_auth_headers.ts b/server/utils/filter_auth_headers.ts index 01255c201..fd2dfb565 100644 --- a/server/utils/filter_auth_headers.ts +++ b/server/utils/filter_auth_headers.ts @@ -1,3 +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. + */ import _ from 'lodash'; import { Headers } from '../../../../src/core/server/http/router/headers';