From 9578811f80490859bf34e7388fb9159fb96e9b3f Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 10 Sep 2021 14:05:30 +0300 Subject: [PATCH] feat: CB-1236 sso authentication --- .../server/jetty/CBStaticServlet.java | 3 +- .../src/AuthInfoService.ts | 58 +++++++++++++++++-- .../src/HTMLTemplates/sso.html.ejs | 6 ++ .../configs/webpack.product.config.js | 17 +++++- .../configs/webpack.product.dev.config.js | 22 +++++-- .../src/Dialog/AuthDialog.tsx | 5 +- .../AuthProviderForm/ConfigurationsList.tsx | 44 ++++++++++---- webapp/packages/plugin-sso/.gitignore | 17 ++++++ webapp/packages/plugin-sso/package.json | 19 ++++++ webapp/packages/plugin-sso/src/index.html.ejs | 19 ++++++ webapp/packages/plugin-sso/src/index.ts | 0 webapp/packages/plugin-sso/tsconfig.json | 8 +++ 12 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 webapp/packages/core-blocks/src/HTMLTemplates/sso.html.ejs create mode 100644 webapp/packages/plugin-sso/.gitignore create mode 100644 webapp/packages/plugin-sso/package.json create mode 100644 webapp/packages/plugin-sso/src/index.html.ejs create mode 100644 webapp/packages/plugin-sso/src/index.ts create mode 100644 webapp/packages/plugin-sso/tsconfig.json diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java index b85a421854..718ea9dd0f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java @@ -40,7 +40,8 @@ public class CBStaticServlet extends DefaultServlet { private static class ProxyResourceService extends ResourceService { @Override protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, HttpContent content, Enumeration reqRanges) throws IOException { - if (content.getResource().getName().endsWith("index.html")) { + String resourceName = content.getResource().getName(); + if (resourceName.endsWith("index.html") || resourceName.endsWith("sso.html")) { return patchIndexHtml(response, content); } return super.sendData(request, response, include, content, reqRanges); diff --git a/webapp/packages/core-authentication/src/AuthInfoService.ts b/webapp/packages/core-authentication/src/AuthInfoService.ts index f8d0769674..cea80c19eb 100644 --- a/webapp/packages/core-authentication/src/AuthInfoService.ts +++ b/webapp/packages/core-authentication/src/AuthInfoService.ts @@ -7,25 +7,73 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; +import { SessionDataResource } from '@cloudbeaver/core-root'; +import type { AuthProviderConfiguration, UserInfo } from '@cloudbeaver/core-sdk'; import { UserInfoResource } from './UserInfoResource'; @injectable() export class AuthInfoService { - constructor( - private userInfoResource: UserInfoResource, - ) { } - get userInfo(): UserInfo | null { return this.userInfoResource.data; } + private activeSSO: Promise | null; + + constructor( + private userInfoResource: UserInfoResource, + private sessionDataResource: SessionDataResource + ) { + this.activeSSO = null; + } + async login(provider: string, credentials: Record, link?: boolean): Promise { return this.userInfoResource.login(provider, credentials, link); } + async sso(providerId: string, configuration: AuthProviderConfiguration): Promise { + if (!this.activeSSO) { + this.activeSSO = this.ssoAuth(providerId, configuration); + } + + try { + return await this.activeSSO; + } finally { + this.activeSSO = null; + } + } + async logout(): Promise { await this.userInfoResource.logout(); } + + private async ssoAuth(providerId: string, configuration: AuthProviderConfiguration): Promise { + const w = 600; + const h = 700; + const systemZoom = window.top.outerWidth / window.screen.availWidth; + const top = (window.top.outerHeight - h) / 2 / systemZoom + window.top.screenY; + const left = (window.top.outerWidth - w) / 2 / systemZoom + window.top.screenX; + const strWindowFeatures = `toolbar=no, menubar=no, width=${w / systemZoom}, height=${h / systemZoom}, top=${top}, left=${left}`; + const popup = window.open(configuration.signInLink, configuration.displayName, strWindowFeatures); + + if (popup) { + popup.focus(); + await this.waitWindowsClose(popup); + const user = await this.userInfoResource.refresh(); + await this.sessionDataResource.refresh(); + return user; + } + + return null; + } + + private async waitWindowsClose(window: Window): Promise { + return new Promise(resolve => { + setInterval(() => { + if (window.closed) { + resolve(); + } + }, 100); + }); + } } diff --git a/webapp/packages/core-blocks/src/HTMLTemplates/sso.html.ejs b/webapp/packages/core-blocks/src/HTMLTemplates/sso.html.ejs new file mode 100644 index 0000000000..e5d60e5b85 --- /dev/null +++ b/webapp/packages/core-blocks/src/HTMLTemplates/sso.html.ejs @@ -0,0 +1,6 @@ +
+ Success + +
\ No newline at end of file diff --git a/webapp/packages/core-cli/configs/webpack.product.config.js b/webapp/packages/core-cli/configs/webpack.product.config.js index 60788bb72d..b1cf866d20 100644 --- a/webapp/packages/core-cli/configs/webpack.product.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.config.js @@ -8,7 +8,9 @@ const { merge } = require('webpack-merge') const commonConfig = require('./webpack.config.js'); const { getAssets, withTimestamp } = require('./webpack.product.utils'); -const main = resolve('src/index.ts') +const main = resolve('src/index.ts'); +const sso = require.resolve('@cloudbeaver/plugin-sso/src/index.ts'); +const ssoHtmlTemplate = require.resolve('@cloudbeaver/plugin-sso/src/index.html.ejs'); const outputDir = resolve('lib') const package = require(resolve('package.json')) @@ -39,7 +41,10 @@ const excludedFromVendor = [ const timestampVersion = withTimestamp(package.version) module.exports = (env, argv) => merge(commonConfig(env, argv), { - entry: main, + entry: { + main, + sso + }, output: { filename: 'index.[contenthash].js', chunkFilename: '[name].[contenthash].bundle.js', @@ -79,6 +84,14 @@ module.exports = (env, argv) => merge(commonConfig(env, argv), { new HtmlWebpackPlugin({ template: resolve('src/index.html.ejs'), inject: 'body', + chunks: ['main'], + version: timestampVersion, + }), + new HtmlWebpackPlugin({ + filename: 'sso.html', + template: ssoHtmlTemplate, + inject: 'body', + chunks: ['sso'], version: timestampVersion, }), ], diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index a5906523e1..9567ef634e 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -4,6 +4,9 @@ const { resolve } = require('path'); const webpack = require('webpack'); const commonConfig = require('./webpack.config.js') +const main = resolve('src/index.ts'); +const sso = require.resolve('@cloudbeaver/plugin-sso/src/index.ts'); +const ssoHtmlTemplate = require.resolve('@cloudbeaver/plugin-sso/src/index.html.ejs'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin') @@ -11,18 +14,19 @@ const CopyWebpackPlugin = require('copy-webpack-plugin') const package = require(resolve('package.json')) module.exports = (env, argv) => merge(commonConfig(env, argv), { - entry: [ - 'webpack-dev-server/client?http://localhost:3100', - './src/index.ts', - ], + entry: { + main, + sso + }, mode: 'development', devServer: { + socket: 'socket', hot: true, proxy: { '/api': { target: env.server, }, - }, + } }, devtool: 'eval-source-map', optimization: { @@ -38,8 +42,16 @@ module.exports = (env, argv) => merge(commonConfig(env, argv), { new HtmlWebpackPlugin({ template: resolve('src/index.html.ejs'), inject: 'body', + chunks: ['main'], version: package.version }), + new HtmlWebpackPlugin({ + filename: 'sso.html', + template: ssoHtmlTemplate, + inject: 'body', + chunks: ['sso'], + version: package.version + }), new webpack.HotModuleReplacementPlugin(), ], }); diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 72a5a898b6..afb1c1a6dc 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -144,10 +144,7 @@ export const AuthDialog: DialogComponent = observer(function {controller.provider && ( configurable ? ( - + ) : ( (function ConfigurationsList({ configurations, providerIcon, className }) { +interface Props { + provider: AuthProvider; + onClose?: () => void; + className?: string; +} + +export const ConfigurationsList = observer(function ConfigurationsList({ provider, onClose, className }) { + const authInfoService = useService(AuthInfoService); const translate = useTranslate(); const [search, setSearch] = useState(''); + const configurations = useMemo(() => getComputed(() => + (provider.configurations || []) + .filter(configuration => configuration.signInLink) + ), [provider.configurations]); const filteredConfigurations = useMemo(() => computed(() => { - const sortedConfigurations = configurations.slice().sort(comparePublicAuthConfigurations); + const sortedConfigurations = configurations.slice().sort(comparePublicAuthConfigurations) || []; + if (!search) { return sortedConfigurations; } @@ -68,6 +75,14 @@ export const ConfigurationsList = observer(function ConfigurationsList({ }); }), [search, configurations]); + async function auth(configuration: AuthProviderConfiguration) { + const user = await authInfoService.sso(provider.id, configuration); + + if (user) { + onClose?.(); + } + } + return styled(useStyles(styles))( {configurations.length >= 10 && ( @@ -80,10 +95,15 @@ export const ConfigurationsList = observer(function ConfigurationsList({ )} {filteredConfigurations.get().map(configuration => { - const icon = configuration.iconURL || providerIcon; + const icon = configuration.iconURL || provider.icon; const title = `${configuration.displayName}\n${configuration.description || ''}`; return ( - + auth(configuration)} + > : undefined} description={configuration.description} diff --git a/webapp/packages/plugin-sso/.gitignore b/webapp/packages/plugin-sso/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-sso/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-sso/package.json b/webapp/packages/plugin-sso/package.json new file mode 100644 index 0000000000..903dbcdb73 --- /dev/null +++ b/webapp/packages/plugin-sso/package.json @@ -0,0 +1,19 @@ +{ + "name": "@cloudbeaver/plugin-sso", + "sideEffects": false, + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "lib/index.js", + "module": "lib/index.js", + "types": "src/index.ts", + "scripts": { + "prebuild": "rimraf lib/*", + "build": "webpack --config ../../configs/webpack.plugin.config.js", + "postbuild": "tsc", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" + }, + "peerDependencies": { + } +} diff --git a/webapp/packages/plugin-sso/src/index.html.ejs b/webapp/packages/plugin-sso/src/index.html.ejs new file mode 100644 index 0000000000..8dd1236b98 --- /dev/null +++ b/webapp/packages/plugin-sso/src/index.html.ejs @@ -0,0 +1,19 @@ + + + + + <%= require('html-loader!@cloudbeaver/core-blocks/src/HTMLTemplates/head.html.ejs') %> + CloudBeaver + + + +
+ <%= require('html-loader!@cloudbeaver/core-blocks/src/HTMLTemplates/sso.html.ejs') %> + + + <% if (webpackConfig.mode == 'production') { %> + <% } else { %> + <% } %> + + + \ No newline at end of file diff --git a/webapp/packages/plugin-sso/src/index.ts b/webapp/packages/plugin-sso/src/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webapp/packages/plugin-sso/tsconfig.json b/webapp/packages/plugin-sso/tsconfig.json new file mode 100644 index 0000000000..8cd6b539e3 --- /dev/null +++ b/webapp/packages/plugin-sso/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["src"], + "exclude": ["**/node_modules", "**/.*/"], +}