diff --git a/packages/nc-gui/components/ProjectTreeView.vue b/packages/nc-gui/components/ProjectTreeView.vue index ae74e189bd..d07ed1dfe3 100644 --- a/packages/nc-gui/components/ProjectTreeView.vue +++ b/packages/nc-gui/components/ProjectTreeView.vue @@ -280,7 +280,9 @@ + + + + + Disable shared base + + + Readonly link + + + + + +
+ {{ base.url }} + mdi-reload + mdi-content-copy + mdi-xml +
+
+ + + + + + diff --git a/packages/nc-gui/layouts/default.vue b/packages/nc-gui/layouts/default.vue index aa2facac8f..e859cf6ccf 100644 --- a/packages/nc-gui/layouts/default.vue +++ b/packages/nc-gui/layouts/default.vue @@ -44,7 +44,7 @@ {{ ghStarText }} Docs - + diff --git a/packages/nc-gui/plugins/axiosInterceptor.js b/packages/nc-gui/plugins/axiosInterceptor.js index 0ffc8fd5aa..d6626f70e0 100644 --- a/packages/nc-gui/plugins/axiosInterceptor.js +++ b/packages/nc-gui/plugins/axiosInterceptor.js @@ -6,7 +6,7 @@ // }); // } -export default ({ store, $axios, redirect, $toast }) => { +export default ({ store, $axios, redirect, $toast, route, app }) => { // Add a request interceptor $axios.interceptors.request.use(function(config) { config.headers['xc-gui'] = 'true' @@ -17,6 +17,12 @@ export default ({ store, $axios, redirect, $toast }) => { config.headers['xc-preview'] = store.state.users.previewAs } + if (!config.url.endsWith('/user/me') && !config.url.endsWith('/admin/roles')) { + if (app.context && app.context.route && app.context.route.params && app.context.route.params.shared_base_id) { + config.headers['xc-shared-base-id'] = app.context.route.params.shared_base_id + } + } + return config }) diff --git a/packages/nc-gui/store/project.js b/packages/nc-gui/store/project.js index 67c33f651e..9cba78259a 100644 --- a/packages/nc-gui/store/project.js +++ b/packages/nc-gui/store/project.js @@ -20,13 +20,17 @@ export const state = () => ({ defaultProject, projectInfo: null, activeEnv: null, - authDbAlias: null + authDbAlias: null, + projectId: null }); export const mutations = { add(state, project) { state.list.push(project); }, + MutProjectId(state, projectId) { + state.projectId = projectId; + }, update(state, data) { }, remove(state, {project}) { @@ -78,27 +82,27 @@ export const getters = { }, GtrFirstDbAlias(state, getters) { return (state.unserializedList - && state.unserializedList[0] - && state.unserializedList[0].projectJson - && state.unserializedList[0].projectJson.envs - && getters.GtrEnv - && state.unserializedList[0].projectJson.envs[getters.GtrEnv] - && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db - && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db[0] - && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db[0].meta - && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db[0].meta.dbAlias) + && state.unserializedList[0] + && state.unserializedList[0].projectJson + && state.unserializedList[0].projectJson.envs + && getters.GtrEnv + && state.unserializedList[0].projectJson.envs[getters.GtrEnv] + && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db + && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db[0] + && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db[0].meta + && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db[0].meta.dbAlias) || 'db'; }, GtrDbAliasList(state, getters) { return (state.unserializedList - && state.unserializedList[0] - && state.unserializedList[0].projectJson - && state.unserializedList[0].projectJson.envs - && getters.GtrEnv - && state.unserializedList[0].projectJson.envs[getters.GtrEnv] - && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db) + && state.unserializedList[0] + && state.unserializedList[0].projectJson + && state.unserializedList[0].projectJson.envs + && getters.GtrEnv + && state.unserializedList[0].projectJson.envs[getters.GtrEnv] + && state.unserializedList[0].projectJson.envs[getters.GtrEnv].db) // && state.unserializedList[0].projectJson.envs[gettersGtrEnv].db.map(db => db.meta.dbAlias)) || []; }, @@ -261,16 +265,27 @@ export const actions = { }, 5000) }); try { + let data,projectId; if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.project_id) { + commit('MutProjectId', projectId = this.$router.currentRoute.params.project_id) await dispatch('users/ActGetProjectUserDetails', this.$router.currentRoute.params.project_id, {root: true}); + data = await this.dispatch('sqlMgr/ActSqlOp', [null, 'PROJECT_READ_BY_WEB']); // unsearialized data + } else if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.shared_base_id) { + const baseData = await this.dispatch('sqlMgr/ActSqlOp', [null, 'sharedBaseGet', {shared_base_id: this.$router.currentRoute.params.shared_base_id}]); // unsearialized data + commit('MutProjectId', projectId = baseData.project_id) + data = await this.dispatch('sqlMgr/ActSqlOp', [{project_id: baseData.project_id}, 'PROJECT_READ_BY_WEB']); // unsearialized data + await dispatch('users/ActGetBaseUserDetails', this.$router.currentRoute.params.shared_base_id, {root: true}); + } else { + commit('MutProjectId', null) + return } - const data = await this.dispatch('sqlMgr/ActSqlOp', [null, 'PROJECT_READ_BY_WEB']); // unsearialized data + commit("list", data.data.list); commit("meta/MutClear", null, {root: true}); - if(this.$ncApis){ - this.$ncApis.clear(); - this.$ncApis.setProjectId(this.$router.currentRoute.params.project_id); - } + if (this.$ncApis) { + this.$ncApis.clear(); + this.$ncApis.setProjectId(projectId); + } } catch (e) { this.$toast.error(e).goAway(3000); this.$router.push('/projects') diff --git a/packages/nc-gui/store/sqlMgr.js b/packages/nc-gui/store/sqlMgr.js index b12021c0d3..66b7a7fba4 100644 --- a/packages/nc-gui/store/sqlMgr.js +++ b/packages/nc-gui/store/sqlMgr.js @@ -361,9 +361,14 @@ export const actions = { dispatch }, [args, op, opArgs, cusHeaders, cusAxiosOptions, queryParams, returnResponse]) { const params = {} - if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.project_id) { - params.project_id = this.$router.currentRoute.params.project_id + if (this.$router.currentRoute && this.$router.currentRoute.params) { + if (this.$router.currentRoute.params.project_id) { + params.project_id = this.$router.currentRoute.params.project_id + } else if (this.$router.currentRoute.params.shared_base_id) { + params.project_id = rootState.project.projectId + } } + try { const headers = {} if (rootState.project.projectInfo && rootState.project.projectInfo.authType === 'masterKey') { @@ -443,6 +448,11 @@ export const actions = { if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.project_id) { params.project_id = this.$router.currentRoute.params.project_id } + + if (this.$router.currentRoute && this.$router.currentRoute.params && this.$router.currentRoute.params.project_id) { + params.project_id = this.$router.currentRoute.params.project_id + } + const headers = { 'Content-Type': 'multipart/form-data' } diff --git a/packages/nc-gui/store/users.js b/packages/nc-gui/store/users.js index 1b2433983e..92894fcac9 100644 --- a/packages/nc-gui/store/users.js +++ b/packages/nc-gui/store/users.js @@ -49,9 +49,6 @@ export const getters = { return state.paidUser }, - - - GtrIsAuthenticated(state, getters, rootState) { return rootState.project.projectInfo && (rootState.project.projectInfo.authType === 'none' || @@ -80,7 +77,7 @@ export const getters = { [state.previewAs]: true } } - return user && user.roles && Object.entries(roles).some(([name, hasRole]) => { + return Object.entries(roles).some(([name, hasRole]) => { return hasRole && rolePermissions[name] && (rolePermissions[name] === '*' || rolePermissions[name][page]) }) } @@ -94,11 +91,8 @@ export const getters = { }, GtrUserEmail(state) { - if(state.user && state.user.email) - return state.user.email; - else - return ''; - }, + if (state.user && state.user.email) { return state.user.email } else { return '' } + } } @@ -387,6 +381,22 @@ export const actions = { console.log('ignoring user/me error') } }, + async ActGetBaseUserDetails({ commit, state }, sharedBaseId) { + try { + try { + const user = await this.$axios.get('/user/me', { + headers: { + 'xc-shared-base-id': sharedBaseId + } + }) + commit('MutProjectRole', user && user.data && user.data.roles) + } catch (e) { + console.log('ignoring user/me error') + } + } catch (e) { + console.log('ignoring user/me error') + } + }, async ActGetUserUiAbility({ commit, state }) { try { diff --git a/packages/nocodb/package-lock.json b/packages/nocodb/package-lock.json index 2a87c96390..b08af4fcfe 100644 --- a/packages/nocodb/package-lock.json +++ b/packages/nocodb/package-lock.json @@ -1,6 +1,6 @@ { "name": "nocodb", - "version": "0.81.0", + "version": "0.81.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -13929,6 +13929,14 @@ "passport-oauth": "1.0.x" } }, + "passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "requires": { + "passport-strategy": "1.x.x" + } + }, "passport-github": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz", diff --git a/packages/nocodb/package.json b/packages/nocodb/package.json index 9e65093308..17d8e33054 100644 --- a/packages/nocodb/package.json +++ b/packages/nocodb/package.json @@ -158,6 +158,7 @@ "passport": "^0.4.1", "passport-auth-token": "^1.0.1", "passport-azure-ad-oauth2": "0.0.4", + "passport-custom": "^1.1.1", "passport-github": "^1.1.0", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.0", diff --git a/packages/nocodb/src/lib/noco/common/XcMigrationSource.ts b/packages/nocodb/src/lib/noco/common/XcMigrationSource.ts index cd2ca26aa2..8ecb978ca2 100644 --- a/packages/nocodb/src/lib/noco/common/XcMigrationSource.ts +++ b/packages/nocodb/src/lib/noco/common/XcMigrationSource.ts @@ -5,6 +5,7 @@ import * as viewType from '../migrations/nc_004_add_view_type_column'; import * as viewName from '../migrations/nc_005_add_view_name_column'; import * as nc_006_alter_nc_shared_views from '../migrations/nc_006_alter_nc_shared_views'; import * as nc_007_alter_nc_shared_views_1 from '../migrations/nc_007_alter_nc_shared_views_1'; +import * as nc_008_add_nc_shared_bases from '../migrations/nc_008_add_nc_shared_bases'; // Create a custom migration source class export default class XcMigrationSource { @@ -14,13 +15,14 @@ export default class XcMigrationSource { public getMigrations(): Promise { // In this example we are just returning migration names return Promise.resolve([ - 'project', - 'm2m', - 'fkn', - 'viewType', - 'viewName', - 'nc_006_alter_nc_shared_views', - 'nc_007_alter_nc_shared_views_1' + 'project', + 'm2m', + 'fkn', + 'viewType', + 'viewName', + 'nc_006_alter_nc_shared_views', + 'nc_007_alter_nc_shared_views_1', + 'nc_008_add_nc_shared_bases' ]); } @@ -44,7 +46,8 @@ export default class XcMigrationSource { return nc_006_alter_nc_shared_views; case 'nc_007_alter_nc_shared_views_1': return nc_007_alter_nc_shared_views_1; - + case 'nc_008_add_nc_shared_bases': + return nc_008_add_nc_shared_bases; } } } diff --git a/packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts b/packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts index 18710d5b88..69d2b2a0df 100644 --- a/packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts +++ b/packages/nocodb/src/lib/noco/meta/NcMetaIOImpl.ts @@ -27,12 +27,11 @@ export default class NcMetaIOImpl extends NcMetaIO { ): Promise<{ list: any[]; count: number }> { const query = this.knexConnection(target); const countQuery = this.knexConnection(target); - - if (projectId !== null) { + if (projectId !== null && projectId !== undefined) { query.where('project_id', projectId); countQuery.where('project_id', projectId); } - if (dbAlias !== null) { + if (dbAlias !== null && dbAlias !== undefined) { query.where('db_alias', dbAlias); countQuery.where('db_alias', dbAlias); } @@ -116,10 +115,10 @@ export default class NcMetaIOImpl extends NcMetaIO { ): Promise { const query = this.knexConnection(target); - if (project_id !== null) { + if (project_id !== null && project_id !== undefined) { query.where('project_id', project_id); } - if (dbAlias !== null) { + if (dbAlias !== null && dbAlias !== undefined) { query.where('db_alias', dbAlias); } @@ -154,16 +153,17 @@ export default class NcMetaIOImpl extends NcMetaIO { query.select(...fields); } - if (project_id !== null) { + if (project_id !== null && project_id !== undefined) { query.where('project_id', project_id); } - if (dbAlias !== null) { + if (dbAlias !== null && dbAlias !== undefined) { query.where('db_alias', dbAlias); } if (!idOrCondition) { return query.first(); } + if (typeof idOrCondition !== 'object') { query.where('id', idOrCondition); } else { @@ -204,10 +204,10 @@ export default class NcMetaIOImpl extends NcMetaIO { ): Promise { const query = this.knexConnection(target); - if (project_id !== null) { + if (project_id !== null && project_id !== undefined) { query.where('project_id', project_id); } - if (dbAlias !== null) { + if (dbAlias !== null && dbAlias !== undefined) { query.where('db_alias', dbAlias); } @@ -240,10 +240,10 @@ export default class NcMetaIOImpl extends NcMetaIO { xcCondition? ): Promise { const query = this.knexConnection(target); - if (project_id !== null) { + if (project_id !== null && project_id !== undefined) { query.where('project_id', project_id); } - if (dbAlias !== null) { + if (dbAlias !== null && dbAlias !== undefined) { query.where('db_alias', dbAlias); } @@ -280,10 +280,10 @@ export default class NcMetaIOImpl extends NcMetaIO { dbAlias: string ): Promise { const query = this.knexConnection('nc_models'); - if (project_id !== null) { + if (project_id !== null && project_id !== undefined) { query.where('project_id', project_id); } - if (dbAlias !== null) { + if (dbAlias !== null && dbAlias !== undefined) { query.where('db_alias', dbAlias); } const data = await query.first(); diff --git a/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts b/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts index bc08bff1a5..356bda1f9a 100644 --- a/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts +++ b/packages/nocodb/src/lib/noco/meta/NcMetaMgr.ts @@ -152,6 +152,7 @@ export default class NcMetaMgr { if (req?.session?.passport?.user?.isAuthorized) { if ( req?.body?.project_id && + !req.session?.passport?.user?.isPublicBase && !(await this.xcMeta.isUserHaveAccessToProject( req?.body?.project_id, req?.session?.passport?.user?.id @@ -1337,6 +1338,9 @@ export default class NcMetaMgr { case 'sharedViewGet': result = await this.sharedViewGet(req, args); break; + case 'sharedBaseGet': + result = await this.sharedBaseGet(req, args); + break; case 'sharedViewExportAsCsv': result = await this.sharedViewExportAsCsv(req, args, res); break; @@ -1498,6 +1502,15 @@ export default class NcMetaMgr { case 'createSharedViewLink': result = await this.createSharedViewLink(req, args); break; + case 'createSharedBaseLink': + result = await this.createSharedBaseLink(req, args); + break; + case 'disableSharedBaseLink': + result = await this.disableSharedBaseLink(req, args); + break; + case 'getSharedBaseLink': + result = await this.getSharedBaseLink(req, args); + break; case 'updateSharedViewLinkPassword': result = await this.updateSharedViewLinkPassword(args); @@ -3385,6 +3398,83 @@ export default class NcMetaMgr { } } + protected async createSharedBaseLink(req, args: any): Promise { + try { + let sharedBase = await this.xcMeta.metaGet( + this.getProjectId(args), + this.getDbAlias(args), + 'nc_shared_bases', + { + project_id: this.getProjectId(args) + } + ); + + if (!sharedBase) { + const insertData = { + project_id: args.project_id, + db_alias: this.getDbAlias(args), + shared_base_id: uuidv4(), + password: args?.args?.password + }; + + await this.xcMeta.metaInsert( + args.project_id, + this.getDbAlias(args), + 'nc_shared_bases', + insertData + ); + sharedBase = await this.xcMeta.metaGet( + this.getProjectId(args), + this.getDbAlias(args), + 'nc_shared_bases', + {}, + ['id', 'shared_base_id', 'enabled'] + ); + } + + sharedBase.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/base/${sharedBase.shared_base_id}`; + + Tele.emit('evt', { evt_type: 'sharedBase:generated-link' }); + return sharedBase; + } catch (e) { + console.log(e); + } + } + + protected async disableSharedBaseLink(_req, args: any): Promise { + try { + await this.xcMeta.metaDelete( + this.getProjectId(args), + this.getDbAlias(args), + 'nc_shared_bases', + { + project_id: this.getProjectId(args) + } + ); + } catch (e) { + console.log(e); + } + } + + protected async getSharedBaseLink(req, args: any): Promise { + try { + const sharedBase = await this.xcMeta.metaGet( + this.getProjectId(args), + this.getDbAlias(args), + 'nc_shared_bases', + { + project_id: this.getProjectId(args) + } + ); + if (sharedBase) + sharedBase.url = `${req.ncSiteUrl}${this.config.dashboardPath}#/nc/base/${sharedBase.shared_base_id}`; + + return sharedBase; + } catch (e) { + console.log(e); + } + } + protected async updateSharedViewLinkPassword(_args: any): Promise { // try { // @@ -3898,6 +3988,23 @@ export default class NcMetaMgr { return { ...sharedViewMeta, ...viewMeta }; } + protected async sharedBaseGet(_req, args: any): Promise { + const sharedBaseMeta = await this.xcMeta + .knex('nc_shared_bases') + .select('project_id') + .where({ + shared_base_id: args.args.shared_base_id, + enabled: true + }) + .first(); + + if (!sharedBaseMeta) { + throw new Error('Meta not found'); + } + + return sharedBaseMeta; + } + protected async sharedViewExportAsCsv(_req, args: any, res): Promise { const sharedViewMeta = await this.xcMeta .knex('nc_shared_views') diff --git a/packages/nocodb/src/lib/noco/migrations/nc_008_add_nc_shared_bases.ts b/packages/nocodb/src/lib/noco/migrations/nc_008_add_nc_shared_bases.ts new file mode 100644 index 0000000000..b9dd907dd7 --- /dev/null +++ b/packages/nocodb/src/lib/noco/migrations/nc_008_add_nc_shared_bases.ts @@ -0,0 +1,43 @@ +import Knex from 'knex'; + +const up = async (knex: Knex) => { + await knex.schema.createTable('nc_shared_bases', table => { + table.increments(); + table.string('project_id'); + table.string('db_alias'); + table.string('roles').defaultTo('viewer'); + table.string('shared_base_id'); + table.boolean('enabled').defaultTo(true); + table.string('password'); + table.timestamps(true, true); + }); +}; + +const down = async knex => { + await knex.schema.dropTable('nc_shared_bases'); +}; + +export { up, down }; + +/** + * @copyright Copyright (c) 2021, Xgene Cloud Ltd + * + * @author Naveen MR + * @author Pranav C Balan + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ diff --git a/packages/nocodb/src/lib/noco/rest/RestAuthCtrl.ts b/packages/nocodb/src/lib/noco/rest/RestAuthCtrl.ts index bedd293099..f6f12e41ef 100644 --- a/packages/nocodb/src/lib/noco/rest/RestAuthCtrl.ts +++ b/packages/nocodb/src/lib/noco/rest/RestAuthCtrl.ts @@ -17,6 +17,8 @@ import Noco from '../Noco'; const autoBind = require('auto-bind'); const PassportLocalStrategy = require('passport-local').Strategy; +import { Strategy as CustomStrategy } from 'passport-custom'; + const { v4: uuidv4 } = require('uuid'); import * as crypto from 'crypto'; @@ -39,12 +41,14 @@ passport.serializeUser(function( provider, firstname, lastname, - isAuthorized + isAuthorized, + isPublicBase }, done ) { done(null, { isAuthorized, + isPublicBase, id, email, email_verified, @@ -249,6 +253,18 @@ export default class RestAuthCtrl { } } )(req, res, next); + } else if (req.headers['xc-shared-base-id']) { + passport.authenticate('baseView', {}, (_err, user, _info) => { + if (user) { + return resolve({ + ...user, + isAuthorized: true, + isPublicBase: true + }); + } else { + resolve({ roles: 'guest' }); + } + })(req, res, next); } else { resolve({ roles: 'guest' }); } @@ -290,6 +306,7 @@ export default class RestAuthCtrl { } }) ); + this.initCustomStrategy(); this.initJwtStrategy(); passport.use( @@ -569,6 +586,30 @@ export default class RestAuthCtrl { ); } + protected initCustomStrategy() { + passport.use( + 'baseView', + new CustomStrategy(async (req: any, callback) => { + let user; + if (req.headers['xc-shared-base-id']) { + const sharedBase = await this.xcMeta + .knex('nc_shared_bases') + .where({ + enabled: true, + shared_base_id: req.headers['xc-shared-base-id'] + }) + .first(); + + user = { + roles: sharedBase?.roles + }; + } + + callback(null, user); + }) + ); + } + protected async signin(req, res, next): Promise { passport.authenticate( 'local',