Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';

ALTER TABLE public.company ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.company_settings ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_account ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.refresh_token ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_agreement_consent ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.password_reset_token ENABLE ROW LEVEL SECURITY;

ALTER TABLE public.worker ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_document ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.stored_file ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.task ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.task_checklist_item ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.task_transition_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.document_request_draft ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.document_request_draft_type ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.approval_request ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.external_submission ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.task_evidence ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.audit_event ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.workflow_case ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.document_ocr_run ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.notification ENABLE ROW LEVEL SECURITY;

ALTER TABLE public.worker_link ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_response ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_response_upload ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_document_upload_idempotency ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_import_job ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_import_row ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.worker_import_commit_idempotency ENABLE ROW LEVEL SECURITY;

ALTER TABLE public.ai_run ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_attempt ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_question ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_candidate ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_candidate_decision_batch ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_candidate_decision ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_candidate_decision_task ENABLE ROW LEVEL SECURITY;

ALTER TABLE public.event_publication ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.event_consumption ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.outbox_manual_retry ENABLE ROW LEVEL SECURITY;
74 changes: 72 additions & 2 deletions src/test/java/com/fowoco/server/PostgreSqlMigrationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,47 @@
@EnabledIfEnvironmentVariable(named = "POSTGRES_TEST_ENABLED", matches = "true")
class PostgreSqlMigrationTests {

private static final Set<String> RLS_TABLES = Set.of(
"company",
"company_settings",
"user_account",
"refresh_token",
"user_agreement_consent",
"password_reset_token",
"worker",
"worker_document",
"stored_file",
"task",
"task_checklist_item",
"task_transition_history",
"document_request_draft",
"document_request_draft_type",
"approval_request",
"external_submission",
"task_evidence",
"audit_event",
"workflow_case",
"document_ocr_run",
"notification",
"worker_link",
"worker_response",
"worker_response_upload",
"worker_document_upload_idempotency",
"worker_import_job",
"worker_import_row",
"worker_import_commit_idempotency",
"ai_run",
"ai_attempt",
"ai_question",
"ai_candidate",
"ai_candidate_decision_batch",
"ai_candidate_decision",
"ai_candidate_decision_task",
"event_publication",
"event_consumption",
"outbox_manual_retry"
);

private static final String COMPANY_A = "10000000-0000-0000-0000-000000000001";
private static final String COMPANY_B = "20000000-0000-0000-0000-000000000002";
private static final String USER_A = "11000000-0000-0000-0000-000000000001";
Expand Down Expand Up @@ -109,7 +150,8 @@ private void assertSchemaContract(Connection connection) throws SQLException {
"worker_import_job",
"worker_import_row",
"worker_import_commit_idempotency",
"document_ocr_run"
"document_ocr_run",
"notification"
);

assertThat(columnSpecs(connection, "company"))
Expand Down Expand Up @@ -528,7 +570,11 @@ private void assertSchemaContract(Connection connection) throws SQLException {
"pl_document_ocr_run_tenant_isolation",
"pl_notification_tenant_isolation"
);
assertThat(rlsEnabledTables(connection)).isEmpty();
assertThat(policyTableNames(connection))
.containsExactlyInAnyOrderElementsOf(RLS_TABLES);
assertThat(rlsEnabledTables(connection))
.containsExactlyInAnyOrderElementsOf(RLS_TABLES);
assertThat(rlsForcedTables(connection)).isEmpty();
assertThat(securityDefinerFunctionNames(connection))
.containsExactlyInAnyOrder(
"bootstrap_company_id_by_normalized_email",
Expand Down Expand Up @@ -1061,6 +1107,17 @@ private Set<String> policyNames(Connection connection) throws SQLException {
);
}

private Set<String> policyTableNames(Connection connection) throws SQLException {
return queryStrings(
connection,
"""
SELECT DISTINCT tablename
FROM pg_catalog.pg_policies
WHERE schemaname = 'public'
"""
);
}

private Set<String> rlsEnabledTables(Connection connection) throws SQLException {
return queryStrings(
connection,
Expand All @@ -1074,6 +1131,19 @@ private Set<String> rlsEnabledTables(Connection connection) throws SQLException
);
}

private Set<String> rlsForcedTables(Connection connection) throws SQLException {
return queryStrings(
connection,
"""
SELECT relname
FROM pg_catalog.pg_class
WHERE relnamespace = 'public'::regnamespace
AND relkind = 'r'
AND relforcerowsecurity
"""
);
}

private Set<String> securityDefinerFunctionNames(Connection connection) throws SQLException {
return queryStrings(
connection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ void resetFixture() {
void statementTimeoutRollsBackTransactionAndPoolServesNextQuery() {
Throwable failure = catchThrowable(() -> transactionTemplate.executeWithoutResult(
status -> {
runtimeJdbc.update(
tenantDatabaseContext.setCompanyIdForCurrentTransaction(
FIXTURE_COMPANY_ID
);
assertThat(runtimeJdbc.update(
"UPDATE company SET name = ? WHERE company_id = ?",
"must roll back",
FIXTURE_COMPANY_ID
);
)).isEqualTo(1);
runtimeJdbc.execute("SELECT pg_catalog.pg_sleep(1.0)");
}
));
Expand Down Expand Up @@ -72,14 +75,17 @@ void lockTimeoutDoesNotAffectLockOwnerAndPoolRecovers() throws Exception {

for (int attempt = 0; attempt < 3; attempt++) {
Throwable failure = catchThrowable(() ->
transactionTemplate.executeWithoutResult(status ->
runtimeJdbc.update(
"UPDATE company SET name = ? "
+ "WHERE company_id = ?",
"blocked update",
FIXTURE_COMPANY_ID
)
)
transactionTemplate.executeWithoutResult(status -> {
tenantDatabaseContext.setCompanyIdForCurrentTransaction(
FIXTURE_COMPANY_ID
);
runtimeJdbc.update(
"UPDATE company SET name = ? "
+ "WHERE company_id = ?",
"blocked update",
FIXTURE_COMPANY_ID
);
})
);
PostgreSqlTimeoutClassification classification =
classifier.classify(failure);
Expand All @@ -95,11 +101,14 @@ void lockTimeoutDoesNotAffectLockOwnerAndPoolRecovers() throws Exception {
}
}

transactionTemplate.executeWithoutResult(status -> runtimeJdbc.update(
"UPDATE company SET name = ? WHERE company_id = ?",
"after lock release",
FIXTURE_COMPANY_ID
));
transactionTemplate.executeWithoutResult(status -> {
tenantDatabaseContext.setCompanyIdForCurrentTransaction(FIXTURE_COMPANY_ID);
assertThat(runtimeJdbc.update(
"UPDATE company SET name = ? WHERE company_id = ?",
"after lock release",
FIXTURE_COMPANY_ID
)).isEqualTo(1);
});
assertThat(migrationJdbc.queryForObject(
"SELECT name FROM company WHERE company_id = ?",
String.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fowoco.server.ServerApplication;
import com.fowoco.server.common.security.PostgreSqlRlsTestLock;
import com.fowoco.server.common.security.TenantDatabaseContext;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.DriverManager;
Expand Down Expand Up @@ -41,6 +42,7 @@ abstract class PostgreSqlRuntimeTimeoutIntegrationSupport {
protected JdbcTemplate runtimeJdbc;
protected HikariDataSource runtimeDataSource;
protected TransactionTemplate transactionTemplate;
protected TenantDatabaseContext tenantDatabaseContext;
private PostgreSqlRlsTestLock rlsTestLock;

protected abstract String statementTimeout();
Expand Down Expand Up @@ -122,6 +124,9 @@ ON CONFLICT (company_id) DO UPDATE SET
transactionTemplate = new TransactionTemplate(
applicationContext.getBean(PlatformTransactionManager.class)
);
tenantDatabaseContext = applicationContext.getBean(
TenantDatabaseContext.class
);
} catch (Throwable setupFailure) {
try {
tearDownRuntimeTimeoutFixture();
Expand Down
Loading