diff --git a/src/Audit/Adapter.php b/src/Audit/Adapter.php index a6d5a53..33f0d8b 100644 --- a/src/Audit/Adapter.php +++ b/src/Audit/Adapter.php @@ -43,7 +43,6 @@ abstract public function getById(string $id): ?Log; * resource: string, * userAgent: string, * ip: string, - * location?: string, * data?: array * } $log * @return Log The created log entry @@ -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 * }> $logs diff --git a/src/Audit/Adapter/SQL.php b/src/Audit/Adapter/SQL.php index f35625b..4307db4 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -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, @@ -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 `/`, + * e.g. `database/`, `database//collection/`, + * `database//collection//document/`. 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 [ diff --git a/src/Audit/Audit.php b/src/Audit/Audit.php index 0e1deb4..bd51560 100644 --- a/src/Audit/Audit.php +++ b/src/Audit/Audit.php @@ -52,22 +52,20 @@ public function setup(): void * @param string $resource * @param string $userAgent * @param string $ip - * @param string $location * @param array $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} $log */ + /** @var array{userId?: string|null, event: string, resource: string, userAgent: string, ip: string, data?: array} $log */ $log = [ 'userId' => $userId, 'event' => $event, 'resource' => $resource, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => $data, ]; @@ -77,7 +75,7 @@ public function log(?string $userId, string $event, string $resource, string $us /** * Add multiple event logs in batch. * - * @param array}> $events + * @param array}> $events * @return bool * * @throws \Exception diff --git a/src/Audit/Log.php b/src/Audit/Log.php index ffbf197..f251575 100644 --- a/src/Audit/Log.php +++ b/src/Audit/Log.php @@ -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. * diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index b50da71..0e3f3f4 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -61,6 +61,7 @@ protected function getRequiredAttributes(): array 'teamId' => 'team-1', 'teamInternalId' => 'team-int-1', 'hostname' => 'example.org', + 'country' => 'us', ]; } @@ -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()) ?? '' ] @@ -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'; @@ -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); @@ -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 @@ -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']), diff --git a/tests/Audit/AuditBase.php b/tests/Audit/AuditBase.php index 823f4a5..30531b1 100644 --- a/tests/Audit/AuditBase.php +++ b/tests/Audit/AuditBase.php @@ -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 @@ -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 @@ -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 @@ -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)) ?? ''; @@ -212,7 +208,6 @@ public function testLogByBatch(): void 'resource' => 'database/document/batch1', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value1'], 'time' => $timestamp1 ], @@ -222,7 +217,6 @@ public function testLogByBatch(): void 'resource' => 'database/document/batch2', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value2'], 'time' => $timestamp2 ], @@ -232,7 +226,6 @@ public function testLogByBatch(): void 'resource' => 'database/document/batch3', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value3'], 'time' => $timestamp3 ], @@ -242,7 +235,6 @@ public function testLogByBatch(): void 'resource' => 'user1/null', 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['key' => 'value4'], 'time' => $timestamp3 ] @@ -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) ?? '' ]; @@ -362,7 +353,6 @@ public function testTimeRangeFilters(): void 'resource' => 'doc/1', 'userAgent' => 'Mozilla', 'ip' => '127.0.0.1', - 'location' => 'US', 'data' => [], 'time' => $old ], @@ -372,7 +362,6 @@ public function testTimeRangeFilters(): void 'resource' => 'doc/2', 'userAgent' => 'Mozilla', 'ip' => '127.0.0.1', - 'location' => 'US', 'data' => [], 'time' => $recent ] @@ -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 @@ -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'); @@ -455,7 +442,6 @@ public function testRetrievalParameters(): void 'resource' => 'doc/' . $i, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['sequence' => $i], 'time' => $timestamp ]; @@ -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'); @@ -627,7 +612,6 @@ public function testFind(): void 'resource' => 'doc/' . $i, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['sequence' => $i], 'time' => $timestamp ]; @@ -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'); @@ -742,7 +725,6 @@ public function testCount(): void 'resource' => 'doc/' . $i, 'userAgent' => $userAgent, 'ip' => $ip, - 'location' => $location, 'data' => ['sequence' => $i], 'time' => $timestamp ]; @@ -813,17 +795,17 @@ public function testCount(): void * Apply adapter-specific required attributes to batch events. * * @param array> $batchEvents - * @return array}> + * @return array}> */ protected function applyRequiredAttributesToBatch(array $batchEvents): array { $requiredAttributes = $this->getRequiredAttributes(); if ($requiredAttributes === []) { - /** @var array}> */ + /** @var array}> */ return $batchEvents; } - /** @var array}> */ + /** @var array}> */ return array_map(static fn (array $event) => array_merge($event, $requiredAttributes), $batchEvents); }