From 06d4292fa7631e7668a3f5b1ee5451026958e4ae Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 01:16:30 +0000 Subject: [PATCH 1/5] feat: support int, string, or null types for tenant Update tenant property and methods across ClickHouse adapter and Log model to accept int|string|null instead of ?int. Change ClickHouse column type from Nullable(UInt64) to Nullable(String) to store both numeric and string tenant identifiers. https://claude.ai/code/session_01X8MFsu464fPHGaF7uPeWAZ --- src/Audit/Adapter/ClickHouse.php | 19 ++++++++++--------- src/Audit/Log.php | 8 ++++---- tests/Audit/Adapter/ClickHouseTest.php | 6 +++++- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index 0d598d5..7793d83 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -42,7 +42,7 @@ class ClickHouse extends SQL protected string $namespace = ''; - protected ?int $tenant = null; + protected int|string|null $tenant = null; protected bool $sharedTables = false; @@ -208,10 +208,10 @@ public function getNamespace(): string * Set the tenant ID for multi-tenant support. * Tenant is used to isolate audit logs by tenant. * - * @param int|null $tenant + * @param int|string|null $tenant * @return self */ - public function setTenant(?int $tenant): self + public function setTenant(int|string|null $tenant): self { $this->tenant = $tenant; return $this; @@ -220,9 +220,9 @@ public function setTenant(?int $tenant): self /** * Get the tenant ID. * - * @return int|null + * @return int|string|null */ - public function getTenant(): ?int + public function getTenant(): int|string|null { return $this->tenant; } @@ -631,7 +631,7 @@ public function setup(): void // Add tenant column only if tables are shared across tenants if ($this->sharedTables) { - $columns[] = 'tenant Nullable(UInt64)'; // Supports 11-digit MySQL auto-increment IDs + $columns[] = 'tenant Nullable(String)'; } // Build indexes from base adapter schema @@ -1197,13 +1197,13 @@ private function parseJsonResults(string $result): array $document[$columnName] = $value ?? []; } } elseif ($columnName === 'tenant') { - // Parse tenant as integer or null + // Parse tenant as int, string, or null if ($value === null || $value === '') { $document[$columnName] = null; } elseif (is_numeric($value)) { $document[$columnName] = (int) $value; } else { - $document[$columnName] = null; + $document[$columnName] = (string) $value; } } elseif ($columnName === 'time') { // Convert ClickHouse timestamp format back to ISO 8601 @@ -1279,7 +1279,8 @@ private function getTenantFilter(): string } $escapedTenant = $this->escapeIdentifier('tenant'); - return " AND {$escapedTenant} = {$this->tenant}"; + $tenantValue = "'" . addslashes((string) $this->tenant) . "'"; + return " AND {$escapedTenant} = {$tenantValue}"; } /** diff --git a/src/Audit/Log.php b/src/Audit/Log.php index ffbf197..6708008 100644 --- a/src/Audit/Log.php +++ b/src/Audit/Log.php @@ -126,9 +126,9 @@ public function getData(): array /** * Get the tenant ID (for multi-tenant setups). * - * @return int|null + * @return int|string|null */ - public function getTenant(): ?int + public function getTenant(): int|string|null { $tenant = $this->getAttribute('tenant'); @@ -140,8 +140,8 @@ public function getTenant(): ?int return $tenant; } - if (is_numeric($tenant)) { - return (int) $tenant; + if (is_string($tenant)) { + return $tenant; } return null; diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index 1374c71..f252833 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -299,11 +299,15 @@ public function testSharedTablesConfiguration(): void $this->assertInstanceOf(ClickHouse::class, $result); $this->assertTrue($adapter->isSharedTables()); - // Test setting tenant + // Test setting tenant to int $result2 = $adapter->setTenant(12345); $this->assertInstanceOf(ClickHouse::class, $result2); $this->assertEquals(12345, $adapter->getTenant()); + // Test setting tenant to string + $adapter->setTenant('tenant_abc'); + $this->assertSame('tenant_abc', $adapter->getTenant()); + // Test setting tenant to null $adapter->setTenant(null); $this->assertNull($adapter->getTenant()); From 3f2ab03615a4da3f2fc57b0cb04708b196b5fd9d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 01:28:23 +0000 Subject: [PATCH 2/5] fix: use parameterized queries for tenant filter to prevent SQL injection Replace addslashes-based string interpolation with ClickHouse native query parameters ({_tenant:String}) for tenant filtering. All call sites now merge tenant params via getTenantParams(). https://claude.ai/code/session_01X8MFsu464fPHGaF7uPeWAZ --- src/Audit/Adapter/ClickHouse.php | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index 7793d83..aa754de 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -799,7 +799,7 @@ public function getById(string $id): ?Log FORMAT JSON "; - $result = $this->query($sql, ['id' => $id]); + $result = $this->query($sql, array_merge(['id' => $id], $this->getTenantParams())); $logs = $this->parseJsonResults($result); return $logs[0] ?? null; @@ -850,7 +850,7 @@ public function find(array $queries = []): array FORMAT JSON "; - $result = $this->query($sql, $parsed['params']); + $result = $this->query($sql, array_merge($parsed['params'], $this->getTenantParams())); return $this->parseJsonResults($result); } @@ -890,7 +890,7 @@ public function count(array $queries = []): int FORMAT TabSeparated "; - $result = $this->query($sql, $params); + $result = $this->query($sql, array_merge($params, $this->getTenantParams())); $trimmed = trim($result); return $trimmed !== '' ? (int) $trimmed : 0; @@ -1279,8 +1279,21 @@ private function getTenantFilter(): string } $escapedTenant = $this->escapeIdentifier('tenant'); - $tenantValue = "'" . addslashes((string) $this->tenant) . "'"; - return " AND {$escapedTenant} = {$tenantValue}"; + return " AND {$escapedTenant} = {_tenant:String}"; + } + + /** + * Get query parameters for tenant filtering. + * + * @return array + */ + private function getTenantParams(): array + { + if (!$this->sharedTables || $this->tenant === null) { + return []; + } + + return ['_tenant' => (string) $this->tenant]; } /** @@ -1571,7 +1584,7 @@ public function cleanup(\DateTime $datetime): bool WHERE time < {datetime:String}{$tenantFilter} "; - $this->query($sql, ['datetime' => $datetimeString]); + $this->query($sql, array_merge(['datetime' => $datetimeString], $this->getTenantParams())); return true; } From 6a040ebb22fee43ed0af102b6ef8f193f8faeb9f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 01:34:38 +0000 Subject: [PATCH 3/5] fix: add type check before casting mixed tenant value to string PHPStan does not allow casting mixed to string directly. Use is_scalar() guard before the cast. https://claude.ai/code/session_01X8MFsu464fPHGaF7uPeWAZ --- src/Audit/Adapter/ClickHouse.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index aa754de..1ebd7b7 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -1202,8 +1202,10 @@ private function parseJsonResults(string $result): array $document[$columnName] = null; } elseif (is_numeric($value)) { $document[$columnName] = (int) $value; - } else { + } elseif (is_scalar($value)) { $document[$columnName] = (string) $value; + } else { + $document[$columnName] = null; } } elseif ($columnName === 'time') { // Convert ClickHouse timestamp format back to ISO 8601 From a7f3251f63df526bfe3acda2c68008adcc2d224c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 07:00:03 +0000 Subject: [PATCH 4/5] fix: return int for numeric tenant values in getTenant() Address review comment from abnegate: if tenant is numeric, cast to int before returning, maintaining backward compatibility with integer tenant IDs. https://claude.ai/code/session_01X8MFsu464fPHGaF7uPeWAZ --- src/Audit/Log.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Audit/Log.php b/src/Audit/Log.php index 6708008..f46ef69 100644 --- a/src/Audit/Log.php +++ b/src/Audit/Log.php @@ -140,6 +140,10 @@ public function getTenant(): int|string|null return $tenant; } + if (is_numeric($tenant)) { + return (int) $tenant; + } + if (is_string($tenant)) { return $tenant; } From c7abe55c010ddb47fc2538a7616c0a14f07abcd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 07:02:23 +0000 Subject: [PATCH 5/5] revert: keep numeric string tenants as strings in getTenant() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do not cast numeric strings to int — if tenant was set as a string, return it as a string. The is_int() check already handles actual integers. https://claude.ai/code/session_01X8MFsu464fPHGaF7uPeWAZ --- src/Audit/Log.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Audit/Log.php b/src/Audit/Log.php index f46ef69..6708008 100644 --- a/src/Audit/Log.php +++ b/src/Audit/Log.php @@ -140,10 +140,6 @@ public function getTenant(): int|string|null return $tenant; } - if (is_numeric($tenant)) { - return (int) $tenant; - } - if (is_string($tenant)) { return $tenant; }