From c353eb1d70ac5f5515889cc84f5c1420dbaf3d9e Mon Sep 17 00:00:00 2001 From: sid Date: Mon, 23 Dec 2024 17:44:38 +0100 Subject: [PATCH] feat: Ruleset endpoint --- README.md | 1 + composer.lock | 15 ++-- src/Endpoints/Ruleset.php | 145 ++++++++++++++++++++++++++++++++ tests/Endpoints/RulesetTest.php | 143 +++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 src/Endpoints/Ruleset.php create mode 100644 tests/Endpoints/RulesetTest.php diff --git a/README.md b/README.md index f07db941..95d3a897 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Each API call is provided via a similarly named function within various classes - [x] User Administration (partial) - [x] [Cloudflare IPs](https://www.cloudflare.com/ips/) - [x] [Page Rules](https://support.cloudflare.com/hc/en-us/articles/200168306-Is-there-a-tutorial-for-Page-Rules-) +- [x] [Rulesets](https://developers.cloudflare.com/ruleset-engine/rulesets-api/) - [x] [Web Application Firewall (WAF)](https://www.cloudflare.com/waf/) - [ ] Virtual DNS Management - [x] Custom hostnames diff --git a/composer.lock b/composer.lock index 633000df..989a71f4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dfd011c55474ffaab84b7a0a6d164049", + "content-hash": "669205656fa385d3d9b4d3cf919839d6", "packages": [ { "name": "guzzlehttp/guzzle", @@ -3003,12 +3003,12 @@ "version": "1.7.0", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", + "url": "https://github.com/webmozarts/assert.git", "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", "shasum": "" }, @@ -3049,12 +3049,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "phpmd/phpmd": 0 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.0.0", + "php": ">=7.2.5", "ext-json": "*" }, - "platform-dev": [] + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/src/Endpoints/Ruleset.php b/src/Endpoints/Ruleset.php new file mode 100644 index 00000000..841b8b8e --- /dev/null +++ b/src/Endpoints/Ruleset.php @@ -0,0 +1,145 @@ +adapter = $adapter; + } + + /** + * Get all rulesets for a zone. + * + * @param string $zoneID The ID of the zone. + * @return array The list of rulesets. + */ + public function listZoneRulesets(string $zoneID): array + { + $response = $this->adapter->get("zones/{$zoneID}/rulesets"); + + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + + return $data["result"] ?? []; + } + + /** + * Get rulesets for a specific phase within a zone. + * + * @param string $zoneID The ID of the zone. + * @param string $phase The phase of the ruleset (e.g., http_request_dynamic_redirect). + * @return array The filtered list of rulesets. + */ + public function getRulesetsByPhase(string $zoneID, string $phase): array + { + $rulesets = $this->listZoneRulesets($zoneID); + return array_filter($rulesets, function ($ruleset) use ($phase) { + return isset($ruleset['phase']) && $ruleset['phase'] === $phase; + }); + } + + /** + * Get a specific ruleset by ID. + * + * @param string $zoneID The ID of the zone. + * @param string $rulesetID The ID of the ruleset. + * @return array The ruleset details. + */ + public function getRuleset(string $zoneID, string $rulesetID): array + { + $response = $this->adapter->get("zones/{$zoneID}/rulesets/{$rulesetID}"); + + $body = $response->getBody()->getContents(); + + $data = json_decode($body, true); + + return $data["result"] ?? []; + } + + /** + * Create a new ruleset for a zone. + * + * @param string $zoneID The ID of the zone. + * @param array $payload The payload for the new ruleset. + * @return array The created ruleset details. + */ + public function createRuleset(string $zoneID, array $payload): array + { + $response = $this->adapter->post("zones/{$zoneID}/rulesets", $payload); + + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + + return $data["result"] ?? []; + } + + /** + * Update an existing ruleset. + * + * @param string $zoneID The ID of the zone. + * @param string $rulesetID The ID of the ruleset. + * @param array $payload The payload with updated ruleset details. + * @return array The updated ruleset details. + */ + public function updateRuleset(string $zoneID, string $rulesetID, array $payload): array + { + $response = $this->adapter->put("zones/{$zoneID}/rulesets/{$rulesetID}", $payload); + + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + + return $data["result"] ?? []; + } + + /** + * Delete a specific rule by name from a ruleset. + * + * @param string $zoneID The ID of the zone. + * @param string $rulesetID The ID of the ruleset. + * @param string $ruleName The name of the rule to delete. + * @return bool True if deletion was successful, false otherwise. + */ + public function deleteRuleByName(string $zoneID, string $rulesetID, string $ruleName): bool + { + $rulesetDetails = $this->getRuleset($zoneID, $rulesetID); + $rules = $rulesetDetails['rules'] ?? []; + + // Filter out the rule with the specified name + $updatedRules = array_filter($rules, function ($rule) use ($ruleName) { + return $rule['description'] !== $ruleName; + }); + + if (count($updatedRules) === count($rules)) { + // No rule was removed + return false; + } + + $payload = ['rules' => array_values($updatedRules)]; + $updatedRuleset = $this->updateRuleset($zoneID, $rulesetID, $payload); + + return !empty($updatedRuleset); + } + + /** + * Delete a ruleset from a zone. + * + * @param string $zoneID The ID of the zone. + * @param string $rulesetID The ID of the ruleset. + * @return bool True if deletion was successful, false otherwise. + */ + public function deleteRuleset(string $zoneID, string $rulesetID): bool + { + $response = $this->adapter->delete("zones/{$zoneID}/rulesets/{$rulesetID}"); + + $body = $response->getBody()->getContents(); + $data = json_decode($body, true); + + return $data["success"] ?? false; + } +} diff --git a/tests/Endpoints/RulesetTest.php b/tests/Endpoints/RulesetTest.php new file mode 100644 index 00000000..8ad19798 --- /dev/null +++ b/tests/Endpoints/RulesetTest.php @@ -0,0 +1,143 @@ +adapterMock = $this->createMock(Adapter::class); + $this->ruleset = new Ruleset($this->adapterMock); + } + + public function testListZoneRulesets(): void + { + $zoneID = 'example-zone-id'; + $expectedResult = [ + 'result' => [ + [ + 'id' => 'example-ruleset-id', + 'name' => 'Example Ruleset', + 'phase' => 'http_request_dynamic_redirect', + ], + ], + ]; + + $this->adapterMock->expects($this->once()) + ->method('get') + ->with("zones/{$zoneID}/rulesets") + ->willReturn($this->createResponseMock($expectedResult)); + + $result = $this->ruleset->listZoneRulesets($zoneID); + + $this->assertEquals($expectedResult['result'], $result); + } + + public function testGetRulesetsByPhase(): void + { + $zoneID = 'example-zone-id'; + $phase = 'http_request_dynamic_redirect'; + $rulesets = [ + [ + 'id' => 'example-ruleset-id', + 'name' => 'Example Ruleset', + 'phase' => $phase, + ], + [ + 'id' => 'another-ruleset-id', + 'name' => 'Another Ruleset', + 'phase' => 'http_request_firewall', + ], + ]; + + $this->adapterMock->expects($this->once()) + ->method('get') + ->with("zones/{$zoneID}/rulesets") + ->willReturn($this->createResponseMock(['result' => $rulesets])); + + $result = $this->ruleset->getRulesetsByPhase($zoneID, $phase); + + $this->assertCount(1, $result); + $this->assertEquals($phase, $result[0]['phase']); + } + + public function testCreateRuleset(): void + { + $zoneID = 'example-zone-id'; + $payload = [ + 'name' => 'Test Ruleset', + 'description' => 'A ruleset for testing', + 'kind' => 'zone', + 'phase' => 'http_request_dynamic_redirect', + 'rules' => [], + ]; + $expectedResult = ['id' => 'new-ruleset-id']; + + $this->adapterMock->expects($this->once()) + ->method('post') + ->with("zones/{$zoneID}/rulesets", $payload) + ->willReturn($this->createResponseMock(['result' => $expectedResult])); + + $result = $this->ruleset->createRuleset($zoneID, $payload); + + $this->assertEquals($expectedResult, $result); + } + + public function testDeleteRuleByName(): void + { + $zoneID = 'example-zone-id'; + $rulesetID = 'example-ruleset-id'; + $ruleName = 'Example Rule'; + $rulesetDetails = [ + 'rules' => [ + [ + 'description' => $ruleName, + ], + [ + 'description' => 'Another Rule', + ], + ], + ]; + + $updatedRuleset = [ + 'rules' => [ + [ + 'description' => 'Another Rule', + ], + ], + ]; + + $this->adapterMock->expects($this->once()) + ->method('get') + ->with("zones/{$zoneID}/rulesets/{$rulesetID}") + ->willReturn($this->createResponseMock(['result' => $rulesetDetails])); + + $this->adapterMock->expects($this->once()) + ->method('put') + ->with("zones/{$zoneID}/rulesets/{$rulesetID}", ['rules' => $updatedRuleset['rules']]) + ->willReturn($this->createResponseMock(['result' => $updatedRuleset])); + + $result = $this->ruleset->deleteRuleByName($zoneID, $rulesetID, $ruleName); + + $this->assertTrue($result); + } + + private function createResponseMock(array $body): object + { + $responseMock = $this->createMock(\Psr\Http\Message\ResponseInterface::class); + $responseMock->method('getBody')->willReturn($this->createStreamMock(json_encode($body))); + return $responseMock; + } + + private function createStreamMock(string $content): object + { + $streamMock = $this->createMock(\Psr\Http\Message\StreamInterface::class); + $streamMock->method('getContents')->willReturn($content); + return $streamMock; + } +}