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/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 4f8852d73..ca20c45d7 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'); @@ -9,12 +9,18 @@ const jwt = require('jsonwebtoken'); const logger = getLogger('authProvider'); class AuthProviderBase { - async login(login, password, options = undefined) { - return {}; - } + amoid = 'none'; - shouldAuthorizeApi() { - return false; + async login(login, password, options = undefined) { + return { + accessToken: jwt.sign( + { + amoid: this.amoid, + }, + getTokenSecret(), + { expiresIn: getTokenLifetime() } + ), + }; } oauthToken(params) { @@ -36,14 +42,6 @@ class AuthProviderBase { return permissions || process.env.PERMISSIONS; } - isLoginForm() { - return false; - } - - getAdditionalConfigProps() { - return {}; - } - getLoginPageConnections() { return null; } @@ -51,12 +49,28 @@ class AuthProviderBase { getSingleConnectionId(req) { return null; } + + toJson() { + return { + amoid: this.amoid, + workflowType: 'anonymous', + name: 'Anonymous', + }; + } + + async redirect({ state }) { + return { + status: 'error', + }; + } + + async getLogoutUrl() { + return null; + } } class OAuthProvider extends AuthProviderBase { - shouldAuthorizeApi() { - return true; - } + amoid = 'oauth'; async oauthToken(params) { const { redirectUri, code } = params; @@ -109,18 +123,35 @@ class OAuthProvider extends AuthProviderBase { return { error: 'Token not found' }; } - getAdditionalConfigProps() { + async getLogoutUrl() { + return process.env.OAUTH_LOGOUT; + } + + toJson() { return { - oauth: process.env.OAUTH_AUTH, - oauthClient: process.env.OAUTH_CLIENT_ID, - oauthScope: process.env.OAUTH_SCOPE, - oauthLogout: process.env.OAUTH_LOGOUT, + ...super.toJson(), + workflowType: 'redirect', + name: 'OAuth 2.0', + }; + } + + 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 { - async login(login, password) { + amoid = 'ad'; + + async login(login, password, options = undefined) { const adConfig = { url: process.env.AD_URL, baseDN: process.env.AD_BASEDN, @@ -140,48 +171,70 @@ 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' }; } } - shouldAuthorizeApi() { - return !process.env.BASIC_AUTH; - } - - isLoginForm() { - return !process.env.BASIC_AUTH; + toJson() { + return { + ...super.toJson(), + workflowType: 'credentials', + name: 'Active Directory', + }; } } class LoginsProvider extends AuthProviderBase { - async login(login, password) { + amoid = 'logins'; + + 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' }; } - shouldAuthorizeApi() { - return !process.env.BASIC_AUTH; - } - - isLoginForm() { - return !process.env.BASIC_AUTH; + toJson() { + return { + ...super.toJson(), + workflowType: 'credentials', + name: 'Login & Password', + }; } } class DenyAllProvider extends AuthProviderBase { - shouldAuthorizeApi() { - return true; + amoid = 'deny'; + + async login(login, password, options = undefined) { + return { error: 'Login not allowed' }; } - async login(login, password) { - return { error: 'Login not allowed' }; + toJson() { + return { + ...super.toJson(), + workflowType: 'credentials', + name: 'Deny all', + }; } } @@ -233,19 +286,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..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 { getAuthProvider } = require('../auth/authProvider'); +const { + getAuthProviderFromReq, + getAuthProviders, + getDefaultAuthProvider, + getAuthProviderById, +} = require('../auth/authProvider'); const storage = require('./storage'); const logger = getLogger('auth'); @@ -23,11 +28,14 @@ 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', + '/auth/redirect', '/stream', 'storage/get-connections-for-login-page', + 'auth/get-providers', '/connections/dblogin', '/connections/dblogin-auth', '/connections/dblogin-auth-token', @@ -35,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 && !getAuthProvider().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; @@ -68,11 +78,12 @@ function authMiddleware(req, res, next) { module.exports = { oauthToken_meta: true, async oauthToken(params) { - return getAuthProvider().oauthToken(params); + const { amoid } = params; + return getAuthProviderById(amoid).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 +105,21 @@ 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, + }; + }, + + 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 f551a5153..9456b56a9 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,11 +28,9 @@ 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(); - const additionalConfigProps = authProvider.getAdditionalConfigProps(); const isUserLoggedIn = authProvider.isUserLoggedIn(req); const singleConid = authProvider.getSingleConnectionId(req); @@ -41,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, @@ -52,12 +56,19 @@ module.exports = { isDocker: platformInfo.isDocker, isElectron: platformInfo.isElectron, isLicenseValid: platformInfo.isLicenseValid, - licenseError: platformInfo.licenseError, + checkedLicense: platformInfo.checkedLicense, + configurationError, + logoutUrl: await authProvider.getLogoutUrl(), 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/controllers/connections.js b/packages/api/src/controllers/connections.js index 193d5fd5b..7476f3803 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,30 @@ 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; }, + + 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/api/src/main.js b/packages/api/src/main.js index 731d882ae..72f36415e 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'); @@ -45,10 +45,10 @@ 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 getAuthProvider().login(username, password); + const resp = await getDefaultAuthProvider().login(username, password); if (resp.accessToken) { cb(null, true); } else { 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/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/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/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/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/ErrorPage.svelte b/packages/web/src/ErrorPage.svelte index e04f5b034..d13f16eac 100644 --- a/packages/web/src/ErrorPage.svelte +++ b/packages/web/src/ErrorPage.svelte @@ -24,10 +24,12 @@
Configuration error
- {#if $config?.isLicenseValid == false} + {#if $config?.checkedLicense?.status == 'error'} + {:else if $config?.configurationError} + {:else if error} {:else} diff --git a/packages/web/src/LoginPage.svelte b/packages/web/src/LoginPage.svelte index e76bd2419..40fecf917 100644 --- a/packages/web/src/LoginPage.svelte +++ b/packages/web/src/LoginPage.svelte @@ -19,18 +19,48 @@ 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 }); + $: 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 }); + if (availableConnections?.length > 0) { + values.update(x => ({ ...x, databaseServer: availableConnections[0].conid })); + } + serversLoadedForAmoId = amoid; + } else { + availableConnections = null; + } + } + + 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]); } } @@ -39,9 +69,56 @@ if (removed) removed.remove(); if (!isAdminPage) { - loadAvailableServers(); + loadAvailableAuthProviders(); } }); + + $: 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`); + }
@@ -53,7 +130,16 @@
Log In
- {#if !isAdminPage && availableConnections} + {#if !isAdminPage && availableProviders?.length >= 2} + ({ value: mtd.amoid, label: mtd.name }))} + /> + {/if} + + {#if !isAdminPage && availableConnections && workflowType == 'database'} {/if} {:else} - {#if !isAdminPage} + {#if !isAdminPage && workflowType == 'credentials'} {/if} - + {#if workflowType == 'credentials'} + + {/if} {/if} {#if isAdminPage && $config && !$config.isAdminLoginForm} @@ -98,7 +186,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=${ @@ -122,6 +210,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'], @@ -137,6 +226,7 @@ } else { enableApi(); const resp = await apiCall('connections/dblogin-auth', { + amoid: $values.amoid, conid: selectedConnection.conid, }); localStorage.setItem('accessToken', resp.accessToken); @@ -146,30 +236,19 @@ /> {:else} { enableApi(); - const resp = await apiCall('auth/login', { - isAdminPage, - ...e.detail, - }); - if (resp.error) { - internalRedirectTo( - `/?page=not-logged&error=${encodeURIComponent(resp.error)}&is-admin=${isAdminPage ? 'true' : ''}` - ); - return; + + if (isAdminPage || workflowType == 'credentials' || workflowType == 'anonymous') { + await processCredentialsLogin($values.amoid, e.detail); + } else if (workflowType == 'redirect') { + await processRedirectLogin($values.amoid); } - const { accessToken } = resp; - if (accessToken) { - localStorage.setItem(isAdminPage ? 'adminAccessToken' : 'accessToken', accessToken); - if (isAdminPage) { - internalRedirectTo('/?page=admin'); - } else { - internalRedirectTo('/'); - } - return; - } - internalRedirectTo(`/?page=not-logged`); }} /> {/if} 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; + } diff --git a/packages/web/src/clientAuth.ts b/packages/web/src/clientAuth.ts index 4e9b6c60b..98aae2ff6 100644 --- a/packages/web/src/clientAuth.ts +++ b/packages/web/src/clientAuth.ts @@ -1,4 +1,5 @@ -import { apiCall, enableApi } from './utility/api'; +import { ca } from 'date-fns/locale'; +import { apiCall, enableApi, getAuthCategory } from './utility/api'; import { getConfig } from './utility/metadataLoaders'; import { isAdminPage } from './utility/pageDefs'; @@ -40,9 +41,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; @@ -83,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); @@ -108,12 +113,12 @@ export function handleOauthCallback() { } export async function handleAuthOnStartup(config, isAdminPage = false) { - if (!config.isLicenseValid) { + if (!config.isLicenseValid || config.configurationError) { internalRedirectTo(`/?page=error`); return; } - if (config.isAdminLoginForm && isAdminPage) { + if (getAuthCategory(config) == 'admin') { if (localStorage.getItem('adminAccessToken')) { return; } @@ -122,10 +127,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 +150,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') { @@ -179,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'; + // } } 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', diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index 875c44c4c..c2fea27cf 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(); @@ -78,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( @@ -144,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', @@ -173,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); } } @@ -251,6 +261,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; } 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..3aa38f8a7 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,14 @@ 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); + }, + getAccessTokenFromAuth: (connection, req) => { + return req?.user?.msentraToken; }, }; @@ -138,6 +154,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: {