From fbf372d856686892b26a3bb573ad0ea2e66a156d Mon Sep 17 00:00:00 2001 From: jjrom Date: Wed, 3 Apr 2024 18:45:43 +0200 Subject: [PATCH 1/9] Add resto-addon-groups to resto core --- app/resto/core/RestoCollections.php | 5 +- app/resto/core/RestoRouter.php | 82 ++-- app/resto/core/RestoUser.php | 47 +- app/resto/core/addons/Group.php | 436 ++++++++++++++++++ app/resto/core/api/UsersAPI.php | 9 - .../core/dbfunctions/FiltersFunctions.php | 2 +- .../core/dbfunctions/GroupsFunctions.php | 177 +++++++ .../core/dbfunctions/RightsFunctions.php | 2 +- app/resto/core/dbfunctions/UsersFunctions.php | 128 +---- .../cont-init.d/18-resto-admin-user.php | 7 +- .../02_resto_common_model.sql | 21 +- 11 files changed, 733 insertions(+), 183 deletions(-) create mode 100755 app/resto/core/addons/Group.php create mode 100755 app/resto/core/dbfunctions/GroupsFunctions.php diff --git a/app/resto/core/RestoCollections.php b/app/resto/core/RestoCollections.php index 18866880..ec6c37da 100755 --- a/app/resto/core/RestoCollections.php +++ b/app/resto/core/RestoCollections.php @@ -195,7 +195,8 @@ public function search($model, $query) */ public function load($params = array()) { - $params['group'] = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? null : $this->user->profile['groups']; + + $params['group'] = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? null : $this->user->getGroupIds(); $cacheKey = 'collections' . ($params['group'] ? join(',', $params['group']) : ''); $collectionsDesc = $this->context->fromCache($cacheKey); @@ -212,7 +213,7 @@ public function load($params = array()) $this->collections[$collectionId] = $collection; $this->updateExtent($collection); } - + return $this; } diff --git a/app/resto/core/RestoRouter.php b/app/resto/core/RestoRouter.php index 077d3f53..66078c93 100755 --- a/app/resto/core/RestoRouter.php +++ b/app/resto/core/RestoRouter.php @@ -34,6 +34,7 @@ class RestoRouter const ROUTE_TO_FEATURES = RestoRouter::ROUTE_TO_COLLECTION . '/items'; const ROUTE_TO_FEATURE = RestoRouter::ROUTE_TO_FEATURES . '/{featureId}'; const ROUTE_TO_FORGOT_PASSWORD = '/services/password/forgot'; + const ROUTE_TO_GROUPS = '/groups'; const ROUTE_TO_OSDD = '/services/osdd'; const ROUTE_TO_LIVENESS = '/_isLive'; const ROUTE_TO_RESET_PASSWORD = '/services/password/reset'; @@ -51,59 +52,66 @@ class RestoRouter private $defaultRoutes = array( // Liveness and readyness for cloud - array('GET', '/', false, 'ServicesAPI::hello'), // Landing page - array('GET', RestoRouter::ROUTE_TO_LIVENESS, false, 'StatusAPI::isLive'), // Liveness + array('GET', '/', false, 'ServicesAPI::hello'), // Landing page + array('GET', RestoRouter::ROUTE_TO_LIVENESS, false, 'StatusAPI::isLive'), // Liveness // Landing page and conformance (see WFS 3.0) - array('GET', RestoRouter::ROUTE_TO_API, false, 'ServicesAPI::api'), // API page - array('GET', RestoRouter::ROUTE_TO_CONFORMANCE, false, 'ServicesAPI::conformance'), // Conformance page + array('GET', RestoRouter::ROUTE_TO_API, false, 'ServicesAPI::api'), // API page + array('GET', RestoRouter::ROUTE_TO_CONFORMANCE, false, 'ServicesAPI::conformance'), // Conformance page // API for users - array('GET', RestoRouter::ROUTE_TO_USERS, true, 'UsersAPI::getUsersProfiles'), // List users profiles - array('POST', RestoRouter::ROUTE_TO_USERS, false, 'UsersAPI::createUser'), // Create user - array('GET', RestoRouter::ROUTE_TO_USER, true, 'UsersAPI::getUserProfile'), // Show user profile - array('PUT', RestoRouter::ROUTE_TO_USER, true, 'UsersAPI::updateUserProfile'), // Update :userid profile - array('GET', RestoRouter::ROUTE_TO_USER . '/logs', true, 'UsersAPI::getUserLogs'), // Show user logs - array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS, true, 'UsersAPI::getUserRights'), // Show user rights - array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS . '/{collectionId}', true, 'UsersAPI::getUserRights'), // Show user rights for :collectionId - array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS . '/{collectionId}/{featureId}', true, 'UsersAPI::getUserRights'), // Show user rights for :featureId + array('GET', RestoRouter::ROUTE_TO_USERS, true, 'UsersAPI::getUsersProfiles'), // List users profiles + array('POST', RestoRouter::ROUTE_TO_USERS, false, 'UsersAPI::createUser'), // Create user + array('GET', RestoRouter::ROUTE_TO_USER, true, 'UsersAPI::getUserProfile'), // Show user profile + array('PUT', RestoRouter::ROUTE_TO_USER, true, 'UsersAPI::updateUserProfile'), // Update :userid profile + array('GET', RestoRouter::ROUTE_TO_USER . '/logs', true, 'UsersAPI::getUserLogs'), // Show user logs + array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS, true, 'UsersAPI::getUserRights'), // Show user rights + array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS . '/{collectionId}', true, 'UsersAPI::getUserRights'), // Show user rights for :collectionId + array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS . '/{collectionId}/{featureId}', true, 'UsersAPI::getUserRights'), // Show user rights for :featureId + // API for groups + array('GET' , RestoRouter::ROUTE_TO_GROUPS, true, 'Group::getGroups'), // List users profiles + array('GET' , RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'Group::getGroup'), // Get group + array('POST' , RestoRouter::ROUTE_TO_GROUPS, true, 'Group::createGroup'), // Create group + array('DELETE', RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'Group::deleteGroup'), // Delete group + array('GET' , RestoRouter::ROUTE_TO_USER . '/groups', true, 'Group::getUserGroups'), // Show user groups + // API for collections - array('GET', RestoRouter::ROUTE_TO_COLLECTIONS, false, 'CollectionsAPI::getCollections'), // List all collections - array('POST', RestoRouter::ROUTE_TO_COLLECTIONS, true, 'CollectionsAPI::createCollection'), // Create collection - array('GET', RestoRouter::ROUTE_TO_COLLECTION, false, 'CollectionsAPI::getCollection'), // Get :collectionId description - array('PUT', RestoRouter::ROUTE_TO_COLLECTION, true, 'CollectionsAPI::updateCollection'), // Update :collectionId - array('DELETE', RestoRouter::ROUTE_TO_COLLECTION, true, 'CollectionsAPI::deleteCollection'), // Delete :collectionId + array('GET', RestoRouter::ROUTE_TO_COLLECTIONS, false, 'CollectionsAPI::getCollections'), // List all collections + array('POST', RestoRouter::ROUTE_TO_COLLECTIONS, true, 'CollectionsAPI::createCollection'), // Create collection + array('GET', RestoRouter::ROUTE_TO_COLLECTION, false, 'CollectionsAPI::getCollection'), // Get :collectionId description + array('PUT', RestoRouter::ROUTE_TO_COLLECTION, true, 'CollectionsAPI::updateCollection'), // Update :collectionId + array('DELETE', RestoRouter::ROUTE_TO_COLLECTION, true, 'CollectionsAPI::deleteCollection'), // Delete :collectionId array('GET', RestoRouter::ROUTE_TO_COLLECTION . '/queryables', false, 'STAC::getQueryables'), // OAFeature API - Queryables // API for features - array('GET', RestoRouter::ROUTE_TO_FEATURES, false, 'FeaturesAPI::getFeaturesInCollection'), // Search features in :collectionId - array('POST', RestoRouter::ROUTE_TO_FEATURES, array('auth' => true, 'upload' => 'files'), 'CollectionsAPI::insertFeatures'), // Insert feature(s) - array('GET', RestoRouter::ROUTE_TO_FEATURE, false, 'FeaturesAPI::getFeature'), // Get feature :featureId - array('PUT', RestoRouter::ROUTE_TO_FEATURE, true, 'FeaturesAPI::updateFeature'), // Update feature :featureId - array('DELETE', RestoRouter::ROUTE_TO_FEATURE, true, 'FeaturesAPI::deleteFeature'), // Delete :featureId - array('PUT', RestoRouter::ROUTE_TO_FEATURE . '/properties/{property}', true, 'FeaturesAPI::updateFeatureProperty'), // Update feature :featureId single property + array('GET', RestoRouter::ROUTE_TO_FEATURES, false, 'FeaturesAPI::getFeaturesInCollection'), // Search features in :collectionId + array('POST', RestoRouter::ROUTE_TO_FEATURES, array('auth' => true, 'upload' => 'files'), 'CollectionsAPI::insertFeatures'), // Insert feature(s) + array('GET', RestoRouter::ROUTE_TO_FEATURE, false, 'FeaturesAPI::getFeature'), // Get feature :featureId + array('PUT', RestoRouter::ROUTE_TO_FEATURE, true, 'FeaturesAPI::updateFeature'), // Update feature :featureId + array('DELETE', RestoRouter::ROUTE_TO_FEATURE, true, 'FeaturesAPI::deleteFeature'), // Delete :featureId + array('PUT', RestoRouter::ROUTE_TO_FEATURE . '/properties/{property}', true, 'FeaturesAPI::updateFeatureProperty'), // Update feature :featureId single property // API for authentication (token based) array('GET', RestoRouter::ROUTE_TO_AUTH, true, 'AuthAPI::getToken'), - array('GET', RestoRouter::ROUTE_TO_AUTH . '/create', true, 'AuthAPI::createToken'), // Return a valid auth token - array('GET', RestoRouter::ROUTE_TO_AUTH . '/check/{token}', false, 'AuthAPI::checkToken'), // Check auth token validity - array('DELETE', RestoRouter::ROUTE_TO_AUTH . '/revoke/{token}', true, 'AuthAPI::revokeToken'), // Revoke auth token - array('PUT', RestoRouter::ROUTE_TO_AUTH . '/activate/{token}', false, 'AuthAPI::activateUser'), // Activate owner of the token + array('GET', RestoRouter::ROUTE_TO_AUTH . '/create', true, 'AuthAPI::createToken'), // Return a valid auth token + array('GET', RestoRouter::ROUTE_TO_AUTH . '/check/{token}', false, 'AuthAPI::checkToken'), // Check auth token validity + array('DELETE', RestoRouter::ROUTE_TO_AUTH . '/revoke/{token}', true, 'AuthAPI::revokeToken'), // Revoke auth token + array('PUT', RestoRouter::ROUTE_TO_AUTH . '/activate/{token}', false, 'AuthAPI::activateUser'), // Activate owner of the token // API for services - array('GET', RestoRouter::ROUTE_TO_OSDD, false, 'ServicesAPI::getOSDD'), // Opensearch service description at collections level - array('GET', RestoRouter::ROUTE_TO_OSDD . '/{collectionId}', false, 'ServicesAPI::getOSDDForCollection'), // Opensearch service description for products on {collection} - array('POST', RestoRouter::ROUTE_TO_SEND_ACTIVATION_LINK, false, 'ServicesAPI::sendActivationLink'), // Send activation link - array('POST', RestoRouter::ROUTE_TO_FORGOT_PASSWORD, 'ServicesAPI::forgotPassword'), // Send reset password link - array('POST', RestoRouter::ROUTE_TO_RESET_PASSWORD, false, 'ServicesAPI::resetPassword'), // Reset password + array('GET', RestoRouter::ROUTE_TO_OSDD, false, 'ServicesAPI::getOSDD'), // Opensearch service description at collections level + array('GET', RestoRouter::ROUTE_TO_OSDD . '/{collectionId}', false, 'ServicesAPI::getOSDDForCollection'), // Opensearch service description for products on {collection} + array('POST', RestoRouter::ROUTE_TO_SEND_ACTIVATION_LINK, false, 'ServicesAPI::sendActivationLink'), // Send activation link + array('POST', RestoRouter::ROUTE_TO_FORGOT_PASSWORD, 'ServicesAPI::forgotPassword'), // Send reset password link + array('POST', RestoRouter::ROUTE_TO_RESET_PASSWORD, false, 'ServicesAPI::resetPassword'), // Reset password // STAC - array('GET', RestoRouter::ROUTE_TO_ASSETS . '/{urlInBase64}', false, 'STAC::getAsset'), // Get an asset using HTTP 301 permanent redirect - array('GET', RestoRouter::ROUTE_TO_CATALOGS . '/*', false, 'STAC::getCatalogs'), // Get catalogs - array('GET', RestoRouter::ROUTE_TO_STAC_CHILDREN, false, 'STAC::getChildren'), // STAC API - Children - array('GET', RestoRouter::ROUTE_TO_STAC_QUERYABLES, false, 'STAC::getQueryables'), // STAC/OAFeature API - Queryables - array('GET', RestoRouter::ROUTE_TO_STAC_SEARCH, false, 'STAC::search') // STAC API - core search + array('GET', RestoRouter::ROUTE_TO_ASSETS . '/{urlInBase64}', false, 'STAC::getAsset'), // Get an asset using HTTP 301 permanent redirect + array('GET', RestoRouter::ROUTE_TO_CATALOGS . '/*', false, 'STAC::getCatalogs'), // Get catalogs + array('GET', RestoRouter::ROUTE_TO_STAC_CHILDREN, false, 'STAC::getChildren'), // STAC API - Children + array('GET', RestoRouter::ROUTE_TO_STAC_QUERYABLES, false, 'STAC::getQueryables'), // STAC/OAFeature API - Queryables + array('GET', RestoRouter::ROUTE_TO_STAC_SEARCH, false, 'STAC::search') // STAC API - core search ); /* diff --git a/app/resto/core/RestoUser.php b/app/resto/core/RestoUser.php index 65de83ba..58a6dece 100755 --- a/app/resto/core/RestoUser.php +++ b/app/resto/core/RestoUser.php @@ -139,6 +139,11 @@ class RestoUser */ private $rights; + /* + * Reference to groups object + */ + private $groups; + /* * Fallback rights if no collection is found */ @@ -154,9 +159,6 @@ class RestoUser private $unregistered = array( 'id' => null, 'email' => 'unregistered', - 'groups' => array( - RestoConstants::GROUP_DEFAULT_ID - ), 'activated' => 0 ); @@ -378,6 +380,40 @@ public function removeRights($rights, $collectionId = null, $featureId = null) return true; } + /** + * Return the list of user groups + * + * @throws Exception + */ + public function getGroups() + { + if ( !isset($this->groups) ) { + return (new GroupsFunctions($this->context->dbDriver))->getGroups(array('userid' => $this->profile['id'])); + } + return $this->groups; + } + + /** + * Return the list of user group ids + * + * @throws Exception + */ + public function getGroupIds() + { + $groups = $this->getGroups(); + + // Everybody is in the default RestoConstants::GROUP_DEFAULT_ID; + $ids = [ + RestoConstants::GROUP_DEFAULT_ID + ]; + + for ($i = 0, $ii = count($groups); $i < $ii; $i++) { + $ids[] = $groups[$i]['id']; + } + + return $ids; + } + /** * Return true if user is in $group * @@ -386,8 +422,7 @@ public function removeRights($rights, $collectionId = null, $featureId = null) */ public function hasGroup($group) { - $this->loadProfile(); - return in_array($group, $this->profile['groups']); + return in_array($group, $this->getGroupIds()); } /** @@ -438,7 +473,7 @@ public function sendResetPasswordLink() */ public function loadProfile() { - if (!$this->isComplete && isset($this->profile['id'])) { + if ( !$this->isComplete && isset($this->profile['id']) ) { $this->profile = (new UsersFunctions($this->context->dbDriver))->getUserProfile('id', $this->profile['id']); $this->isComplete = true; } diff --git a/app/resto/core/addons/Group.php b/app/resto/core/addons/Group.php new file mode 100755 index 00000000..0d977d0f --- /dev/null +++ b/app/resto/core/addons/Group.php @@ -0,0 +1,436 @@ + (new GroupsFunctions($this->context->dbDriver))->getGroups(array( + 'q' => $params['q'] ?? null, + 'viewAll' => $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) + )) + ); + } + + /** + * Get group + * + * @OA\Get( + * path="/groups/{id}", + * summary="Get group", + * tags={"Group"}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="Group identifier", + * @OA\Schema( + * type="integer" + * ) + * ), + * @OA\Response( + * response="200", + * description="User group", + * @OA\JsonContent(ref="#/components/schemas/OutputGroup") + * ), + * @OA\Response( + * response="401", + * description="Unauthorized", + * @OA\JsonContent(ref="#/components/schemas/UnauthorizedError") + * ), + * @OA\Response( + * response="403", + * description="Forbidden", + * @OA\JsonContent(ref="#/components/schemas/ForbiddenError") + * ), + * @OA\Response( + * response="404", + * description="Resource not found", + * @OA\JsonContent(ref="#/components/schemas/NotFoundError") + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + */ + public function getGroup($params) + { + $this->checkGroupId($params['id']); + return (new GroupsFunctions($this->context->dbDriver))->getGroups(array( + 'id' => $params['id'] + )); + } + + /** + * + * Add group + * + * @OA\Post( + * path="/groups", + * summary="Create group", + * description="Create a group from name - the name must be unique", + * tags={"Group"}, + * @OA\Response( + * response="200", + * description="Group is created and unique identifier is returned", + * @OA\JsonContent( + * @OA\Property( + * property="status", + * type="string", + * description="Status is *success*" + * ), + * @OA\Property( + * property="message", + * type="string", + * description="Group created" + * ), + * @OA\Property( + * property="id", + * type="integer", + * description="Newly created group identifier" + * ), + * example={ + * "status": "success", + * "message": "Group created", + * "id": 100 + * } + * ) + * ), + * @OA\Response( + * response="400", + * description="This group already exist", + * @OA\JsonContent(ref="#/components/schemas/BadRequestError") + * ), + * @OA\RequestBody( + * description="Group description", + * required=true, + * @OA\JsonContent( + * required={"name"}, + * @OA\Property( + * property="name", + * type="string", + * description="Unique name for the group" + * ), + * @OA\Property( + * property="description", + * type="string", + * description="" + * ), + * example={ + * "name": "My first group", + * "description": "Any user can create a group." + * } + * ) + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + * + * @param array $params + * @param array $body + * + */ + public function createGroup($params, $body) + { + $group = (new GroupsFunctions($this->context->dbDriver))->createGroup(array( + 'id' => $this->user->profile['id'], + 'body' => $body + )); + + return RestoLogUtil::success('Group created', $group); + } + + /** + * + * Remove a group + * + * @OA\Delete( + * path="/groups/{id}", + * summary="Delete group", + * description="Only administrator and owner of a group can delete it", + * tags={"Group"}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="Group identifier", + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Response( + * response="200", + * description="The group is delete", + * @OA\JsonContent( + * @OA\Property( + * property="status", + * type="string", + * description="Status is *success*" + * ), + * @OA\Property( + * property="message", + * type="string", + * description="Message information" + * ), + * example={ + * "status": "success", + * "message": "Delete group 1919831313561420822" + * } + * ) + * ), + * @OA\Response( + * response="400", + * description="Missing mandatory group identifier", + * @OA\JsonContent(ref="#/components/schemas/BadRequestError") + * ), + * @OA\Response( + * response="403", + * description="Forbidden", + * @OA\JsonContent(ref="#/components/schemas/ForbiddenError") + * ), + * @OA\Response( + * response="404", + * description="Group not found", + * @OA\JsonContent(ref="#/components/schemas/NotFoundError") + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + * + * @param array $params + * @param array $body + * + */ + public function deleteGroup($params) + { + $this->checkGroupId($params['id']); + $group = (new GroupsFunctions($this->context->dbDriver))->removeGroup(array( + 'id' => $params['id'], + 'owner' => $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? null : $this->user->profile['id'] + )); + + return RestoLogUtil::success('Deleted group', array('id' => $group['id'])); + + } + + + /** + * Get user groups + * + * @OA\Get( + * path="/users/{userid}/groups", + * summary="Get user's groups", + * tags={"User"}, + * @OA\Parameter( + * name="userid", + * in="path", + * required=true, + * description="User's identifier", + * @OA\Schema( + * type="string" + * ) + * ), + * @OA\Response( + * response="200", + * description="User groups", + * @OA\JsonContent( + * @OA\Property( + * property="id", + * type="string", + * description="User identifier" + * ), + * @OA\Property( + * property="groups", + * type="array", + * description="Array of user's groups", + * @OA\Items(ref="#/components/schemas/OutputGroup") + * ), + * example={ + * "id": "1356771884787565573", + * "groups":{ + * { + * "id":"1", + * "name":"Default", + * "description":"Default group" + * } + * } + * } + * ) + * ), + * @OA\Response( + * response="401", + * description="Unauthorized", + * @OA\JsonContent(ref="#/components/schemas/UnauthorizedError") + * ), + * @OA\Response( + * response="404", + * description="Resource not found", + * @OA\JsonContent(ref="#/components/schemas/NotFoundError") + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + */ + public function getUserGroups($params) + { + if ($this->user->profile['id'] === $params['userid']) { + $this->user->loadProfile(); + return array( + 'id' => $this->user->profile['id'], + 'groups' => (new GroupsFunctions($this->context->dbDriver))->getGroups(array( + 'in' => $this->user->getGroupIds() + )) + ); + } + + $user = new RestoUser(array('id' => $params['userid']), $this->context, true); + return isset($user->profile['id']) ? array( + 'id' => $user->profile['id'], + 'groups' => (new GroupsFunctions($this->context->dbDriver))->getGroups(array( + 'in' => $user->getGroupIds() + )) + ) : RestoLogUtil::httpError(404); + } + + /* + * Check that id is an integer stricly lower than Postgres INTEGER MAX_VALUE + * + * @param string $id + */ + private function checkGroupId($groupid) + { + if (! ctype_digit($groupid) || intval($groupid) >= Resto::INT_MAX_VALUE) { + RestoLogUtil::httpError(400, 'Invalid group identifier'); + } + } +} diff --git a/app/resto/core/api/UsersAPI.php b/app/resto/core/api/UsersAPI.php index 4787eeae..b9ea5426 100755 --- a/app/resto/core/api/UsersAPI.php +++ b/app/resto/core/api/UsersAPI.php @@ -398,11 +398,6 @@ public function createUser($params, $body) 'followings' => 0 ); - // Admin can set the user groups - if ( isset($body['groups']) && $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID)) { - $profile['groups'] = $body['groups']; - } - return $this->storeProfile($profile, $this->context->core['storageInfo']); } @@ -517,10 +512,6 @@ public function updateUserProfile($params, $body) unset($body['activated'], $body['validatedby'], $body['validationdate'], $body['country'], $body['organization'], $body['organizationcountry'], $body['flags']); } - /* - * These properties can only be changed by admin - */ - unset($body['groups']); } /* diff --git a/app/resto/core/dbfunctions/FiltersFunctions.php b/app/resto/core/dbfunctions/FiltersFunctions.php index ade645ea..cd653a32 100755 --- a/app/resto/core/dbfunctions/FiltersFunctions.php +++ b/app/resto/core/dbfunctions/FiltersFunctions.php @@ -210,7 +210,7 @@ private function prepareFilterQueryContextualSearch($tableName) } return array( - 'value' => $tableName . '.visibility IN (' . join(',', $this->user->profile['groups']) . ')', + 'value' => $tableName . '.visibility IN (' . join(',', $this->user->getGroupIds()) . ')', 'isGeo' => false ); } diff --git a/app/resto/core/dbfunctions/GroupsFunctions.php b/app/resto/core/dbfunctions/GroupsFunctions.php new file mode 100755 index 00000000..ebd34cda --- /dev/null +++ b/app/resto/core/dbfunctions/GroupsFunctions.php @@ -0,0 +1,177 @@ +dbDriver = $dbDriver; + } + + /** + * List all groups + * + * @return array + * @throws Exception + */ + public function getGroups($params = array()) + { + $where = array(); + + /* Show hidden groups */ + if (!isset($params['viewAll']) || ! $params['viewAll']) { + $where[] = 'id NOT IN (' . join(',', array(RestoConstants::GROUP_ADMIN_ID, RestoConstants::GROUP_DEFAULT_ID)) . ')'; + } + + // Return group by id + if (isset($params['id'])) { + $where[] = 'id=' . pg_escape_string($params['id']); + } + + // Return all groups in + if (isset($params['in'])) { + $where[] = 'id IN (' . pg_escape_string(join(',', $params['in'])) . ')'; + } + + // Search by name + if (isset($params['q'])) { + $where[] = 'normalize(name) LIKE normalize(\'%' . pg_escape_string($params['q']) . '%\')'; + } + + // Return groups by userid + if (isset($params['userid'])) { + $where[] = 'id IN (SELECT DISTINCT groupid FROM ' . $this->dbDriver->commonSchema . '.group_member WHERE userid=' . pg_escape_string($params['userid']) . ')'; + } + + return $this->formatGroups($this->dbDriver->fetch($this->dbDriver->query('SELECT id, name, description, owner, to_iso8601(created) as created FROM ' . $this->dbDriver->commonSchema . '.group' . (count($where) > 0 ? ' WHERE ' . join(' AND ', $where) : '') . ' ORDER BY id DESC')), $params['id'] ?? null); + } + + /** + * Create a new group - name must be unique + * + * @param Array $params + * @throws Exception + */ + public function createGroup($params) + { + if (! isset($params['body']) || ! isset($params['body']['name'])) { + RestoLogUtil::httpError(400, 'Missing mandatory group name'); + } + + try { + $result = pg_query_params($this->dbDriver->getConnection(), 'INSERT INTO ' . $this->dbDriver->commonSchema . '.group (name, description, owner, created) VALUES ($1, $2, $3, now()) ON CONFLICT (normalize(name)) DO NOTHING RETURNING id ', array( + $params['body']['name'], + $params['body']['description'] ?? null, + $params['id'] + )); + + if (!$result) { + throw new Exception('Cannot create group', 500); + } + + $row = pg_fetch_assoc($result); + if (empty($row)) { + throw new Exception('A group named - ' . $params['body']['name'] . ' - already exist', 400); + } + + return array( + 'id' => intval($row['id']), + 'name' => $params['body']['name'], + 'owner' => $params['id'] + ); + + } catch (Exception $e) { + RestoLogUtil::httpError($e->getCode(), $e->getMessage()); + } + } + + /** + * Remove a group + * + * @param Array $params + * @throws Exception + */ + public function removeGroup($params) + { + if (! isset($params['id'])) { + RestoLogUtil::httpError(400, 'Missing mandatory group identifier'); + } + + try { + if (isset($params['owner'])) { + $result = pg_query_params($this->dbDriver->getConnection(), 'DELETE FROM ' . $this->dbDriver->commonSchema . '.group WHERE id=($1) AND owner=($2)', array( + $params['id'], + $params['owner'] + )); + } else { + $result = pg_query_params($this->dbDriver->getConnection(), 'DELETE FROM ' . $this->dbDriver->commonSchema . '.group WHERE id=($1)', array( + $params['id'] + )); + } + + if (!$result || pg_affected_rows($result) !== 1) { + throw new Exception(); + } + + return array( + 'id' => intval($params['id']) + ); + } catch (Exception $e) { + RestoLogUtil::httpError(403, 'Cannot delete group'); + } + } + + /** + * Format group results for nice output + * + * @param array $results Groups from database + * @param string $groupId Group id + */ + private function formatGroups($results, $groupId) + { + + // 404 if no empty results when id is specified + if (! isset($results) || (isset($groupId) && count($results) === 0)) { + return RestoLogUtil::httpError(404); + } + + $length = isset($groupId) ? 1 : count($results); + + // Format groups + for ($i = $length; $i--;) { + $results[$i]['id'] = intval($results[$i]['id']); + if (! isset($results[$i]['owner']) ) { + unset($results[$i]['owner']); + } + } + + return isset($groupId) ? $results[0] : $results; + + } +} diff --git a/app/resto/core/dbfunctions/RightsFunctions.php b/app/resto/core/dbfunctions/RightsFunctions.php index f2a47f25..bf809cb8 100755 --- a/app/resto/core/dbfunctions/RightsFunctions.php +++ b/app/resto/core/dbfunctions/RightsFunctions.php @@ -80,7 +80,7 @@ public function getRightsForUser($user, $target, $collectionId, $featureId) /* * Retrieve rights for user groups */ - $groupsRights = $this->getRightsForGroups($user->profile['groups'], $target, $collectionId, $featureId, false); + $groupsRights = $this->getRightsForGroups($user->getGroupIds(), $target, $collectionId, $featureId, false); /* * Merge rights from user and from user's groups diff --git a/app/resto/core/dbfunctions/UsersFunctions.php b/app/resto/core/dbfunctions/UsersFunctions.php index b604b7a6..06f29c62 100755 --- a/app/resto/core/dbfunctions/UsersFunctions.php +++ b/app/resto/core/dbfunctions/UsersFunctions.php @@ -25,6 +25,8 @@ class UsersFunctions private $countLimit = 50; + private $userFields = 'id,email,name,firstname,lastname,bio,lang,country,organization,organizationcountry,flags,topics,password,picture,to_iso8601(registrationdate) as registrationdate,activated,followers,followings,validatedby,to_iso8601(validationdate) as validationdate,externalidp,settings'; + /** * Constructor * @@ -70,10 +72,6 @@ public static function formatUserProfile($rawProfile) $profile[$key] = (integer) $value; break; - case 'groups': - $profile[$key] = array_map('intval', explode(",", substr($rawProfile['groups'], 1, -1))); - break; - case 'topics': $profile[$key] = isset($rawProfile['topics']) ? substr($rawProfile['topics'], 1, -1) : null; break; @@ -122,18 +120,10 @@ public static function formatUserProfile($rawProfile) */ public static function formatPartialUserProfile($rawProfile) { - // Remove leading "{" and trailing "}" for INTEGER[] (Database returns {group1,group2,etc.}) - $groups = array_map('intval', explode(",", substr($rawProfile['groups'], 1, -1))); - - // [SECURITY] Never return users with only one group and group < 100 - if (count($groups) === 1 && $groups[0] < 100) { - return null; - } $profile = array( 'id' => $rawProfile['id'], 'picture' => $rawProfile['picture'], - 'groups' => $groups, 'name' => $rawProfile['name'], 'registrationdate' => $rawProfile['registrationdate'], 'followers' => (integer) $rawProfile['followers'], @@ -162,7 +152,7 @@ public static function formatPartialUserProfile($rawProfile) break; case 'bio': - $profile[$key] = $rawProfile[$value]; + $profile[$key] = $value; break; default: @@ -197,7 +187,7 @@ public function getUserPassword($identifier) public function getUserProfile($fieldName, $fieldValue, $params = array()) { // Add followed and followme booleans - $fields = 'id,email,name,firstname,lastname,bio,groups,lang,country,organization,organizationcountry,flags,topics,password,picture,to_iso8601(registrationdate),activated,followers,followings,validatedby,to_iso8601(validationdate),externalidp,settings'; + $fields = $this->userFields; if (isset($params['from'])) { $fields = $fields . ',EXISTS(SELECT followerid FROM ' . $this->dbDriver->commonSchema . '.follower WHERE followerid=id AND userid=' . pg_escape_string($this->dbDriver->getConnection(), $params['from']) . ') AS followme,EXISTS(SELECT followerid FROM ' . $this->dbDriver->commonSchema . '.follower WHERE userid=id AND followerid=' . pg_escape_string($this->dbDriver->getConnection(), $params['from']) . ') AS followed'; } @@ -251,10 +241,6 @@ public function getUsersProfiles($params, $userid) if (isset($params['lt'])) { $where[] = 'id < ' . $params['lt']; } - - if (isset($params['groupid'])) { - $where[] = 'groups @> ARRAY[' . $params['groupid'] . ']'; - } if (isset($params['in'])) { $where[] = 'id in (' . pg_escape_string($this->dbDriver->getConnection(), $params['in']) . ')'; @@ -269,7 +255,7 @@ public function getUsersProfiles($params, $userid) } // Add followed and followme booleans - $fields = 'id,email,name,firstname,lastname,bio,groups,lang,country,organization,organizationcountry,flags,topics,password,picture,to_iso8601(registrationdate),activated,followers,followings,validatedby,to_iso8601(validationdate),externalidp,settings'; + $fields = $this->userFields; if (isset($userid)) { $fields = $fields . ',EXISTS(SELECT followerid FROM ' . $this->dbDriver->commonSchema . '.follower WHERE followerid=id AND userid=' . pg_escape_string($this->dbDriver->getConnection(), $userid) . ') AS followme,EXISTS(SELECT followerid FROM ' . $this->dbDriver->commonSchema . '.follower WHERE userid=id AND followerid=' . pg_escape_string($this->dbDriver->getConnection(), $userid) . ') AS followed'; } @@ -349,21 +335,12 @@ public function storeUserProfile($profile, $storageInfo) */ $picture = $this->getPicture($profile, $storageInfo); - /* - * Every user is in the default RestoConstants::GROUP_DEFAULT_ID - */ - $groups = $profile['groups'] ?? array(); - if (!in_array(RestoConstants::GROUP_DEFAULT_ID, $groups)) { - $groups[] = RestoConstants::GROUP_DEFAULT_ID; - } - /* * Store everything */ $toBeSet = array( 'email' => '\'' . pg_escape_string($this->dbDriver->getConnection(), $email) . '\'', 'password' => '\'' . (isset($profile['password']) ? password_hash($profile['password'], PASSWORD_BCRYPT) : str_repeat('*', 60)) . '\'', - 'groups' => '\'{' . pg_escape_string($this->dbDriver->getConnection(), join(',', $groups)) . '}\'', 'topics' => isset($profile['topics']) ? '\'{' . pg_escape_string($this->dbDriver->getConnection(), $profile['topics']) . '}\'' : 'NULL', 'picture' => '\'' . pg_escape_string($this->dbDriver->getConnection(), $picture) . '\'', 'bio' => isset($profile['bio']) ? '\'' . pg_escape_string($this->dbDriver->getConnection(), $profile['bio']) . '\'' : 'NULL', @@ -430,7 +407,7 @@ public function updateUserProfile($profile, $storageInfo) * - registrationdate */ $values = array(); - foreach (array_values(array('password', 'activated', 'bio', 'name', 'firstname', 'lastname', 'groups', 'country', 'organization', 'topics', 'organizationcountry', 'flags', 'lang', 'settings', 'picture', 'externalidp')) as $field) { + foreach (array_values(array('password', 'activated', 'bio', 'name', 'firstname', 'lastname', 'country', 'organization', 'topics', 'organizationcountry', 'flags', 'lang', 'settings', 'picture', 'externalidp')) as $field) { if (isset($profile[$field])) { switch ($field) { case 'password': @@ -448,7 +425,6 @@ public function updateUserProfile($profile, $storageInfo) RestoLogUtil::httpError(400); } break; - case 'groups': case 'topics': $values[] = $field . '=\'{' . pg_escape_string($this->dbDriver->getConnection(), $profile[$field]) . '}\''; break; @@ -493,32 +469,6 @@ public function updateResetToken($email, $resettoken) return count($results) === 1 ? $results[0]['id'] : null; } - /** - * Add groups to user $userid - * - * @param integer $userid - * @param string $groups - * @return null - * @throws Exception - */ - public function storeUserGroups($userid, $groups) - { - return $this->storeOrRemoveUserGroups('store', $userid, $groups); - } - - /** - * Remove groups for user $userid - * - * @param integer $userid - * @param string $groups - * @return null - * @throws Exception - */ - public function removeUserGroups($userid, $groups) - { - return $this->storeOrRemoveUserGroups('remove', $userid, $groups); - } - /** * Activate user * @@ -600,72 +550,6 @@ public function unvalidateUser($userid) return count($this->dbDriver->fetch($this->dbDriver->query('UPDATE ' . $this->dbDriver->commonSchema . '.user SET ' . join(',', $toBeSet) . ' WHERE id=' . pg_escape_string($this->dbDriver->getConnection(), $userid) . ' RETURNING id'))) === 1 ? true : false; } - /** - * Store or remove groups for user $userid - * - * @param string $storeOrRemove - * @param integer $userid - * @param array $groups - * @return null - * @throws Exception - */ - private function storeOrRemoveUserGroups($storeOrRemove, $userid, $groups) - { - if (!isset($userid)) { - RestoLogUtil::httpError(400, 'Cannot ' . $storeOrRemove . ' groups - invalid user identifier : ' . $userid); - } - if (empty($groups)) { - RestoLogUtil::httpError(400, 'Cannot ' . $storeOrRemove . ' groups - empty input groups'); - } - - $profile = $this->getUserProfile('id', $userid); - if (!isset($profile)) { - RestoLogUtil::httpError(404, 'Cannot ' . $storeOrRemove . ' groups - user profile not found for : ' . $userid); - } - - /* - * Explode existing groups into an associative array - */ - $userGroups = !empty($profile['groups']) ? array_flip($profile['groups']) : array(); - - /* - * Explode input groups - */ - $newGroups = array(); - $rawNewGroups = $groups; - for ($i = 0, $ii = count($rawNewGroups); $i < $ii; $i++) { - if ($rawNewGroups[$i] !== '') { - $newGroups[$rawNewGroups[$i]] = 1; - } - } - - /* - * Store - merge new groups with user groups - */ - if ($storeOrRemove === 'store') { - $newGroups = array_keys(array_merge($newGroups, $userGroups)); - } - - /* - * Remove - note that RestoConstants::GROUP_DEFAULT_ID group cannot be removed - */ else { - foreach (array_keys($newGroups) as $key) { - if ($key !== RestoConstants::GROUP_DEFAULT_ID) { - unset($userGroups[$key]); - } - } - $newGroups = array_keys($userGroups); - } - - /* - * Update user profile - */ - $results = count($newGroups) > 0 ? implode(',', $newGroups) : null; - $this->dbDriver->fetch($this->dbDriver->query('UPDATE ' . $this->dbDriver->commonSchema . '.user SET groups=' . (isset($results) ? '\'{' . pg_escape_string($this->dbDriver->getConnection(), $results) . '}\'' : 'NULL') . ' WHERE id=' . $userid)); - - return $results; - } - /** * Return picture url * diff --git a/build/resto/container_root/cont-init.d/18-resto-admin-user.php b/build/resto/container_root/cont-init.d/18-resto-admin-user.php index 0e3d5b14..9b424f71 100755 --- a/build/resto/container_root/cont-init.d/18-resto-admin-user.php +++ b/build/resto/container_root/cont-init.d/18-resto-admin-user.php @@ -18,9 +18,12 @@ $config = include($configFile); $dbDriver = new RestoDatabaseDriver($config['database'] ?? null); - $dbDriver->pQuery('INSERT INTO ' . $dbDriver->commonSchema . '.user (id,email,groups,firstname,password,activated,registrationdate) VALUES ($1,$2,$3,$2,$4,1,now_utc()) ON CONFLICT (id) DO UPDATE SET password=$4', array( + $dbDriver->pQuery('INSERT INTO ' . $dbDriver->commonSchema . '.user (id,email,firstname,password,activated,registrationdate) VALUES ($1,$2,$2,$3,1,now_utc()) ON CONFLICT (id) DO UPDATE SET password=$3', array( 100, getenv('ADMIN_USER_NAME') ?? 'admin', - '{0,100}', password_hash(getenv('ADMIN_USER_PASSWORD') ?? 'admin', PASSWORD_BCRYPT) )); + $dbDriver->pQuery('INSERT INTO ' . $dbDriver->commonSchema . '.group_member (groupid,userid,created) VALUES ($1,$2,now_utc()) ON CONFLICT (groupid,userid) DO NOTHING', array( + 100, + 100 + )); diff --git a/resto-database-model/02_resto_common_model.sql b/resto-database-model/02_resto_common_model.sql index bcbc9282..f49afebc 100644 --- a/resto-database-model/02_resto_common_model.sql +++ b/resto-database-model/02_resto_common_model.sql @@ -30,9 +30,6 @@ CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.user ( -- User description bio TEXT, - -- Array of groups referenced by __DATABASE_COMMON_SCHEMA__.group.id - groups INTEGER[], - -- User lang lang TEXT, @@ -130,6 +127,24 @@ CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.group ( ); +-- +-- Group members +-- +CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.group_member ( + + -- Group id + groupid INTEGER NOT NULL REFERENCES __DATABASE_COMMON_SCHEMA__.group (id) ON DELETE CASCADE, + + -- User in the group + userid BIGINT NOT NULL REFERENCES __DATABASE_COMMON_SCHEMA__.user (id) ON DELETE CASCADE, + + -- When user join the group + created TIMESTAMP NOT NULL DEFAULT now(), + + PRIMARY KEY (groupid, userid) +); +CREATE INDEX IF NOT EXISTS idx_groupid_group_member ON __DATABASE_COMMON_SCHEMA__.group_member (groupid); + -- -- topics table -- From 19a47a90a56916edb4884f52a3c38a09547595ec Mon Sep 17 00:00:00 2001 From: jjrom Date: Thu, 4 Apr 2024 14:33:31 +0200 Subject: [PATCH 2/9] Missing authentication flag in forgotPassword route --- app/resto/core/RestoRouter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/resto/core/RestoRouter.php b/app/resto/core/RestoRouter.php index 66078c93..45a8df45 100755 --- a/app/resto/core/RestoRouter.php +++ b/app/resto/core/RestoRouter.php @@ -103,7 +103,7 @@ class RestoRouter array('GET', RestoRouter::ROUTE_TO_OSDD, false, 'ServicesAPI::getOSDD'), // Opensearch service description at collections level array('GET', RestoRouter::ROUTE_TO_OSDD . '/{collectionId}', false, 'ServicesAPI::getOSDDForCollection'), // Opensearch service description for products on {collection} array('POST', RestoRouter::ROUTE_TO_SEND_ACTIVATION_LINK, false, 'ServicesAPI::sendActivationLink'), // Send activation link - array('POST', RestoRouter::ROUTE_TO_FORGOT_PASSWORD, 'ServicesAPI::forgotPassword'), // Send reset password link + array('POST', RestoRouter::ROUTE_TO_FORGOT_PASSWORD, false, 'ServicesAPI::forgotPassword'), // Send reset password link array('POST', RestoRouter::ROUTE_TO_RESET_PASSWORD, false, 'ServicesAPI::resetPassword'), // Reset password // STAC From efb5db22224518da0b0f71aa5fc271693a12ddfd Mon Sep 17 00:00:00 2001 From: jjrom Date: Thu, 4 Apr 2024 15:41:40 +0200 Subject: [PATCH 3/9] Store properties names in alphabetical order for code lisibility --- app/resto/core/RestoCollection.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/resto/core/RestoCollection.php b/app/resto/core/RestoCollection.php index 53cce4b8..f99cd280 100755 --- a/app/resto/core/RestoCollection.php +++ b/app/resto/core/RestoCollection.php @@ -782,24 +782,23 @@ class RestoCollection * Properties that are not stored in properties column */ private $notInProperties = array( - 'id', - 'type', - 'title', + 'assets', 'description', - 'stac_version', - 'stac_extension', 'extent', - 'model', - 'version', - 'visibility', - 'rights', + 'id', + 'keywords', 'license', 'links', + 'model', 'osDescription', 'providers', 'rights', - 'assets', - 'keywords' + 'stac_extension', + 'stac_version', + 'title', + 'type', + 'version', + 'visibility' ); /** From 3bd79b5a1ea04abab1d0c3a2d49b693fe2d3e073 Mon Sep 17 00:00:00 2001 From: jjrom Date: Fri, 5 Apr 2024 11:17:49 +0200 Subject: [PATCH 4/9] [In progress] Keep collection objects in memory to avoid multiple calls to backend database --- app/resto/core/RestoCollection.php | 41 ++++++----- app/resto/core/RestoCollections.php | 38 +++++++---- app/resto/core/RestoContext.php | 10 +++ app/resto/core/RestoKeeper.php | 68 +++++++++++++++++++ app/resto/core/addons/Group.php | 3 +- app/resto/core/addons/STAC.php | 10 +-- app/resto/core/api/CollectionsAPI.php | 12 ++-- app/resto/core/api/FeaturesAPI.php | 10 +-- app/resto/core/api/ServicesAPI.php | 4 +- .../core/dbfunctions/GroupsFunctions.php | 5 -- app/resto/core/utils/RestoFeatureUtil.php | 2 +- app/resto/core/utils/STACUtil.php | 4 +- .../cont-init.d/18-resto-admin-user.php | 2 +- 13 files changed, 148 insertions(+), 61 deletions(-) create mode 100755 app/resto/core/RestoKeeper.php diff --git a/app/resto/core/RestoCollection.php b/app/resto/core/RestoCollection.php index f99cd280..7da12ab2 100755 --- a/app/resto/core/RestoCollection.php +++ b/app/resto/core/RestoCollection.php @@ -836,28 +836,35 @@ public function __construct($id, $context, $user) */ public function load($object = null, $modelName = null) { - if (isset($object)) { - return $this->loadFromJSON($object, $modelName); - } + + if ( !$this->isLoaded ) { + + $this->isLoaded = true; + + if (isset($object)) { + return $this->loadFromJSON($object, $modelName); + } + + $cacheKey = 'collection:' . $this->id; + $collectionObject = $this->context->fromCache($cacheKey); - $cacheKey = 'collection:' . $this->id; - $collectionObject = $this->context->fromCache($cacheKey); + if (! isset($collectionObject)) { + $collectionObject = (new CollectionsFunctions($this->context->dbDriver))->getCollectionDescription($this->id); + + if (! isset($collectionObject)) { + return RestoLogUtil::httpError(404); + } - if (! isset($collectionObject)) { - $collectionObject = (new CollectionsFunctions($this->context->dbDriver))->getCollectionDescription($this->id); + $this->context->toCache($cacheKey, $collectionObject); + } - if (! isset($collectionObject)) { - return RestoLogUtil::httpError(404); + foreach ($collectionObject as $key => $value) { + $this->$key = $key === 'model' ? new $value(array( + 'collectionId' => $this->id, + 'addons' => $this->context->addons + )) : $value; } - $this->context->toCache($cacheKey, $collectionObject); - } - - foreach ($collectionObject as $key => $value) { - $this->$key = $key === 'model' ? new $value(array( - 'collectionId' => $this->id, - 'addons' => $this->context->addons - )) : $value; } return $this; diff --git a/app/resto/core/RestoCollections.php b/app/resto/core/RestoCollections.php index ec6c37da..b7f82520 100755 --- a/app/resto/core/RestoCollections.php +++ b/app/resto/core/RestoCollections.php @@ -122,6 +122,11 @@ class RestoCollections */ private $statistics; + /* + * Avoid multiple database calls + */ + private $isLoaded = false; + /** * Constructor * @@ -165,7 +170,7 @@ public function create($object, $modelName) /* * Create collection */ - $collection = new RestoCollection($object['id'], $this->context, $this->user); + $collection = $this->context->keeper->getRestoCollection($object['id'], $this->user); $collection->load($object, $modelName)->store(); return true; @@ -196,22 +201,25 @@ public function search($model, $query) public function load($params = array()) { - $params['group'] = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? null : $this->user->getGroupIds(); - $cacheKey = 'collections' . ($params['group'] ? join(',', $params['group']) : ''); - - $collectionsDesc = $this->context->fromCache($cacheKey); - if (!isset($collectionsDesc)) { - $collectionsDesc = (new CollectionsFunctions($this->context->dbDriver))->getCollectionsDescriptions($params); - $this->context->toCache($cacheKey, $collectionsDesc); - } + if ( !$this->isLoaded ) { + $params['group'] = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? null : $this->user->getGroupIds(); + $cacheKey = 'collections' . ($params['group'] ? join(',', $params['group']) : ''); + + $collectionsDesc = $this->context->fromCache($cacheKey); + if (!isset($collectionsDesc)) { + $collectionsDesc = (new CollectionsFunctions($this->context->dbDriver))->getCollectionsDescriptions($params); + $this->context->toCache($cacheKey, $collectionsDesc); + } - foreach (array_keys($collectionsDesc) as $collectionId) { - $collection = new RestoCollection($collectionId, $this->context, $this->user); - foreach ($collectionsDesc[$collectionId] as $key => $value) { - $collection->$key = $key === 'model' ? new $value() : $value; + foreach (array_keys($collectionsDesc) as $collectionId) { + $collection = $this->context->keeper->getRestoCollection($collectionId, $this->user); + foreach ($collectionsDesc[$collectionId] as $key => $value) { + $collection->$key = $key === 'model' ? new $value() : $value; + } + $this->collections[$collectionId] = $collection; + $this->updateExtent($collection); } - $this->collections[$collectionId] = $collection; - $this->updateExtent($collection); + $this->isLoaded = true; } return $this; diff --git a/app/resto/core/RestoContext.php b/app/resto/core/RestoContext.php index 3544ba31..2ffd586a 100755 --- a/app/resto/core/RestoContext.php +++ b/app/resto/core/RestoContext.php @@ -159,6 +159,11 @@ class RestoContext */ public $query = array(); + /** + * Keeper function + */ + public $keeper; + /** * Constructor * @@ -197,6 +202,11 @@ public function __construct($config) */ $this->addons = $config['addons'] ?? array(); + /* + * Initialize keeper + */ + $this->keeper = new RestoKeeper($this); + /* * Initialize objects */ diff --git a/app/resto/core/RestoKeeper.php b/app/resto/core/RestoKeeper.php new file mode 100755 index 00000000..d98ba507 --- /dev/null +++ b/app/resto/core/RestoKeeper.php @@ -0,0 +1,68 @@ +context = $context; + } + + /** + * Return RestoCollection object + * + * @param string $collectionId + * @param RestoUser $user + * @return RestoCollection + */ + public function getRestoCollection($collectionId, $user) + { + if ( !isset($this->restoCollectionObjects[$collectionId]) ) { + $this->restoCollectionObjects[$collectionId] = new RestoCollection($collectionId, $this->context, $user); + } + + return $this->restoCollectionObjects[$collectionId]; + } + + /** + * Return RestoCollections object + * + * @param RestoUser $user + * @return RestoCollections + */ + public function getRestoCollections($user) + { + if ( !isset($this->restoCollectionsObject) ) { + $this->restoCollectionsObject = new RestoCollections($this->context, $user); + } + return $this->restoCollectionsObject; + } + +} diff --git a/app/resto/core/addons/Group.php b/app/resto/core/addons/Group.php index 0d977d0f..f0596946 100755 --- a/app/resto/core/addons/Group.php +++ b/app/resto/core/addons/Group.php @@ -133,8 +133,7 @@ public function getGroups($params) { return array( 'groups' => (new GroupsFunctions($this->context->dbDriver))->getGroups(array( - 'q' => $params['q'] ?? null, - 'viewAll' => $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) + 'q' => $params['q'] ?? null )) ); } diff --git a/app/resto/core/addons/STAC.php b/app/resto/core/addons/STAC.php index f62db74d..2a1bb46b 100644 --- a/app/resto/core/addons/STAC.php +++ b/app/resto/core/addons/STAC.php @@ -416,7 +416,7 @@ public function getQueryables($params) 'title' => 'Queryables for Example STAC API', 'description' => 'Queryable names for the example STAC API Item Search filter.', // Get common queryables (/queryables) or per collection (/collections/{collectionId}/queryables) - 'properties' => (isset($params['collectionId']) ? ((new RestoCollection($params['collectionId'], $this->context, $this->user))->load())->model : new DefaultModel())->getQueryables(), + 'properties' => (isset($params['collectionId']) ? ($this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load())->model : new DefaultModel())->getQueryables(), 'additionalProperties' => true ); } @@ -948,11 +948,11 @@ public function search($params, $body) if (count($collections) === 1) { $params['collectionId'] = $params['collections']; unset($params['collections']); - return (new RestoCollection($params['collectionId'], $this->context, $this->user))->load()->search($params); + return $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load()->search($params); } } - $restoCollections = (new RestoCollections($this->context, $this->user))->load($params); + $restoCollections = $this->context->keeper->getRestoCollections($this->user)->load($params); /* [TODO] Faire un UNION sur les collections if ( !isset($model) ) { @@ -1132,7 +1132,7 @@ private function setThemesLinks() $this->description = 'Collections for theme **' . $this->segments[1] . '**'; // Load collections - $collections = (new RestoCollections($this->context, $this->user))->load(); + $collections = $this->context->keeper->getRestoCollections($this->user)->load(); $candidates = array(); foreach (array_values($collections->collections) as $collectionContent) { @@ -1367,7 +1367,7 @@ private function setFeatureCollection($hashtag, $params) $this->context->query['fields'] = '_all'; - return $this->featureCollection = (new RestoCollections($this->context, $this->user))->load()->search(null, $searchParams); + return $this->featureCollection = $this->context->keeper->getRestoCollections($this->user)->load()->search(null, $searchParams); } /** diff --git a/app/resto/core/api/CollectionsAPI.php b/app/resto/core/api/CollectionsAPI.php index 9ca543b9..cbbf0589 100755 --- a/app/resto/core/api/CollectionsAPI.php +++ b/app/resto/core/api/CollectionsAPI.php @@ -247,7 +247,7 @@ public function __construct($context, $user) */ public function getCollections($params) { - return (new RestoCollections($this->context, $this->user))->load($params); + return $this->context->keeper->getRestoCollections($this->user)->load($params); } /** @@ -292,7 +292,7 @@ public function getCollections($params) */ public function getCollection($params) { - return (new RestoCollection($params['collectionId'], $this->context, $this->user))->load(); + return $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); } /** @@ -370,7 +370,7 @@ public function createCollection($params, $body) RestoLogUtil::httpError(403); } - (new RestoCollections($this->context, $this->user))->create($body, $params['model'] ?? null); + $this->context->keeper->getRestoCollections($this->user)->create($body, $params['model'] ?? null); return RestoLogUtil::success('Collection ' . $body['id'] . ' created'); } @@ -446,7 +446,7 @@ public function createCollection($params, $body) */ public function updateCollection($params, $body) { - $collection = new RestoCollection($params['collectionId'], $this->context, $this->user); + $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user); $collection->load(); /* @@ -531,7 +531,7 @@ public function updateCollection($params, $body) */ public function deleteCollection($params) { - $collection = (new RestoCollection($params['collectionId'], $this->context, $this->user))->load(); + $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); /* * Only owner of a collection can delete it @@ -681,7 +681,7 @@ public function insertFeatures($params, $body) /* * Load collection */ - $collection = new RestoCollection($params['collectionId'], $this->context, $this->user); + $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user); $collection->load(); /* diff --git a/app/resto/core/api/FeaturesAPI.php b/app/resto/core/api/FeaturesAPI.php index 462db4dd..a3b09717 100755 --- a/app/resto/core/api/FeaturesAPI.php +++ b/app/resto/core/api/FeaturesAPI.php @@ -92,7 +92,7 @@ public function getFeature($params) $feature = new RestoFeature($this->context, $this->user, array( 'featureId' => $params['featureId'], 'fields' => $params['fields'] ?? null, - 'collection' => (new RestoCollection($params['collectionId'], $this->context, $this->user))->load() + 'collection' => $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load() )); if (!$feature->isValid()) { @@ -593,7 +593,7 @@ public function getFeaturesInCollection($params) $this->context->outputFormat = 'geojson'; } - return (new RestoCollection($params['collectionId'], $this->context, $this->user))->load()->search($params); + return $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load()->search($params); } /** @@ -677,7 +677,7 @@ public function getFeaturesInCollection($params) public function updateFeature($params, $body) { // Load collection - $collection = (new RestoCollection($params['collectionId'], $this->context, $this->user))->load(); + $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); $feature = new RestoFeature($this->context, $this->user, array( 'featureId' => $params['featureId'], @@ -804,7 +804,7 @@ public function updateFeatureProperty($params, $body) { $feature = new RestoFeature($this->context, $this->user, array( 'featureId' => $params['featureId'], - 'collection' => (new RestoCollection($params['collectionId'], $this->context, $this->user))->load() + 'collection' => $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load() )); if (!$feature->isValid()) { @@ -912,7 +912,7 @@ public function deleteFeature($params) { $feature = new RestoFeature($this->context, $this->user, array( 'featureId' => $params['featureId'], - 'collection' => (new RestoCollection($params['collectionId'], $this->context, $this->user))->load() + 'collection' => $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load() )); if (!$feature->isValid()) { diff --git a/app/resto/core/api/ServicesAPI.php b/app/resto/core/api/ServicesAPI.php index 0199fd53..002abddc 100755 --- a/app/resto/core/api/ServicesAPI.php +++ b/app/resto/core/api/ServicesAPI.php @@ -282,7 +282,7 @@ public function hello() public function getOSDDForCollection($params) { $this->context->outputFormat = 'xml'; - return (new RestoCollection($params['collectionId'], $this->context, $this->user))->load()->getOSDD(); + return $this->context->keeper->getRestoCollection($params['collectionId'],$this->user)->load()->getOSDD(); } /** @@ -327,7 +327,7 @@ public function getOSDD($params) 'addons' => $this->context->addons )); } - return (new RestoCollections($this->context, $this->user))->getOSDD($model); + return $this->context->keeper->getRestoCollections($this->user)->getOSDD($model); } /** diff --git a/app/resto/core/dbfunctions/GroupsFunctions.php b/app/resto/core/dbfunctions/GroupsFunctions.php index ebd34cda..8cb10143 100755 --- a/app/resto/core/dbfunctions/GroupsFunctions.php +++ b/app/resto/core/dbfunctions/GroupsFunctions.php @@ -44,11 +44,6 @@ public function getGroups($params = array()) { $where = array(); - /* Show hidden groups */ - if (!isset($params['viewAll']) || ! $params['viewAll']) { - $where[] = 'id NOT IN (' . join(',', array(RestoConstants::GROUP_ADMIN_ID, RestoConstants::GROUP_DEFAULT_ID)) . ')'; - } - // Return group by id if (isset($params['id'])) { $where[] = 'id=' . pg_escape_string($params['id']); diff --git a/app/resto/core/utils/RestoFeatureUtil.php b/app/resto/core/utils/RestoFeatureUtil.php index 6f0ec08c..afe7a807 100755 --- a/app/resto/core/utils/RestoFeatureUtil.php +++ b/app/resto/core/utils/RestoFeatureUtil.php @@ -70,7 +70,7 @@ public function toFeatureArray($rawFeatureArray) */ $collection = $this->collections[$rawFeatureArray['collection']] ?? null; if (!isset($collection)) { - $collection = (new RestoCollection($rawFeatureArray['collection'], $this->context, $this->user))->load(); + $collection = $this->context->keeper->getRestoCollection($rawFeatureArray['collection'], $this->user)->load(); $this->collections[$rawFeatureArray['collection']] = $collection; } diff --git a/app/resto/core/utils/STACUtil.php b/app/resto/core/utils/STACUtil.php index 76da795f..4665e963 100755 --- a/app/resto/core/utils/STACUtil.php +++ b/app/resto/core/utils/STACUtil.php @@ -82,7 +82,7 @@ public function getRootCatalogLinks($minMatch) /* * [STAC] Duplicate rel="data" */ - $collections = ((new RestoCollections($this->context, $this->user))->load())->toArray(); + $collections = ($this->context->keeper->getRestoCollections($this->user)->load())->toArray(); if (count($collections) > 0) { $links[] = array( 'rel' => 'child', @@ -147,7 +147,7 @@ public function getThemesRootLinks() $links = array(); // Load collections - $collections = (new RestoCollections($this->context, $this->user))->load(); + $collections = $this->context->keeper->getRestoCollections($this->user)->load(); // Get themes $themes = array(); diff --git a/build/resto/container_root/cont-init.d/18-resto-admin-user.php b/build/resto/container_root/cont-init.d/18-resto-admin-user.php index 9b424f71..ff371068 100755 --- a/build/resto/container_root/cont-init.d/18-resto-admin-user.php +++ b/build/resto/container_root/cont-init.d/18-resto-admin-user.php @@ -24,6 +24,6 @@ password_hash(getenv('ADMIN_USER_PASSWORD') ?? 'admin', PASSWORD_BCRYPT) )); $dbDriver->pQuery('INSERT INTO ' . $dbDriver->commonSchema . '.group_member (groupid,userid,created) VALUES ($1,$2,now_utc()) ON CONFLICT (groupid,userid) DO NOTHING', array( - 100, + 0, 100 )); From 0302bff73cec50593d09720813fdb472b584af77 Mon Sep 17 00:00:00 2001 From: jjrom Date: Fri, 5 Apr 2024 12:04:59 +0200 Subject: [PATCH 5/9] Update migration script to remove unusued groups column in user table --- resto-database-model/migrations/00_migration.sql | 10 ++++++++++ resto-database-model/migrations/00_resto_6_to_7.sql | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 resto-database-model/migrations/00_migration.sql delete mode 100644 resto-database-model/migrations/00_resto_6_to_7.sql diff --git a/resto-database-model/migrations/00_migration.sql b/resto-database-model/migrations/00_migration.sql new file mode 100644 index 00000000..d178e97b --- /dev/null +++ b/resto-database-model/migrations/00_migration.sql @@ -0,0 +1,10 @@ +-- +-- resto v6.x to v7.x +-- +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right ADD COLUMN IF NOT EXISTS target TEXT; + +-- +-- Add resto-addon-groups to resto core +-- https://github.com/jjrom/resto/commit/fbf372d856686892b26a3bb573ad0ea2e66a156d +-- +ALTER TABLE __DATABASE_COMMON_SCHEMA__.user DROP COLUMN IF EXISTS groups; \ No newline at end of file diff --git a/resto-database-model/migrations/00_resto_6_to_7.sql b/resto-database-model/migrations/00_resto_6_to_7.sql deleted file mode 100644 index 5af251f6..00000000 --- a/resto-database-model/migrations/00_resto_6_to_7.sql +++ /dev/null @@ -1,6 +0,0 @@ --- --- resto v6.x to v7.x SQL migration script --- - -ALTER TABLE resto.right ADD COLUMN IF NOT EXISTS target TEXT; - From 93e1d28606a4c91415c4f3303ad13fc8b4589b1c Mon Sep 17 00:00:00 2001 From: jjrom Date: Fri, 5 Apr 2024 16:27:21 +0200 Subject: [PATCH 6/9] [NEW] Add collection aliases --- app/resto/core/RestoCollection.php | 48 +++++++---- app/resto/core/RestoCollections.php | 9 +- app/resto/core/RestoKeeper.php | 19 +++-- app/resto/core/addons/STAC.php | 2 +- .../core/dbfunctions/CollectionsFunctions.php | 82 ++++++++++++++++++- .../core/dbfunctions/FacetsFunctions.php | 8 +- .../core/dbfunctions/FeaturesFunctions.php | 4 +- .../core/dbfunctions/FiltersFunctions.php | 8 +- .../core/dbfunctions/GeneralFunctions.php | 2 +- .../core/dbfunctions/GroupsFunctions.php | 4 +- examples/collections/L8.json | 3 + .../02_resto_common_model.sql | 4 +- .../03_resto_target_model.sql | 23 ++++-- 13 files changed, 164 insertions(+), 52 deletions(-) diff --git a/app/resto/core/RestoCollection.php b/app/resto/core/RestoCollection.php index 7da12ab2..6b403c5f 100755 --- a/app/resto/core/RestoCollection.php +++ b/app/resto/core/RestoCollection.php @@ -37,6 +37,14 @@ * description="Detailed multi-line description to fully explain the Collection. CommonMark 0.29 syntax MAY be used for rich text representation." * ), * @OA\Property( + * property="aliases", + * type="array", + * description="Alias names for this collection. Each alias must be unique and not be the same as an already existing collection name", + * @OA\Items( + * type="string", + * ) + * ), + * @OA\Property( * property="version", * type="string", * description="Version of the collection." @@ -276,6 +284,14 @@ * ) * ), * @OA\Property( + * property="aliases", + * type="array", + * description="Alias names for this collection. Each alias must be unique and not be the same as an already existing collection name", + * @OA\Items( + * type="string", + * ) + * ), + * @OA\Property( * property="license", * type="string", * enum={"proprietary", "various", ""}, @@ -570,6 +586,7 @@ class RestoCollection /* * [STAC] Collection root attributes */ + public $aliases = array(); public $visibility = RestoConstants::GROUP_DEFAULT_ID; public $version = '1.0.0'; public $license = 'proprietary'; @@ -782,6 +799,7 @@ class RestoCollection * Properties that are not stored in properties column */ private $notInProperties = array( + 'aliases', 'assets', 'description', 'extent', @@ -801,6 +819,11 @@ class RestoCollection 'visibility' ); + /** + * Avoid call to database when object is already loaded + */ + private $isLoaded = false; + /** * Constructor * @@ -810,17 +833,6 @@ class RestoCollection */ public function __construct($id, $context, $user) { - /* - * Remove collection name constraint - if (isset($id)) { - // Collection identifier is an alphanumeric string without special characters - if (preg_match("/^[a-zA-Z0-9\-_\.\:]+$/", $id) !== 1) { - RestoLogUtil::httpError(400, 'Collection identifier must be an alphanumeric string containing only [a-zA-Z0-9\-_.:]'); - } - - $this->id = $id; - }*/ - $this->id = $id; $this->context = $context; $this->user = $user; @@ -837,14 +849,14 @@ public function __construct($id, $context, $user) public function load($object = null, $modelName = null) { + if (isset($object)) { + return $this->loadFromJSON($object, $modelName); + } + if ( !$this->isLoaded ) { $this->isLoaded = true; - if (isset($object)) { - return $this->loadFromJSON($object, $modelName); - } - $cacheKey = 'collection:' . $this->id; $collectionObject = $this->context->fromCache($cacheKey); @@ -937,6 +949,7 @@ public function toArray($options = array()) 'title' => $osDescription['LongName'] ?? $osDescription['ShortName'], 'version' => $this->version ?? null, 'description' => $osDescription['Description'], + 'aliases' => $this->aliases ?? array(), 'license' => $this->license, 'extent' => $this->extent, 'links' => array_merge( @@ -1098,7 +1111,7 @@ private function loadFromJSON($object, $modelName = null) /* * Set values */ - foreach (array_values(array('version', 'license', 'links', 'osDescription', 'providers', 'rights', 'assets', 'keywords', 'extent')) as $key) { + foreach (array_values(array('aliases', 'version', 'license', 'links', 'osDescription', 'providers', 'rights', 'assets', 'keywords', 'extent')) as $key) { if (isset($object[$key])) { $this->$key = $key === 'links' ? $this->cleanInputLinks($object['links']) : $object[$key]; } @@ -1116,7 +1129,8 @@ private function loadFromJSON($object, $modelName = null) } } - + $this->isLoaded = true; + return $this; } diff --git a/app/resto/core/RestoCollections.php b/app/resto/core/RestoCollections.php index b7f82520..8739565e 100755 --- a/app/resto/core/RestoCollections.php +++ b/app/resto/core/RestoCollections.php @@ -161,9 +161,14 @@ public function create($object, $modelName) } /* - * Check that collection does not exist based on id + * Check that collection does not exist i.e. id or alias not used */ - if ((new CollectionsFunctions($this->context->dbDriver))->collectionExists($object['id'])) { + $collectionsFunctions = new CollectionsFunctions($this->context->dbDriver); + $collectionId = $collectionsFunctions->aliasToCollectionId($object['id']); + if ( isset($collectionId) ) { + RestoLogUtil::httpError(409, 'Collection ' . $object['id'] . ' already exist as an alias of collection ' . $collectionId); + } + if ($collectionsFunctions->collectionExists($object['id'])) { RestoLogUtil::httpError(409, 'Collection ' . $object['id'] . ' already exist'); } diff --git a/app/resto/core/RestoKeeper.php b/app/resto/core/RestoKeeper.php index d98ba507..605cc230 100755 --- a/app/resto/core/RestoKeeper.php +++ b/app/resto/core/RestoKeeper.php @@ -21,9 +21,12 @@ */ class RestoKeeper { + + private $context; + + private $collectionsObject = null; - private $restoCollectionsObject = null; - private $restoCollectionObjects = array(); + private $collectionObjects = array(); /** * Constructor @@ -44,11 +47,11 @@ public function __construct($context) */ public function getRestoCollection($collectionId, $user) { - if ( !isset($this->restoCollectionObjects[$collectionId]) ) { - $this->restoCollectionObjects[$collectionId] = new RestoCollection($collectionId, $this->context, $user); + if ( !isset($this->collectionObjects[$collectionId]) ) { + $this->collectionObjects[$collectionId] = new RestoCollection($collectionId, $this->context, $user); } - return $this->restoCollectionObjects[$collectionId]; + return $this->collectionObjects[$collectionId]; } /** @@ -59,10 +62,10 @@ public function getRestoCollection($collectionId, $user) */ public function getRestoCollections($user) { - if ( !isset($this->restoCollectionsObject) ) { - $this->restoCollectionsObject = new RestoCollections($this->context, $user); + if ( !isset($this->collectionsObject) ) { + $this->collectionsObject = new RestoCollections($this->context, $user); } - return $this->restoCollectionsObject; + return $this->collectionsObject; } } diff --git a/app/resto/core/addons/STAC.php b/app/resto/core/addons/STAC.php index 2a1bb46b..b3a5334d 100644 --- a/app/resto/core/addons/STAC.php +++ b/app/resto/core/addons/STAC.php @@ -1384,7 +1384,7 @@ private function setTitleAndDescription($facetId) $this->description = 'Search on ' . $title; try { - $results = $this->context->dbDriver->pQuery('SELECT value, description FROM ' . $this->context->dbDriver->targetSchema . '.facet WHERE normalize(id)=normalize($1)', array($facetId)); + $results = $this->context->dbDriver->pQuery('SELECT value, description FROM ' . $this->context->dbDriver->targetSchema . '.facet WHERE public.normalize(id)=public.normalize($1)', array($facetId)); if (!$results) { throw new Exception(); } diff --git a/app/resto/core/dbfunctions/CollectionsFunctions.php b/app/resto/core/dbfunctions/CollectionsFunctions.php index 902bac2a..15af2f9a 100755 --- a/app/resto/core/dbfunctions/CollectionsFunctions.php +++ b/app/resto/core/dbfunctions/CollectionsFunctions.php @@ -42,14 +42,33 @@ public function __construct($dbDriver) */ public function getCollectionDescription($id) { - // Get Opensearch description - $osDescriptions = $this->getOSDescriptions($id); + + // Eventually convert input alias to the real collection id + $collectionId = $this->aliasToCollectionId($id); + if ( isset($collectionId) ) { + $id = $collectionId; + } + + // Query with aliases + $query = join(' ', array( + 'SELECT id, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords, STRING_AGG(ca.alias, \', \' ORDER BY ca.alias) AS aliases', + 'FROM ' . $this->dbDriver->targetSchema . '.collection', + 'LEFT JOIN ' . $this->dbDriver->targetSchema . '.collection_alias ca ON id = ca.collection', + 'WHERE public.normalize(id)=public.normalize($1)', + 'GROUP BY id' + )); + + $results = $this->dbDriver->pQuery($query, array($id)); + $collection = null; - $results = $this->dbDriver->pQuery('SELECT id, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords FROM ' . $this->dbDriver->targetSchema . '.collection WHERE normalize(id)=normalize($1)', array($id)); while ($rowDescription = pg_fetch_assoc($results)) { + // Get Opensearch description + $osDescriptions = $this->getOSDescriptions($id); $collection = $this->format($rowDescription, $osDescriptions[$id] ?? null); } + return $collection; + } /** @@ -77,7 +96,17 @@ public function getCollectionsDescriptions($params = array()) $where[] = 'keywords @> ARRAY[\'' . pg_escape_string($this->dbDriver->getConnection(), $params['ck']) . '\']'; } - $results = $this->dbDriver->query('SELECT id, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords FROM ' . $this->dbDriver->targetSchema . '.collection' . (count($where) > 0 ? ' WHERE ' . join(' AND ', $where) : '') . ' ORDER BY id'); + // Query with aliases + $query = join(' ', array( + 'SELECT id, version, visibility, owner, model, licenseid, to_iso8601(startdate) as startdate, to_iso8601(completiondate) as completiondate, Box2D(bbox) as box2d, providers, properties, links, assets, array_to_json(keywords) as keywords, STRING_AGG(ca.alias, \', \' ORDER BY ca.alias) AS aliases', + 'FROM ' . $this->dbDriver->targetSchema . '.collection', + 'LEFT JOIN ' . $this->dbDriver->targetSchema . '.collection_alias ca ON id = ca.collection', + (count($where) > 0 ? 'WHERE ' . join(' AND ', $where) : ''), + 'GROUP BY id', + 'ORDER BY id' + )); + + $results = $this->dbDriver->query($query); while ($rowDescription = pg_fetch_assoc($results)) { $collections[$rowDescription['id']] = $this->format($rowDescription, $osDescriptions[$rowDescription['id']] ?? null); } @@ -252,7 +281,48 @@ public function updateExtent($collection, $extentArrays) return true; } + + /** + * Store collection aliases + * + * @param String $collectionName + * @param array $aliases + * + */ + public function updateAliases($collectionName, $aliases) + { + + // First DELETE all existing collection aliases + $this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->targetSchema . '.collection_alias WHERE collection=$1', array( + $collectionName + )); + + // Next INSERT one row per alias + for ($i = 0, $ii = count($aliases); $i < $ii; $i++) { + $this->dbDriver->pQuery('INSERT INTO ' . $this->dbDriver->targetSchema . '.collection_alias (alias, collection) VALUES($1, $2) ON CONFLICT (alias) DO NOTHING', array( + $aliases[$i], + $collectionName + )); + } + + } + /** + * Return collection name from alias + * + * @param string $alias + * @return string + */ + public function aliasToCollectionId($alias) { + $results = $this->dbDriver->fetch($this->dbDriver->pQuery('SELECT collection FROM ' . $this->dbDriver->targetSchema . '.collection_alias WHERE alias=$1', array( + $alias + ))); + if ( isset($results) && count($results) === 1 ) { + return $results[0]['collection']; + } + return null; + } + /** * Get time extent from a list of time extent * @@ -433,6 +503,9 @@ private function storeCollectionDescription($collection) )); } + // Store aliases + $this->updateAliases($collection->id, $collection->aliases ?? array()); + // OpenSearch description is stored in another table $this->storeOSDescription($collection); @@ -540,6 +613,7 @@ private function format($rawDescription, $osDescription) { $collection = array( 'id' => $rawDescription['id'], + 'aliases' => $rawDescription['aliases'] ?? null, 'version' => $rawDescription['version'] ?? null, 'model' => $rawDescription['model'], 'visibility' => (integer) $rawDescription['visibility'], diff --git a/app/resto/core/dbfunctions/FacetsFunctions.php b/app/resto/core/dbfunctions/FacetsFunctions.php index c79b5a62..9b9f9dfa 100755 --- a/app/resto/core/dbfunctions/FacetsFunctions.php +++ b/app/resto/core/dbfunctions/FacetsFunctions.php @@ -65,7 +65,7 @@ public static function format($rawFacet) */ public function getFacet($facetId) { - $results = $this->dbDriver->fetch($this->dbDriver->pQuery('SELECT id, collection, value, type, pid, to_iso8601(created) as created, creator, description FROM ' . $this->dbDriver->targetSchema . '.facet WHERE normalize(id)=normalize($1) LIMIT 1', array( + $results = $this->dbDriver->fetch($this->dbDriver->pQuery('SELECT id, collection, value, type, pid, to_iso8601(created) as created, creator, description FROM ' . $this->dbDriver->targetSchema . '.facet WHERE public.normalize(id)=public.normalize($1) LIMIT 1', array( $facetId ))); if (isset($results[0])) { @@ -126,7 +126,7 @@ public function storeFacets($facets, $collectionId = '*') * [IMPORTANT] UPSERT with check on parentId only if $facetElement['parentId'] is set */ $insert = 'INSERT INTO ' . $this->dbDriver->targetSchema . '.facet (id, collection, value, type, pid, creator, description, created, counter, isleaf) SELECT $1,$2,$3,$4,$5,$6,$7,now(),$8,$9'; - $upsert = 'UPDATE ' . $this->dbDriver->targetSchema . '.facet SET counter=' .(isset($facetElement['counter']) ? 'counter' : 'counter+1') . ' WHERE normalize(id)=normalize($1) AND normalize(collection)=normalize($2)' . (isset($facetElement['parentId']) ? ' AND normalize(pid)=normalize($5)' : ''); + $upsert = 'UPDATE ' . $this->dbDriver->targetSchema . '.facet SET counter=' .(isset($facetElement['counter']) ? 'counter' : 'counter+1') . ' WHERE public.normalize(id)=public.normalize($1) AND public.normalize(collection)=public.normalize($2)' . (isset($facetElement['parentId']) ? ' AND public.normalize(pid)=public.normalize($5)' : ''); $this->dbDriver->pQuery('WITH upsert AS (' . $upsert . ' RETURNING *) ' . $insert . ' WHERE NOT EXISTS (SELECT * FROM upsert)', array( $facetElement['id'], $facetElement['collection'] ?? $collectionId, @@ -150,7 +150,7 @@ public function storeFacets($facets, $collectionId = '*') */ public function removeFacet($facetId, $collectionId) { - $this->dbDriver->pQuery('UPDATE ' . $this->dbDriver->targetSchema . '.facet SET counter = GREATEST(0, counter - 1) WHERE normalize(id)=normalize($1) AND (normalize(collection)=normalize($2) OR normalize(collection)=\'*\')', array($facetId, $collectionId), 500, 'Cannot delete facet for ' . $collectionId); + $this->dbDriver->pQuery('UPDATE ' . $this->dbDriver->targetSchema . '.facet SET counter = GREATEST(0, counter - 1) WHERE public.normalize(id)=public.normalize($1) AND (public.normalize(collection)=public.normalize($2) OR public.normalize(collection)=\'*\')', array($facetId, $collectionId), 500, 'Cannot delete facet for ' . $collectionId); } /** @@ -319,7 +319,7 @@ private function getFacetsPivots($collection, $fields) ); if (isset($collectionId)) { - $where[] = '(normalize(collection)=normalize(\'' . pg_escape_string($this->dbDriver->getConnection(), $collectionId) . '\') OR normalize(collection)=\'*\')'; + $where[] = '(public.normalize(collection)=public.normalize(\'' . pg_escape_string($this->dbDriver->getConnection(), $collectionId) . '\') OR public.normalize(collection)=\'*\')'; } if (isset($fields)) { $where[] = 'type IN(\'' . join('\',\'', $fields) . '\')'; diff --git a/app/resto/core/dbfunctions/FeaturesFunctions.php b/app/resto/core/dbfunctions/FeaturesFunctions.php index 46408e1e..cc430ee9 100755 --- a/app/resto/core/dbfunctions/FeaturesFunctions.php +++ b/app/resto/core/dbfunctions/FeaturesFunctions.php @@ -555,7 +555,7 @@ public function updateFeatureDescription($feature, $description) /* * Update description, hashtags and normalized_hashtags */ - $this->dbDriver->pQuery('UPDATE ' . $this->dbDriver->targetSchema . '.' . $model->dbParams['tablePrefix'] . 'feature SET description=$1, hashtags=$2, normalized_hashtags=normalize_array($2) WHERE id=$3', array( + $this->dbDriver->pQuery('UPDATE ' . $this->dbDriver->targetSchema . '.' . $model->dbParams['tablePrefix'] . 'feature SET description=$1, hashtags=$2, normalized_hashtags=public.normalize_array($2) WHERE id=$3', array( $description, '{' . join(',', $hashtags) . '}', $feature->id @@ -809,7 +809,7 @@ private function featureArrayToKeysValues($collection, $featureArray, $protected $output['keysAndValues'] = array_merge($protected, $keysAndValues); foreach (array_keys($output['keysAndValues'] ?? array()) as $key) { if ($key === 'normalized_hashtags') { - $output['params'][] = 'normalize_array($' . ++$counter . ')'; + $output['params'][] = 'public.normalize_array($' . ++$counter . ')'; } elseif ($key === 'created_idx' || $key === 'startdate_idx') { $output['params'][] = 'public.timestamp_to_id($' . ++$counter . ')'; } else { diff --git a/app/resto/core/dbfunctions/FiltersFunctions.php b/app/resto/core/dbfunctions/FiltersFunctions.php index cd653a32..36cb9f67 100755 --- a/app/resto/core/dbfunctions/FiltersFunctions.php +++ b/app/resto/core/dbfunctions/FiltersFunctions.php @@ -628,7 +628,7 @@ private function processSearchTerms($searchTerm, &$filters, $featureTableName, $ for ($j = count($exploded); $j--;) { $quotedValues[] = '\'' . pg_escape_string($this->context->dbDriver->getConnection(), trim($exploded[$j])) . '\''; } - return array($this->addNot($exclusion) . '(' . $featureTableName . '.' . $this->model->searchFilters[$filterName]['key'] . $operator . 'normalize_array(ARRAY[' . join(',', $quotedValues) . ']))'); + return array($this->addNot($exclusion) . '(' . $featureTableName . '.' . $this->model->searchFilters[$filterName]['key'] . $operator . 'public.normalize_array(ARRAY[' . join(',', $quotedValues) . ']))'); } $filters[$exclusion ? 'without' : 'with'][] = "'" . pg_escape_string($this->context->dbDriver->getConnection(), $searchTerm) . "'"; @@ -648,10 +648,10 @@ private function mergeHashesFilters($key, $filters) { $terms = array(); if (count($filters['without']) > 0) { - $terms[] = 'NOT ' . $key . " @> normalize_array(ARRAY[" . join(',', $filters['without']) . "])"; + $terms[] = 'NOT ' . $key . " @> public.normalize_array(ARRAY[" . join(',', $filters['without']) . "])"; } if (count($filters['with']) > 0) { - $terms[] = $key . " @> normalize_array(ARRAY[" . join(',', $filters['with']) . "])"; + $terms[] = $key . " @> public.normalize_array(ARRAY[" . join(',', $filters['with']) . "])"; } return $terms; } @@ -754,7 +754,7 @@ private function addPrefix($searchTerm, $prefix) * 'resto.feature_optical.cloudCover > 10', * 'resto.feature_optical.cloudCover <= 30', * 'ST_Intersects(resto.feature.geom, ST_GeomFromText('POINT(10 10)', 4326))', - * 'resto.feature.normalized_hashtags @> normalize_array(ARRAY['instrument:PHR'] + * 'resto.feature.normalized_hashtags @> public.normalize_array(ARRAY['instrument:PHR'] * ) * * diff --git a/app/resto/core/dbfunctions/GeneralFunctions.php b/app/resto/core/dbfunctions/GeneralFunctions.php index 1a847631..7f9b9c55 100755 --- a/app/resto/core/dbfunctions/GeneralFunctions.php +++ b/app/resto/core/dbfunctions/GeneralFunctions.php @@ -62,7 +62,7 @@ public function tableExists($schemaName, $tableName) public function getKeywords($language = 'en', $types = array()) { $keywords = array(); - $results = $this->dbDriver->query('SELECT name, normalize(name) as normalized, type, value, location FROM ' . $this->dbDriver->commonSchema . '.keyword WHERE ' . 'lang IN(\'' . pg_escape_string($this->dbDriver->getConnection(), $language) . '\', \'**\')' . (count($types) > 0 ? ' AND type IN(' . join(',', $types) . ')' : '')); + $results = $this->dbDriver->query('SELECT name, public.normalize(name) as normalized, type, value, location FROM ' . $this->dbDriver->commonSchema . '.keyword WHERE ' . 'lang IN(\'' . pg_escape_string($this->dbDriver->getConnection(), $language) . '\', \'**\')' . (count($types) > 0 ? ' AND type IN(' . join(',', $types) . ')' : '')); while ($result = pg_fetch_assoc($results)) { if (!isset($keywords[$result['type']])) { $keywords[$result['type']] = array(); diff --git a/app/resto/core/dbfunctions/GroupsFunctions.php b/app/resto/core/dbfunctions/GroupsFunctions.php index 8cb10143..3851cb3f 100755 --- a/app/resto/core/dbfunctions/GroupsFunctions.php +++ b/app/resto/core/dbfunctions/GroupsFunctions.php @@ -56,7 +56,7 @@ public function getGroups($params = array()) // Search by name if (isset($params['q'])) { - $where[] = 'normalize(name) LIKE normalize(\'%' . pg_escape_string($params['q']) . '%\')'; + $where[] = 'public.normalize(name) LIKE public.normalize(\'%' . pg_escape_string($params['q']) . '%\')'; } // Return groups by userid @@ -80,7 +80,7 @@ public function createGroup($params) } try { - $result = pg_query_params($this->dbDriver->getConnection(), 'INSERT INTO ' . $this->dbDriver->commonSchema . '.group (name, description, owner, created) VALUES ($1, $2, $3, now()) ON CONFLICT (normalize(name)) DO NOTHING RETURNING id ', array( + $result = pg_query_params($this->dbDriver->getConnection(), 'INSERT INTO ' . $this->dbDriver->commonSchema . '.group (name, description, owner, created) VALUES ($1, $2, $3, now()) ON CONFLICT (public.normalize(name)) DO NOTHING RETURNING id ', array( $params['body']['name'], $params['body']['description'] ?? null, $params['id'] diff --git a/examples/collections/L8.json b/examples/collections/L8.json index f6ddcedb..13584f60 100644 --- a/examples/collections/L8.json +++ b/examples/collections/L8.json @@ -1,5 +1,8 @@ { "id": "L8", + "aliases":[ + "Landsat8" + ], "version": "1.0", "model": "OpticalModel", "rights": { diff --git a/resto-database-model/02_resto_common_model.sql b/resto-database-model/02_resto_common_model.sql index f49afebc..8b1fb800 100644 --- a/resto-database-model/02_resto_common_model.sql +++ b/resto-database-model/02_resto_common_model.sql @@ -294,8 +294,8 @@ CREATE INDEX IF NOT EXISTS idx_userid_right ON __DATABASE_COMMON_SCHEMA__.right CREATE INDEX IF NOT EXISTS idx_groupid_right ON __DATABASE_COMMON_SCHEMA__.right (groupid); -- [TABLE __DATABASE_COMMON_SCHEMA__.group] -CREATE UNIQUE INDEX IF NOT EXISTS idx_uname_group ON __DATABASE_COMMON_SCHEMA__.group (normalize(name)); -CREATE INDEX IF NOT EXISTS idx_name_group ON __DATABASE_COMMON_SCHEMA__.group USING GIN (normalize(name) gin_trgm_ops); +CREATE UNIQUE INDEX IF NOT EXISTS idx_uname_group ON __DATABASE_COMMON_SCHEMA__.group (public.normalize(name)); +CREATE INDEX IF NOT EXISTS idx_name_group ON __DATABASE_COMMON_SCHEMA__.group USING GIN (public.normalize(name) gin_trgm_ops); -- [TABLE __DATABASE_COMMON_SCHEMA__.log] CREATE INDEX IF NOT EXISTS idx_userid_log ON __DATABASE_COMMON_SCHEMA__.log (userid); diff --git a/resto-database-model/03_resto_target_model.sql b/resto-database-model/03_resto_target_model.sql index d69c7623..ffd63b91 100644 --- a/resto-database-model/03_resto_target_model.sql +++ b/resto-database-model/03_resto_target_model.sql @@ -100,6 +100,19 @@ CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.osdescription ( ); +-- +-- Collection aliases +-- +CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.collection_alias ( + + -- Alternate name to this collection + alias TEXT PRIMARY KEY, + + -- [INDEXED] Reference __DATABASE_TARGET_SCHEMA__.collection.id + collection TEXT REFERENCES __DATABASE_TARGET_SCHEMA__.collection (id) ON DELETE CASCADE + +); + -- -- Features table - handle every metadata -- @@ -401,7 +414,7 @@ CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.facet ( -- --------------------- INDEXES --------------------------- -- [TABLE __DATABASE_TARGET_SCHEMA__.collection] -CREATE UNIQUE INDEX IF NOT EXISTS idx_id_collection on __DATABASE_TARGET_SCHEMA__.collection (normalize(id)); +CREATE UNIQUE INDEX IF NOT EXISTS idx_id_collection on __DATABASE_TARGET_SCHEMA__.collection (public.normalize(id)); CREATE INDEX IF NOT EXISTS idx_lineage_collection ON __DATABASE_TARGET_SCHEMA__.collection USING GIN (lineage); CREATE INDEX IF NOT EXISTS idx_visibility_collection ON __DATABASE_TARGET_SCHEMA__.collection (visibility); CREATE INDEX IF NOT EXISTS idx_created_collection ON __DATABASE_TARGET_SCHEMA__.collection (created); @@ -415,11 +428,11 @@ CREATE INDEX IF NOT EXISTS idx_collection_osdescription ON __DATABASE_TARGET_SCH -- CREATE INDEX IF NOT EXISTS idx_lang_osdescription ON __DATABASE_TARGET_SCHEMA__.osdescription (lang); -- [TABLE __DATABASE_TARGET_SCHEMA__.facet] -CREATE INDEX IF NOT EXISTS idx_id_facet ON __DATABASE_TARGET_SCHEMA__.facet (normalize(id)); -CREATE INDEX IF NOT EXISTS idx_pid_facet ON __DATABASE_TARGET_SCHEMA__.facet (normalize(pid)); +CREATE INDEX IF NOT EXISTS idx_id_facet ON __DATABASE_TARGET_SCHEMA__.facet (public.normalize(id)); +CREATE INDEX IF NOT EXISTS idx_pid_facet ON __DATABASE_TARGET_SCHEMA__.facet (public.normalize(pid)); CREATE INDEX IF NOT EXISTS idx_type_facet ON __DATABASE_TARGET_SCHEMA__.facet (type); -CREATE INDEX IF NOT EXISTS idx_collection_facet ON __DATABASE_TARGET_SCHEMA__.facet (normalize(collection)); -CREATE INDEX IF NOT EXISTS idx_value_facet ON __DATABASE_TARGET_SCHEMA__.facet USING GIN (normalize(value) gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_collection_facet ON __DATABASE_TARGET_SCHEMA__.facet (public.normalize(collection)); +CREATE INDEX IF NOT EXISTS idx_value_facet ON __DATABASE_TARGET_SCHEMA__.facet USING GIN (public.normalize(value) gin_trgm_ops); -- [TABLE __DATABASE_TARGET_SCHEMA__.feature] CREATE INDEX IF NOT EXISTS idx_collection_feature ON __DATABASE_TARGET_SCHEMA__.feature USING btree (collection); From 876f72182987b3d3a64363b237549b53a7c25ac3 Mon Sep 17 00:00:00 2001 From: jjrom Date: Sun, 7 Apr 2024 19:29:48 +0200 Subject: [PATCH 7/9] Rights almost working - needs to check hasRightsTo in RestoUser class --- README.md | 2 + app/resto/core/RestoRouter.php | 22 +- app/resto/core/RestoUser.php | 116 ++---- app/resto/core/api/CollectionsAPI.php | 8 +- .../{addons/Group.php => api/GroupAPI.php} | 13 +- app/resto/core/api/RightsAPI.php | 357 +++++++++++++++++ app/resto/core/api/UsersAPI.php | 21 - .../core/dbfunctions/CollectionsFunctions.php | 16 - .../core/dbfunctions/GroupsFunctions.php | 8 +- .../core/dbfunctions/RightsFunctions.php | 376 ++++-------------- examples/collections/L8.json | 5 - examples/collections/S2.json | 5 - .../02_resto_common_model.sql | 37 +- .../03_resto_target_model.sql | 4 +- .../migrations/00_migration.sql | 13 +- 15 files changed, 531 insertions(+), 472 deletions(-) rename app/resto/core/{addons/Group.php => api/GroupAPI.php} (98%) create mode 100755 app/resto/core/api/RightsAPI.php diff --git a/README.md b/README.md index 97067790..827bab3e 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Then get the feature : curl "http://localhost:5252/collections/S2/items/S2A_MSIL1C_20190611T160901_N0207_R140_T23XMD_20190611T193040" +## Rights and groups + # TL;DR The [INSTALLATION.md](INSTALLATION.md) file provides additional information on the installation process. diff --git a/app/resto/core/RestoRouter.php b/app/resto/core/RestoRouter.php index 45a8df45..231d37c8 100755 --- a/app/resto/core/RestoRouter.php +++ b/app/resto/core/RestoRouter.php @@ -44,7 +44,6 @@ class RestoRouter const ROUTE_TO_STAC_SEARCH = '/search'; const ROUTE_TO_USERS = '/users'; const ROUTE_TO_USER = RestoRouter::ROUTE_TO_USERS . '/{userid}'; - const ROUTE_TO_USER_RIGHTS = RestoRouter::ROUTE_TO_USERS . '/rights'; /* * Default routes @@ -65,17 +64,20 @@ class RestoRouter array('GET', RestoRouter::ROUTE_TO_USER, true, 'UsersAPI::getUserProfile'), // Show user profile array('PUT', RestoRouter::ROUTE_TO_USER, true, 'UsersAPI::updateUserProfile'), // Update :userid profile array('GET', RestoRouter::ROUTE_TO_USER . '/logs', true, 'UsersAPI::getUserLogs'), // Show user logs - array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS, true, 'UsersAPI::getUserRights'), // Show user rights - array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS . '/{collectionId}', true, 'UsersAPI::getUserRights'), // Show user rights for :collectionId - array('GET', RestoRouter::ROUTE_TO_USER_RIGHTS . '/{collectionId}/{featureId}', true, 'UsersAPI::getUserRights'), // Show user rights for :featureId // API for groups - array('GET' , RestoRouter::ROUTE_TO_GROUPS, true, 'Group::getGroups'), // List users profiles - array('GET' , RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'Group::getGroup'), // Get group - array('POST' , RestoRouter::ROUTE_TO_GROUPS, true, 'Group::createGroup'), // Create group - array('DELETE', RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'Group::deleteGroup'), // Delete group - array('GET' , RestoRouter::ROUTE_TO_USER . '/groups', true, 'Group::getUserGroups'), // Show user groups - + array('GET' , RestoRouter::ROUTE_TO_GROUPS, true, 'GroupAPI::getGroups'), // List users profiles + array('GET' , RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'GroupAPI::getGroup'), // Get group + array('POST' , RestoRouter::ROUTE_TO_GROUPS, true, 'GroupAPI::createGroup'), // Create group + array('DELETE', RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'GroupAPI::deleteGroup'), // Delete group + array('GET' , RestoRouter::ROUTE_TO_USER . '/groups', true, 'GroupAPI::getUserGroups'), // Show user groups + + // API for rights + array('GET', RestoRouter::ROUTE_TO_USER . '/rights', true, 'RightsAPI::getUserRights'), // Show user rights + array('GET', RestoRouter::ROUTE_TO_GROUPS . '/{id}/rights', true, 'RightsAPI::getGroupRights'), // Show group rights + array('POST' , RestoRouter::ROUTE_TO_USER . '/rights', true, 'RightsAPI::setUserRights'), // Set user rights + array('POST' , RestoRouter::ROUTE_TO_GROUPS . '/{id}/rights', true, 'RightsAPI::setGroupRights'), // Set group rights + // API for collections array('GET', RestoRouter::ROUTE_TO_COLLECTIONS, false, 'CollectionsAPI::getCollections'), // List all collections array('POST', RestoRouter::ROUTE_TO_COLLECTIONS, true, 'CollectionsAPI::createCollection'), // Create collection diff --git a/app/resto/core/RestoUser.php b/app/resto/core/RestoUser.php index 58a6dece..afc185e2 100755 --- a/app/resto/core/RestoUser.php +++ b/app/resto/core/RestoUser.php @@ -24,10 +24,21 @@ */ class RestoUser { - const CREATE = 'create'; - const DOWNLOAD = 'download'; - const UPDATE = 'update'; - const VISUALIZE = 'visualize'; + const CREATE_COLLECTION = 'createCollection'; + const DELETE_COLLECTION = 'deleteCollection'; + const UPDATE_COLLECTION = 'updateCollection'; + + const DELETE_ANY_COLLECTION = 'deleteAnyCollection'; + const UPDATE_ANY_COLLECTION = 'updateAnyCollection'; + + const CREATE_FEATURE = 'createFeature'; + const DELETE_FEATURE = 'deleteFeature'; + const UPDATE_FEATURE = 'updateFeature'; + + const DELETE_ANY_FEATURE = 'deleteAnyFeature'; + const UPDATE_ANY_FEATURE = 'updateAnyFeature'; + + const DOWNLOAD_FEATURE = 'downloadFeature'; /** * User profile @@ -144,15 +155,6 @@ class RestoUser */ private $groups; - /* - * Fallback rights if no collection is found - */ - private $fallbackRights = array( - 'download' => 0, - 'visualize' => 0, - 'create' => 0 - ); - /* * Unregistered profile */ @@ -222,11 +224,20 @@ public function isValidated() } /** - * Do user has rights to : - * - 'download' feature, - * - 'view' feature, - * - 'create' collection, - * - 'update' collection (i.e. add/delete feature and/or delete collection) + * User rights are : + * + * - createCollection : create a collection + * - deleteCollection : delete a collection owned by user + * - updateCollection : update a collection owned by user + * + * - deleteAnyCollection : delete a collection owned by user + * - updateAnyCollection : update a collection owned by user + * + * - createFeature + * - updateFeature + * - deleteFeature + * + * - downloadFeature * * @param string $action * @param array $params @@ -234,8 +245,11 @@ public function isValidated() */ public function hasRightsTo($action, $params = array()) { + + $this->getRights(); + /* switch ($action) { - case RestoUser::DOWNLOAD: + case RestoUser::CREATE_COLLECTION: case RestoUser::VISUALIZE: return $this->hasDownloadOrVisualizeRights($action, $params['collectionId'] ?? null, $params['featureId'] ?? null); case RestoUser::CREATE: @@ -247,7 +261,7 @@ public function hasRightsTo($action, $params = array()) // no break default: break; - } + }*/ return false; } @@ -282,41 +296,19 @@ public function getOrders() /** * Returns rights - * - * @param string $collectionId - * @param string $featureId */ - public function getRights($collectionId = null, $featureId = null) + public function getRights() { + $this->loadProfile(); /* * Compute rights if they are not already set */ - if (!isset($this->rights)) { - $this->rights = (new RightsFunctions($this->context->dbDriver))->getRightsForUser($this, $this->context->dbDriver->targetSchema, null, null); + if ( !isset($this->rights) ) { + $this->rights = (new RightsFunctions($this->context->dbDriver))->getRightsForUser($this); } - /* - * Return specific rights for feature - */ - if (isset($collectionId) && isset($featureId)) { - if (isset($this->rights['features'][$featureId])) { - return $this->rights['features'][$featureId]; - } - return $this->getRights($collectionId); - } - - /* - * Return specific rights for collection - */ - if (isset($collectionId)) { - return $this->rights['collections'][$collectionId] ?? ($this->rights['collections']['*'] ?? $this->fallbackRights); - } - - /* - * Return rights for all collections/features - */ return $this->rights; } @@ -348,38 +340,6 @@ public function getFollowings() )); } - /** - * Set/update user rights - * - * @param array $rights - * @param string $collectionId - * @param string $featureId - * @throws Exception - */ - public function setRights($rights, $collectionId = null, $featureId = null) - { - $this->loadProfile(); - (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights($this->getRightsArray($rights, $collectionId, $featureId)); - $this->rights = (new RightsFunctions($this->context->dbDriver))->getRightsForUser($this, $this->context->dbDriver->targetSchema, null, null); - return true; - } - - /** - * Remove user rights - * - * @param array $rights - * @param string $collectionId - * @param string $featureId - * @throws Exception - */ - public function removeRights($rights, $collectionId = null, $featureId = null) - { - $this->loadProfile(); - (new RightsFunctions($this->context->dbDriver))->removeRights($this->getRightsArray($rights, $collectionId, $featureId)); - $this->rights = (new RightsFunctions($this->context->dbDriver))->getRightsForUser($this, $this->context->dbDriver->targetSchema, null, null); - return true; - } - /** * Return the list of user groups * diff --git a/app/resto/core/api/CollectionsAPI.php b/app/resto/core/api/CollectionsAPI.php index cbbf0589..d9d26e66 100755 --- a/app/resto/core/api/CollectionsAPI.php +++ b/app/resto/core/api/CollectionsAPI.php @@ -366,7 +366,7 @@ public function createCollection($params, $body) /* * Only a user with 'create' rights can POST a collection */ - if (!$this->user->hasRightsTo(RestoUser::CREATE)) { + if (!$this->user->hasRightsTo(RestoUser::CREATE_COLLECTION)) { RestoLogUtil::httpError(403); } @@ -452,7 +452,7 @@ public function updateCollection($params, $body) /* * Only owner of the collection can update it */ - if (! $this->user->hasRightsTo(RestoUser::UPDATE, array('collection' => $collection))) { + if (! $this->user->hasRightsTo(RestoUser::UPDATE_COLLECTION, array('collection' => $collection))) { RestoLogUtil::httpError(403); } @@ -536,7 +536,7 @@ public function deleteCollection($params) /* * Only owner of a collection can delete it */ - if (!$this->user->hasRightsTo(RestoUser::UPDATE, array('collection' => $collection))) { + if (!$this->user->hasRightsTo(RestoUser::DELETE_COLLECTION, array('collection' => $collection))) { RestoLogUtil::httpError(403); } @@ -687,7 +687,7 @@ public function insertFeatures($params, $body) /* * Only a user with 'update' rights on collection can POST feature */ - if (!$this->user->hasRightsTo(RestoUser::UPDATE, array('collection' => $collection))) { + if (!$this->user->hasRightsTo(RestoUser::CREATE_FEATURE, array('collection' => $collection))) { RestoLogUtil::httpError(403); } diff --git a/app/resto/core/addons/Group.php b/app/resto/core/api/GroupAPI.php similarity index 98% rename from app/resto/core/addons/Group.php rename to app/resto/core/api/GroupAPI.php index f0596946..ad8d7da7 100755 --- a/app/resto/core/addons/Group.php +++ b/app/resto/core/api/GroupAPI.php @@ -18,13 +18,11 @@ /** * Groups API */ -class Group extends RestoAddOn +class GroupAPI { - /** - * Add-on version - */ - public $version = '1.0.0'; + private $context; + private $user; /** * @@ -65,7 +63,8 @@ class Group extends RestoAddOn */ public function __construct($context, $user) { - parent::__construct($context, $user); + $this->context = $context; + $this->user = $user; } /** @@ -428,7 +427,7 @@ public function getUserGroups($params) */ private function checkGroupId($groupid) { - if (! ctype_digit($groupid) || intval($groupid) >= Resto::INT_MAX_VALUE) { + if (! ctype_digit($groupid) || intval($groupid) >= RestoConstants::INT_MAX_VALUE) { RestoLogUtil::httpError(400, 'Invalid group identifier'); } } diff --git a/app/resto/core/api/RightsAPI.php b/app/resto/core/api/RightsAPI.php new file mode 100755 index 00000000..6dfb07a8 --- /dev/null +++ b/app/resto/core/api/RightsAPI.php @@ -0,0 +1,357 @@ +context = $context; + $this->user = $user; + } + + /** + * @OA\Get( + * path="/users/{id}/rights", + * summary="Get user rights", + * tags={"Rights"}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="User identifier", + * @OA\Schema( + * type="integer" + * ) + * ), + * @OA\Response( + * response="200", + * description="User rights", + * @OA\JsonContent(ref="#/components/schemas/Rights") + * ), + * @OA\Response( + * response="401", + * description="Unauthorized", + * @OA\JsonContent(ref="#/components/schemas/UnauthorizedError") + * ), + * @OA\Response( + * response="403", + * description="Forbidden", + * @OA\JsonContent(ref="#/components/schemas/ForbiddenError") + * ), + * @OA\Response( + * response="404", + * description="Resource not found", + * @OA\JsonContent(ref="#/components/schemas/NotFoundError") + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + */ + public function getUserRights($params) + { + + /* + * [SECURITY] Only user and admin can see user rights + */ + $isAdmin = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID); + if ( !$isAdmin ) { + RestoUtil::checkUser($this->user, $params['userid']); + } + + return array( + 'rights' => $this->user->getRights() + ); + + } + + /** + * Get group rights + * + * @OA\Get( + * path="/groups/{id}/rights", + * summary="Get group rights", + * tags={"Rights"}, + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="Group identifier", + * @OA\Schema( + * type="integer" + * ) + * ), + * @OA\Response( + * response="200", + * description="User group rights", + * @OA\JsonContent(ref="#/components/schemas/Rights") + * ), + * @OA\Response( + * response="401", + * description="Unauthorized", + * @OA\JsonContent(ref="#/components/schemas/UnauthorizedError") + * ), + * @OA\Response( + * response="403", + * description="Forbidden", + * @OA\JsonContent(ref="#/components/schemas/ForbiddenError") + * ), + * @OA\Response( + * response="404", + * description="Resource not found", + * @OA\JsonContent(ref="#/components/schemas/NotFoundError") + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + */ + public function getGroupRights($params) + { + return array( + 'rights' => (new RightsFunctions($this->context->dbDriver))->getRightsForGroup($params['id']) + ); + } + + /** + * + * Set user rights + * + * @OA\Post( + * path="/users/{id}/rights", + * summary="Set rights for user", + * description="Set rights for a given user", + * tags={"Rights"}, + * @OA\Response( + * response="200", + * description="Rights is created or updated", + * @OA\JsonContent( + * @OA\Property( + * property="status", + * type="string", + * description="Status is *success*" + * ), + * @OA\Property( + * property="message", + * type="string", + * description="Rights updated" + * ), + * @OA\Property( + * property="rights", + * description="Set rights" + * @OA\JsonContent(ref="#/components/schemas/Rights") + * ), + * example={ + * "status": "success", + * "message": "Rights updated", + * "rights": { + * "createCollection": false, + * "deleteCollection": true, + * "updateCollection": true, + * "deleteAnyCollection": false, + * "updateAnyCollection": false, + * "createFeature": false, + * "updateFeature": true, + * "deleteFeature": true, + * "downloadFeature": false + * } + * } + * ) + * ), + * @OA\Response( + * response="400", + * description="Rights is not set", + * @OA\JsonContent(ref="#/components/schemas/BadRequestError") + * ), + * @OA\RequestBody( + * description="Rights to create/udpated", + * required=true, + * @OA\JsonContent( + * required={"rights"}, + * @OA\Property( + * property="rights", + * description="right to create/update" + * @OA\JsonContent(ref="#/components/schemas/Rights") + * ) + * ) + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + * + * @param array $params + * @param array $body + * + */ + public function setUserRights($params, $body) + { + + /* + * [SECURITY] Only admin can set user rights + */ + if ( !$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ) { + return RestoLogUtil::httpError(403); + } + + $rights = $params['rights'] ?? array(); + if ( !empty($rights) ) { + return RestoLogUtil::httpError(400, 'The rights array is not set or empty'); + } + + // Get user just to be sure that it exists ! + new RestoUser(array('id' => $params['id']), $this->context, true); + $rights = (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights('userid', $params['id'], $rights); + + return RestoLogUtil::success('Rights set', array( + 'rights' => $rights + )); + + } + + /** + * + * Set group rights + * + * @OA\Post( + * path="/groups/{id}/rights", + * summary="Set rights for group", + * description="Set rights for a given group", + * tags={"Rights"}, + * @OA\Response( + * response="200", + * description="Rights is created or updated", + * @OA\JsonContent( + * @OA\Property( + * property="status", + * type="string", + * description="Status is *success*" + * ), + * @OA\Property( + * property="message", + * type="string", + * description="Rights updated" + * ), + * @OA\Property( + * property="rights", + * description="Created/update rights" + * @OA\JsonContent(ref="#/components/schemas/Rights") + * ), + * example={ + * "status": "success", + * "message": "Rights updated", + * "rights": { + * "createCollection": false, + * "deleteCollection": true, + * "updateCollection": true, + * "deleteAnyCollection": false, + * "updateAnyCollection": false, + * "createFeature": false, + * "updateFeature": true, + * "deleteFeature": true, + * "deleteAnyFeature": false, + * "updateAnyFeature": false, + * "downloadFeature": false + * } + * } + * ) + * ), + * @OA\Response( + * response="400", + * description="Rights is not set", + * @OA\JsonContent(ref="#/components/schemas/BadRequestError") + * ), + * @OA\RequestBody( + * description="Rights to create/udpated", + * required=true, + * @OA\JsonContent( + * required={"rights"}, + * @OA\Property( + * property="rights", + * description="right to create/update" + * @OA\JsonContent(ref="#/components/schemas/Rights") + * ) + * ) + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + * + * @param array $params + * @param array $body + * + */ + public function setGroupRights($params, $body) + { + /* + * [SECURITY] Only admin can set group rights + */ + if ( !$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ) { + return RestoLogUtil::httpError(403); + } + + $rights = $params['rights'] ?? array(); + if ( !empty($rights) ) { + return RestoLogUtil::httpError(400, 'The rights array is not set or empty'); + } + + // Get group just to be sure that it exists ! + (new GroupsFunctions($this->context->dbDriver))->getGroups(array( + 'id' => $params['id'] + )); + $rights = (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights('groupid', $params['id'], $rights); + + return RestoLogUtil::success('Rights set', array( + 'rights' => $rights + )); + + } + +} diff --git a/app/resto/core/api/UsersAPI.php b/app/resto/core/api/UsersAPI.php index b9ea5426..e76f02d3 100755 --- a/app/resto/core/api/UsersAPI.php +++ b/app/resto/core/api/UsersAPI.php @@ -228,27 +228,6 @@ public function getUserProfile($params) ); } - /** - * Get user rights - * - * [TODO] - Write API - */ - public function getUserRights($params) - { - RestoUtil::checkUser($this->user, $params['userid']); - $result = array( - 'id' => $this->user->profile['id'] - ); - if (isset($params['collectionId'])) { - $result['collection'] = $params['collectionId']; - } - if (isset($params['featureId'])) { - $result['feature'] = $params['featureId']; - } - $result['rights'] = $this->user->getRights($params['collectionId'] ?? null, $params['featureId'] ?? null); - return $result; - } - /** * Create user * diff --git a/app/resto/core/dbfunctions/CollectionsFunctions.php b/app/resto/core/dbfunctions/CollectionsFunctions.php index 15af2f9a..a8c1dc67 100755 --- a/app/resto/core/dbfunctions/CollectionsFunctions.php +++ b/app/resto/core/dbfunctions/CollectionsFunctions.php @@ -196,22 +196,6 @@ public function storeCollection($collection, $rights) * Create new entry in collections osdescriptions tables */ $this->storeCollectionDescription($collection); - - /* - * Store default rights for collection - * - * [TODO] Should get userid from input user ? - */ - (new RightsFunctions($this->dbDriver))->storeOrUpdateRights( - array( - 'right' => $rights, - 'id' => null, - 'groupid' => RestoConstants::GROUP_DEFAULT_ID, - 'collectionId' => $collection->id, - 'featureId' => null, - 'target' => $this->dbDriver->targetSchema - ) - ); /* * Close transaction diff --git a/app/resto/core/dbfunctions/GroupsFunctions.php b/app/resto/core/dbfunctions/GroupsFunctions.php index 3851cb3f..63e0bd9a 100755 --- a/app/resto/core/dbfunctions/GroupsFunctions.php +++ b/app/resto/core/dbfunctions/GroupsFunctions.php @@ -46,22 +46,22 @@ public function getGroups($params = array()) // Return group by id if (isset($params['id'])) { - $where[] = 'id=' . pg_escape_string($params['id']); + $where[] = 'id=' . pg_escape_string($this->dbDriver->getConnection(), $params['id']); } // Return all groups in if (isset($params['in'])) { - $where[] = 'id IN (' . pg_escape_string(join(',', $params['in'])) . ')'; + $where[] = 'id IN (' . pg_escape_string($this->dbDriver->getConnection(), join(',', $params['in'])) . ')'; } // Search by name if (isset($params['q'])) { - $where[] = 'public.normalize(name) LIKE public.normalize(\'%' . pg_escape_string($params['q']) . '%\')'; + $where[] = 'public.normalize(name) LIKE public.normalize(\'%' . pg_escape_string($this->dbDriver->getConnection(), $params['q']) . '%\')'; } // Return groups by userid if (isset($params['userid'])) { - $where[] = 'id IN (SELECT DISTINCT groupid FROM ' . $this->dbDriver->commonSchema . '.group_member WHERE userid=' . pg_escape_string($params['userid']) . ')'; + $where[] = 'id IN (SELECT DISTINCT groupid FROM ' . $this->dbDriver->commonSchema . '.group_member WHERE userid=' . pg_escape_string($this->dbDriver->getConnection(), $params['userid']) . ')'; } return $this->formatGroups($this->dbDriver->fetch($this->dbDriver->query('SELECT id, name, description, owner, to_iso8601(created) as created FROM ' . $this->dbDriver->commonSchema . '.group' . (count($where) > 0 ? ' WHERE ' . join(' AND ', $where) : '') . ' ORDER BY id DESC')), $params['id'] ?? null); diff --git a/app/resto/core/dbfunctions/RightsFunctions.php b/app/resto/core/dbfunctions/RightsFunctions.php index bf809cb8..31814b59 100755 --- a/app/resto/core/dbfunctions/RightsFunctions.php +++ b/app/resto/core/dbfunctions/RightsFunctions.php @@ -35,359 +35,153 @@ public function __construct($dbDriver) } /** - * Return rights for groups - * - * @param array $groups - * @param string $target - * @param string $collectionId - * @param string $featureId - * @param string $target - * - * @return array - * @throws exception - */ - public function getRightsForGroups($groups, $target, $collectionId, $featureId, $merge = true) - { - $filter = $this->getFilterFromTarget($target, $collectionId, $featureId); - $query = 'SELECT groupid, target, collection, featureid, download, visualize, createcollection as create FROM ' . $this->dbDriver->commonSchema . '.right WHERE groupid IN (' . join(',', $groups) . ')' . (isset($filter) ? ' AND ' . $filter : ''); - return $this->getRightsFromQuery($query, $merge); - } - - /** - * Return rights for user + * Return rights for user i.e. aggregation of user rights including its group rights * * @param RestoUser $user - * @param string $target - * @param string $collectionId - * @param string $featureId + * @param boolean $noGroups * * @return array * @throws exception */ - public function getRightsForUser($user, $target, $collectionId, $featureId) + public function getRightsForUser($user, $noGroups = false) { + $userRights = array(); /* * Retrieve rights for user */ - if (isset($user->profile['id'])) { - $filter = $this->getFilterFromTarget($target, $collectionId, $featureId); - $query = 'SELECT userid, target, collection, featureid, download, visualize, createcollection as create FROM ' . $this->dbDriver->commonSchema . '.right WHERE userid=' . pg_escape_string($this->dbDriver->getConnection(), $user->profile['id']) . (isset($filter) ? ' AND ' . $filter : ''); - $userRights = $this->getRightsFromQuery($query, false); + if ( isset($user->profile['id']) ) { + $results = $this->dbDriver->fetch($this->dbDriver->pQuery('SELECT rights FROM ' . $this->dbDriver->commonSchema . '.right WHERE userid=$1', array( + $user->profile['id'] + ))); + if ( isset($results) && count($results) === 1 ) { + $userRights = json_decode($results[0]['rights'], true); + } } - - /* - * Retrieve rights for user groups - */ - $groupsRights = $this->getRightsForGroups($user->getGroupIds(), $target, $collectionId, $featureId, false); - + /* - * Merge rights from user and from user's groups + * Merge rights from user and from user's groups unless specified */ - return $this->mergeRights(array_merge($userRights, $groupsRights)); + return $noGroups ? $this->$userRights : $this->mergeRights(array_merge(array($userRights), $this->getRightsForGroups($user->getGroupIds()))); } /** - * Store or update rights to database + * Return rights for a given group * - * array( - * 'visualize' => // 0 or 1 - * 'download' => // 0 or 1 - * 'create' => // 0 or 1 - * ) + * @param string $groupId * - * @param array params - * - * @throws Exception + * @return array + * @throws exception */ - public function storeOrUpdateRights($params) + public function getRightsForGroup($groupId) { - $rights = $params['rights'] ?? array(); - $userid = $params['id'] ?? null; - $groupid = $params['groupid'] ?? null; - $collectionId = $params['collectionId'] ?? null; - $featureId = $params['featureId'] ?? null; - $target = $params['target'] ?? null; - - /* - * Store or update - */ - if ($this->rightExists($userid, $groupid, $target, $collectionId, $featureId)) { - return $this->updateRights($rights, $userid, $groupid, $target, $collectionId, $featureId); - } - - return $this->storeRights($rights, $userid, $groupid, $target, $collectionId, $featureId); + $results = $this->dbDriver->fetch($this->dbDriver->pQuery('SELECT rights FROM ' . $this->dbDriver->commonSchema . '.right WHERE groupid=$1', array( + $groupId + ))); + return isset($results) && count($results) == 1 ? json_decode($results[0]['rights'], true) : array(); } /** - * Delete rights from database + * Store or update rights to database * - * @param array params + * @param string $targetCol + * @param string $targetValue + * @param array rights * * @throws Exception */ - public function removeRights($params) + public function storeOrUpdateRights($targetCol, $targetValue, $rights) { + + $query = join(' ', array( + 'INSERT INTO ' . $this->dbDriver->commonSchema . '.right (' . $targetCol . ', rights)', + 'VALUES ($1, $2)', + 'ON CONFLICT (' . $targetCol . ')', + 'DO UPDATE SET rights = (SELECT json_object_agg(key, CASE WHEN json_typeof(existing) = \'object\' THEN json_set(existing, key, value::json) ELSE json_set(\'{"\' || key || \'":\' || value || \'}\'::json, key, value::json) END) AS merged_rights', + 'FROM json_each_text(rights) AS rights_entries', + 'JOIN json_each_text(EXCLUDED.rights) AS new_entries ON rights_entries.key = new_entries.key', + 'RETURNING rights' + )); + try { - $filterOwner = $this->getFilterFromOwner($params['id'] ?? null, $params['groupid'] ?? null); - if (!isset($filterOwner)) { - throw new Exception(); - } - $filterTarget = $this->getFilterFromTarget($params['target'] ?? null, $params['collectionId'] ?? null, $params['featureId'] ?? null); - $result = pg_query($this->dbDriver->getConnection(), 'DELETE from ' . $this->dbDriver->commonSchema . '.right WHERE ' . (isset($filterTarget) ? ' AND ' . $filterTarget : '')); - if (!$result) { + $result = pg_fetch_assoc(pg_query_params($this->dbDriver->getConnection(), $query, array( + $targetValue, + $rights + ))); + if ( !$result ) { throw new Exception(); } } catch (Exception $e) { - RestoLogUtil::httpError(400); + RestoLogUtil::httpError(500, 'Cannot set rights'); } + + return $result[0]['rights']; + } /** - * Return rights for user/group classified by collections/features - * (if merge is set to true) + * Return rights for groups * - * @param string $query - * @param boolean $merge + * @param array $groupIds * * @return array * @throws exception */ - private function getRightsFromQuery($query, $merge) - { - $results = $this->dbDriver->fetch($this->dbDriver->query($query)); - return $merge ? $this->mergeRights($results) : $results; - } - - /** - * Merge right - * - * @param array $newRight - * @param array $existingRight - */ - private function mergeRight($newRight, $existingRight) + private function getRightsForGroups($groupIds) { - if (isset($existingRight)) { - return array( - 'download' => max((integer) $newRight['download'], $existingRight['download']), - 'visualize' => max((integer) $newRight['visualize'], $existingRight['visualize']), - 'create' => max((integer) $newRight['create'], $existingRight['create']) - ); + $groupRights = array(); + $results = $this->dbDriver->fetch($this->dbDriver->query('SELECT rights FROM ' . $this->dbDriver->commonSchema . '.right WHERE groupid IN (' . pg_escape_string($this->dbDriver->getConnection(), join(',', $groupIds)) . ')')); + if ( isset($results) && count($results) >= 1 ) { + for ($i = count($results); $i--;) { + $groupRights[] = json_decode($results[$i]['rights'], true); + } } - return array( - 'download' => (integer) $newRight['download'], - 'visualize' => (integer) $newRight['visualize'], - 'create' => (integer) $newRight['create'] - ); + return $groupRights; } /** - * Merge simple rights array to an associative collections/features array + * Merge an array of rights to an aggregate right * + * Aggregation means that at least one true gives true. False in all other cases + * * @param array $rights */ private function mergeRights($rights) { + + // Default rights allows only to delete/update things + // that belongs to user $merged = array( - 'collections' => array(), - 'features' => array() + 'createCollection' => false, + 'deleteCollection' => true, + 'updateCollection' => true, + 'deleteAnyCollection' => false, + 'updateAnyCollection' => false, + 'createFeature' => false, + 'updateFeature' => true, + 'deleteFeature' => true, + 'deleteAnyFeature' => false, + 'updateAnyFeature' => false, + 'downloadFeature' => false ); + // [IMPORTANT] Assume only boolean otherwise it will be converted to anyway for ($i = count($rights); $i--;) { - if (isset($rights[$i]['collection'])) { - $merged['collections'][$rights[$i]['collection']] = $this->mergeRight($rights[$i], $merged['collections'][$rights[$i]['collection']] ?? null); - } else { - $merged['features'][$rights[$i]['featureid']] = $this->mergeRight($rights[$i], $merged['features'][$rights[$i]['featureid']] ?? null); - } - } + foreach ($rights[$i] as $key => $value) { - /* - * Update collections with '*' rights - */ - if (isset($merged['collections']['*'])) { - foreach (array_keys($merged['collections']) as $collectionId) { - if ($collectionId !== '*') { - $merged['collections'][$collectionId] = $this->mergeRight($merged['collections'][$collectionId], $merged['collections']['*']); + if (array_key_exists($key, $merged)) { + if ( $value ) { + $merged[$key] = true; + } + } + else { + $merged[$key] = $value ?? false; } } } - return $merged; - } - - /** - * Store rights to database - * - * @param array $rights - * @param string $userid - * @param string $groupid - * @param string $target - * @param string $collectionId - * @param string $featureId - * - * @throws Exception - */ - private function storeRights($rights, $userid, $groupid, $target, $collectionId, $featureId) - { - if (!isset($userid) && !isset($groupid)) { - RestoLogUtil::httpError(400, 'Missing owner'); - } - if (!isset($collectionId) && !isset($featureId)) { - RestoLogUtil::httpError(400, 'Missing collectionId or featureId'); - } - - $values = array( - 'visualize' => isset($rights['visualize']) ? $this->integerOrZero($rights['visualize']) : 0, - 'download' => isset($rights['download']) ? $this->integerOrZero($rights['download']) : 0, - 'createcollection' => isset($rights['create']) ? $this->integerOrZero($rights['create']) : 0, - 'userid' => isset($userid) ? pg_escape_string($this->dbDriver->getConnection(), $userid) : 'NULL', - 'groupid' => isset($groupid) ? pg_escape_string($this->dbDriver->getConnection(), $groupid) : 'NULL', - 'collection' => isset($collectionId) ? '\'' . pg_escape_string($this->dbDriver->getConnection(), $collectionId) . '\'' : 'NULL', - 'featureId' => isset($featureId) ? '\'' . pg_escape_string($this->dbDriver->getConnection(), $featureId) . '\'' : 'NULL', - 'target' => isset($target) ? '\'' . pg_escape_string($this->dbDriver->getConnection(), $target) . '\'' : 'NULL' - ); - - try { - $result = pg_query($this->dbDriver->getConnection(), 'INSERT INTO ' . $this->dbDriver->commonSchema . '.right (' . join(',', array_keys($values)) . ') VALUES (' . join(',', array_values($values)) . ')'); - if (!$result) { - throw new Exception(); - } - } catch (Exception $e) { - RestoLogUtil::httpError(500, 'Cannot store right'); - } - } - - /** - * Update rights to database - * - * @param string $userid - * @param string $groupid - * @param string $target - * @param string $collectionId - * @param string $featureId - * - * @throws Exception - */ - private function updateRights($rights, $userid, $groupid, $target, $collectionId, $featureId) - { - if (!isset($userid) && !isset($groupid)) { - RestoLogUtil::httpError(400, 'Missing owner'); - } - if (!isset($collectionId) && !isset($featureId)) { - RestoLogUtil::httpError(400, 'Missing target'); - } - - $where = array(); - - if (isset($target)) { - $where[] = 'target IN (\'' . pg_escape_string($this->dbDriver->getConnection(), $this->dbDriver->targetSchema) . '\', \'*\')'; - } - - // Owner - $where[] = isset($userid) ? 'userid=' . pg_escape_string($this->dbDriver->getConnection(), $userid) : 'groupid=' . pg_escape_string($this->dbDriver->getConnection(), $groupid); - - // Target - $where[] = isset($collectionId) ? 'collection=\'' . pg_escape_string($this->dbDriver->getConnection(), $collectionId) . '\'' : 'featureid=\'' . pg_escape_string($this->dbDriver->getConnection(), $featureId) . '\''; - - $toBeSet = array(); - foreach (array_values(array('visualize', 'download', 'create')) as $right) { - if (isset($rights[$right])) { - $toBeSet[] = ($right === 'create' ? 'createcollection' : $right) . "=" . $this->integerOrZero($rights[$right]); - } - } + print_r($merged); - if (count($toBeSet) > 0) { - $this->dbDriver->query('UPDATE ' . $this->dbDriver->commonSchema . '.right SET ' . join(',', $toBeSet) . ' WHERE ' . join(' AND ', $where)); - } - return true; } - /** - * Return $value or NULL - * @param string $value - */ - private function integerOrZero($value) - { - return isset($value) && is_int($value) && ($value === 0 || $value === 1) ? $value : 0; - } - - /** - * Check if right exists in database - * - * @param string $userid - * @param string $groupid - * @param string $target - * @param string $collectionId - * @param string $featureId - */ - private function rightExists($userid, $groupid, $target, $collectionId, $featureId) - { - if (!isset($userid) && !isset($groupid)) { - return false; - } - if (!isset($collectionId) && !isset($featureId)) { - return false; - } - - $where = array(); - - if (isset($target)) { - $where[] = 'target IN (\'' . pg_escape_string($this->dbDriver->getConnection(), $target) . '\', \'*\')'; - } - - - // Owner - $where[] = isset($userid) ? 'userid=' . pg_escape_string($this->dbDriver->getConnection(), $userid) : 'groupid=' . pg_escape_string($this->dbDriver->getConnection(), $groupid); - - // Target - $where[] = isset($collectionId) ? 'collection=\'' . pg_escape_string($this->dbDriver->getConnection(), $collectionId) . '\'' : 'featureid=\'' . pg_escape_string($this->dbDriver->getConnection(), $featureId) . '\''; - - $query = 'SELECT 1 from ' . $this->dbDriver->commonSchema . '.right WHERE ' . join(' AND ', $where); - $results = $this->dbDriver->fetch($this->dbDriver->query($query)); - return !empty($results); - } - - /** - * Return filter WHERE from target - * - * @param string $target - * @param string $collectionId - * @param string $featureId - */ - private function getFilterFromTarget($target, $collectionId, $featureId) - { - if (!isset($collectionId) && !isset($featureId)) { - return null; - } - - $where = array(); - if (isset($target)) { - $where[] = 'target IN (\'' . pg_escape_string($this->dbDriver->targetSchema) . '\', \'*\')'; - } - if (isset($collectionId)) { - $where[] = 'collection IN (\'' . pg_escape_string($this->dbDriver->getConnection(), $collectionId) . '\', \'*\')'; - } elseif (isset($featureId)) { - $where[] = 'featureid IN (\'' . pg_escape_string($this->dbDriver->getConnection(), $featureId) . '\', \'*\')'; - } - return join(' AND ', $where); - } - - /** - * Return filter WHERE from owner - * - * @param string $userid - * @param string $groupid - */ - private function getFilterFromOwner($userid, $groupid) - { - if (!isset($userid) && !isset($groupid)) { - return null; - } - if (isset($userid)) { - return 'userid='. pg_escape_string($this->dbDriver->getConnection(), $userid); - } elseif (isset($groupid)) { - return 'groupid=' . pg_escape_string($this->dbDriver->getConnection(), $groupid); - } - return null; - } } diff --git a/examples/collections/L8.json b/examples/collections/L8.json index 13584f60..77176163 100644 --- a/examples/collections/L8.json +++ b/examples/collections/L8.json @@ -5,11 +5,6 @@ ], "version": "1.0", "model": "OpticalModel", - "rights": { - "download": 1, - "visualize": 1 - }, - "visibility": 1, "license": "PDDL-1.0", "osDescription": { "en": { diff --git a/examples/collections/S2.json b/examples/collections/S2.json index dab6049b..04758767 100644 --- a/examples/collections/S2.json +++ b/examples/collections/S2.json @@ -2,11 +2,6 @@ "id": "S2", "version": "1.0", "model": "OpticalModel", - "rights": { - "download": 1, - "visualize": 1 - }, - "visibility": 1, "license": "proprietary", "osDescription": { "en": { diff --git a/resto-database-model/02_resto_common_model.sql b/resto-database-model/02_resto_common_model.sql index 8b1fb800..ace2f333 100644 --- a/resto-database-model/02_resto_common_model.sql +++ b/resto-database-model/02_resto_common_model.sql @@ -173,23 +173,8 @@ CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.right ( -- Reference to __DATABASE_COMMON_SCHEMA__.group.groupid groupid BIGINT, - -- Reference the schema name the collection belongs to - target TEXT, - - -- Reference __DATABASE_TARGET_SCHEMA__.collection.id - collection TEXT, - - -- Reference __DATABASE_TARGET_SCHEMA__.feature.id - featureid UUID, - - -- Has right to download = 1. Otherwise 0 - download INTEGER DEFAULT 0, - - -- Has right to visualize = 1. Otherwise 0 - visualize INTEGER DEFAULT 0, - - -- Has right to create a collection = 1. Otherwise 0 - createcollection INTEGER DEFAULT 0 + -- rights + rights JSON ); @@ -266,19 +251,15 @@ CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.log ( -- ------------------------- INSERT ---------------------------- --- [TABLE __DATABASE_COMMON_SCHEMA__.right] admin rights -INSERT INTO __DATABASE_COMMON_SCHEMA__.right - (groupid, target, collection, download, visualize, createcollection) -SELECT 0,'*','*',1,1,1 -WHERE - NOT EXISTS ( - SELECT groupid, target, collection FROM __DATABASE_COMMON_SCHEMA__.right WHERE groupid = 0 AND target = '*' AND collection = '*' - ); - -- [TABLE __DATABASE_COMMON_SCHEMA__.group] default groups INSERT INTO __DATABASE_COMMON_SCHEMA__.group (id, name, description, created) VALUES (0, 'admin', 'Special group for admin', now()) ON CONFLICT (id) DO NOTHING; INSERT INTO __DATABASE_COMMON_SCHEMA__.group (id, name, description, created) VALUES (100, 'default', 'Default group', now()) ON CONFLICT (id) DO NOTHING; +-- [TABLE __DATABASE_COMMON_SCHEMA__.right] admin rights +INSERT INTO __DATABASE_COMMON_SCHEMA__.right (groupid, rights) +VALUES (0, '{"createCollection":true, "deleteAnyCollection": true, "updateAnyCollection": true, "createFeature": true, "deleteAnyFeature": true, "updateAnyFeature": true, "downloadFeature": true}') +ON CONFLICT(groupid) DO NOTHING; + -- ------------------------- INDEXES ---------------------------- -- [TABLE __DATABASE_COMMON_SCHEMA__.user] @@ -290,8 +271,8 @@ CREATE INDEX IF NOT EXISTS idx_userid_follower ON __DATABASE_COMMON_SCHEMA__.fol CREATE INDEX IF NOT EXISTS idx_followerid_follower ON __DATABASE_COMMON_SCHEMA__.follower (followerid); -- [TABLE __DATABASE_COMMON_SCHEMA__.right] -CREATE INDEX IF NOT EXISTS idx_userid_right ON __DATABASE_COMMON_SCHEMA__.right (userid); -CREATE INDEX IF NOT EXISTS idx_groupid_right ON __DATABASE_COMMON_SCHEMA__.right (groupid); +CREATE UNIQUE INDEX IF NOT EXISTS idx_userid_right ON __DATABASE_COMMON_SCHEMA__.right (userid); +CREATE UNIQUE INDEX IF NOT EXISTS idx_groupid_right ON __DATABASE_COMMON_SCHEMA__.right (groupid); -- [TABLE __DATABASE_COMMON_SCHEMA__.group] CREATE UNIQUE INDEX IF NOT EXISTS idx_uname_group ON __DATABASE_COMMON_SCHEMA__.group (public.normalize(name)); diff --git a/resto-database-model/03_resto_target_model.sql b/resto-database-model/03_resto_target_model.sql index ffd63b91..2b44591b 100644 --- a/resto-database-model/03_resto_target_model.sql +++ b/resto-database-model/03_resto_target_model.sql @@ -214,8 +214,8 @@ CREATE TABLE IF NOT EXISTS __DATABASE_TARGET_SCHEMA__.feature ( -- List of hashtags (without prefix # !) -- -- Two kind of hashtags: - -- * hashtags without {Resto::TAG_SEPARATOR} hashtags *provided* by user from description - -- * hashtags with {Resto::TAG_SEPARATOR} hashtags *computed* by Tag add-on (depend on collection) + -- * hashtags without {RestoConstants::TAG_SEPARATOR} hashtags *provided* by user from description + -- * hashtags with {RestoConstants::TAG_SEPARATOR} hashtags *computed* by Tag add-on (depend on collection) hashtags TEXT[], -- Original geometry as provided during feature insertion diff --git a/resto-database-model/migrations/00_migration.sql b/resto-database-model/migrations/00_migration.sql index d178e97b..35575e89 100644 --- a/resto-database-model/migrations/00_migration.sql +++ b/resto-database-model/migrations/00_migration.sql @@ -7,4 +7,15 @@ ALTER TABLE __DATABASE_COMMON_SCHEMA__.right ADD COLUMN IF NOT EXISTS target TEX -- Add resto-addon-groups to resto core -- https://github.com/jjrom/resto/commit/fbf372d856686892b26a3bb573ad0ea2e66a156d -- -ALTER TABLE __DATABASE_COMMON_SCHEMA__.user DROP COLUMN IF EXISTS groups; \ No newline at end of file +ALTER TABLE __DATABASE_COMMON_SCHEMA__.user DROP COLUMN IF EXISTS groups; + +-- +-- Complete rewrite rights for EDITO-Infra +-- +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS target; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS collection; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS featureid; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS download; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS visualize; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS createcollection; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right ADD COLUMN IF NOT EXISTS rights JSON; From fd09b40709861ae4e30cbb78e2a5e07c0fe377e0 Mon Sep 17 00:00:00 2001 From: jjrom Date: Sun, 7 Apr 2024 20:01:50 +0200 Subject: [PATCH 8/9] Missing hasRightsTo implementation to finish rights management --- app/resto/core/RestoUser.php | 46 ++++++++----------- app/resto/core/api/CollectionsAPI.php | 16 ++----- app/resto/core/api/RightsAPI.php | 11 +++-- .../core/dbfunctions/RightsFunctions.php | 6 ++- .../02_resto_common_model.sql | 2 +- 5 files changed, 36 insertions(+), 45 deletions(-) diff --git a/app/resto/core/RestoUser.php b/app/resto/core/RestoUser.php index afc185e2..17a7b851 100755 --- a/app/resto/core/RestoUser.php +++ b/app/resto/core/RestoUser.php @@ -24,6 +24,7 @@ */ class RestoUser { + const CREATE_COLLECTION = 'createCollection'; const DELETE_COLLECTION = 'deleteCollection'; const UPDATE_COLLECTION = 'updateCollection'; @@ -35,6 +36,7 @@ class RestoUser const DELETE_FEATURE = 'deleteFeature'; const UPDATE_FEATURE = 'updateFeature'; + const CREATE_ANY_FEATURE = 'createAnyFeature'; const DELETE_ANY_FEATURE = 'deleteAnyFeature'; const UPDATE_ANY_FEATURE = 'updateAnyFeature'; @@ -226,18 +228,22 @@ public function isValidated() /** * User rights are : * - * - createCollection : create a collection - * - deleteCollection : delete a collection owned by user - * - updateCollection : update a collection owned by user + * - createCollection : create a collection + * - deleteCollection : delete a collection owned by user + * - updateCollection : update a collection owned by user * - * - deleteAnyCollection : delete a collection owned by user - * - updateAnyCollection : update a collection owned by user + * - deleteAnyCollection : delete any collection i.e. including not owned by user + * - updateAnyCollection : update any collection i.e. including not owned by user * - * - createFeature - * - updateFeature - * - deleteFeature + * - createFeature : create a feature in a collection owned by user + * - deleteFeature : delete a feature owned by user + * - updateFeature : update a feature owned by user * - * - downloadFeature + * - createAnyFeature : create a feature in any collection + * - deleteAnyFeature : delete any feature i.e. including not owned by user + * - updateAnyFeature : update any feature i.e. including not owned by user + * + * - downloadFeature : download a feature [NOT USED] * * @param string $action * @param array $params @@ -246,23 +252,11 @@ public function isValidated() public function hasRightsTo($action, $params = array()) { - $this->getRights(); - /* - switch ($action) { - case RestoUser::CREATE_COLLECTION: - case RestoUser::VISUALIZE: - return $this->hasDownloadOrVisualizeRights($action, $params['collectionId'] ?? null, $params['featureId'] ?? null); - case RestoUser::CREATE: - return $this->hasCreateRights(); - case RestoUser::UPDATE: - if (isset($params['collection'])) { - return $this->hasUpdateRights($params['collection']); - } - // no break - default: - break; - }*/ - return false; + $rights = $this->getRights(); + + switch ($action) {} + + } /** diff --git a/app/resto/core/api/CollectionsAPI.php b/app/resto/core/api/CollectionsAPI.php index d9d26e66..725ebb93 100755 --- a/app/resto/core/api/CollectionsAPI.php +++ b/app/resto/core/api/CollectionsAPI.php @@ -363,9 +363,7 @@ public function getCollection($params) */ public function createCollection($params, $body) { - /* - * Only a user with 'create' rights can POST a collection - */ + if (!$this->user->hasRightsTo(RestoUser::CREATE_COLLECTION)) { RestoLogUtil::httpError(403); } @@ -446,12 +444,8 @@ public function createCollection($params, $body) */ public function updateCollection($params, $body) { - $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user); - $collection->load(); + $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); - /* - * Only owner of the collection can update it - */ if (! $this->user->hasRightsTo(RestoUser::UPDATE_COLLECTION, array('collection' => $collection))) { RestoLogUtil::httpError(403); } @@ -533,9 +527,6 @@ public function deleteCollection($params) { $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); - /* - * Only owner of a collection can delete it - */ if (!$this->user->hasRightsTo(RestoUser::DELETE_COLLECTION, array('collection' => $collection))) { RestoLogUtil::httpError(403); } @@ -681,8 +672,7 @@ public function insertFeatures($params, $body) /* * Load collection */ - $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user); - $collection->load(); + $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); /* * Only a user with 'update' rights on collection can POST feature diff --git a/app/resto/core/api/RightsAPI.php b/app/resto/core/api/RightsAPI.php index 6dfb07a8..652c28fc 100755 --- a/app/resto/core/api/RightsAPI.php +++ b/app/resto/core/api/RightsAPI.php @@ -36,9 +36,10 @@ class RightsAPI * "updateCollection": true, * "deleteAnyCollection": false, * "updateAnyCollection": false, - * "createFeature": false, + * "createFeature": true, * "updateFeature": true, * "deleteFeature": true, + * "createAnyFeature": false, * "deleteAnyFeature": false, * "updateAnyFeature": false, * "downloadFeature": false @@ -196,9 +197,12 @@ public function getGroupRights($params) * "updateCollection": true, * "deleteAnyCollection": false, * "updateAnyCollection": false, - * "createFeature": false, + * "createFeature": true, * "updateFeature": true, * "deleteFeature": true, + * "createAnyFeature": false, + * "deleteAnyFeature": false, + * "updateAnyFeature": false, * "downloadFeature": false * } * } @@ -292,9 +296,10 @@ public function setUserRights($params, $body) * "updateCollection": true, * "deleteAnyCollection": false, * "updateAnyCollection": false, - * "createFeature": false, + * "createFeature": true, * "updateFeature": true, * "deleteFeature": true, + * "createAnyFeature": false, * "deleteAnyFeature": false, * "updateAnyFeature": false, * "downloadFeature": false diff --git a/app/resto/core/dbfunctions/RightsFunctions.php b/app/resto/core/dbfunctions/RightsFunctions.php index 31814b59..e02d9dbc 100755 --- a/app/resto/core/dbfunctions/RightsFunctions.php +++ b/app/resto/core/dbfunctions/RightsFunctions.php @@ -158,9 +158,10 @@ private function mergeRights($rights) 'updateCollection' => true, 'deleteAnyCollection' => false, 'updateAnyCollection' => false, - 'createFeature' => false, + 'createFeature' => true, 'updateFeature' => true, 'deleteFeature' => true, + 'createAnyFeature' => false, 'deleteAnyFeature' => false, 'updateAnyFeature' => false, 'downloadFeature' => false @@ -180,7 +181,8 @@ private function mergeRights($rights) } } } - print_r($merged); + + return $merged; } diff --git a/resto-database-model/02_resto_common_model.sql b/resto-database-model/02_resto_common_model.sql index ace2f333..f57a13c5 100644 --- a/resto-database-model/02_resto_common_model.sql +++ b/resto-database-model/02_resto_common_model.sql @@ -257,7 +257,7 @@ INSERT INTO __DATABASE_COMMON_SCHEMA__.group (id, name, description, created) VA -- [TABLE __DATABASE_COMMON_SCHEMA__.right] admin rights INSERT INTO __DATABASE_COMMON_SCHEMA__.right (groupid, rights) -VALUES (0, '{"createCollection":true, "deleteAnyCollection": true, "updateAnyCollection": true, "createFeature": true, "deleteAnyFeature": true, "updateAnyFeature": true, "downloadFeature": true}') +VALUES (0, '{"createCollection":true, "deleteAnyCollection": true, "updateAnyCollection": true, "createAnyFeature": true, "deleteAnyFeature": true, "updateAnyFeature": true, "downloadFeature": true}') ON CONFLICT(groupid) DO NOTHING; -- ------------------------- INDEXES ---------------------------- From c220f8de5677ace029963c50659af77330611b54 Mon Sep 17 00:00:00 2001 From: jjrom Date: Mon, 8 Apr 2024 13:49:43 +0200 Subject: [PATCH 9/9] [MAJOR] Add groups and rights API --- README.md | 20 +- USERS.md | 176 ++ app/resto/core/RestoConstants.php | 2 +- app/resto/core/RestoRouter.php | 2 + app/resto/core/RestoUser.php | 131 +- app/resto/core/api/CollectionsAPI.php | 3 - app/resto/core/api/FeaturesAPI.php | 29 +- app/resto/core/api/GroupAPI.php | 102 ++ app/resto/core/api/RightsAPI.php | 57 +- app/resto/core/api/UsersAPI.php | 6 +- .../core/dbfunctions/CollectionsFunctions.php | 5 - .../core/dbfunctions/GroupsFunctions.php | 65 + .../core/dbfunctions/RightsFunctions.php | 11 +- docs/resto-api.html | 1539 ++++++++++++++++- docs/resto-api.json | 1167 +++++++++++-- examples/users/dummyGroup.json | 4 + examples/users/dummyGroup_addJohnDoe.json | 3 + examples/users/dummyGroup_rights.json | 3 + examples/users/johnDoe.json | 6 + examples/users/johnDoe_rights.json | 3 + .../02_resto_common_model.sql | 2 +- .../migrations/00_migration.sql | 2 +- 22 files changed, 3003 insertions(+), 335 deletions(-) create mode 100644 USERS.md create mode 100644 examples/users/dummyGroup.json create mode 100644 examples/users/dummyGroup_addJohnDoe.json create mode 100644 examples/users/dummyGroup_rights.json create mode 100644 examples/users/johnDoe.json create mode 100644 examples/users/johnDoe_rights.json diff --git a/README.md b/README.md index 827bab3e..a630e175 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,11 @@ Then get the collections list : curl "http://localhost:5252/collections" +### Collection aliases +A collection can contain an array of *aliases* (see [./examples/collections/L8.json](./examples/collections/L8.json#L3-L5) for instance). These aliases are alternate names to the collection id. Thus {collectionId} value in /collections/{collectionId}/* endpoints can use the original collection id or one of its aliases. + +Note that id and aliases must be unique in the database. As a consequence, you cannot create a new collection or set an alias to an existing collection that as the same value of one of the aliases of an existing collection. + ## Ingest a feature To ingest a feature using the default **ADMIN_USER_NAME** and **ADMIN_USER_PASSWORD** (see [config.env](config.env)) : @@ -60,7 +65,8 @@ Then get the feature : curl "http://localhost:5252/collections/S2/items/S2A_MSIL1C_20190611T160901_N0207_R140_T23XMD_20190611T193040" -## Rights and groups +## Users, groups and rights +See [./USERS.md](./USERS.md) # TL;DR The [INSTALLATION.md](INSTALLATION.md) file provides additional information on the installation process. @@ -70,7 +76,7 @@ The [INSTALLATION.md](INSTALLATION.md) file provides additional information on t Here are some projects that use resto. * [SnapPlanet](https://snapplanet.io) -* [Copernicus Data Space Ecosystem](https://dataspace.copernicus.eu/) +* [European Digital Twin of the Ocean](https://www.edito.eu) * [CREODIAS](https://creodias.eu/eo-data-finder-api-manual) * [Rocket - The Earth in your pocket](https://rocket.snapplanet.io) * [The French Sentinel Data Processing center](https://peps.cnes.fr/rocket/#/home) @@ -88,12 +94,6 @@ Here are some projects that use resto. If you plan to use resto and would like to have your project added to this list, feel free to contact [support](#support) # Support -resto is developped and maintained by [jeobrowser](https://mapshup.com). - -For questions, support or anything related to resto feel free to contact +resto is developped and maintained by [jeobrowser](https://snapplanet.io). - jeobrowser - 50 quai de Tounis - 31000 Toulouse - Tel : +33 6 19 59 17 35 - email : jerome.gasperi@gmail.com +For questions, support or anything related to resto feel free to contact *jerome[dot]gasperi[at]gmail[dot]com* diff --git a/USERS.md b/USERS.md new file mode 100644 index 00000000..d2e3e920 --- /dev/null +++ b/USERS.md @@ -0,0 +1,176 @@ +# Users, rights and groups +resto provide a user authentication and authorization mechanism allowing to manage access to ressources in particular to authorize CRUD operations (Create, Read, Update, Delete) on collections and features. + +## Users +On the first launch of resto, one user (admin) is created with a user *{userId}* equals to **100**. This user is automatically added to the **admin group** (see chapter on groups below). + +### Add a new user +The following example shows how to add a new user. When adding a new user, it will be automatically associated with default rights (see chapter on rights below). + + # Add a new user + curl -X POST -d@examples/users/johnDoe.json "http://localhost:5252/users" + +The above command should returns an HTTP 200 response including the newly create user profile + + { + "status":"success", + "message":"User johndoe@localhost created and activated", + "profile":{ + "id":"224468756040777732", + "email":"johndoe@localhost", + "name":"John Doe", + "firstname":"John", + "lastname":"Doe", + "lang":"en", + "topics":null, + "picture":"https://robohash.org/a6b506f3dae99e4c35ae50ae240e8f5d?gravatar=hashed&bgset=any&size=400x400", + "registrationdate":"2024-04-08 07:18:19.77169", + "activated":1, + "followers":0, + "followings":0 + } + } + +Notes : + +* An unique *id* is created for the user +* The *activated* value set to 1. This means that the user is created and validated i.e. you can use authenticate with this user within resto. If you want to check for email address before allowing user to authenticate to resto, you have to set the **USER_AUTOVALIDATION** environment value to *false* in [config.env](./config.env). In this case, the user will receive an email including a validation link. The *activated* value will be set to 1 upon user's validation link resolution. + +### Get an authorization token (optional) +To authenticate to resto endpoint, you can either provide the email/password of an existing user or an authentication token. + +You can generate a bearer authentication token valid for 100 days for the above user with the following command: + + ./scripts/generateAuthToken -i 224468756040777732 -d 100 + +The result should be: + + {"userId":"224468756040777732","duration":100,"valid_until":"2024-07-17T09:41:49","token":"eyJzdWIiOiIyMjQ0Njg3NTYwNDA3Nzc3MzIiLCJpYXQiOjE3MTI1NjIxMDksImV4cCI6MTcyMTIwMjEwOX0.XatRV4bLbuRyvsQrL2etPAumpPPg5SK2h-7qVRrPub4"} + +The token can be used to request authenticated endpoint, for instance to get the user profile: + + # Using email/password + curl "http://johnDoe%40localhost:dummy@localhost:5252/users/224468756040777732" + + # Using bearer token + export JOHN_DOE_BEARER=eyJzdWIiOiIyMjQ0Njg3NTYwNDA3Nzc3MzIiLCJpYXQiOjE3MTI1NjIxMDksImV4cCI6MTcyMTIwMjEwOX0.XatRV4bLbuRyvsQrL2etPAumpPPg5SK2h-7qVRrPub4 + curl -H "Authorization: Bearer ${JOHN_DOE_BEARER}" "http://localhost:5252/users/224468756040777732" + +## Rights +The rights defines access to resto ressources in particular to authorize CRUD operations (Create, Read, Update, Delete) on collections and features. + +rights are defined as boolean properties within a JSON object. The default user's rights are the following: + + { + // If true the user can create a collection + "createCollection": false, + + // If true the user can delete a collection he owns + "deleteCollection": true, + + // If true the user can update a collection he owns + "updateCollection": true, + + // If true the user can delete any collection whether he owns it or not + "deleteAnyCollection": false, + + // If true the user can update any collection whether he owns it or not + "updateAnyCollection": false, + + // If true the user can add a feature to a collection owns + "createFeature": true, + + // If true the user can delete a feature he owns + "deleteFeature": true, + + // If true the user can update a feature he owns + "updateFeature": true, + + // If true the user can add a feature to any collection whether he owns it or not + "createAnyFeature": false, + + // If true the user can delete any feature whether he owns it or not + "deleteAnyFeature": false, + + // If true the user can update any feature whether he owns it or not + "updateAnyFeature": false + + } + +### Get user rights +To get the rights for John Doe: + + curl -H "Authorization: Bearer ${JOHN_DOE_BEARER}" "http://localhost:5252/users/224468756040777732/rights" + +The result should be : + + {"rights":{"createCollection":false,"deleteCollection":true,"updateCollection":true,"deleteAnyCollection":false,"updateAnyCollection":false,"createFeature":true,"updateFeature":true,"deleteFeature":true,"createAnyFeature":false,"deleteAnyFeature":false,"updateAnyFeature":false,"downloadFeature":false}} + +### Set user rights +Only a user in the **admin group** (see chapter on groups below) can set the rights of a user. + + # Allow John Doe to create collection + curl -X POST -d@examples/users/johnDoe_rights.json "http://admin:admin@localhost:5252/users/224468756040777732/rights" + +The result should returns : + + {"status":"success","message":"Rights set","rights":{"createCollection":true}} + +Note that existing rights are not deleted when setting rights but are merged with input rights. + +## Groups +groups can be used to share rights among group members. + +On the first launch of resto, two groups are created : + +* The *admin group* identified by id **0**. +* The *default group* identifier by id **100** + +All users are automatically added to the default group + +### Add a group +Any user can add a group. Note that the group name must be unique. + + # Create dummy group + curl -X POST -d@examples/users/dummyGroup.json "http://johnDoe%40localhost:dummy@localhost:5252/groups" + +The result should be + + {"status":"success","message":"Group created","id":103,"name":"My first group","owner":"224468756040777732"} + +### Set group rights +Only a user in the **admin group** can set the rights for a group + + # Set rights for dummy group allowing members to createAnyFeature + curl -X POST -d@examples/users/dummyGroup_rights.json "http://admin:admin@localhost:5252/groups/103/rights" + +The result should returns : + + {"status":"success","message":"Rights set","rights":{"createAnyFeature":true}} + +Note that existing rights are not deleted when setting rights but are merged with input rights. + +### Add user to a group +Only a user in the **admin group** or the owner of the group can add user to a group + + # Add John Doe in group dummyGroup + curl -X POST -d@examples/users/dummyGroup_addJohnDoe.json "http://admin:admin@localhost:5252/groups/103/users" + + # Consequently, John Doe's rights now includes rights from its groups + curl -H "Authorization: Bearer ${JOHN_DOE_BEARER}" "http://localhost:5252/users/224468756040777732/rights" + + # Result of previous request shows that John Doe can now createAnyFeature since he is in dummyGroup + # {"rights":{"createCollection":true,"deleteCollection":true,"updateCollection":true,"deleteAnyCollection":false,"updateAnyCollection":false,"createFeature":true,"updateFeature":true,"deleteFeature":true,"createAnyFeature":true,"deleteAnyFeature":false,"updateAnyFeature":false,"downloadFeature":false} + +### Remove user from a group +Only a user in the **admin group** or the owner of the group can remove a user from a group + + # Remove John Doe from dummyGroup + curl -X DELETE "http://admin:admin@localhost:5252/groups/103/users/224468756040777732" + + # Consequently, John Doe's rights do not include anymore rights from dummyGroup + curl -H "Authorization: Bearer ${JOHN_DOE_BEARER}" "http://localhost:5252/users/224468756040777732/rights" + + # {"rights":{"createCollection":true,"deleteCollection":true,"updateCollection":true,"deleteAnyCollection":false,"updateAnyCollection":false,"createFeature":true,"updateFeature":true,"deleteFeature":true,"createAnyFeature":false,"deleteAnyFeature":false,"updateAnyFeature":false,"downloadFeature":false} + +## Ownership and visibility diff --git a/app/resto/core/RestoConstants.php b/app/resto/core/RestoConstants.php index 432a7617..a0d37bfa 100644 --- a/app/resto/core/RestoConstants.php +++ b/app/resto/core/RestoConstants.php @@ -20,7 +20,7 @@ class RestoConstants // [IMPORTANT] Starting resto 7.x, default routes are defined in RestoRouter class // resto version - const VERSION = '7.0.2'; + const VERSION = '8.0.0'; /* ============================================================ * NEVER EVER TOUCH THESE VALUES diff --git a/app/resto/core/RestoRouter.php b/app/resto/core/RestoRouter.php index 231d37c8..0a5fbfd9 100755 --- a/app/resto/core/RestoRouter.php +++ b/app/resto/core/RestoRouter.php @@ -71,6 +71,8 @@ class RestoRouter array('POST' , RestoRouter::ROUTE_TO_GROUPS, true, 'GroupAPI::createGroup'), // Create group array('DELETE', RestoRouter::ROUTE_TO_GROUPS . '/{id}', true, 'GroupAPI::deleteGroup'), // Delete group array('GET' , RestoRouter::ROUTE_TO_USER . '/groups', true, 'GroupAPI::getUserGroups'), // Show user groups + array('POST' , RestoRouter::ROUTE_TO_GROUPS . '/{id}/users', true, 'GroupAPI::addUser'), // Add user to group + array('DELETE', RestoRouter::ROUTE_TO_GROUPS . '/{id}/users/{userid}', true, 'GroupAPI::deleteUser'), // Delete user from group // API for rights array('GET', RestoRouter::ROUTE_TO_USER . '/rights', true, 'RightsAPI::getUserRights'), // Show user rights diff --git a/app/resto/core/RestoUser.php b/app/resto/core/RestoUser.php index 17a7b851..7e47ab2c 100755 --- a/app/resto/core/RestoUser.php +++ b/app/resto/core/RestoUser.php @@ -254,8 +254,60 @@ public function hasRightsTo($action, $params = array()) $rights = $this->getRights(); - switch ($action) {} + /* + * 1) Handle actions that are not known + * and actions that do not need params to be set + */ + $withParams = array( + RestoUser::DELETE_COLLECTION, + RestoUser::UPDATE_COLLECTION, + RestoUser::CREATE_FEATURE, + RestoUser::DELETE_FEATURE, + RestoUser::UPDATE_FEATURE, + ); + if ( !in_array($action, $withParams) ) { + return $rights[$action] ?? false; + } + /* + * Split camel case action into parts + * The first token is the action (create, update, delete) + * The last token is the target (collection, feature) + */ + $splittedAction = preg_split('/(?<=[a-z])(?=[A-Z])/x', $action); + + if ( count($splittedAction) === 2 ) { + + // 2) Handle "Any" cases + $any = $splittedAction[0] . 'Any' . $splittedAction[1]; + if ( isset($rights[$any]) && $rights[$any] ) { + return true; + } + + } + + + // 3) Handle action with params + switch ($action) { + + // Only owner of collection can do this + case RestoUser::CREATE_FEATURE: + case RestoUser::DELETE_COLLECTION: + case RestoUser::UPDATE_COLLECTION: + return $rights[$action] && isset($params['collection']) && $params['collection']->owner === $this->profile['id']; + + // Only owner of feature can do this + case RestoUser::DELETE_FEATURE: + case RestoUser::UPDATE_FEATURE: + if ( !isset($feature) ) { + return false; + } + $featureArray = $feature->toArray(); + return $rights[$action] && isset($featureArray['properties']['owner']) && $featureArray['properties']['owner'] !== $this->profile['id']; + + default: + return $rights[$action] ?? false; + } } @@ -433,81 +485,4 @@ public function loadProfile() } } - /** - * Can User download or visualize - * - * @param string $action - * @param string $collectionId - * @param string $featureId - * @return boolean - */ - private function hasDownloadOrVisualizeRights($action, $collectionId, $featureId = null) - { - $rights = $this->getRights($collectionId, $featureId); - return $rights[$action]; - } - - /** - * Can user create collection ? - * - * @return boolean - */ - private function hasCreateRights() - { - $rights = $this->getRights(); - return isset($rights['collections']['*']) ? $rights['collections']['*']['create'] : 0; - } - - /** - * A user can update a collection if he is the owner of the collection - * or if he is an admin - * - * @param RestoCollection $collection - * @return boolean - */ - private function hasUpdateRights($collection) - { - if (!$this->hasCreateRights()) { - return false; - } - - /* - * Only collection owner and admin can update the collection - */ - if (!$this->hasGroup(RestoConstants::GROUP_ADMIN_ID) && $collection->owner !== $this->profile['id']) { - return false; - } - - return true; - } - - /** - * Return rights array for add/update/delete - * - * @param array $rights - * @param string $collectionId - * @param string $featureId - * - * @return string - */ - private function getRightsArray($rights, $collectionId, $featureId) - { - /* - * Check that collection/feature exists - */ - if (isset($collectionId)) { - if (! (new CollectionsFunctions($this->context->dbDriver))->collectionExists($collectionId)) { - RestoLogUtil::httpError(404, 'Collection does not exist'); - } - } - - return array( - 'rights' => $rights, - 'id' => $this->profile['id'], - 'groupid' => null, - 'collectionId' => $collectionId, - 'featureId' => $featureId, - 'target' => $this->context->dbDriver->targetSchema - ); - } } diff --git a/app/resto/core/api/CollectionsAPI.php b/app/resto/core/api/CollectionsAPI.php index 725ebb93..6cc26a39 100755 --- a/app/resto/core/api/CollectionsAPI.php +++ b/app/resto/core/api/CollectionsAPI.php @@ -674,9 +674,6 @@ public function insertFeatures($params, $body) */ $collection = $this->context->keeper->getRestoCollection($params['collectionId'], $this->user)->load(); - /* - * Only a user with 'update' rights on collection can POST feature - */ if (!$this->user->hasRightsTo(RestoUser::CREATE_FEATURE, array('collection' => $collection))) { RestoLogUtil::httpError(403); } diff --git a/app/resto/core/api/FeaturesAPI.php b/app/resto/core/api/FeaturesAPI.php index a3b09717..a28ad534 100755 --- a/app/resto/core/api/FeaturesAPI.php +++ b/app/resto/core/api/FeaturesAPI.php @@ -688,14 +688,8 @@ public function updateFeature($params, $body) RestoLogUtil::httpError(404); } - /* - * Only owner of a feature or admin can update it - */ - $featureArray = $feature->toArray(); - if (! isset($featureArray['properties']['owner']) || $featureArray['properties']['owner'] !== $this->user->profile['id']) { - if (! $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID)) { - RestoLogUtil::httpError(403); - } + if (!$this->user->hasRightsTo(RestoUser::UPDATE_FEATURE, array('feature' => $feature))) { + RestoLogUtil::httpError(403); } // Specifically set splitGeometry @@ -811,11 +805,8 @@ public function updateFeatureProperty($params, $body) RestoLogUtil::httpError(404); } - // Only admin or owner can change feature properties - if (! isset($featureArray['properties']['owner']) || $featureArray['properties']['owner'] !== $this->user->profile['id']) { - if (! $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID)) { - RestoLogUtil::httpError(403); - } + if (!$this->user->hasRightsTo(RestoUser::UPDATE_FEATURE, array('feature' => $feature))) { + RestoLogUtil::httpError(403); } // A value key is mandatory @@ -919,16 +910,10 @@ public function deleteFeature($params) RestoLogUtil::httpError(404); } - /* - * Only owner of a feature or admin can delete it - */ - $featureArray = $feature->toArray(); - if (!isset($featureArray['properties']['owner']) || $featureArray['properties']['owner'] !== $this->user->profile['id']) { - if (! $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID)) { - RestoLogUtil::httpError(403); - } + if (!$this->user->hasRightsTo(RestoUser::DELETE_FEATURE, array('feature' => $feature))) { + RestoLogUtil::httpError(403); } - + // Result contains boolean for facetsDeleted $result = (new FeaturesFunctions($this->context->dbDriver))->removeFeature($feature); diff --git a/app/resto/core/api/GroupAPI.php b/app/resto/core/api/GroupAPI.php index ad8d7da7..146c7931 100755 --- a/app/resto/core/api/GroupAPI.php +++ b/app/resto/core/api/GroupAPI.php @@ -340,6 +340,108 @@ public function deleteGroup($params) } + /** + * + * Add a user to group + * + * @OA\Post( + * path="/groups/{id}/users", + * summary="Add a user", + * description="Add a user to a group", + * tags={"Group"}, + * @OA\Response( + * response="200", + * description="User is added", + * @OA\JsonContent( + * @OA\Property( + * property="status", + * type="string", + * description="Status is *success*" + * ), + * @OA\Property( + * property="message", + * type="string", + * description="User added" + * ), + * example={ + * "status": "success", + * "message": "User added" + * } + * ) + * ), + * @OA\RequestBody( + * description="User info", + * required=true, + * @OA\JsonContent( + * required={"userid"}, + * @OA\Property( + * property="userid", + * type="string", + * description="User identifier" + * ), + * example={ + * "userid": "100" + * } + * ) + * ), + * security={ + * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} + * } + * ) + * + * @param array $params + * @param array $body + * + */ + public function addUser($params, $body) + { + + $group = (new GroupsFunctions($this->context->dbDriver))->getGroups(array('id' => $params['id'])); + + if ( !isset($body['userid']) ) { + RestoLogUtil::httpError(400, 'Mandatory userid property is missing in message body'); + } + + /* + * [SECURITY] Only user and admin can add user to group + */ + $isAdmin = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID); + if ( !$isAdmin ) { + if ( !isset($group['owner']) ) { + RestoLogUtil::httpError(403); + } + RestoUtil::checkUser($this->user, $group['owner']); + } + + if ( (new GroupsFunctions($this->context->dbDriver))->addUserToGroup(array('id' => $params['id']), $body['userid']) ) { + return RestoLogUtil::success('User added to group'); + } + + + } + + public function deleteUser($params) + { + + $group = (new GroupsFunctions($this->context->dbDriver))->getGroups(array('id' => $params['id'])); + + /* + * [SECURITY] Only user and admin can delete user from group + */ + $isAdmin = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID); + if ( !$isAdmin ) { + if ( !isset($group['owner']) ) { + RestoLogUtil::httpError(403); + } + RestoUtil::checkUser($this->user, $group['owner']); + } + + if ( (new GroupsFunctions($this->context->dbDriver))->removeUserFromGroup(array('id' => $params['id']), $params['userid']) ) { + return RestoLogUtil::success('User removed from group'); + } + + + } /** * Get user groups diff --git a/app/resto/core/api/RightsAPI.php b/app/resto/core/api/RightsAPI.php index 652c28fc..a3ee6369 100755 --- a/app/resto/core/api/RightsAPI.php +++ b/app/resto/core/api/RightsAPI.php @@ -58,11 +58,11 @@ public function __construct($context, $user) /** * @OA\Get( - * path="/users/{id}/rights", + * path="/users/{userid}/rights", * summary="Get user rights", * tags={"Rights"}, * @OA\Parameter( - * name="id", + * name="userid", * in="path", * required=true, * description="User identifier", @@ -165,10 +165,19 @@ public function getGroupRights($params) * Set user rights * * @OA\Post( - * path="/users/{id}/rights", + * path="/users/{userid}/rights", * summary="Set rights for user", * description="Set rights for a given user", * tags={"Rights"}, + * @OA\Parameter( + * name="userid", + * in="path", + * required=true, + * description="User identifier", + * @OA\Schema( + * type="integer" + * ) + * ), * @OA\Response( * response="200", * description="Rights is created or updated", @@ -185,7 +194,7 @@ public function getGroupRights($params) * ), * @OA\Property( * property="rights", - * description="Set rights" + * description="Set rights", * @OA\JsonContent(ref="#/components/schemas/Rights") * ), * example={ @@ -216,14 +225,7 @@ public function getGroupRights($params) * @OA\RequestBody( * description="Rights to create/udpated", * required=true, - * @OA\JsonContent( - * required={"rights"}, - * @OA\Property( - * property="rights", - * description="right to create/update" - * @OA\JsonContent(ref="#/components/schemas/Rights") - * ) - * ) + * @OA\JsonContent(ref="#/components/schemas/Rights") * ), * security={ * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} @@ -244,17 +246,15 @@ public function setUserRights($params, $body) return RestoLogUtil::httpError(403); } - $rights = $params['rights'] ?? array(); - if ( !empty($rights) ) { - return RestoLogUtil::httpError(400, 'The rights array is not set or empty'); + if ( empty($body) ) { + return RestoLogUtil::httpError(400, 'No rights to set'); } // Get user just to be sure that it exists ! - new RestoUser(array('id' => $params['id']), $this->context, true); - $rights = (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights('userid', $params['id'], $rights); + new RestoUser(array('id' => $params['userid']), $this->context, true); return RestoLogUtil::success('Rights set', array( - 'rights' => $rights + 'rights' => (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights('userid', $params['userid'], $body) )); } @@ -284,7 +284,7 @@ public function setUserRights($params, $body) * ), * @OA\Property( * property="rights", - * description="Created/update rights" + * description="Created/update rights", * @OA\JsonContent(ref="#/components/schemas/Rights") * ), * example={ @@ -315,14 +315,7 @@ public function setUserRights($params, $body) * @OA\RequestBody( * description="Rights to create/udpated", * required=true, - * @OA\JsonContent( - * required={"rights"}, - * @OA\Property( - * property="rights", - * description="right to create/update" - * @OA\JsonContent(ref="#/components/schemas/Rights") - * ) - * ) + * @OA\JsonContent(ref="#/components/schemas/Rights") * ), * security={ * {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}} @@ -342,19 +335,17 @@ public function setGroupRights($params, $body) return RestoLogUtil::httpError(403); } - $rights = $params['rights'] ?? array(); - if ( !empty($rights) ) { - return RestoLogUtil::httpError(400, 'The rights array is not set or empty'); + if ( empty($body) ) { + return RestoLogUtil::httpError(400, 'No rights to set'); } // Get group just to be sure that it exists ! (new GroupsFunctions($this->context->dbDriver))->getGroups(array( 'id' => $params['id'] )); - $rights = (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights('groupid', $params['id'], $rights); - + return RestoLogUtil::success('Rights set', array( - 'rights' => $rights + 'rights' => (new RightsFunctions($this->context->dbDriver))->storeOrUpdateRights('groupid', $params['id'], $body) )); } diff --git a/app/resto/core/api/UsersAPI.php b/app/resto/core/api/UsersAPI.php index e76f02d3..69fe2410 100755 --- a/app/resto/core/api/UsersAPI.php +++ b/app/resto/core/api/UsersAPI.php @@ -665,11 +665,11 @@ private function storeProfile($profile, $storageInfo) if (isset($userInfo)) { // Auto activation no email sent - if ($profile['activated'] === 1) { - return RestoLogUtil::success('User ' . $profile['email'] . ' created and activated (id : ' . $profile['id'] . ')'); + if ($userInfo['activated'] === 1) { + return RestoLogUtil::success('User ' . $userInfo['email'] . ' created and activated', array('profile' => $userInfo)); } - if (!(new RestoNotifier($this->context->servicesInfos, $this->context->lang))->sendMailForUserActivation($profile['email'], $this->context->core['sendmail'], array( + if (!(new RestoNotifier($this->context->servicesInfos, $this->context->lang))->sendMailForUserActivation($userInfo['email'], $this->context->core['sendmail'], array( 'token' => $this->context->createRJWT($userInfo['id'], $this->context->core['tokenDuration']) ))) { RestoLogUtil::httpError(500, 'Cannot send activation link'); diff --git a/app/resto/core/dbfunctions/CollectionsFunctions.php b/app/resto/core/dbfunctions/CollectionsFunctions.php index a8c1dc67..cff8205b 100755 --- a/app/resto/core/dbfunctions/CollectionsFunctions.php +++ b/app/resto/core/dbfunctions/CollectionsFunctions.php @@ -152,11 +152,6 @@ public function removeCollection($collection) $collection->id )); - $this->dbDriver->pQuery('DELETE FROM ' . $this->dbDriver->commonSchema . '.right WHERE collection=$1 and target=$2', array( - $collection->id, - $this->dbDriver->targetSchema - )); - $this->dbDriver->query('COMMIT'); /* diff --git a/app/resto/core/dbfunctions/GroupsFunctions.php b/app/resto/core/dbfunctions/GroupsFunctions.php index 63e0bd9a..03aa89e7 100755 --- a/app/resto/core/dbfunctions/GroupsFunctions.php +++ b/app/resto/core/dbfunctions/GroupsFunctions.php @@ -63,6 +63,11 @@ public function getGroups($params = array()) if (isset($params['userid'])) { $where[] = 'id IN (SELECT DISTINCT groupid FROM ' . $this->dbDriver->commonSchema . '.group_member WHERE userid=' . pg_escape_string($this->dbDriver->getConnection(), $params['userid']) . ')'; } + + // Return groups by owner + if (isset($params['owner'])) { + $where[] = 'owner=' . pg_escape_string($this->dbDriver->getConnection(), $params['owner']); + } return $this->formatGroups($this->dbDriver->fetch($this->dbDriver->query('SELECT id, name, description, owner, to_iso8601(created) as created FROM ' . $this->dbDriver->commonSchema . '.group' . (count($where) > 0 ? ' WHERE ' . join(' AND ', $where) : '') . ' ORDER BY id DESC')), $params['id'] ?? null); } @@ -142,6 +147,66 @@ public function removeGroup($params) } } + /** + * Add user to group + * + * @param Array $params + * @throws Exception + */ + public function addUserToGroup($params, $userid) + { + if (! isset($params['id'])) { + RestoLogUtil::httpError(400, 'Missing mandatory group identifier'); + } + + try { + + $result = pg_query_params($this->dbDriver->getConnection(), 'INSERT INTO ' . $this->dbDriver->commonSchema . '.group_member (groupid,userid,created) VALUES ($1,$2,now_utc()) ON CONFLICT (groupid,userid) DO NOTHING RETURNING groupid, userid', array( + $params['id'], + $userid + )); + + if ( !isset($result) ) { + throw new Exception(); + } + + } catch (Exception $e) { + RestoLogUtil::httpError(400, 'Cannot add user to group'); + } + + return true; + } + + /** + * Add user to group + * + * @param Array $params + * @throws Exception + */ + public function removeUserFromGroup($params, $userid) + { + if (! isset($params['id'])) { + RestoLogUtil::httpError(400, 'Missing mandatory group identifier'); + } + + try { + + $result = pg_query_params($this->dbDriver->getConnection(), 'DELETE FROM ' . $this->dbDriver->commonSchema . '.group_member WHERE groupid=$1 AND userid=$2', array( + $params['id'], + $userid + )); + + if ( !isset($result) ) { + throw new Exception(); + } + + } catch (Exception $e) { + RestoLogUtil::httpError(400, 'Cannot remove user from group'); + } + + return true; + } + /** * Format group results for nice output * diff --git a/app/resto/core/dbfunctions/RightsFunctions.php b/app/resto/core/dbfunctions/RightsFunctions.php index e02d9dbc..c7bfcbd5 100755 --- a/app/resto/core/dbfunctions/RightsFunctions.php +++ b/app/resto/core/dbfunctions/RightsFunctions.php @@ -95,19 +95,16 @@ public function storeOrUpdateRights($targetCol, $targetValue, $rights) { $query = join(' ', array( - 'INSERT INTO ' . $this->dbDriver->commonSchema . '.right (' . $targetCol . ', rights)', + 'INSERT INTO ' . $this->dbDriver->commonSchema . '.right as r (' . $targetCol . ', rights)', 'VALUES ($1, $2)', 'ON CONFLICT (' . $targetCol . ')', - 'DO UPDATE SET rights = (SELECT json_object_agg(key, CASE WHEN json_typeof(existing) = \'object\' THEN json_set(existing, key, value::json) ELSE json_set(\'{"\' || key || \'":\' || value || \'}\'::json, key, value::json) END) AS merged_rights', - 'FROM json_each_text(rights) AS rights_entries', - 'JOIN json_each_text(EXCLUDED.rights) AS new_entries ON rights_entries.key = new_entries.key', - 'RETURNING rights' + 'DO UPDATE SET rights = COALESCE(r.rights::jsonb || $2::jsonb) RETURNING rights' )); try { $result = pg_fetch_assoc(pg_query_params($this->dbDriver->getConnection(), $query, array( $targetValue, - $rights + json_encode($rights, JSON_UNESCAPED_SLASHES) ))); if ( !$result ) { throw new Exception(); @@ -116,7 +113,7 @@ public function storeOrUpdateRights($targetCol, $targetValue, $rights) RestoLogUtil::httpError(500, 'Cannot set rights'); } - return $result[0]['rights']; + return json_decode($result['rights'], true); } diff --git a/docs/resto-api.html b/docs/resto-api.html index efecfa23..06e9976e 100644 --- a/docs/resto-api.html +++ b/docs/resto-api.html @@ -1496,7 +1496,7 @@ @@ -1833,7 +1916,7 @@
-

Welcome to resto v7.0.0

+

Welcome to resto v8.0.0

Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.

@@ -2159,6 +2242,13 @@

Response Schema

List of keywords describing the collection. +»» aliases +[string] +false +none +Alias names for this collection. Each alias must be unique and not be the same as an already existing collection name + + »» license string true @@ -2459,6 +2549,13 @@

Parameters

+model +query +string +false +Set the model for the collection (e.g. OpticalModel). This superseed the model property from the input collection description + + body body InputCollection @@ -3365,7 +3462,7 @@

Parameters

query boolean false -Set to false to not split geometry during feature insertion. Default is true +Superseed the SPLIT_GEOMETRY_ON_DATELINE configuration i.e. set to true to split geometry during feature insertion - false otherwise. Default is set to SPLIT_GEOMETRY_ON_DATELINE _useItag @@ -5667,6 +5764,157 @@

Response Schema

+

GroupAPI::getUserGroups

+

+
+

Code samples

+
+
# You can also use wget
+curl -X GET http://127.0.0.1:5252/users/{userid}/groups \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

GET /users/{userid}/groups

+

Get user's groups

+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
useridpathstringtrueUser's identifier
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "id": "1356771884787565573",
+  "groups": [
+    {
+      "id": "1",
+      "name": "Default",
+      "description": "Default group"
+    }
+  ]
+}
+
+
+

