From 14c9c4f92301e9a2e37ac252c915f03cfeb068be Mon Sep 17 00:00:00 2001 From: jld3103 Date: Wed, 25 Jan 2023 15:07:56 +0100 Subject: [PATCH] cloud_federation_api: Introduce OpenAPI spec Signed-off-by: jld3103 --- .../Controller/RequestHandlerController.php | 131 +++----- .../Response/CloudFederationAddShare.php | 37 +++ .../Model/Response/CloudFederationError.php | 37 +++ .../CloudFederationValidationError.php | 41 +++ apps/cloud_federation_api/openapi.json | 311 ++++++++++++++++++ 5 files changed, 473 insertions(+), 84 deletions(-) create mode 100644 apps/cloud_federation_api/lib/Model/Response/CloudFederationAddShare.php create mode 100644 apps/cloud_federation_api/lib/Model/Response/CloudFederationError.php create mode 100644 apps/cloud_federation_api/lib/Model/Response/CloudFederationValidationError.php create mode 100644 apps/cloud_federation_api/openapi.json diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index ef77f2fa317de..0cc92952ba45d 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -5,6 +5,7 @@ * @author Bjoern Schiessle * @author Christoph Wurst * @author Roeland Jago Douma + * @author Kate Döen * * @license GNU AGPL version 3 or any later version * @@ -22,9 +23,14 @@ * along with this program. If not, see . * */ + namespace OCA\CloudFederationAPI\Controller; +use Exception; use OCA\CloudFederationAPI\Config; +use OCA\CloudFederationAPI\Model\Response\CloudFederationAddShare; +use OCA\CloudFederationAPI\Model\Response\CloudFederationError; +use OCA\CloudFederationAPI\Model\Response\CloudFederationValidationError; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -41,6 +47,7 @@ use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Util; use Psr\Log\LoggerInterface; /** @@ -108,46 +115,36 @@ public function __construct($appName, * * @param string $shareWith * @param string $name resource name (e.g. document.odt) - * @param string $description share description (optional) + * @param string|null $description share description * @param string $providerId resource UID on the provider side * @param string $owner provider specific UID of the user who owns the resource - * @param string $ownerDisplayName display name of the user who shared the item - * @param string $sharedBy provider specific UID of the user who shared the resource - * @param string $sharedByDisplayName display name of the user who shared the resource - * @param array $protocol (e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]) - * @param string $shareType ('group' or 'user' share) - * @param $resourceType ('file', 'calendar',...) - * @return Http\DataResponse|JSONResponse + * @param string|null $ownerDisplayName display name of the user who shared the item + * @param string|null $sharedBy provider specific UID of the user who shared the resource + * @param string|null $sharedByDisplayName display name of the user who shared the resource + * @param array{name: string[], options: array{}} $protocol e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]] + * @param string $shareType 'group' or 'user' share + * @param string $resourceType 'file', 'calendar',... + * + * @return JSONResponse 201 The notification was successfully received. The display name of the recepient might be returned in the body. + * @return JSONResponse 400 Bad request due to invalid parameters, e.g. when `shareWith` is not found or required properties are missing. + * @return JSONResponse 501 Share type or the resource type is not supported. * * Example: curl -H "Content-Type: application/json" -X POST -d '{"shareWith":"admin1@serve1","name":"welcome server2.txt","description":"desc","providerId":"2","owner":"admin2@http://localhost/server2","ownerDisplayName":"admin2 display","shareType":"user","resourceType":"file","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' http://localhost/server/index.php/ocm/shares */ - public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { + public function addShare(string $shareWith, string $name, ?string $description, string $providerId, string $owner, ?string $ownerDisplayName, ?string $sharedBy, ?string $sharedByDisplayName, array $protocol, string $shareType, string $resourceType) { // check if all required parameters are set - if ($shareWith === null || - $name === null || - $providerId === null || - $owner === null || - $resourceType === null || - $shareType === null || - !is_array($protocol) || - !isset($protocol['name']) || + if (!isset($protocol['name']) || !isset($protocol['options']) || !is_array($protocol['options']) || !isset($protocol['options']['sharedSecret']) ) { - return new JSONResponse( - ['message' => 'Missing arguments'], - Http::STATUS_BAD_REQUEST - ); + return new JSONResponse(new CloudFederationValidationError(message: 'Missing arguments'), Http::STATUS_BAD_REQUEST); } $supportedShareTypes = $this->config->getSupportedShareTypes($resourceType); if (!in_array($shareType, $supportedShareTypes)) { - return new JSONResponse( - ['message' => 'Share type "' . $shareType . '" not implemented'], - Http::STATUS_NOT_IMPLEMENTED - ); + return new JSONResponse(new CloudFederationError(message: 'Share type "' . $shareType . '" not implemented'), Http::STATUS_NOT_IMPLEMENTED); } $cloudId = $this->cloudIdManager->resolveCloudId($shareWith); @@ -157,10 +154,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ $shareWith = $this->mapUid($shareWith); if (!$this->userManager->userExists($shareWith)) { - $response = new JSONResponse( - ['message' => 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], - Http::STATUS_BAD_REQUEST - ); + $response = new JSONResponse(new CloudFederationValidationError(message: 'User "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()), Http::STATUS_BAD_REQUEST); $response->throttle(); return $response; } @@ -168,10 +162,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ if ($shareType === 'group') { if (!$this->groupManager->groupExists($shareWith)) { - $response = new JSONResponse( - ['message' => 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()], - Http::STATUS_BAD_REQUEST - ); + $response = new JSONResponse(new CloudFederationValidationError(message: 'Group "' . $shareWith . '" does not exists at ' . $this->urlGenerator->getBaseUrl()), Http::STATUS_BAD_REQUEST); $response->throttle(); return $response; } @@ -193,21 +184,12 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ $share->setProtocol($protocol); $provider->shareReceived($share); } catch (ProviderDoesNotExistsException $e) { - return new JSONResponse( - ['message' => $e->getMessage()], - Http::STATUS_NOT_IMPLEMENTED - ); + return new JSONResponse(new CloudFederationError(message: $e->getMessage()), Http::STATUS_NOT_IMPLEMENTED); } catch (ProviderCouldNotAddShareException $e) { - return new JSONResponse( - ['message' => $e->getMessage()], - $e->getCode() - ); - } catch (\Exception $e) { + return new JSONResponse(new CloudFederationError(message: $e->getMessage()), $e->getCode()); + } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); - return new JSONResponse( - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], - Http::STATUS_BAD_REQUEST - ); + return new JSONResponse(new CloudFederationValidationError(message: 'Internal error at ' . $this->urlGenerator->getBaseUrl()), Http::STATUS_BAD_REQUEST); } $user = $this->userManager->get($shareWith); @@ -216,9 +198,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ $recipientDisplayName = $user->getDisplayName(); } - return new JSONResponse( - ['recipientDisplayName' => $recipientDisplayName], - Http::STATUS_CREATED); + return new JSONResponse(new CloudFederationAddShare($recipientDisplayName), Http::STATUS_CREATED); } /** @@ -228,60 +208,43 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ * @PublicPage * @BruteForceProtection(action=receiveFederatedShareNotification) * - * @param string $notificationType (notification type, e.g. SHARE_ACCEPTED) - * @param string $resourceType (calendar, file, contact,...) - * @param string $providerId id of the share - * @param array $notification the actual payload of the notification - * @return JSONResponse + * @param string $notificationType notification type, e.g. SHARE_ACCEPTED + * @param string $resourceType calendar, file, contact,... + * @param string|null $providerId id of the share + * @param array{}|null $notification the actual payload of the notification + * @return JSONResponse 201 The notification was successfully received + * @return JSONResponse 400 Bad request due to invalid parameters, e.g. when `type` is invalid or missing. + * @return JSONResponse 501 The resource type is not supported. */ - public function receiveNotification($notificationType, $resourceType, $providerId, array $notification) { + public function receiveNotification(string $notificationType, string $resourceType, ?string $providerId, ?array $notification) { // check if all required parameters are set - if ($notificationType === null || - $resourceType === null || - $providerId === null || - !is_array($notification) - ) { - return new JSONResponse( - ['message' => 'Missing arguments'], - Http::STATUS_BAD_REQUEST - ); + if ($providerId === null || !is_array($notification)) { + return new JSONResponse(new CloudFederationValidationError(message: 'Missing arguments'), Http::STATUS_BAD_REQUEST); } try { $provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType); $result = $provider->notificationReceived($notificationType, $providerId, $notification); } catch (ProviderDoesNotExistsException $e) { - return new JSONResponse( - ['message' => $e->getMessage()], - Http::STATUS_BAD_REQUEST - ); + return new JSONResponse(new CloudFederationValidationError(message: $e->getMessage()), Http::STATUS_BAD_REQUEST); } catch (ShareNotFound $e) { - $response = new JSONResponse( - ['message' => $e->getMessage()], - Http::STATUS_BAD_REQUEST - ); + $response = new JSONResponse(new CloudFederationValidationError(message: $e->getMessage()), Http::STATUS_BAD_REQUEST); $response->throttle(); return $response; } catch (ActionNotSupportedException $e) { - return new JSONResponse( - ['message' => $e->getMessage()], - Http::STATUS_NOT_IMPLEMENTED - ); + return new JSONResponse(new CloudFederationError(message: $e->getMessage()), Http::STATUS_NOT_IMPLEMENTED); } catch (BadRequestException $e) { return new JSONResponse($e->getReturnMessage(), Http::STATUS_BAD_REQUEST); } catch (AuthenticationFailedException $e) { - $response = new JSONResponse(['message' => 'RESOURCE_NOT_FOUND'], Http::STATUS_FORBIDDEN); + $response = new JSONResponse(new CloudFederationError(message: 'RESOURCE_NOT_FOUND'), Http::STATUS_FORBIDDEN); $response->throttle(); return $response; - } catch (\Exception $e) { - return new JSONResponse( - ['message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl()], - Http::STATUS_BAD_REQUEST - ); + } catch (Exception $e) { + return new JSONResponse(new CloudFederationValidationError(message: 'Internal error at ' . $this->urlGenerator->getBaseUrl()), Http::STATUS_BAD_REQUEST); } - return new JSONResponse($result,Http::STATUS_CREATED); + return new JSONResponse($result, Http::STATUS_CREATED); } /** @@ -293,7 +256,7 @@ public function receiveNotification($notificationType, $resourceType, $providerI private function mapUid($uid) { // FIXME this should be a method in the user management instead $this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]); - \OCP\Util::emitHook( + Util::emitHook( '\OCA\Files_Sharing\API\Server2Server', 'preLoginNameUsedAsUserName', ['uid' => &$uid] diff --git a/apps/cloud_federation_api/lib/Model/Response/CloudFederationAddShare.php b/apps/cloud_federation_api/lib/Model/Response/CloudFederationAddShare.php new file mode 100644 index 0000000000000..deda39f666e15 --- /dev/null +++ b/apps/cloud_federation_api/lib/Model/Response/CloudFederationAddShare.php @@ -0,0 +1,37 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\CloudFederationAPI\Model\Response; + +use JsonSerializable; + +class CloudFederationAddShare implements JsonSerializable { + public function __construct(protected ?string $recipientDisplayName) { + } + + public function jsonSerialize(): array { + return ['recipientDisplayName' => $this->recipientDisplayName]; + } +} diff --git a/apps/cloud_federation_api/lib/Model/Response/CloudFederationError.php b/apps/cloud_federation_api/lib/Model/Response/CloudFederationError.php new file mode 100644 index 0000000000000..6fc38156637d5 --- /dev/null +++ b/apps/cloud_federation_api/lib/Model/Response/CloudFederationError.php @@ -0,0 +1,37 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\CloudFederationAPI\Model\Response; + +use JsonSerializable; + +class CloudFederationError implements JsonSerializable { + public function __construct(protected string $message) { + } + + public function jsonSerialize(): array { + return ['message' => $this->message]; + } +} diff --git a/apps/cloud_federation_api/lib/Model/Response/CloudFederationValidationError.php b/apps/cloud_federation_api/lib/Model/Response/CloudFederationValidationError.php new file mode 100644 index 0000000000000..cb28333abce6e --- /dev/null +++ b/apps/cloud_federation_api/lib/Model/Response/CloudFederationValidationError.php @@ -0,0 +1,41 @@ + + * + * @author Kate Döen + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\CloudFederationAPI\Model\Response; + +use JsonSerializable; + +class CloudFederationValidationError extends CloudFederationError implements JsonSerializable { + /** + * @param array{name: string|null, message: string|null}[]|null $validationErrors + */ + public function __construct(string $message, protected ?array $validationErrors = null) { + parent::__construct($message); + } + + public function jsonSerialize(): array { + return array_merge(parent::jsonSerialize(), ['validationErrors' => $this->validationErrors]); + } +} diff --git a/apps/cloud_federation_api/openapi.json b/apps/cloud_federation_api/openapi.json new file mode 100644 index 0000000000000..f435f3c12ee05 --- /dev/null +++ b/apps/cloud_federation_api/openapi.json @@ -0,0 +1,311 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Cloud Federation API", + "description": "Enable clouds to communicate with each other and exchange data", + "license": { + "name": "agpl" + }, + "version": "1.9.0" + }, + "paths": { + "/index.php/ocm/shares": { + "post": { + "tags": [ + "cloud_federation_api" + ], + "summary": "add share", + "description": "Example: curl -H \"Content-Type: application/json\" -X POST -d '{\"shareWith\":\"admin1@serve1\",\"name\":\"welcome server2.txt\",\"description\":\"desc\",\"providerId\":\"2\",\"owner\":\"admin2@http://localhost/server2\",\"ownerDisplayName\":\"admin2 display\",\"shareType\":\"user\",\"resourceType\":\"file\",\"protocol\":{\"name\":\"webdav\",\"options\":{\"sharedSecret\":\"secret\",\"permissions\":\"webdav-property\"}}}' http://localhost/server/index.php/ocm/shares", + "operationId": "add-share", + "parameters": [ + { + "name": "shareWith", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "description": "resource name (e.g. document.odt)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "description", + "in": "query", + "description": "share description", + "schema": { + "type": "string" + } + }, + { + "name": "providerId", + "in": "query", + "description": "resource UID on the provider side", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "description": "provider specific UID of the user who owns the resource", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "ownerDisplayName", + "in": "query", + "description": "display name of the user who shared the item", + "schema": { + "type": "string" + } + }, + { + "name": "sharedBy", + "in": "query", + "description": "provider specific UID of the user who shared the resource", + "schema": { + "type": "string" + } + }, + { + "name": "sharedByDisplayName", + "in": "query", + "description": "display name of the user who shared the resource", + "schema": { + "type": "string" + } + }, + { + "name": "protocol", + "in": "query", + "description": "e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]", + "required": true, + "schema": { + "required": [ + "name", + "options" + ], + "type": "object", + "properties": { + "name": { + "type": "array", + "items": { + "type": "string" + } + }, + "options": { + "type": "object", + "additionalProperties": true + } + } + } + }, + { + "name": "shareType", + "in": "query", + "description": "'group' or 'user' share", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "resourceType", + "in": "query", + "description": "'file', 'calendar',...", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "The notification was successfully received. The display name of the recepient might be returned in the body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloudFederationAddShare" + } + } + } + }, + "400": { + "description": "Bad request due to invalid parameters, e.g. when `shareWith` is not found or required properties are missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloudFederationValidationError" + } + } + } + }, + "501": { + "description": "Share type or the resource type is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloudFederationError" + } + } + } + } + } + } + }, + "/index.php/ocm/notifications": { + "post": { + "tags": [ + "cloud_federation_api" + ], + "description": "receive notification about existing share", + "operationId": "receive-notification", + "parameters": [ + { + "name": "notificationType", + "in": "query", + "description": "notification type, e.g. SHARE_ACCEPTED", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "resourceType", + "in": "query", + "description": "calendar, file, contact,...", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "providerId", + "in": "query", + "description": "id of the share", + "schema": { + "type": "string" + } + }, + { + "name": "notification", + "in": "query", + "description": "the actual payload of the notification", + "schema": { + "type": "object", + "additionalProperties": true + } + } + ], + "responses": { + "201": { + "description": "The notification was successfully received", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "400": { + "description": "Bad request due to invalid parameters, e.g. when `type` is invalid or missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloudFederationValidationError" + } + } + } + }, + "501": { + "description": "The resource type is not supported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloudFederationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CloudFederationAddShare": { + "type": "object", + "properties": { + "recipientDisplayName": { + "type": "string" + } + } + }, + "CloudFederationError": { + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "CloudFederationValidationError": { + "allOf": [ + { + "$ref": "#/components/schemas/CloudFederationError" + }, + { + "type": "object", + "properties": { + "validationErrors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + ] + } + }, + "securitySchemes": { + "basic_auth": { + "type": "http", + "scheme": "basic" + } + } + }, + "security": [ + { + "basic_auth": [] + } + ], + "tags": [ + { + "name": "cloud_federation_api" + } + ] +} \ No newline at end of file