mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-29 22:43:58 +00:00
Merge branch 'develop'
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
17
packages/api/src/utility/azureAuth.js
Normal file
17
packages/api/src/utility/azureAuth.js
Normal 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,
|
||||||
|
};
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
function checkLicense() {
|
function checkLicense() {
|
||||||
return null;
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
type: 'community',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
1
packages/types/engines.d.ts
vendored
1
packages/types/engines.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user