mirror of https://github.com/nocodb/nocodb
56 changed files with 1175 additions and 149 deletions
@ -0,0 +1,18 @@ |
|||||||
|
import type { Knex } from 'knex'; |
||||||
|
import { MetaTable } from '~/utils/globals'; |
||||||
|
|
||||||
|
const up = async (knex: Knex) => { |
||||||
|
await knex.schema.alterTable(MetaTable.BASES, (table) => { |
||||||
|
table.boolean('is_schema_readonly').defaultTo(false); |
||||||
|
table.boolean('is_data_readonly').defaultTo(false); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const down = async (knex: Knex) => { |
||||||
|
await knex.schema.alterTable(MetaTable.BASES, (table) => { |
||||||
|
table.dropColumn('is_schema_readonly'); |
||||||
|
table.dropColumn('is_data_readonly'); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
export { up, down }; |
@ -0,0 +1,152 @@ |
|||||||
|
import 'mocha'; |
||||||
|
import request from 'supertest'; |
||||||
|
import { beforeEach } from 'mocha'; |
||||||
|
import { Exception } from 'handlebars'; |
||||||
|
import { expect } from 'chai'; |
||||||
|
import { Base } from '../../../../src/models'; |
||||||
|
import { createTable, getTable } from '../../factory/table'; |
||||||
|
import init from '../../init'; |
||||||
|
import { |
||||||
|
createProject, |
||||||
|
createSakilaProject, |
||||||
|
createSharedBase, |
||||||
|
} from '../../factory/base'; |
||||||
|
import { RootScopes } from '../../../../src/utils/globals'; |
||||||
|
import { generateDefaultRowAttributes } from '../../factory/row'; |
||||||
|
import { defaultColumns } from '../../factory/column'; |
||||||
|
|
||||||
|
// Test case list
|
||||||
|
// 1. Create data readonly source
|
||||||
|
// 2. Create schema readonly source
|
||||||
|
|
||||||
|
function sourceTest() { |
||||||
|
let context; |
||||||
|
|
||||||
|
beforeEach(async function () { |
||||||
|
console.time('#### readonlySourceTest'); |
||||||
|
context = await init(); |
||||||
|
console.timeEnd('#### readonlySourceTest'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Readonly data', async () => { |
||||||
|
const base = await createSakilaProject(context, { |
||||||
|
is_schema_readonly: false, |
||||||
|
is_data_readonly: true, |
||||||
|
}); |
||||||
|
|
||||||
|
const countryTable = await getTable({ |
||||||
|
base, |
||||||
|
name: 'country', |
||||||
|
}); |
||||||
|
|
||||||
|
const sakilaCtx = { |
||||||
|
workspace_id: base.fk_workspace_id, |
||||||
|
base_id: base.id, |
||||||
|
}; |
||||||
|
|
||||||
|
const countryColumns = (await countryTable.getColumns(sakilaCtx)).filter(c => !c.pk); |
||||||
|
const rowAttributes = Array(99) |
||||||
|
.fill(0) |
||||||
|
.map((index) => |
||||||
|
generateDefaultRowAttributes({ columns: countryColumns, index }), |
||||||
|
); |
||||||
|
|
||||||
|
await request(context.app) |
||||||
|
.post(`/api/v1/db/data/bulk/noco/${base.id}/${countryTable.id}`) |
||||||
|
.set('xc-auth', context.token) |
||||||
|
.send(rowAttributes) |
||||||
|
.expect(403); |
||||||
|
|
||||||
|
await request(context.app) |
||||||
|
.post(`/api/v1/db/meta/projects/${base.id}/tables`) |
||||||
|
.set('xc-auth', context.token) |
||||||
|
.send({ |
||||||
|
table_name: 'new_title', |
||||||
|
title: 'new_title', |
||||||
|
columns: defaultColumns(context), |
||||||
|
}) |
||||||
|
.expect(200); |
||||||
|
}); |
||||||
|
|
||||||
|
it('Readonly schema', async () => { |
||||||
|
const base = await createSakilaProject(context, { |
||||||
|
is_schema_readonly: true, |
||||||
|
is_data_readonly: false, |
||||||
|
}); |
||||||
|
|
||||||
|
const countryTable = await getTable({ |
||||||
|
base, |
||||||
|
name: 'country', |
||||||
|
}); |
||||||
|
|
||||||
|
const sakilaCtx = { |
||||||
|
workspace_id: base.fk_workspace_id, |
||||||
|
base_id: base.id, |
||||||
|
}; |
||||||
|
|
||||||
|
const countryColumns = (await countryTable.getColumns(sakilaCtx)).filter(c => !c.pk); |
||||||
|
const rowAttributes = Array(99) |
||||||
|
.fill(0) |
||||||
|
.map((index) => |
||||||
|
generateDefaultRowAttributes({ columns: countryColumns, index }), |
||||||
|
); |
||||||
|
|
||||||
|
await request(context.app) |
||||||
|
.post(`/api/v1/db/data/bulk/noco/${base.id}/${countryTable.id}`) |
||||||
|
.set('xc-auth', context.token) |
||||||
|
.send(rowAttributes) |
||||||
|
.expect(200); |
||||||
|
await request(context.app) |
||||||
|
.post(`/api/v1/db/meta/projects/${base.id}/tables`) |
||||||
|
.set('xc-auth', context.token) |
||||||
|
.send({ |
||||||
|
table_name: 'new_title', |
||||||
|
title: 'new_title', |
||||||
|
columns: defaultColumns(context), |
||||||
|
}) |
||||||
|
.expect(403); |
||||||
|
}); |
||||||
|
it('Readonly schema & data', async () => { |
||||||
|
const base = await createSakilaProject(context, { |
||||||
|
is_schema_readonly: true, |
||||||
|
is_data_readonly: true, |
||||||
|
}); |
||||||
|
|
||||||
|
const countryTable = await getTable({ |
||||||
|
base, |
||||||
|
name: 'country', |
||||||
|
}); |
||||||
|
|
||||||
|
const sakilaCtx = { |
||||||
|
workspace_id: base.fk_workspace_id, |
||||||
|
base_id: base.id, |
||||||
|
}; |
||||||
|
|
||||||
|
const countryColumns = (await countryTable.getColumns(sakilaCtx)).filter(c => !c.pk); |
||||||
|
const rowAttributes = Array(99) |
||||||
|
.fill(0) |
||||||
|
.map((index) => |
||||||
|
generateDefaultRowAttributes({ columns: countryColumns, index }), |
||||||
|
); |
||||||
|
|
||||||
|
await request(context.app) |
||||||
|
.post(`/api/v1/db/data/bulk/noco/${base.id}/${countryTable.id}`) |
||||||
|
.set('xc-auth', context.token) |
||||||
|
.send(rowAttributes) |
||||||
|
.expect(403); |
||||||
|
|
||||||
|
await request(context.app) |
||||||
|
.post(`/api/v1/db/meta/projects/${base.id}/tables`) |
||||||
|
.set('xc-auth', context.token) |
||||||
|
.send({ |
||||||
|
table_name: 'new_title', |
||||||
|
title: 'new_title', |
||||||
|
columns: defaultColumns(context), |
||||||
|
}) |
||||||
|
.expect(403); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export default function () { |
||||||
|
describe('SourceRestriction', sourceTest); |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
import { expect } from '@playwright/test'; |
||||||
|
import { SettingsPage } from '.'; |
||||||
|
import BasePage from '../../Base'; |
||||||
|
|
||||||
|
export class SourcePage extends BasePage { |
||||||
|
private readonly settings: SettingsPage; |
||||||
|
|
||||||
|
constructor(settings: SettingsPage) { |
||||||
|
super(settings.rootPage); |
||||||
|
this.settings = settings; |
||||||
|
} |
||||||
|
|
||||||
|
get() { |
||||||
|
return this.rootPage.getByTestId('nc-settings-datasources'); |
||||||
|
} |
||||||
|
|
||||||
|
async openEditWindow({ sourceName }: { sourceName: string }) { |
||||||
|
await this.get().locator('.ds-table-row', { hasText: sourceName }).click(); |
||||||
|
await this.get().getByTestId('nc-connection-tab').click(); |
||||||
|
} |
||||||
|
|
||||||
|
async updateSchemaReadOnly({ sourceName, readOnly }: { sourceName: string; readOnly: boolean }) { |
||||||
|
await this.openEditWindow({ sourceName }); |
||||||
|
const switchBtn = this.get().getByTestId('nc-allow-meta-write'); |
||||||
|
if (switchBtn.getAttribute('checked') !== readOnly.toString()) { |
||||||
|
await switchBtn.click(); |
||||||
|
} |
||||||
|
await this.saveConnection(); |
||||||
|
} |
||||||
|
|
||||||
|
async updateDataReadOnly({ sourceName, readOnly = true }: { sourceName: string; readOnly?: boolean }) { |
||||||
|
await this.openEditWindow({ sourceName }); |
||||||
|
const switchBtn = this.get().getByTestId('nc-allow-data-write'); |
||||||
|
if (switchBtn.getAttribute('checked') !== readOnly.toString()) { |
||||||
|
await switchBtn.click(); |
||||||
|
} |
||||||
|
await this.saveConnection(); |
||||||
|
} |
||||||
|
|
||||||
|
async saveConnection() { |
||||||
|
await this.get().locator('.nc-extdb-btn-test-connection').click(); |
||||||
|
await this.get().locator('.nc-extdb-btn-submit:enabled').click(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
import { expect, test } from '@playwright/test'; |
||||||
|
import { DashboardPage } from '../../../pages/Dashboard'; |
||||||
|
import setup, { NcContext, unsetup } from '../../../setup'; |
||||||
|
import { Api } from 'nocodb-sdk'; |
||||||
|
import { SettingsPage } from '../../../pages/Dashboard/Settings'; |
||||||
|
|
||||||
|
test.describe('Source Restrictions', () => { |
||||||
|
let dashboard: DashboardPage; |
||||||
|
let settingsPage: SettingsPage; |
||||||
|
let context: NcContext; |
||||||
|
let api: Api<any>; |
||||||
|
test.setTimeout(150000); |
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => { |
||||||
|
page.setDefaultTimeout(70000); |
||||||
|
context = await setup({ page }); |
||||||
|
dashboard = new DashboardPage(page, context.base); |
||||||
|
settingsPage = new SettingsPage(dashboard); |
||||||
|
api = new Api({ |
||||||
|
baseURL: `http://localhost:8080/`, |
||||||
|
headers: { |
||||||
|
'xc-auth': context.token, |
||||||
|
}, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
test.afterEach(async () => { |
||||||
|
await unsetup(context); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Readonly data source', async () => { |
||||||
|
await dashboard.treeView.openProjectSourceSettings({ title: context.base.title, context }); |
||||||
|
|
||||||
|
await settingsPage.selectTab({ tab: 'dataSources' }); |
||||||
|
await dashboard.rootPage.waitForTimeout(300); |
||||||
|
|
||||||
|
await settingsPage.source.updateSchemaReadOnly({ sourceName: 'Default', readOnly: true }); |
||||||
|
await settingsPage.source.updateDataReadOnly({ sourceName: 'Default', readOnly: true }); |
||||||
|
await settingsPage.close(); |
||||||
|
|
||||||
|
// reload page to reflect source changes
|
||||||
|
await dashboard.rootPage.reload(); |
||||||
|
|
||||||
|
await dashboard.treeView.verifyTable({ title: 'Actor' }); |
||||||
|
|
||||||
|
// open table and verify that it is readonly
|
||||||
|
await dashboard.treeView.openTable({ title: 'Actor' }); |
||||||
|
await expect(dashboard.grid.get().locator('.nc-grid-add-new-cell')).toHaveCount(0); |
||||||
|
|
||||||
|
await dashboard.grid.get().getByTestId(`cell-FirstName-0`).click({ |
||||||
|
button: 'right', |
||||||
|
}); |
||||||
|
|
||||||
|
await expect(dashboard.rootPage.locator('.ant-dropdown-menu-item:has-text("Copy")')).toHaveCount(1); |
||||||
|
await expect(dashboard.rootPage.locator('.ant-dropdown-menu-item:has-text("Delete record")')).toHaveCount(0); |
||||||
|
}); |
||||||
|
|
||||||
|
test('Readonly schema source', async () => { |
||||||
|
await dashboard.treeView.openProjectSourceSettings({ title: context.base.title, context }); |
||||||
|
|
||||||
|
await settingsPage.selectTab({ tab: 'dataSources' }); |
||||||
|
await dashboard.rootPage.waitForTimeout(300); |
||||||
|
|
||||||
|
await settingsPage.source.updateSchemaReadOnly({ sourceName: 'Default', readOnly: true }); |
||||||
|
await settingsPage.close(); |
||||||
|
|
||||||
|
// reload page to reflect source changes
|
||||||
|
await dashboard.rootPage.reload(); |
||||||
|
|
||||||
|
await dashboard.treeView.verifyTable({ title: 'Actor' }); |
||||||
|
|
||||||
|
// open table and verify that it is readonly
|
||||||
|
await dashboard.treeView.openTable({ title: 'Actor' }); |
||||||
|
|
||||||
|
await dashboard.grid |
||||||
|
.get() |
||||||
|
.locator(`th[data-title="LastName"]`) |
||||||
|
.first() |
||||||
|
.locator('.nc-ui-dt-dropdown') |
||||||
|
.scrollIntoViewIfNeeded(); |
||||||
|
await dashboard.grid.get().locator(`th[data-title="LastName"]`).first().locator('.nc-ui-dt-dropdown').click(); |
||||||
|
for (const item of ['Edit', 'Delete', 'Duplicate']) { |
||||||
|
await expect( |
||||||
|
await dashboard.rootPage.locator(`li[role="menuitem"]:has-text("${item}"):visible`).last() |
||||||
|
).toBeVisible(); |
||||||
|
await expect( |
||||||
|
await dashboard.rootPage.locator(`li[role="menuitem"]:has-text("${item}"):visible`).last() |
||||||
|
).toHaveClass(/ant-dropdown-menu-item-disabled/); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue