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
2 changes: 0 additions & 2 deletions src/Audit/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ abstract public function getById(string $id): ?Log;
* resource: string,
* userAgent: string,
* ip: string,
* location?: string,
* data?: array<string, mixed>
* } $log
* @return Log The created log entry
Expand All @@ -61,7 +60,6 @@ abstract public function create(array $log): Log;
* resource: string,
* userAgent: string,
* ip: string,
* location?: string,
* time: \DateTime|string|null,
* data?: array<string, mixed>
* }> $logs
Expand Down
36 changes: 14 additions & 22 deletions src/Audit/Adapter/SQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,6 @@ public function getAttributes(): array
'array' => false,
'filters' => [],
],
[
'$id' => 'location',
'type' => Database::VAR_STRING,
'size' => 45,
'required' => false,
'signed' => true,
'array' => false,
'filters' => [],
],
[
'$id' => 'time',
'type' => Database::VAR_DATETIME,
Expand Down Expand Up @@ -222,30 +213,31 @@ protected function getAllColumnDefinitions(): array
/**
* Parses the resource string from the payload and extracts its ID, type, and parent.
*
* Supports any even number of segments shaped as alternating `<type>/<id>`,
* e.g. `database/<id>`, `database/<id>/collection/<id>`,
* `database/<id>/collection/<id>/document/<id>`. The last segment is the
* resource id, the second-to-last is the resource type, and any preceding
* segments form the resource parent path.
*
* @param string $resource
* @return array{ resourceId: string, resourceType: string, resourceParent: string }
*/
protected function parseResource(string $resource): array
{
$parts = explode('/', $resource);
$count = count($parts);

$resourceId = $resource;
$resourceType = '';
$resourceParent = '';

// resource/resourceId/subResource/subResourceId
if (count($parts) === 4) {
$resourceId = $parts[3];
$resourceType = $parts[2];
if ($count >= 2 && $count % 2 === 0) {
$resourceId = $parts[$count - 1];
$resourceType = $parts[$count - 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;
if ($count > 2) {
$resourceParent = implode('/', array_slice($parts, 0, $count - 2));
}
}

return [
Expand Down
8 changes: 3 additions & 5 deletions src/Audit/Audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,20 @@ public function setup(): void
* @param string $resource
* @param string $userAgent
* @param string $ip
* @param string $location
* @param array<string, mixed> $data
* @return Log
*
* @throws \Exception
*/
public function log(?string $userId, string $event, string $resource, string $userAgent, string $ip, string $location, array $data = []): Log
public function log(?string $userId, string $event, string $resource, string $userAgent, string $ip, array $data = []): Log
{
/** @var array{userId?: string|null, event: string, resource: string, userAgent: string, ip: string, location?: string, data?: array<string, mixed>} $log */
/** @var array{userId?: string|null, event: string, resource: string, userAgent: string, ip: string, data?: array<string, mixed>} $log */
$log = [
'userId' => $userId,
'event' => $event,
'resource' => $resource,
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => $data,
];

Expand All @@ -77,7 +75,7 @@ public function log(?string $userId, string $event, string $resource, string $us
/**
* Add multiple event logs in batch.
*
* @param array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, location: string, time: string, data?: array<string, mixed>}> $events
* @param array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, time: string, data?: array<string, mixed>}> $events
* @return bool
*
* @throws \Exception
Expand Down
11 changes: 0 additions & 11 deletions src/Audit/Log.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ public function getIp(): string
return is_string($ip) ? $ip : '';
}

/**
* Get the location information.
*
* @return string|null
*/
public function getLocation(): ?string
{
$location = $this->getAttribute('location');
return is_string($location) ? $location : null;
}

/**
* Get the timestamp.
*
Expand Down
31 changes: 27 additions & 4 deletions tests/Audit/Adapter/ClickHouseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ protected function getRequiredAttributes(): array
'teamId' => 'team-1',
'teamInternalId' => 'team-int-1',
'hostname' => 'example.org',
'country' => 'us',
];
}

Expand Down Expand Up @@ -418,7 +419,6 @@ public function testBatchOperationsWithSpecialCharacters(): void
'resource' => 'doc/"quotes"',
'userAgent' => "User'Agent\"With'Quotes",
'ip' => '192.168.1.1',
'location' => 'UK',
'data' => ['special' => "data with 'quotes'"],
'time' => \Utopia\Database\DateTime::formatTz(\Utopia\Database\DateTime::now()) ?? ''
]
Expand Down Expand Up @@ -519,7 +519,6 @@ public function testParseResourceComplexPath(): void
$userId = 'parseUser';
$userAgent = 'UnitTestAgent/1.0';
$ip = '127.0.0.1';
$location = 'US';

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

Expand All @@ -532,7 +531,7 @@ public function testParseResourceComplexPath(): void
unset($required['resourceType'], $required['resourceId'], $required['resourceParent']);
$dataWithAttributes = array_merge($data, $required);

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

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

Expand Down Expand Up @@ -566,6 +565,30 @@ public function testParseResourceMethod(): void
$this->assertEquals('697848498066e3d2ef64', $parsed['resourceId']);
$this->assertEquals('table', $parsed['resourceType']);
$this->assertEquals('database/6978484940ff05762e1a', $parsed['resourceParent']);

$sixPart = 'database/693586330029ae2f0d3f/collection/watch_history/document/6a06d9a7001c3cd05d20';
/** @var array{resourceId: string, resourceType: string, resourceParent: string} $parsedSix */
$parsedSix = $method->invoke($adapter, $sixPart);

$this->assertEquals('6a06d9a7001c3cd05d20', $parsedSix['resourceId']);
$this->assertEquals('document', $parsedSix['resourceType']);
$this->assertEquals('database/693586330029ae2f0d3f/collection/watch_history', $parsedSix['resourceParent']);

$twoPart = 'user/abc123';
/** @var array{resourceId: string, resourceType: string, resourceParent: string} $parsedTwo */
$parsedTwo = $method->invoke($adapter, $twoPart);

$this->assertEquals('abc123', $parsedTwo['resourceId']);
$this->assertEquals('user', $parsedTwo['resourceType']);
$this->assertEquals('', $parsedTwo['resourceParent']);

$oddPart = 'foo/bar/baz';
/** @var array{resourceId: string, resourceType: string, resourceParent: string} $parsedOdd */
$parsedOdd = $method->invoke($adapter, $oddPart);

$this->assertEquals('foo/bar/baz', $parsedOdd['resourceId']);
$this->assertEquals('', $parsedOdd['resourceType']);
$this->assertEquals('', $parsedOdd['resourceParent']);
}

public function testCursorAfterPaginatesLogs(): void
Expand Down Expand Up @@ -813,7 +836,7 @@ public function testSelectAutoIncludesTenantWhenShared(): void
$adapter->setup();

$audit = new Audit($adapter);
$audit->log('u1', 'create', 'doc/1', 'agent', '127.0.0.1', 'US', $this->getRequiredAttributes());
$audit->log('u1', 'create', 'doc/1', 'agent', '127.0.0.1', $this->getRequiredAttributes());

$logs = $audit->find([
Query::select(['event']),
Expand Down
40 changes: 11 additions & 29 deletions tests/Audit/AuditBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,15 @@ public function createLogs(): void
$userId = 'userId';
$userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36';
$ip = '127.0.0.1';
$location = 'US';
$data = ['key1' => 'value1', 'key2' => 'value2'];

$requiredAttributes = $this->getRequiredAttributes();
$dataWithAttributes = array_merge($data, $requiredAttributes);

$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log(null, 'insert', 'user/null', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log(null, 'insert', 'user/null', $userAgent, $ip, $dataWithAttributes));
}

public function testGetLogsByUser(): void
Expand Down Expand Up @@ -164,12 +163,11 @@ public function testGetLogById(): void
$userId = 'testGetByIdUser';
$userAgent = 'Mozilla/5.0 Test';
$ip = '192.168.1.100';
$location = 'US';
$data = ['test' => 'getById'];

$requiredAttributes = $this->getRequiredAttributes();
$dataWithAttributes = array_merge($data, $requiredAttributes);
$log = $this->audit->log($userId, 'create', 'test/resource/123', $userAgent, $ip, $location, $dataWithAttributes);
$log = $this->audit->log($userId, 'create', 'test/resource/123', $userAgent, $ip, $dataWithAttributes);
$logId = $log->getId();

// Retrieve the log by ID
Expand All @@ -182,7 +180,6 @@ public function testGetLogById(): void
$this->assertEquals('test/resource/123', $retrievedLog->getAttribute('resource'));
$this->assertEquals($userAgent, $retrievedLog->getAttribute('userAgent'));
$this->assertEquals($ip, $retrievedLog->getAttribute('ip'));
$this->assertEquals($location, $retrievedLog->getAttribute('location'));
$this->assertEquals($data, $retrievedLog->getAttribute('data'));

// Test with non-existent ID
Expand All @@ -198,7 +195,6 @@ public function testLogByBatch(): void
$userId = 'batchUserId';
$userAgent = 'Mozilla/5.0 (Test User Agent)';
$ip = '192.168.1.1';
$location = 'UK';

// Create timestamps 1 minute apart
$timestamp1 = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -120)) ?? '';
Expand All @@ -212,7 +208,6 @@ public function testLogByBatch(): void
'resource' => 'database/document/batch1',
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['key' => 'value1'],
'time' => $timestamp1
],
Expand All @@ -222,7 +217,6 @@ public function testLogByBatch(): void
'resource' => 'database/document/batch2',
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['key' => 'value2'],
'time' => $timestamp2
],
Expand All @@ -232,7 +226,6 @@ public function testLogByBatch(): void
'resource' => 'database/document/batch3',
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['key' => 'value3'],
'time' => $timestamp3
],
Expand All @@ -242,7 +235,6 @@ public function testLogByBatch(): void
'resource' => 'user1/null',
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['key' => 'value4'],
'time' => $timestamp3
]
Expand Down Expand Up @@ -326,7 +318,6 @@ public function testLargeBatchInsert(): void
'resource' => 'doc/' . $i,
'userAgent' => 'Mozilla',
'ip' => '127.0.0.1',
'location' => 'US',
'data' => ['index' => $i],
'time' => DateTime::formatTz($baseTime) ?? ''
];
Expand Down Expand Up @@ -362,7 +353,6 @@ public function testTimeRangeFilters(): void
'resource' => 'doc/1',
'userAgent' => 'Mozilla',
'ip' => '127.0.0.1',
'location' => 'US',
'data' => [],
'time' => $old
],
Expand All @@ -372,7 +362,6 @@ public function testTimeRangeFilters(): void
'resource' => 'doc/2',
'userAgent' => 'Mozilla',
'ip' => '127.0.0.1',
'location' => 'US',
'data' => [],
'time' => $recent
]
Expand Down Expand Up @@ -404,17 +393,16 @@ public function testCleanup(): void
$userId = 'userId';
$userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36';
$ip = '127.0.0.1';
$location = 'US';
$data = ['key1' => 'value1', 'key2' => 'value2'];

$requiredAttributes = $this->getRequiredAttributes();
$dataWithAttributes = array_merge($data, $requiredAttributes);

$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/1', $userAgent, $ip, $dataWithAttributes));
sleep(5);
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'update', 'database/document/2', $userAgent, $ip, $dataWithAttributes));
sleep(5);
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $location, $dataWithAttributes));
$this->assertInstanceOf('Utopia\\Audit\\Log', $this->audit->log($userId, 'delete', 'database/document/2', $userAgent, $ip, $dataWithAttributes));
sleep(5);

// DELETE logs older than 11 seconds and check that status is true
Expand All @@ -439,7 +427,6 @@ public function testRetrievalParameters(): void
$userId = 'paramtestuser';
$userAgent = 'Mozilla/5.0';
$ip = '192.168.1.1';
$location = 'US';

// Create 5 logs with different timestamps
$baseTime = new \DateTime('2024-06-15 12:00:00');
Expand All @@ -455,7 +442,6 @@ public function testRetrievalParameters(): void
'resource' => 'doc/' . $i,
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['sequence' => $i],
'time' => $timestamp
];
Expand Down Expand Up @@ -611,7 +597,6 @@ public function testFind(): void
$userId = 'userId';
$userAgent = 'Mozilla/5.0';
$ip = '192.168.1.1';
$location = 'US';