401 Response

+
+
{
+  "ErrorCode": 401,
+  "ErrorMessage": "Unauthorized"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKUser groupsInline
401UnauthorizedUnauthorizedUnauthorizedError
404Not FoundResource not foundNotFoundError
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» idstringfalsenoneUser identifier
» groups[OutputGroup]falsenoneArray of user's groups
»» idintegertruenoneUnique group identifier - generated by resto during group creation
»» namestringtruenoneUnique name of the group - free text
»» descriptionstringtruenoneA description for this group
»» ownerstringtruenoneOwner of the group (i.e. user unique identifier)
+

UsersAPI::getUsersProfiles

@@ -7128,20 +7376,22 @@

Response Schema

To perform this operation, you must be authenticated by means of one of the following methods: basicAuth & bearerAuth & queryAuth -

Server

-

ServicesAPI::api

-

+

Group

+

GroupAPI::getGroups

+

Code samples

# You can also use wget
-curl -X GET http://127.0.0.1:5252/api.{format}
+curl -X GET http://127.0.0.1:5252/groups \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
 
 
-

GET /api.{format}

-

OpenAPI definition

-

Returns the server API definition as an OpenAPI 3.0 JSON document (default) or as an HTML page (if format is specified and set to html)

-

Parameters

+

GET /groups

+

Get groups

+

The list is ordered by most recently created

+

Parameters

@@ -7154,23 +7404,1147 @@

Parameters

- - + + - - + +
formatpathqquery stringtrueOutput format - json or htmlfalseFilter by group name
-

Enumerated Values

+
+

Example responses

+
+
+

200 Response

+
+
{
+  "totalResults": 2,
+  "groups": [
+    {
+      "id": 100,
+      "name": "My first group",
+      "description": "Any user can create a group.",
+      "owner": "1919730603616371731"
+    },
+    {
+      "id": 101,
+      "name": "My second group",
+      "description": "Any user can create a group.",
+      "owner": "1919730603616371731"
+    }
+  ]
+}
+
+
+

401 Response

+
+
{
+  "ErrorCode": 401,
+  "ErrorMessage": "Unauthorized"
+}
+
+

Responses

- - - - - + + + + + + + + + + + + + + + + + + + + +
ParameterValue
StatusMeaningDescriptionSchema
200OKList of groupsInline
401UnauthorizedUnauthorizedUnauthorizedError
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» totalResultsintegerfalsenoneTotal number of groups
» results[OutputGroup]falsenoneReturn groups
»» idintegertruenoneUnique group identifier - generated by resto during group creation
»» namestringtruenoneUnique name of the group - free text
»» descriptionstringtruenoneA description for this group
»» ownerstringtruenoneOwner of the group (i.e. user unique identifier)
+ +

GroupAPI::createGroup

+

+
+

Code samples

+
+
# You can also use wget
+curl -X POST http://127.0.0.1:5252/groups \
+  -H 'Content-Type: application/json' \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

POST /groups

+

Create group

+

Create a group from name - the name must be unique

+
+

Body parameter

+
+
{
+  "name": "string",
+  "description": "string"
+}
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
bodybodyobjecttrueGroup description
» namebodystringtrueUnique name for the group
» descriptionbodystringfalsenone
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "status": "success",
+  "message": "Group created",
+  "id": 100
+}
+
+
+

