diff --git a/app/src/mainMenuDefinition.js b/app/src/mainMenuDefinition.js
index c4de505d0..466999fc2 100644
--- a/app/src/mainMenuDefinition.js
+++ b/app/src/mainMenuDefinition.js
@@ -10,6 +10,7 @@ module.exports = ({ editMenu, isMac }) => [
{ command: 'new.queryDesign', hideDisabled: true },
{ command: 'new.diagram', hideDisabled: true },
{ command: 'new.perspective', hideDisabled: true },
+ { command: 'new.application', hideDisabled: true },
{ command: 'new.shell', hideDisabled: true },
{ command: 'new.jsonl', hideDisabled: true },
{ command: 'new.modelTransform', hideDisabled: true },
diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js
index 153782ae8..af57d0cd4 100644
--- a/packages/api/src/auth/authProvider.js
+++ b/packages/api/src/auth/authProvider.js
@@ -55,6 +55,10 @@ class AuthProviderBase {
return [];
}
+ async getCurrentFilePermissions(req) {
+ return [];
+ }
+
getLoginPageConnections() {
return null;
}
diff --git a/packages/api/src/controllers/apps.js b/packages/api/src/controllers/apps.js
index 43c2d6e28..156848d51 100644
--- a/packages/api/src/controllers/apps.js
+++ b/packages/api/src/controllers/apps.js
@@ -1,233 +1,98 @@
const fs = require('fs-extra');
const _ = require('lodash');
const path = require('path');
-const { appdir } = require('../utility/directories');
+const { appdir, filesdir } = require('../utility/directories');
const socket = require('../utility/socket');
const connections = require('./connections');
+const {
+ loadPermissionsFromRequest,
+ loadFilePermissionsFromRequest,
+ hasPermission,
+ getFilePermissionRole,
+} = require('../utility/hasPermission');
module.exports = {
- folders_meta: true,
- async folders() {
- const folders = await fs.readdir(appdir());
- return [
- ...folders.map(name => ({
- name,
- })),
- ];
- },
-
- createFolder_meta: true,
- async createFolder({ folder }) {
- const name = await this.getNewAppFolder({ name: folder });
- await fs.mkdir(path.join(appdir(), name));
- socket.emitChanged('app-folders-changed');
- this.emitChangedDbApp(folder);
- return name;
- },
-
- files_meta: true,
- async files({ folder }) {
- if (!folder) return [];
- const dir = path.join(appdir(), folder);
- if (!(await fs.exists(dir))) return [];
- const files = await fs.readdir(dir);
-
- function fileType(ext, type) {
- return files
- .filter(name => name.endsWith(ext))
- .map(name => ({
- name: name.slice(0, -ext.length),
- label: path.parse(name.slice(0, -ext.length)).base,
- type,
- }));
- }
-
- return [
- ...fileType('.command.sql', 'command.sql'),
- ...fileType('.query.sql', 'query.sql'),
- ...fileType('.config.json', 'config.json'),
- ];
- },
-
- async emitChangedDbApp(folder) {
- const used = await this.getUsedAppFolders();
- if (used.includes(folder)) {
- socket.emitChanged('used-apps-changed');
- }
- },
-
- refreshFiles_meta: true,
- async refreshFiles({ folder }) {
- socket.emitChanged('app-files-changed', { app: folder });
- },
-
- refreshFolders_meta: true,
- async refreshFolders() {
- socket.emitChanged(`app-folders-changed`);
- },
-
- deleteFile_meta: true,
- async deleteFile({ folder, file, fileType }) {
- await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
- socket.emitChanged('app-files-changed', { app: folder });
- this.emitChangedDbApp(folder);
- },
-
- renameFile_meta: true,
- async renameFile({ folder, file, newFile, fileType }) {
- await fs.rename(
- path.join(path.join(appdir(), folder), `${file}.${fileType}`),
- path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
- );
- socket.emitChanged('app-files-changed', { app: folder });
- this.emitChangedDbApp(folder);
- },
-
- renameFolder_meta: true,
- async renameFolder({ folder, newFolder }) {
- const uniqueName = await this.getNewAppFolder({ name: newFolder });
- await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
- socket.emitChanged(`app-folders-changed`);
- },
-
- deleteFolder_meta: true,
- async deleteFolder({ folder }) {
- if (!folder) throw new Error('Missing folder parameter');
- await fs.rmdir(path.join(appdir(), folder), { recursive: true });
- socket.emitChanged(`app-folders-changed`);
- socket.emitChanged('app-files-changed', { app: folder });
- socket.emitChanged('used-apps-changed');
- },
-
- async getNewAppFolder({ name }) {
- if (!(await fs.exists(path.join(appdir(), name)))) return name;
- let index = 2;
- while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
- index += 1;
- }
- return `${name}${index}`;
- },
-
- getUsedAppFolders_meta: true,
- async getUsedAppFolders() {
- const list = await connections.list();
- const apps = [];
-
- for (const connection of list) {
- for (const db of connection.databases || []) {
- for (const key of _.keys(db || {})) {
- if (key.startsWith('useApp:') && db[key]) {
- apps.push(key.substring('useApp:'.length));
- }
- }
- }
- }
-
- return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
- },
-
- getUsedApps_meta: true,
- async getUsedApps() {
- const apps = await this.getUsedAppFolders();
+ getAllApps_meta: true,
+ async getAllApps({}, req) {
+ const dir = path.join(filesdir(), 'apps');
const res = [];
+ const loadedPermissions = await loadPermissionsFromRequest(req);
+ const filePermissions = await loadFilePermissionsFromRequest(req);
- for (const folder of apps) {
- res.push(await this.loadApp({ folder }));
- }
- return res;
- },
-
- // getAppsForDb_meta: true,
- // async getAppsForDb({ conid, database }) {
- // const connection = await connections.get({ conid });
- // if (!connection) return [];
- // const db = (connection.databases || []).find(x => x.name == database);
- // const apps = [];
- // const res = [];
- // if (db) {
- // for (const key of _.keys(db || {})) {
- // if (key.startsWith('useApp:') && db[key]) {
- // apps.push(key.substring('useApp:'.length));
- // }
- // }
- // }
- // for (const folder of apps) {
- // res.push(await this.loadApp({ folder }));
- // }
- // return res;
- // },
-
- loadApp_meta: true,
- async loadApp({ folder }) {
- const res = {
- queries: [],
- commands: [],
- name: folder,
- };
- const dir = path.join(appdir(), folder);
- if (await fs.exists(dir)) {
- const files = await fs.readdir(dir);
-
- async function processType(ext, field) {
- for (const file of files) {
- if (file.endsWith(ext)) {
- res[field].push({
- name: file.slice(0, -ext.length),
- sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
- });
- }
- }
+ for (const file of await fs.readdir(dir)) {
+ if (!hasPermission(`all-files`, loadedPermissions)) {
+ const role = getFilePermissionRole('apps', file, filePermissions);
+ if (role == 'deny') continue;
}
+ const content = await fs.readFile(path.join(dir, file), { encoding: 'utf-8' });
+ const appJson = JSON.parse(content);
+ // const app = {
+ // appid: file,
+ // name: appJson.applicationName,
+ // usageRules: appJson.usageRules || [],
+ // icon: appJson.applicationIcon || 'img app',
+ // color: appJson.applicationColor,
+ // queries: Object.values(appJson.files || {})
+ // .filter(x => x.type == 'query')
+ // .map(x => ({
+ // name: x.label,
+ // sql: x.sql,
+ // })),
+ // commands: Object.values(appJson.files || {})
+ // .filter(x => x.type == 'command')
+ // .map(x => ({
+ // name: x.label,
+ // sql: x.sql,
+ // })),
+ // virtualReferences: appJson.virtualReferences,
+ // dictionaryDescriptions: appJson.dictionaryDescriptions,
+ // };
+ const app = {
+ ...appJson,
+ appid: file,
+ };
- await processType('.command.sql', 'commands');
- await processType('.query.sql', 'queries');
+ res.push(app);
}
-
- try {
- res.virtualReferences = JSON.parse(
- await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
- );
- } catch (err) {
- res.virtualReferences = [];
- }
- try {
- res.dictionaryDescriptions = JSON.parse(
- await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
- );
- } catch (err) {
- res.dictionaryDescriptions = [];
- }
-
return res;
},
- async saveConfigFile(appFolder, filename, filterFunc, newItem) {
- const file = path.join(appdir(), appFolder, filename);
-
- let json;
- try {
- json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
- } catch (err) {
- json = [];
+ createAppFromDb_meta: true,
+ async createAppFromDb({ appName, server, database }, req) {
+ const appdir = path.join(filesdir(), 'apps');
+ if (!fs.existsSync(appdir)) {
+ await fs.mkdir(appdir);
}
-
- if (filterFunc) {
- json = json.filter(filterFunc);
+ const appId = _.kebabCase(appName);
+ let suffix = undefined;
+ while (fs.existsSync(path.join(appdir, `${appId}${suffix || ''}`))) {
+ if (!suffix) suffix = 2;
+ else suffix++;
}
+ const finalAppId = `${appId}${suffix || ''}`;
- json = [...json, newItem];
+ const appJson = {
+ applicationName: appName,
+ usageRules: [
+ {
+ serverHostsList: server,
+ databaseNamesList: database,
+ },
+ ],
+ };
- await fs.writeFile(file, JSON.stringify(json, undefined, 2));
+ await fs.writeFile(path.join(appdir, `${finalAppId}`), JSON.stringify(appJson, undefined, 2));
- socket.emitChanged('app-files-changed', { app: appFolder });
- socket.emitChanged('used-apps-changed');
+ socket.emitChanged(`files-changed`, { folder: 'apps' });
+
+ return finalAppId;
},
saveVirtualReference_meta: true,
- async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
- await this.saveConfigFile(
- appFolder,
- 'virtual-references.config.json',
+ async saveVirtualReference({ appid, schemaName, pureName, refSchemaName, refTableName, columns }) {
+ await this.saveConfigItem(
+ appid,
+ 'virtualReferences',
columns.length == 1
? x =>
!(
@@ -245,14 +110,17 @@ module.exports = {
columns,
}
);
+
+ socket.emitChanged(`files-changed`, { folder: 'apps' });
+
return true;
},
saveDictionaryDescription_meta: true,
- async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
- await this.saveConfigFile(
- appFolder,
- 'dictionary-descriptions.config.json',
+ async saveDictionaryDescription({ appid, pureName, schemaName, expression, columns, delimiter }) {
+ await this.saveConfigItem(
+ appid,
+ 'dictionaryDescriptions',
x => !(x.schemaName == schemaName && x.pureName == pureName),
{
schemaName,
@@ -263,18 +131,271 @@ module.exports = {
}
);
+ socket.emitChanged(`files-changed`, { folder: 'apps' });
+
return true;
},
- createConfigFile_meta: true,
- async createConfigFile({ appFolder, fileName, content }) {
- const file = path.join(appdir(), appFolder, fileName);
- if (!(await fs.exists(file))) {
- await fs.writeFile(file, JSON.stringify(content, undefined, 2));
- socket.emitChanged('app-files-changed', { app: appFolder });
- socket.emitChanged('used-apps-changed');
- return true;
+ async saveConfigItem(appid, fieldName, filterFunc, newItem) {
+ const file = path.join(filesdir(), 'apps', appid);
+
+ const appJson = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
+ let json = appJson[fieldName] || [];
+
+ if (filterFunc) {
+ json = json.filter(filterFunc);
}
- return false;
+
+ json = [...json, newItem];
+
+ await fs.writeFile(
+ file,
+ JSON.stringify(
+ {
+ ...appJson,
+ [fieldName]: json,
+ },
+ undefined,
+ 2
+ )
+ );
+
+ socket.emitChanged('files-changed', { folder: 'apps' });
},
+
+ // folders_meta: true,
+ // async folders() {
+ // const folders = await fs.readdir(appdir());
+ // return [
+ // ...folders.map(name => ({
+ // name,
+ // })),
+ // ];
+ // },
+
+ // createFolder_meta: true,
+ // async createFolder({ folder }) {
+ // const name = await this.getNewAppFolder({ name: folder });
+ // await fs.mkdir(path.join(appdir(), name));
+ // socket.emitChanged('app-folders-changed');
+ // this.emitChangedDbApp(folder);
+ // return name;
+ // },
+
+ // files_meta: true,
+ // async files({ folder }) {
+ // if (!folder) return [];
+ // const dir = path.join(appdir(), folder);
+ // if (!(await fs.exists(dir))) return [];
+ // const files = await fs.readdir(dir);
+
+ // function fileType(ext, type) {
+ // return files
+ // .filter(name => name.endsWith(ext))
+ // .map(name => ({
+ // name: name.slice(0, -ext.length),
+ // label: path.parse(name.slice(0, -ext.length)).base,
+ // type,
+ // }));
+ // }
+
+ // return [
+ // ...fileType('.command.sql', 'command.sql'),
+ // ...fileType('.query.sql', 'query.sql'),
+ // ...fileType('.config.json', 'config.json'),
+ // ];
+ // },
+
+ // async emitChangedDbApp(folder) {
+ // const used = await this.getUsedAppFolders();
+ // if (used.includes(folder)) {
+ // socket.emitChanged('used-apps-changed');
+ // }
+ // },
+
+ // refreshFiles_meta: true,
+ // async refreshFiles({ folder }) {
+ // socket.emitChanged('app-files-changed', { app: folder });
+ // },
+
+ // refreshFolders_meta: true,
+ // async refreshFolders() {
+ // socket.emitChanged(`app-folders-changed`);
+ // },
+
+ // deleteFile_meta: true,
+ // async deleteFile({ folder, file, fileType }) {
+ // await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
+ // socket.emitChanged('app-files-changed', { app: folder });
+ // this.emitChangedDbApp(folder);
+ // },
+
+ // renameFile_meta: true,
+ // async renameFile({ folder, file, newFile, fileType }) {
+ // await fs.rename(
+ // path.join(path.join(appdir(), folder), `${file}.${fileType}`),
+ // path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
+ // );
+ // socket.emitChanged('app-files-changed', { app: folder });
+ // this.emitChangedDbApp(folder);
+ // },
+
+ // renameFolder_meta: true,
+ // async renameFolder({ folder, newFolder }) {
+ // const uniqueName = await this.getNewAppFolder({ name: newFolder });
+ // await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
+ // socket.emitChanged(`app-folders-changed`);
+ // },
+
+ // deleteFolder_meta: true,
+ // async deleteFolder({ folder }) {
+ // if (!folder) throw new Error('Missing folder parameter');
+ // await fs.rmdir(path.join(appdir(), folder), { recursive: true });
+ // socket.emitChanged(`app-folders-changed`);
+ // socket.emitChanged('app-files-changed', { app: folder });
+ // socket.emitChanged('used-apps-changed');
+ // },
+
+ // async getNewAppFolder({ name }) {
+ // if (!(await fs.exists(path.join(appdir(), name)))) return name;
+ // let index = 2;
+ // while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
+ // index += 1;
+ // }
+ // return `${name}${index}`;
+ // },
+
+ // getUsedAppFolders_meta: true,
+ // async getUsedAppFolders() {
+ // const list = await connections.list();
+ // const apps = [];
+
+ // for (const connection of list) {
+ // for (const db of connection.databases || []) {
+ // for (const key of _.keys(db || {})) {
+ // if (key.startsWith('useApp:') && db[key]) {
+ // apps.push(key.substring('useApp:'.length));
+ // }
+ // }
+ // }
+ // }
+
+ // return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
+ // },
+
+ // // getAppsForDb_meta: true,
+ // // async getAppsForDb({ conid, database }) {
+ // // const connection = await connections.get({ conid });
+ // // if (!connection) return [];
+ // // const db = (connection.databases || []).find(x => x.name == database);
+ // // const apps = [];
+ // // const res = [];
+ // // if (db) {
+ // // for (const key of _.keys(db || {})) {
+ // // if (key.startsWith('useApp:') && db[key]) {
+ // // apps.push(key.substring('useApp:'.length));
+ // // }
+ // // }
+ // // }
+ // // for (const folder of apps) {
+ // // res.push(await this.loadApp({ folder }));
+ // // }
+ // // return res;
+ // // },
+
+ // loadApp_meta: true,
+ // async loadApp({ folder }) {
+ // const res = {
+ // queries: [],
+ // commands: [],
+ // name: folder,
+ // };
+ // const dir = path.join(appdir(), folder);
+ // if (await fs.exists(dir)) {
+ // const files = await fs.readdir(dir);
+
+ // async function processType(ext, field) {
+ // for (const file of files) {
+ // if (file.endsWith(ext)) {
+ // res[field].push({
+ // name: file.slice(0, -ext.length),
+ // sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
+ // });
+ // }
+ // }
+ // }
+
+ // await processType('.command.sql', 'commands');
+ // await processType('.query.sql', 'queries');
+ // }
+
+ // try {
+ // res.virtualReferences = JSON.parse(
+ // await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
+ // );
+ // } catch (err) {
+ // res.virtualReferences = [];
+ // }
+ // try {
+ // res.dictionaryDescriptions = JSON.parse(
+ // await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
+ // );
+ // } catch (err) {
+ // res.dictionaryDescriptions = [];
+ // }
+
+ // return res;
+ // },
+
+ // async saveConfigFile(appFolder, filename, filterFunc, newItem) {
+ // const file = path.join(appdir(), appFolder, filename);
+
+ // let json;
+ // try {
+ // json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
+ // } catch (err) {
+ // json = [];
+ // }
+
+ // if (filterFunc) {
+ // json = json.filter(filterFunc);
+ // }
+
+ // json = [...json, newItem];
+
+ // await fs.writeFile(file, JSON.stringify(json, undefined, 2));
+
+ // socket.emitChanged('app-files-changed', { app: appFolder });
+ // socket.emitChanged('used-apps-changed');
+ // },
+
+ // saveDictionaryDescription_meta: true,
+ // async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
+ // await this.saveConfigFile(
+ // appFolder,
+ // 'dictionary-descriptions.config.json',
+ // x => !(x.schemaName == schemaName && x.pureName == pureName),
+ // {
+ // schemaName,
+ // pureName,
+ // expression,
+ // columns,
+ // delimiter,
+ // }
+ // );
+
+ // return true;
+ // },
+
+ // createConfigFile_meta: true,
+ // async createConfigFile({ appFolder, fileName, content }) {
+ // const file = path.join(appdir(), appFolder, fileName);
+ // if (!(await fs.exists(file))) {
+ // await fs.writeFile(file, JSON.stringify(content, undefined, 2));
+ // socket.emitChanged('app-files-changed', { app: appFolder });
+ // socket.emitChanged('used-apps-changed');
+ // return true;
+ // }
+ // return false;
+ // },
};
diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js
index 4d776c73c..57c1b68c2 100644
--- a/packages/api/src/controllers/files.js
+++ b/packages/api/src/controllers/files.js
@@ -3,7 +3,12 @@ const path = require('path');
const crypto = require('crypto');
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories');
const getChartExport = require('../utility/getChartExport');
-const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
+const {
+ hasPermission,
+ loadPermissionsFromRequest,
+ loadFilePermissionsFromRequest,
+ getFilePermissionRole,
+} = require('../utility/hasPermission');
const socket = require('../utility/socket');
const scheduler = require('./scheduler');
const getDiagramExport = require('../utility/getDiagramExport');
diff --git a/packages/api/src/storageModel.js b/packages/api/src/storageModel.js
index 88c1e8346..3c8ee800f 100644
--- a/packages/api/src/storageModel.js
+++ b/packages/api/src/storageModel.js
@@ -745,6 +745,88 @@ module.exports = {
}
]
},
+ {
+ "pureName": "file_permission_roles",
+ "columns": [
+ {
+ "pureName": "file_permission_roles",
+ "columnName": "id",
+ "dataType": "int",
+ "autoIncrement": true,
+ "notNull": true
+ },
+ {
+ "pureName": "file_permission_roles",
+ "columnName": "name",
+ "dataType": "varchar(100)",
+ "notNull": true
+ }
+ ],
+ "foreignKeys": [],
+ "primaryKey": {
+ "pureName": "file_permission_roles",
+ "constraintType": "primaryKey",
+ "constraintName": "PK_file_permission_roles",
+ "columns": [
+ {
+ "columnName": "id"
+ }
+ ]
+ },
+ "preloadedRows": [
+ {
+ "id": -1,
+ "name": "allow"
+ },
+ {
+ "id": -2,
+ "name": "deny"
+ }
+ ]
+ },
+ {
+ "pureName": "roles",
+ "columns": [
+ {
+ "pureName": "roles",
+ "columnName": "id",
+ "dataType": "int",
+ "autoIncrement": true,
+ "notNull": true
+ },
+ {
+ "pureName": "roles",
+ "columnName": "name",
+ "dataType": "varchar(250)",
+ "notNull": false
+ }
+ ],
+ "foreignKeys": [],
+ "primaryKey": {
+ "pureName": "roles",
+ "constraintType": "primaryKey",
+ "constraintName": "PK_roles",
+ "columns": [
+ {
+ "columnName": "id"
+ }
+ ]
+ },
+ "preloadedRows": [
+ {
+ "id": -1,
+ "name": "anonymous-user"
+ },
+ {
+ "id": -2,
+ "name": "logged-user"
+ },
+ {
+ "id": -3,
+ "name": "superadmin"
+ }
+ ]
+ },
{
"pureName": "role_connections",
"columns": [
@@ -899,6 +981,85 @@ module.exports = {
]
}
},
+ {
+ "pureName": "role_files",
+ "columns": [
+ {
+ "pureName": "role_files",
+ "columnName": "id",
+ "dataType": "int",
+ "autoIncrement": true,
+ "notNull": true
+ },
+ {
+ "pureName": "role_files",
+ "columnName": "role_id",
+ "dataType": "int",
+ "notNull": true
+ },
+ {
+ "pureName": "role_files",
+ "columnName": "folder_name",
+ "dataType": "varchar(100)",
+ "notNull": false
+ },
+ {
+ "pureName": "role_files",
+ "columnName": "file_names_list",
+ "dataType": "varchar(1000)",
+ "notNull": false
+ },
+ {
+ "pureName": "role_files",
+ "columnName": "file_names_regex",
+ "dataType": "varchar(1000)",
+ "notNull": false
+ },
+ {
+ "pureName": "role_files",
+ "columnName": "file_permission_role_id",
+ "dataType": "int",
+ "notNull": true
+ }
+ ],
+ "foreignKeys": [
+ {
+ "constraintType": "foreignKey",
+ "constraintName": "FK_role_files_role_id",
+ "pureName": "role_files",
+ "refTableName": "roles",
+ "deleteAction": "CASCADE",
+ "columns": [
+ {
+ "columnName": "role_id",
+ "refColumnName": "id"
+ }
+ ]
+ },
+ {
+ "constraintType": "foreignKey",
+ "constraintName": "FK_role_files_file_permission_role_id",
+ "pureName": "role_files",
+ "refTableName": "file_permission_roles",
+ "columns": [
+ {
+ "columnName": "file_permission_role_id",
+ "refColumnName": "id"
+ }
+ ]
+ }
+ ],
+ "primaryKey": {
+ "pureName": "role_files",
+ "constraintType": "primaryKey",
+ "constraintName": "PK_role_files",
+ "columns": [
+ {
+ "columnName": "id"
+ }
+ ]
+ }
+ },
{
"pureName": "role_permissions",
"columns": [
@@ -1082,49 +1243,6 @@ module.exports = {
]
}
},
- {
- "pureName": "roles",
- "columns": [
- {
- "pureName": "roles",
- "columnName": "id",
- "dataType": "int",
- "autoIncrement": true,
- "notNull": true
- },
- {
- "pureName": "roles",
- "columnName": "name",
- "dataType": "varchar(250)",
- "notNull": false
- }
- ],
- "foreignKeys": [],
- "primaryKey": {
- "pureName": "roles",
- "constraintType": "primaryKey",
- "constraintName": "PK_roles",
- "columns": [
- {
- "columnName": "id"
- }
- ]
- },
- "preloadedRows": [
- {
- "id": -1,
- "name": "anonymous-user"
- },
- {
- "id": -2,
- "name": "logged-user"
- },
- {
- "id": -3,
- "name": "superadmin"
- }
- ]
- },
{
"pureName": "table_permission_roles",
"columns": [
@@ -1243,6 +1361,47 @@ module.exports = {
}
]
},
+ {
+ "pureName": "users",
+ "columns": [
+ {
+ "pureName": "users",
+ "columnName": "id",
+ "dataType": "int",
+ "autoIncrement": true,
+ "notNull": true
+ },
+ {
+ "pureName": "users",
+ "columnName": "login",
+ "dataType": "varchar(250)",
+ "notNull": false
+ },
+ {
+ "pureName": "users",
+ "columnName": "password",
+ "dataType": "varchar(250)",
+ "notNull": false
+ },
+ {
+ "pureName": "users",
+ "columnName": "email",
+ "dataType": "varchar(250)",
+ "notNull": false
+ }
+ ],
+ "foreignKeys": [],
+ "primaryKey": {
+ "pureName": "users",
+ "constraintType": "primaryKey",
+ "constraintName": "PK_users",
+ "columns": [
+ {
+ "columnName": "id"
+ }
+ ]
+ }
+ },
{
"pureName": "user_connections",
"columns": [
@@ -1397,6 +1556,85 @@ module.exports = {
]
}
},
+ {
+ "pureName": "user_files",
+ "columns": [
+ {
+ "pureName": "user_files",
+ "columnName": "id",
+ "dataType": "int",
+ "autoIncrement": true,
+ "notNull": true
+ },
+ {
+ "pureName": "user_files",
+ "columnName": "user_id",
+ "dataType": "int",
+ "notNull": true
+ },
+ {
+ "pureName": "user_files",
+ "columnName": "folder_name",
+ "dataType": "varchar(100)",
+ "notNull": false
+ },
+ {
+ "pureName": "user_files",
+ "columnName": "file_names_list",
+ "dataType": "varchar(1000)",
+ "notNull": false
+ },
+ {
+ "pureName": "user_files",
+ "columnName": "file_names_regex",
+ "dataType": "varchar(1000)",
+ "notNull": false
+ },
+ {
+ "pureName": "user_files",
+ "columnName": "file_permission_role_id",
+ "dataType": "int",
+ "notNull": true
+ }
+ ],
+ "foreignKeys": [
+ {
+ "constraintType": "foreignKey",
+ "constraintName": "FK_user_files_user_id",
+ "pureName": "user_files",
+ "refTableName": "users",
+ "deleteAction": "CASCADE",
+ "columns": [
+ {
+ "columnName": "user_id",
+ "refColumnName": "id"
+ }
+ ]
+ },
+ {
+ "constraintType": "foreignKey",
+ "constraintName": "FK_user_files_file_permission_role_id",
+ "pureName": "user_files",
+ "refTableName": "file_permission_roles",
+ "columns": [
+ {
+ "columnName": "file_permission_role_id",
+ "refColumnName": "id"
+ }
+ ]
+ }
+ ],
+ "primaryKey": {
+ "pureName": "user_files",
+ "constraintType": "primaryKey",
+ "constraintName": "PK_user_files",
+ "columns": [
+ {
+ "columnName": "id"
+ }
+ ]
+ }
+ },
{
"pureName": "user_permissions",
"columns": [
@@ -1641,47 +1879,6 @@ module.exports = {
}
]
}
- },
- {
- "pureName": "users",
- "columns": [
- {
- "pureName": "users",
- "columnName": "id",
- "dataType": "int",
- "autoIncrement": true,
- "notNull": true
- },
- {
- "pureName": "users",
- "columnName": "login",
- "dataType": "varchar(250)",
- "notNull": false
- },
- {
- "pureName": "users",
- "columnName": "password",
- "dataType": "varchar(250)",
- "notNull": false
- },
- {
- "pureName": "users",
- "columnName": "email",
- "dataType": "varchar(250)",
- "notNull": false
- }
- ],
- "foreignKeys": [],
- "primaryKey": {
- "pureName": "users",
- "constraintType": "primaryKey",
- "constraintName": "PK_users",
- "columns": [
- {
- "columnName": "id"
- }
- ]
- }
}
],
"collections": [],
diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js
index c34bd410c..2e9902c2f 100644
--- a/packages/api/src/utility/hasPermission.js
+++ b/packages/api/src/utility/hasPermission.js
@@ -85,6 +85,16 @@ async function loadTablePermissionsFromRequest(req) {
return tablePermissions;
}
+async function loadFilePermissionsFromRequest(req) {
+ const authProvider = getAuthProviderFromReq(req);
+ if (!req) {
+ return null;
+ }
+
+ const filePermissions = await authProvider.getCurrentFilePermissions(req);
+ return filePermissions;
+}
+
function matchDatabasePermissionRow(conid, database, permissionRow) {
if (permissionRow.connection_id) {
if (conid != permissionRow.connection_id) {
@@ -135,6 +145,27 @@ function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissi
return true;
}
+function matchFilePermissionRow(folder, file, permissionRow) {
+ if (permissionRow.folder_name) {
+ if (folder != permissionRow.folder_name) {
+ return false;
+ }
+ }
+ if (permissionRow.file_names_list) {
+ const items = permissionRow.file_names_list.split('\n');
+ if (!items.find(item => item.trim()?.toLowerCase() === file?.toLowerCase())) {
+ return false;
+ }
+ }
+ if (permissionRow.file_names_regex) {
+ const regex = new RegExp(permissionRow.file_names_regex, 'i');
+ if (!regex.test(file)) {
+ return false;
+ }
+ }
+ return true;
+}
+
const DATABASE_ROLE_ID_NAMES = {
'-1': 'view',
'-2': 'read_content',
@@ -143,6 +174,11 @@ const DATABASE_ROLE_ID_NAMES = {
'-5': 'deny',
};
+const FILE_ROLE_ID_NAMES = {
+ '-1': 'allow',
+ '-2': 'deny',
+};
+
function getDatabaseRoleLevelIndex(roleName) {
if (!roleName) {
return 6;
@@ -198,6 +234,17 @@ function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) {
return res;
}
+function getFilePermissionRole(folder, file, loadedFilePermissions) {
+ let res = 'deny';
+ for (const permissionRow of loadedFilePermissions) {
+ if (!matchFilePermissionRow(folder, file, permissionRow)) {
+ continue;
+ }
+ res = FILE_ROLE_ID_NAMES[permissionRow.file_permission_role_id];
+ }
+ return res;
+}
+
const TABLE_ROLE_ID_NAMES = {
'-1': 'read',
'-2': 'update_only',
@@ -308,8 +355,10 @@ module.exports = {
loadPermissionsFromRequest,
loadDatabasePermissionsFromRequest,
loadTablePermissionsFromRequest,
+ loadFilePermissionsFromRequest,
getDatabasePermissionRole,
getTablePermissionRole,
+ getFilePermissionRole,
testStandardPermission,
testDatabaseRolePermission,
getTablePermissionRoleLevelIndex,
diff --git a/packages/tools/src/nameTools.ts b/packages/tools/src/nameTools.ts
index 617481a3e..b63922ac9 100644
--- a/packages/tools/src/nameTools.ts
+++ b/packages/tools/src/nameTools.ts
@@ -111,3 +111,20 @@ export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
}
return res;
}
+
+export const DATA_FOLDER_NAMES = [
+ { name: 'sql', label: 'SQL scripts' },
+ { name: 'shell', label: 'Shell scripts' },
+ { name: 'markdown', label: 'Markdown files' },
+ { name: 'charts', label: 'Charts' },
+ { name: 'query', label: 'Query designs' },
+ { name: 'sqlite', label: 'SQLite files' },
+ { name: 'duckdb', label: 'DuckDB files' },
+ { name: 'diagrams', label: 'Diagrams' },
+ { name: 'perspectives', label: 'Perspectives' },
+ { name: 'impexp', label: 'Import/Export jobs' },
+ { name: 'modtrans', label: 'Model transforms' },
+ { name: 'datadeploy', label: 'Data deploy jobs' },
+ { name: 'dbcompare', label: 'Database compare jobs' },
+ { name: 'apps', label: 'Applications' },
+];
diff --git a/packages/types/appdefs.d.ts b/packages/types/appdefs.d.ts
index b9826bebf..549b9cd5c 100644
--- a/packages/types/appdefs.d.ts
+++ b/packages/types/appdefs.d.ts
@@ -1,12 +1,12 @@
-interface ApplicationCommand {
- name: string;
- sql: string;
-}
+// interface ApplicationCommand {
+// name: string;
+// sql: string;
+// }
-interface ApplicationQuery {
- name: string;
- sql: string;
-}
+// interface ApplicationQuery {
+// name: string;
+// sql: string;
+// }
interface VirtualReferenceDefinition {
pureName: string;
@@ -27,11 +27,31 @@ interface DictionaryDescriptionDefinition {
delimiter: string;
}
-export interface ApplicationDefinition {
- name: string;
-
- queries: ApplicationQuery[];
- commands: ApplicationCommand[];
- virtualReferences: VirtualReferenceDefinition[];
- dictionaryDescriptions: DictionaryDescriptionDefinition[];
+interface ApplicationUsageRule {
+ conditionGroup?: string;
+ serverHostsRegex?: string;
+ serverHostsList?: string[];
+ databaseNamesRegex?: string;
+ databaseNamesList?: string[];
+ tableNamesRegex?: string;
+ tableNamesList?: string[];
+ columnNamesRegex?: string;
+ columnNamesList?: string[];
+}
+
+export interface ApplicationDefinition {
+ appid: string;
+ applicationName: string;
+ applicationIcon?: string;
+ applicationColor?: string;
+ usageRules?: ApplicationUsageRule[];
+ files?: {
+ [key: string]: {
+ label: string;
+ sql: string;
+ type: 'query' | 'command';
+ };
+ };
+ virtualReferences?: VirtualReferenceDefinition[];
+ dictionaryDescriptions?: DictionaryDescriptionDefinition[];
}
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte
index 081f2a90f..4c08d2554 100644
--- a/packages/web/src/App.svelte
+++ b/packages/web/src/App.svelte
@@ -20,7 +20,7 @@
installNewVolatileConnectionListener,
refreshPublicCloudFiles,
} from './utility/api';
- import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
+ import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
import AppTitleProvider from './utility/AppTitleProvider.svelte';
import getElectron from './utility/getElectron';
import AppStartInfo from './widgets/AppStartInfo.svelte';
@@ -49,7 +49,7 @@
const connections = await apiCall('connections/list');
const settings = await getSettings();
- const apps = await getUsedApps();
+ const apps = await getAllApps();
const loadedApiValue = !!(settings && connections && config && apps);
if (loadedApiValue) {
diff --git a/packages/web/src/admin/FolderPermissionChooser.svelte b/packages/web/src/admin/FolderPermissionChooser.svelte
new file mode 100644
index 000000000..860896fa3
--- /dev/null
+++ b/packages/web/src/admin/FolderPermissionChooser.svelte
@@ -0,0 +1,35 @@
+
+
+