From ecfaa7198b23f0ee6db66528a69f954d941e507d Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 12:11:03 +0200 Subject: [PATCH 01/12] multiauth --- packages/web/src/datagrid/CollapseButton.svelte | 13 ++++++++++++- packages/web/src/icons/FontIcon.svelte | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/web/src/datagrid/CollapseButton.svelte b/packages/web/src/datagrid/CollapseButton.svelte index 8a72343e8..592ddbd6a 100644 --- a/packages/web/src/datagrid/CollapseButton.svelte +++ b/packages/web/src/datagrid/CollapseButton.svelte @@ -2,10 +2,19 @@ import FontIcon from '../icons/FontIcon.svelte'; export let collapsed; + export let vertical = false;
- +
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 6bd0312d4..46711ff3b 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -89,6 +89,8 @@ 'icon arrow-right-bold': 'mdi mdi-arrow-right-bold', 'icon triple-left': 'mdi mdi-chevron-triple-left', 'icon triple-right': 'mdi mdi-chevron-triple-right', + 'icon triple-up': 'mdi mdi-chevron-triple-up', + 'icon triple-down': 'mdi mdi-chevron-triple-down', 'icon format-code': 'mdi mdi-code-tags-check', 'icon show-wizard': 'mdi mdi-comment-edit', 'icon disconnected': 'mdi mdi-lan-disconnect', From 591945dc9321bfec3c6485d4edde43029a18a04a Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 12:26:28 +0200 Subject: [PATCH 02/12] css --- packages/web/src/buttons/ToolStripContainer.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/web/src/buttons/ToolStripContainer.svelte b/packages/web/src/buttons/ToolStripContainer.svelte index 61d74e44f..a8fdc863d 100644 --- a/packages/web/src/buttons/ToolStripContainer.svelte +++ b/packages/web/src/buttons/ToolStripContainer.svelte @@ -11,10 +11,12 @@ export function activate() { activator?.activate(); } + + export let scrollContent;
-
+
@@ -44,4 +46,8 @@ flex-wrap: wrap; background: var(--theme-bg-1); } + + .scrollContent { + overflow-y: auto; + } From 42c71c12048980513bee1b92e415071ac4273962 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 13:58:44 +0200 Subject: [PATCH 03/12] multiauth WIP --- packages/api/src/auth/authProvider.js | 48 +++++++++++++++++++---- packages/api/src/controllers/auth.js | 19 ++++++--- packages/api/src/controllers/config.js | 4 +- packages/api/src/main.js | 4 +- packages/api/src/utility/hasPermission.js | 4 +- packages/web/src/LoginPage.svelte | 40 ++++++++++++++++--- 6 files changed, 95 insertions(+), 24 deletions(-) diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 4f8852d73..7503feee4 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -9,6 +9,8 @@ const jwt = require('jsonwebtoken'); const logger = getLogger('authProvider'); class AuthProviderBase { + amoid = 'none'; + async login(login, password, options = undefined) { return {}; } @@ -51,9 +53,17 @@ class AuthProviderBase { getSingleConnectionId(req) { return null; } + + toJson() { + return { + amoid: this.amoid, + }; + } } class OAuthProvider extends AuthProviderBase { + amoid = 'oauth'; + shouldAuthorizeApi() { return true; } @@ -120,6 +130,8 @@ class OAuthProvider extends AuthProviderBase { } class ADProvider extends AuthProviderBase { + amoid = 'ad'; + async login(login, password) { const adConfig = { url: process.env.AD_URL, @@ -157,6 +169,8 @@ class ADProvider extends AuthProviderBase { } class LoginsProvider extends AuthProviderBase { + amoid = 'logins'; + async login(login, password) { if (password == process.env[`LOGIN_PASSWORD_${login}`]) { return { @@ -176,6 +190,8 @@ class LoginsProvider extends AuthProviderBase { } class DenyAllProvider extends AuthProviderBase { + amoid = 'deny'; + shouldAuthorizeApi() { return true; } @@ -233,19 +249,37 @@ function createEnvAuthProvider() { } } -let authProvider = createEnvAuthProvider(); +let defaultAuthProvider = createEnvAuthProvider(); +let authProviders = [defaultAuthProvider]; -function getAuthProvider() { - return authProvider; +function getAuthProviders() { + return authProviders; } -function setAuthProvider(value) { - authProvider = value; +function getAuthProviderById(amoid) { + return authProviders.find(x => x.amoid == amoid); +} + +function getDefaultAuthProvider() { + return defaultAuthProvider; +} + +function getAuthProviderFromReq(req) { + const authProviderId = req?.auth?.amoid || req?.user?.amoid; + return getAuthProviderById(authProviderId) ?? getDefaultAuthProvider(); +} + +function setAuthProviders(value, defaultProvider = null) { + authProviders = value; + defaultAuthProvider = defaultProvider || value[0]; } module.exports = { AuthProviderBase, detectEnvAuthProvider, - getAuthProvider, - setAuthProvider, + getAuthProviders, + getDefaultAuthProvider, + setAuthProviders, + getAuthProviderById, + getAuthProviderFromReq, }; diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index e52a32ffe..95cf7281e 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -5,7 +5,7 @@ const { getLogger } = require('dbgate-tools'); const AD = require('activedirectory2').promiseWrapper; const crypto = require('crypto'); const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon'); -const { getAuthProvider } = require('../auth/authProvider'); +const { getAuthProviderFromReq, getAuthProviders, getDefaultAuthProvider, getAuthProviderById } = require('../auth/authProvider'); const storage = require('./storage'); const logger = getLogger('auth'); @@ -28,6 +28,7 @@ function authMiddleware(req, res, next) { '/auth/login', '/stream', 'storage/get-connections-for-login-page', + 'auth/get-providers', '/connections/dblogin', '/connections/dblogin-auth', '/connections/dblogin-auth-token', @@ -37,7 +38,7 @@ function authMiddleware(req, res, next) { const isAdminPage = req.headers['x-is-admin-page'] == 'true'; - if (!isAdminPage && !getAuthProvider().shouldAuthorizeApi()) { + if (!isAdminPage && !getAuthProviderFromReq(req).shouldAuthorizeApi()) { return next(); } let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x)); @@ -68,11 +69,11 @@ function authMiddleware(req, res, next) { module.exports = { oauthToken_meta: true, async oauthToken(params) { - return getAuthProvider().oauthToken(params); + return getDefaultAuthProvider().oauthToken(params); }, login_meta: true, async login(params) { - const { login, password, isAdminPage } = params; + const { amoid, login, password, isAdminPage } = params; if (isAdminPage) { if (process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD == password) { @@ -94,7 +95,15 @@ module.exports = { return { error: 'Login failed' }; } - return getAuthProvider().login(login, password); + return getAuthProviderById(amoid).login(login, password); + }, + + getProviders_meta: true, + getProviders() { + return { + providers: getAuthProviders().map(x => x.toJson()), + default: getDefaultAuthProvider()?.amoid, + }; }, authMiddleware, diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index f551a5153..85c3cdc46 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -11,7 +11,7 @@ const AsyncLock = require('async-lock'); const currentVersion = require('../currentVersion'); const platformInfo = require('../utility/platformInfo'); const connections = require('../controllers/connections'); -const { getAuthProvider } = require('../auth/authProvider'); +const { getAuthProviderFromReq } = require('../auth/authProvider'); const lock = new AsyncLock(); @@ -28,7 +28,7 @@ module.exports = { get_meta: true, async get(_params, req) { - const authProvider = getAuthProvider(); + const authProvider = getAuthProviderFromReq(req); const login = authProvider.getCurrentLogin(req); const permissions = authProvider.getCurrentPermissions(req); const isLoginForm = authProvider.isLoginForm(); diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 731d882ae..2cd80986f 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -34,7 +34,7 @@ const platformInfo = require('./utility/platformInfo'); const getExpressPath = require('./utility/getExpressPath'); const _ = require('lodash'); const { getLogger } = require('dbgate-tools'); -const { getAuthProvider } = require('./auth/authProvider'); +const { getDefaultAuthProvider } = require('./auth/authProvider'); const logger = getLogger('main'); @@ -48,7 +48,7 @@ function start() { if (process.env.BASIC_AUTH) { async function authorizer(username, password, cb) { try { - const resp = await getAuthProvider().login(username, password); + const resp = await getDefaultAuthProvider().login(username, password); if (resp.accessToken) { cb(null, true); } else { diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js index 1d6b63abf..654ba8ceb 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -1,6 +1,6 @@ const { compilePermissions, testPermission } = require('dbgate-tools'); const _ = require('lodash'); -const { getAuthProvider } = require('../auth/authProvider'); +const { getAuthProviderFromReq } = require('../auth/authProvider'); const cachedPermissions = {}; @@ -10,7 +10,7 @@ function hasPermission(tested, req) { return true; } - const permissions = getAuthProvider().getCurrentPermissions(req); + const permissions = getAuthProviderFromReq(req).getCurrentPermissions(req); if (!cachedPermissions[permissions]) { cachedPermissions[permissions] = compilePermissions(permissions); diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index e76bd2419..a1b1f31dd 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -19,29 +19,47 @@ const config = useConfig(); let availableConnections = null; + let availableProviders = []; let isTesting = false; const testIdRef = createRef(0); let sqlConnectResult; - const values = writable({ databaseServer: null }); + let serversLoadedForAmoId = null; + + const values = writable({ amoid: null, databaseServer: null }); $: selectedConnection = availableConnections?.find(x => x.conid == $values.databaseServer); - async function loadAvailableServers() { - availableConnections = await apiCall('storage/get-connections-for-login-page'); - if (availableConnections?.length > 0) { - values.set({ databaseServer: availableConnections[0].conid }); + async function loadAvailableServers(amoid) { + if (amoid) { + availableConnections = await apiCall('storage/get-connections-for-login-page', { amoid }); + if (availableConnections?.length > 0) { + values.update(x => ({ ...x, databaseServer: availableConnections[0].conid })); + } + serversLoadedForAmoId = amoid; + } else { + availableConnections = null; } } + async function loadAvailableAuthProviders() { + const resp = await apiCall('auth/get-providers'); + availableProviders = resp.providers; + values.update(x => ({ ...x, amoid: resp.default })); + } + onMount(() => { const removed = document.getElementById('starting_dbgate_zero'); if (removed) removed.remove(); if (!isAdminPage) { - loadAvailableServers(); + loadAvailableAuthProviders(); } }); + + $: if ($values.amoid != serversLoadedForAmoId) { + loadAvailableServers($values.amoid); + }
@@ -53,6 +71,15 @@
Log In
+ {#if !isAdminPage} + ({ value: mtd.amoid, label: mtd.name }))} + /> + {/if} + {#if !isAdminPage && availableConnections} { enableApi(); const resp = await apiCall('auth/login', { + amoid: $values.amoid, isAdminPage, ...e.detail, }); From 35532b718a8a2a8c7ee8c99363b078fa7bbcc6eb Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 14:47:33 +0200 Subject: [PATCH 04/12] multiauth WIP --- packages/api/src/auth/authProvider.js | 65 ++++++++++++++++++++++++--- packages/web/src/LoginPage.svelte | 60 +++++++++++++++---------- 2 files changed, 94 insertions(+), 31 deletions(-) diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 7503feee4..2b0e69da7 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -1,7 +1,7 @@ const { getTokenSecret, getTokenLifetime } = require('./authCommon'); const _ = require('lodash'); const axios = require('axios'); -const { getLogger } = require('dbgate-tools'); +const { getLogger, getPredefinedPermissions } = require('dbgate-tools'); const AD = require('activedirectory2').promiseWrapper; const jwt = require('jsonwebtoken'); @@ -12,7 +12,15 @@ class AuthProviderBase { amoid = 'none'; async login(login, password, options = undefined) { - return {}; + return { + accessToken: jwt.sign( + { + amoid: this.amoid, + }, + getTokenSecret(), + { expiresIn: getTokenLifetime() } + ), + }; } shouldAuthorizeApi() { @@ -57,6 +65,7 @@ class AuthProviderBase { toJson() { return { amoid: this.amoid, + workflowType: 'anonymous', }; } } @@ -127,12 +136,19 @@ class OAuthProvider extends AuthProviderBase { oauthLogout: process.env.OAUTH_LOGOUT, }; } + + toJson() { + return { + ...super.toJson(), + workflowType: 'redirect', + }; + } } class ADProvider extends AuthProviderBase { amoid = 'ad'; - async login(login, password) { + async login(login, password, options = undefined) { const adConfig = { url: process.env.AD_URL, baseDN: process.env.AD_BASEDN, @@ -152,7 +168,14 @@ class ADProvider extends AuthProviderBase { return { error: `Username ${login} not allowed to log in` }; } return { - accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }), + accessToken: jwt.sign( + { + amoid: this.amoid, + login, + }, + getTokenSecret(), + { expiresIn: getTokenLifetime() } + ), }; } catch (e) { return { error: 'Login failed' }; @@ -166,15 +189,29 @@ class ADProvider extends AuthProviderBase { isLoginForm() { return !process.env.BASIC_AUTH; } + + toJson() { + return { + ...super.toJson(), + workflowType: 'credentials', + }; + } } class LoginsProvider extends AuthProviderBase { amoid = 'logins'; - async login(login, password) { + async login(login, password, options = undefined) { if (password == process.env[`LOGIN_PASSWORD_${login}`]) { return { - accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }), + accessToken: jwt.sign( + { + amoid: this.amoid, + login, + }, + getTokenSecret(), + { expiresIn: getTokenLifetime() } + ), }; } return { error: 'Invalid credentials' }; @@ -187,6 +224,13 @@ class LoginsProvider extends AuthProviderBase { isLoginForm() { return !process.env.BASIC_AUTH; } + + toJson() { + return { + ...super.toJson(), + workflowType: 'credentials', + }; + } } class DenyAllProvider extends AuthProviderBase { @@ -196,9 +240,16 @@ class DenyAllProvider extends AuthProviderBase { return true; } - async login(login, password) { + async login(login, password, options = undefined) { return { error: 'Login not allowed' }; } + + toJson() { + return { + ...super.toJson(), + workflowType: 'credentials', + }; + } } function hasEnvLogins() { diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index a1b1f31dd..79d6c071d 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -30,6 +30,9 @@ $: selectedConnection = availableConnections?.find(x => x.conid == $values.databaseServer); + $: selectedProvider = availableProviders?.find(x => x.amoid == $values.amoid); + $: workflowType = selectedProvider?.workflowType ?? 'credentials'; + async function loadAvailableServers(amoid) { if (amoid) { availableConnections = await apiCall('storage/get-connections-for-login-page', { amoid }); @@ -80,7 +83,7 @@ /> {/if} - {#if !isAdminPage && availableConnections} + {#if !isAdminPage && availableConnections && workflowType == 'database'} {/if} {:else} - {#if !isAdminPage} + {#if !isAdminPage && workflowType == 'credentials'} {/if} - + {#if workflowType == 'credentials'} + + {/if} {/if} {#if isAdminPage && $config && !$config.isAdminLoginForm} @@ -173,31 +178,38 @@ /> {:else} { enableApi(); - const resp = await apiCall('auth/login', { - amoid: $values.amoid, - isAdminPage, - ...e.detail, - }); - if (resp.error) { - internalRedirectTo( - `/?page=not-logged&error=${encodeURIComponent(resp.error)}&is-admin=${isAdminPage ? 'true' : ''}` - ); - return; - } - const { accessToken } = resp; - if (accessToken) { - localStorage.setItem(isAdminPage ? 'adminAccessToken' : 'accessToken', accessToken); - if (isAdminPage) { - internalRedirectTo('/?page=admin'); - } else { - internalRedirectTo('/'); + + if (isAdminPage || workflowType == 'credentials' || workflowType == 'anonymous') { + const resp = await apiCall('auth/login', { + amoid: $values.amoid, + isAdminPage, + ...e.detail, + }); + if (resp.error) { + internalRedirectTo( + `/?page=not-logged&error=${encodeURIComponent(resp.error)}&is-admin=${isAdminPage ? 'true' : ''}` + ); + return; } - return; + const { accessToken } = resp; + if (accessToken) { + localStorage.setItem(isAdminPage ? 'adminAccessToken' : 'accessToken', accessToken); + if (isAdminPage) { + internalRedirectTo('/?page=admin'); + } else { + internalRedirectTo('/'); + } + return; + } + internalRedirectTo(`/?page=not-logged`); } - internalRedirectTo(`/?page=not-logged`); }} /> {/if} From e179b0f20bead22d1b40567272c07b1505de7f3b Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 15:13:47 +0200 Subject: [PATCH 05/12] logout fix --- packages/api/src/controllers/auth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index 95cf7281e..c61bf96a2 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -23,6 +23,7 @@ function unauthorizedResponse(req, res, text) { function authMiddleware(req, res, next) { const SKIP_AUTH_PATHS = [ '/config/get', + '/config/logout', '/config/get-settings', '/auth/oauth-token', '/auth/login', From 5f14da38447cf5b8c9bfadb58c0e05482eba6ce7 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 16:28:24 +0200 Subject: [PATCH 06/12] multiauth refactor --- packages/api/src/auth/authProvider.js | 54 ++++++++---------------- packages/api/src/controllers/auth.js | 23 ++++++++-- packages/api/src/controllers/config.js | 15 ++++--- packages/api/src/utility/checkLicense.js | 5 ++- packages/api/src/utility/platformInfo.js | 7 ++- packages/web/src/ErrorPage.svelte | 4 +- packages/web/src/LoginPage.svelte | 16 +++++++ packages/web/src/clientAuth.ts | 17 +++++--- packages/web/src/utility/api.ts | 14 ++++++ packages/web/src/utility/resolveApi.ts | 6 +-- 10 files changed, 98 insertions(+), 63 deletions(-) diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 2b0e69da7..074a351cc 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -23,10 +23,6 @@ class AuthProviderBase { }; } - shouldAuthorizeApi() { - return false; - } - oauthToken(params) { return {}; } @@ -46,14 +42,6 @@ class AuthProviderBase { return permissions || process.env.PERMISSIONS; } - isLoginForm() { - return false; - } - - getAdditionalConfigProps() { - return {}; - } - getLoginPageConnections() { return null; } @@ -68,15 +56,17 @@ class AuthProviderBase { workflowType: 'anonymous', }; } + + redirect({ state }) { + return { + status: 'error', + }; + } } class OAuthProvider extends AuthProviderBase { amoid = 'oauth'; - shouldAuthorizeApi() { - return true; - } - async oauthToken(params) { const { redirectUri, code } = params; @@ -143,6 +133,18 @@ class OAuthProvider extends AuthProviderBase { workflowType: 'redirect', }; } + + redirect({ state, redirectUri }) { + const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : ''; + return { + status: 'ok', + uri: `${process.env.OAUTH_AUTH}?client_id=${ + process.env.OAUTH_CLIENT_ID + }&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&state=${encodeURIComponent( + state + )}${scopeParam}`, + }; + } } class ADProvider extends AuthProviderBase { @@ -182,14 +184,6 @@ class ADProvider extends AuthProviderBase { } } - shouldAuthorizeApi() { - return !process.env.BASIC_AUTH; - } - - isLoginForm() { - return !process.env.BASIC_AUTH; - } - toJson() { return { ...super.toJson(), @@ -217,14 +211,6 @@ class LoginsProvider extends AuthProviderBase { return { error: 'Invalid credentials' }; } - shouldAuthorizeApi() { - return !process.env.BASIC_AUTH; - } - - isLoginForm() { - return !process.env.BASIC_AUTH; - } - toJson() { return { ...super.toJson(), @@ -236,10 +222,6 @@ class LoginsProvider extends AuthProviderBase { class DenyAllProvider extends AuthProviderBase { amoid = 'deny'; - shouldAuthorizeApi() { - return true; - } - async login(login, password, options = undefined) { return { error: 'Login not allowed' }; } diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index c61bf96a2..fb4565424 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -5,7 +5,12 @@ const { getLogger } = require('dbgate-tools'); const AD = require('activedirectory2').promiseWrapper; const crypto = require('crypto'); const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon'); -const { getAuthProviderFromReq, getAuthProviders, getDefaultAuthProvider, getAuthProviderById } = require('../auth/authProvider'); +const { + getAuthProviderFromReq, + getAuthProviders, + getDefaultAuthProvider, + getAuthProviderById, +} = require('../auth/authProvider'); const storage = require('./storage'); const logger = getLogger('auth'); @@ -27,6 +32,7 @@ function authMiddleware(req, res, next) { '/config/get-settings', '/auth/oauth-token', '/auth/login', + '/auth/redirect', '/stream', 'storage/get-connections-for-login-page', 'auth/get-providers', @@ -37,11 +43,13 @@ function authMiddleware(req, res, next) { // console.log('********************* getAuthProvider()', getAuthProvider()); - const isAdminPage = req.headers['x-is-admin-page'] == 'true'; + // const isAdminPage = req.headers['x-is-admin-page'] == 'true'; - if (!isAdminPage && !getAuthProviderFromReq(req).shouldAuthorizeApi()) { + if (process.env.BASIC_AUTH) { + // API is not authorized for basic auth return next(); } + let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x)); const authHeader = req.headers.authorization; @@ -70,7 +78,8 @@ function authMiddleware(req, res, next) { module.exports = { oauthToken_meta: true, async oauthToken(params) { - return getDefaultAuthProvider().oauthToken(params); + const { amoid } = params; + return getAuthProviderById(amoid).oauthToken(params); }, login_meta: true, async login(params) { @@ -107,5 +116,11 @@ module.exports = { }; }, + redirect_meta: true, + async redirect(params) { + const { amoid } = params; + return getAuthProviderById(amoid).redirect(params); + }, + authMiddleware, }; diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 85c3cdc46..1c70c64a5 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -31,8 +31,6 @@ module.exports = { const authProvider = getAuthProviderFromReq(req); const login = authProvider.getCurrentLogin(req); const permissions = authProvider.getCurrentPermissions(req); - const isLoginForm = authProvider.isLoginForm(); - const additionalConfigProps = authProvider.getAdditionalConfigProps(); const isUserLoggedIn = authProvider.isUserLoggedIn(req); const singleConid = authProvider.getSingleConnectionId(req); @@ -52,12 +50,17 @@ module.exports = { isDocker: platformInfo.isDocker, isElectron: platformInfo.isElectron, isLicenseValid: platformInfo.isLicenseValid, - licenseError: platformInfo.licenseError, + checkedLicense: platformInfo.checkedLicense, permissions, login, - ...additionalConfigProps, - isLoginForm, - isAdminLoginForm: !!(process.env.STORAGE_DATABASE && process.env.ADMIN_PASSWORD && !process.env.BASIC_AUTH), + // ...additionalConfigProps, + isBasicAuth: !!process.env.BASIC_AUTH, + isAdminLoginForm: !!( + process.env.STORAGE_DATABASE && + process.env.ADMIN_PASSWORD && + !process.env.BASIC_AUTH && + platformInfo.checkedLicense?.type == 'premium' + ), storageDatabase: process.env.STORAGE_DATABASE, logsFilePath: getLogsFilePath(), connectionsFilePath: path.join(datadir(), 'connections.jsonl'), diff --git a/packages/api/src/utility/checkLicense.js b/packages/api/src/utility/checkLicense.js index 78e37202c..b43b3c1f7 100644 --- a/packages/api/src/utility/checkLicense.js +++ b/packages/api/src/utility/checkLicense.js @@ -1,5 +1,8 @@ function checkLicense() { - return null; + return { + status: 'ok', + type: 'community', + }; } module.exports = { diff --git a/packages/api/src/utility/platformInfo.js b/packages/api/src/utility/platformInfo.js index f11cbf86d..9464b77ab 100644 --- a/packages/api/src/utility/platformInfo.js +++ b/packages/api/src/utility/platformInfo.js @@ -13,8 +13,7 @@ const isDocker = fs.existsSync('/home/dbgate-docker/public'); const isDevMode = process.env.DEVMODE == '1'; const isNpmDist = !!global['IS_NPM_DIST']; const isForkedApi = processArgs.isForkedApi; -const licenseError = checkLicense(); -const isLicenseValid = licenseError == null; +const checkedLicense = checkLicense(); // function moduleAvailable(name) { // try { @@ -33,8 +32,8 @@ const platformInfo = { isElectronBundle: isElectron() && !isDevMode, isForkedApi, isElectron: isElectron(), - isLicenseValid, - licenseError, + checkedLicense, + isLicenseValid: checkedLicense?.status == 'ok', isDevMode, isNpmDist, isSnap: process.env.ELECTRON_SNAP == 'true', diff --git a/packages/web/src/ErrorPage.svelte b/packages/web/src/ErrorPage.svelte index e04f5b034..e053195da 100644 --- a/packages/web/src/ErrorPage.svelte +++ b/packages/web/src/ErrorPage.svelte @@ -24,9 +24,9 @@
Configuration error
- {#if $config?.isLicenseValid == false} + {#if $config?.checkedLicense?.status == 'error'} {:else if error} diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index 79d6c071d..e01ee7a3d 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -209,6 +209,22 @@ return; } internalRedirectTo(`/?page=not-logged`); + } else if (workflowType == 'redirect') { + const state = `dbg-oauth:${strmid}:${$values.amoid}`; + + sessionStorage.setItem('oauthState', state); + console.log('Redirecting to OAUTH provider'); + + const resp = await apiCall('auth/redirect', { + amoid: $values.amoid, + state, + redirectUri: location.origin + location.pathname, + }); + + const { uri } = resp; + if (uri) { + location.replace(uri); + } } }} /> diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index 4e9b6c60b..582f597da 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -1,4 +1,4 @@ -import { apiCall, enableApi } from './utility/api'; +import { apiCall, enableApi, getAuthCategory } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; import { isAdminPage } from './utility/pageDefs'; @@ -40,9 +40,12 @@ export function handleOauthCallback() { const sentCode = params.get('code'); if (isOauthCallback()) { + const [_prefix, strmid, amoid] = sessionStorage.getItem('oauthState').split(':'); + sessionStorage.removeItem('oauthState'); apiCall('auth/oauth-token', { code: sentCode, + amoid, redirectUri: location.origin + location.pathname, }).then(authResp => { const { accessToken, error, errorMessage } = authResp; @@ -113,7 +116,7 @@ export async function handleAuthOnStartup(config, isAdminPage = false) { return; } - if (config.isAdminLoginForm && isAdminPage) { + if (getAuthCategory(config) == 'admin') { if (localStorage.getItem('adminAccessToken')) { return; } @@ -122,10 +125,10 @@ export async function handleAuthOnStartup(config, isAdminPage = false) { return; } - if (config.oauth) { - console.log('OAUTH callback URL:', location.origin + location.pathname); - } - if (config.oauth || config.isLoginForm) { + // if (config.oauth) { + // console.log('OAUTH callback URL:', location.origin + location.pathname); + // } + if (getAuthCategory(config) == 'token') { if (localStorage.getItem('accessToken')) { return; } @@ -145,7 +148,7 @@ export async function redirectToLogin(config = null, force = false) { config = await getConfig(); } - if (config.isLoginForm) { + if (getAuthCategory(config) == 'token') { if (!force) { const params = new URLSearchParams(location.search); if (params.get('page') == 'login' || params.get('page') == 'admin-login' || params.get('page') == 'not-logged') { diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 875c44c4c..b8c4eea47 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -12,6 +12,7 @@ import uuidv1 from 'uuid/v1'; import { openWebLink } from './exportFileTools'; import { callServerPing } from './connectionsPinger'; import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache'; +import { isAdminPage } from './pageDefs'; export const strmid = uuidv1(); @@ -251,6 +252,19 @@ export function installNewVolatileConnectionListener() { }); } +export function getAuthCategory(config) { + if (config.isBasicAuth) { + return 'basic'; + } + if (isAdminPage() && config.isAdminLoginForm) { + return 'admin'; + } + if (getElectron()) { + return 'electron'; + } + return 'token'; +} + function enableApiLog() { apiLogging = true; console.log('API loggin enabled'); diff --git a/packages/web/src/utility/resolveApi.ts b/packages/web/src/utility/resolveApi.ts index 40329867b..091806077 100644 --- a/packages/web/src/utility/resolveApi.ts +++ b/packages/web/src/utility/resolveApi.ts @@ -21,8 +21,8 @@ export function resolveApiHeaders() { if (accessToken) { res['Authorization'] = `Bearer ${accessToken}`; } - if (isAdminPage()) { - res['x-is-admin-page'] = 'true'; - } + // if (isAdminPage()) { + // res['x-is-admin-page'] = 'true'; + // } return res; } From e537b43563882ddad992069f531a464c3e99722d Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Wed, 7 Aug 2024 17:02:19 +0200 Subject: [PATCH 07/12] multiauth --- packages/api/src/auth/authProvider.js | 13 +++---- packages/api/src/controllers/config.js | 1 + packages/api/src/controllers/connections.js | 12 +++--- packages/web/src/LoginPage.svelte | 4 +- packages/web/src/clientAuth.ts | 42 +++++++++++++++------ 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 074a351cc..131757ee6 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -62,6 +62,10 @@ class AuthProviderBase { status: 'error', }; } + + async getLogoutUrl() { + return null; + } } class OAuthProvider extends AuthProviderBase { @@ -118,13 +122,8 @@ class OAuthProvider extends AuthProviderBase { return { error: 'Token not found' }; } - getAdditionalConfigProps() { - return { - oauth: process.env.OAUTH_AUTH, - oauthClient: process.env.OAUTH_CLIENT_ID, - oauthScope: process.env.OAUTH_SCOPE, - oauthLogout: process.env.OAUTH_LOGOUT, - }; + async getLogoutUrl() { + return process.env.OAUTH_LOGOUT; } toJson() { diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 1c70c64a5..f7ce0ec14 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -51,6 +51,7 @@ module.exports = { isElectron: platformInfo.isElectron, isLicenseValid: platformInfo.isLicenseValid, checkedLicense: platformInfo.checkedLicense, + logoutUrl: await authProvider.getLogoutUrl(), permissions, login, // ...additionalConfigProps, diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 193d5fd5b..f8b3461ec 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -17,7 +17,7 @@ const platformInfo = require('../utility/platformInfo'); const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission'); const pipeForkLogs = require('../utility/pipeForkLogs'); const requireEngineDriver = require('../utility/requireEngineDriver'); -const { getAuthProvider } = require('../auth/authProvider'); +const { getAuthProviderById } = require('../auth/authProvider'); const logger = getLogger('connections'); @@ -413,13 +413,13 @@ module.exports = { }, dbloginAuthToken_meta: true, - async dbloginAuthToken({ code, conid, redirectUri }) { + async dbloginAuthToken({ amoid, code, conid, redirectUri }) { try { const connection = await this.getCore({ conid }); const driver = requireEngineDriver(connection); const accessToken = await driver.getAuthTokenFromCode(connection, { code, redirectUri }); const volatile = await this.saveVolatile({ conid, accessToken }); - const authProvider = getAuthProvider(); + const authProvider = getAuthProviderById(amoid); const resp = await authProvider.login(null, null, { conid: volatile._id }); return resp; } catch (err) { @@ -429,18 +429,18 @@ module.exports = { }, dbloginAuth_meta: true, - async dbloginAuth({ conid, user, password }) { + async dbloginAuth({ amoid, conid, user, password }) { if (user || password) { const saveResp = await this.saveVolatile({ conid, user, password, test: true }); if (saveResp.msgtype == 'connected') { - const loginResp = await getAuthProvider().login(user, password, { conid: saveResp._id }); + const loginResp = await getAuthProviderById(amoid).login(user, password, { conid: saveResp._id }); return loginResp; } return saveResp; } // user and password is stored in connection, volatile connection is not needed - const loginResp = await getAuthProvider().login(null, null, { conid }); + const loginResp = await getAuthProviderById(amoid).login(null, null, { conid }); return loginResp; }, }; diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index e01ee7a3d..e4880d6c0 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -130,7 +130,7 @@ { - const state = `dbg-dblogin:${strmid}:${selectedConnection?.conid}`; + const state = `dbg-dblogin:${strmid}:${selectedConnection?.conid}:${$values.amoid}`; sessionStorage.setItem('dbloginAuthState', state); // openWebLink( // `connections/dblogin?conid=${selectedConnection?.conid}&state=${encodeURIComponent(state)}&redirectUri=${ @@ -154,6 +154,7 @@ testIdRef.update(x => x + 1); const testid = testIdRef.get(); const resp = await apiCall('connections/dblogin-auth', { + amoid: $values.amoid, conid: selectedConnection.conid, user: $values['login'], password: $values['password'], @@ -169,6 +170,7 @@ } else { enableApi(); const resp = await apiCall('connections/dblogin-auth', { + amoid: $values.amoid, conid: selectedConnection.conid, }); localStorage.setItem('accessToken', resp.accessToken); diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index 582f597da..e543d5826 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -1,3 +1,4 @@ +import { ca } from 'date-fns/locale'; import { apiCall, enableApi, getAuthCategory } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; import { isAdminPage } from './utility/pageDefs'; @@ -86,13 +87,14 @@ export function handleOauthCallback() { } if (isDbLoginAuthCallback()) { - const [_prefix, strmid, conid] = sessionStorage.getItem('dbloginAuthState').split(':'); + const [_prefix, strmid, conid, amoid] = sessionStorage.getItem('dbloginAuthState').split(':'); sessionStorage.removeItem('dbloginAuthState'); apiCall('connections/dblogin-auth-token', { code: sentCode, conid, redirectUri: location.origin + location.pathname, + amoid, }).then(authResp => { if (authResp.accessToken) { localStorage.setItem('accessToken', authResp.accessToken); @@ -182,20 +184,36 @@ export function internalRedirectTo(path) { export async function doLogout() { enableApi(); const config = await getConfig(); - if (config.oauth) { - localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken'); - if (config.oauthLogout) { - window.location.href = config.oauthLogout; + const category = getAuthCategory(config); + + if (category == 'admin') { + localStorage.removeItem('adminAccessToken'); + internalRedirectTo('/?page=admin-login&is-admin=true'); + } else if (category == 'token') { + localStorage.removeItem('accessToken'); + if (config.logoutUrl) { + window.location.href = config.logoutUrl; } else { internalRedirectTo('/?page=not-logged'); } - } else if (config.isLoginForm) { - localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken'); - internalRedirectTo(`/?page=not-logged&is-admin=${isAdminPage() ? 'true' : ''}`); - } else if (config.isAdminLoginForm && isAdminPage()) { - localStorage.removeItem('adminAccessToken'); - internalRedirectTo('/?page=admin-login&is-admin=true'); - } else { + } else if (category == 'basic') { window.location.href = 'config/logout'; } + + // if (config.oauth) { + // localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken'); + // if (config.oauthLogout) { + // window.location.href = config.oauthLogout; + // } else { + // internalRedirectTo('/?page=not-logged'); + // } + // } else if (config.isLoginForm) { + // localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken'); + // internalRedirectTo(`/?page=not-logged&is-admin=${isAdminPage() ? 'true' : ''}`); + // } else if (config.isAdminLoginForm && isAdminPage()) { + // localStorage.removeItem('adminAccessToken'); + // internalRedirectTo('/?page=admin-login&is-admin=true'); + // } else { + // window.location.href = 'config/logout'; + // } } From 47729d8cc306e85ba391c9c704c92b6ebd5a0703 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 8 Aug 2024 09:16:50 +0200 Subject: [PATCH 08/12] auto login for single provider --- packages/api/src/auth/authProvider.js | 5 ++ packages/web/src/LoginPage.svelte | 97 ++++++++++++++++----------- 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 131757ee6..1b01a1d95 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -54,6 +54,7 @@ class AuthProviderBase { return { amoid: this.amoid, workflowType: 'anonymous', + name: 'Anonymous', }; } @@ -130,6 +131,7 @@ class OAuthProvider extends AuthProviderBase { return { ...super.toJson(), workflowType: 'redirect', + name: 'OAuth 2.0', }; } @@ -187,6 +189,7 @@ class ADProvider extends AuthProviderBase { return { ...super.toJson(), workflowType: 'credentials', + name: 'Active Directory', }; } } @@ -214,6 +217,7 @@ class LoginsProvider extends AuthProviderBase { return { ...super.toJson(), workflowType: 'credentials', + name: 'Login & Password', }; } } @@ -229,6 +233,7 @@ class DenyAllProvider extends AuthProviderBase { return { ...super.toJson(), workflowType: 'credentials', + name: 'Deny all', }; } } diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index e4880d6c0..40fecf917 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -45,10 +45,23 @@ } } + async function processSingleProvider(provider) { + if (provider.workflowType == 'redirect') { + await processRedirectLogin(provider.amoid); + } + if (provider.workflowType == 'anonymous') { + processCredentialsLogin(provider.amoid, {}); + } + } + async function loadAvailableAuthProviders() { const resp = await apiCall('auth/get-providers'); availableProviders = resp.providers; values.update(x => ({ ...x, amoid: resp.default })); + + if (availableProviders.length == 1) { + processSingleProvider(availableProviders[0]); + } } onMount(() => { @@ -63,6 +76,49 @@ $: if ($values.amoid != serversLoadedForAmoId) { loadAvailableServers($values.amoid); } + + async function processRedirectLogin(amoid) { + const state = `dbg-oauth:${strmid}:${amoid}`; + + sessionStorage.setItem('oauthState', state); + console.log('Redirecting to OAUTH provider'); + + const resp = await apiCall('auth/redirect', { + amoid: amoid, + state, + redirectUri: location.origin + location.pathname, + }); + + const { uri } = resp; + if (uri) { + location.replace(uri); + } + } + + async function processCredentialsLogin(amoid, detail) { + const resp = await apiCall('auth/login', { + amoid, + isAdminPage, + ...detail, + }); + if (resp.error) { + internalRedirectTo( + `/?page=not-logged&error=${encodeURIComponent(resp.error)}&is-admin=${isAdminPage ? 'true' : ''}` + ); + return; + } + const { accessToken } = resp; + if (accessToken) { + localStorage.setItem(isAdminPage ? 'adminAccessToken' : 'accessToken', accessToken); + if (isAdminPage) { + internalRedirectTo('/?page=admin'); + } else { + internalRedirectTo('/'); + } + return; + } + internalRedirectTo(`/?page=not-logged`); + }
@@ -74,7 +130,7 @@
Log In
- {#if !isAdminPage} + {#if !isAdminPage && availableProviders?.length >= 2} From a9352f2a93611cac9ffc7d6e2ab7254cb70cd3b6 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 8 Aug 2024 09:46:42 +0200 Subject: [PATCH 09/12] config error detection --- packages/api/src/controllers/config.js | 7 +++++++ packages/api/src/main.js | 2 +- packages/web/src/ErrorPage.svelte | 2 ++ packages/web/src/clientAuth.ts | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index f7ce0ec14..9456b56a9 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -39,6 +39,12 @@ module.exports = { ? await connections.getCore({ conid: singleConid }) : connections.singleConnection; + let configurationError = null; + if (process.env.STORAGE_DATABASE && process.env.BASIC_AUTH) { + configurationError = + 'Basic authentization is not allowed, when using storage. Cannot use both STORAGE_DATABASE and BASIC_AUTH'; + } + return { runAsPortal: !!connections.portalConnections, singleDbConnection: connections.singleDbConnection, @@ -51,6 +57,7 @@ module.exports = { isElectron: platformInfo.isElectron, isLicenseValid: platformInfo.isLicenseValid, checkedLicense: platformInfo.checkedLicense, + configurationError, logoutUrl: await authProvider.getLogoutUrl(), permissions, login, diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 2cd80986f..72f36415e 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -45,7 +45,7 @@ function start() { const server = http.createServer(app); - if (process.env.BASIC_AUTH) { + if (process.env.BASIC_AUTH && !process.env.STORAGE_DATABASE) { async function authorizer(username, password, cb) { try { const resp = await getDefaultAuthProvider().login(username, password); diff --git a/packages/web/src/ErrorPage.svelte b/packages/web/src/ErrorPage.svelte index e053195da..d13f16eac 100644 --- a/packages/web/src/ErrorPage.svelte +++ b/packages/web/src/ErrorPage.svelte @@ -28,6 +28,8 @@ + {:else if $config?.configurationError} + {:else if error} {:else} diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index e543d5826..98aae2ff6 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -113,7 +113,7 @@ export function handleOauthCallback() { } export async function handleAuthOnStartup(config, isAdminPage = false) { - if (!config.isLicenseValid) { + if (!config.isLicenseValid || config.configurationError) { internalRedirectTo(`/?page=error`); return; } From 9132bfb656488a7eee1ae538558c4f171fa11d75 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 8 Aug 2024 10:30:39 +0200 Subject: [PATCH 10/12] azure auth - moved from plugin into API --- packages/api/package.json | 1 + packages/api/src/shell/requirePlugin.js | 2 ++ packages/api/src/utility/azureAuth.js | 17 +++++++++++++ plugins/dbgate-plugin-mssql/package.json | 1 - .../src/backend/azureAuth.js | 22 ----------------- .../dbgate-plugin-mssql/src/backend/driver.js | 24 +++++++++++++++---- .../src/backend/tediousDriver.js | 11 ++++++--- 7 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 packages/api/src/utility/azureAuth.js delete mode 100644 plugins/dbgate-plugin-mssql/src/backend/azureAuth.js diff --git a/packages/api/package.json b/packages/api/package.json index f9f11f2f1..5f31f4545 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -17,6 +17,7 @@ "dbgate" ], "dependencies": { + "@azure/msal-node": "^2.12.0", "activedirectory2": "^2.1.0", "async-lock": "^1.2.4", "axios": "^0.21.1", diff --git a/packages/api/src/shell/requirePlugin.js b/packages/api/src/shell/requirePlugin.js index 6db3714d5..b112cafe3 100644 --- a/packages/api/src/shell/requirePlugin.js +++ b/packages/api/src/shell/requirePlugin.js @@ -3,6 +3,7 @@ const fs = require('fs'); const { pluginsdir, packagedPluginsDir, getPluginBackendPath } = require('../utility/directories'); const nativeModules = require('../nativeModules'); const platformInfo = require('../utility/platformInfo'); +const azureAuth = require('../utility/azureAuth'); const { getLogger } = require('dbgate-tools'); const logger = getLogger('requirePlugin'); @@ -12,6 +13,7 @@ const dbgateEnv = { dbgateApi: null, nativeModules, platformInfo, + azureAuth, }; function requirePlugin(packageName, requiredPlugin = null) { if (!packageName) throw new Error('Missing packageName in plugin'); diff --git a/packages/api/src/utility/azureAuth.js b/packages/api/src/utility/azureAuth.js new file mode 100644 index 000000000..f98cd4a73 --- /dev/null +++ b/packages/api/src/utility/azureAuth.js @@ -0,0 +1,17 @@ +function isAzureAuthSupported() { + return false; +} + +async function azureGetRedirectAuthUrl(options) { + return null; +} + +async function azureGetAuthTokenFromCode(options) { + return null; +} + +module.exports = { + isAzureAuthSupported, + azureGetRedirectAuthUrl, + azureGetAuthTokenFromCode, +}; diff --git a/plugins/dbgate-plugin-mssql/package.json b/plugins/dbgate-plugin-mssql/package.json index 47925b83f..22e7f1ce4 100644 --- a/plugins/dbgate-plugin-mssql/package.json +++ b/plugins/dbgate-plugin-mssql/package.json @@ -32,7 +32,6 @@ }, "devDependencies": { "async-lock": "^1.2.6", - "@azure/msal-node": "^2.12.0", "dbgate-plugin-tools": "^1.0.7", "dbgate-query-splitter": "^4.10.1", "dbgate-tools": "^5.0.0-alpha.1", diff --git a/plugins/dbgate-plugin-mssql/src/backend/azureAuth.js b/plugins/dbgate-plugin-mssql/src/backend/azureAuth.js deleted file mode 100644 index 5ebca70d1..000000000 --- a/plugins/dbgate-plugin-mssql/src/backend/azureAuth.js +++ /dev/null @@ -1,22 +0,0 @@ -function getAzureAuthTypes(platformInfo) { - return null; -} - -async function azureGetRedirectAuthUrl(connection) { - return null; -} - -async function azureGetAuthTokenFromCode(connection, code) { - return null; -} - -function getAzureAuthOptions(connection) { - return null; -} - -module.exports = { - getAzureAuthTypes, - azureGetRedirectAuthUrl, - azureGetAuthTokenFromCode, - getAzureAuthOptions, -}; diff --git a/plugins/dbgate-plugin-mssql/src/backend/driver.js b/plugins/dbgate-plugin-mssql/src/backend/driver.js index 1e688254e..055890d3b 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/driver.js @@ -8,11 +8,11 @@ const AsyncLock = require('async-lock'); const nativeDriver = require('./nativeDriver'); const lock = new AsyncLock(); const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver'); -const { getAzureAuthTypes, azureGetRedirectAuthUrl, azureGetAuthTokenFromCode } = require('./azureAuth'); const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = nativeDriver; let requireMsnodesqlv8; let platformInfo; +let azureAuth; const versionQuery = ` SELECT @@ -57,8 +57,20 @@ const driver = { getAuthTypes() { const res = []; if (requireMsnodesqlv8) res.push(...windowsAuthTypes); - const azureAuthTypes = getAzureAuthTypes(platformInfo); - if (azureAuthTypes) res.push(...azureAuthTypes); + + if (azureAuth.isAzureAuthSupported()) { + res.push( + { + title: 'NodeJs portable driver (tedious) - recomended', + name: 'tedious', + }, + { + title: 'Microsoft Entra ID (with MFA support)', + name: 'msentra', + disabledFields: ['user', 'password'], + } + ); + } if (res.length > 0) { return _.uniqBy(res, 'name'); } @@ -126,10 +138,11 @@ const driver = { return rows; }, getRedirectAuthUrl(connection, options) { - return azureGetRedirectAuthUrl(connection, options); + if (connection.authType != 'msentra') return null; + return azureAuth.azureGetRedirectAuthUrl(options); }, getAuthTokenFromCode(connection, options) { - return azureGetAuthTokenFromCode(connection, options); + return azureAuth.azureGetAuthTokenFromCode(options); }, }; @@ -138,6 +151,7 @@ driver.initialize = dbgateEnv => { requireMsnodesqlv8 = dbgateEnv.nativeModules.msnodesqlv8; } platformInfo = dbgateEnv.platformInfo; + azureAuth = dbgateEnv.azureAuth; nativeDriver.initialize(dbgateEnv); }; diff --git a/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js b/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js index 91bf1fda5..aff9291fb 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js @@ -2,7 +2,6 @@ const _ = require('lodash'); const stream = require('stream'); const tedious = require('tedious'); const makeUniqueColumnNames = require('./makeUniqueColumnNames'); -const { getAzureAuthOptions } = require('./azureAuth'); function extractTediousColumns(columns, addDriverNativeColumn = false) { const res = columns.map(col => { @@ -24,7 +23,8 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) { } async function tediousConnect(storedConnection) { - const { server, port, user, password, database, ssl, trustServerCertificate, windowsDomain, authType } = storedConnection; + const { server, port, user, password, database, ssl, trustServerCertificate, windowsDomain, authType, accessToken } = + storedConnection; return new Promise((resolve, reject) => { const connectionOptions = { encrypt: !!ssl || authType == 'msentra', @@ -44,7 +44,12 @@ async function tediousConnect(storedConnection) { const authentication = authType == 'msentra' - ? getAzureAuthOptions(storedConnection) + ? { + type: 'azure-active-directory-access-token', + options: { + token: accessToken, + }, + } : { type: windowsDomain ? 'ntlm' : 'default', options: { From cfa08286de47050277181d467f3f0a04a54ee8d5 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 8 Aug 2024 10:51:12 +0200 Subject: [PATCH 11/12] authProvider.redirect is async --- packages/api/src/auth/authProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 1b01a1d95..ca20c45d7 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -58,7 +58,7 @@ class AuthProviderBase { }; } - redirect({ state }) { + async redirect({ state }) { return { status: 'error', }; From 7e5364d40001c562da96beac055832aa9247173d Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 8 Aug 2024 11:45:21 +0200 Subject: [PATCH 12/12] msentra auth --- packages/api/src/controllers/connections.js | 12 ++++++++++++ packages/tools/src/driverBase.ts | 1 + packages/types/engines.d.ts | 1 + packages/web/src/utility/api.ts | 15 ++++++++++++--- plugins/dbgate-plugin-mssql/src/backend/driver.js | 3 +++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index f8b3461ec..7476f3803 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -443,4 +443,16 @@ module.exports = { const loginResp = await getAuthProviderById(amoid).login(null, null, { conid }); return loginResp; }, + + volatileDbloginFromAuth_meta: true, + async volatileDbloginFromAuth({ conid }, req) { + const connection = await this.getCore({ conid }); + const driver = requireEngineDriver(connection); + const accessToken = await driver.getAccessTokenFromAuth(connection, req); + if (accessToken) { + const volatile = await this.saveVolatile({ conid, accessToken }); + return volatile; + } + return null; + }, }; diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index 626ed0b24..20044d7a5 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -146,4 +146,5 @@ export const driverBase = { }, showConnectionField: (field, values) => false, showConnectionTab: field => true, + getAccessTokenFromAuth: async (connection, req) => null, }; diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index e8bfc8155..585618646 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -151,6 +151,7 @@ export interface EngineDriver { stopProfiler(pool, profiler): Promise; getRedirectAuthUrl(connection, options): Promise; getAuthTokenFromCode(connection, options): Promise; + getAccessTokenFromAuth(connection, req): Promise; analyserClass?: any; dumperClass?: any; diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index b8c4eea47..c2fea27cf 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -79,13 +79,22 @@ function wantEventSource() { } } -function processApiResponse(route, args, resp) { +async function processApiResponse(route, args, resp) { // if (apiLogging) { // console.log('<<< API RESPONSE', route, args, resp); // } if (resp?.missingCredentials) { if (resp.detail.redirectToDbLogin) { + const volatile = await apiCall('connections/volatile-dblogin-from-auth', { conid: resp.detail.conid }); + if (volatile) { + setVolatileConnectionRemapping(resp.detail.conid, volatile._id); + await callServerPing(); + dispatchCacheChange({ key: `server-status-changed` }); + batchDispatchCacheTriggers(x => x.conid == resp.detail.conid); + return null; + } + const state = `dbg-dblogin:${strmid}:${resp.detail.conid}`; localStorage.setItem('dbloginState', state); openWebLink( @@ -145,7 +154,7 @@ export async function apiCall(route: string, args: {} = undefined) { const electron = getElectron(); if (electron) { const resp = await electron.invoke(route.replace('/', '-'), args); - return processApiResponse(route, args, resp); + return await processApiResponse(route, args, resp); } else { const resp = await fetch(`${resolveApi()}/${route}`, { method: 'POST', @@ -174,7 +183,7 @@ export async function apiCall(route: string, args: {} = undefined) { } const json = await resp.json(); - return processApiResponse(route, args, json); + return await processApiResponse(route, args, json); } } diff --git a/plugins/dbgate-plugin-mssql/src/backend/driver.js b/plugins/dbgate-plugin-mssql/src/backend/driver.js index 055890d3b..3aa38f8a7 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/driver.js @@ -144,6 +144,9 @@ const driver = { getAuthTokenFromCode(connection, options) { return azureAuth.azureGetAuthTokenFromCode(options); }, + getAccessTokenFromAuth: (connection, req) => { + return req?.user?.msentraToken; + }, }; driver.initialize = dbgateEnv => {