400 Response

+
+
{
+  "ErrorCode": 400,
+  "ErrorMessage": "Bad request"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKGroup is created and unique identifier is returnedInline
400Bad RequestThis group already existBadRequestError
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» statusstringfalsenoneStatus is success
» messagestringfalsenoneGroup created
» idintegerfalsenoneNewly created group identifier
+ +

GroupAPI::getGroup

+

+
+

Code samples

+
+
# You can also use wget
+curl -X GET http://127.0.0.1:5252/groups/{id} \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

GET /groups/{id}

+

Get group

+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
idpathintegertrueGroup identifier
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "id": 100,
+  "name": "My first group",
+  "description": "Any user can create a group.",
+  "owner": "1919730603616371731"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKUser groupOutputGroup
401UnauthorizedUnauthorizedUnauthorizedError
403ForbiddenForbiddenForbiddenError
404Not FoundResource not foundNotFoundError
+ +

GroupAPI::deleteGroup

+

+
+

Code samples

+
+
# You can also use wget
+curl -X DELETE http://127.0.0.1:5252/groups/{id} \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

DELETE /groups/{id}

+

Delete group

+

Only administrator and owner of a group can delete it

+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
idpathstringtrueGroup identifier
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "status": "success",
+  "message": "Delete group 1919831313561420822"
+}
+
+
+