// Create test logs with specific attributes
$baseTime = new \DateTime('2024-06-15 12:00:00');
Expand All @@ -627,7 +612,6 @@ public function testFind(): void
'resource' => 'doc/' . $i,
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['sequence' => $i],
'time' => $timestamp
];
Expand Down Expand Up @@ -726,7 +710,6 @@ public function testCount(): void
$userId = 'userId';
$userAgent = 'Mozilla/5.0';
$ip = '192.168.1.1';
$location = 'US';

// Create test logs with specific attributes
$baseTime = new \DateTime('2024-06-15 12:00:00');
Expand All @@ -742,7 +725,6 @@ public function testCount(): void
'resource' => 'doc/' . $i,
'userAgent' => $userAgent,
'ip' => $ip,
'location' => $location,
'data' => ['sequence' => $i],
'time' => $timestamp
];
Expand Down Expand Up @@ -813,17 +795,17 @@ public function testCount(): void
* Apply adapter-specific required attributes to batch events.
*
* @param array<int, array<string, mixed>> $batchEvents
* @return array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, location: string, time: string, data?: array<string, mixed>}>
* @return array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, time: string, data?: array<string, mixed>}>
*/
protected function applyRequiredAttributesToBatch(array $batchEvents): array
{
$requiredAttributes = $this->getRequiredAttributes();
if ($requiredAttributes === []) {
/** @var array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, location: string, time: string, data?: array<string, mixed>}> */
/** @var array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, time: string, data?: array<string, mixed>}> */
return $batchEvents;
}

/** @var array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, location: string, time: string, data?: array<string, mixed>}> */
/** @var array<array{userId: string|null, event: string, resource: string, userAgent: string, ip: string, time: string, data?: array<string, mixed>}> */
return array_map(static fn (array $event) => array_merge($event, $requiredAttributes), $batchEvents);
}

Expand Down
Loading