diff --git a/packages/api/src/auth/authCommon.js b/packages/api/src/auth/authCommon.js new file mode 100644 index 000000000..1ebeeafdc --- /dev/null +++ b/packages/api/src/auth/authCommon.js @@ -0,0 +1,11 @@ +const crypto = require('crypto'); + +const tokenSecret = crypto.randomUUID(); + +export function getTokenLifetime() { + return process.env.TOKEN_LIFETIME || '1d'; +} + +export function getTokenSecret() { + return tokenSecret; +} diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js new file mode 100644 index 000000000..81eccb9e7 --- /dev/null +++ b/packages/api/src/auth/authProvider.js @@ -0,0 +1,216 @@ +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'); + +let envLoginsCache = null; +let envLoginsLoaded = false; + +function getEnvLogins() { + if (envLoginsLoaded) { + return envLoginsCache; + } + + 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, + }); + } + } + } + + envLoginsCache = res.length > 0 ? res : null; + envLoginsLoaded = true; + return envLoginsCache; +} + +class AuthProviderBase { + async login(login, password) { + return {}; + } + + getBasicAuthLogins() { + return null; + } + + shouldAuthorizeApi() { + return false; + } + + oauthToken(params) { + return {}; + } + + getCurrentLogin(req) {} + + getCurrentPermissions(req) { + return process.env.PERMISSIONS; + } +} + +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 true; + } +} + +class LoginsProvider extends AuthProviderBase { + async login(login, password) { + const logins = getEnvLogins(); + 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 }, getTokenSecret(), { expiresIn: getTokenLifetime() }), + }; + } + return { error: 'Invalid credentials' }; + } + + getBasicAuthLogins() { + const logins = getEnvLogins(); + if (logins && process.env.BASIC_AUTH) { + return _.fromPairs(logins.filter(x => x.password).map(x => [x.login, x.password])); + } + } + + shouldAuthorizeApi() { + return !process.env.BASIC_AUTH; + } +} + +export 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 (getEnvLogins()) { + return 'logins'; + } + return 'none'; +} + +export 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(); + } +} diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index bf5b84a1f..aa4df5d74 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -5,18 +5,14 @@ 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 +42,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 +59,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/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..0c295b71a 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -35,6 +35,7 @@ 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 +46,11 @@ function start() { const server = http.createServer(app); - const logins = getLogins(); - if (logins && process.env.BASIC_AUTH) { + const basicAuthLogins = createAuthProvider().getBasicAuthLogins(); + if (basicAuthLogins) { app.use( basicAuth({ - users: _.fromPairs(logins.filter(x => x.password).map(x => [x.login, x.password])), + users: basicAuthLogins, challenge: true, realm: 'DbGate Web App', }) diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js index d8c7d15bc..fc40d3871 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -24,49 +24,48 @@ function hasPermission(tested, req) { 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) { 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 @@ {/if} - {#each widgets.filter(x => hasPermission(`widgets/${x.name}`)) as item} + {#each widgets.filter(x => x && hasPermission(`widgets/${x.name}`)) as item}