400 Response

+
+
{
+  "ErrorCode": 400,
+  "ErrorMessage": "Bad request"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKThe group is deleteInline
400Bad RequestMissing mandatory group identifierBadRequestError
403ForbiddenForbiddenForbiddenError
404Not FoundGroup not foundNotFoundError
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» statusstringfalsenoneStatus is success
» messagestringfalsenoneMessage information
+ +

GroupAPI::addUser

+

+
+

Code samples

+
+
# You can also use wget
+curl -X POST http://127.0.0.1:5252/groups/{id}/users \
+  -H 'Content-Type: application/json' \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

POST /groups/{id}/users

+

Add a user

+

Add a user to a group

+
+

Body parameter

+
+
{
+  "userid": "string"
+}
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
bodybodyobjecttrueUser info
» useridbodystringtrueUser identifier
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "status": "success",
+  "message": "User added"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKUser is addedInline
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» statusstringfalsenoneStatus is success
» messagestringfalsenoneUser added
+ +

Rights

+

RightsAPI::getUserRights

+

+
+

Code samples

+
+
# You can also use wget
+curl -X GET http://127.0.0.1:5252/users/{userid}/rights \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

GET /users/{userid}/rights

+

Get user rights

+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
useridpathintegertrueUser identifier
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "createCollection": false,
+  "deleteCollection": true,
+  "updateCollection": true,
+  "deleteAnyCollection": false,
+  "updateAnyCollection": false,
+  "createFeature": true,
+  "updateFeature": true,
+  "deleteFeature": true,
+  "createAnyFeature": false,
+  "deleteAnyFeature": false,
+  "updateAnyFeature": false,
+  "downloadFeature": false
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKUser rightsRights
401UnauthorizedUnauthorizedUnauthorizedError
403ForbiddenForbiddenForbiddenError
404Not FoundResource not foundNotFoundError
+ +

RightsAPI::setUserRights

+

+
+

Code samples

+
+
# You can also use wget
+curl -X POST http://127.0.0.1:5252/users/{userid}/rights \
+  -H 'Content-Type: application/json' \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

POST /users/{userid}/rights

+

Set rights for user

+

Set rights for a given user

+
+

Body parameter

+
+
{
+  "createCollection": false,
+  "deleteCollection": true,
+  "updateCollection": true,
+  "deleteAnyCollection": false,
+  "updateAnyCollection": false,
+  "createFeature": true,
+  "updateFeature": true,
+  "deleteFeature": true,
+  "createAnyFeature": false,
+  "deleteAnyFeature": false,
+  "updateAnyFeature": false,
+  "downloadFeature": false
+}
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
useridpathintegertrueUser identifier
bodybodyRightstrueRights to create/udpated
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "status": "success",
+  "message": "Rights updated",
+  "rights": {
+    "createCollection": false,
+    "deleteCollection": true,
+    "updateCollection": true,
+    "deleteAnyCollection": false,
+    "updateAnyCollection": false,
+    "createFeature": true,
+    "updateFeature": true,
+    "deleteFeature": true,
+    "createAnyFeature": false,
+    "deleteAnyFeature": false,
+    "updateAnyFeature": false,
+    "downloadFeature": false
+  }
+}
+
+
+

