mirror of https://github.com/nocodb/nocodb
Pranav C
5 months ago
committed by
GitHub
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