Skip to content
Open
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
18 changes: 18 additions & 0 deletions src/Audit/Adapter/ClickHouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,12 @@ public function create(array $log): Log
// Separate data for the data column (non-schema attributes)
$nonSchemaData = $logData;

$resourceValue = $log['resource'] ?? null;
if (!\is_string($resourceValue)) {
$resourceValue = '';
}
$resource = $this->parseResource($resourceValue);

foreach ($schemaColumns as $columnName) {
if ($columnName === 'time') {
// Skip time - already handled above
Expand Down Expand Up @@ -790,6 +796,10 @@ public function create(array $log): Log
$hasAttributeValue = true;
// Remove from non-schema data as it's now a dedicated column
unset($nonSchemaData[$columnName]);
} elseif (isset($resource[$columnName])) {
// Value is in parsed resource (e.g., resourceType, resourceId, resourceParent)
$attributeValue = $resource[$columnName];
$hasAttributeValue = true;
}

// Validate required attributes
Expand Down Expand Up @@ -1140,6 +1150,11 @@ public function createBatch(array $logs): bool

// Separate data for non-schema attributes
$nonSchemaData = $logData;
$resourceValue = $log['resource'] ?? null;
if (!\is_string($resourceValue)) {
$resourceValue = '';
}
$resource = $this->parseResource($resourceValue);
$processedLog = $log;

// Extract schema attributes: check main log first, then data array
Expand All @@ -1152,6 +1167,9 @@ public function createBatch(array $logs): bool
if (!isset($processedLog[$columnName]) && isset($logData[$columnName])) {
$processedLog[$columnName] = $logData[$columnName];
unset($nonSchemaData[$columnName]);
} elseif (!isset($processedLog[$columnName]) && isset($resource[$columnName])) {
// Check parsed resource for resourceType, resourceId, resourceParent
$processedLog[$columnName] = $resource[$columnName];
} elseif (isset($processedLog[$columnName]) && isset($logData[$columnName])) {
// If in both, main log takes precedence, remove from data
unset($nonSchemaData[$columnName]);
Expand Down
36 changes: 36 additions & 0 deletions src/Audit/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,40 @@ protected function getAllColumnDefinitions(): array

return $definitions;
}

/**
* Parses the resource string from the payload and extracts its ID, type, and parent.
*
* @param string $resource
* @return array{ resourceId: string, resourceType: string, resourceParent: string }
*/
protected function parseResource(string $resource): array
{
$parts = explode('/', $resource);

$resourceType = '';
$resourceParent = '';

// resource/resourceId/subResource/subResourceId
if (count($parts) === 4) {
$resourceId = $parts[3];
$resourceType = $parts[2];

// resource/resourceId
$resourceParent = "{$parts[0]}/{$parts[1]}";
} // resource/resourceId
elseif (count($parts) === 2) {
$resourceId = $parts[1];
$resourceType = $parts[0];
} else {
// default fallback
$resourceId = $resource;
}

return [
'resourceId' => $resourceId,
'resourceType' => $resourceType,
'resourceParent' => $resourceParent,
];
}
}
57 changes: 57 additions & 0 deletions tests/Audit/Adapter/ClickHouseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -413,4 +413,61 @@ public function testClickHouseAdapterIndexes(): void
$this->assertContains($expected, $indexIds, "Parent index '{$expected}' not found in ClickHouse adapter");
}
}

/**
* Test parsing of complex resource paths into resourceType/resourceId/resourceParent
*/
public function testParseResourceComplexPath(): void
{
$userId = 'parseUser';
$userAgent = 'UnitTestAgent/1.0';
$ip = '127.0.0.1';
$location = 'US';

$resource = 'database/6978484940ff05762e1a/table/697848498066e3d2ef64';

// Ensure we don't provide resourceType/resourceId in data so adapter must parse it
$data = ['example' => 'value'];

// Merge required adapter attributes so ClickHouse won't reject the log,
// but ensure we do NOT supply resourceType/resourceId/resourceParent so adapter parses them
$required = $this->getRequiredAttributes();
unset($required['resourceType'], $required['resourceId'], $required['resourceParent']);
$dataWithAttributes = array_merge($data, $required);

$log = $this->audit->log($userId, 'create', $resource, $userAgent, $ip, $location, $dataWithAttributes);

$this->assertInstanceOf(\Utopia\Audit\Log::class, $log);

$this->assertEquals('table', $log->getAttribute('resourceType'));
$this->assertEquals('697848498066e3d2ef64', $log->getAttribute('resourceId'));
$this->assertEquals('database/6978484940ff05762e1a', $log->getAttribute('resourceParent'));
}

/**
* Directly test the protected parseResource method via reflection.
*/
public function testParseResourceMethod(): void
{
$adapter = new ClickHouse(
host: 'clickhouse',
username: 'default',
password: 'clickhouse'
);

$method = new \ReflectionMethod($adapter, 'parseResource');
$method->setAccessible(true);

$resource = 'database/6978484940ff05762e1a/table/697848498066e3d2ef64';
$parsed = $method->invoke($adapter, $resource);

$this->assertIsArray($parsed);
$this->assertArrayHasKey('resourceId', $parsed);
$this->assertArrayHasKey('resourceType', $parsed);
$this->assertArrayHasKey('resourceParent', $parsed);

$this->assertEquals('697848498066e3d2ef64', $parsed['resourceId']);
$this->assertEquals('table', $parsed['resourceType']);
$this->assertEquals('database/6978484940ff05762e1a', $parsed['resourceParent']);
}
}