400 Response

+
+
{
+  "ErrorCode": 400,
+  "ErrorMessage": "Bad request"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKRights is created or updatedInline
400Bad RequestRights is not setBadRequestError
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» statusstringfalsenoneStatus is success
» messagestringfalsenoneRights updated
» rightsanyfalsenoneSet rights
+ +

RightsAPI::getGroupRights

+

+
+

Code samples

+
+
# You can also use wget
+curl -X GET http://127.0.0.1:5252/groups/{id}/rights \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

GET /groups/{id}/rights

+

Get group rights

+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
idpathintegertrueGroup identifier
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "createCollection": false,
+  "deleteCollection": true,
+  "updateCollection": true,
+  "deleteAnyCollection": false,
+  "updateAnyCollection": false,
+  "createFeature": true,
+  "updateFeature": true,
+  "deleteFeature": true,
+  "createAnyFeature": false,
+  "deleteAnyFeature": false,
+  "updateAnyFeature": false,
+  "downloadFeature": false
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKUser group rightsRights
401UnauthorizedUnauthorizedUnauthorizedError
403ForbiddenForbiddenForbiddenError
404Not FoundResource not foundNotFoundError
+ +

RightsAPI::setGroupRights

+

+
+

Code samples

+
+
# You can also use wget
+curl -X POST http://127.0.0.1:5252/groups/{id}/rights \
+  -H 'Content-Type: application/json' \
+  -H 'Accept: application/json' \
+  -H 'Authorization: Bearer {access-token}'
+
+
+

POST /groups/{id}/rights

+

Set rights for group

+

Set rights for a given group

+
+

Body parameter

+
+
{
+  "createCollection": false,
+  "deleteCollection": true,
+  "updateCollection": true,
+  "deleteAnyCollection": false,
+  "updateAnyCollection": false,
+  "createFeature": true,
+  "updateFeature": true,
+  "deleteFeature": true,
+  "createAnyFeature": false,
+  "deleteAnyFeature": false,
+  "updateAnyFeature": false,
+  "downloadFeature": false
+}
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
bodybodyRightstrueRights to create/udpated
+
+

Example responses

+
+
+

200 Response

+
+
{
+  "status": "success",
+  "message": "Rights updated",
+  "rights": {
+    "createCollection": false,
+    "deleteCollection": true,
+    "updateCollection": true,
+    "deleteAnyCollection": false,
+    "updateAnyCollection": false,
+    "createFeature": true,
+    "updateFeature": true,
+    "deleteFeature": true,
+    "createAnyFeature": false,
+    "deleteAnyFeature": false,
+    "updateAnyFeature": false,
+    "downloadFeature": false
+  }
+}
+
+
+

400 Response

+
+
{
+  "ErrorCode": 400,
+  "ErrorMessage": "Bad request"
+}
+
+

Responses

+ + + + + + + + + + + + + + + + + + + + + + + +
StatusMeaningDescriptionSchema
200OKRights is created or updatedInline
400Bad RequestRights is not setBadRequestError
+

Response Schema

+

Status Code 200

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
» statusstringfalsenoneStatus is success
» messagestringfalsenoneRights updated
» rightsanyfalsenoneCreated/update rights
+ +

Server

+

ServicesAPI::api

+

+
+

Code samples

+
+
# You can also use wget
+curl -X GET http://127.0.0.1:5252/api.{format}
+
+
+

GET /api.{format}

+

OpenAPI definition

+

Returns the server API definition as an OpenAPI 3.0 JSON document (default) or as an HTML page (if format is specified and set to html)

+

Parameters

+ + + + + + + + + + + + + + + + + + + +
NameInTypeRequiredDescription
formatpathstringtrueOutput format - json or html
+

Enumerated Values

+ + + + + + + + @@ -7665,6 +9039,13 @@

Properties

+ + + + + + + @@ -8022,6 +9403,13 @@

Properties

+ + + + + + + @@ -9583,6 +10971,79 @@

Properties

ParameterValue
format json Detailed multi-line description to fully explain the Collection. CommonMark 0.29 syntax MAY be used for rich text representation.
aliases[string]falsenoneAlias names for this collection. Each alias must be unique and not be the same as an already existing collection name
version string false List of keywords describing the collection.
aliases[string]falsenoneAlias names for this collection. Each alias must be unique and not be the same as an already existing collection name
license string true
+

OutputGroup

+

+
{
+  "id": 100,
+  "name": "My first group",
+  "description": "Any user can create a group.",
+  "owner": "1919730603616371731"
+}
+
+
+

Properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
idintegertruenoneUnique group identifier - generated by resto during group creation
namestringtruenoneUnique name of the group - free text
descriptionstringtruenoneA description for this group
ownerstringtruenoneOwner of the group (i.e. user unique identifier)
+

Rights

+

+
{
+  "createCollection": false,
+  "deleteCollection": true,
+  "updateCollection": true,
+  "deleteAnyCollection": false,
+  "updateAnyCollection": false,
+  "createFeature": true,
+  "updateFeature": true,
+  "deleteFeature": true,
+  "createAnyFeature": false,
+  "deleteAnyFeature": false,
+  "updateAnyFeature": false,
+  "downloadFeature": false
+}
+
+
+

A list of boolean rights

+

Properties

+

None

GenericError

{
@@ -9770,6 +11231,42 @@ 

NotFoundError

"ErrorMessage": "Not Found" } +
+

Properties

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredRestrictionsDescription
ErrorCodeintegertruenoneHTTP status code
ErrorMessagestringtruenoneError message
+

GoneError

+

+
{
+  "ErrorCode": 410,
+  "ErrorMessage": "Gone"
+}
+
 

Properties

diff --git a/docs/resto-api.json b/docs/resto-api.json index 92452024..88593473 100644 --- a/docs/resto-api.json +++ b/docs/resto-api.json @@ -6,7 +6,7 @@ "contact": { "email": "jerome.gasperi@gmail.com" }, - "version": "7.0.0" + "version": "8.0.0" }, "servers": [ { @@ -994,6 +994,18 @@ ], "summary": "Create collection", "operationId": "CollectionsAPI::createCollection", + "parameters": [ + { + "name": "model", + "in": "query", + "description": "Set the model for the collection (e.g. *OpticalModel*). This superseed the *model* property from the input collection description", + "required": false, + "style": "form", + "schema": { + "type": "string" + } + } + ], "requestBody": { "description": "Collection description", "content": { @@ -1826,7 +1838,7 @@ { "name": "_splitGeom", "in": "query", - "description": "Set to false to not split geometry during feature insertion. Default is true", + "description": "Superseed the SPLIT_GEOMETRY_ON_DATELINE configuration i.e. set to true to split geometry during feature insertion - false otherwise. Default is set to SPLIT_GEOMETRY_ON_DATELINE", "required": false, "style": "form", "schema": { @@ -2044,47 +2056,816 @@ } } ], - "requestBody": { - "description": "Feature description", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InputFeature" - } - } - } - }, + "requestBody": { + "description": "Feature description", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InputFeature" + } + } + } + }, + "responses": { + "200": { + "description": "The feature is updated", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Status is *success*", + "type": "string" + }, + "message": { + "description": "Message information", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "status": "success", + "message": "Update feature b9eeaf6b-9868-5418-9455-3e77cd349e21" + } + } + } + }, + "400": { + "description": "Invalid property", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "Feature not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Feature" + ], + "summary": "Delete feature", + "description": "Delete feature {featureId}", + "operationId": "FeaturesAPI::deleteFeature", + "parameters": [ + { + "name": "collectionId", + "in": "path", + "description": "Collection identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "featureId", + "in": "path", + "description": "Feature identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The feature is delete", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Status is *success*", + "type": "string" + }, + "message": { + "description": "Message information", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "status": "success", + "message": "Feature 7e5caa78-5127-53e5-97ff-ddf44984ef56 deleted" + } + } + } + }, + "400": { + "description": "Missing mandatory feature identifier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "Only user with *update* rights can delete a feature", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "Feature not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/collections/{collectionId}/items/{featureId}/properties/{property}": { + "put": { + "tags": [ + "Feature" + ], + "summary": "Update feature property", + "description": "Update {property} for feature {featureId}", + "operationId": "FeaturesAPI::updateFeatureProperty", + "parameters": [ + { + "name": "collectionId", + "in": "path", + "description": "Collection identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "featureId", + "in": "path", + "description": "Feature identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "property", + "in": "path", + "description": "Property to update", + "required": true, + "schema": { + "type": "string", + "enum": [ + "title", + "description", + "visibility", + "owner", + "status" + ] + } + } + ], + "requestBody": { + "description": "Property value to update", + "content": { + "application/json": { + "schema": { + "properties": { + "value": { + "description": "New property value" + } + }, + "type": "object" + }, + "example": { + "value": 1 + } + } + } + }, + "responses": { + "200": { + "description": "The property is updated", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Status is *success*", + "type": "string" + }, + "message": { + "description": "Message information", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "status": "success", + "message": "Update property for feature b9eeaf6b-9868-5418-9455-3e77cd349e21" + } + } + } + }, + "400": { + "description": "Invalid property", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "Feature not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/groups": { + "get": { + "tags": [ + "Group" + ], + "summary": "Get groups", + "description": "The list is ordered by most recently created", + "operationId": "GroupAPI::getGroups", + "parameters": [ + { + "name": "q", + "in": "query", + "description": "Filter by group name", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "List of groups", + "content": { + "application/json": { + "schema": { + "properties": { + "totalResults": { + "description": "Total number of groups", + "type": "integer" + }, + "results": { + "description": "Return groups", + "type": "array", + "items": { + "$ref": "#/components/schemas/OutputGroup" + } + } + }, + "type": "object" + }, + "example": { + "totalResults": 2, + "groups": [ + { + "id": 100, + "name": "My first group", + "description": "Any user can create a group.", + "owner": "1919730603616371731" + }, + { + "id": 101, + "name": "My second group", + "description": "Any user can create a group.", + "owner": "1919730603616371731" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + }, + "post": { + "tags": [ + "Group" + ], + "summary": "Create group", + "description": "Create a group from name - the name must be unique", + "operationId": "GroupAPI::createGroup", + "requestBody": { + "description": "Group description", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Unique name for the group", + "type": "string" + }, + "description": { + "description": "", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "name": "My first group", + "description": "Any user can create a group." + } + } + } + }, + "responses": { + "200": { + "description": "Group is created and unique identifier is returned", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Status is *success*", + "type": "string" + }, + "message": { + "description": "Group created", + "type": "string" + }, + "id": { + "description": "Newly created group identifier", + "type": "integer" + } + }, + "type": "object" + }, + "example": { + "status": "success", + "message": "Group created", + "id": 100 + } + } + } + }, + "400": { + "description": "This group already exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/groups/{id}": { + "get": { + "tags": [ + "Group" + ], + "summary": "Get group", + "operationId": "GroupAPI::getGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Group identifier", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "User group", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OutputGroup" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + }, + "delete": { + "tags": [ + "Group" + ], + "summary": "Delete group", + "description": "Only administrator and owner of a group can delete it", + "operationId": "GroupAPI::deleteGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Group identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The group is delete", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Status is *success*", + "type": "string" + }, + "message": { + "description": "Message information", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "status": "success", + "message": "Delete group 1919831313561420822" + } + } + } + }, + "400": { + "description": "Missing mandatory group identifier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "404": { + "description": "Group not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/groups/{id}/users": { + "post": { + "tags": [ + "Group" + ], + "summary": "Add a user", + "description": "Add a user to a group", + "operationId": "GroupAPI::addUser", + "requestBody": { + "description": "User info", + "required": true, + "content": { + "application/json": { + "schema": { + "required": [ + "userid" + ], + "properties": { + "userid": { + "description": "User identifier", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "userid": "100" + } + } + } + }, + "responses": { + "200": { + "description": "User is added", + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Status is *success*", + "type": "string" + }, + "message": { + "description": "User added", + "type": "string" + } + }, + "type": "object" + }, + "example": { + "status": "success", + "message": "User added" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/users/{userid}/groups": { + "get": { + "tags": [ + "User" + ], + "summary": "Get user's groups", + "operationId": "GroupAPI::getUserGroups", + "parameters": [ + { + "name": "userid", + "in": "path", + "description": "User's identifier", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User groups", + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "description": "User identifier", + "type": "string" + }, + "groups": { + "description": "Array of user's groups", + "type": "array", + "items": { + "$ref": "#/components/schemas/OutputGroup" + } + } + }, + "type": "object" + }, + "example": { + "id": "1356771884787565573", + "groups": [ + { + "id": "1", + "name": "Default", + "description": "Default group" + } + ] + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/users/{userid}/rights": { + "get": { + "tags": [ + "Rights" + ], + "summary": "Get user rights", + "operationId": "RightsAPI::getUserRights", + "parameters": [ + { + "name": "userid", + "in": "path", + "description": "User identifier", + "required": true, + "schema": { + "type": "integer" + } + } + ], "responses": { "200": { - "description": "The feature is updated", - "content": { - "application/json": { - "schema": { - "properties": { - "status": { - "description": "Status is *success*", - "type": "string" - }, - "message": { - "description": "Message information", - "type": "string" - } - }, - "type": "object" - }, - "example": { - "status": "success", - "message": "Update feature b9eeaf6b-9868-5418-9455-3e77cd349e21" - } - } - } - }, - "400": { - "description": "Invalid property", + "description": "User rights", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BadRequestError" + "$ref": "#/components/schemas/Rights" } } } @@ -2110,7 +2891,7 @@ } }, "404": { - "description": "Feature not found", + "description": "Resource not found", "content": { "application/json": { "schema": { @@ -2128,36 +2909,38 @@ } ] }, - "delete": { + "post": { "tags": [ - "Feature" + "Rights" ], - "summary": "Delete feature", - "description": "Delete feature {featureId}", - "operationId": "FeaturesAPI::deleteFeature", + "summary": "Set rights for user", + "description": "Set rights for a given user", + "operationId": "RightsAPI::setUserRights", "parameters": [ { - "name": "collectionId", - "in": "path", - "description": "Collection identifier", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "featureId", + "name": "userid", "in": "path", - "description": "Feature identifier", + "description": "User identifier", "required": true, "schema": { - "type": "string" + "type": "integer" } } ], + "requestBody": { + "description": "Rights to create/udpated", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rights" + } + } + } + }, "responses": { "200": { - "description": "The feature is delete", + "description": "Rights is created or updated", "content": { "application/json": { "schema": { @@ -2167,21 +2950,38 @@ "type": "string" }, "message": { - "description": "Message information", + "description": "Rights updated", "type": "string" + }, + "rights": { + "description": "Set rights" } }, "type": "object" }, "example": { "status": "success", - "message": "Feature 7e5caa78-5127-53e5-97ff-ddf44984ef56 deleted" + "message": "Rights updated", + "rights": { + "createCollection": false, + "deleteCollection": true, + "updateCollection": true, + "deleteAnyCollection": false, + "updateAnyCollection": false, + "createFeature": true, + "updateFeature": true, + "deleteFeature": true, + "createAnyFeature": false, + "deleteAnyFeature": false, + "updateAnyFeature": false, + "downloadFeature": false + } } } } }, "400": { - "description": "Missing mandatory feature identifier", + "description": "Rights is not set", "content": { "application/json": { "schema": { @@ -2189,6 +2989,45 @@ } } } + } + }, + "security": [ + { + "basicAuth": [], + "bearerAuth": [], + "queryAuth": [] + } + ] + } + }, + "/groups/{id}/rights": { + "get": { + "tags": [ + "Rights" + ], + "summary": "Get group rights", + "operationId": "RightsAPI::getGroupRights", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Group identifier", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "User group rights", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Rights" + } + } + } }, "401": { "description": "Unauthorized", @@ -2201,7 +3040,7 @@ } }, "403": { - "description": "Only user with *update* rights can delete a feature", + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -2211,7 +3050,7 @@ } }, "404": { - "description": "Feature not found", + "description": "Resource not found", "content": { "application/json": { "schema": { @@ -2228,73 +3067,28 @@ "queryAuth": [] } ] - } - }, - "/collections/{collectionId}/items/{featureId}/properties/{property}": { - "put": { + }, + "post": { "tags": [ - "Feature" - ], - "summary": "Update feature property", - "description": "Update {property} for feature {featureId}", - "operationId": "FeaturesAPI::updateFeatureProperty", - "parameters": [ - { - "name": "collectionId", - "in": "path", - "description": "Collection identifier", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "featureId", - "in": "path", - "description": "Feature identifier", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "property", - "in": "path", - "description": "Property to update", - "required": true, - "schema": { - "type": "string", - "enum": [ - "title", - "description", - "visibility", - "owner", - "status" - ] - } - } + "Rights" ], + "summary": "Set rights for group", + "description": "Set rights for a given group", + "operationId": "RightsAPI::setGroupRights", "requestBody": { - "description": "Property value to update", + "description": "Rights to create/udpated", + "required": true, "content": { "application/json": { "schema": { - "properties": { - "value": { - "description": "New property value" - } - }, - "type": "object" - }, - "example": { - "value": 1 + "$ref": "#/components/schemas/Rights" } } } }, "responses": { "200": { - "description": "The property is updated", + "description": "Rights is created or updated", "content": { "application/json": { "schema": { @@ -2304,21 +3098,38 @@ "type": "string" }, "message": { - "description": "Message information", + "description": "Rights updated", "type": "string" + }, + "rights": { + "description": "Created/update rights" } }, "type": "object" }, "example": { "status": "success", - "message": "Update property for feature b9eeaf6b-9868-5418-9455-3e77cd349e21" + "message": "Rights updated", + "rights": { + "createCollection": false, + "deleteCollection": true, + "updateCollection": true, + "deleteAnyCollection": false, + "updateAnyCollection": false, + "createFeature": true, + "updateFeature": true, + "deleteFeature": true, + "createAnyFeature": false, + "deleteAnyFeature": false, + "updateAnyFeature": false, + "downloadFeature": false + } } } } }, "400": { - "description": "Invalid property", + "description": "Rights is not set", "content": { "application/json": { "schema": { @@ -2326,36 +3137,6 @@ } } } - }, - "401": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedError" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForbiddenError" - } - } - } - }, - "404": { - "description": "Feature not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } } }, "security": [ @@ -3033,6 +3814,13 @@ "description": "Detailed multi-line description to fully explain the Collection. CommonMark 0.29 syntax MAY be used for rich text representation.", "type": "string" }, + "aliases": { + "description": "Alias names for this collection. Each alias must be unique and not be the same as an already existing collection name", + "type": "array", + "items": { + "type": "string" + } + }, "version": { "description": "Version of the collection.", "type": "string" @@ -3281,6 +4069,13 @@ "type": "string" } }, + "aliases": { + "description": "Alias names for this collection. Each alias must be unique and not be the same as an already existing collection name", + "type": "array", + "items": { + "type": "string" + } + }, "license": { "description": "License for this collection as a SPDX License identifier or expression. Alternatively, use proprietary if the license is not on the SPDX license list or various if multiple licenses apply. In these two cases links to the license texts SHOULD be added, see the license link relation type.", "type": "string", @@ -4596,6 +5391,57 @@ ] } }, + "OutputGroup": { + "required": [ + "id", + "name", + "description", + "owner" + ], + "properties": { + "id": { + "description": "Unique group identifier - generated by resto during group creation", + "type": "integer" + }, + "name": { + "description": "Unique name of the group - free text", + "type": "string" + }, + "description": { + "description": "A description for this group", + "type": "string" + }, + "owner": { + "description": "Owner of the group (i.e. user unique identifier)", + "type": "string" + } + }, + "type": "object", + "example": { + "id": 100, + "name": "My first group", + "description": "Any user can create a group.", + "owner": "1919730603616371731" + } + }, + "Rights": { + "description": "A list of boolean rights", + "type": "object", + "example": { + "createCollection": false, + "deleteCollection": true, + "updateCollection": true, + "deleteAnyCollection": false, + "updateAnyCollection": false, + "createFeature": true, + "updateFeature": true, + "deleteFeature": true, + "createAnyFeature": false, + "deleteAnyFeature": false, + "updateAnyFeature": false, + "downloadFeature": false + } + }, "GenericError": { "required": [ "ErrorCode", @@ -4717,6 +5563,27 @@ "ErrorCode": 404, "ErrorMessage": "Not Found" } + }, + "GoneError": { + "required": [ + "ErrorCode", + "ErrorMessage" + ], + "properties": { + "ErrorCode": { + "description": "HTTP status code", + "type": "integer" + }, + "ErrorMessage": { + "description": "Error message", + "type": "string" + } + }, + "type": "object", + "example": { + "ErrorCode": 410, + "ErrorMessage": "Gone" + } } }, "securitySchemes": { diff --git a/examples/users/dummyGroup.json b/examples/users/dummyGroup.json new file mode 100644 index 00000000..d8c0a4a9 --- /dev/null +++ b/examples/users/dummyGroup.json @@ -0,0 +1,4 @@ +{ + "name": "My first group", + "description": "Any user can create a group." +} \ No newline at end of file diff --git a/examples/users/dummyGroup_addJohnDoe.json b/examples/users/dummyGroup_addJohnDoe.json new file mode 100644 index 00000000..5f58196f --- /dev/null +++ b/examples/users/dummyGroup_addJohnDoe.json @@ -0,0 +1,3 @@ +{ + "userid": 224468756040777732 +} \ No newline at end of file diff --git a/examples/users/dummyGroup_rights.json b/examples/users/dummyGroup_rights.json new file mode 100644 index 00000000..ba9e7c2f --- /dev/null +++ b/examples/users/dummyGroup_rights.json @@ -0,0 +1,3 @@ +{ + "createAnyFeature": true +} \ No newline at end of file diff --git a/examples/users/johnDoe.json b/examples/users/johnDoe.json new file mode 100644 index 00000000..f9fa987d --- /dev/null +++ b/examples/users/johnDoe.json @@ -0,0 +1,6 @@ +{ + "firstname": "John", + "lastname": "Doe", + "email": "johnDoe@localhost", + "password": "dummy" +} \ No newline at end of file diff --git a/examples/users/johnDoe_rights.json b/examples/users/johnDoe_rights.json new file mode 100644 index 00000000..96bd1cd4 --- /dev/null +++ b/examples/users/johnDoe_rights.json @@ -0,0 +1,3 @@ +{ + "createCollection": true +} \ No newline at end of file diff --git a/resto-database-model/02_resto_common_model.sql b/resto-database-model/02_resto_common_model.sql index f57a13c5..7346392b 100644 --- a/resto-database-model/02_resto_common_model.sql +++ b/resto-database-model/02_resto_common_model.sql @@ -107,7 +107,7 @@ CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.follower ( -- -- groups table list -- -CREATE SEQUENCE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.group_id_seq START 100 INCREMENT 1; +CREATE SEQUENCE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.group_id_seq START 1000 INCREMENT 1; CREATE TABLE IF NOT EXISTS __DATABASE_COMMON_SCHEMA__.group ( -- Group identifier is a serial diff --git a/resto-database-model/migrations/00_migration.sql b/resto-database-model/migrations/00_migration.sql index 35575e89..68923c3f 100644 --- a/resto-database-model/migrations/00_migration.sql +++ b/resto-database-model/migrations/00_migration.sql @@ -18,4 +18,4 @@ ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS featureid; ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS download; ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS visualize; ALTER TABLE __DATABASE_COMMON_SCHEMA__.right DROP COLUMN IF EXISTS createcollection; -ALTER TABLE __DATABASE_COMMON_SCHEMA__.right ADD COLUMN IF NOT EXISTS rights JSON; +ALTER TABLE __DATABASE_COMMON_SCHEMA__.right ADD COLUMN IF NOT EXISTS rights JSON; \ No newline at end of file