Merge branch 'develop'

This commit is contained in:
SPRINX0\prochazka
2024-08-08 12:30:30 +02:00
25 changed files with 457 additions and 171 deletions

View File

@@ -17,6 +17,7 @@
"dbgate" "dbgate"
], ],
"dependencies": { "dependencies": {
"@azure/msal-node": "^2.12.0",
"activedirectory2": "^2.1.0", "activedirectory2": "^2.1.0",
"async-lock": "^1.2.4", "async-lock": "^1.2.4",
"axios": "^0.21.1", "axios": "^0.21.1",

View File

@@ -1,7 +1,7 @@
const { getTokenSecret, getTokenLifetime } = require('./authCommon'); const { getTokenSecret, getTokenLifetime } = require('./authCommon');
const _ = require('lodash'); const _ = require('lodash');
const axios = require('axios'); const axios = require('axios');
const { getLogger } = require('dbgate-tools'); const { getLogger, getPredefinedPermissions } = require('dbgate-tools');
const AD = require('activedirectory2').promiseWrapper; const AD = require('activedirectory2').promiseWrapper;
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
@@ -9,12 +9,18 @@ const jwt = require('jsonwebtoken');
const logger = getLogger('authProvider'); const logger = getLogger('authProvider');
class AuthProviderBase { class AuthProviderBase {
async login(login, password, options = undefined) { amoid = 'none';
return {};
}
shouldAuthorizeApi() { async login(login, password, options = undefined) {
return false; return {
accessToken: jwt.sign(
{
amoid: this.amoid,
},
getTokenSecret(),
{ expiresIn: getTokenLifetime() }
),
};
} }
oauthToken(params) { oauthToken(params) {
@@ -36,14 +42,6 @@ class AuthProviderBase {
return permissions || process.env.PERMISSIONS; return permissions || process.env.PERMISSIONS;
} }
isLoginForm() {
return false;
}
getAdditionalConfigProps() {
return {};
}
getLoginPageConnections() { getLoginPageConnections() {
return null; return null;
} }
@@ -51,12 +49,28 @@ class AuthProviderBase {
getSingleConnectionId(req) { getSingleConnectionId(req) {
return null; 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 { class OAuthProvider extends AuthProviderBase {
shouldAuthorizeApi() { amoid = 'oauth';
return true;
}
async oauthToken(params) { async oauthToken(params) {
const { redirectUri, code } = params; const { redirectUri, code } = params;
@@ -109,18 +123,35 @@ class OAuthProvider extends AuthProviderBase {
return { error: 'Token not found' }; return { error: 'Token not found' };
} }
getAdditionalConfigProps() { async getLogoutUrl() {
return process.env.OAUTH_LOGOUT;
}
toJson() {
return { return {
oauth: process.env.OAUTH_AUTH, ...super.toJson(),
oauthClient: process.env.OAUTH_CLIENT_ID, workflowType: 'redirect',
oauthScope: process.env.OAUTH_SCOPE, name: 'OAuth 2.0',
oauthLogout: process.env.OAUTH_LOGOUT, };
}
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 { class ADProvider extends AuthProviderBase {
async login(login, password) { amoid = 'ad';
async login(login, password, options = undefined) {
const adConfig = { const adConfig = {
url: process.env.AD_URL, url: process.env.AD_URL,
baseDN: process.env.AD_BASEDN, baseDN: process.env.AD_BASEDN,
@@ -140,48 +171,70 @@ class ADProvider extends AuthProviderBase {
return { error: `Username ${login} not allowed to log in` }; return { error: `Username ${login} not allowed to log in` };
} }
return { return {
accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }), accessToken: jwt.sign(
{
amoid: this.amoid,
login,
},
getTokenSecret(),
{ expiresIn: getTokenLifetime() }
),
}; };
} catch (e) { } catch (e) {
return { error: 'Login failed' }; return { error: 'Login failed' };
} }
} }
shouldAuthorizeApi() { toJson() {
return !process.env.BASIC_AUTH; return {
} ...super.toJson(),
workflowType: 'credentials',
isLoginForm() { name: 'Active Directory',
return !process.env.BASIC_AUTH; };
} }
} }
class LoginsProvider extends AuthProviderBase { class LoginsProvider extends AuthProviderBase {
async login(login, password) { amoid = 'logins';
async login(login, password, options = undefined) {
if (password == process.env[`LOGIN_PASSWORD_${login}`]) { if (password == process.env[`LOGIN_PASSWORD_${login}`]) {
return { return {
accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }), accessToken: jwt.sign(
{
amoid: this.amoid,
login,
},
getTokenSecret(),
{ expiresIn: getTokenLifetime() }
),
}; };
} }
return { error: 'Invalid credentials' }; return { error: 'Invalid credentials' };
} }
shouldAuthorizeApi() { toJson() {
return !process.env.BASIC_AUTH; return {
} ...super.toJson(),
workflowType: 'credentials',
isLoginForm() { name: 'Login & Password',
return !process.env.BASIC_AUTH; };
} }
} }
class DenyAllProvider extends AuthProviderBase { class DenyAllProvider extends AuthProviderBase {
shouldAuthorizeApi() { amoid = 'deny';
return true;
async login(login, password, options = undefined) {
return { error: 'Login not allowed' };
} }
async login(login, password) { toJson() {
return { error: 'Login not allowed' }; 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() { function getAuthProviders() {
return authProvider; return authProviders;
} }
function setAuthProvider(value) { function getAuthProviderById(amoid) {
authProvider = value; 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 = { module.exports = {
AuthProviderBase, AuthProviderBase,
detectEnvAuthProvider, detectEnvAuthProvider,
getAuthProvider, getAuthProviders,
setAuthProvider, getDefaultAuthProvider,
setAuthProviders,
getAuthProviderById,
getAuthProviderFromReq,
}; };

View File

@@ -5,7 +5,12 @@ const { getLogger } = require('dbgate-tools');
const AD = require('activedirectory2').promiseWrapper; const AD = require('activedirectory2').promiseWrapper;
const crypto = require('crypto'); const crypto = require('crypto');
const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon'); const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon');
const { getAuthProvider } = require('../auth/authProvider'); const {
getAuthProviderFromReq,
getAuthProviders,
getDefaultAuthProvider,
getAuthProviderById,
} = require('../auth/authProvider');
const storage = require('./storage'); const storage = require('./storage');
const logger = getLogger('auth'); const logger = getLogger('auth');
@@ -23,11 +28,14 @@ function unauthorizedResponse(req, res, text) {
function authMiddleware(req, res, next) { function authMiddleware(req, res, next) {
const SKIP_AUTH_PATHS = [ const SKIP_AUTH_PATHS = [
'/config/get', '/config/get',
'/config/logout',
'/config/get-settings', '/config/get-settings',
'/auth/oauth-token', '/auth/oauth-token',
'/auth/login', '/auth/login',
'/auth/redirect',
'/stream', '/stream',
'storage/get-connections-for-login-page', 'storage/get-connections-for-login-page',
'auth/get-providers',
'/connections/dblogin', '/connections/dblogin',
'/connections/dblogin-auth', '/connections/dblogin-auth',
'/connections/dblogin-auth-token', '/connections/dblogin-auth-token',
@@ -35,11 +43,13 @@ function authMiddleware(req, res, next) {
// console.log('********************* getAuthProvider()', getAuthProvider()); // 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(); return next();
} }
let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x)); let skipAuth = !!SKIP_AUTH_PATHS.find(x => req.path == getExpressPath(x));
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
@@ -68,11 +78,12 @@ function authMiddleware(req, res, next) {
module.exports = { module.exports = {
oauthToken_meta: true, oauthToken_meta: true,
async oauthToken(params) { async oauthToken(params) {
return getAuthProvider().oauthToken(params); const { amoid } = params;
return getAuthProviderById(amoid).oauthToken(params);
}, },
login_meta: true, login_meta: true,
async login(params) { async login(params) {
const { login, password, isAdminPage } = params; const { amoid, login, password, isAdminPage } = params;
if (isAdminPage) { if (isAdminPage) {
if (process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD == password) { if (process.env.ADMIN_PASSWORD && process.env.ADMIN_PASSWORD == password) {
@@ -94,7 +105,21 @@ module.exports = {
return { error: 'Login failed' }; 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, authMiddleware,

View File

@@ -11,7 +11,7 @@ const AsyncLock = require('async-lock');
const currentVersion = require('../currentVersion'); const currentVersion = require('../currentVersion');
const platformInfo = require('../utility/platformInfo'); const platformInfo = require('../utility/platformInfo');
const connections = require('../controllers/connections'); const connections = require('../controllers/connections');
const { getAuthProvider } = require('../auth/authProvider'); const { getAuthProviderFromReq } = require('../auth/authProvider');
const lock = new AsyncLock(); const lock = new AsyncLock();
@@ -28,11 +28,9 @@ module.exports = {
get_meta: true, get_meta: true,
async get(_params, req) { async get(_params, req) {
const authProvider = getAuthProvider(); const authProvider = getAuthProviderFromReq(req);
const login = authProvider.getCurrentLogin(req); const login = authProvider.getCurrentLogin(req);
const permissions = authProvider.getCurrentPermissions(req); const permissions = authProvider.getCurrentPermissions(req);
const isLoginForm = authProvider.isLoginForm();
const additionalConfigProps = authProvider.getAdditionalConfigProps();
const isUserLoggedIn = authProvider.isUserLoggedIn(req); const isUserLoggedIn = authProvider.isUserLoggedIn(req);
const singleConid = authProvider.getSingleConnectionId(req); const singleConid = authProvider.getSingleConnectionId(req);
@@ -41,6 +39,12 @@ module.exports = {
? await connections.getCore({ conid: singleConid }) ? await connections.getCore({ conid: singleConid })
: connections.singleConnection; : 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 { return {
runAsPortal: !!connections.portalConnections, runAsPortal: !!connections.portalConnections,
singleDbConnection: connections.singleDbConnection, singleDbConnection: connections.singleDbConnection,
@@ -52,12 +56,19 @@ module.exports = {
isDocker: platformInfo.isDocker, isDocker: platformInfo.isDocker,
isElectron: platformInfo.isElectron, isElectron: platformInfo.isElectron,
isLicenseValid: platformInfo.isLicenseValid, isLicenseValid: platformInfo.isLicenseValid,
licenseError: platformInfo.licenseError, checkedLicense: platformInfo.checkedLicense,
configurationError,
logoutUrl: await authProvider.getLogoutUrl(),
permissions, permissions,
login, login,
...additionalConfigProps, // ...additionalConfigProps,
isLoginForm, isBasicAuth: !!process.env.BASIC_AUTH,
isAdminLoginForm: !!(process.env.STORAGE_DATABASE && process.env.ADMIN_PASSWORD && !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, storageDatabase: process.env.STORAGE_DATABASE,
logsFilePath: getLogsFilePath(), logsFilePath: getLogsFilePath(),
connectionsFilePath: path.join(datadir(), 'connections.jsonl'), connectionsFilePath: path.join(datadir(), 'connections.jsonl'),

View File

@@ -17,7 +17,7 @@ const platformInfo = require('../utility/platformInfo');
const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission'); const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission');
const pipeForkLogs = require('../utility/pipeForkLogs'); const pipeForkLogs = require('../utility/pipeForkLogs');
const requireEngineDriver = require('../utility/requireEngineDriver'); const requireEngineDriver = require('../utility/requireEngineDriver');
const { getAuthProvider } = require('../auth/authProvider'); const { getAuthProviderById } = require('../auth/authProvider');
const logger = getLogger('connections'); const logger = getLogger('connections');
@@ -413,13 +413,13 @@ module.exports = {
}, },
dbloginAuthToken_meta: true, dbloginAuthToken_meta: true,
async dbloginAuthToken({ code, conid, redirectUri }) { async dbloginAuthToken({ amoid, code, conid, redirectUri }) {
try { try {
const connection = await this.getCore({ conid }); const connection = await this.getCore({ conid });
const driver = requireEngineDriver(connection); const driver = requireEngineDriver(connection);
const accessToken = await driver.getAuthTokenFromCode(connection, { code, redirectUri }); const accessToken = await driver.getAuthTokenFromCode(connection, { code, redirectUri });
const volatile = await this.saveVolatile({ conid, accessToken }); const volatile = await this.saveVolatile({ conid, accessToken });
const authProvider = getAuthProvider(); const authProvider = getAuthProviderById(amoid);
const resp = await authProvider.login(null, null, { conid: volatile._id }); const resp = await authProvider.login(null, null, { conid: volatile._id });
return resp; return resp;
} catch (err) { } catch (err) {
@@ -429,18 +429,30 @@ module.exports = {
}, },
dbloginAuth_meta: true, dbloginAuth_meta: true,
async dbloginAuth({ conid, user, password }) { async dbloginAuth({ amoid, conid, user, password }) {
if (user || password) { if (user || password) {
const saveResp = await this.saveVolatile({ conid, user, password, test: true }); const saveResp = await this.saveVolatile({ conid, user, password, test: true });
if (saveResp.msgtype == 'connected') { 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 loginResp;
} }
return saveResp; return saveResp;
} }
// user and password is stored in connection, volatile connection is not needed // 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; 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;
},
}; };

View File

@@ -34,7 +34,7 @@ const platformInfo = require('./utility/platformInfo');
const getExpressPath = require('./utility/getExpressPath'); const getExpressPath = require('./utility/getExpressPath');
const _ = require('lodash'); const _ = require('lodash');
const { getLogger } = require('dbgate-tools'); const { getLogger } = require('dbgate-tools');
const { getAuthProvider } = require('./auth/authProvider'); const { getDefaultAuthProvider } = require('./auth/authProvider');
const logger = getLogger('main'); const logger = getLogger('main');
@@ -45,10 +45,10 @@ function start() {
const server = http.createServer(app); 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) { async function authorizer(username, password, cb) {
try { try {
const resp = await getAuthProvider().login(username, password); const resp = await getDefaultAuthProvider().login(username, password);
if (resp.accessToken) { if (resp.accessToken) {
cb(null, true); cb(null, true);
} else { } else {

View File

@@ -3,6 +3,7 @@ const fs = require('fs');
const { pluginsdir, packagedPluginsDir, getPluginBackendPath } = require('../utility/directories'); const { pluginsdir, packagedPluginsDir, getPluginBackendPath } = require('../utility/directories');
const nativeModules = require('../nativeModules'); const nativeModules = require('../nativeModules');
const platformInfo = require('../utility/platformInfo'); const platformInfo = require('../utility/platformInfo');
const azureAuth = require('../utility/azureAuth');
const { getLogger } = require('dbgate-tools'); const { getLogger } = require('dbgate-tools');
const logger = getLogger('requirePlugin'); const logger = getLogger('requirePlugin');
@@ -12,6 +13,7 @@ const dbgateEnv = {
dbgateApi: null, dbgateApi: null,
nativeModules, nativeModules,
platformInfo, platformInfo,
azureAuth,
}; };
function requirePlugin(packageName, requiredPlugin = null) { function requirePlugin(packageName, requiredPlugin = null) {
if (!packageName) throw new Error('Missing packageName in plugin'); if (!packageName) throw new Error('Missing packageName in plugin');

View File

@@ -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,
};

View File

@@ -1,5 +1,8 @@
function checkLicense() { function checkLicense() {
return null; return {
status: 'ok',
type: 'community',
};
} }
module.exports = { module.exports = {

View File

@@ -1,6 +1,6 @@
const { compilePermissions, testPermission } = require('dbgate-tools'); const { compilePermissions, testPermission } = require('dbgate-tools');
const _ = require('lodash'); const _ = require('lodash');
const { getAuthProvider } = require('../auth/authProvider'); const { getAuthProviderFromReq } = require('../auth/authProvider');
const cachedPermissions = {}; const cachedPermissions = {};
@@ -10,7 +10,7 @@ function hasPermission(tested, req) {
return true; return true;
} }
const permissions = getAuthProvider().getCurrentPermissions(req); const permissions = getAuthProviderFromReq(req).getCurrentPermissions(req);
if (!cachedPermissions[permissions]) { if (!cachedPermissions[permissions]) {
cachedPermissions[permissions] = compilePermissions(permissions); cachedPermissions[permissions] = compilePermissions(permissions);

View File

@@ -13,8 +13,7 @@ const isDocker = fs.existsSync('/home/dbgate-docker/public');
const isDevMode = process.env.DEVMODE == '1'; const isDevMode = process.env.DEVMODE == '1';
const isNpmDist = !!global['IS_NPM_DIST']; const isNpmDist = !!global['IS_NPM_DIST'];
const isForkedApi = processArgs.isForkedApi; const isForkedApi = processArgs.isForkedApi;
const licenseError = checkLicense(); const checkedLicense = checkLicense();
const isLicenseValid = licenseError == null;
// function moduleAvailable(name) { // function moduleAvailable(name) {
// try { // try {
@@ -33,8 +32,8 @@ const platformInfo = {
isElectronBundle: isElectron() && !isDevMode, isElectronBundle: isElectron() && !isDevMode,
isForkedApi, isForkedApi,
isElectron: isElectron(), isElectron: isElectron(),
isLicenseValid, checkedLicense,
licenseError, isLicenseValid: checkedLicense?.status == 'ok',
isDevMode, isDevMode,
isNpmDist, isNpmDist,
isSnap: process.env.ELECTRON_SNAP == 'true', isSnap: process.env.ELECTRON_SNAP == 'true',

View File

@@ -146,4 +146,5 @@ export const driverBase = {
}, },
showConnectionField: (field, values) => false, showConnectionField: (field, values) => false,
showConnectionTab: field => true, showConnectionTab: field => true,
getAccessTokenFromAuth: async (connection, req) => null,
}; };

View File

@@ -151,6 +151,7 @@ export interface EngineDriver {
stopProfiler(pool, profiler): Promise<void>; stopProfiler(pool, profiler): Promise<void>;
getRedirectAuthUrl(connection, options): Promise<string>; getRedirectAuthUrl(connection, options): Promise<string>;
getAuthTokenFromCode(connection, options): Promise<string>; getAuthTokenFromCode(connection, options): Promise<string>;
getAccessTokenFromAuth(connection, req): Promise<string | null>;
analyserClass?: any; analyserClass?: any;
dumperClass?: any; dumperClass?: any;

View File

@@ -24,10 +24,12 @@
</div> </div>
<div class="box"> <div class="box">
<div class="heading">Configuration error</div> <div class="heading">Configuration error</div>
{#if $config?.isLicenseValid == false} {#if $config?.checkedLicense?.status == 'error'}
<ErrorInfo <ErrorInfo
message={`Invalid license. Please contact sales@dbgate.eu for more details. ${$config?.licenseError}`} message={`Invalid license. Please contact sales@dbgate.eu for more details. ${$config?.checkedLicense?.error}`}
/> />
{:else if $config?.configurationError}
<ErrorInfo message={$config?.configurationError} />
{:else if error} {:else if error}
<ErrorInfo message={error} /> <ErrorInfo message={error} />
{:else} {:else}

View File

@@ -19,18 +19,48 @@
const config = useConfig(); const config = useConfig();
let availableConnections = null; let availableConnections = null;
let availableProviders = [];
let isTesting = false; let isTesting = false;
const testIdRef = createRef(0); const testIdRef = createRef(0);
let sqlConnectResult; 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); $: selectedConnection = availableConnections?.find(x => x.conid == $values.databaseServer);
async function loadAvailableServers() { $: selectedProvider = availableProviders?.find(x => x.amoid == $values.amoid);
availableConnections = await apiCall('storage/get-connections-for-login-page'); $: workflowType = selectedProvider?.workflowType ?? 'credentials';
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 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 (removed) removed.remove();
if (!isAdminPage) { 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`);
}
</script> </script>
<div class="root theme-light theme-type-light"> <div class="root theme-light theme-type-light">
@@ -53,7 +130,16 @@
<div class="box"> <div class="box">
<div class="heading">Log In</div> <div class="heading">Log In</div>
<FormProviderCore {values}> <FormProviderCore {values}>
{#if !isAdminPage && availableConnections} {#if !isAdminPage && availableProviders?.length >= 2}
<FormSelectField
label="Authentization method"
name="amoid"
isNative
options={availableProviders.map(mtd => ({ value: mtd.amoid, label: mtd.name }))}
/>
{/if}
{#if !isAdminPage && availableConnections && workflowType == 'database'}
<FormSelectField <FormSelectField
label="Database server" label="Database server"
name="databaseServer" name="databaseServer"
@@ -70,10 +156,12 @@
<FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput /> <FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput />
{/if} {/if}
{:else} {:else}
{#if !isAdminPage} {#if !isAdminPage && workflowType == 'credentials'}
<FormTextField label="Username" name="login" autocomplete="username" saveOnInput /> <FormTextField label="Username" name="login" autocomplete="username" saveOnInput />
{/if} {/if}
<FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput /> {#if workflowType == 'credentials'}
<FormPasswordField label="Password" name="password" autocomplete="current-password" saveOnInput />
{/if}
{/if} {/if}
{#if isAdminPage && $config && !$config.isAdminLoginForm} {#if isAdminPage && $config && !$config.isAdminLoginForm}
@@ -98,7 +186,7 @@
<FormSubmit <FormSubmit
value="Open database login page" value="Open database login page"
on:click={async e => { on:click={async e => {
const state = `dbg-dblogin:${strmid}:${selectedConnection?.conid}`; const state = `dbg-dblogin:${strmid}:${selectedConnection?.conid}:${$values.amoid}`;
sessionStorage.setItem('dbloginAuthState', state); sessionStorage.setItem('dbloginAuthState', state);
// openWebLink( // openWebLink(
// `connections/dblogin?conid=${selectedConnection?.conid}&state=${encodeURIComponent(state)}&redirectUri=${ // `connections/dblogin?conid=${selectedConnection?.conid}&state=${encodeURIComponent(state)}&redirectUri=${
@@ -122,6 +210,7 @@
testIdRef.update(x => x + 1); testIdRef.update(x => x + 1);
const testid = testIdRef.get(); const testid = testIdRef.get();
const resp = await apiCall('connections/dblogin-auth', { const resp = await apiCall('connections/dblogin-auth', {
amoid: $values.amoid,
conid: selectedConnection.conid, conid: selectedConnection.conid,
user: $values['login'], user: $values['login'],
password: $values['password'], password: $values['password'],
@@ -137,6 +226,7 @@
} else { } else {
enableApi(); enableApi();
const resp = await apiCall('connections/dblogin-auth', { const resp = await apiCall('connections/dblogin-auth', {
amoid: $values.amoid,
conid: selectedConnection.conid, conid: selectedConnection.conid,
}); });
localStorage.setItem('accessToken', resp.accessToken); localStorage.setItem('accessToken', resp.accessToken);
@@ -146,30 +236,19 @@
/> />
{:else} {:else}
<FormSubmit <FormSubmit
value={isAdminPage ? 'Log In as Administrator' : 'Log In'} value={isAdminPage
? 'Log In as Administrator'
: workflowType == 'redirect'
? 'Redirect to login page'
: 'Log In'}
on:click={async e => { on:click={async e => {
enableApi(); enableApi();
const resp = await apiCall('auth/login', {
isAdminPage, if (isAdminPage || workflowType == 'credentials' || workflowType == 'anonymous') {
...e.detail, await processCredentialsLogin($values.amoid, e.detail);
}); } else if (workflowType == 'redirect') {
if (resp.error) { await processRedirectLogin($values.amoid);
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`);
}} }}
/> />
{/if} {/if}

View File

@@ -11,10 +11,12 @@
export function activate() { export function activate() {
activator?.activate(); activator?.activate();
} }
export let scrollContent;
</script> </script>
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content" class:scrollContent>
<slot /> <slot />
</div> </div>
@@ -44,4 +46,8 @@
flex-wrap: wrap; flex-wrap: wrap;
background: var(--theme-bg-1); background: var(--theme-bg-1);
} }
.scrollContent {
overflow-y: auto;
}
</style> </style>

View File

@@ -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 { getConfig } from './utility/metadataLoaders';
import { isAdminPage } from './utility/pageDefs'; import { isAdminPage } from './utility/pageDefs';
@@ -40,9 +41,12 @@ export function handleOauthCallback() {
const sentCode = params.get('code'); const sentCode = params.get('code');
if (isOauthCallback()) { if (isOauthCallback()) {
const [_prefix, strmid, amoid] = sessionStorage.getItem('oauthState').split(':');
sessionStorage.removeItem('oauthState'); sessionStorage.removeItem('oauthState');
apiCall('auth/oauth-token', { apiCall('auth/oauth-token', {
code: sentCode, code: sentCode,
amoid,
redirectUri: location.origin + location.pathname, redirectUri: location.origin + location.pathname,
}).then(authResp => { }).then(authResp => {
const { accessToken, error, errorMessage } = authResp; const { accessToken, error, errorMessage } = authResp;
@@ -83,13 +87,14 @@ export function handleOauthCallback() {
} }
if (isDbLoginAuthCallback()) { if (isDbLoginAuthCallback()) {
const [_prefix, strmid, conid] = sessionStorage.getItem('dbloginAuthState').split(':'); const [_prefix, strmid, conid, amoid] = sessionStorage.getItem('dbloginAuthState').split(':');
sessionStorage.removeItem('dbloginAuthState'); sessionStorage.removeItem('dbloginAuthState');
apiCall('connections/dblogin-auth-token', { apiCall('connections/dblogin-auth-token', {
code: sentCode, code: sentCode,
conid, conid,
redirectUri: location.origin + location.pathname, redirectUri: location.origin + location.pathname,
amoid,
}).then(authResp => { }).then(authResp => {
if (authResp.accessToken) { if (authResp.accessToken) {
localStorage.setItem('accessToken', authResp.accessToken); localStorage.setItem('accessToken', authResp.accessToken);
@@ -108,12 +113,12 @@ export function handleOauthCallback() {
} }
export async function handleAuthOnStartup(config, isAdminPage = false) { export async function handleAuthOnStartup(config, isAdminPage = false) {
if (!config.isLicenseValid) { if (!config.isLicenseValid || config.configurationError) {
internalRedirectTo(`/?page=error`); internalRedirectTo(`/?page=error`);
return; return;
} }
if (config.isAdminLoginForm && isAdminPage) { if (getAuthCategory(config) == 'admin') {
if (localStorage.getItem('adminAccessToken')) { if (localStorage.getItem('adminAccessToken')) {
return; return;
} }
@@ -122,10 +127,10 @@ export async function handleAuthOnStartup(config, isAdminPage = false) {
return; return;
} }
if (config.oauth) { // if (config.oauth) {
console.log('OAUTH callback URL:', location.origin + location.pathname); // console.log('OAUTH callback URL:', location.origin + location.pathname);
} // }
if (config.oauth || config.isLoginForm) { if (getAuthCategory(config) == 'token') {
if (localStorage.getItem('accessToken')) { if (localStorage.getItem('accessToken')) {
return; return;
} }
@@ -145,7 +150,7 @@ export async function redirectToLogin(config = null, force = false) {
config = await getConfig(); config = await getConfig();
} }
if (config.isLoginForm) { if (getAuthCategory(config) == 'token') {
if (!force) { if (!force) {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
if (params.get('page') == 'login' || params.get('page') == 'admin-login' || params.get('page') == 'not-logged') { 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() { export async function doLogout() {
enableApi(); enableApi();
const config = await getConfig(); const config = await getConfig();
if (config.oauth) { const category = getAuthCategory(config);
localStorage.removeItem(isAdminPage() ? 'adminAccessToken' : 'accessToken');
if (config.oauthLogout) { if (category == 'admin') {
window.location.href = config.oauthLogout; 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 { } else {
internalRedirectTo('/?page=not-logged'); internalRedirectTo('/?page=not-logged');
} }
} else if (config.isLoginForm) { } else if (category == 'basic') {
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'; 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';
// }
} }

View File

@@ -2,10 +2,19 @@
import FontIcon from '../icons/FontIcon.svelte'; import FontIcon from '../icons/FontIcon.svelte';
export let collapsed; export let collapsed;
export let vertical = false;
</script> </script>
<div on:click|stopPropagation class="collapseButtonMarker"> <div on:click|stopPropagation class="collapseButtonMarker">
<FontIcon icon={collapsed ? 'icon triple-right' : 'icon triple-left'} /> <FontIcon
icon={collapsed
? vertical
? 'icon triple-down'
: 'icon triple-right'
: vertical
? 'icon triple-up'
: 'icon triple-left'}
/>
</div> </div>
<style> <style>
@@ -17,10 +26,12 @@
top: 4px; top: 4px;
background-color: var(--theme-bg-1); background-color: var(--theme-bg-1);
border: 1px solid var(--theme-bg-1); */ border: 1px solid var(--theme-bg-1); */
margin: 1px;
} }
div:hover { div:hover {
color: var(--theme-font-hover); color: var(--theme-font-hover);
border: 1px solid var(--theme-font-1); border: 1px solid var(--theme-font-1);
margin: 0px;
} }
</style> </style>

View File

@@ -89,6 +89,8 @@
'icon arrow-right-bold': 'mdi mdi-arrow-right-bold', 'icon arrow-right-bold': 'mdi mdi-arrow-right-bold',
'icon triple-left': 'mdi mdi-chevron-triple-left', 'icon triple-left': 'mdi mdi-chevron-triple-left',
'icon triple-right': 'mdi mdi-chevron-triple-right', '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 format-code': 'mdi mdi-code-tags-check',
'icon show-wizard': 'mdi mdi-comment-edit', 'icon show-wizard': 'mdi mdi-comment-edit',
'icon disconnected': 'mdi mdi-lan-disconnect', 'icon disconnected': 'mdi mdi-lan-disconnect',

View File

@@ -12,6 +12,7 @@ import uuidv1 from 'uuid/v1';
import { openWebLink } from './exportFileTools'; import { openWebLink } from './exportFileTools';
import { callServerPing } from './connectionsPinger'; import { callServerPing } from './connectionsPinger';
import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache'; import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache';
import { isAdminPage } from './pageDefs';
export const strmid = uuidv1(); export const strmid = uuidv1();
@@ -78,13 +79,22 @@ function wantEventSource() {
} }
} }
function processApiResponse(route, args, resp) { async function processApiResponse(route, args, resp) {
// if (apiLogging) { // if (apiLogging) {
// console.log('<<< API RESPONSE', route, args, resp); // console.log('<<< API RESPONSE', route, args, resp);
// } // }
if (resp?.missingCredentials) { if (resp?.missingCredentials) {
if (resp.detail.redirectToDbLogin) { 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}`; const state = `dbg-dblogin:${strmid}:${resp.detail.conid}`;
localStorage.setItem('dbloginState', state); localStorage.setItem('dbloginState', state);
openWebLink( openWebLink(
@@ -144,7 +154,7 @@ export async function apiCall(route: string, args: {} = undefined) {
const electron = getElectron(); const electron = getElectron();
if (electron) { if (electron) {
const resp = await electron.invoke(route.replace('/', '-'), args); const resp = await electron.invoke(route.replace('/', '-'), args);
return processApiResponse(route, args, resp); return await processApiResponse(route, args, resp);
} else { } else {
const resp = await fetch(`${resolveApi()}/${route}`, { const resp = await fetch(`${resolveApi()}/${route}`, {
method: 'POST', method: 'POST',
@@ -173,7 +183,7 @@ export async function apiCall(route: string, args: {} = undefined) {
} }
const json = await resp.json(); 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() { function enableApiLog() {
apiLogging = true; apiLogging = true;
console.log('API loggin enabled'); console.log('API loggin enabled');

View File

@@ -21,8 +21,8 @@ export function resolveApiHeaders() {
if (accessToken) { if (accessToken) {
res['Authorization'] = `Bearer ${accessToken}`; res['Authorization'] = `Bearer ${accessToken}`;
} }
if (isAdminPage()) { // if (isAdminPage()) {
res['x-is-admin-page'] = 'true'; // res['x-is-admin-page'] = 'true';
} // }
return res; return res;
} }

View File

@@ -32,7 +32,6 @@
}, },
"devDependencies": { "devDependencies": {
"async-lock": "^1.2.6", "async-lock": "^1.2.6",
"@azure/msal-node": "^2.12.0",
"dbgate-plugin-tools": "^1.0.7", "dbgate-plugin-tools": "^1.0.7",
"dbgate-query-splitter": "^4.10.1", "dbgate-query-splitter": "^4.10.1",
"dbgate-tools": "^5.0.0-alpha.1", "dbgate-tools": "^5.0.0-alpha.1",

View File

@@ -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,
};

View File

@@ -8,11 +8,11 @@ const AsyncLock = require('async-lock');
const nativeDriver = require('./nativeDriver'); const nativeDriver = require('./nativeDriver');
const lock = new AsyncLock(); const lock = new AsyncLock();
const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver'); const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver');
const { getAzureAuthTypes, azureGetRedirectAuthUrl, azureGetAuthTokenFromCode } = require('./azureAuth');
const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = nativeDriver; const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = nativeDriver;
let requireMsnodesqlv8; let requireMsnodesqlv8;
let platformInfo; let platformInfo;
let azureAuth;
const versionQuery = ` const versionQuery = `
SELECT SELECT
@@ -57,8 +57,20 @@ const driver = {
getAuthTypes() { getAuthTypes() {
const res = []; const res = [];
if (requireMsnodesqlv8) res.push(...windowsAuthTypes); 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) { if (res.length > 0) {
return _.uniqBy(res, 'name'); return _.uniqBy(res, 'name');
} }
@@ -126,10 +138,14 @@ const driver = {
return rows; return rows;
}, },
getRedirectAuthUrl(connection, options) { getRedirectAuthUrl(connection, options) {
return azureGetRedirectAuthUrl(connection, options); if (connection.authType != 'msentra') return null;
return azureAuth.azureGetRedirectAuthUrl(options);
}, },
getAuthTokenFromCode(connection, 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; requireMsnodesqlv8 = dbgateEnv.nativeModules.msnodesqlv8;
} }
platformInfo = dbgateEnv.platformInfo; platformInfo = dbgateEnv.platformInfo;
azureAuth = dbgateEnv.azureAuth;
nativeDriver.initialize(dbgateEnv); nativeDriver.initialize(dbgateEnv);
}; };

View File

@@ -2,7 +2,6 @@ const _ = require('lodash');
const stream = require('stream'); const stream = require('stream');
const tedious = require('tedious'); const tedious = require('tedious');
const makeUniqueColumnNames = require('./makeUniqueColumnNames'); const makeUniqueColumnNames = require('./makeUniqueColumnNames');
const { getAzureAuthOptions } = require('./azureAuth');
function extractTediousColumns(columns, addDriverNativeColumn = false) { function extractTediousColumns(columns, addDriverNativeColumn = false) {
const res = columns.map(col => { const res = columns.map(col => {
@@ -24,7 +23,8 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) {
} }
async function tediousConnect(storedConnection) { 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) => { return new Promise((resolve, reject) => {
const connectionOptions = { const connectionOptions = {
encrypt: !!ssl || authType == 'msentra', encrypt: !!ssl || authType == 'msentra',
@@ -44,7 +44,12 @@ async function tediousConnect(storedConnection) {
const authentication = const authentication =
authType == 'msentra' authType == 'msentra'
? getAzureAuthOptions(storedConnection) ? {
type: 'azure-active-directory-access-token',
options: {
token: accessToken,
},
}
: { : {
type: windowsDomain ? 'ntlm' : 'default', type: windowsDomain ? 'ntlm' : 'default',
options: { options: {