Merge branch 'auth-provider-refactor' into develop

This commit is contained in:
Jan Prochazka
2024-07-26 12:31:06 +02:00
10 changed files with 320 additions and 172 deletions

View File

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

View File

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

View File

@@ -1,22 +1,17 @@
const axios = require('axios'); const axios = require('axios');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const getExpressPath = require('../utility/getExpressPath'); const getExpressPath = require('../utility/getExpressPath');
const { getLogins } = require('../utility/hasPermission');
const { getLogger } = require('dbgate-tools'); 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 { createAuthProvider } = require('../auth/authProvider');
const { create } = require('lodash');
const logger = getLogger('auth'); const logger = getLogger('auth');
const tokenSecret = crypto.randomUUID();
function shouldAuthorizeApi() { function shouldAuthorizeApi() {
const logins = getLogins(); return createAuthProvider().shouldAuthorizeApi();
return !!process.env.OAUTH_AUTH || !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH);
}
function getTokenLifetime() {
return process.env.TOKEN_LIFETIME || '1d';
} }
function unauthorizedResponse(req, res, text) { function unauthorizedResponse(req, res, text) {
@@ -46,7 +41,7 @@ function authMiddleware(req, res, next) {
} }
const token = authHeader.split(' ')[1]; const token = authHeader.split(' ')[1];
try { try {
const decoded = jwt.verify(token, tokenSecret); const decoded = jwt.verify(token, getTokenSecret());
req.user = decoded; req.user = decoded;
return next(); return next();
} catch (err) { } catch (err) {
@@ -63,104 +58,13 @@ function authMiddleware(req, res, next) {
module.exports = { module.exports = {
oauthToken_meta: true, oauthToken_meta: true,
async oauthToken(params) { async oauthToken(params) {
const { redirectUri, code } = params; return createAuthProvider().oauthToken(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' };
}, },
login_meta: true, login_meta: true,
async login(params) { async login(params) {
const { login, password } = params; const { login, password } = params;
if (process.env.AD_URL) { return createAuthProvider().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 }, 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' };
}, },
authMiddleware, authMiddleware,

View File

@@ -3,7 +3,7 @@ const os = require('os');
const path = require('path'); const path = require('path');
const axios = require('axios'); const axios = require('axios');
const { datadir, getLogsFilePath } = require('../utility/directories'); const { datadir, getLogsFilePath } = require('../utility/directories');
const { hasPermission, getLogins } = require('../utility/hasPermission'); const { hasPermission } = require('../utility/hasPermission');
const socket = require('../utility/socket'); const socket = require('../utility/socket');
const _ = require('lodash'); const _ = require('lodash');
const AsyncLock = require('async-lock'); const AsyncLock = require('async-lock');
@@ -11,6 +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 { createAuthProvider } = require('../auth/authProvider');
const lock = new AsyncLock(); const lock = new AsyncLock();
@@ -27,11 +28,10 @@ module.exports = {
get_meta: true, get_meta: true,
async get(_params, req) { async get(_params, req) {
const logins = getLogins(); const authProvider = createAuthProvider();
const loginName = const login = authProvider.getCurrentLogin(req);
req && req.user && req.user.login ? req.user.login : req && req.auth && req.auth.user ? req.auth.user : null; const permissions = authProvider.getCurrentPermissions(req);
const login = logins && loginName ? logins.find(x => x.login == loginName) : null; const isLoginForm = authProvider.isLoginForm();
const permissions = login ? login.permissions : process.env.PERMISSIONS;
return { return {
runAsPortal: !!connections.portalConnections, runAsPortal: !!connections.portalConnections,
@@ -47,7 +47,7 @@ module.exports = {
oauthClient: process.env.OAUTH_CLIENT_ID, oauthClient: process.env.OAUTH_CLIENT_ID,
oauthScope: process.env.OAUTH_SCOPE, oauthScope: process.env.OAUTH_SCOPE,
oauthLogout: process.env.OAUTH_LOGOUT, oauthLogout: process.env.OAUTH_LOGOUT,
isLoginForm: !!process.env.AD_URL || (!!logins && !process.env.BASIC_AUTH), isLoginForm,
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

@@ -3,4 +3,8 @@ module.exports = {
async connections() { async connections() {
return null; return null;
}, },
async getConnection({ conid }) {
return null;
},
}; };

View File

@@ -32,9 +32,9 @@ const onFinished = require('on-finished');
const { rundir } = require('./utility/directories'); const { rundir } = require('./utility/directories');
const platformInfo = require('./utility/platformInfo'); const platformInfo = require('./utility/platformInfo');
const getExpressPath = require('./utility/getExpressPath'); const getExpressPath = require('./utility/getExpressPath');
const { getLogins } = require('./utility/hasPermission');
const _ = require('lodash'); const _ = require('lodash');
const { getLogger } = require('dbgate-tools'); const { getLogger } = require('dbgate-tools');
const { createAuthProvider } = require('./auth/authProvider');
const logger = getLogger('main'); const logger = getLogger('main');
@@ -45,11 +45,23 @@ function start() {
const server = http.createServer(app); const server = http.createServer(app);
const logins = getLogins(); if (process.env.BASIC_AUTH) {
if (logins && 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( app.use(
basicAuth({ basicAuth({
users: _.fromPairs(logins.filter(x => x.password).map(x => [x.login, x.password])), authorizer,
authorizeAsync: true,
challenge: true, challenge: true,
realm: 'DbGate Web App', realm: 'DbGate Web App',
}) })

View File

@@ -1,72 +1,81 @@
const { compilePermissions, testPermission } = require('dbgate-tools'); const { compilePermissions, testPermission } = require('dbgate-tools');
const _ = require('lodash'); const _ = require('lodash');
const { createAuthProvider } = require('../auth/authProvider');
const userPermissions = {}; const cachedPermissions = {};
function hasPermission(tested, req) { function hasPermission(tested, req) {
if (!req) { if (!req) {
// request object not available, allow all // request object not available, allow all
return true; 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]) { const permissions = createAuthProvider().getCurrentPermissions(req);
if (logins) {
const login = logins.find(x => x.login == user); if (!cachedPermissions[permissions]) {
userPermissions[key] = compilePermissions(login ? login.permissions : null); cachedPermissions[permissions] = compilePermissions(permissions);
} else {
userPermissions[key] = compilePermissions(process.env.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 loginsCache = null;
let loginsLoaded = false; // let loginsLoaded = false;
function getLogins() { // function getLogins() {
if (loginsLoaded) { // if (loginsLoaded) {
return loginsCache; // return loginsCache;
} // }
const res = []; // const res = [];
if (process.env.LOGIN && process.env.PASSWORD) { // if (process.env.LOGIN && process.env.PASSWORD) {
res.push({ // res.push({
login: process.env.LOGIN, // login: process.env.LOGIN,
password: process.env.PASSWORD, // password: process.env.PASSWORD,
permissions: process.env.PERMISSIONS, // permissions: process.env.PERMISSIONS,
}); // });
} // }
if (process.env.LOGINS) { // if (process.env.LOGINS) {
const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim())); // const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim()));
for (const login of logins) { // for (const login of logins) {
const password = process.env[`LOGIN_PASSWORD_${login}`]; // const password = process.env[`LOGIN_PASSWORD_${login}`];
const permissions = process.env[`LOGIN_PERMISSIONS_${login}`]; // const permissions = process.env[`LOGIN_PERMISSIONS_${login}`];
if (password) { // if (password) {
res.push({ // res.push({
login, // login,
password, // password,
permissions, // permissions,
}); // });
} // }
} // }
} // } else if (process.env.OAUTH_PERMISSIONS) {
else if (process.env.OAUTH_PERMISSIONS) { // const login_permission_keys = Object.keys(process.env).filter(key => _.startsWith(key, 'LOGIN_PERMISSIONS_'));
const login_permission_keys = Object.keys(process.env).filter((key) => _.startsWith(key, 'LOGIN_PERMISSIONS_')) // for (const permissions_key of login_permission_keys) {
for (const permissions_key of login_permission_keys) { // const login = permissions_key.replace('LOGIN_PERMISSIONS_', '');
const login = permissions_key.replace('LOGIN_PERMISSIONS_', ''); // const permissions = process.env[permissions_key];
const permissions = process.env[permissions_key]; // userPermissions[login] = compilePermissions(permissions);
userPermissions[login] = compilePermissions(permissions); // }
} // }
}
loginsCache = res.length > 0 ? res : null; // loginsCache = res.length > 0 ? res : null;
loginsLoaded = true; // loginsLoaded = true;
return loginsCache; // return loginsCache;
} // }
function connectionHasPermission(connection, req) { function connectionHasPermission(connection, req) {
if (!connection) { if (!connection) {
@@ -87,7 +96,6 @@ function testConnectionPermission(connection, req) {
module.exports = { module.exports = {
hasPermission, hasPermission,
getLogins,
connectionHasPermission, connectionHasPermission,
testConnectionPermission, testConnectionPermission,
}; };

View File

@@ -27,6 +27,7 @@
'icon users': 'mdi mdi-account-multiple', 'icon users': 'mdi mdi-account-multiple',
'icon role': 'mdi mdi-account-group', 'icon role': 'mdi mdi-account-group',
'icon admin': 'mdi mdi-security', 'icon admin': 'mdi mdi-security',
'icon auth': 'mdi mdi-account-key',
'icon version': 'mdi mdi-ticket-confirmation', 'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin', 'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back', 'icon arrange': 'mdi mdi-arrange-send-to-back',
@@ -191,6 +192,7 @@
'img users': 'mdi mdi-account-multiple color-icon-blue', 'img users': 'mdi mdi-account-multiple color-icon-blue',
'img role': 'mdi mdi-account-group color-icon-blue', 'img role': 'mdi mdi-account-group color-icon-blue',
'img admin': 'mdi mdi-security 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 add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red', 'img minus': 'mdi mdi-minus-circle color-icon-red',

View File

@@ -1,5 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { openedConnections, currentDatabase, openedConnectionsWithTemporary } from '../stores'; import { openedConnections, currentDatabase, openedConnectionsWithTemporary, getCurrentConfig } from '../stores';
import { apiCall, strmid } from './api'; import { apiCall, strmid } from './api';
import { getConnectionList } from './metadataLoaders'; import { getConnectionList } from './metadataLoaders';
@@ -10,7 +10,10 @@ import { getConnectionList } from './metadataLoaders';
// }; // };
const doServerPing = value => { 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 => { const doDatabasePing = value => {

View File

@@ -8,6 +8,7 @@
visibleWidgetSideBar, visibleWidgetSideBar,
visibleHamburgerMenuWidget, visibleHamburgerMenuWidget,
lockedDatabaseMode, lockedDatabaseMode,
getCurrentConfig,
} from '../stores'; } from '../stores';
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition'; import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';
@@ -16,7 +17,7 @@
let domMainMenu; let domMainMenu;
const widgets = [ const widgets = [
{ getCurrentConfig().storageDatabase && {
icon: 'icon admin', icon: 'icon admin',
name: 'admin', name: 'admin',
title: 'Administration', title: 'Administration',
@@ -103,7 +104,7 @@
<FontIcon icon="icon menu" /> <FontIcon icon="icon menu" />
</div> </div>
{/if} {/if}
{#each widgets.filter(x => hasPermission(`widgets/${x.name}`)) as item} {#each widgets.filter(x => x && hasPermission(`widgets/${x.name}`)) as item}
<div <div
class="wrapper" class="wrapper"
class:selected={item.name == $visibleSelectedWidget} class:selected={item.name == $visibleSelectedWidget}