Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/components/engine-canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
buildSelectedDataFromCell,
CellInputBuilder,
Payload,
isErrorMessage,
} from 'logisheets-engine'
import {callerRegistry} from '@/core/permissions/caller-registry'
import {tx} from '@/core/transaction'
import {
FormulaEditorWrapper,
Expand Down Expand Up @@ -397,11 +399,27 @@ export const EngineCanvas: FC<EngineCanvasProps> = ({

// Handle startEdit events from engine
useEffect(() => {
const handleStartEdit = (data: {
const handleStartEdit = async (data: {
row: number
col: number
initialText: string
}) => {
const sheetIdx = dataSvc.getCurrentSheetIdx()
const cellId = await dataSvc.getWorkbook().getCellId({
sheetIdx,
rowIdx: data.row,
colIdx: data.col,
})
if (!isErrorMessage(cellId) && cellId.cellId.type === 'blockCell') {
const blockId = cellId.cellId.value.blockId
const owner = callerRegistry.getBlockOwner(sheetIdx, blockId)
if (
owner !== undefined &&
owner !== callerRegistry.getUserUuid()
) {
return
}
}
startEditing(data.row, data.col, data.initialText)
}

Expand All @@ -410,7 +428,7 @@ export const EngineCanvas: FC<EngineCanvasProps> = ({
return () => {
engine.off('startEdit', handleStartEdit)
}
}, [engine, startEditing])
}, [engine, startEditing, dataSvc])

// Sync React activeSheet to engine (when changed externally)
useEffect(() => {
Expand Down
13 changes: 13 additions & 0 deletions src/core/permissions/caller-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const USER_KEY = '__user__'

class CallerRegistry {
private _entries = new Map<string, string>()
private _blockOwners = new Map<string, string>()

getUserUuid(): string {
return this._getOrAssign(USER_KEY)
Expand All @@ -20,6 +21,18 @@ class CallerRegistry {
return uuid === this._entries.get(USER_KEY)
}

registerBlockOwner(
sheetIdx: number,
blockId: number,
callerUuid: string
): void {
this._blockOwners.set(`${sheetIdx}-${blockId}`, callerUuid)
}

getBlockOwner(sheetIdx: number, blockId: number): string | undefined {
return this._blockOwners.get(`${sheetIdx}-${blockId}`)
}

private _getOrAssign(key: string): string {
const existing = this._entries.get(key)
if (existing) return existing
Expand Down
151 changes: 140 additions & 11 deletions src/core/permissions/patch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import {WorkbookClient, Transaction} from 'logisheets-engine'
import {
WorkbookClient,
Transaction,
Payload,
isErrorMessage,
} from 'logisheets-engine'
import {callerRegistry} from './caller-registry'

const CALLER_UUID_KEY = '__callerUuid'
const PATCHED_FLAG = '__logisheetsPermPatched'

declare module 'logisheets-engine' {
interface WorkbookClient {
validate(transaction: unknown, callerUuid: string): boolean
validate(transaction: unknown, callerUuid: string): Promise<boolean>
}
}

Expand All @@ -19,18 +24,118 @@ function permissionDeniedError() {
return {msg: 'permission denied by modify policy', ty: 403}
}

function isBlockPayload(payload: Payload): boolean {
const blockTypes = [
'blockInput',
'moveBlock',
'removeBlock',
'resizeBlock',
'convertBlock',
'bindFormSchema',
'bindRandomSchema',
'upsertFieldRenderInfo',
'blockStyleUpdate',
'blockLineStyleUpdate',
'blockLineNameFieldUpdate',
'insertColsInBlock',
'deleteColsInBlock',
'insertRowsInBlock',
'deleteRowsInBlock',
]
return blockTypes.includes(payload.type)
}

function getBlockRefFromPayload(
payload: Payload
): {sheetIdx: number; blockId: number} | undefined {
const v = payload.value as unknown as Record<string, unknown>
if (
payload.type === 'moveBlock' ||
payload.type === 'removeBlock' ||
payload.type === 'resizeBlock' ||
payload.type === 'convertBlock'
) {
return {
sheetIdx: v.sheetIdx as number,
blockId: v.id as number,
}
}
if ('blockId' in v && 'sheetIdx' in v) {
return {
sheetIdx: v.sheetIdx as number,
blockId: v.blockId as number,
}
}
return undefined
}

async function validateCellInput(
client: WorkbookClient,
payload: Payload,
callerUuid: string
): Promise<boolean> {
const v = payload.value as {
sheetIdx: number
row: number
col: number
}
const sheetCellId = await client.getCellId({
sheetIdx: v.sheetIdx,
rowIdx: v.row,
colIdx: v.col,
})
if (isErrorMessage(sheetCellId)) {
return true
}
if (sheetCellId.cellId.type !== 'blockCell') {
return true
}
const blockId = sheetCellId.cellId.value.blockId
const owner = callerRegistry.getBlockOwner(v.sheetIdx, blockId)
if (owner !== undefined && owner !== callerUuid) {
return false
}
return true
}

function applyPatch() {
const proto = WorkbookClient.prototype as unknown as Record<string, unknown>
if (proto[PATCHED_FLAG]) return
proto[PATCHED_FLAG] = true

if (typeof proto.validate !== 'function') {
proto.validate = function (
_transaction: unknown,
_callerUuid: string
): boolean {
// todo: validate the function call that attempts
// to modify blocks.
proto.validate = async function (
this: WorkbookClient,
transaction: unknown,
callerUuid: string
): Promise<boolean> {
const tx = transaction as Transaction
for (const payload of tx.payloads) {
if (payload.type === 'cellInput') {
const ok = await validateCellInput(
this,
payload,
callerUuid
)
if (!ok) return false
continue
}
if (
!isBlockPayload(payload) ||
payload.type === 'createBlock'
) {
continue
}
const ref = getBlockRefFromPayload(payload)
if (!ref) continue
const owner = callerRegistry.getBlockOwner(
ref.sheetIdx,
ref.blockId
)
if (owner !== undefined && owner !== callerUuid) {
return false
}
}
return true
}
}
Expand All @@ -44,7 +149,7 @@ function applyPatch() {
| undefined
if (typeof original !== 'function') continue

proto[name] = function (
proto[name] = async function (
this: WorkbookClient,
params: ParamsWithCaller
) {
Expand All @@ -53,14 +158,38 @@ function applyPatch() {

const validate = (
this as unknown as {
validate: (tx: Transaction, uuid: string) => boolean
validate: (
tx: Transaction,
uuid: string
) => Promise<boolean>
}
).validate
const ok = validate.call(this, params?.transaction, callerUuid)
const ok = await validate.call(
this,
params?.transaction,
callerUuid
)
if (!ok) {
return Promise.resolve(permissionDeniedError())
}

const tx = params?.transaction as Transaction | undefined
if (tx) {
for (const payload of tx.payloads) {
if (payload.type === 'createBlock') {
const v = payload.value as {
sheetIdx: number
id: number
}
callerRegistry.registerBlockOwner(
v.sheetIdx,
v.id,
callerUuid
)
}
}
}

const forwarded: {transaction: Transaction} = {
transaction: params.transaction,
}
Expand Down
Loading