From 3fe8ad1ea4dcc263f47fbe59fd9410e6e9fc6f81 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sat, 7 Dec 2024 13:22:21 +0100 Subject: [PATCH 01/20] First endpoint fixes --- appinfo/routes.php | 1 + lib/Controller/EndpointController.php | 179 ++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 lib/Controller/EndpointController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index a07197c6..5531ecdc 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -2,6 +2,7 @@ return [ 'resources' => [ + 'Endpoints' => ['url' => 'api/endpoints'], 'Sources' => ['url' => 'api/sources'], 'Mappings' => ['url' => 'api/mappings'], 'Jobs' => ['url' => 'api/jobs'], diff --git a/lib/Controller/EndpointController.php b/lib/Controller/EndpointController.php new file mode 100644 index 00000000..124eeba4 --- /dev/null +++ b/lib/Controller/EndpointController.php @@ -0,0 +1,179 @@ +request->getParams(); + $fieldsToSearch = ['name', 'description', 'url']; + + $searchParams = $searchService->createMySQLSearchParams(filters: $filters); + $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams(filters: $filters); + + return new JSONResponse(['results' => $this->endpointMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + } + + /** + * Retrieves a single endpoint by its ID + * + * This method returns a JSON response containing the details of a specific endpoint. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the endpoint to retrieve + * @return JSONResponse A JSON response containing the endpoint details + */ + public function show(string $id): JSONResponse + { + try { + return new JSONResponse($this->endpointMapper->find(id: (int) $id)); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } + } + + /** + * Creates a new endpoint + * + * This method creates a new endpoint based on POST data. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse A JSON response containing the created endpoint + */ + public function create(): JSONResponse + { + $data = $this->request->getParams(); + + foreach ($data as $key => $value) { + if (str_starts_with($key, '_')) { + unset($data[$key]); + } + } + + if (isset($data['id'])) { + unset($data['id']); + } + + // Create the endpoint + $endpoint = $this->endpointMapper->createFromArray(object: $data); + + return new JSONResponse($endpoint); + } + + /** + * Updates an existing endpoint + * + * This method updates an existing endpoint based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the endpoint to update + * @return JSONResponse A JSON response containing the updated endpoint details + */ + public function update(int $id): JSONResponse + { + $data = $this->request->getParams(); + + foreach ($data as $key => $value) { + if (str_starts_with($key, '_')) { + unset($data[$key]); + } + } + if (isset($data['id'])) { + unset($data['id']); + } + + // Update the endpoint + $endpoint = $this->endpointMapper->updateFromArray(id: (int) $id, object: $data); + + return new JSONResponse($endpoint); + } + + /** + * Deletes an endpoint + * + * This method deletes an endpoint based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the endpoint to delete + * @return JSONResponse An empty JSON response + */ + public function destroy(int $id): JSONResponse + { + $this->endpointMapper->delete($this->endpointMapper->find((int) $id)); + + return new JSONResponse([]); + } +} From 9065d3d92d78f9d4517421d687851a3633e4a5a2 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 15 Dec 2024 22:46:25 +0100 Subject: [PATCH 02/20] First real code on endpoints --- appinfo/routes.php | 10 +- lib/Controller/EndpointController.php | 179 ------------------------- lib/Controller/EndpointsController.php | 94 +++++-------- lib/Db/EndpointMapper.php | 31 +++++ lib/Service/EndpointService.php | 134 ++++++++++++++++++ 5 files changed, 202 insertions(+), 246 deletions(-) delete mode 100644 lib/Controller/EndpointController.php create mode 100644 lib/Service/EndpointService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 6b6150fe..8a4aed1b 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -25,10 +25,10 @@ ['name' => 'mappings#test', 'url' => '/api/mappings/test', 'verb' => 'POST'], ['name' => 'mappings#saveObject', 'url' => '/api/mappings/objects', 'verb' => 'POST'], ['name' => 'mappings#getObjects', 'url' => '/api/mappings/objects', 'verb' => 'GET'], - // Running endpoints - ['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'GET'], - ['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'PUT'], - ['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'POST'], - ['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'DELETE'], + // Running endpoints - allow any path after /api/endpoints/ + ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+']], + ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+']], + ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+']], + ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+']], ], ]; diff --git a/lib/Controller/EndpointController.php b/lib/Controller/EndpointController.php deleted file mode 100644 index 124eeba4..00000000 --- a/lib/Controller/EndpointController.php +++ /dev/null @@ -1,179 +0,0 @@ -request->getParams(); - $fieldsToSearch = ['name', 'description', 'url']; - - $searchParams = $searchService->createMySQLSearchParams(filters: $filters); - $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); - $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - - return new JSONResponse(['results' => $this->endpointMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); - } - - /** - * Retrieves a single endpoint by its ID - * - * This method returns a JSON response containing the details of a specific endpoint. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the endpoint to retrieve - * @return JSONResponse A JSON response containing the endpoint details - */ - public function show(string $id): JSONResponse - { - try { - return new JSONResponse($this->endpointMapper->find(id: (int) $id)); - } catch (DoesNotExistException $exception) { - return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); - } - } - - /** - * Creates a new endpoint - * - * This method creates a new endpoint based on POST data. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return JSONResponse A JSON response containing the created endpoint - */ - public function create(): JSONResponse - { - $data = $this->request->getParams(); - - foreach ($data as $key => $value) { - if (str_starts_with($key, '_')) { - unset($data[$key]); - } - } - - if (isset($data['id'])) { - unset($data['id']); - } - - // Create the endpoint - $endpoint = $this->endpointMapper->createFromArray(object: $data); - - return new JSONResponse($endpoint); - } - - /** - * Updates an existing endpoint - * - * This method updates an existing endpoint based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the endpoint to update - * @return JSONResponse A JSON response containing the updated endpoint details - */ - public function update(int $id): JSONResponse - { - $data = $this->request->getParams(); - - foreach ($data as $key => $value) { - if (str_starts_with($key, '_')) { - unset($data[$key]); - } - } - if (isset($data['id'])) { - unset($data['id']); - } - - // Update the endpoint - $endpoint = $this->endpointMapper->updateFromArray(id: (int) $id, object: $data); - - return new JSONResponse($endpoint); - } - - /** - * Deletes an endpoint - * - * This method deletes an endpoint based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the endpoint to delete - * @return JSONResponse An empty JSON response - */ - public function destroy(int $id): JSONResponse - { - $this->endpointMapper->delete($this->endpointMapper->find((int) $id)); - - return new JSONResponse([]); - } -} diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index 0b571570..05f4a09f 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -4,6 +4,7 @@ use OCA\OpenConnector\Service\ObjectService; use OCA\OpenConnector\Service\SearchService; +use OCA\OpenConnector\Service\EndpointService; use OCA\OpenConnector\Db\Endpoint; use OCA\OpenConnector\Db\EndpointMapper; use OCP\AppFramework\Controller; @@ -13,6 +14,9 @@ use OCP\IRequest; use OCP\AppFramework\Db\DoesNotExistException; +/** + * Controller for handling endpoint related operations + */ class EndpointsController extends Controller { /** @@ -22,12 +26,14 @@ class EndpointsController extends Controller * @param IRequest $request The request object * @param IAppConfig $config The app configuration object * @param EndpointMapper $endpointMapper The endpoint mapper object + * @param EndpointService $endpointService Service for handling endpoint operations */ public function __construct( $appName, IRequest $request, private IAppConfig $config, - private EndpointMapper $endpointMapper + private EndpointMapper $endpointMapper, + private EndpointService $endpointService ) { parent::__construct($appName, $request); @@ -171,74 +177,38 @@ public function destroy(int $id): JSONResponse } /** - * Retrieves call logs for an endpoint - * - * This method returns all the call logs associated with an endpoint based on its ID. + * Handles generic path requests by matching against registered endpoints + * + * This method checks if the current path matches any registered endpoint patterns + * and forwards the request to the appropriate endpoint service if found * * @NoAdminRequired * @NoCSRFRequired - * - * @param int $id The ID of the endpoint to retrieve logs for - * @return JSONResponse A JSON response containing the call logs + * + * @param string $path The request path to match + * @return JSONResponse The response from the endpoint service or 404 if no match */ - public function logs(int $id): JSONResponse + public function handlePath(string $path): JSONResponse { - try { - $endpoint = $this->endpointMapper->find($id); - $endpointLogs = $this->endpointLogMapper->findAll(null, null, ['endpoint_id' => $endpoint->getId()]); - return new JSONResponse($endpointLogs); - } catch (DoesNotExistException $e) { - return new JSONResponse(['error' => 'Endpoint not found'], 404); - } - } + // Find matching endpoints for the given path and method + $matchingEndpoints = $this->endpointMapper->findByPathRegex( + path: $path, + method: $this->request->getMethod() + ); - /** - * Test an endpoint - * - * This method fires a test call to the endpoint and returns the response. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * Endpoint: /api/endpoints-test/{id} - * - * @param int $id The ID of the endpoint to test - * @return JSONResponse A JSON response containing the test results - */ - public function test(int $id): JSONResponse - { - try { - $endpoint = $this->endpointMapper->find(id: $id); - // Implement the logic to test the endpoint here - // This is a placeholder implementation - $testResult = ['status' => 'success', 'message' => 'Endpoint test successful']; - return new JSONResponse($testResult); - } catch (DoesNotExistException $exception) { - return new JSONResponse(data: ['error' => 'Endpoint not found'], statusCode: 404); + // If no matching endpoints found, return 404 + if (empty($matchingEndpoints)) { + return new JSONResponse( + data: ['error' => 'No matching endpoint found for path and method: ' . $path . ' ' . $this->request->getMethod()], + statusCode: 404 + ); } - } - /** - * Actually run an endpoint - * - * This method runs an endpoint based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param int $id The ID of the endpoint to run - * @return JSONResponse A JSON response containing the run results - */ - public function run(int $id): JSONResponse - { - try { - $endpoint = $this->endpointMapper->find(id: $id); - // Implement the logic to run the endpoint here - // This is a placeholder implementation - $runResult = ['status' => 'success', 'message' => 'Endpoint run successful']; - return new JSONResponse($runResult); - } catch (DoesNotExistException $exception) { - return new JSONResponse(data: ['error' => 'Endpoint not found'], statusCode: 404); - } + // Get the first matching endpoint since we already filtered by method + $endpoint = reset($matchingEndpoints); + + // Forward the request to the endpoint service + return $this->endpointService->handleRequest($endpoint, $this->request); } + } diff --git a/lib/Db/EndpointMapper.php b/lib/Db/EndpointMapper.php index b391fc53..8620ad36 100644 --- a/lib/Db/EndpointMapper.php +++ b/lib/Db/EndpointMapper.php @@ -9,6 +9,9 @@ use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; +/** + * Mapper class for handling Endpoint database operations + */ class EndpointMapper extends QBMapper { public function __construct(IDBConnection $db) @@ -101,4 +104,32 @@ public function getTotalCallCount(): int // Return the total count return (int)$row['count']; } + + /** + * Find endpoints that match a given path and method using regex comparison + * + * @param string $path The path to match against endpoint regex patterns + * @param string $method The HTTP method to filter by (GET, POST, etc) + * @return array Array of matching Endpoint entities + */ + public function findByPathRegex(string $path, string $method): array + { + // Get all endpoints first since we need to do regex comparison + $endpoints = $this->findAll(); + + // Filter endpoints where both path matches regex pattern and method matches + return array_filter($endpoints, function(Endpoint $endpoint) use ($path, $method) { + // Get the regex pattern from the endpoint + $pattern = $endpoint->getEndpointRegex(); + + // Skip if no regex pattern is set + if (empty($pattern)) { + return false; + } + + // Check if both path matches the regex pattern and method matches + return preg_match($pattern, $path) === 1 && + $endpoint->getMethod() === $method; + }); + } } diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php new file mode 100644 index 00000000..ba741333 --- /dev/null +++ b/lib/Service/EndpointService.php @@ -0,0 +1,134 @@ +getSchema() !== null) { + // Handle CRUD operations via ObjectService + return $this->handleSchemaRequest($endpoint, $request); + } + + // Check if endpoint connects to a source + if ($endpoint->getSource() !== null) { + // Proxy request to source via CallService + return $this->handleSourceRequest($endpoint, $request); + } + + // Invalid endpoint configuration + throw new \Exception('Endpoint must specify either a schema or source connection'); + + } catch (\Exception $e) { + $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); + return new \OCP\AppFramework\Http\JSONResponse( + ['error' => $e->getMessage()], + 400 + ); + } + } + + /** + * Handles requests for schema-based endpoints + * + * @param Endpoint $endpoint The endpoint configuration + * @param IRequest $request The incoming request + * @return \OCP\AppFramework\Http\JSONResponse + */ + private function handleSchemaRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + // Get request method + $method = $request->getMethod(); + + // Route to appropriate ObjectService method based on HTTP method + return match($method) { + 'GET' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->get($endpoint->getSchema(), $request->getParams()) + ), + 'POST' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->create($endpoint->getSchema(), $request->getParams()) + ), + 'PUT' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->update($endpoint->getSchema(), $request->getParams()) + ), + 'DELETE' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->delete($endpoint->getSchema(), $request->getParams()) + ), + default => throw new \Exception('Unsupported HTTP method') + }; + } + + /** + * Handles requests for source-based endpoints + * + * @param Endpoint $endpoint The endpoint configuration + * @param IRequest $request The incoming request + * @return \OCP\AppFramework\Http\JSONResponse + */ + private function handleSourceRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + // Proxy the request to the source via CallService + $response = $this->callService->call( + source: $endpoint->getSource(), + endpoint: $endpoint->getPath(), + method: $request->getMethod(), + config: [ + 'query' => $request->getParams(), + 'headers' => $request->getHeaders(), + 'body' => $request->getContent() + ] + ); + + return new \OCP\AppFramework\Http\JSONResponse( + $response->getResponse(), + $response->getStatusCode() + ); + } +} From b8f68957c3dbbdfcba1404edcae9bcc95219f67f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 15 Dec 2024 22:46:26 +0100 Subject: [PATCH 03/20] First real code on endpoints --- lib/Service/ConsumertService.php | 134 +++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 lib/Service/ConsumertService.php diff --git a/lib/Service/ConsumertService.php b/lib/Service/ConsumertService.php new file mode 100644 index 00000000..ba741333 --- /dev/null +++ b/lib/Service/ConsumertService.php @@ -0,0 +1,134 @@ +getSchema() !== null) { + // Handle CRUD operations via ObjectService + return $this->handleSchemaRequest($endpoint, $request); + } + + // Check if endpoint connects to a source + if ($endpoint->getSource() !== null) { + // Proxy request to source via CallService + return $this->handleSourceRequest($endpoint, $request); + } + + // Invalid endpoint configuration + throw new \Exception('Endpoint must specify either a schema or source connection'); + + } catch (\Exception $e) { + $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); + return new \OCP\AppFramework\Http\JSONResponse( + ['error' => $e->getMessage()], + 400 + ); + } + } + + /** + * Handles requests for schema-based endpoints + * + * @param Endpoint $endpoint The endpoint configuration + * @param IRequest $request The incoming request + * @return \OCP\AppFramework\Http\JSONResponse + */ + private function handleSchemaRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + // Get request method + $method = $request->getMethod(); + + // Route to appropriate ObjectService method based on HTTP method + return match($method) { + 'GET' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->get($endpoint->getSchema(), $request->getParams()) + ), + 'POST' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->create($endpoint->getSchema(), $request->getParams()) + ), + 'PUT' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->update($endpoint->getSchema(), $request->getParams()) + ), + 'DELETE' => new \OCP\AppFramework\Http\JSONResponse( + $this->objectService->delete($endpoint->getSchema(), $request->getParams()) + ), + default => throw new \Exception('Unsupported HTTP method') + }; + } + + /** + * Handles requests for source-based endpoints + * + * @param Endpoint $endpoint The endpoint configuration + * @param IRequest $request The incoming request + * @return \OCP\AppFramework\Http\JSONResponse + */ + private function handleSourceRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + // Proxy the request to the source via CallService + $response = $this->callService->call( + source: $endpoint->getSource(), + endpoint: $endpoint->getPath(), + method: $request->getMethod(), + config: [ + 'query' => $request->getParams(), + 'headers' => $request->getHeaders(), + 'body' => $request->getContent() + ] + ); + + return new \OCP\AppFramework\Http\JSONResponse( + $response->getResponse(), + $response->getStatusCode() + ); + } +} From b624417633afec5305823ca68209b11872297746 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Tue, 17 Dec 2024 16:44:58 +0100 Subject: [PATCH 04/20] Working get endpoints --- appinfo/routes.php | 9 +- lib/Controller/EndpointsController.php | 13 +- lib/Db/Endpoint.php | 53 ++++--- lib/Db/EndpointMapper.php | 21 ++- lib/Service/ConsumertService.php | 134 ---------------- lib/Service/EndpointService.php | 208 ++++++++++++++++++++++--- lib/Service/ObjectService.php | 11 ++ 7 files changed, 249 insertions(+), 200 deletions(-) delete mode 100644 lib/Service/ConsumertService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 8a4aed1b..e8ee2bf2 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -7,7 +7,6 @@ 'Mappings' => ['url' => 'api/mappings'], 'Jobs' => ['url' => 'api/jobs'], 'Synchronizations' => ['url' => 'api/synchronizations'], - 'Endpoints' => ['url' => 'api/endpoints'], 'Consumers' => ['url' => 'api/consumers'], ], 'routes' => [ @@ -26,9 +25,9 @@ ['name' => 'mappings#saveObject', 'url' => '/api/mappings/objects', 'verb' => 'POST'], ['name' => 'mappings#getObjects', 'url' => '/api/mappings/objects', 'verb' => 'GET'], // Running endpoints - allow any path after /api/endpoints/ - ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'GET', 'requirements' => ['path' => '.+']], - ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+']], - ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+']], - ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+']], + ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{_path}', 'verb' => 'GET', 'requirements' => ['_path' => '.+']], +// ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'PUT', 'requirements' => ['path' => '.+']], +// ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'POST', 'requirements' => ['path' => '.+']], +// ['name' => 'endpoints#handlePath', 'url' => '/api/endpoint/{path}', 'verb' => 'DELETE', 'requirements' => ['path' => '.+']], ], ]; diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index 05f4a09f..bd0b6804 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -70,6 +70,7 @@ public function page(): TemplateResponse */ public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { + $filters = $this->request->getParams(); $fieldsToSearch = ['name', 'description', 'endpoint']; @@ -178,28 +179,28 @@ public function destroy(int $id): JSONResponse /** * Handles generic path requests by matching against registered endpoints - * + * * This method checks if the current path matches any registered endpoint patterns * and forwards the request to the appropriate endpoint service if found * * @NoAdminRequired * @NoCSRFRequired - * + * * @param string $path The request path to match * @return JSONResponse The response from the endpoint service or 404 if no match */ - public function handlePath(string $path): JSONResponse + public function handlePath(string $_path): JSONResponse { // Find matching endpoints for the given path and method $matchingEndpoints = $this->endpointMapper->findByPathRegex( - path: $path, + path: $_path, method: $this->request->getMethod() ); // If no matching endpoints found, return 404 if (empty($matchingEndpoints)) { return new JSONResponse( - data: ['error' => 'No matching endpoint found for path and method: ' . $path . ' ' . $this->request->getMethod()], + data: ['error' => 'No matching endpoint found for path and method: ' . $_path . ' ' . $this->request->getMethod()], statusCode: 404 ); } @@ -208,7 +209,7 @@ public function handlePath(string $path): JSONResponse $endpoint = reset($matchingEndpoints); // Forward the request to the endpoint service - return $this->endpointService->handleRequest($endpoint, $this->request); + return $this->endpointService->handleRequest($endpoint, $this->request, $_path); } } diff --git a/lib/Db/Endpoint.php b/lib/Db/Endpoint.php index a4cc78d5..8333009f 100644 --- a/lib/Db/Endpoint.php +++ b/lib/Db/Endpoint.php @@ -8,34 +8,37 @@ class Endpoint extends Entity implements JsonSerializable { - protected ?string $uuid = null; - protected ?string $name = null; // The name of the endpoint - protected ?string $description = null; // The description of the endpoint - protected ?string $reference = null; // The reference of the endpoint - protected ?string $version = '0.0.0'; // The version of the endpoint - protected ?string $endpoint = null; // The actual endpoint e.g /api/buildings/{{id}}. An endpoint may contain parameters e.g {{id}} - protected ?array $endpointArray = null; // An array representation of the endpoint. Automatically generated - protected ?string $endpointRegex = null; // A regex representation of the endpoint. Automatically generated - protected ?string $method = null; // One of GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD. method and endpoint combination should be unique - protected ?string $targetType = null; // The target to attach this endpoint to, should be one of source (to create a proxy endpoint) or register/schema (to create an object endpoint) or job (to fire an event) or synchronization (to create a synchronization endpoint) - protected ?string $targetId = null; // The target id to attach this endpoint to + protected ?string $uuid = null; + protected ?string $name = null; // The name of the endpoint + protected ?string $description = null; // The description of the endpoint + protected ?string $reference = null; // The reference of the endpoint + protected ?string $version = '0.0.0'; // The version of the endpoint + protected ?string $endpoint = null; // The actual endpoint e.g /api/buildings/{{id}}. An endpoint may contain parameters e.g {{id}} + protected ?array $endpointArray = null; // An array representation of the endpoint. Automatically generated + protected ?string $endpointRegex = null; // A regex representation of the endpoint. Automatically generated + protected ?string $method = null; // One of GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD. method and endpoint combination should be unique + protected ?string $targetType = null; // The target to attach this endpoint to, should be one of source (to create a proxy endpoint) or register/schema (to create an object endpoint) or job (to fire an event) or synchronization (to create a synchronization endpoint) + protected ?string $targetId = null; // The target id to attach this endpoint to protected ?DateTime $created = null; protected ?DateTime $updated = null; public function __construct() { - $this->addType('uuid', 'string'); - $this->addType('name', 'string'); - $this->addType('description', 'string'); - $this->addType('reference', 'string'); - $this->addType('version', 'string'); - $this->addType('endpoint', 'string'); - $this->addType('endpointArray', 'json'); - $this->addType('endpointRegex', 'string'); - $this->addType('method', 'string'); - $this->addType('targetType', 'string'); - $this->addType('targetId', 'string'); - $this->addType('created', 'datetime'); - $this->addType('updated', 'datetime'); + $this->addType(fieldName:'uuid', type: 'string'); + $this->addType(fieldName:'name', type: 'string'); + $this->addType(fieldName:'description', type: 'string'); + $this->addType(fieldName:'reference', type: 'string'); + $this->addType(fieldName:'version', type: 'string'); + $this->addType(fieldName:'endpoint', type: 'string'); + $this->addType(fieldName:'endpointArray', type: 'json'); + $this->addType(fieldName:'endpointRegex', type: 'string'); + $this->addType(fieldName:'method', type: 'string'); + $this->addType(fieldName:'targetType', type: 'string'); + $this->addType(fieldName:'targetId', type: 'string'); + $this->addType(fieldName:'schema', type: 'int'); + $this->addType(fieldName:'register', type: 'int'); + $this->addType(fieldName:'source', type: 'int'); + $this->addType(fieldName:'created', type: 'datetime'); + $this->addType(fieldName:'updated', type: 'datetime'); } public function getJsonFields(): array @@ -85,7 +88,7 @@ public function jsonSerialize(): array 'targetId' => $this->targetId, 'created' => isset($this->created) ? $this->created->format('c') : null, 'updated' => isset($this->updated) ? $this->updated->format('c') : null, - + ]; } } diff --git a/lib/Db/EndpointMapper.php b/lib/Db/EndpointMapper.php index 8620ad36..0479b765 100644 --- a/lib/Db/EndpointMapper.php +++ b/lib/Db/EndpointMapper.php @@ -61,6 +61,10 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters return $this->findEntities(query: $qb); } + private function createEndpointRegex(string $endpoint) { + return '#'.preg_replace(pattern: ['#\/{{([^}}]+)}}\/#', '#\/{{([^}}]+)}}$#'], replacement: ['/([^/]+)/', '(/([^/]+))?'], subject: $endpoint).'#'; + } + public function createFromArray(array $object): Endpoint { $obj = new Endpoint(); @@ -69,6 +73,10 @@ public function createFromArray(array $object): Endpoint if ($obj->getUuid() === null){ $obj->setUuid(Uuid::v4()); } + + $obj->setEndpointRegex($this->createEndpointRegex($obj->getEndpoint())); + $obj->setEndpointArray(explode('/', $obj->getEndpoint())); + return $this->insert(entity: $obj); } @@ -82,6 +90,9 @@ public function updateFromArray(int $id, array $object): Endpoint $version[2] = (int)$version[2] + 1; $obj->setVersion(implode('.', $version)); + $obj->setEndpointRegex($this->createEndpointRegex($obj->getEndpoint())); + $obj->setEndpointArray(explode('/', $obj->getEndpoint())); + return $this->update($obj); } @@ -112,23 +123,23 @@ public function getTotalCallCount(): int * @param string $method The HTTP method to filter by (GET, POST, etc) * @return array Array of matching Endpoint entities */ - public function findByPathRegex(string $path, string $method): array + public function findByPathRegex(string $path, string $method): array { // Get all endpoints first since we need to do regex comparison $endpoints = $this->findAll(); - + // Filter endpoints where both path matches regex pattern and method matches return array_filter($endpoints, function(Endpoint $endpoint) use ($path, $method) { // Get the regex pattern from the endpoint $pattern = $endpoint->getEndpointRegex(); - + // Skip if no regex pattern is set if (empty($pattern)) { return false; } - + // Check if both path matches the regex pattern and method matches - return preg_match($pattern, $path) === 1 && + return preg_match($pattern, $path) === 1 && $endpoint->getMethod() === $method; }); } diff --git a/lib/Service/ConsumertService.php b/lib/Service/ConsumertService.php deleted file mode 100644 index ba741333..00000000 --- a/lib/Service/ConsumertService.php +++ /dev/null @@ -1,134 +0,0 @@ -getSchema() !== null) { - // Handle CRUD operations via ObjectService - return $this->handleSchemaRequest($endpoint, $request); - } - - // Check if endpoint connects to a source - if ($endpoint->getSource() !== null) { - // Proxy request to source via CallService - return $this->handleSourceRequest($endpoint, $request); - } - - // Invalid endpoint configuration - throw new \Exception('Endpoint must specify either a schema or source connection'); - - } catch (\Exception $e) { - $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); - return new \OCP\AppFramework\Http\JSONResponse( - ['error' => $e->getMessage()], - 400 - ); - } - } - - /** - * Handles requests for schema-based endpoints - * - * @param Endpoint $endpoint The endpoint configuration - * @param IRequest $request The incoming request - * @return \OCP\AppFramework\Http\JSONResponse - */ - private function handleSchemaRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { - // Get request method - $method = $request->getMethod(); - - // Route to appropriate ObjectService method based on HTTP method - return match($method) { - 'GET' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->get($endpoint->getSchema(), $request->getParams()) - ), - 'POST' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->create($endpoint->getSchema(), $request->getParams()) - ), - 'PUT' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->update($endpoint->getSchema(), $request->getParams()) - ), - 'DELETE' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->delete($endpoint->getSchema(), $request->getParams()) - ), - default => throw new \Exception('Unsupported HTTP method') - }; - } - - /** - * Handles requests for source-based endpoints - * - * @param Endpoint $endpoint The endpoint configuration - * @param IRequest $request The incoming request - * @return \OCP\AppFramework\Http\JSONResponse - */ - private function handleSourceRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { - // Proxy the request to the source via CallService - $response = $this->callService->call( - source: $endpoint->getSource(), - endpoint: $endpoint->getPath(), - method: $request->getMethod(), - config: [ - 'query' => $request->getParams(), - 'headers' => $request->getHeaders(), - 'body' => $request->getContent() - ] - ); - - return new \OCP\AppFramework\Http\JSONResponse( - $response->getResponse(), - $response->getStatusCode() - ); - } -} diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index ba741333..be3a974a 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -16,8 +16,13 @@ use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\ServerException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; +use OCP\IURLGenerator; use Psr\Log\LoggerInterface; +use ValueError; /** * Service class for handling endpoint requests @@ -37,7 +42,8 @@ class EndpointService { public function __construct( private readonly ObjectService $objectService, private readonly CallService $callService, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly IURLGenerator $urlGenerator, ) {} /** @@ -48,19 +54,19 @@ public function __construct( * * @param Endpoint $endpoint The endpoint configuration to handle * @param IRequest $request The incoming request object - * @return \OCP\AppFramework\Http\JSONResponse Response containing the result + * @return JSONResponse Response containing the result * @throws \Exception When endpoint configuration is invalid */ - public function handleRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + public function handleRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse { try { // Check if endpoint connects to a schema - if ($endpoint->getSchema() !== null) { + if ($endpoint->getTargetType() === 'register/schema') { // Handle CRUD operations via ObjectService - return $this->handleSchemaRequest($endpoint, $request); + return $this->handleSchemaRequest($endpoint, $request, $path); } - + // Check if endpoint connects to a source - if ($endpoint->getSource() !== null) { + if ($endpoint->getTargetType() === 'api') { // Proxy request to source via CallService return $this->handleSourceRequest($endpoint, $request); } @@ -70,50 +76,202 @@ public function handleRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFr } catch (\Exception $e) { $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); - return new \OCP\AppFramework\Http\JSONResponse( + return new JSONResponse( ['error' => $e->getMessage()], 400 ); } } + private function getPathParameters(array $endpointArray, string $path): array + { + $pathParts = explode(separator: '/', string: $path); + + $endpointArrayNormalized = array_map( + function($item) { + return str_replace( + search: ['{{', '{{ ', '}}', '}}'], + replace: '', + subject: $item + ); + }, + $endpointArray); + + try { + $pathParams = array_combine( + keys: $endpointArrayNormalized, + values: $pathParts + ); + } catch (ValueError $error) { + array_pop($endpointArrayNormalized); + $pathParams = array_combine( + keys: $endpointArrayNormalized, + values: $pathParts + ); + } + + return $pathParams; + } + + private function getObjects( + \OCA\OpenRegister\Service\ObjectService|QBMapper $mapper, + array $parameters, + array $pathParams, + int &$status = 200 + ): Entity|array + { + if(isset($pathParams['id']) === true && $pathParams['id'] === end($pathParams)) { + return $mapper->find($pathParams['id']); + } else if (isset($pathParams['id']) === true) { + while(prev($pathParams) !== $pathParams['id']){}; + + $property = next($pathParams); + + if(next($pathParams) !== false) { + $id = pos($pathParams); + } + + $main = $mapper->find($pathParams['id'])->getObject(); + $ids = $main[$property]; + + if(isset($id) === true && in_array(needle: $id, haystack: $ids) === true) { + + return $mapper->findSubObjects([$id], $property)[0]; + } else if (isset($id) === true) { + $status = 404; + return ['error' => 'not found', 'message' => "the subobject with id $id does not exist"]; + + } + + return $mapper->findSubObjects($ids, $property); + } + + $result = $mapper->findAllPaginated(requestParams: $parameters); + + $returnArray = [ + 'count' => $result['total'], + ]; + + if($result['page'] < $result['pages']) { + $parameters['page'] = $result['page'] + 1; + $parameters['_path'] = implode('/', $pathParams); + + + $returnArray['next'] = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute( + routeName: 'openconnector.endpoints.handlepath', + arguments: $parameters + ) + ); + } + if($result['page'] > 1) { + $parameters['page'] = $result['page'] - 1; + $parameters['_path'] = implode('/', $pathParams); + + $returnArray['previous'] = $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute( + routeName: 'openconnector.endpoints.handlepath', + arguments: $parameters + ) + ); + } + + $returnArray['results'] = $result['results']; + + return $returnArray; + } + /** * Handles requests for schema-based endpoints - * + * * @param Endpoint $endpoint The endpoint configuration * @param IRequest $request The incoming request - * @return \OCP\AppFramework\Http\JSONResponse + * @return JSONResponse */ - private function handleSchemaRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + private function handleSchemaRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse { // Get request method $method = $request->getMethod(); - + $target = explode('/', $endpoint->getTargetId()); + + $register = $target[0]; + $schema = $target[1]; + + $mapper = $this->objectService->getMapper(schema: $schema, register: $register); + + $parameters = $request->getParams(); + + $pathParams = $this->getPathParameters($endpoint->getEndpointArray(), $path); + + unset($parameters['_route'], $parameters['_path']); + + $status = 200; + // Route to appropriate ObjectService method based on HTTP method return match($method) { - 'GET' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->get($endpoint->getSchema(), $request->getParams()) + 'GET' => new JSONResponse( + $this->getObjects(mapper: $mapper, parameters: $parameters, pathParams: $pathParams, status: $status), statusCode: $status ), - 'POST' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->create($endpoint->getSchema(), $request->getParams()) + 'POST' => new JSONResponse( + $mapper->createFromArray(object: $parameters) ), - 'PUT' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->update($endpoint->getSchema(), $request->getParams()) + 'PUT' => new JSONResponse( + $mapper->updateFromArray($request->getParams()['id'], $request->getParams(), true, true) ), - 'DELETE' => new \OCP\AppFramework\Http\JSONResponse( - $this->objectService->delete($endpoint->getSchema(), $request->getParams()) + 'DELETE' => new JSONResponse( + $mapper->delete($request->getParams()) ), default => throw new \Exception('Unsupported HTTP method') }; } + private function getRawContent(): string + { + return file_get_contents(filename: 'php://input'); + } + + private function getHeaders(array $server, bool $proxyHeaders = false): array + { + $headers = array_filter( + array: $server, + callback: function (string $key) use ($proxyHeaders){ + if(str_starts_with($key, 'HTTP_') === false) { + return false; + } else if ($proxyHeaders === false + && (str_starts_with(haystack: $key, needle: 'HTTP_X_FORWARDED') + || $key === 'HTTP_X_REAL_IP' || $key === 'HTTP_X_ORIGINAL_URI' + ) + ) { + return false; + } + + return true; + }, + mode: ARRAY_FILTER_USE_KEY + ); + + $keys = array_keys($headers); + + return array_combine( + array_map( + callback: function($key) { + return strtolower(string: substr(string: $key, offset: 5)); + }, + array: $keys), + $headers + ); + } + /** * Handles requests for source-based endpoints * * @param Endpoint $endpoint The endpoint configuration * @param IRequest $request The incoming request - * @return \OCP\AppFramework\Http\JSONResponse + * @return JSONResponse */ - private function handleSourceRequest(Endpoint $endpoint, IRequest $request): \OCP\AppFramework\Http\JSONResponse { + private function handleSourceRequest(Endpoint $endpoint, IRequest $request): JSONResponse { + + $headers = $this->getHeaders($request->server); + // Proxy the request to the source via CallService $response = $this->callService->call( source: $endpoint->getSource(), @@ -121,12 +279,12 @@ private function handleSourceRequest(Endpoint $endpoint, IRequest $request): \OC method: $request->getMethod(), config: [ 'query' => $request->getParams(), - 'headers' => $request->getHeaders(), - 'body' => $request->getContent() + 'headers' => $headers, + 'body' => $this->getRawContent(), ] ); - return new \OCP\AppFramework\Http\JSONResponse( + return new JSONResponse( $response->getResponse(), $response->getStatusCode() ); diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index eb5f2c00..692e76ae 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -8,6 +8,7 @@ use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\GuzzleException; use OCP\App\IAppManager; +use OCP\AppFramework\Db\QBMapper; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -251,4 +252,14 @@ public function getOpenRegisters(): ?\OCA\OpenRegister\Service\ObjectService return null; } + public function getMapper(?string $objecttype = null, ?int $schema = null, ?int $register = null): QBMapper|\OCA\OpenRegister\Service\ObjectService|null + { + if($register !== null && $schema !== null && $objecttype === null) { + return $this->getOpenRegisters()->getMapper(register: $register, schema: $schema); + } + + return null; + + } + } From 0cc81ad95f20e7a05a7351ec76a1c3088d0115ae Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Tue, 17 Dec 2024 17:44:04 +0100 Subject: [PATCH 05/20] Mappings and config added for xxllnc v1 -> zgw --- .../xxllnc-suite-to-publications copy.json | 26 +++++++++++ .../mappings/xxllnc-v1-case-to-zgw-zaak.json | 43 +++++++++++++++++++ .../xxllnc-v1-casetype-to-zgw-zaaktype.json | 38 ++++++++++++++++ ...-document-to-zgw-zaakinformatieobject.json | 28 ++++++++++++ .../xxllnc-v1-milestone-to-zgw-status.json | 15 +++++++ .../xxllnc-v1-outcome-to-zgw-resultaat.json | 11 +++++ .../xxllnc-v1-rolerequestor-to-zgw-rol.json | 34 +++++++++++++++ ...xxllnc-v1-value-to-zgw-zaakeigenschap.json | 12 ++++++ .../xxllnc-zgw/sources/xxllnc-v1.json | 7 +++ .../xxllnc-suite-to-publications copy.json | 25 +++++++++++ 10 files changed, 239 insertions(+) create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json create mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-v1-value-to-zgw-zaakeigenschap.json create mode 100644 configurations/xxllnc-zgw/sources/xxllnc-v1.json create mode 100644 configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json b/configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json new file mode 100644 index 00000000..57ab360a --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json @@ -0,0 +1,26 @@ +{ + "name": "Xxllnc suite to Publication", + "version": "0.0.1", + "mapping": { + "title": "omschrijving", + "summary": "zaaktypeomschrijving", + "description": "zaaktypeomschrijving", + "category": "{% if zaaktypecode|default %}{% set wooVerzoekenEnBesluiten = ['LP00000431', 'B1873'] %}{% set klachtoordelen = ['LP00000091', 'LP00001132', 'LP00000121', 'B0757', 'LP00000832', 'LP00001096'] %}{% if zaaktypecode in wooVerzoekenEnBesluiten %}{{ 'Woo-verzoeken en -besluiten' }}{% elseif zaaktypecode in klachtoordelen %}{{ 'Klachtoordelen' }}{% endif %}{% endif %}", + "published": "startdatum", + "modified": "{{ 'now'|date('H:i:sTm-d-Y') }}", + "attachments": "[{% if files|default %}{% for file in files %} { {% if file['titel']|default %}\"title\": \"{{ file['titel'] }}\",{% endif %}\"labels\": [\"{{ 'Informatieverzoek' }}\"],{% if file['formaat']|default %}\"extension\": \"{{ file['formaat']|split('/')|last }}\",\"type\": \"{{ file['formaat'] }}\",{% endif %}{% if file['inhoud']|default and file['formaat']|default %}\"accessUrl\": \"data:{{ file['formaat'] }};base64,{{ file.inhoud }}\"{% endif %} }{{ loop.last ? '' : ',' }} {% endfor %}{% endif %}]", + "status": "Concept" + }, + "unset": [ + "" + ], + "cast": { + "title": "unsetIfValue==omschrijving", + "summary": "unsetIfValue==zaaktypeomschrijving", + "description": "unsetIfValue==zaaktypeomschrijving", + "category": "unsetIfValue==", + "published": "unsetIfValue==startdatum", + "attachments": "jsonToArray" + }, + "passThrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json new file mode 100644 index 00000000..4f286d30 --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json @@ -0,0 +1,43 @@ +{ + "name": "Xxllnc Case to ZGW Zaak", + "version": "0.0.1", + "mapping": { + "identificatie": "reference", + "omschrijving": "instance.subject", + "toelichting": "instance.subject_external", + "registratiedatum": "instance.date_of_registration", + "startdatum": "instance.date_of_registration", + "einddatum": "instance.date_of_completion", + "einddatumGepland": "instance.date_target", + "publicatiedatum": "instance.date_target", + "communicatiekanaal": "instance.channel_of_contact", + "vertrouwelijkheidaanduidng": "instance.confidentiality.mapped", + "kenmerken.0.kenmerk": "reference", + "kenmerken.0.bron": "xxllnc v1 api", + "betalingsindicatie": "{% if instance.price == '-' %}{{ 'nvt' }}{% elseif instance.payment_status is null or instance.payment_status == '' %}{{ 'nog_niet' }}{% else %}{{ 'geheel' }}{% endif %}", + "betalingsindicatieWeergave": "{% if instance.price == '-' %}{{ 'Zaak kent geen prijs' }}{% elseif instance.payment_status is null or instance.payment_status == '' %}{{ 'Er moet nog betaald worden' }}{% else %}{{ 'De volledige kosten zijn betaald' }}{% endif %}", + "resultaattoelichting": "{% if instance.outcome.instance.name|default %}{{ instance.outcome.instance.name }}{% endif %}", + "resultaat": "{% if instance.outcome|default %}{% set data = {'outcome': instance.outcome, 'resultaattypen': zaaktype.resultaattypen} %}{{ executeMapping(2, data)|json_encode }}{% endif %}", + "rollen": "[{% if instance.route.instance.role|default %}{% set dataRol = {'requestor': instance.requestor, 'role': instance.route.instance.role, 'roltypen': zaaktype.roltypen} %}{{ executeMapping(3, dataRol)|json_encode }}{% endif %}]", + "status": "{% if instance.milestone|default %}{% set data = {'milestone': instance.milestone, 'statustypen': zaaktype.statustypen} %}{{ executeMapping(4, data)|json_encode }}{% endif %}", + "zaaktype": "{% if zaaktype['_self']['id']|default %}{{ zaaktype['_self']['id'] }}{% endif %}", + "eigenschappen": "[{% set index=0 %}{% if zaaktype.eigenschappen|default %}{% for key, attribute in instance.attributes %}{% if index != 0 %},{% endif %}{% set data = {'name': key, 'value': attribute, 'eigenschappen': zaaktype.eigenschappen} %}{% if attribute[0][0]|default and attribute[0][0] is iterable %}{{ attribute|json_encode }}{% else %}{{ executeMapping(5, data)|json_encode }}{% endif %}{% set index=index+1 %}{% endfor %}{% endif %}]", + "zaakinformatieobjecten": "[{% set index=0 %}{% if documents|default %}{% for document in documents %}{% if index != 0 %},{% endif %}{{ executeMapping(6, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", + "bronorganisatie": "bronorganisatie", + "verantwoordelijkeOrganisatie": "bronorganisatie", + "opschorting.indicatie": "{% if instance.stalled_until|default %}true{% else %}{% endif %}", + "opschorting.reden": "{% if instance.stalled_until|default %}Unknown{% endif %}" + }, + "unset": [], + "cast": { + "resultaat": "jsonToArray", + "rollen": "jsonToArray", + "status": "jsonToArray", + "eigenschappen": "jsonToArray", + "opschorting.indicatie": "unsetIfValue==", + "opschorting.reden": "unsetIfValue==", + "opschorting": "unsetIfValue==", + "zaakinformatieobjecten": "jsonToArray" + }, + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json new file mode 100644 index 00000000..0a58fb86 --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json @@ -0,0 +1,38 @@ +{ + "name": "Xxllnc Casetype to ZGW ZaakType", + "version": "0.0.1", + "mapping": { + "identificatie": "instance.legacy.zaaktype_id", + "catalogus": "_catalogus", + "onderwerp": "instance.title", + "indicatieInternOfExtern": "extern", + "doorlooptijd": "{% if instance.properties.lead_time_legal.weken|default %}P{{ instance.properties.lead_time_legal.weken }}W{% elseif instance.properties.lead_time_legal.kalenderdagen|default %}P{{ instance.properties.lead_time_legal.kalenderdagen }}D{% else %}{{ \"\" }}{% endif %}", + "servicenorm": "{% if instance.properties.lead_time_service.weken|default %}P{{ instance.properties.lead_time_service.weken }}W{% elseif instance.properties.lead_time_service.kalenderdagen|default %}P{{ instance.properties.lead_time_service.kalenderdagen }}D{% else %}{{ \"\" }}{% endif %}", + "vertrouwelijkheidaanduiding": "{{ instance.properties.designation_of_confidentiality|lower }}", + "verlengingMogelijk": "instance.properties.extension", + "publicatieIndicatie": "instance.properties.publication", + "omschrijving": "instance.title", + "opschortingEnAanhoudingMogelijk": "instance.properties.suspension", + "statustypen": "[{% for phase in instance.phases %}{% set phase = phase|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if not loop.first %},{% endif %}{{ executeMapping(7, phase)|json_encode }}{% endfor %}]", + "informatieobjecttypen": "[{% set index=0 %}{% for phase in instance.phases %}{% for field in phase.fields %}{% if field.type == 'file' %}{% set field = field|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if index != 0 %},{% endif %}{{ executMapping(8, field)|json_encode }}{% set index=index+1 %}{% endif %}{% endfor %}{% endfor %}]", + "eigenschappen": "[{% set index=0 %}{% for phase in instance.phases %}{% for field in phase.fields %}{% if field.type != 'file' %}{% set field = field|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if index != 0 %},{% endif %}{{ executMapping(9, field)|json_encode }}{% set index=index+1 %}{% endif %}{% endfor %}{% endfor %}]", + "roltypen": "[{% set index=0 %}{% for phase in instance.phases %}{% if phase.route.role|default %}{% set role = phase.route.role|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if index != 0 %},{% endif %}{{ executMapping(10, role)|json_encode }}{% set index=index+1 %}{% endif %}{% endfor %}]", + "resultaattypen": "[{% if instance.results|default %}{% for result in instance.results %}{% set result = result|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if not loop.first %},{% endif %}{{ executMapping(11, result)|json_encode }}{% endfor %}{% endif %}]", + "bronzaaktype.url": "url", + "bronzaaktype.identificatie": "reference", + "bronzaaktype.omschrijving": "instance.title", + "referentieproces.naam": "preview", + "verantwoordelijke": "instance.properties.supervisor" + }, + "unset": { + "identificatie": "unsetIfValue==instance.legacy.zaaktype_id" + }, + "cast": { + "statustypen": "jsonToArray", + "informatieobjecttypen": "jsonToArray", + "eigenschappen": "jsonToArray", + "roltypen": "jsonToArray", + "resultaattypen": "jsonToArray" + }, + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json new file mode 100644 index 00000000..63618a12 --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json @@ -0,0 +1,28 @@ +{ + "name": "Xxllnc Document to ZGW ZaakInformatieObject", + "version": "0.0.1", + "mapping": { + "titel": "result.instance.name", + "registratiedatum": "result.instance.file.instance.date_created", + "informatieobject.identificatie": "result.instance.number", + "informatieobject.creatiedatum": "result.instance.file.instance.date_created", + "informatieobject.titel": "result.instance.name", + "informatieobject.vertrouwelijksheidaanduiding": "{% if result.instance.metadata.instance.trust_level|default %}{{ result.instance.metadata.instance.trust_level|lower }}{% endif %}", + "informatieobject.formaat": "result.instance.file.instance.mimetype", + "informatieobject.versie": "1", + "informatieobject.beginRegistratie": "result.instance.file.instance.date_modified", + "informatieobject.bestandsnaam": "result.instance.filename", + "informatieobject.bestandsomvang": "result.instance.file.instance.size", + "informatieobject.verschijningsvorm": "{% if result.instance.metadata.instance.category|default %}{{ result.instance.metadata.instance.category }}{% endif %}", + "informatieobject.integriteit.algoritme": "{% if result.instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ algoritme }}{% endif %}{% endfor %}{% endif %}", + "informatieobject.integriteit.waarde": "{% if result.instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ result.instance.file.instance[algoritme] }}{% endif %}{% endfor %}{% endif %}", + "informatieobject.integriteit.datum": "result.instance.file.instance.date_created", + "informatieobject.inhoud": "inhoud" + }, + "unset": [], + "cast": { + "informatieobject.versie": "integer", + "informatieobject.bestandsomvang": "integer" + }, + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json new file mode 100644 index 00000000..b7409599 --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json @@ -0,0 +1,15 @@ +{ + "name": "Xxllnc Milestone to ZGW Status", + "version": "0.0.1", + "mapping": { + "datumStatusGezet": "milestone.instance.date_modified", + "statustoelichting": "milestone.instance.phase_label", + "indicatieLaatstGezetteStatus": "{{ true }}", + "statustype": "{% for statustype in statustypen %}{% if statustype.omschrijving == milestone.instance.phase_label %}{{ statustype['_self']['id'] }}{% endif %}{% endfor %}" + }, + "unset": [], + "cast": { + "indicatieLaatstGezetteStatus": "bool" + }, + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json new file mode 100644 index 00000000..4ff8c3ec --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json @@ -0,0 +1,11 @@ +{ + "name": "Xxllnc Outcome to ZGW Resultaat", + "version": "0.0.1", + "mapping": { + "toelichting": "{% if outcome.instance.name|default %}{{ outcome.instance.name }}{% endif %}", + "resultaattype": "{% if resultaattypen|default %}{% for resultaattype in resultaattypen %}{% if resultaattype.toelichting == outcome.instance.name %}{{ resultaattype['_self']['id'] }}{% endif %}{% endfor %}{% endif %}" + }, + "unset": [], + "cast": [], + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json new file mode 100644 index 00000000..5910b347 --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json @@ -0,0 +1,34 @@ +{ + "name": "Xxllnc RoleRequestor to ZGW Rol", + "version": "0.0.1", + "mapping": { + "betrokkeneType": "{% if role.instance.name|default %}{% if role.instance.name == 'Behandelaar'|lower %}medewerker{% else %}medewerker{% endif %}{% endif %}", + "omschrijving": "{% if role.instance.name|default %}{{ role.instance.name }}{% endif %}", + "omschrijvingGeneriek": "{% if role.instance.description|default %}{{ role.instance.description }}{% endif %}", + "roltoelichting": "{% if role.instance.name|default %}{{ role.instance.name }}{% endif %}", + "registratiedatum": "{% if role.instance.date_created|default %}{{ role.instance.date_created }}{% endif %}", + "contactpersoonRol.emailadres": "{% if requestor.instance.subject.instance.email_adress|default %}{{ requestor.instance.subject.instance.email_adress }}{% endif %}", + "contactpersoonRol.functie": "{% if requestor.instance.subject.instance.positions.instance.rows|default %}{% for position in requestor.instance.subject.instance.positions.instance.rows %}{% if role.preview|default %}{{ role.preview }}{% endif %}{% endfor %}{% endif %}", + "contactpersoonRol.telefoonnummer": "{% if requestor.instance.subject.instance.phone_number|default %}{{ requestor.instance.subject.instance.phone_number }}{% endif %}", + "contactpersoonRol.naam": "{% if requestor.instance.subject.instance.firstname|default %}{{ requestor.instance.subject.instance.firstname }} {% endif %}{% if requestor.instance.subject.instance.surname|default %}{{ requestor.instance.subject.instance.surname }}{% endif %}", + "betrokkeneIdentificatie._sourceId": "{% if requestor.reference|default %}{{ requestor.reference }}{% endif %}", + "betrokkeneIdentificatie.inpBsn": "{% if requestor.instance.subject.instance.personal_number|default %}{{ requestor.instance.subject.instance.personal_number }}{% endif %}", + "betrokkeneIdentificatie.inpA_nummer": "{% if requestor.instance.subject.instance.personal_number_a|default %}{{ requestor.instance.subject.instance.personal_number_a }}{% endif %}", + "betrokkeneIdentificatie.geslachtsnaam": "{% if requestor.instance.subject.instance.gender|default %}{{ requestor.instance.subject.instance.gender }}{% endif %}", + "betrokkeneIdentificatie.voorvoegselGeslachtsnaam": "{% if requestor.instance.subject.instance.prefix|default %}{{ requestor.instance.subject.instance.prefix }}{% endif %}", + "betrokkeneIdentificatie.voorletters": "{% if requestor.instance.subject.instance.initials|default %}{{ requestor.instance.subject.instance.initials }}{% endif %}", + "betrokkeneIdentificatie.voornamen": "{% if requestor.instance.subject.instance.first_names|default %}{{ requestor.instance.subject.instance.first_names }}{% endif %}", + "betrokkeneIdentificatie.geslachtsaanduiding": "{% if requestor.instance.subject.instance.geslachtsnaam|default %}{{ requestor.instance.subject.instance.geslachtsnaam }}{% endif %}", + "betrokkeneIdentificatie.geboortedatum": "{% if requestor.instance.subject.instance.date_of_birth|default %}{{ requestor.instance.subject.instance.date_of_birth }}{% endif %}", + "betrokkeneIdentificatie.achternaam": "{% if requestor.instance.subject.instance.surname|default %}{{ requestor.instance.subject.instance.surname }}{% endif %}", + "betrokkeneIdentificatie.verblijfsadres.wplWoonplaatsNaam": "{% if requestor.instance.subject.instance.address_residence.instance.city|default %}{{ requestor.instance.subject.instance.address_residence.instance.city }}{% endif %}", + "betrokkeneIdentificatie.verblijfsadres.aoaPostcode": "{% if requestor.instance.subject.instance.address_residence.instance.zipcode|default %}{{ requestor.instance.subject.instance.address_residence.instance.zipcode }}{% endif %}", + "betrokkeneIdentificatie.verblijfsadres.aoaHuisnummer": "{% if requestor.instance.subject.instance.address_residence.instance.street_number|default %}{{ requestor.instance.subject.instance.address_residence.instance.street_number }}{% endif %}", + "betrokkeneIdentificatie.verblijfsadres.aoaHuisletter": "{% if requestor.instance.subject.instance.address_residence.instance.street_number_letter|default %}{{ requestor.instance.subject.instance.address_residence.instance.street_number_letter }}{% endif %}", + "betrokkeneIdentificatie.verblijfsadres.aoaHuisnummertoevoeging": "{% if requestor.instance.subject.instance.address_residence.instance.street_number_suffix|default %}{{ requestor.instance.subject.instance.address_residence.instance.street_number_suffix }}{% endif %}", + "roltype": "{% if role.instance.name|default %}{% for roltype in roltypen %}{% if roltype.omschrijvingGeneriek|lower == role.instance.name|lower %}{{ roltype['_self']['id'] }}{% endif %}{% endfor %}{% endif %}" + }, + "unset": [], + "cast": [], + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-value-to-zgw-zaakeigenschap.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-value-to-zgw-zaakeigenschap.json new file mode 100644 index 00000000..88828190 --- /dev/null +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-value-to-zgw-zaakeigenschap.json @@ -0,0 +1,12 @@ +{ + "name": "Xxllnc Value to ZGW ZaakEigenschap", + "version": "0.0.1", + "mapping": { + "naam": "{% if name|default %}{{ name }}{% endif %}", + "waarde": "{% if value[0]|default and value[0] is not iterable %}{{ value[0] }}{% elseif value[0][0]|default and value[0][0] is not iterable %}{{ value[0][0] }}{% elseif value[0][0][0]|default and value[0][0][0] is not iterable %}{{ value[0][0][0] }}{% else %}{{ value|json_encode}}{% endif %}", + "eigenschap": "{% for eigenschap in eigenschappen %}{% if eigenschap.naam == name %}{{ eigenschap['_self']['id'] }}{% endif %}{% endfor %}" + }, + "unset": [], + "cast": [], + "passTrough": false +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/sources/xxllnc-v1.json b/configurations/xxllnc-zgw/sources/xxllnc-v1.json new file mode 100644 index 00000000..c9e0998f --- /dev/null +++ b/configurations/xxllnc-zgw/sources/xxllnc-v1.json @@ -0,0 +1,7 @@ +{ + "name": "Xxllnc V1 API", + "version": "0.0.1", + "location": "", + "isEnabled": true, + "type": "api" +} \ No newline at end of file diff --git a/configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json b/configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json new file mode 100644 index 00000000..cd09f29c --- /dev/null +++ b/configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json @@ -0,0 +1,25 @@ +{ + "name": "Xxllnc cases to ZGW zaken", + "description": "", + "version": "0.0.1", + "sourceId": "1", + "sourceType": "api", + "sourceHash": "", + "sourceHashMapping": "", + "sourceTargetMapping": "1", + "sourceConfig": { + "idPosition": "reference", + "resultsPosition": "result.instance.rows", + "endpoint": "\/case", + "query": [], + "headers.API-KEY": "", + "headers.API-Interface-ID": "" + }, + "currentPage": 1, + "targetId": "1\/1", + "targetType": "register\/schema", + "targetHash": "", + "targetSourceMapping": "", + "targetConfig": [], + "followUps": [] +} \ No newline at end of file From abfa0f5812c2ee62afc0fc7f6f0e8feffdb487f0 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 13:51:14 +0100 Subject: [PATCH 06/20] Add authorization to endpoints --- lib/Controller/EndpointsController.php | 16 +- lib/Db/Consumer.php | 6 +- lib/Exception/AuthenticationException.php | 19 +++ lib/Migration/Version1Date20241218122708.php | 57 +++++++ lib/Migration/Version1Date20241218122932.php | 58 +++++++ lib/Service/AuthorizationService.php | 164 +++++++++++++++++++ 6 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 lib/Exception/AuthenticationException.php create mode 100644 lib/Migration/Version1Date20241218122708.php create mode 100644 lib/Migration/Version1Date20241218122932.php create mode 100644 lib/Service/AuthorizationService.php diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index bd0b6804..cc42407c 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -2,6 +2,8 @@ namespace OCA\OpenConnector\Controller; +use OCA\OpenConnector\Exception\AuthenticationException; +use OCA\OpenConnector\Service\AuthorizationService; use OCA\OpenConnector\Service\ObjectService; use OCA\OpenConnector\Service\SearchService; use OCA\OpenConnector\Service\EndpointService; @@ -33,7 +35,8 @@ public function __construct( IRequest $request, private IAppConfig $config, private EndpointMapper $endpointMapper, - private EndpointService $endpointService + private EndpointService $endpointService, + private AuthorizationService $authorizationService ) { parent::__construct($appName, $request); @@ -185,12 +188,23 @@ public function destroy(int $id): JSONResponse * * @NoAdminRequired * @NoCSRFRequired + * @PublicPage * * @param string $path The request path to match * @return JSONResponse The response from the endpoint service or 404 if no match */ public function handlePath(string $_path): JSONResponse { + try { + $token = $this->request->getHeader('Authorization'); + $this->authorizationService->authorize(authorization: $token); + } catch (AuthenticationException $exception) { + return new JSONResponse( + data: ['error' => $exception->getMessage(), 'details' => $exception->getDetails()], + statusCode: 401 + ); + } + // Find matching endpoints for the given path and method $matchingEndpoints = $this->endpointMapper->findByPathRegex( path: $_path, diff --git a/lib/Db/Consumer.php b/lib/Db/Consumer.php index c5c8b4b5..38a14f61 100644 --- a/lib/Db/Consumer.php +++ b/lib/Db/Consumer.php @@ -18,12 +18,12 @@ class Consumer extends Entity implements JsonSerializable { protected ?string $uuid = null; - protected ?string $name = null; // The name of the consumer + protected ?string $name = null; // The name of the consumer protected ?string $description = null; // The description of the consumer protected ?array $domains = []; // The domains the consumer is allowed to run from protected ?array $ips = []; // The ips the consumer is allowed to run from protected ?string $authorizationType = null; // The authorization type of the consumer, should be one of the following: 'none', 'basic', 'bearer', 'apiKey', 'oauth2', 'jwt'. Keep in mind that the consumer needs to be able to handle the authorization type. - protected ?string $authorizationConfiguration = null; // The authorization configuration of the consumer + protected ?array $authorizationConfiguration = []; // The authorization configuration of the consumer protected ?DateTime $created = null; // the date and time the consumer was created protected ?DateTime $updated = null; // the date and time the consumer was updated @@ -38,7 +38,7 @@ public function __construct() { $this->addType('domains', 'json'); $this->addType('ips', 'json'); $this->addType('authorizationType', 'string'); - $this->addType('authorizationConfiguration', 'string'); + $this->addType('authorizationConfiguration', 'json'); $this->addType('created', 'datetime'); $this->addType('updated', 'datetime'); } diff --git a/lib/Exception/AuthenticationException.php b/lib/Exception/AuthenticationException.php new file mode 100644 index 00000000..d640ae39 --- /dev/null +++ b/lib/Exception/AuthenticationException.php @@ -0,0 +1,19 @@ +details = $details; + parent::__construct($message); + } + + public function getDetails(): array + { + return $this->details; + } +} diff --git a/lib/Migration/Version1Date20241218122708.php b/lib/Migration/Version1Date20241218122708.php new file mode 100644 index 00000000..e92966ed --- /dev/null +++ b/lib/Migration/Version1Date20241218122708.php @@ -0,0 +1,57 @@ +hasTable(tableName: 'openconnector_consumers') === true) { + $table = $schema->getTable(tableName: 'openconnector_consumers'); + $table->dropColumn('authorization_configuration'); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Migration/Version1Date20241218122932.php b/lib/Migration/Version1Date20241218122932.php new file mode 100644 index 00000000..6c3307fa --- /dev/null +++ b/lib/Migration/Version1Date20241218122932.php @@ -0,0 +1,58 @@ +hasTable(tableName: 'openconnector_consumers') === true) { + $table = $schema->getTable(tableName: 'openconnector_consumers'); + $table->addColumn('authorization_configuration', Types::JSON); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/AuthorizationService.php b/lib/Service/AuthorizationService.php new file mode 100644 index 00000000..4c38eed5 --- /dev/null +++ b/lib/Service/AuthorizationService.php @@ -0,0 +1,164 @@ +consumerMapper->findAll(filters: ['name' => $issuer]); + + if(count($consumers) === 0) { + throw new AuthenticationException(message: 'The issuer was not found', details: ['iss' => $issuer]); + } + + return $consumers[0]; + } + + private function checkHeaders(JWS $token): void { + $headerChecker = new HeaderCheckerManager( + checkers: [ + new AlgorithmChecker(array_merge(self::HMAC_ALGORITHMS, self::PKCS1_ALGORITHMS, self::PSS_ALGORITHMS)) + ], + tokenTypes: [new JWSTokenSupport()]); + + $headerChecker->check(jwt: $token, index: 0); + + } + + private function getJWK(string $publicKey, string $algorithm): JWKSet + { + + if ( + in_array(needle: $algorithm, haystack: self::HMAC_ALGORITHMS) === true + ) { + return new JWKSet([ + JWKFactory::createFromSecret( + secret: $publicKey, + additional_values: ['alg' => $algorithm, 'use' => 'sig']) + ]); + } else if ( + in_array( + needle: $algorithm, + haystack: self::PKCS1_ALGORITHMS + ) === true + || in_array( + needle: $algorithm, + haystack: self::PSS_ALGORITHMS + ) === true + ) { + $stamp = microtime().getmypid(); + $filename = "/var/tmp/publickey-$stamp"; + file_put_contents($filename, base64_decode($publicKey)); + $jwk = new JWKSet([JWKFactory::createFromKeyFile(file: $filename)]); + unlink($filename); + return $jwk; + } + throw new AuthenticationException(message: 'The token algorithm is not supported', details: ['algorithm' => $algorithm]); + } + + public function validatePayload(array $payload): void + { + $now = new DateTime(); + + if(isset($payload['iat']) === true) { + $iat = new DateTime('@'.$payload['iat']); + } else { + throw new AuthenticationException(message: 'The token has no time of creation', details: ['iat' => null]); + } + + if(isset($payload['exp']) === true) { + $exp = new DateTime('@'.$payload['exp']); + } else { + $exp = clone $iat; + $exp->modify('+1 Hour'); + } + + if($exp->diff($now)->format('%R') === '+') { + throw new AuthenticationException(message: 'The token has expired', details: ['iat' => $iat->getTimestamp(), 'exp' => $exp->getTimestamp(), 'time checked' => $now->getTimestamp()]); + } + } + public function authorize(string $authorization): void + { + $token = substr(string: $authorization, offset: strlen('Bearer ')); + + if($token === '') { + throw new AuthenticationException(message: 'No token has been provided', details: []); + } + + $algorithmManager = new AlgorithmManager([ + new HS256(), + new HS384(), + new HS256(), + new RS256(), + new RS384(), + new RS512(), + new PS256(), + new PS384(), + new PS512() + ]); + $verifier = new JWSVerifier($algorithmManager); + $serializerManager = new JWSSerializerManager([new CompactSerializer()]); + + + + $jws = $serializerManager->unserialize(input: $token); + + try{ + $this->checkHeaders($jws); + } catch (InvalidHeaderException $exception) { + throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => $exception->getMessage()]); + } + + $payload = json_decode(json: $jws->getPayload(), associative: true); + $issuer = $this->findIssuer(issuer: $payload['iss']); + + $publicKey = $issuer->getAuthorizationConfiguration()['publicKey']; + $algorithm = $issuer->getAuthorizationConfiguration()['algorithm']; + + $jwkSet = $this->getJWK(publicKey: $publicKey, algorithm: $algorithm); + + if($verifier->verifyWithKeySet(jws: $jws, jwkset: $jwkSet, signatureIndex: 0) === false) { + throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => 'The token does not match the public key']); + } + $this->validatePayload($payload); +// $this->userSession->setUser($this->userManager->get($issuer->getUserId())); + } +} From e40da74df622e92df43cc56931c0cd6cc3348bf0 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 14:11:32 +0100 Subject: [PATCH 07/20] Set user id in consumer --- lib/Db/Consumer.php | 3 +++ lib/Migration/Version1Date20241218122932.php | 1 + lib/Service/AuthorizationService.php | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Db/Consumer.php b/lib/Db/Consumer.php index 38a14f61..2fcd8c7b 100644 --- a/lib/Db/Consumer.php +++ b/lib/Db/Consumer.php @@ -26,6 +26,7 @@ class Consumer extends Entity implements JsonSerializable protected ?array $authorizationConfiguration = []; // The authorization configuration of the consumer protected ?DateTime $created = null; // the date and time the consumer was created protected ?DateTime $updated = null; // the date and time the consumer was updated + protected ?string $userId = null; /** * Consumer constructor. @@ -41,6 +42,7 @@ public function __construct() { $this->addType('authorizationConfiguration', 'json'); $this->addType('created', 'datetime'); $this->addType('updated', 'datetime'); + $this->addType('userId', 'string'); } /** @@ -100,6 +102,7 @@ public function jsonSerialize(): array 'ips' => $this->ips, 'authorizationType' => $this->authorizationType, 'authorizationConfiguration' => $this->authorizationConfiguration, + 'userId' => $this->userId, 'created' => isset($this->created) ? $this->created->format('c') : null, 'updated' => isset($this->updated) ? $this->updated->format('c') : null, ]; diff --git a/lib/Migration/Version1Date20241218122932.php b/lib/Migration/Version1Date20241218122932.php index 6c3307fa..7c66532e 100644 --- a/lib/Migration/Version1Date20241218122932.php +++ b/lib/Migration/Version1Date20241218122932.php @@ -43,6 +43,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if($schema->hasTable(tableName: 'openconnector_consumers') === true) { $table = $schema->getTable(tableName: 'openconnector_consumers'); $table->addColumn('authorization_configuration', Types::JSON); + $table->addColumn('user_id', Types::STRING); } return $schema; diff --git a/lib/Service/AuthorizationService.php b/lib/Service/AuthorizationService.php index 4c38eed5..5f338ea3 100644 --- a/lib/Service/AuthorizationService.php +++ b/lib/Service/AuthorizationService.php @@ -159,6 +159,6 @@ public function authorize(string $authorization): void throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => 'The token does not match the public key']); } $this->validatePayload($payload); -// $this->userSession->setUser($this->userManager->get($issuer->getUserId())); + $this->userSession->setUser($this->userManager->get($issuer->getUserId())); } } From 1c7ea85a32c4df1a666f711dbe53cdbb854ea925 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 14:47:27 +0100 Subject: [PATCH 08/20] Docblocks on authorization service --- lib/Service/AuthorizationService.php | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/lib/Service/AuthorizationService.php b/lib/Service/AuthorizationService.php index 5f338ea3..b81b1948 100644 --- a/lib/Service/AuthorizationService.php +++ b/lib/Service/AuthorizationService.php @@ -28,6 +28,9 @@ use OCP\IUserManager; use OCP\IUserSession; +/** + * Service class for handling authorization on incoming calls. + */ class AuthorizationService { const HMAC_ALGORITHMS = ['HS256', 'HS384', 'HS512']; @@ -35,12 +38,24 @@ class AuthorizationService const PSS_ALGORITHMS = ['PS256', 'PS384', 'PS512']; + /** + * @param IUserManager $userManager + * @param IUserSession $userSession + * @param ConsumerMapper $consumerMapper + */ public function __construct( private readonly IUserManager $userManager, private readonly IUserSession $userSession, private readonly ConsumerMapper $consumerMapper, ) {} + /** + * Find the issuer (consumer) for the request. + * + * @param string $issuer The issuer from the JWT token. + * @return Consumer The consumer for the JWT token. + * @throws AuthenticationException Thrown if no issuer was found. + */ private function findIssuer(string $issuer): Consumer { $consumers = $this->consumerMapper->findAll(filters: ['name' => $issuer]); @@ -52,6 +67,12 @@ private function findIssuer(string $issuer): Consumer return $consumers[0]; } + /** + * Check if the headers of a JWT token are valid. + * + * @param JWS $token The unserialized token. + * @return void + */ private function checkHeaders(JWS $token): void { $headerChecker = new HeaderCheckerManager( checkers: [ @@ -63,6 +84,14 @@ private function checkHeaders(JWS $token): void { } + /** + * Get the Json Web Key for a public key combined with an algorithm. + * + * @param string $publicKey The public key to create a JWK for + * @param string $algorithm The algorithm deciding how the key should be defined. + * @return JWKSet The resulting JWK-set. + * @throws AuthenticationException + */ private function getJWK(string $publicKey, string $algorithm): JWKSet { @@ -94,6 +123,13 @@ private function getJWK(string $publicKey, string $algorithm): JWKSet throw new AuthenticationException(message: 'The token algorithm is not supported', details: ['algorithm' => $algorithm]); } + /** + * Validate data in the payload. + * + * @param array $payload The payload of the JWT token. + * @return void + * @throws AuthenticationException + */ public function validatePayload(array $payload): void { $now = new DateTime(); @@ -115,6 +151,14 @@ public function validatePayload(array $payload): void throw new AuthenticationException(message: 'The token has expired', details: ['iat' => $iat->getTimestamp(), 'exp' => $exp->getTimestamp(), 'time checked' => $now->getTimestamp()]); } } + + /** + * Checks if authorization header contains a valid JWT token. + * + * @param string $authorization The authorization header. + * @return void + * @throws AuthenticationException + */ public function authorize(string $authorization): void { $token = substr(string: $authorization, offset: strlen('Bearer ')); From 5a6fe09b35c50ce443f49747a431cadd1bffaf7f Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 14:58:09 +0100 Subject: [PATCH 09/20] PR comments --- lib/Controller/EndpointsController.php | 384 +++++++++++----------- lib/Exception/AuthenticationException.php | 14 + lib/Service/AuthorizationService.php | 32 +- lib/Service/EndpointService.php | 276 +++++++++------- 4 files changed, 380 insertions(+), 326 deletions(-) diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index cc42407c..e01248f3 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -21,180 +21,180 @@ */ class EndpointsController extends Controller { - /** - * Constructor for the EndpointsController - * - * @param string $appName The name of the app - * @param IRequest $request The request object - * @param IAppConfig $config The app configuration object - * @param EndpointMapper $endpointMapper The endpoint mapper object - * @param EndpointService $endpointService Service for handling endpoint operations - */ - public function __construct( - $appName, - IRequest $request, - private IAppConfig $config, - private EndpointMapper $endpointMapper, - private EndpointService $endpointService, + /** + * Constructor for the EndpointsController + * + * @param string $appName The name of the app + * @param IRequest $request The request object + * @param IAppConfig $config The app configuration object + * @param EndpointMapper $endpointMapper The endpoint mapper object + * @param EndpointService $endpointService Service for handling endpoint operations + */ + public function __construct( + $appName, + IRequest $request, + private IAppConfig $config, + private EndpointMapper $endpointMapper, + private EndpointService $endpointService, private AuthorizationService $authorizationService - ) - { - parent::__construct($appName, $request); - } - - /** - * Returns the template of the main app's page - * - * This method renders the main page of the application, adding any necessary data to the template. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return TemplateResponse The rendered template response - */ - public function page(): TemplateResponse - { - return new TemplateResponse( - 'openconnector', - 'index', - [] - ); - } - - /** - * Retrieves a list of all endpoints - * - * This method returns a JSON response containing an array of all endpoints in the system. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return JSONResponse A JSON response containing the list of endpoints - */ - public function index(ObjectService $objectService, SearchService $searchService): JSONResponse - { - - $filters = $this->request->getParams(); - $fieldsToSearch = ['name', 'description', 'endpoint']; - - $searchParams = $searchService->createMySQLSearchParams(filters: $filters); - $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); - $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - - return new JSONResponse(['results' => $this->endpointMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); - } - - /** - * Retrieves a single endpoint by its ID - * - * This method returns a JSON response containing the details of a specific endpoint. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the endpoint to retrieve - * @return JSONResponse A JSON response containing the endpoint details - */ - public function show(string $id): JSONResponse - { - try { - return new JSONResponse($this->endpointMapper->find(id: (int) $id)); - } catch (DoesNotExistException $exception) { - return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); - } - } - - /** - * Creates a new endpoint - * - * This method creates a new endpoint based on POST data. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @return JSONResponse A JSON response containing the created endpoint - */ - public function create(): JSONResponse - { - $data = $this->request->getParams(); - - foreach ($data as $key => $value) { - if (str_starts_with($key, '_')) { - unset($data[$key]); - } - } - - if (isset($data['id'])) { - unset($data['id']); - } - - $endpoint = $this->endpointMapper->createFromArray(object: $data); - - return new JSONResponse($endpoint); - } - - /** - * Updates an existing endpoint - * - * This method updates an existing endpoint based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the endpoint to update - * @return JSONResponse A JSON response containing the updated endpoint details - */ - public function update(int $id): JSONResponse - { - $data = $this->request->getParams(); - - foreach ($data as $key => $value) { - if (str_starts_with($key, '_')) { - unset($data[$key]); - } - } - if (isset($data['id'])) { - unset($data['id']); - } - - $endpoint = $this->endpointMapper->updateFromArray(id: (int) $id, object: $data); - - return new JSONResponse($endpoint); - } - - /** - * Deletes an endpoint - * - * This method deletes an endpoint based on its ID. - * - * @NoAdminRequired - * @NoCSRFRequired - * - * @param string $id The ID of the endpoint to delete - * @return JSONResponse An empty JSON response - */ - public function destroy(int $id): JSONResponse - { - $this->endpointMapper->delete($this->endpointMapper->find((int) $id)); - - return new JSONResponse([]); - } - - /** - * Handles generic path requests by matching against registered endpoints - * - * This method checks if the current path matches any registered endpoint patterns - * and forwards the request to the appropriate endpoint service if found - * - * @NoAdminRequired - * @NoCSRFRequired + ) + { + parent::__construct($appName, $request); + } + + /** + * Returns the template of the main app's page + * + * This method renders the main page of the application, adding any necessary data to the template. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return TemplateResponse The rendered template response + */ + public function page(): TemplateResponse + { + return new TemplateResponse( + 'openconnector', + 'index', + [] + ); + } + + /** + * Retrieves a list of all endpoints + * + * This method returns a JSON response containing an array of all endpoints in the system. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse A JSON response containing the list of endpoints + */ + public function index(ObjectService $objectService, SearchService $searchService): JSONResponse + { + + $filters = $this->request->getParams(); + $fieldsToSearch = ['name', 'description', 'endpoint']; + + $searchParams = $searchService->createMySQLSearchParams(filters: $filters); + $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); + $filters = $searchService->unsetSpecialQueryParams(filters: $filters); + + return new JSONResponse(['results' => $this->endpointMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + } + + /** + * Retrieves a single endpoint by its ID + * + * This method returns a JSON response containing the details of a specific endpoint. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the endpoint to retrieve + * @return JSONResponse A JSON response containing the endpoint details + */ + public function show(string $id): JSONResponse + { + try { + return new JSONResponse($this->endpointMapper->find(id: (int)$id)); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } + } + + /** + * Creates a new endpoint + * + * This method creates a new endpoint based on POST data. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @return JSONResponse A JSON response containing the created endpoint + */ + public function create(): JSONResponse + { + $data = $this->request->getParams(); + + foreach ($data as $key => $value) { + if (str_starts_with($key, '_')) { + unset($data[$key]); + } + } + + if (isset($data['id'])) { + unset($data['id']); + } + + $endpoint = $this->endpointMapper->createFromArray(object: $data); + + return new JSONResponse($endpoint); + } + + /** + * Updates an existing endpoint + * + * This method updates an existing endpoint based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the endpoint to update + * @return JSONResponse A JSON response containing the updated endpoint details + */ + public function update(int $id): JSONResponse + { + $data = $this->request->getParams(); + + foreach ($data as $key => $value) { + if (str_starts_with($key, '_')) { + unset($data[$key]); + } + } + if (isset($data['id'])) { + unset($data['id']); + } + + $endpoint = $this->endpointMapper->updateFromArray(id: (int)$id, object: $data); + + return new JSONResponse($endpoint); + } + + /** + * Deletes an endpoint + * + * This method deletes an endpoint based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $id The ID of the endpoint to delete + * @return JSONResponse An empty JSON response + */ + public function destroy(int $id): JSONResponse + { + $this->endpointMapper->delete($this->endpointMapper->find((int)$id)); + + return new JSONResponse([]); + } + + /** + * Handles generic path requests by matching against registered endpoints + * + * This method checks if the current path matches any registered endpoint patterns + * and forwards the request to the appropriate endpoint service if found + * + * @NoAdminRequired + * @NoCSRFRequired * @PublicPage - * - * @param string $path The request path to match - * @return JSONResponse The response from the endpoint service or 404 if no match - */ - public function handlePath(string $_path): JSONResponse - { + * + * @param string $path The request path to match + * @return JSONResponse The response from the endpoint service or 404 if no match + */ + public function handlePath(string $_path): JSONResponse + { try { $token = $this->request->getHeader('Authorization'); $this->authorizationService->authorize(authorization: $token); @@ -205,25 +205,25 @@ public function handlePath(string $_path): JSONResponse ); } - // Find matching endpoints for the given path and method - $matchingEndpoints = $this->endpointMapper->findByPathRegex( - path: $_path, - method: $this->request->getMethod() - ); - - // If no matching endpoints found, return 404 - if (empty($matchingEndpoints)) { - return new JSONResponse( - data: ['error' => 'No matching endpoint found for path and method: ' . $_path . ' ' . $this->request->getMethod()], - statusCode: 404 - ); - } - - // Get the first matching endpoint since we already filtered by method - $endpoint = reset($matchingEndpoints); - - // Forward the request to the endpoint service - return $this->endpointService->handleRequest($endpoint, $this->request, $_path); - } + // Find matching endpoints for the given path and method + $matchingEndpoints = $this->endpointMapper->findByPathRegex( + path: $_path, + method: $this->request->getMethod() + ); + + // If no matching endpoints found, return 404 + if (empty($matchingEndpoints)) { + return new JSONResponse( + data: ['error' => 'No matching endpoint found for path and method: ' . $_path . ' ' . $this->request->getMethod()], + statusCode: 404 + ); + } + + // Get the first matching endpoint since we already filtered by method + $endpoint = reset($matchingEndpoints); + + // Forward the request to the endpoint service + return $this->endpointService->handleRequest($endpoint, $this->request, $_path); + } } diff --git a/lib/Exception/AuthenticationException.php b/lib/Exception/AuthenticationException.php index d640ae39..e7e04ed9 100644 --- a/lib/Exception/AuthenticationException.php +++ b/lib/Exception/AuthenticationException.php @@ -4,14 +4,28 @@ use Exception; +/** + * Exception for storing authentication exceptions with details. + */ class AuthenticationException extends Exception { private array $details; + + /** + * @inheritDoc + * + * @param array $details The details describing why an authentication failed. + */ public function __construct(string $message, array $details) { $this->details = $details; parent::__construct($message); } + /** + * Retrieves the details to display them. + * + * @return array The details array. + */ public function getDetails(): array { return $this->details; diff --git a/lib/Service/AuthorizationService.php b/lib/Service/AuthorizationService.php index b81b1948..98323e10 100644 --- a/lib/Service/AuthorizationService.php +++ b/lib/Service/AuthorizationService.php @@ -44,10 +44,12 @@ class AuthorizationService * @param ConsumerMapper $consumerMapper */ public function __construct( - private readonly IUserManager $userManager, - private readonly IUserSession $userSession, + private readonly IUserManager $userManager, + private readonly IUserSession $userSession, private readonly ConsumerMapper $consumerMapper, - ) {} + ) + { + } /** * Find the issuer (consumer) for the request. @@ -60,7 +62,7 @@ private function findIssuer(string $issuer): Consumer { $consumers = $this->consumerMapper->findAll(filters: ['name' => $issuer]); - if(count($consumers) === 0) { + if (count($consumers) === 0) { throw new AuthenticationException(message: 'The issuer was not found', details: ['iss' => $issuer]); } @@ -73,7 +75,8 @@ private function findIssuer(string $issuer): Consumer * @param JWS $token The unserialized token. * @return void */ - private function checkHeaders(JWS $token): void { + private function checkHeaders(JWS $token): void + { $headerChecker = new HeaderCheckerManager( checkers: [ new AlgorithmChecker(array_merge(self::HMAC_ALGORITHMS, self::PKCS1_ALGORITHMS, self::PSS_ALGORITHMS)) @@ -113,7 +116,7 @@ private function getJWK(string $publicKey, string $algorithm): JWKSet haystack: self::PSS_ALGORITHMS ) === true ) { - $stamp = microtime().getmypid(); + $stamp = microtime() . getmypid(); $filename = "/var/tmp/publickey-$stamp"; file_put_contents($filename, base64_decode($publicKey)); $jwk = new JWKSet([JWKFactory::createFromKeyFile(file: $filename)]); @@ -134,20 +137,20 @@ public function validatePayload(array $payload): void { $now = new DateTime(); - if(isset($payload['iat']) === true) { - $iat = new DateTime('@'.$payload['iat']); + if (isset($payload['iat']) === true) { + $iat = new DateTime('@' . $payload['iat']); } else { throw new AuthenticationException(message: 'The token has no time of creation', details: ['iat' => null]); } - if(isset($payload['exp']) === true) { - $exp = new DateTime('@'.$payload['exp']); + if (isset($payload['exp']) === true) { + $exp = new DateTime('@' . $payload['exp']); } else { $exp = clone $iat; $exp->modify('+1 Hour'); } - if($exp->diff($now)->format('%R') === '+') { + if ($exp->diff($now)->format('%R') === '+') { throw new AuthenticationException(message: 'The token has expired', details: ['iat' => $iat->getTimestamp(), 'exp' => $exp->getTimestamp(), 'time checked' => $now->getTimestamp()]); } } @@ -163,7 +166,7 @@ public function authorize(string $authorization): void { $token = substr(string: $authorization, offset: strlen('Bearer ')); - if($token === '') { + if ($token === '') { throw new AuthenticationException(message: 'No token has been provided', details: []); } @@ -182,10 +185,9 @@ public function authorize(string $authorization): void $serializerManager = new JWSSerializerManager([new CompactSerializer()]); - $jws = $serializerManager->unserialize(input: $token); - try{ + try { $this->checkHeaders($jws); } catch (InvalidHeaderException $exception) { throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => $exception->getMessage()]); @@ -199,7 +201,7 @@ public function authorize(string $authorization): void $jwkSet = $this->getJWK(publicKey: $publicKey, algorithm: $algorithm); - if($verifier->verifyWithKeySet(jws: $jws, jwkset: $jwkSet, signatureIndex: 0) === false) { + if ($verifier->verifyWithKeySet(jws: $jws, jwkset: $jwkSet, signatureIndex: 0) === false) { throw new AuthenticationException(message: 'The token could not be validated', details: ['reason' => 'The token does not match the public key']); } $this->validatePayload($payload); diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index be3a974a..1636d74e 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -30,65 +30,77 @@ * This class provides functionality to handle requests to endpoints, either by * connecting to a schema within a register or by proxying to a source. */ -class EndpointService { - - /** - * Constructor for EndpointService - * - * @param ObjectService $objectService Service for handling object operations - * @param CallService $callService Service for making external API calls - * @param LoggerInterface $logger Logger interface for error logging - */ - public function __construct( - private readonly ObjectService $objectService, - private readonly CallService $callService, - private readonly LoggerInterface $logger, - private readonly IURLGenerator $urlGenerator, - ) {} - - /** - * Handles incoming requests to endpoints - * - * This method determines how to handle the request based on the endpoint configuration. - * It either routes to a schema within a register or proxies to an external source. - * - * @param Endpoint $endpoint The endpoint configuration to handle - * @param IRequest $request The incoming request object - * @return JSONResponse Response containing the result - * @throws \Exception When endpoint configuration is invalid - */ - public function handleRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse { - try { - // Check if endpoint connects to a schema - if ($endpoint->getTargetType() === 'register/schema') { - // Handle CRUD operations via ObjectService - return $this->handleSchemaRequest($endpoint, $request, $path); - } - - // Check if endpoint connects to a source - if ($endpoint->getTargetType() === 'api') { - // Proxy request to source via CallService - return $this->handleSourceRequest($endpoint, $request); - } - - // Invalid endpoint configuration - throw new \Exception('Endpoint must specify either a schema or source connection'); - - } catch (\Exception $e) { - $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); - return new JSONResponse( - ['error' => $e->getMessage()], - 400 - ); - } - } +class EndpointService +{ + + /** + * Constructor for EndpointService + * + * @param ObjectService $objectService Service for handling object operations + * @param CallService $callService Service for making external API calls + * @param LoggerInterface $logger Logger interface for error logging + */ + public function __construct( + private readonly ObjectService $objectService, + private readonly CallService $callService, + private readonly LoggerInterface $logger, + private readonly IURLGenerator $urlGenerator, + ) + { + } + + /** + * Handles incoming requests to endpoints + * + * This method determines how to handle the request based on the endpoint configuration. + * It either routes to a schema within a register or proxies to an external source. + * + * @param Endpoint $endpoint The endpoint configuration to handle + * @param IRequest $request The incoming request object + * @return JSONResponse Response containing the result + * @throws \Exception When endpoint configuration is invalid + */ + public function handleRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse + { + try { + // Check if endpoint connects to a schema + if ($endpoint->getTargetType() === 'register/schema') { + // Handle CRUD operations via ObjectService + return $this->handleSchemaRequest($endpoint, $request, $path); + } + + // Check if endpoint connects to a source + if ($endpoint->getTargetType() === 'api') { + // Proxy request to source via CallService + return $this->handleSourceRequest($endpoint, $request); + } + + // Invalid endpoint configuration + throw new \Exception('Endpoint must specify either a schema or source connection'); + + } catch (\Exception $e) { + $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); + return new JSONResponse( + ['error' => $e->getMessage()], + 400 + ); + } + } + /** + * Parses a path to get the parameters in a path. + * + * @param array $endpointArray The endpoint array from an endpoint object. + * @param string $path The path called by the client. + * + * @return array The parsed path with the fields having the correct name. + */ private function getPathParameters(array $endpointArray, string $path): array { $pathParts = explode(separator: '/', string: $path); $endpointArrayNormalized = array_map( - function($item) { + function ($item) { return str_replace( search: ['{{', '{{ ', '}}', '}}'], replace: '', @@ -113,28 +125,39 @@ function($item) { return $pathParams; } + /** + * Fetch objects for the endpoint. + * + * @param \OCA\OpenRegister\Service\ObjectService|QBMapper $mapper The mapper for the object type + * @param array $parameters The parameters from the request + * @param array $pathParams The parameters in the path + * @param int $status The HTTP status to return. + * @return Entity|array The object(s) confirming to the request. + * @throws Exception + */ private function getObjects( \OCA\OpenRegister\Service\ObjectService|QBMapper $mapper, - array $parameters, - array $pathParams, - int &$status = 200 + array $parameters, + array $pathParams, + int &$status = 200 ): Entity|array { - if(isset($pathParams['id']) === true && $pathParams['id'] === end($pathParams)) { + if (isset($pathParams['id']) === true && $pathParams['id'] === end($pathParams)) { return $mapper->find($pathParams['id']); } else if (isset($pathParams['id']) === true) { - while(prev($pathParams) !== $pathParams['id']){}; + while (prev($pathParams) !== $pathParams['id']) { + }; $property = next($pathParams); - if(next($pathParams) !== false) { + if (next($pathParams) !== false) { $id = pos($pathParams); } $main = $mapper->find($pathParams['id'])->getObject(); $ids = $main[$property]; - if(isset($id) === true && in_array(needle: $id, haystack: $ids) === true) { + if (isset($id) === true && in_array(needle: $id, haystack: $ids) === true) { return $mapper->findSubObjects([$id], $property)[0]; } else if (isset($id) === true) { @@ -152,7 +175,7 @@ private function getObjects( 'count' => $result['total'], ]; - if($result['page'] < $result['pages']) { + if ($result['page'] < $result['pages']) { $parameters['page'] = $result['page'] + 1; $parameters['_path'] = implode('/', $pathParams); @@ -164,7 +187,7 @@ private function getObjects( ) ); } - if($result['page'] > 1) { + if ($result['page'] > 1) { $parameters['page'] = $result['page'] - 1; $parameters['_path'] = implode('/', $pathParams); @@ -181,16 +204,17 @@ private function getObjects( return $returnArray; } - /** - * Handles requests for schema-based endpoints - * - * @param Endpoint $endpoint The endpoint configuration - * @param IRequest $request The incoming request - * @return JSONResponse - */ - private function handleSchemaRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse { - // Get request method - $method = $request->getMethod(); + /** + * Handles requests for schema-based endpoints + * + * @param Endpoint $endpoint The endpoint configuration + * @param IRequest $request The incoming request + * @return JSONResponse + */ + private function handleSchemaRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse + { + // Get request method + $method = $request->getMethod(); $target = explode('/', $endpoint->getTargetId()); $register = $target[0]; @@ -206,39 +230,52 @@ private function handleSchemaRequest(Endpoint $endpoint, IRequest $request, stri $status = 200; - // Route to appropriate ObjectService method based on HTTP method - return match($method) { - 'GET' => new JSONResponse( - $this->getObjects(mapper: $mapper, parameters: $parameters, pathParams: $pathParams, status: $status), statusCode: $status - ), - 'POST' => new JSONResponse( - $mapper->createFromArray(object: $parameters) - ), - 'PUT' => new JSONResponse( - $mapper->updateFromArray($request->getParams()['id'], $request->getParams(), true, true) - ), - 'DELETE' => new JSONResponse( - $mapper->delete($request->getParams()) - ), - default => throw new \Exception('Unsupported HTTP method') - }; - } + // Route to appropriate ObjectService method based on HTTP method + return match ($method) { + 'GET' => new JSONResponse( + $this->getObjects(mapper: $mapper, parameters: $parameters, pathParams: $pathParams, status: $status), statusCode: $status + ), + 'POST' => new JSONResponse( + $mapper->createFromArray(object: $parameters) + ), + 'PUT' => new JSONResponse( + $mapper->updateFromArray($request->getParams()['id'], $request->getParams(), true, true) + ), + 'DELETE' => new JSONResponse( + $mapper->delete($request->getParams()) + ), + default => throw new \Exception('Unsupported HTTP method') + }; + } + /** + * Gets the raw content for an http request from the input stream. + * + * @return string The raw content body for an http request + */ private function getRawContent(): string { return file_get_contents(filename: 'php://input'); } + /** + * Get all headers for a HTTP request. + * + * @param array $server The server data from the request. + * @param bool $proxyHeaders Whether the proxy headers should be returned. + * + * @return array The resulting headers. + */ private function getHeaders(array $server, bool $proxyHeaders = false): array { $headers = array_filter( array: $server, - callback: function (string $key) use ($proxyHeaders){ - if(str_starts_with($key, 'HTTP_') === false) { + callback: function (string $key) use ($proxyHeaders) { + if (str_starts_with($key, 'HTTP_') === false) { return false; } else if ($proxyHeaders === false && (str_starts_with(haystack: $key, needle: 'HTTP_X_FORWARDED') - || $key === 'HTTP_X_REAL_IP' || $key === 'HTTP_X_ORIGINAL_URI' + || $key === 'HTTP_X_REAL_IP' || $key === 'HTTP_X_ORIGINAL_URI' ) ) { return false; @@ -253,40 +290,41 @@ private function getHeaders(array $server, bool $proxyHeaders = false): array return array_combine( array_map( - callback: function($key) { + callback: function ($key) { return strtolower(string: substr(string: $key, offset: 5)); - }, + }, array: $keys), $headers ); } - /** - * Handles requests for source-based endpoints - * - * @param Endpoint $endpoint The endpoint configuration - * @param IRequest $request The incoming request - * @return JSONResponse - */ - private function handleSourceRequest(Endpoint $endpoint, IRequest $request): JSONResponse { + /** + * Handles requests for source-based endpoints + * + * @param Endpoint $endpoint The endpoint configuration + * @param IRequest $request The incoming request + * @return JSONResponse + */ + private function handleSourceRequest(Endpoint $endpoint, IRequest $request): JSONResponse + { $headers = $this->getHeaders($request->server); - // Proxy the request to the source via CallService - $response = $this->callService->call( - source: $endpoint->getSource(), - endpoint: $endpoint->getPath(), - method: $request->getMethod(), - config: [ - 'query' => $request->getParams(), - 'headers' => $headers, - 'body' => $this->getRawContent(), - ] - ); - - return new JSONResponse( - $response->getResponse(), - $response->getStatusCode() - ); - } + // Proxy the request to the source via CallService + $response = $this->callService->call( + source: $endpoint->getSource(), + endpoint: $endpoint->getPath(), + method: $request->getMethod(), + config: [ + 'query' => $request->getParams(), + 'headers' => $headers, + 'body' => $this->getRawContent(), + ] + ); + + return new JSONResponse( + $response->getResponse(), + $response->getStatusCode() + ); + } } From bf178e6ae5b83900aa133ea3eae4a4636d03f400 Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 15:34:29 +0100 Subject: [PATCH 10/20] More handling of PR comments --- lib/Controller/EndpointsController.php | 1 - lib/Db/EndpointMapper.php | 2 +- lib/Migration/Version1Date20241218122708.php | 2 +- lib/Service/AuthorizationService.php | 15 +++------------ lib/Service/EndpointService.php | 14 +++++++------- 5 files changed, 12 insertions(+), 22 deletions(-) diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index e01248f3..2bbab0be 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -73,7 +73,6 @@ public function page(): TemplateResponse */ public function index(ObjectService $objectService, SearchService $searchService): JSONResponse { - $filters = $this->request->getParams(); $fieldsToSearch = ['name', 'description', 'endpoint']; diff --git a/lib/Db/EndpointMapper.php b/lib/Db/EndpointMapper.php index 0479b765..a7727cbe 100644 --- a/lib/Db/EndpointMapper.php +++ b/lib/Db/EndpointMapper.php @@ -134,7 +134,7 @@ public function findByPathRegex(string $path, string $method): array $pattern = $endpoint->getEndpointRegex(); // Skip if no regex pattern is set - if (empty($pattern)) { + if (empty($pattern) === true) { return false; } diff --git a/lib/Migration/Version1Date20241218122708.php b/lib/Migration/Version1Date20241218122708.php index e92966ed..2925f4a0 100644 --- a/lib/Migration/Version1Date20241218122708.php +++ b/lib/Migration/Version1Date20241218122708.php @@ -39,7 +39,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt */ $schema = $schemaClosure(); - if($schema->hasTable(tableName: 'openconnector_consumers') === true) { + if ($schema->hasTable(tableName: 'openconnector_consumers') === true) { $table = $schema->getTable(tableName: 'openconnector_consumers'); $table->dropColumn('authorization_configuration'); } diff --git a/lib/Service/AuthorizationService.php b/lib/Service/AuthorizationService.php index 98323e10..dd50243f 100644 --- a/lib/Service/AuthorizationService.php +++ b/lib/Service/AuthorizationService.php @@ -98,23 +98,14 @@ private function checkHeaders(JWS $token): void private function getJWK(string $publicKey, string $algorithm): JWKSet { - if ( - in_array(needle: $algorithm, haystack: self::HMAC_ALGORITHMS) === true - ) { + if (in_array(needle: $algorithm, haystack: self::HMAC_ALGORITHMS) === true) { return new JWKSet([ JWKFactory::createFromSecret( secret: $publicKey, additional_values: ['alg' => $algorithm, 'use' => 'sig']) ]); - } else if ( - in_array( - needle: $algorithm, - haystack: self::PKCS1_ALGORITHMS - ) === true - || in_array( - needle: $algorithm, - haystack: self::PSS_ALGORITHMS - ) === true + } else if (in_array(needle: $algorithm, haystack: self::PKCS1_ALGORITHMS) === true + || in_array(needle: $algorithm, haystack: self::PSS_ALGORITHMS) === true ) { $stamp = microtime() . getmypid(); $filename = "/var/tmp/publickey-$stamp"; diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index 1636d74e..a4a7efb4 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -58,7 +58,7 @@ public function __construct( * @param Endpoint $endpoint The endpoint configuration to handle * @param IRequest $request The incoming request object * @return JSONResponse Response containing the result - * @throws \Exception When endpoint configuration is invalid + * @throws Exception When endpoint configuration is invalid */ public function handleRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse { @@ -76,9 +76,9 @@ public function handleRequest(Endpoint $endpoint, IRequest $request, string $pat } // Invalid endpoint configuration - throw new \Exception('Endpoint must specify either a schema or source connection'); + throw new Exception('Endpoint must specify either a schema or source connection'); - } catch (\Exception $e) { + } catch (Exception $e) { $this->logger->error('Error handling endpoint request: ' . $e->getMessage()); return new JSONResponse( ['error' => $e->getMessage()], @@ -145,8 +145,10 @@ private function getObjects( if (isset($pathParams['id']) === true && $pathParams['id'] === end($pathParams)) { return $mapper->find($pathParams['id']); } else if (isset($pathParams['id']) === true) { + + // Set the array pointer to the location of the id, so we can fetch the parameters further down the line in order. while (prev($pathParams) !== $pathParams['id']) { - }; + } $property = next($pathParams); @@ -179,7 +181,6 @@ private function getObjects( $parameters['page'] = $result['page'] + 1; $parameters['_path'] = implode('/', $pathParams); - $returnArray['next'] = $this->urlGenerator->getAbsoluteURL( $this->urlGenerator->linkToRoute( routeName: 'openconnector.endpoints.handlepath', @@ -244,7 +245,7 @@ private function handleSchemaRequest(Endpoint $endpoint, IRequest $request, stri 'DELETE' => new JSONResponse( $mapper->delete($request->getParams()) ), - default => throw new \Exception('Unsupported HTTP method') + default => throw new Exception('Unsupported HTTP method') }; } @@ -307,7 +308,6 @@ private function getHeaders(array $server, bool $proxyHeaders = false): array */ private function handleSourceRequest(Endpoint $endpoint, IRequest $request): JSONResponse { - $headers = $this->getHeaders($request->server); // Proxy the request to the source via CallService From f35e9f35a2e014cfd1ff522537b1cd6e8c9736df Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 15:37:28 +0100 Subject: [PATCH 11/20] Yet some more PR comments --- lib/Service/ObjectService.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index 692e76ae..c1d8e6a5 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -252,9 +252,20 @@ public function getOpenRegisters(): ?\OCA\OpenRegister\Service\ObjectService return null; } + /** + * Get the mapper for the given objecttype (usually the proper instantiation of the objectService of OpenRegister. + * + * @param string|null $objecttype The objecttype as string + * @param int|null $schema The openregister schema + * @param int|null $register The openregister register + * + * @return QBMapper|\OCA\OpenRegister\Service\ObjectService|null The resulting mapper + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function getMapper(?string $objecttype = null, ?int $schema = null, ?int $register = null): QBMapper|\OCA\OpenRegister\Service\ObjectService|null { - if($register !== null && $schema !== null && $objecttype === null) { + if ($register !== null && $schema !== null && $objecttype === null) { return $this->getOpenRegisters()->getMapper(register: $register, schema: $schema); } From 75760f5381850a0ab6988eff3e851c6ed9747edb Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Wed, 18 Dec 2024 16:46:22 +0100 Subject: [PATCH 12/20] First sketch logic on endpoint data --- lib/Service/EndpointService.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index a4a7efb4..a8a41aee 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -4,6 +4,7 @@ use Adbar\Dot; use Exception; +use JWadhams\JsonLogic; use OCA\OpenConnector\Db\SourceMapper; use OCA\OpenConnector\Service\AuthenticationService; use OCA\OpenConnector\Service\MappingService; @@ -62,6 +63,12 @@ public function __construct( */ public function handleRequest(Endpoint $endpoint, IRequest $request, string $path): JSONResponse { + $errors = $this->checkConditions($endpoint, $request); + + if($errors !== []) { + return new JSONResponse(['error' => 'The following parameters are not correctly set', 'fields' => $errors], 400); + } + try { // Check if endpoint connects to a schema if ($endpoint->getTargetType() === 'register/schema') { @@ -299,6 +306,21 @@ private function getHeaders(array $server, bool $proxyHeaders = false): array ); } + private function checkConditions(Endpoint $endpoint, IRequest $request): array + { + $conditions = $endpoint->getConditions(); + $data['parameters'] = $request->getParams(); + $data['headers'] = $this->getHeaders($request->server, true); + + $result = JsonLogic::apply(logic: $conditions, data: $data); + + if($result === true || $result === []) { + return []; + } + + return $result; + } + /** * Handles requests for source-based endpoints * From 922e9fcf8300817a079de7a2dabc782dff0c2477 Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 19 Dec 2024 07:20:43 +0100 Subject: [PATCH 13/20] zgw mapping fixes --- .../mappings/xxllnc-v1-case-to-zgw-zaak.json | 4 +-- .../xxllnc-v1-casetype-to-zgw-zaaktype.json | 5 ++- ...-document-to-zgw-zaakinformatieobject.json | 33 ++++++++++--------- .../xxllnc-v1-rolerequestor-to-zgw-rol.json | 5 ++- ...json => xxllnc-v1-cases-to-zgw-zaken.json} | 6 +++- 5 files changed, 30 insertions(+), 23 deletions(-) rename configurations/xxllnc-zgw/synchronizations/{xxllnc-suite-to-publications copy.json => xxllnc-v1-cases-to-zgw-zaken.json} (66%) diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json index 4f286d30..62fe4e95 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json @@ -20,9 +20,9 @@ "resultaat": "{% if instance.outcome|default %}{% set data = {'outcome': instance.outcome, 'resultaattypen': zaaktype.resultaattypen} %}{{ executeMapping(2, data)|json_encode }}{% endif %}", "rollen": "[{% if instance.route.instance.role|default %}{% set dataRol = {'requestor': instance.requestor, 'role': instance.route.instance.role, 'roltypen': zaaktype.roltypen} %}{{ executeMapping(3, dataRol)|json_encode }}{% endif %}]", "status": "{% if instance.milestone|default %}{% set data = {'milestone': instance.milestone, 'statustypen': zaaktype.statustypen} %}{{ executeMapping(4, data)|json_encode }}{% endif %}", - "zaaktype": "{% if zaaktype['_self']['id']|default %}{{ zaaktype['_self']['id'] }}{% endif %}", + "zaaktype": "{% if zaaktype|default %}{{ executeMapping(7, zaaktype)|json_encode }}{% endif %}", "eigenschappen": "[{% set index=0 %}{% if zaaktype.eigenschappen|default %}{% for key, attribute in instance.attributes %}{% if index != 0 %},{% endif %}{% set data = {'name': key, 'value': attribute, 'eigenschappen': zaaktype.eigenschappen} %}{% if attribute[0][0]|default and attribute[0][0] is iterable %}{{ attribute|json_encode }}{% else %}{{ executeMapping(5, data)|json_encode }}{% endif %}{% set index=index+1 %}{% endfor %}{% endif %}]", - "zaakinformatieobjecten": "[{% set index=0 %}{% if documents|default %}{% for document in documents %}{% if index != 0 %},{% endif %}{{ executeMapping(6, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", + "zaakinformatieobjecten": "[{% set index=0 %}{% if documents.result.instance.rows|default %}{% for document in documents.result.instance.rows %}{% if index != 0 %},{% endif %}{{ executeMapping(6, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", "bronorganisatie": "bronorganisatie", "verantwoordelijkeOrganisatie": "bronorganisatie", "opschorting.indicatie": "{% if instance.stalled_until|default %}true{% else %}{% endif %}", diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json index 0a58fb86..0e4e0d71 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json @@ -24,10 +24,9 @@ "referentieproces.naam": "preview", "verantwoordelijke": "instance.properties.supervisor" }, - "unset": { - "identificatie": "unsetIfValue==instance.legacy.zaaktype_id" - }, + "unset": [], "cast": { + "identificatie": "unsetIfValue==instance.legacy.zaaktype_id", "statustypen": "jsonToArray", "informatieobjecttypen": "jsonToArray", "eigenschappen": "jsonToArray", diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json index 63618a12..3db72eb7 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json @@ -2,27 +2,28 @@ "name": "Xxllnc Document to ZGW ZaakInformatieObject", "version": "0.0.1", "mapping": { - "titel": "result.instance.name", - "registratiedatum": "result.instance.file.instance.date_created", - "informatieobject.identificatie": "result.instance.number", - "informatieobject.creatiedatum": "result.instance.file.instance.date_created", - "informatieobject.titel": "result.instance.name", - "informatieobject.vertrouwelijksheidaanduiding": "{% if result.instance.metadata.instance.trust_level|default %}{{ result.instance.metadata.instance.trust_level|lower }}{% endif %}", - "informatieobject.formaat": "result.instance.file.instance.mimetype", + "titel": "instance.name", + "registratiedatum": "instance.file.instance.date_created", + "informatieobject.identificatie": "instance.number", + "informatieobject.creatiedatum": "instance.file.instance.date_created", + "informatieobject.titel": "instance.name", + "informatieobject.vertrouwelijksheidaanduiding": "{% if instance.metadata.instance.trust_level|default %}{{ instance.metadata.instance.trust_level|lower }}{% endif %}", + "informatieobject.formaat": "instance.file.instance.mimetype", "informatieobject.versie": "1", - "informatieobject.beginRegistratie": "result.instance.file.instance.date_modified", - "informatieobject.bestandsnaam": "result.instance.filename", - "informatieobject.bestandsomvang": "result.instance.file.instance.size", - "informatieobject.verschijningsvorm": "{% if result.instance.metadata.instance.category|default %}{{ result.instance.metadata.instance.category }}{% endif %}", - "informatieobject.integriteit.algoritme": "{% if result.instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ algoritme }}{% endif %}{% endfor %}{% endif %}", - "informatieobject.integriteit.waarde": "{% if result.instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ result.instance.file.instance[algoritme] }}{% endif %}{% endfor %}{% endif %}", - "informatieobject.integriteit.datum": "result.instance.file.instance.date_created", - "informatieobject.inhoud": "inhoud" + "informatieobject.beginRegistratie": "instance.file.instance.date_modified", + "informatieobject.bestandsnaam": "instance.filename", + "informatieobject.bestandsomvang": "instance.file.instance.size", + "informatieobject.verschijningsvorm": "{% if instance.metadata.instance.category|default %}{{ instance.metadata.instance.category }}{% endif %}", + "informatieobject.integriteit.algoritme": "{% if instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ algoritme }}{% endif %}{% endfor %}{% endif %}", + "informatieobject.integriteit.waarde": "{% if instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ result.instance.file.instance[algoritme] }}{% endif %}{% endfor %}{% endif %}", + "informatieobject.integriteit.datum": "instance.file.instance.date_created", + "informatieobject.inhoud": "{\"accessUrl\": \"{{ '/case/'~caseId~'/document/'~reference~'/download' }}\", \"source\": \"2\"}" }, "unset": [], "cast": { "informatieobject.versie": "integer", - "informatieobject.bestandsomvang": "integer" + "informatieobject.bestandsomvang": "integer", + "informatieobject.inhoud": "jsonToArray" }, "passTrough": false } \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json index 5910b347..5369ed0e 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-rolerequestor-to-zgw-rol.json @@ -29,6 +29,9 @@ "roltype": "{% if role.instance.name|default %}{% for roltype in roltypen %}{% if roltype.omschrijvingGeneriek|lower == role.instance.name|lower %}{{ roltype['_self']['id'] }}{% endif %}{% endfor %}{% endif %}" }, "unset": [], - "cast": [], + "cast": { + "roltype": "unsetIfValue==", + "betrokkeneIdentificatie": "unsetIfValue==" + }, "passTrough": false } \ No newline at end of file diff --git a/configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json b/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json similarity index 66% rename from configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json rename to configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json index cd09f29c..0d1dc77e 100644 --- a/configurations/xxllnc-zgw/synchronizations/xxllnc-suite-to-publications copy.json +++ b/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json @@ -13,7 +13,11 @@ "endpoint": "\/case", "query": [], "headers.API-KEY": "", - "headers.API-Interface-ID": "" + "headers.API-Interface-ID": "", + "extraDataConfigs.0.staticEndpoint": "/case/{{ originId }}/document", + "extraDataConfigs.0.mergeExtraData": "true", + "extraDataConfigs.0.keyToSetExtraData": "documents", + "extraDataConfigs.0.resultsLocation": "result.instance.rows" }, "currentPage": 1, "targetId": "1\/1", From 268db673bd1efca6e4c8b20d0534900287fbae0a Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 19 Dec 2024 09:46:00 +0100 Subject: [PATCH 14/20] applyConfigDot for extra data requests --- lib/Service/SynchronizationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index 3f51accd..255079b4 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -201,7 +201,7 @@ public function getObjectFromSource(Synchronization $synchronization, string $en $source = $this->sourceMapper->find(id: $synchronization->getSourceId()); // Lets get the source config - $sourceConfig = $synchronization->getSourceConfig(); + $sourceConfig = $this->callService->applyConfigDot($synchronization->getSourceConfig()); $headers = $sourceConfig['headers'] ?? []; $query = $sourceConfig['query'] ?? []; $config = [ From 59fee5e0f417e838a73332b21104057925a8fb7d Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 19 Dec 2024 10:14:44 +0100 Subject: [PATCH 15/20] Add conditions on endpoints for testing parameters and headers --- appinfo/info.xml | 2 +- lib/Db/Endpoint.php | 7 ++++--- lib/Migration/Version1Date20241218122932.php | 6 +++++- lib/Service/EndpointService.php | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index f04f76a4..1e6ce6bb 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -13,7 +13,7 @@ The OpenConnector Nextcloud app provides a ESB-framework to work together in an - 🆓 Map and translate API calls ]]> - 0.1.26 + 0.1.27 agpl integration Conduction diff --git a/lib/Db/Endpoint.php b/lib/Db/Endpoint.php index 8333009f..6f95cd72 100644 --- a/lib/Db/Endpoint.php +++ b/lib/Db/Endpoint.php @@ -19,6 +19,8 @@ class Endpoint extends Entity implements JsonSerializable protected ?string $method = null; // One of GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD. method and endpoint combination should be unique protected ?string $targetType = null; // The target to attach this endpoint to, should be one of source (to create a proxy endpoint) or register/schema (to create an object endpoint) or job (to fire an event) or synchronization (to create a synchronization endpoint) protected ?string $targetId = null; // The target id to attach this endpoint to + + protected array $conditions = []; protected ?DateTime $created = null; protected ?DateTime $updated = null; @@ -34,9 +36,7 @@ public function __construct() { $this->addType(fieldName:'method', type: 'string'); $this->addType(fieldName:'targetType', type: 'string'); $this->addType(fieldName:'targetId', type: 'string'); - $this->addType(fieldName:'schema', type: 'int'); - $this->addType(fieldName:'register', type: 'int'); - $this->addType(fieldName:'source', type: 'int'); + $this->addType(fieldName:'conditions', type: 'json'); $this->addType(fieldName:'created', type: 'datetime'); $this->addType(fieldName:'updated', type: 'datetime'); } @@ -86,6 +86,7 @@ public function jsonSerialize(): array 'method' => $this->method, 'targetType' => $this->targetType, 'targetId' => $this->targetId, + 'conditions' => $this->conditions, 'created' => isset($this->created) ? $this->created->format('c') : null, 'updated' => isset($this->updated) ? $this->updated->format('c') : null, diff --git a/lib/Migration/Version1Date20241218122932.php b/lib/Migration/Version1Date20241218122932.php index 7c66532e..0da96f8b 100644 --- a/lib/Migration/Version1Date20241218122932.php +++ b/lib/Migration/Version1Date20241218122932.php @@ -43,7 +43,11 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if($schema->hasTable(tableName: 'openconnector_consumers') === true) { $table = $schema->getTable(tableName: 'openconnector_consumers'); $table->addColumn('authorization_configuration', Types::JSON); - $table->addColumn('user_id', Types::STRING); + $table->addColumn('user_id', Types::STRING)->setNotnull(false); + } + if($schema->hasTable(tableName: 'openconnector_endpoints') === true) { + $table = $schema->getTable(tableName: 'openconnector_endpoints'); + $table->addColumn('conditions', Types::JSON); } return $schema; diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index a8a41aee..1ba784e6 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -238,10 +238,12 @@ private function handleSchemaRequest(Endpoint $endpoint, IRequest $request, stri $status = 200; + $headers = $request->getHeader('Accept-Crs') === '' ? [] : ['Content-Crs' => $request->getHeader('Accept-Crs')]; + // Route to appropriate ObjectService method based on HTTP method return match ($method) { 'GET' => new JSONResponse( - $this->getObjects(mapper: $mapper, parameters: $parameters, pathParams: $pathParams, status: $status), statusCode: $status + $this->getObjects(mapper: $mapper, parameters: $parameters, pathParams: $pathParams, status: $status), statusCode: $status, headers: $headers ), 'POST' => new JSONResponse( $mapper->createFromArray(object: $parameters) From b2a4b213f72dc988a421de7a2bb1d1c108a0ea5c Mon Sep 17 00:00:00 2001 From: Robert Zondervan Date: Thu, 19 Dec 2024 10:18:58 +0100 Subject: [PATCH 16/20] Add docblock --- lib/Service/EndpointService.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Service/EndpointService.php b/lib/Service/EndpointService.php index 1ba784e6..ea778512 100644 --- a/lib/Service/EndpointService.php +++ b/lib/Service/EndpointService.php @@ -308,6 +308,14 @@ private function getHeaders(array $server, bool $proxyHeaders = false): array ); } + /** + * Check conditions for using an endpoint. + * + * @param Endpoint $endpoint The endpoint for which the checks should be done. + * @param IRequest $request The inbound request. + * @return array + * @throws Exception + */ private function checkConditions(Endpoint $endpoint, IRequest $request): array { $conditions = $endpoint->getConditions(); From 8b51f8965a577f100d763ddef9bfcf8ca8ae490b Mon Sep 17 00:00:00 2001 From: Wilco Louwerse Date: Thu, 19 Dec 2024 10:37:26 +0100 Subject: [PATCH 17/20] Pr review --- lib/Controller/EndpointsController.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Controller/EndpointsController.php b/lib/Controller/EndpointsController.php index 2bbab0be..07716953 100644 --- a/lib/Controller/EndpointsController.php +++ b/lib/Controller/EndpointsController.php @@ -2,6 +2,7 @@ namespace OCA\OpenConnector\Controller; +use Exception; use OCA\OpenConnector\Exception\AuthenticationException; use OCA\OpenConnector\Service\AuthorizationService; use OCA\OpenConnector\Service\ObjectService; @@ -123,7 +124,7 @@ public function create(): JSONResponse } } - if (isset($data['id'])) { + if (isset($data['id']) === true) { unset($data['id']); } @@ -140,7 +141,7 @@ public function create(): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the endpoint to update + * @param int $id The ID of the endpoint to update * @return JSONResponse A JSON response containing the updated endpoint details */ public function update(int $id): JSONResponse @@ -152,7 +153,7 @@ public function update(int $id): JSONResponse unset($data[$key]); } } - if (isset($data['id'])) { + if (isset($data['id']) === true) { unset($data['id']); } @@ -169,8 +170,9 @@ public function update(int $id): JSONResponse * @NoAdminRequired * @NoCSRFRequired * - * @param string $id The ID of the endpoint to delete + * @param int $id The ID of the endpoint to delete * @return JSONResponse An empty JSON response + * @throws \OCP\DB\Exception */ public function destroy(int $id): JSONResponse { @@ -189,8 +191,9 @@ public function destroy(int $id): JSONResponse * @NoCSRFRequired * @PublicPage * - * @param string $path The request path to match + * @param string $_path * @return JSONResponse The response from the endpoint service or 404 if no match + * @throws Exception */ public function handlePath(string $_path): JSONResponse { From 66ddae31340dbc3ae600c452a9bddcaeb188773b Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 19 Dec 2024 10:47:04 +0100 Subject: [PATCH 18/20] Fix originId and extra fetching --- .../mappings/xxllnc-v1-case-to-zgw-zaak.json | 9 +++++---- .../xxllnc-v1-milestone-to-zgw-status.json | 3 ++- .../xxllnc-v1-outcome-to-zgw-resultaat.json | 4 +++- .../xxllnc-v1-cases-to-zgw-zaken.json | 5 ++++- lib/Service/SynchronizationService.php | 20 +++++++++++++++---- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json index 62fe4e95..a86c02b0 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json @@ -20,9 +20,9 @@ "resultaat": "{% if instance.outcome|default %}{% set data = {'outcome': instance.outcome, 'resultaattypen': zaaktype.resultaattypen} %}{{ executeMapping(2, data)|json_encode }}{% endif %}", "rollen": "[{% if instance.route.instance.role|default %}{% set dataRol = {'requestor': instance.requestor, 'role': instance.route.instance.role, 'roltypen': zaaktype.roltypen} %}{{ executeMapping(3, dataRol)|json_encode }}{% endif %}]", "status": "{% if instance.milestone|default %}{% set data = {'milestone': instance.milestone, 'statustypen': zaaktype.statustypen} %}{{ executeMapping(4, data)|json_encode }}{% endif %}", - "zaaktype": "{% if zaaktype|default %}{{ executeMapping(7, zaaktype)|json_encode }}{% endif %}", - "eigenschappen": "[{% set index=0 %}{% if zaaktype.eigenschappen|default %}{% for key, attribute in instance.attributes %}{% if index != 0 %},{% endif %}{% set data = {'name': key, 'value': attribute, 'eigenschappen': zaaktype.eigenschappen} %}{% if attribute[0][0]|default and attribute[0][0] is iterable %}{{ attribute|json_encode }}{% else %}{{ executeMapping(5, data)|json_encode }}{% endif %}{% set index=index+1 %}{% endfor %}{% endif %}]", - "zaakinformatieobjecten": "[{% set index=0 %}{% if documents.result.instance.rows|default %}{% for document in documents.result.instance.rows %}{% if index != 0 %},{% endif %}{{ executeMapping(6, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", + "zaaktype": "{% if casetype|default %}{{ executeMapping(5, casetype)|json_encode }}{% endif %}", + "eigenschappen": "[{% set index=0 %}{% if zaaktype.eigenschappen|default %}{% for key, attribute in instance.attributes %}{% if index != 0 %},{% endif %}{% set data = {'name': key, 'value': attribute, 'eigenschappen': zaaktype.eigenschappen} %}{% if attribute[0][0]|default and attribute[0][0] is iterable %}{{ attribute|json_encode }}{% else %}{{ executeMapping(6, data)|json_encode }}{% endif %}{% set index=index+1 %}{% endfor %}{% endif %}]", + "zaakinformatieobjecten": "[{% set index=0 %}{% if documents.result.instance.rows|default %}{% for document in documents.result.instance.rows %}{% if index != 0 %},{% endif %}{{ executeMapping(7, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", "bronorganisatie": "bronorganisatie", "verantwoordelijkeOrganisatie": "bronorganisatie", "opschorting.indicatie": "{% if instance.stalled_until|default %}true{% else %}{% endif %}", @@ -37,7 +37,8 @@ "opschorting.indicatie": "unsetIfValue==", "opschorting.reden": "unsetIfValue==", "opschorting": "unsetIfValue==", - "zaakinformatieobjecten": "jsonToArray" + "zaakinformatieobjecten": "jsonToArray", + "zaaktype": "jsonToArray" }, "passTrough": false } \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json index b7409599..ce2564de 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-milestone-to-zgw-status.json @@ -9,7 +9,8 @@ }, "unset": [], "cast": { - "indicatieLaatstGezetteStatus": "bool" + "indicatieLaatstGezetteStatus": "bool", + "statustype": "unsetIfValue==" }, "passTrough": false } \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json index 4ff8c3ec..ea9754ab 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-outcome-to-zgw-resultaat.json @@ -6,6 +6,8 @@ "resultaattype": "{% if resultaattypen|default %}{% for resultaattype in resultaattypen %}{% if resultaattype.toelichting == outcome.instance.name %}{{ resultaattype['_self']['id'] }}{% endif %}{% endfor %}{% endif %}" }, "unset": [], - "cast": [], + "cast": { + "resultaattype": "unsetIfValue==" + }, "passTrough": false } \ No newline at end of file diff --git a/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json b/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json index 0d1dc77e..660461e5 100644 --- a/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json +++ b/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json @@ -17,7 +17,10 @@ "extraDataConfigs.0.staticEndpoint": "/case/{{ originId }}/document", "extraDataConfigs.0.mergeExtraData": "true", "extraDataConfigs.0.keyToSetExtraData": "documents", - "extraDataConfigs.0.resultsLocation": "result.instance.rows" + "extraDataConfigs.1.staticEndpoint": "/casetype/{{ originId }}", + "extraDataConfigs.1.endpointIdLocation": "instance.casetype.reference", + "extraDataConfigs.1.mergeExtraData": "true", + "extraDataConfigs.1.keyToSetExtraData": "casetype" }, "currentPage": 1, "targetId": "1\/1", diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index 255079b4..b00daf8f 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -234,12 +234,13 @@ public function getObjectFromSource(Synchronization $synchronization, string $en * - KEY_FOR_EXTRA_DATA_LOCATION: The key under which the extra data should be returned. * - MERGE_EXTRA_DATA_OBJECT_LOCATION: Boolean flag indicating whether to merge the extra data with the object. * @param array $object The original object for which extra data needs to be fetched. + * @param string|null $originId * * @return array The original object merged with the extra data, or the extra data itself based on the configuration. * * @throws Exception If both dynamic and static endpoint configurations are missing or the endpoint cannot be determined. */ - private function fetchExtraDataForObject(Synchronization $synchronization, array $extraDataConfig, array $object) + private function fetchExtraDataForObject(Synchronization $synchronization, array $extraDataConfig, array $object, ?string $originId = null) { if (isset($extraDataConfig[$this::EXTRA_DATA_DYNAMIC_ENDPOINT_LOCATION]) === false && isset($extraDataConfig[$this::EXTRA_DATA_STATIC_ENDPOINT_LOCATION]) === false) { return $object; @@ -253,9 +254,20 @@ private function fetchExtraDataForObject(Synchronization $synchronization, array // Get endpoint static defined in config. if (isset($extraDataConfig[$this::EXTRA_DATA_STATIC_ENDPOINT_LOCATION]) === true) { + + if ($originId === null) { + $originId = $this->getOriginId($synchronization, $object); + } + + if (isset($extraDataConfig['endpointIdLocation']) === true) { + $dotObject = new Dot($object); + $originId = $dotObject->get($extraDataConfig['endpointIdLocation']); + } + + $endpoint = $extraDataConfig[$this::EXTRA_DATA_STATIC_ENDPOINT_LOCATION]; - $endpoint = str_replace(search: '{{ originId }}', replace: $this->getOriginId($synchronization, $object), subject: $endpoint); - $endpoint = str_replace(search: '{{originId}}', replace: $this->getOriginId($synchronization, $object), subject: $endpoint); + $endpoint = str_replace(search: '{{ originId }}', replace: $originId, subject: $endpoint); + $endpoint = str_replace(search: '{{originId}}', replace: $originId, subject: $endpoint); } if (!$endpoint) { @@ -275,7 +287,7 @@ private function fetchExtraDataForObject(Synchronization $synchronization, array $results = $dotObject->get($extraDataConfig['resultsLocation']); foreach ($results as $key => $result) { - $results[$key] = $this->fetchExtraDataForObject($synchronization, $extraDataConfig['extraDataConfigPerResult'], $result); + $results[$key] = $this->fetchExtraDataForObject(synchronization: $synchronization, extraDataConfig: $extraDataConfig['extraDataConfigPerResult'], object: $result, originId: $originId); } $extraData = $results; From 9babc0d7bcb14c5c3d74cee7c7d1e646916eacf9 Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 19 Dec 2024 11:52:04 +0100 Subject: [PATCH 19/20] Mapping fixes --- .../mappings/xxllnc-v1-case-to-zgw-zaak.json | 2 +- .../mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json | 12 +----------- ...llnc-v1-document-to-zgw-zaakinformatieobject.json | 2 +- .../xxllnc-v1-cases-to-zgw-zaken.json | 4 +--- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json index a86c02b0..129c219f 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-case-to-zgw-zaak.json @@ -22,7 +22,7 @@ "status": "{% if instance.milestone|default %}{% set data = {'milestone': instance.milestone, 'statustypen': zaaktype.statustypen} %}{{ executeMapping(4, data)|json_encode }}{% endif %}", "zaaktype": "{% if casetype|default %}{{ executeMapping(5, casetype)|json_encode }}{% endif %}", "eigenschappen": "[{% set index=0 %}{% if zaaktype.eigenschappen|default %}{% for key, attribute in instance.attributes %}{% if index != 0 %},{% endif %}{% set data = {'name': key, 'value': attribute, 'eigenschappen': zaaktype.eigenschappen} %}{% if attribute[0][0]|default and attribute[0][0] is iterable %}{{ attribute|json_encode }}{% else %}{{ executeMapping(6, data)|json_encode }}{% endif %}{% set index=index+1 %}{% endfor %}{% endif %}]", - "zaakinformatieobjecten": "[{% set index=0 %}{% if documents.result.instance.rows|default %}{% for document in documents.result.instance.rows %}{% if index != 0 %},{% endif %}{{ executeMapping(7, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", + "zaakinformatieobjecten": "[{% set index=0 %}{% if documents.result.instance.rows|default %}{% for document in documents.result.instance.rows %}{% set document = document|merge({'caseId': reference}) %}{% if index != 0 %},{% endif %}{{ executeMapping(7, document)|json_encode }}{% set index=index+1 %}{% endfor %}{% endif %}]", "bronorganisatie": "bronorganisatie", "verantwoordelijkeOrganisatie": "bronorganisatie", "opschorting.indicatie": "{% if instance.stalled_until|default %}true{% else %}{% endif %}", diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json index 0e4e0d71..1d4e5f02 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-casetype-to-zgw-zaaktype.json @@ -13,11 +13,6 @@ "publicatieIndicatie": "instance.properties.publication", "omschrijving": "instance.title", "opschortingEnAanhoudingMogelijk": "instance.properties.suspension", - "statustypen": "[{% for phase in instance.phases %}{% set phase = phase|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if not loop.first %},{% endif %}{{ executeMapping(7, phase)|json_encode }}{% endfor %}]", - "informatieobjecttypen": "[{% set index=0 %}{% for phase in instance.phases %}{% for field in phase.fields %}{% if field.type == 'file' %}{% set field = field|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if index != 0 %},{% endif %}{{ executMapping(8, field)|json_encode }}{% set index=index+1 %}{% endif %}{% endfor %}{% endfor %}]", - "eigenschappen": "[{% set index=0 %}{% for phase in instance.phases %}{% for field in phase.fields %}{% if field.type != 'file' %}{% set field = field|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if index != 0 %},{% endif %}{{ executMapping(9, field)|json_encode }}{% set index=index+1 %}{% endif %}{% endfor %}{% endfor %}]", - "roltypen": "[{% set index=0 %}{% for phase in instance.phases %}{% if phase.route.role|default %}{% set role = phase.route.role|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if index != 0 %},{% endif %}{{ executMapping(10, role)|json_encode }}{% set index=index+1 %}{% endif %}{% endfor %}]", - "resultaattypen": "[{% if instance.results|default %}{% for result in instance.results %}{% set result = result|merge({'_catalogus': _catalogus, '_zaaktypeIdentificatie': instance.legacy.zaaktype_id}) %}{% if not loop.first %},{% endif %}{{ executMapping(11, result)|json_encode }}{% endfor %}{% endif %}]", "bronzaaktype.url": "url", "bronzaaktype.identificatie": "reference", "bronzaaktype.omschrijving": "instance.title", @@ -26,12 +21,7 @@ }, "unset": [], "cast": { - "identificatie": "unsetIfValue==instance.legacy.zaaktype_id", - "statustypen": "jsonToArray", - "informatieobjecttypen": "jsonToArray", - "eigenschappen": "jsonToArray", - "roltypen": "jsonToArray", - "resultaattypen": "jsonToArray" + "identificatie": "unsetIfValue==instance.legacy.zaaktype_id" }, "passTrough": false } \ No newline at end of file diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json b/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json index 3db72eb7..56b8a012 100644 --- a/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json +++ b/configurations/xxllnc-zgw/mappings/xxllnc-v1-document-to-zgw-zaakinformatieobject.json @@ -17,7 +17,7 @@ "informatieobject.integriteit.algoritme": "{% if instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ algoritme }}{% endif %}{% endfor %}{% endif %}", "informatieobject.integriteit.waarde": "{% if instance.file.instance|default %}{% set algoritmeArray = ['crc_16','crc_32','crc_64','fletcher4','fletcher8','fletcher16','fletcher32','hmac','md5','sha_1','sha_256','sha_512','sha_3'] %}{% for algoritme in algoritmeArray %}{% if result.instance.file.instance[algoritme]|default %}{{ result.instance.file.instance[algoritme] }}{% endif %}{% endfor %}{% endif %}", "informatieobject.integriteit.datum": "instance.file.instance.date_created", - "informatieobject.inhoud": "{\"accessUrl\": \"{{ '/case/'~caseId~'/document/'~reference~'/download' }}\", \"source\": \"2\"}" + "informatieobject.inhoud": "{\"downloadUrl\": \"{{ 'https://xxllncapiv1/case/'~caseId~'/document/'~reference~'/download' }}\", \"source\": \"2\"}" }, "unset": [], "cast": { diff --git a/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json b/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json index 660461e5..c6fbc6cb 100644 --- a/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json +++ b/configurations/xxllnc-zgw/synchronizations/xxllnc-v1-cases-to-zgw-zaken.json @@ -11,9 +11,7 @@ "idPosition": "reference", "resultsPosition": "result.instance.rows", "endpoint": "\/case", - "query": [], - "headers.API-KEY": "", - "headers.API-Interface-ID": "", + "query.zql": "select {} from case where case.casetype.id=", "extraDataConfigs.0.staticEndpoint": "/case/{{ originId }}/document", "extraDataConfigs.0.mergeExtraData": "true", "extraDataConfigs.0.keyToSetExtraData": "documents", From fa8be6b8fa7508d40f71579e9c5312c2cc240745 Mon Sep 17 00:00:00 2001 From: Barry Brands Date: Thu, 19 Dec 2024 11:56:09 +0100 Subject: [PATCH 20/20] remove copy --- .../xxllnc-suite-to-publications copy.json | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json diff --git a/configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json b/configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json deleted file mode 100644 index 57ab360a..00000000 --- a/configurations/xxllnc-zgw/mappings/xxllnc-suite-to-publications copy.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "Xxllnc suite to Publication", - "version": "0.0.1", - "mapping": { - "title": "omschrijving", - "summary": "zaaktypeomschrijving", - "description": "zaaktypeomschrijving", - "category": "{% if zaaktypecode|default %}{% set wooVerzoekenEnBesluiten = ['LP00000431', 'B1873'] %}{% set klachtoordelen = ['LP00000091', 'LP00001132', 'LP00000121', 'B0757', 'LP00000832', 'LP00001096'] %}{% if zaaktypecode in wooVerzoekenEnBesluiten %}{{ 'Woo-verzoeken en -besluiten' }}{% elseif zaaktypecode in klachtoordelen %}{{ 'Klachtoordelen' }}{% endif %}{% endif %}", - "published": "startdatum", - "modified": "{{ 'now'|date('H:i:sTm-d-Y') }}", - "attachments": "[{% if files|default %}{% for file in files %} { {% if file['titel']|default %}\"title\": \"{{ file['titel'] }}\",{% endif %}\"labels\": [\"{{ 'Informatieverzoek' }}\"],{% if file['formaat']|default %}\"extension\": \"{{ file['formaat']|split('/')|last }}\",\"type\": \"{{ file['formaat'] }}\",{% endif %}{% if file['inhoud']|default and file['formaat']|default %}\"accessUrl\": \"data:{{ file['formaat'] }};base64,{{ file.inhoud }}\"{% endif %} }{{ loop.last ? '' : ',' }} {% endfor %}{% endif %}]", - "status": "Concept" - }, - "unset": [ - "" - ], - "cast": { - "title": "unsetIfValue==omschrijving", - "summary": "unsetIfValue==zaaktypeomschrijving", - "description": "unsetIfValue==zaaktypeomschrijving", - "category": "unsetIfValue==", - "published": "unsetIfValue==startdatum", - "attachments": "jsonToArray" - }, - "passThrough": false -} \ No newline at end of file