Block stage-replace (RTAS) on locked tables - #676
Conversation
TablesServiceImpl#putTable() enforces isTableLocked() on the ordinary update branch, but the adjacent stage-replace (CREATE OR REPLACE TABLE AS SELECT) branch only checks checkReplaceTablePrivilege() and never checks the table's lock state. This lets the table's own creator silently replace a locked table's entire definition (schema, partitioning, properties, etc.), bypassing the lock. Add the same isTableLocked() check to the stage-replace branch, throwing UnsupportedClientOperationException(LOCKED_TABLE_OPERATION) consistent with the other lock-enforced code paths in this class. Also add a new e2e test, testStageReplaceBlockedOnLockedTable, since there was previously no test coverage at all for stage-replace interacting with table locks. Co-authored-by: Copilot <[email protected]>
|
Self-review notes while this is in draft:
|
The ordinary update branch of putTable() rejects any request that tries to change a table's lock state disguised as a metadata update (checkIfLockPoliciesUpdated). The stage-replace branch had no equivalent check, so a stage-replace request against an unlocked table could smuggle in a lockState change alongside the schema replacement without going through the dedicated createLock/deleteLock APIs. Add the same checkIfLockPoliciesUpdated(...) call to the stage-replace branch, and add a regression test, testStageReplaceCannotSmuggleLockStateChange, asserting this is now rejected with IllegalArgumentException. Co-authored-by: Copilot <[email protected]>
|
Update: addressed point 2 from my self-review above. Pushed a second commit that adds the missing Re-ran locally after this change:
PR is now 2 commits / 2 files changed / +69 lines. Still in draft, will keep iterating as CI and review come in. |
|
✅ CI passed ( |
Summary
TablesServiceImpl#putTable()has two branches for an existing table:CREATE OR REPLACE TABLE ... AS SELECT, i.e.createUpdateTableRequestBody.isStageReplace()), which only callsauthorizationUtils.checkReplaceTablePrivilege(...).authorizationUtils.checkTableWritePathPrivileges(...)andisTableLocked(tableDto.get()), throwingUnsupportedClientOperationException(LOCKED_TABLE_OPERATION)if the table is locked.The stage-replace branch never checks
isTableLocked. Locking a table is meant to prevent further mutation of its definition (used e.g. during migrations, freezes, or investigations), but aCREATE OR REPLACE TABLE AS SELECTfrom the table's own creator can silently replace the entire table definition (schema, partitioning, sort order, properties, location) while the table is locked — bypassing the lock entirely.Fix
Adds the same
isTableLocked()check already used elsewhere in this method to the stage-replace branch, before the existing privilege check, throwing the sameUnsupportedClientOperationException(LOCKED_TABLE_OPERATION)used by the other lock-enforced code paths in this class.Testing
Added
testStageReplaceBlockedOnLockedTabletoTablesServiceTest(e2e/h2 Spring-Boot-backed test), since there was no existing coverage of stage-replace at all in this test class:replace.enabled=true.putTable(...)call throwsUnsupportedClientOperationException.Ran locally:
./gradlew :services:tables:test --tests "com.linkedin.openhouse.tables.e2e.h2.TablesServiceTest"— all 36 tests pass (including the new one), no regressions../gradlew :services:tables:test(full module) — passes../gradlew :services:tables:spotlessJavaCheck— passes.Notes
This is a draft PR — happy to iterate on this based on review.