diff --git a/package.json b/package.json index 70d7116c4..0451e5e58 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "start:api:auth": "yarn workspace dbgate-api start:auth | pino-pretty", "start:api:dblogin": "yarn workspace dbgate-api start:dblogin | pino-pretty", "start:api:storage": "yarn workspace dbgate-api start:storage | pino-pretty", + "start:api:sfill": "yarn workspace dbgate-api start:sfill | pino-pretty", "start:api:storage:built": "yarn workspace dbgate-api start:storage:built | pino-pretty", "start:api:azure": "yarn workspace dbgate-api start:azure | pino-pretty", "start:api:e2e:team": "yarn workspace dbgate-api start:e2e:team | pino-pretty", diff --git a/packages/api/env/sfill/.env b/packages/api/env/sfill/.env new file mode 100644 index 000000000..68462836c --- /dev/null +++ b/packages/api/env/sfill/.env @@ -0,0 +1,46 @@ +DEVMODE=1 +DEVWEB=1 + +STORAGE_SERVER=localhost +STORAGE_USER=root +STORAGE_PASSWORD=Pwd2020Db +STORAGE_PORT=3306 +STORAGE_DATABASE=dbgate-filled +STORAGE_ENGINE=mysql@dbgate-plugin-mysql + +CONNECTIONS=mysql,postgres,mongo,redis + +LABEL_mysql=MySql +SERVER_mysql=dbgatedckstage1.sprinx.cz +USER_mysql=root +PASSWORD_mysql=Pwd2020Db +PORT_mysql=3306 +ENGINE_mysql=mysql@dbgate-plugin-mysql + +LABEL_postgres=Postgres +SERVER_postgres=dbgatedckstage1.sprinx.cz +USER_postgres=postgres +PASSWORD_postgres=Pwd2020Db +PORT_postgres=5432 +ENGINE_postgres=postgres@dbgate-plugin-postgres + +LABEL_mongo=Mongo +SERVER_mongo=dbgatedckstage1.sprinx.cz +USER_mongo=root +PASSWORD_mongo=Pwd2020Db +PORT_mongo=27017 +ENGINE_mongo=mongo@dbgate-plugin-mongo + +LABEL_redis=Redis +SERVER_redis=dbgatedckstage1.sprinx.cz +ENGINE_redis=redis@dbgate-plugin-redis +PORT_redis=6379 + +ROLE_test1_CONNECTIONS=mysql +ROLE_test1_PERMISSIONS=widgets/* +ROLE_test1_DATABASES_db1_CONNECTION=mysql +ROLE_test1_DATABASES_db1_PERMISSION=run_script +ROLE_test1_DATABASES_db1_DATABASES=db1 +ROLE_test1_DATABASES_db2_CONNECTION=redis +ROLE_test1_DATABASES_db2_PERMISSION=run_script +ROLE_test1_DATABASES_db2_DATABASES=db2 diff --git a/packages/api/package.json b/packages/api/package.json index 6ce7a2d02..c347bd5bb 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -75,6 +75,7 @@ "start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api", "start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api", "start:storage": "env-cmd -f env/storage/.env node src/index.js --listen-api", + "start:sfill": "env-cmd -f env/sfill/.env node src/index.js --listen-api", "start:storage:built": "env-cmd -f env/storage/.env cross-env DEVMODE= BUILTWEBMODE=1 node dist/bundle.js --listen-api", "start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api", "start:azure": "env-cmd -f env/azure/.env node src/index.js --listen-api", diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 45f02dc14..4260c4b11 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -23,6 +23,7 @@ const pipeForkLogs = require('../utility/pipeForkLogs'); const requireEngineDriver = require('../utility/requireEngineDriver'); const { getAuthProviderById } = require('../auth/authProvider'); const { startTokenChecking } = require('../utility/authProxy'); +const { extractConnectionsFromEnv } = require('../utility/envtools'); const logger = getLogger('connections'); @@ -61,55 +62,7 @@ function getDatabaseFileLabel(databaseFile) { function getPortalCollections() { if (process.env.CONNECTIONS) { - const connections = _.compact(process.env.CONNECTIONS.split(',')).map(id => ({ - _id: id, - engine: process.env[`ENGINE_${id}`], - server: process.env[`SERVER_${id}`], - user: process.env[`USER_${id}`], - password: process.env[`PASSWORD_${id}`], - passwordMode: process.env[`PASSWORD_MODE_${id}`], - port: process.env[`PORT_${id}`], - databaseUrl: process.env[`URL_${id}`], - useDatabaseUrl: !!process.env[`URL_${id}`], - databaseFile: process.env[`FILE_${id}`]?.replace( - '%%E2E_TEST_DATA_DIRECTORY%%', - path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata') - ), - socketPath: process.env[`SOCKET_PATH_${id}`], - serviceName: process.env[`SERVICE_NAME_${id}`], - authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined), - defaultDatabase: - process.env[`DATABASE_${id}`] || - (process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null), - singleDatabase: !!process.env[`DATABASE_${id}`] || !!process.env[`FILE_${id}`], - displayName: process.env[`LABEL_${id}`], - isReadOnly: process.env[`READONLY_${id}`], - databases: process.env[`DBCONFIG_${id}`] ? safeJsonParse(process.env[`DBCONFIG_${id}`]) : null, - allowedDatabases: process.env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'), - allowedDatabasesRegex: process.env[`ALLOWED_DATABASES_REGEX_${id}`], - parent: process.env[`PARENT_${id}`] || undefined, - useSeparateSchemas: !!process.env[`USE_SEPARATE_SCHEMAS_${id}`], - localDataCenter: process.env[`LOCAL_DATA_CENTER_${id}`], - - // SSH tunnel - useSshTunnel: process.env[`USE_SSH_${id}`], - sshHost: process.env[`SSH_HOST_${id}`], - sshPort: process.env[`SSH_PORT_${id}`], - sshMode: process.env[`SSH_MODE_${id}`], - sshLogin: process.env[`SSH_LOGIN_${id}`], - sshPassword: process.env[`SSH_PASSWORD_${id}`], - sshKeyfile: process.env[`SSH_KEY_FILE_${id}`], - sshKeyfilePassword: process.env[`SSH_KEY_FILE_PASSWORD_${id}`], - - // SSL - useSsl: process.env[`USE_SSL_${id}`], - sslCaFile: process.env[`SSL_CA_FILE_${id}`], - sslCertFile: process.env[`SSL_CERT_FILE_${id}`], - sslCertFilePassword: process.env[`SSL_CERT_FILE_PASSWORD_${id}`], - sslKeyFile: process.env[`SSL_KEY_FILE_${id}`], - sslRejectUnauthorized: process.env[`SSL_REJECT_UNAUTHORIZED_${id}`], - trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`], - })); + const connections = extractConnectionsFromEnv(process.env); for (const conn of connections) { for (const prop in process.env) { @@ -229,6 +182,15 @@ module.exports = { ); } await this.checkUnsavedConnectionsLimit(); + + if (process.env.STORAGE_DATABASE && process.env.CONNECTIONS) { + const storage = require('./storage'); + try { + await storage.fillStorageConnectionsFromEnv(); + } catch (err) { + logger.error(extractErrorLogData(err), 'DBGM-00268 Error filling storage connections from env'); + } + } }, list_meta: true, diff --git a/packages/api/src/shell/copyStream.js b/packages/api/src/shell/copyStream.js index 5da2aaad0..978d41261 100644 --- a/packages/api/src/shell/copyStream.js +++ b/packages/api/src/shell/copyStream.js @@ -65,6 +65,8 @@ async function copyStream(input, output, options) { }); } } catch (err) { + logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed'); + process.send({ msgtype: 'copyStreamError', copyStreamError: { @@ -82,8 +84,6 @@ async function copyStream(input, output, options) { errorMessage: extractErrorMessage(err), }); } - - logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed'); // throw err; } } diff --git a/packages/api/src/storageModel.js b/packages/api/src/storageModel.js index 208358e5d..1557a224f 100644 --- a/packages/api/src/storageModel.js +++ b/packages/api/src/storageModel.js @@ -686,9 +686,34 @@ module.exports = { "columnName": "connectionDefinition", "dataType": "text", "notNull": false + }, + { + "pureName": "connections", + "columnName": "import_source_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "connections", + "columnName": "id_original", + "dataType": "varchar(250)", + "notNull": false + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_connections_import_source_id", + "pureName": "connections", + "refTableName": "import_sources", + "columns": [ + { + "columnName": "import_source_id", + "refColumnName": "id" + } + ] } ], - "foreignKeys": [], "primaryKey": { "pureName": "connections", "constraintType": "primaryKey", @@ -790,6 +815,41 @@ module.exports = { } ] }, + { + "pureName": "import_sources", + "columns": [ + { + "pureName": "import_sources", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "import_sources", + "columnName": "name", + "dataType": "varchar(250)", + "notNull": true + } + ], + "foreignKeys": [], + "primaryKey": { + "pureName": "import_sources", + "constraintType": "primaryKey", + "constraintName": "PK_import_sources", + "columns": [ + { + "columnName": "id" + } + ] + }, + "preloadedRows": [ + { + "id": -1, + "name": "env" + } + ] + }, { "pureName": "roles", "columns": [ @@ -805,9 +865,34 @@ module.exports = { "columnName": "name", "dataType": "varchar(250)", "notNull": false + }, + { + "pureName": "roles", + "columnName": "import_source_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "roles", + "columnName": "id_original", + "dataType": "varchar(250)", + "notNull": false + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_roles_import_source_id", + "pureName": "roles", + "refTableName": "import_sources", + "columns": [ + { + "columnName": "import_source_id", + "refColumnName": "id" + } + ] } ], - "foreignKeys": [], "primaryKey": { "pureName": "roles", "constraintType": "primaryKey", @@ -854,6 +939,12 @@ module.exports = { "columnName": "connection_id", "dataType": "int", "notNull": true + }, + { + "pureName": "role_connections", + "columnName": "import_source_id", + "dataType": "int", + "notNull": false } ], "foreignKeys": [ @@ -882,6 +973,18 @@ module.exports = { "refColumnName": "id" } ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_connections_import_source_id", + "pureName": "role_connections", + "refTableName": "import_sources", + "columns": [ + { + "columnName": "import_source_id", + "refColumnName": "id" + } + ] } ], "primaryKey": { @@ -934,6 +1037,18 @@ module.exports = { "columnName": "database_permission_role_id", "dataType": "int", "notNull": true + }, + { + "pureName": "role_databases", + "columnName": "import_source_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "role_databases", + "columnName": "id_original", + "dataType": "varchar(250)", + "notNull": false } ], "foreignKeys": [ @@ -974,6 +1089,18 @@ module.exports = { "refColumnName": "id" } ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_databases_import_source_id", + "pureName": "role_databases", + "refTableName": "import_sources", + "columns": [ + { + "columnName": "import_source_id", + "refColumnName": "id" + } + ] } ], "primaryKey": { @@ -1087,6 +1214,12 @@ module.exports = { "columnName": "permission", "dataType": "varchar(250)", "notNull": true + }, + { + "pureName": "role_permissions", + "columnName": "import_source_id", + "dataType": "int", + "notNull": false } ], "foreignKeys": [ @@ -1102,6 +1235,18 @@ module.exports = { "refColumnName": "id" } ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_permissions_import_source_id", + "pureName": "role_permissions", + "refTableName": "import_sources", + "columns": [ + { + "columnName": "import_source_id", + "refColumnName": "id" + } + ] } ], "primaryKey": { @@ -1184,6 +1329,18 @@ module.exports = { "columnName": "table_permission_scope_id", "dataType": "int", "notNull": true + }, + { + "pureName": "role_tables", + "columnName": "import_source_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "role_tables", + "columnName": "id_original", + "dataType": "varchar(250)", + "notNull": false } ], "foreignKeys": [ @@ -1236,6 +1393,18 @@ module.exports = { "refColumnName": "id" } ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_tables_import_source_id", + "pureName": "role_tables", + "refTableName": "import_sources", + "columns": [ + { + "columnName": "import_source_id", + "refColumnName": "id" + } + ] } ], "primaryKey": { diff --git a/packages/api/src/utility/envtools.js b/packages/api/src/utility/envtools.js new file mode 100644 index 000000000..83d09808c --- /dev/null +++ b/packages/api/src/utility/envtools.js @@ -0,0 +1,445 @@ +const path = require('path'); +const _ = require('lodash'); +const { safeJsonParse, getDatabaseFileLabel } = require('dbgate-tools'); +const crypto = require('crypto'); + +function extractConnectionsFromEnv(env) { + if (!env?.CONNECTIONS) { + return null; + } + + const connections = _.compact(env.CONNECTIONS.split(',')).map(id => ({ + _id: id, + engine: env[`ENGINE_${id}`], + server: env[`SERVER_${id}`], + user: env[`USER_${id}`], + password: env[`PASSWORD_${id}`], + passwordMode: env[`PASSWORD_MODE_${id}`], + port: env[`PORT_${id}`], + databaseUrl: env[`URL_${id}`], + useDatabaseUrl: !!env[`URL_${id}`], + databaseFile: env[`FILE_${id}`]?.replace( + '%%E2E_TEST_DATA_DIRECTORY%%', + path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata') + ), + socketPath: env[`SOCKET_PATH_${id}`], + serviceName: env[`SERVICE_NAME_${id}`], + authType: env[`AUTH_TYPE_${id}`] || (env[`SOCKET_PATH_${id}`] ? 'socket' : undefined), + defaultDatabase: env[`DATABASE_${id}`] || (env[`FILE_${id}`] ? getDatabaseFileLabel(env[`FILE_${id}`]) : null), + singleDatabase: !!env[`DATABASE_${id}`] || !!env[`FILE_${id}`], + displayName: env[`LABEL_${id}`], + isReadOnly: env[`READONLY_${id}`], + databases: env[`DBCONFIG_${id}`] ? safeJsonParse(env[`DBCONFIG_${id}`]) : null, + allowedDatabases: env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'), + allowedDatabasesRegex: env[`ALLOWED_DATABASES_REGEX_${id}`], + parent: env[`PARENT_${id}`] || undefined, + useSeparateSchemas: !!env[`USE_SEPARATE_SCHEMAS_${id}`], + localDataCenter: env[`LOCAL_DATA_CENTER_${id}`], + + // SSH tunnel + useSshTunnel: env[`USE_SSH_${id}`], + sshHost: env[`SSH_HOST_${id}`], + sshPort: env[`SSH_PORT_${id}`], + sshMode: env[`SSH_MODE_${id}`], + sshLogin: env[`SSH_LOGIN_${id}`], + sshPassword: env[`SSH_PASSWORD_${id}`], + sshKeyfile: env[`SSH_KEY_FILE_${id}`], + sshKeyfilePassword: env[`SSH_KEY_FILE_PASSWORD_${id}`], + + // SSL + useSsl: env[`USE_SSL_${id}`], + sslCaFile: env[`SSL_CA_FILE_${id}`], + sslCertFile: env[`SSL_CERT_FILE_${id}`], + sslCertFilePassword: env[`SSL_CERT_FILE_PASSWORD_${id}`], + sslKeyFile: env[`SSL_KEY_FILE_${id}`], + sslRejectUnauthorized: env[`SSL_REJECT_UNAUTHORIZED_${id}`], + trustServerCertificate: env[`SSL_TRUST_CERTIFICATE_${id}`], + })); + + return connections; +} + +function extractImportEntitiesFromEnv(env) { + const portalConnections = extractConnectionsFromEnv(env) || []; + + const connections = portalConnections.map((conn, index) => ({ + ...conn, + id_original: conn._id, + import_source_id: -1, + conid: crypto.randomUUID(), + _id: undefined, + id: index + 1, // autoincrement id + })); + + const connectionEnvIdToDbId = {}; + for (const conn of connections) { + connectionEnvIdToDbId[conn.id_original] = conn.id; + } + + const connectionsRegex = /^ROLE_(.+)_CONNECTIONS$/; + const permissionsRegex = /^ROLE_(.+)_PERMISSIONS$/; + + const dbConnectionRegex = /^ROLE_(.+)_DATABASES_(.+)_CONNECTION$/; + const dbDatabasesRegex = /^ROLE_(.+)_DATABASES_(.+)_DATABASES$/; + const dbDatabasesRegexRegex = /^ROLE_(.+)_DATABASES_(.+)_DATABASES_REGEX$/; + const dbPermissionRegex = /^ROLE_(.+)_DATABASES_(.+)_PERMISSION$/; + + const tableConnectionRegex = /^ROLE_(.+)_TABLES_(.+)_CONNECTION$/; + const tableDatabasesRegex = /^ROLE_(.+)_TABLES_(.+)_DATABASES$/; + const tableDatabasesRegexRegex = /^ROLE_(.+)_TABLES_(.+)_DATABASES_REGEX$/; + const tableSchemasRegex = /^ROLE_(.+)_TABLES_(.+)_SCHEMAS$/; + const tableSchemasRegexRegex = /^ROLE_(.+)_TABLES_(.+)_SCHEMAS_REGEX$/; + const tableTablesRegex = /^ROLE_(.+)_TABLES_(.+)_TABLES$/; + const tableTablesRegexRegex = /^ROLE_(.+)_TABLES_(.+)_TABLES_REGEX$/; + const tablePermissionRegex = /^ROLE_(.+)_TABLES_(.+)_PERMISSION$/; + const tableScopeRegex = /^ROLE_(.+)_TABLES_(.+)_SCOPE$/; + + const roles = []; + const role_connections = []; + const role_permissions = []; + const role_databases = []; + const role_tables = []; + + // Permission name to ID mappings + const databasePermissionMap = { + view: -1, + read_content: -2, + write_data: -3, + run_script: -4, + deny: -5, + }; + + const tablePermissionMap = { + read: -1, + update_only: -2, + create_update_delete: -3, + run_script: -4, + deny: -5, + }; + + const tableScopeMap = { + all_objects: -1, + tables: -2, + views: -3, + tables_views_collections: -4, + procedures: -5, + functions: -6, + triggers: -7, + sql_objects: -8, + collections: -9, + }; + + // Collect database and table permissions data + const databasePermissions = {}; + const tablePermissions = {}; + + // First pass: collect all database and table permission data + for (const key in env) { + const dbConnMatch = key.match(dbConnectionRegex); + const dbDatabasesMatch = key.match(dbDatabasesRegex); + const dbDatabasesRegexMatch = key.match(dbDatabasesRegexRegex); + const dbPermMatch = key.match(dbPermissionRegex); + + const tableConnMatch = key.match(tableConnectionRegex); + const tableDatabasesMatch = key.match(tableDatabasesRegex); + const tableDatabasesRegexMatch = key.match(tableDatabasesRegexRegex); + const tableSchemasMatch = key.match(tableSchemasRegex); + const tableSchemasRegexMatch = key.match(tableSchemasRegexRegex); + const tableTablesMatch = key.match(tableTablesRegex); + const tableTablesRegexMatch = key.match(tableTablesRegexRegex); + const tablePermMatch = key.match(tablePermissionRegex); + const tableScopeMatch = key.match(tableScopeRegex); + + // Database permissions + if (dbConnMatch) { + const [, roleName, permId] = dbConnMatch; + if (!databasePermissions[roleName]) databasePermissions[roleName] = {}; + if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {}; + databasePermissions[roleName][permId].connection = env[key]; + } + if (dbDatabasesMatch) { + const [, roleName, permId] = dbDatabasesMatch; + if (!databasePermissions[roleName]) databasePermissions[roleName] = {}; + if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {}; + databasePermissions[roleName][permId].databases = env[key]?.replace(/\|/g, '\n'); + } + if (dbDatabasesRegexMatch) { + const [, roleName, permId] = dbDatabasesRegexMatch; + if (!databasePermissions[roleName]) databasePermissions[roleName] = {}; + if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {}; + databasePermissions[roleName][permId].databasesRegex = env[key]; + } + if (dbPermMatch) { + const [, roleName, permId] = dbPermMatch; + if (!databasePermissions[roleName]) databasePermissions[roleName] = {}; + if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {}; + databasePermissions[roleName][permId].permission = env[key]; + } + + // Table permissions + if (tableConnMatch) { + const [, roleName, permId] = tableConnMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].connection = env[key]; + } + if (tableDatabasesMatch) { + const [, roleName, permId] = tableDatabasesMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].databases = env[key]?.replace(/\|/g, '\n'); + } + if (tableDatabasesRegexMatch) { + const [, roleName, permId] = tableDatabasesRegexMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].databasesRegex = env[key]; + } + if (tableSchemasMatch) { + const [, roleName, permId] = tableSchemasMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].schemas = env[key]; + } + if (tableSchemasRegexMatch) { + const [, roleName, permId] = tableSchemasRegexMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].schemasRegex = env[key]; + } + if (tableTablesMatch) { + const [, roleName, permId] = tableTablesMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].tables = env[key]?.replace(/\|/g, '\n'); + } + if (tableTablesRegexMatch) { + const [, roleName, permId] = tableTablesRegexMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].tablesRegex = env[key]; + } + if (tablePermMatch) { + const [, roleName, permId] = tablePermMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].permission = env[key]; + } + if (tableScopeMatch) { + const [, roleName, permId] = tableScopeMatch; + if (!tablePermissions[roleName]) tablePermissions[roleName] = {}; + if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {}; + tablePermissions[roleName][permId].scope = env[key]; + } + } + + // Second pass: process roles, connections, and permissions + for (const key in env) { + const connMatch = key.match(connectionsRegex); + const permMatch = key.match(permissionsRegex); + if (connMatch) { + const roleName = connMatch[1]; + let role = roles.find(r => r.name === roleName); + if (!role) { + role = { + id: roles.length + 1, + name: roleName, + import_source_id: -1, + }; + roles.push(role); + } + const connIds = env[key] + .split(',') + .map(id => id.trim()) + .filter(id => id.length > 0); + for (const connId of connIds) { + const dbId = connectionEnvIdToDbId[connId]; + if (dbId) { + role_connections.push({ + role_id: role.id, + connection_id: dbId, + import_source_id: -1, + }); + } + } + } + if (permMatch) { + const roleName = permMatch[1]; + let role = roles.find(r => r.name === roleName); + if (!role) { + role = { + id: roles.length + 1, + name: roleName, + import_source_id: -1, + }; + roles.push(role); + } + const permissions = env[key] + .split(',') + .map(p => p.trim()) + .filter(p => p.length > 0); + for (const permission of permissions) { + role_permissions.push({ + role_id: role.id, + permission, + import_source_id: -1, + }); + } + } + } + + // Process database permissions + for (const roleName in databasePermissions) { + let role = roles.find(r => r.name === roleName); + if (!role) { + role = { + id: roles.length + 1, + name: roleName, + import_source_id: -1, + }; + roles.push(role); + } + + for (const permId in databasePermissions[roleName]) { + const perm = databasePermissions[roleName][permId]; + if (perm.connection && perm.permission) { + const dbId = connectionEnvIdToDbId[perm.connection]; + const permissionId = databasePermissionMap[perm.permission]; + if (dbId && permissionId) { + role_databases.push({ + role_id: role.id, + connection_id: dbId, + database_names_list: perm.databases || null, + database_names_regex: perm.databasesRegex || null, + database_permission_role_id: permissionId, + id_original: permId, + import_source_id: -1, + }); + } + } + } + } + + // Process table permissions + for (const roleName in tablePermissions) { + let role = roles.find(r => r.name === roleName); + if (!role) { + role = { + id: roles.length + 1, + name: roleName, + import_source_id: -1, + }; + roles.push(role); + } + + for (const permId in tablePermissions[roleName]) { + const perm = tablePermissions[roleName][permId]; + if (perm.connection && perm.permission) { + const dbId = connectionEnvIdToDbId[perm.connection]; + const permissionId = tablePermissionMap[perm.permission]; + const scopeId = tableScopeMap[perm.scope || 'all_objects']; + if (dbId && permissionId && scopeId) { + role_tables.push({ + role_id: role.id, + connection_id: dbId, + database_names_list: perm.databases || null, + database_names_regex: perm.databasesRegex || null, + schema_names_list: perm.schemas || null, + schema_names_regex: perm.schemasRegex || null, + table_names_list: perm.tables || null, + table_names_regex: perm.tablesRegex || null, + table_permission_role_id: permissionId, + table_permission_scope_id: scopeId, + id_original: permId, + import_source_id: -1, + }); + } + } + } + } + + if (connections.length == 0 && roles.length == 0) { + return null; + } + + return { + connections, + roles, + role_connections, + role_permissions, + role_databases, + role_tables, + }; +} + +function createStorageFromEnvReplicatorItems(importEntities) { + return [ + { + name: 'connections', + findExisting: true, + createNew: true, + updateExisting: true, + matchColumns: ['id_original', 'import_source_id'], + deleteMissing: true, + deleteRestrictionColumns: ['import_source_id'], + skipUpdateColumns: ['conid'], + jsonArray: importEntities.connections, + }, + { + name: 'roles', + findExisting: true, + createNew: true, + updateExisting: true, + matchColumns: ['name', 'import_source_id'], + deleteMissing: true, + deleteRestrictionColumns: ['import_source_id'], + jsonArray: importEntities.roles, + }, + { + name: 'role_connections', + findExisting: true, + createNew: true, + updateExisting: false, + deleteMissing: true, + matchColumns: ['role_id', 'connection_id', 'import_source_id'], + jsonArray: importEntities.role_connections, + deleteRestrictionColumns: ['import_source_id'], + }, + { + name: 'role_permissions', + findExisting: true, + createNew: true, + updateExisting: false, + deleteMissing: true, + matchColumns: ['role_id', 'permission', 'import_source_id'], + jsonArray: importEntities.role_permissions, + deleteRestrictionColumns: ['import_source_id'], + }, + { + name: 'role_databases', + findExisting: true, + createNew: true, + updateExisting: true, + deleteMissing: true, + matchColumns: ['role_id', 'id_original', 'import_source_id'], + jsonArray: importEntities.role_databases, + deleteRestrictionColumns: ['import_source_id'], + }, + { + name: 'role_tables', + findExisting: true, + createNew: true, + updateExisting: true, + deleteMissing: true, + matchColumns: ['role_id', 'id_original', 'import_source_id'], + jsonArray: importEntities.role_tables, + deleteRestrictionColumns: ['import_source_id'], + }, + ]; +} + +module.exports = { + extractConnectionsFromEnv, + extractImportEntitiesFromEnv, + createStorageFromEnvReplicatorItems, +}; diff --git a/packages/web/src/forms/FormArgument.svelte b/packages/web/src/forms/FormArgument.svelte index af7b9d27c..70340cd0a 100644 --- a/packages/web/src/forms/FormArgument.svelte +++ b/packages/web/src/forms/FormArgument.svelte @@ -11,6 +11,7 @@ export let arg; export let namePrefix; + export let isReadOnly = false; $: name = `${namePrefix}${arg.name}`; @@ -24,7 +25,7 @@ defaultValue={arg.default} focused={arg.focused} placeholder={arg.placeholder} - disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled} + disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)} /> {:else if arg.type == 'stringlist'} {:else if arg.type == 'number'} {:else if arg.type == 'checkbox'} {:else if arg.type == 'select'} _.isString(opt) ? { label: opt, value: opt } : { label: opt.name, value: opt.value } )} - disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled} + disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)} /> {:else if arg.type == 'dropdowntext'} setFieldValue(name, _.isString(opt) ? opt : opt.value), })); }} - disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled} + disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)} /> {/if} diff --git a/packages/web/src/forms/FormArgumentList.svelte b/packages/web/src/forms/FormArgumentList.svelte index 87c342022..b9861198c 100644 --- a/packages/web/src/forms/FormArgumentList.svelte +++ b/packages/web/src/forms/FormArgumentList.svelte @@ -3,10 +3,11 @@ export let namePrefix = ''; export let args: any[]; + export let isReadOnly = false;
{#each args as arg (arg.name)} - + {/each}
diff --git a/packages/web/src/forms/FormStringList.svelte b/packages/web/src/forms/FormStringList.svelte index 650872712..f33bdfe13 100644 --- a/packages/web/src/forms/FormStringList.svelte +++ b/packages/web/src/forms/FormStringList.svelte @@ -12,6 +12,7 @@ export let addButtonLabel; export let placeholder; export let templateProps; + export let isReadOnly = false; const { template, values, setFieldValue } = getFormContext(); @@ -20,7 +21,7 @@ {#each stringList as value, index} -
+
(i === index ? e.target['value'] : v)); setFieldValue(name, newValues); }} + disabled={isReadOnly} /> { setFieldValue(name, [...stringList.slice(0, index), ...stringList.slice(index + 1)]); }} + disabled={isReadOnly} > @@ -45,11 +48,12 @@ on:click={() => { setFieldValue(name, [...stringList, '']); }} + disabled={isReadOnly} /> \ No newline at end of file + .input-line-flex { + display: flex; + } + diff --git a/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte b/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte index 43ff82431..91388767b 100644 --- a/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte +++ b/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte @@ -6,6 +6,8 @@ import FormArgumentList from '../forms/FormArgumentList.svelte'; import { _t } from '../translations'; + export let isFormReadOnly; + const { values } = getFormContext(); $: engine = $values.engine; @@ -17,9 +19,18 @@ $: advancedFields = driver?.getAdvancedConnectionFields ? driver?.getAdvancedConnectionFields() : null; - - + + {#if advancedFields} - + {/if} diff --git a/packages/web/src/settings/ConnectionDriverFields.svelte b/packages/web/src/settings/ConnectionDriverFields.svelte index 144325c5d..45a2907b7 100644 --- a/packages/web/src/settings/ConnectionDriverFields.svelte +++ b/packages/web/src/settings/ConnectionDriverFields.svelte @@ -23,6 +23,7 @@ export let getDatabaseList; export let currentConnection; + export let isFormReadOnly; const { values, setFieldValue } = getFormContext(); const electron = getElectron(); @@ -90,10 +91,10 @@ label={_t('connection.type', { defaultMessage: 'Connection type' })} name="engine" isNative - disabled={isConnected} + disabled={isConnected || isFormReadOnly} data-testid="ConnectionDriverFields_connectionType" options={[ - { label: _t('connection.selectType', { defaultMessage: '(select connection type)' })}, + { label: _t('connection.selectType', { defaultMessage: '(select connection type)' }) }, ..._.sortBy( $extensions.drivers // .filter(driver => !driver.isElectronOnly || electron) @@ -113,7 +114,7 @@ data-testid="ConnectionDriverFields_authType" name="authType" isNative - disabled={isConnected} + disabled={isConnected || isFormReadOnly} defaultValue={driver?.defaultAuthTypeName} options={$authTypes.map(auth => ({ value: auth.name, @@ -127,16 +128,18 @@ {/if} {#if driver?.showConnectionField('autoDetectNatMap', $values, showConnectionFieldArgs)} {/if} @@ -146,13 +149,13 @@ {:else} {/if} {/if} @@ -160,11 +163,15 @@ {#if driver?.showConnectionField('useDatabaseUrl', $values, showConnectionFieldArgs)}
!!option.value == !!value} options={[ - { label: _t('connection.fillDetails', { defaultMessage: 'Fill database connection details' }), value: '', default: true }, + { + label: _t('connection.fillDetails', { defaultMessage: 'Fill database connection details' }), + value: '', + default: true, + }, { label: _t('connection.useUrl', { defaultMessage: 'Use database URL' }), value: '1' }, ]} /> @@ -177,7 +184,7 @@ name="databaseUrl" data-testid="ConnectionDriverFields_databaseUrl" placeholder={driver?.databaseUrlPlaceholder} - disabled={isConnected || disabledFields.includes('databaseUrl')} + disabled={isConnected || isFormReadOnly || disabledFields.includes('databaseUrl')} /> {/if} @@ -187,7 +194,7 @@ name="localDataCenter" data-testid="ConnectionDriverFields_localDataCenter" placeholder={driver?.defaultLocalDataCenter} - disabled={isConnected || disabledFields.includes('localDataCenter')} + disabled={isConnected || isFormReadOnly || disabledFields.includes('localDataCenter')} /> {/if} @@ -196,7 +203,7 @@ label={_t('connection.authToken', { defaultMessage: 'Auth token' })} name="authToken" data-testid="ConnectionDriverFields_authToken" - disabled={isConnected || disabledFields.includes('authToken')} + disabled={isConnected || isFormReadOnly || disabledFields.includes('authToken')} /> {/if} @@ -207,7 +214,7 @@ data-testid="ConnectionDriverFields_authType" name="authType" isNative - disabled={isConnected} + disabled={isConnected || isFormReadOnly} defaultValue={driver?.defaultAuthTypeName} options={$authTypes.map(auth => ({ value: auth.name, @@ -219,9 +226,9 @@ {#if driver?.showConnectionField('endpoint', $values, showConnectionFieldArgs)} {/if} @@ -230,7 +237,7 @@ {/if} @@ -239,7 +246,7 @@ {/if} @@ -250,7 +257,7 @@ @@ -260,7 +267,7 @@ - { _t('connection.dockerWarning', { defaultMessage: 'Under docker, localhost and 127.0.0.1 will not work, use dockerhost instead' }) } + {_t('connection.dockerWarning', { + defaultMessage: 'Under docker, localhost and 127.0.0.1 will not work, use dockerhost instead', + })}
{/if} {/if} @@ -280,9 +289,11 @@
@@ -293,7 +304,7 @@ isNative name="serviceNameType" defaultValue="serviceName" - disabled={isConnected} + disabled={isConnected || isFormReadOnly} templateProps={{ noMargin: true }} options={[ { value: 'serviceName', label: _t('connection.serviceName', { defaultMessage: 'Service name' }) }, @@ -309,7 +320,7 @@ @@ -322,7 +333,7 @@ @@ -333,7 +344,7 @@ @@ -345,7 +356,7 @@ {/if} @@ -353,7 +364,7 @@ {/if} @@ -380,7 +391,7 @@ @@ -391,7 +402,7 @@ @@ -405,12 +416,15 @@ isNative name="passwordMode" defaultValue="saveEncrypted" - disabled={isConnected} + disabled={isConnected || isFormReadOnly} options={[ { value: 'saveEncrypted', label: _t('connection.saveEncrypted', { defaultMessage: 'Save and encrypt' }) }, { value: 'saveRaw', label: _t('connection.saveRaw', { defaultMessage: 'Save raw (UNSAFE!!)' }) }, { value: 'askPassword', label: _t('connection.askPassword', { defaultMessage: "Don't save, ask for password" }) }, - { value: 'askUser', label: _t('connection.askUser', { defaultMessage: "Don't save, ask for login and password" }) }, + { + value: 'askUser', + label: _t('connection.askUser', { defaultMessage: "Don't save, ask for login and password" }), + }, ]} data-testid="ConnectionDriverFields_passwordMode" /> @@ -420,7 +434,7 @@ @@ -430,7 +444,7 @@ {/if} @@ -439,7 +453,7 @@ {/if} @@ -448,7 +462,7 @@ {/if} @@ -457,33 +471,42 @@ {/if} {#if defaultDatabase && driver?.showConnectionField('singleDatabase', $values, showConnectionFieldArgs)} {/if} {#if driver?.showConnectionField('useSeparateSchemas', $values, showConnectionFieldArgs)} {/if} {#if driver?.showConnectionField('connectionDefinition', $values, showConnectionFieldArgs)} - + {/if} {#if driver} @@ -493,7 +516,7 @@ label={_t('connection.displayName', { defaultMessage: 'Display name' })} name="displayName" templateProps={{ noMargin: true }} - disabled={isConnected} + disabled={isConnected || isFormReadOnly} data-testid="ConnectionDriverFields_displayName" placeholder={getConnectionLabel(currentConnection)} /> @@ -505,7 +528,7 @@ name="connectionColor" emptyLabel="(not selected)" templateProps={{ noMargin: true }} - disabled={isConnected} + disabled={isConnected || isFormReadOnly} data-testid="ConnectionDriverFields_connectionColor" />
diff --git a/packages/web/src/settings/ConnectionSshTunnelFields.svelte b/packages/web/src/settings/ConnectionSshTunnelFields.svelte index df51c2180..10d257890 100644 --- a/packages/web/src/settings/ConnectionSshTunnelFields.svelte +++ b/packages/web/src/settings/ConnectionSshTunnelFields.svelte @@ -14,6 +14,8 @@ import { extensions, openedConnections, openedSingleDatabaseConnections } from '../stores'; import { _t } from '../translations'; + export let isFormReadOnly; + const { values, setFieldValue } = getFormContext(); const electron = getElectron(); @@ -30,9 +32,9 @@ @@ -41,7 +43,7 @@ @@ -50,23 +52,30 @@
- + {/if} @@ -88,16 +97,16 @@
@@ -110,18 +119,18 @@
{#if electron} {:else}
@@ -143,9 +152,10 @@ {#if useSshTunnel && $values.sshMode == 'agent'}
{#if $platformInfo && $platformInfo.sshAuthSock} - {_t('connection.sshTunnel.agentFound', {defaultMessage: "SSH Agent found"})} + {_t('connection.sshTunnel.agentFound', { defaultMessage: 'SSH Agent found' })} {:else} - {_t('connection.sshTunnel.agentNotFound', {defaultMessage: "SSH Agent not found"})} + + {_t('connection.sshTunnel.agentNotFound', { defaultMessage: 'SSH Agent not found' })} {/if}
{/if} diff --git a/packages/web/src/settings/ConnectionSslFields.svelte b/packages/web/src/settings/ConnectionSslFields.svelte index a50928458..8a4f19fbd 100644 --- a/packages/web/src/settings/ConnectionSslFields.svelte +++ b/packages/web/src/settings/ConnectionSslFields.svelte @@ -9,6 +9,8 @@ import { openedConnections, openedSingleDatabaseConnections } from '../stores'; import { _t } from '../translations'; + export let isFormReadOnly; + const { values, setFieldValue } = getFormContext(); const electron = getElectron(); @@ -16,21 +18,35 @@ $: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id); - - + + + - diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index fa1352dbd..6862be765 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -59,6 +59,8 @@ } ); + $: isFormReadOnly = !!$values.import_source_id; + // $: console.log('ConnectionTab.$values', $values); // $: console.log('ConnectionTab.driver', driver); @@ -302,22 +304,25 @@ { label: _t('common.general', { defaultMessage: 'General' }), component: ConnectionDriverFields, - props: { getDatabaseList, currentConnection }, + props: { getDatabaseList, currentConnection, isFormReadOnly }, testid: 'ConnectionTab_tabGeneral', }, driver?.showConnectionTab('sshTunnel', $values) && { label: 'SSH Tunnel', component: ConnectionSshTunnelFields, + props: { isFormReadOnly }, testid: 'ConnectionTab_tabSshTunnel', }, driver?.showConnectionTab('ssl', $values) && { label: 'SSL', component: ConnectionSslFields, + props: { isFormReadOnly }, testid: 'ConnectionTab_tabSsl', }, { label: _t('common.advanced', { defaultMessage: 'Advanced' }), component: ConnectionAdvancedDriverFields, + props: { isFormReadOnly }, testid: 'ConnectionTab_tabAdvanced', }, ]} @@ -383,7 +388,8 @@ {/if} {#if isTesting}
- {_t('common.testingConnection', { defaultMessage: 'Testing connection' })} + + {_t('common.testingConnection', { defaultMessage: 'Testing connection' })}
{/if}
diff --git a/packages/web/src/translations.ts b/packages/web/src/translations.ts index 00b30f9e2..8ddb2fd12 100644 --- a/packages/web/src/translations.ts +++ b/packages/web/src/translations.ts @@ -124,7 +124,7 @@ export function __t(key: string, options: TranslateOptions): DefferedTranslation }; } -export function _tval(x: string | DefferedTranslationResult): string { +export function _tval(x: any | DefferedTranslationResult): string { if (typeof x === 'string') return x; if (typeof x?._transKey === 'string') { return _t(x._transKey, x._transOptions); @@ -132,7 +132,7 @@ export function _tval(x: string | DefferedTranslationResult): string { if (typeof x?._transCallback === 'function') { return x._transCallback(); } - return ''; + return x?.toString() || ''; } export function isDefferedTranslationResult(x: string | DefferedTranslationResult): x is DefferedTranslationResult {