diff --git a/packages/api/src/auth/authCommon.js b/packages/api/src/auth/authCommon.js
new file mode 100644
index 000000000..824b9baa8
--- /dev/null
+++ b/packages/api/src/auth/authCommon.js
@@ -0,0 +1,16 @@
+const crypto = require('crypto');
+
+const tokenSecret = crypto.randomUUID();
+
+function getTokenLifetime() {
+ return process.env.TOKEN_LIFETIME || '1d';
+}
+
+function getTokenSecret() {
+ return tokenSecret;
+}
+
+module.exports = {
+ getTokenLifetime,
+ getTokenSecret,
+};
diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js
new file mode 100644
index 000000000..fa1d71459
--- /dev/null
+++ b/packages/api/src/auth/authProvider.js
@@ -0,0 +1,198 @@
+const { getTokenSecret, getTokenLifetime } = require('./authCommon');
+const _ = require('lodash');
+const axios = require('axios');
+const { getLogger } = require('dbgate-tools');
+
+const AD = require('activedirectory2').promiseWrapper;
+const jwt = require('jsonwebtoken');
+
+const logger = getLogger('authProvider');
+
+class AuthProviderBase {
+ async login(login, password) {
+ return {};
+ }
+
+ shouldAuthorizeApi() {
+ return false;
+ }
+
+ oauthToken(params) {
+ return {};
+ }
+
+ getCurrentLogin(req) {
+ const login = req?.user?.login ?? req?.auth?.user ?? null;
+ return login;
+ }
+
+ getCurrentPermissions(req) {
+ const login = this.getCurrentLogin(req);
+ const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
+ return permissions || process.env.PERMISSIONS;
+ }
+
+ isLoginForm() {
+ return false;
+ }
+}
+
+class OAuthProvider extends AuthProviderBase {
+ shouldAuthorizeApi() {
+ return true;
+ }
+
+ async oauthToken(params) {
+ const { redirectUri, code } = params;
+
+ const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
+ const resp = await axios.default.post(
+ `${process.env.OAUTH_TOKEN}`,
+ `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
+ redirectUri
+ )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}${scopeParam}`
+ );
+
+ const { access_token, refresh_token } = resp.data;
+
+ const payload = jwt.decode(access_token);
+
+ logger.info({ payload }, 'User payload returned from OAUTH');
+
+ const login =
+ process.env.OAUTH_LOGIN_FIELD && payload && payload[process.env.OAUTH_LOGIN_FIELD]
+ ? payload[process.env.OAUTH_LOGIN_FIELD]
+ : 'oauth';
+
+ if (
+ process.env.OAUTH_ALLOWED_LOGINS &&
+ !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
+ ) {
+ return { error: `Username ${login} not allowed to log in` };
+ }
+
+ const groups =
+ process.env.OAUTH_GROUP_FIELD && payload && payload[process.env.OAUTH_GROUP_FIELD]
+ ? payload[process.env.OAUTH_GROUP_FIELD]
+ : [];
+
+ const allowedGroups = process.env.OAUTH_ALLOWED_GROUPS
+ ? process.env.OAUTH_ALLOWED_GROUPS.split(',').map(group => group.toLowerCase().trim())
+ : [];
+
+ if (process.env.OAUTH_ALLOWED_GROUPS && !groups.some(group => allowedGroups.includes(group.toLowerCase().trim()))) {
+ return { error: `Username ${login} does not belong to an allowed group` };
+ }
+
+ if (access_token) {
+ return {
+ accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
+ };
+ }
+
+ return { error: 'Token not found' };
+ }
+}
+
+class ADProvider extends AuthProviderBase {
+ async login(login, password) {
+ const adConfig = {
+ url: process.env.AD_URL,
+ baseDN: process.env.AD_BASEDN,
+ username: process.env.AD_USERNAME,
+ password: process.env.AD_PASSOWRD,
+ };
+ const ad = new AD(adConfig);
+ try {
+ const res = await ad.authenticate(login, password);
+ if (!res) {
+ return { error: 'Login failed' };
+ }
+ if (
+ process.env.AD_ALLOWED_LOGINS &&
+ !process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
+ ) {
+ return { error: `Username ${login} not allowed to log in` };
+ }
+ return {
+ accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
+ };
+ } catch (e) {
+ return { error: 'Login failed' };
+ }
+ }
+
+ shouldAuthorizeApi() {
+ return !process.env.BASIC_AUTH;
+ }
+
+ isLoginForm() {
+ return !process.env.BASIC_AUTH;
+ }
+}
+
+class LoginsProvider extends AuthProviderBase {
+ async login(login, password) {
+ if (password == process.env[`LOGIN_PASSWORD_${login}`]) {
+ return {
+ accessToken: jwt.sign({ login }, getTokenSecret(), { expiresIn: getTokenLifetime() }),
+ };
+ }
+ return { error: 'Invalid credentials' };
+ }
+
+ shouldAuthorizeApi() {
+ return !process.env.BASIC_AUTH;
+ }
+
+ isLoginForm() {
+ return !process.env.BASIC_AUTH;
+ }
+}
+
+function hasEnvLogins() {
+ if (process.env.LOGIN && process.env.PASSWORD) {
+ return true;
+ }
+ for (const key in process.env) {
+ if (key.startsWith('LOGIN_PASSWORD_')) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function detectEnvAuthProvider() {
+ if (process.env.AUTH_PROVIDER) {
+ return process.env.AUTH_PROVIDER;
+ }
+ if (process.env.OAUTH_AUTH) {
+ return 'oauth';
+ }
+ if (process.env.AD_URL) {
+ return 'ad';
+ }
+ if (hasEnvLogins()) {
+ return 'logins';
+ }
+ return 'none';
+}
+
+function createAuthProvider() {
+ const authProvider = detectEnvAuthProvider();
+ switch (authProvider) {
+ case 'oauth':
+ return new OAuthProvider();
+ case 'ad':
+ return new ADProvider();
+ case 'logins':
+ return new LoginsProvider();
+ default:
+ return new AuthProviderBase();
+ }
+}
+
+module.exports = {
+ detectEnvAuthProvider,
+ createAuthProvider,
+};
diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js
index bf5b84a1f..2b6389216 100644
--- a/packages/api/src/controllers/auth.js
+++ b/packages/api/src/controllers/auth.js
@@ -1,22 +1,17 @@
const axios = require('axios');
const jwt = require('jsonwebtoken');
const getExpressPath = require('../utility/getExpressPath');
-const { getLogins } = require('../utility/hasPermission');
const { getLogger } = require('dbgate-tools');
const AD = require('activedirectory2').promiseWrapper;
const crypto = require('crypto');
+const { getTokenSecret, getTokenLifetime } = require('../auth/authCommon');
+const { createAuthProvider } = require('../auth/authProvider');
+const { create } = require('lodash');
const logger = getLogger('auth');
-const tokenSecret = crypto.randomUUID();
-
function shouldAuthorizeApi() {
- const logins = getLogins();
- return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH);
-}
-
-function getTokenLifetime() {
- return process.env.TOKEN_LIFETIME || '1d';
+ return createAuthProvider().shouldAuthorizeApi();
}
function unauthorizedResponse(req, res, text) {
@@ -46,7 +41,7 @@ function authMiddleware(req, res, next) {
}
const token = authHeader.split(' ')[1];
try {
- const decoded = jwt.verify(token, tokenSecret);
+ const decoded = jwt.verify(token, getTokenSecret());
req.user = decoded;
return next();
} catch (err) {
@@ -63,104 +58,13 @@ function authMiddleware(req, res, next) {
module.exports = {
oauthToken_meta: true,
async oauthToken(params) {
- const { redirectUri, code } = params;
-
- const scopeParam = process.env.OAUTH_SCOPE ? `&scope=${process.env.OAUTH_SCOPE}` : '';
- const resp = await axios.default.post(
- `${process.env.OAUTH_TOKEN}`,
- `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(
- redirectUri
- )}&client_id=${process.env.OAUTH_CLIENT_ID}&client_secret=${process.env.OAUTH_CLIENT_SECRET}${scopeParam}`
- );
-
- const { access_token, refresh_token } = resp.data;
-
- const payload = jwt.decode(access_token);
-
- logger.info({ payload }, 'User payload returned from OAUTH');
-
- const login =
- process.env.OAUTH_LOGIN_FIELD && payload && payload[process.env.OAUTH_LOGIN_FIELD]
- ? payload[process.env.OAUTH_LOGIN_FIELD]
- : 'oauth';
-
- if (
- process.env.OAUTH_ALLOWED_LOGINS &&
- !process.env.OAUTH_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
- ) {
- return { error: `Username ${login} not allowed to log in` };
- }
-
- const groups =
- process.env.OAUTH_GROUP_FIELD && payload && payload[process.env.OAUTH_GROUP_FIELD]
- ? payload[process.env.OAUTH_GROUP_FIELD]
- : [];
-
- const allowedGroups =
- process.env.OAUTH_ALLOWED_GROUPS
- ? process.env.OAUTH_ALLOWED_GROUPS.split(',').map(group => group.toLowerCase().trim())
- : [];
-
- if (
- process.env.OAUTH_ALLOWED_GROUPS &&
- !groups.some(group => allowedGroups.includes(group.toLowerCase().trim()))
- ) {
- return { error: `Username ${login} does not belong to an allowed group` };
- }
-
- if (access_token) {
- return {
- accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
- };
- }
-
- return { error: 'Token not found' };
+ return createAuthProvider().oauthToken(params);
},
login_meta: true,
async login(params) {
const { login, password } = params;
- if (process.env.AD_URL) {
- const adConfig = {
- url: process.env.AD_URL,
- baseDN: process.env.AD_BASEDN,
- username: process.env.AD_USERNAME,
- password: process.env.AD_PASSOWRD,
- };
- const ad = new AD(adConfig);
- try {
- const res = await ad.authenticate(login, password);
- if (!res) {
- return { error: 'Login failed' };
- }
- if (
- process.env.AD_ALLOWED_LOGINS &&
- !process.env.AD_ALLOWED_LOGINS.split(',').find(x => x.toLowerCase().trim() == login.toLowerCase().trim())
- ) {
- return { error: `Username ${login} not allowed to log in` };
- }
- return {
- accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
- };
- } catch (err) {
- logger.error({ err }, 'Failed active directory authentization');
- return {
- error: err.message,
- };
- }
- }
-
- const logins = getLogins();
- if (!logins) {
- return { error: 'Logins not configured' };
- }
- const foundLogin = logins.find(x => x.login == login);
- if (foundLogin && foundLogin.password && foundLogin.password == password) {
- return {
- accessToken: jwt.sign({ login }, tokenSecret, { expiresIn: getTokenLifetime() }),
- };
- }
- return { error: 'Invalid credentials' };
+ return createAuthProvider().login(login, password);
},
authMiddleware,
diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js
index 9960b08bd..6fcb55eb6 100644
--- a/packages/api/src/controllers/config.js
+++ b/packages/api/src/controllers/config.js
@@ -3,7 +3,7 @@ const os = require('os');
const path = require('path');
const axios = require('axios');
const { datadir, getLogsFilePath } = require('../utility/directories');
-const { hasPermission, getLogins } = require('../utility/hasPermission');
+const { hasPermission } = require('../utility/hasPermission');
const socket = require('../utility/socket');
const _ = require('lodash');
const AsyncLock = require('async-lock');
@@ -11,6 +11,7 @@ const AsyncLock = require('async-lock');
const currentVersion = require('../currentVersion');
const platformInfo = require('../utility/platformInfo');
const connections = require('../controllers/connections');
+const { createAuthProvider } = require('../auth/authProvider');
const lock = new AsyncLock();
@@ -27,11 +28,10 @@ module.exports = {
get_meta: true,
async get(_params, req) {
- const logins = getLogins();
- const loginName =
- req && req.user && req.user.login ? req.user.login : req && req.auth && req.auth.user ? req.auth.user : null;
- const login = logins && loginName ? logins.find(x => x.login == loginName) : null;
- const permissions = login ? login.permissions : process.env.PERMISSIONS;
+ const authProvider = createAuthProvider();
+ const login = authProvider.getCurrentLogin(req);
+ const permissions = authProvider.getCurrentPermissions(req);
+ const isLoginForm = authProvider.isLoginForm();
return {
runAsPortal: !!connections.portalConnections,
@@ -47,7 +47,7 @@ module.exports = {
oauthClient: process.env.OAUTH_CLIENT_ID,
oauthScope: process.env.OAUTH_SCOPE,
oauthLogout: process.env.OAUTH_LOGOUT,
- isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH),
+ isLoginForm,
storageDatabase: process.env.STORAGE_DATABASE,
logsFilePath: getLogsFilePath(),
connectionsFilePath: path.join(datadir(), 'connections.jsonl'),
diff --git a/packages/api/src/controllers/storage.js b/packages/api/src/controllers/storage.js
index 3013eaf21..f08605ed0 100644
--- a/packages/api/src/controllers/storage.js
+++ b/packages/api/src/controllers/storage.js
@@ -3,4 +3,8 @@ module.exports = {
async connections() {
return null;
},
+
+ async getConnection({ conid }) {
+ return null;
+ },
};
diff --git a/packages/api/src/main.js b/packages/api/src/main.js
index 6bed33e2f..3c0fa85fc 100644
--- a/packages/api/src/main.js
+++ b/packages/api/src/main.js
@@ -32,9 +32,9 @@ const onFinished = require('on-finished');
const { rundir } = require('./utility/directories');
const platformInfo = require('./utility/platformInfo');
const getExpressPath = require('./utility/getExpressPath');
-const { getLogins } = require('./utility/hasPermission');
const _ = require('lodash');
const { getLogger } = require('dbgate-tools');
+const { createAuthProvider } = require('./auth/authProvider');
const logger = getLogger('main');
@@ -45,11 +45,23 @@ function start() {
const server = http.createServer(app);
- const logins = getLogins();
- if (logins && process.env.BASIC_AUTH) {
+ if (process.env.BASIC_AUTH) {
+ async function authorizer(username, password, cb) {
+ try {
+ const resp = await createAuthProvider().login(username, password);
+ if (resp.accessToken) {
+ cb(null, true);
+ } else {
+ cb(null, false);
+ }
+ } catch (err) {
+ cb(err, false);
+ }
+ }
app.use(
basicAuth({
- users: _.fromPairs(logins.filter(x => x.password).map(x => [x.login, x.password])),
+ authorizer,
+ authorizeAsync: true,
challenge: true,
realm: 'DbGate Web App',
})
diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js
index d8c7d15bc..2dcc36a9d 100644
--- a/packages/api/src/utility/hasPermission.js
+++ b/packages/api/src/utility/hasPermission.js
@@ -1,72 +1,81 @@
const { compilePermissions, testPermission } = require('dbgate-tools');
const _ = require('lodash');
+const { createAuthProvider } = require('../auth/authProvider');
-const userPermissions = {};
+const cachedPermissions = {};
function hasPermission(tested, req) {
if (!req) {
// request object not available, allow all
return true;
}
- const { user } = (req && req.auth) || {};
- const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {};
- const key = user || login || '';
- const logins = getLogins();
- if (!userPermissions[key]) {
- if (logins) {
- const login = logins.find(x => x.login == user);
- userPermissions[key] = compilePermissions(login ? login.permissions : null);
- } else {
- userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
- }
+ const permissions = createAuthProvider().getCurrentPermissions(req);
+
+ if (!cachedPermissions[permissions]) {
+ cachedPermissions[permissions] = compilePermissions(permissions);
}
- return testPermission(tested, userPermissions[key]);
+
+ return testPermission(tested, cachedPermissions[permissions]);
+
+ // const { user } = (req && req.auth) || {};
+ // const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {};
+ // const key = user || login || '';
+ // const logins = getLogins();
+
+ // if (!userPermissions[key]) {
+ // if (logins) {
+ // const login = logins.find(x => x.login == user);
+ // userPermissions[key] = compilePermissions(login ? login.permissions : null);
+ // } else {
+ // userPermissions[key] = compilePermissions(process.env.PERMISSIONS);
+ // }
+ // }
+ // return testPermission(tested, userPermissions[key]);
}
-let loginsCache = null;
-let loginsLoaded = false;
+// let loginsCache = null;
+// let loginsLoaded = false;
-function getLogins() {
- if (loginsLoaded) {
- return loginsCache;
- }
+// function getLogins() {
+// if (loginsLoaded) {
+// return loginsCache;
+// }
- const res = [];
- if (process.env.LOGIN && process.env.PASSWORD) {
- res.push({
- login: process.env.LOGIN,
- password: process.env.PASSWORD,
- permissions: process.env.PERMISSIONS,
- });
- }
- if (process.env.LOGINS) {
- const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim()));
- for (const login of logins) {
- const password = process.env[`LOGIN_PASSWORD_${login}`];
- const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
- if (password) {
- res.push({
- login,
- password,
- permissions,
- });
- }
- }
- }
- else if (process.env.OAUTH_PERMISSIONS) {
- const login_permission_keys = Object.keys(process.env).filter((key) => _.startsWith(key, 'LOGIN_PERMISSIONS_'))
- for (const permissions_key of login_permission_keys) {
- const login = permissions_key.replace('LOGIN_PERMISSIONS_', '');
- const permissions = process.env[permissions_key];
- userPermissions[login] = compilePermissions(permissions);
- }
- }
+// const res = [];
+// if (process.env.LOGIN && process.env.PASSWORD) {
+// res.push({
+// login: process.env.LOGIN,
+// password: process.env.PASSWORD,
+// permissions: process.env.PERMISSIONS,
+// });
+// }
+// if (process.env.LOGINS) {
+// const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim()));
+// for (const login of logins) {
+// const password = process.env[`LOGIN_PASSWORD_${login}`];
+// const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
+// if (password) {
+// res.push({
+// login,
+// password,
+// permissions,
+// });
+// }
+// }
+// } else if (process.env.OAUTH_PERMISSIONS) {
+// const login_permission_keys = Object.keys(process.env).filter(key => _.startsWith(key, 'LOGIN_PERMISSIONS_'));
+// for (const permissions_key of login_permission_keys) {
+// const login = permissions_key.replace('LOGIN_PERMISSIONS_', '');
+// const permissions = process.env[permissions_key];
+// userPermissions[login] = compilePermissions(permissions);
+// }
+// }
- loginsCache = res.length > 0 ? res : null;
- loginsLoaded = true;
- return loginsCache;
-}
+// loginsCache = res.length > 0 ? res : null;
+// loginsLoaded = true;
+// return loginsCache;
+// }
function connectionHasPermission(connection, req) {
if (!connection) {
@@ -87,7 +96,6 @@ function testConnectionPermission(connection, req) {
module.exports = {
hasPermission,
- getLogins,
connectionHasPermission,
testConnectionPermission,
};
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte
index f25e90ea1..6bd0312d4 100644
--- a/packages/web/src/icons/FontIcon.svelte
+++ b/packages/web/src/icons/FontIcon.svelte
@@ -27,6 +27,7 @@
'icon users': 'mdi mdi-account-multiple',
'icon role': 'mdi mdi-account-group',
'icon admin': 'mdi mdi-security',
+ 'icon auth': 'mdi mdi-account-key',
'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back',
@@ -191,6 +192,7 @@
'img users': 'mdi mdi-account-multiple color-icon-blue',
'img role': 'mdi mdi-account-group color-icon-blue',
'img admin': 'mdi mdi-security color-icon-blue',
+ 'img auth': 'mdi mdi-account-key color-icon-blue',
'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red',
diff --git a/packages/web/src/utility/connectionsPinger.js b/packages/web/src/utility/connectionsPinger.js
index aed248bf5..efa0fd7b6 100644
--- a/packages/web/src/utility/connectionsPinger.js
+++ b/packages/web/src/utility/connectionsPinger.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import { openedConnections, currentDatabase, openedConnectionsWithTemporary } from '../stores';
+import { openedConnections, currentDatabase, openedConnectionsWithTemporary, getCurrentConfig } from '../stores';
import { apiCall, strmid } from './api';
import { getConnectionList } from './metadataLoaders';
@@ -10,7 +10,10 @@ import { getConnectionList } from './metadataLoaders';
// };
const doServerPing = value => {
- apiCall('server-connections/ping', { conidArray: ['__storage', ...value], strmid });
+ apiCall('server-connections/ping', {
+ conidArray: getCurrentConfig().storageDatabase ? ['__storage', ...value] : value,
+ strmid,
+ });
};
const doDatabasePing = value => {
diff --git a/packages/web/src/widgets/WidgetIconPanel.svelte b/packages/web/src/widgets/WidgetIconPanel.svelte
index 44b5f9639..4995e7c36 100644
--- a/packages/web/src/widgets/WidgetIconPanel.svelte
+++ b/packages/web/src/widgets/WidgetIconPanel.svelte
@@ -8,6 +8,7 @@
visibleWidgetSideBar,
visibleHamburgerMenuWidget,
lockedDatabaseMode,
+ getCurrentConfig,
} from '../stores';
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
import hasPermission from '../utility/hasPermission';
@@ -16,7 +17,7 @@
let domMainMenu;
const widgets = [
- {
+ getCurrentConfig().storageDatabase && {
icon: 'icon admin',
name: 'admin',
title: 'Administration',
@@ -103,7 +104,7 @@