From 3a65c4c073e8affb3346411688c2aa6b31b74f20 Mon Sep 17 00:00:00 2001 From: Alex Dryden Date: Mon, 17 Jun 2024 15:48:57 -0400 Subject: [PATCH 1/5] Add skeleton REST api create feature --- src/Api/Adapter/AbstractTeamEntityAdapter.php | 210 ++++++++++++++++++ src/Api/Adapter/TeamResourceAdapter.php | 151 +++++++++++-- src/Mvc/Controller/Plugin/TeamAuth.php | 65 ++++-- 3 files changed, 387 insertions(+), 39 deletions(-) create mode 100644 src/Api/Adapter/AbstractTeamEntityAdapter.php diff --git a/src/Api/Adapter/AbstractTeamEntityAdapter.php b/src/Api/Adapter/AbstractTeamEntityAdapter.php new file mode 100644 index 0000000..7e0a6a1 --- /dev/null +++ b/src/Api/Adapter/AbstractTeamEntityAdapter.php @@ -0,0 +1,210 @@ +getOperation()){ +// //validate correct payload data exists +// if(!$request->getValue('team') || !is_int($request->getValue('team'))){ +// $errorStore->addError('o-module-teams:team', 'Your payload needs to indicate team with a numeric value'); +// } else { +// $team = $this->getEntityManager() +// ->getRepository('Teams\Entity\Team') +// ->findOneBy(['id'=>$request->getValue('team')]); +// if($team){ +// $errorStore->addError('o-module-teams:team', new Message( +// 'A team with id %s does not exist.', // @translate +// $request->getValue('team') )); +// +// } +// } +// if(!$request->getValue('resource') || !is_int($request->getValue('resource'))){ +// $errorStore->addError('o-module-teams:team', 'Your payload needs to indicate resource with a numeric value'); +// } +// +// //validate team and resource exist +// +// +// +// +// } +// if ($errorStore->hasErrors()) { +// $validationException = new Exception\ValidationException; +// $validationException->setErrorStore($errorStore); +// throw $validationException; +// } +// +// $entity_index = 'o:' . $this->getMappedEntityName(); +// $services = $this->getServiceLocator(); +// $logger = $services->get('Omeka\Logger'); +// //does the request contain a team and resource +// $data = []; +// if (Request::CREATE === $request->getOperation()){ +// $data = $request->getContent(); +// } elseif (Request::DELETE === $request->getOperation()) { +// $data = $request->getId(); +// } +// if (!is_array($data)){ +// $errorStore->addError('o:id', new Message('The %s id must be an array.', $this->getResourceName())); // @translate +// return; +// } +// if (!array_key_exists('o:team',$data)){ +// $errorStore->addError('o:team', 'The request lacks a team id.'); // @translate +// ->err('T$loggerhe request lacks a team id.'); +// +// } +// if (!array_key_exists($entity_index,$data)){ +// $errorStore->addError($entity_index, new Message('The request lacks a %s id.',$this->getMappedEntityName())); // @translate +// } +// +// +// //is that id a team +// +// $team = $this->getEntityManager() +// ->getRepository('Teams\Entity\Team') +// ->findOneBy(['id'=>$data['o:team']]); +// if (! $team) { +// $errorStore->addError('o:team', new Message( +// 'A team with id = "%s" can not be found', // @translate +// $data['o:team'] +// )); +// } +// +// //is that a resource +// $mapped_entity = $this->getEntityManager() +// ->find($this->getMappedEntityClass(), $data[$entity_index]); +// +// if (! $mapped_entity) { +// $errorStore->addError($entity_index, new Message( +// 'A %1$s with id = "%2$s" can not be found', // @translate +// $this->getMappedEntityName(), +// $data[$entity_index] +// )); +// } +// +// //does the team resource already exist +// if ($team && $mapped_entity){ +// if (Request::CREATE === $request->getOperation() && $this->teamEntityExists($team, $mapped_entity)){ +// $errorStore->addError('o:resource', 'That team resource already exists.'); // @translate +// } elseif (Request::DELETE === $request->getOperation() && ! $this->teamEntityExists($team, $mapped_entity)){ +// $errorStore->addError('o:resource', 'That team resource you are trying to delete does not exists.'); // @translate +// } +// } + + } + + //PHP 8 can implement multiple types as type hint: Resource|User|ResourceTemplate|Asset|Site + public function teamEntityExists(Team $team, EntityInterface $entity ) + { + $entity_name = $this->getMappedEntityName(); + return $this->getEntityManager() + ->getRepository($this->getEntityClass()) + ->findOneBy(['team'=>$team->getId(), $entity_name => $entity->getId()]); + + } + + public function teamAuthority($request, $team, $user, $resource=null) + { + $em = $this->getEntityManager(); + $user = $this->getServiceLocator()->get('Omeka\AuthenticationService')->getIdentity(); + $operation = $request->getOperation(); + $services = $this->getServiceLocator(); + $logger = $services->get('Omeka\Logger'); + $teamAuth = new TeamAuth($em, $logger); + $teamId = 0; + if (array_key_exists('team',$request->getContent())){ + $teamId = $request->getContent()['team']; + } elseif (array_key_exists('o:team', $request->getContent())){ + $teamId = $request->getContent()['o:team']; + } + + if (! $teamAuth->teamAuthorized($user, $operation, 'resource', $teamId)){ + throw new Exception\PermissionDeniedException(sprintf( + $this->getTranslator()->translate( + 'Permission denied for the current user to %1$s a team resource in team_id = %2$s.' + ), + $operation, $request->getContent()['o:team']) + ); + } + } + + public function read(Request $request) + { + AbstractAdapter::read($request); + } + + public function batchCreate(Request $request) + { + AbstractAdapter::batchCreate($request); + } + + public function batchDelete(Request $request) + { + AbstractAdapter::batchDelete($request); + } + + + public function update(Request $request) + { + AbstractAdapter::update($request); + } + + public function batchUpdate(Request $request) + { + AbstractAdapter::batchUpdate($request); + } + + + + + +} \ No newline at end of file diff --git a/src/Api/Adapter/TeamResourceAdapter.php b/src/Api/Adapter/TeamResourceAdapter.php index 73db0ee..84210ed 100644 --- a/src/Api/Adapter/TeamResourceAdapter.php +++ b/src/Api/Adapter/TeamResourceAdapter.php @@ -4,18 +4,24 @@ use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; use Laminas\EventManager\Event; +use NumericDataTypes\DataType\Integer; use Omeka\Api\Adapter\AbstractAdapter; use Omeka\Api\Adapter\AbstractEntityAdapter; use Omeka\Api\Exception; use Omeka\Api\Request; use Omeka\Api\Response; use Omeka\Entity\EntityInterface; +use Omeka\Entity\Item; +use Omeka\Entity\Resource; +use Omeka\Entity\User; use Omeka\Stdlib\ErrorStore; +use Omeka\Stdlib\Message; use Teams\Api\Representation\TeamResourceRepresentation; use Teams\Entity\TeamResource; +use Teams\Mvc\Controller\Plugin\TeamAuth; //legacy from deciding how much of the module to expose to the API -class TeamResourceAdapter extends AbstractEntityAdapter +class TeamResourceAdapter extends AbstractTeamEntityAdapter { protected $sortFields = [ 'resource_id' => 'resource_id', @@ -38,6 +44,16 @@ public function getEntityClass() return TeamResource::class; } + public function getMappedEntityClass() + { + return Resource::class; + } + + public function getMappedEntityName() + { + return 'resource'; + } + public function hydrate( Request $request, EntityInterface $entity, @@ -205,13 +221,13 @@ public function search(Request $request) return $response; } - public function findEntity($criteria, $request = null) + public function findMappedEntity($criteria, $request = null) { if (!is_array($criteria)) { $criteria = ['id' => $criteria]; } - $entityClass = $this->getEntityClass(); + $entityClass = $this->getMappedEntityClass(); $this->index = 0; $qb = $this->getEntityManager()->createQueryBuilder(); $qb->select('omeka_root')->from($entityClass, 'omeka_root'); @@ -229,14 +245,7 @@ public function findEntity($criteria, $request = null) ]); $this->getEventManager()->triggerEvent($event); - $entity = $qb->getQuery()->getOneOrNullResult(); - if (!$entity) { - throw new Exception\NotFoundException(sprintf( - $this->getTranslator()->translate('%1$s entity with criteria %2$s not found'), - $entityClass, json_encode($criteria) - )); - } - return $entity; + return $qb->getQuery()->getOneOrNullResult(); } public function read(Request $request) @@ -245,17 +254,42 @@ public function read(Request $request) } public function create(Request $request) { - AbstractAdapter::create($request); + + if ($request->getValue('batch')){ + $this->batchCreate($request); + } + + $user = $this->getServiceLocator()->get('Omeka\AuthenticationService')->getIdentity(); + + + $this->validateRequest($request, new ErrorStore()); + $this->teamAuthority($request, $request->getValue('team'), $user); + if (!$this->resourceAuthority($request->getValue('resource'),$user)){ + throw new Exception\PermissionDeniedException('Permission denied for the current user to add this resource to a team.' + ); + } + + if ($request->getValue('team')){ + $team = $request->getValue('team'); + } + $team = $request->getContent(); + + throw new Exception\OperationNotImplementedException(sprintf( + $this->getTranslator()->translate( + 'The %1$s adapter does not implement the search operation.' + ), + $request->getValue('team') + )); } public function batchCreate(Request $request) { - AbstractAdapter::batchCreate($request); + AbstractEntityAdapter::batchCreate($request); } public function update(Request $request) { - AbstractAdapter::batchCreate($request); + AbstractAdapter::update($request); } public function batchUpdate(Request $request) @@ -275,10 +309,93 @@ public function batchDelete(Request $request) public function validateRequest(Request $request, ErrorStore $errorStore) { - $data = $request->getContent(); - if (array_key_exists('team', $data) && array_key_exists('resource', $data)) { - $result = $this->validateName($data['o:name'], $errorStore); + if (Request::CREATE === $request->getOperation()){ + //validate payload data refers to real entities + + //validate team data + if(!$request->getValue('team') || !is_int($request->getValue('team'))){ + $errorStore->addError('o-module-teams:team', 'Your payload needs to indicate team with a numeric value'); + } else { + $team = $this->getEntityManager() + ->getRepository('Teams\Entity\Team') + ->findOneBy(['id'=>$request->getValue('team')]); + if(is_null($team)){ + $errorStore->addError('o-module-teams:team', new Message( + 'A team with id %s does not exist.', // @translate + $request->getValue('team') )); + } + } + + //validate resource data + if(!$request->getValue('resource') || !is_int($request->getValue('resource'))){ + $errorStore->addError('o-module-teams:team', 'Your payload needs to indicate resource with a numeric value'); + } else { + $mappedEntity = $this->findMappedEntity($request->getValue('resource'), $request); + if(is_null($mappedEntity)){ + $errorStore->addError('o-module-teams:team', new Message( + 'A resource with id %s does not exist.', // @translate + $request->getValue('resource') )); + } + } + } + if ($errorStore->hasErrors()) { + $validationException = new Exception\ValidationException; + $validationException->setErrorStore($errorStore); + throw $validationException; + } + + } + public function teamAuthority($request, $team, $user, $resource=null) + { + $em = $this->getEntityManager(); + $operation = $request->getOperation(); + $logger = $this->getServiceLocator()->get('Omeka\Logger'); + $teamAuth = new TeamAuth($em, $logger); + if (! $teamAuth->teamAuthorized($user, $operation, 'resource', $team)){ + throw new Exception\PermissionDeniedException(sprintf( + $this->getTranslator()->translate( + 'Permission denied for the current user to %1$s a team resource in team_id = %2$s.' + ), + $operation, $team) + ); } } + /** + * @param $request + * @return void + * + * Does the user have the authority to modify the resource + */ + public function resourceAuthority($resource, User $user ):bool + { + + //if the resource belongs to any team where the user has resource authority, or if the resource belongs to no team + + //iterate through the teams of the resource + + $resourceTeams = $this->getEntityManager() + ->getRepository('Teams\Entity\TeamResource') + ->findBy(['resource'=>$resource]); + if (!$resourceTeams){ + return true; + } else { + $userTeams = $this->getEntityManager() + ->getRepository('Teams\Entity\TeamUser') + ->findBy(['user'=>$user->getId()]); + + //if the user has a resource permission in any team the resource belongs to, return true + foreach ($resourceTeams as $resourceTeam) { + $resourceTeamId = $resourceTeam->getTeam()->getId(); + foreach($userTeams as $userTeam){ + if ($resourceTeamId == $userTeam->getTeam()->getId()){ + if ($userTeam->getRole()->getCanAddItems()){ + return true; + } + } + } + } + } + return false; + } } diff --git a/src/Mvc/Controller/Plugin/TeamAuth.php b/src/Mvc/Controller/Plugin/TeamAuth.php index 733905c..2982300 100644 --- a/src/Mvc/Controller/Plugin/TeamAuth.php +++ b/src/Mvc/Controller/Plugin/TeamAuth.php @@ -6,13 +6,14 @@ use Laminas\Mvc\Controller\Plugin\AbstractPlugin; use \Omeka\Entity\User; use Omeka\Mvc\Controller\Plugin\Logger; +use Omeka\Stdlib\ErrorStore; /** * Controller plugin for authorize the current user. */ class TeamAuth extends AbstractPlugin { - public $actions = ['add', 'delete', 'update']; + public $actions = ['add', 'create','delete', 'update']; public $domains = ['resource', 'team', 'site', 'team_user', 'role']; /** @@ -46,6 +47,9 @@ public function isGlobAdmin(User $user): bool public function teamAuthorized(User $user, string $action, string $domain, int $context=0): bool { + if ($action=='create'){ + $action = 'add'; + } //validate inputs if (!in_array($action, $this->actions)) { @@ -64,7 +68,6 @@ public function teamAuthorized(User $user, string $action, string $domain, int $ ) ); } -// $this->logger->err(get_class($this->identity())); if ($this->isGlobAdmin($user)) { return true; @@ -75,42 +78,60 @@ public function teamAuthorized(User $user, string $action, string $domain, int $ $authorized = false; - //if the user has a current team - if ($has_role = $em->getRepository('Teams\Entity\TeamUser') - ->findOneBy(['is_current' => true, 'user'=>$user_id]) - ) { - $current_role = $has_role->getRole(); + //see if the user has a role in the supplied team + if ($context>0){ + $teamUser = $em->getRepository('Teams\Entity\TeamUser') + ->findOneBy(['team' => $context, 'user'=>$user_id]); + if ($teamUser) { + $role = $teamUser->getRole(); + } else { + return false; + } + // if no team is supplied get their current team + } else { + $currentTeam = $em->getRepository('Teams\Entity\TeamUser') + ->findOneBy(['is_current' => true, 'user'=>$user_id]); + if ($currentTeam){ + $role = $currentTeam->getRole(); + } else { + return false; + } + } - //go through each domain and determine if user is authorized for actions in that domain + //go through each domain and determine if user is authorized for actions in that domain //only the global admin can create, delete or modify teams if ($domain == 'team' || $domain == 'role') { - $authorized = $this->isGlobAdmin(); + $authorized = $this->isGlobAdmin($user); } //if they can manage users of the team (including their role) elseif ($domain == 'team_user') { - $authorized = $current_role->getCanAddUsers(); + $authorized = $role->getCanAddUsers(); } elseif ($domain == 'resource') { if ($action == 'add') { - $authorized = $current_role->getCanAddItems(); + $authorized = $role->getCanAddItems(); } elseif ($action == 'update') { - $authorized = $current_role->getCanModifyResources(); + $authorized = $role->getCanModifyResources(); } elseif ($action == 'delete') { - $authorized = $current_role->getCanDeleteResources(); - } - } elseif ($domain == 'site') { - - //only the global admin can add and delete sites - if ($action == 'add' || $action == 'delete') { - $authorized = $this->isGlobAdmin(); - } elseif ($action == 'update') { - $authorized = $current_role->getCanAddSitePages(); + $authorized = $role->getCanDeleteResources(); } } +// elseif ($domain == 'site') { +// +// //only the global admin can add and delete sites +// if ($action == 'add' || $action == 'delete') { +// $authorized = $this->isGlobAdmin($user); +// } elseif ($action == 'update') { +// $authorized = $role->getCanAddSitePages(); +// } +// } + if ($authorized) { + return true; + } else { + return false; } - return $authorized; } } From 7f29837bc267002060ec4f762d141a33f7a7beb2 Mon Sep 17 00:00:00 2001 From: Alex Dryden Date: Mon, 17 Jun 2024 16:21:49 -0400 Subject: [PATCH 2/5] Persist new team resource --- src/Api/Adapter/TeamResourceAdapter.php | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Api/Adapter/TeamResourceAdapter.php b/src/Api/Adapter/TeamResourceAdapter.php index 84210ed..30c055c 100644 --- a/src/Api/Adapter/TeamResourceAdapter.php +++ b/src/Api/Adapter/TeamResourceAdapter.php @@ -14,6 +14,7 @@ use Omeka\Entity\Item; use Omeka\Entity\Resource; use Omeka\Entity\User; +use Omeka\Mvc\Controller\Plugin\Api; use Omeka\Stdlib\ErrorStore; use Omeka\Stdlib\Message; use Teams\Api\Representation\TeamResourceRepresentation; @@ -261,25 +262,27 @@ public function create(Request $request) $user = $this->getServiceLocator()->get('Omeka\AuthenticationService')->getIdentity(); - $this->validateRequest($request, new ErrorStore()); + $team = $request->getValue('team'); + $resource = $request->getValue('resource'); $this->teamAuthority($request, $request->getValue('team'), $user); if (!$this->resourceAuthority($request->getValue('resource'),$user)){ throw new Exception\PermissionDeniedException('Permission denied for the current user to add this resource to a team.' ); } - - if ($request->getValue('team')){ - $team = $request->getValue('team'); + $teamEntity = $this->getEntityManager()->getRepository('Teams\Entity\Team')->findOneBy(['id'=>$team]); + $resourceEntity = $this->getEntityManager()->getRepository('Omeka\Entity\Resource')->findOneBy(['id'=>$resource]); + $teamResource = new TeamResource($teamEntity, $resourceEntity); + $this->getEntityManager()->persist($teamResource); + if ($request->getOption('flushEntityManager', true)) { + $this->getEntityManager()->flush(); + // Refresh the entity on the chance that it contains associations + // that have not been loaded. + $this->getEntityManager()->refresh($teamResource); } - $team = $request->getContent(); - - throw new Exception\OperationNotImplementedException(sprintf( - $this->getTranslator()->translate( - 'The %1$s adapter does not implement the search operation.' - ), - $request->getValue('team') - )); + return new Response($teamResource); + + } public function batchCreate(Request $request) @@ -349,7 +352,6 @@ public function teamAuthority($request, $team, $user, $resource=null) { $em = $this->getEntityManager(); $operation = $request->getOperation(); - $logger = $this->getServiceLocator()->get('Omeka\Logger'); $teamAuth = new TeamAuth($em, $logger); if (! $teamAuth->teamAuthorized($user, $operation, 'resource', $team)){ throw new Exception\PermissionDeniedException(sprintf( @@ -369,11 +371,13 @@ public function teamAuthority($request, $team, $user, $resource=null) */ public function resourceAuthority($resource, User $user ):bool { - //if the resource belongs to any team where the user has resource authority, or if the resource belongs to no team //iterate through the teams of the resource + if ($user->getRole() == 'global_admin'){ + return true; + } $resourceTeams = $this->getEntityManager() ->getRepository('Teams\Entity\TeamResource') ->findBy(['resource'=>$resource]); From 5e68249fb0b8be736394e47f8110dc114dd2c1e9 Mon Sep 17 00:00:00 2001 From: Alex Dryden Date: Mon, 17 Jun 2024 16:47:14 -0400 Subject: [PATCH 3/5] Restore logger for teamAuth() --- src/Api/Adapter/TeamResourceAdapter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Api/Adapter/TeamResourceAdapter.php b/src/Api/Adapter/TeamResourceAdapter.php index 30c055c..e0c182f 100644 --- a/src/Api/Adapter/TeamResourceAdapter.php +++ b/src/Api/Adapter/TeamResourceAdapter.php @@ -352,6 +352,7 @@ public function teamAuthority($request, $team, $user, $resource=null) { $em = $this->getEntityManager(); $operation = $request->getOperation(); + $logger = $this->getServiceLocator()->get('Omeka\Logger'); $teamAuth = new TeamAuth($em, $logger); if (! $teamAuth->teamAuthorized($user, $operation, 'resource', $team)){ throw new Exception\PermissionDeniedException(sprintf( @@ -371,6 +372,7 @@ public function teamAuthority($request, $team, $user, $resource=null) */ public function resourceAuthority($resource, User $user ):bool { + //if the resource belongs to any team where the user has resource authority, or if the resource belongs to no team //iterate through the teams of the resource From 07ccf574596660c5f87ace1dbe02d99d99b0deb7 Mon Sep 17 00:00:00 2001 From: Alex Dryden Date: Mon, 17 Jun 2024 17:23:02 -0400 Subject: [PATCH 4/5] Return not implemented error for batch create --- src/Api/Adapter/TeamResourceAdapter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Api/Adapter/TeamResourceAdapter.php b/src/Api/Adapter/TeamResourceAdapter.php index e0c182f..0dd2a93 100644 --- a/src/Api/Adapter/TeamResourceAdapter.php +++ b/src/Api/Adapter/TeamResourceAdapter.php @@ -1,6 +1,7 @@ Date: Tue, 18 Jun 2024 09:47:40 -0400 Subject: [PATCH 5/5] Tolerate team ids sent as scalar --- Module.php | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Module.php b/Module.php index 1f69285..a247710 100644 --- a/Module.php +++ b/Module.php @@ -1980,10 +1980,8 @@ public function itemCreate(Event $event) $operation = $request->getOperation(); $em = $this->getServiceLocator()->get('Omeka\EntityManager'); - if ($operation == 'create') { $response = $event->getParam('response'); - $resource = $response->getContent(); $team_key = ''; if (array_key_exists('team', $request->getContent())){ @@ -1994,24 +1992,24 @@ public function itemCreate(Event $event) if ($team_key) { $teams = $request->getContent()[$team_key]; - + if (!is_array($teams)){ + $teams = [$teams]; + } //add items to team - foreach ($teams as $team_id): + foreach ($teams as $team_id){ $team = $em->getRepository('Teams\Entity\Team')->findOneBy(['id'=>$team_id]); - $tr = new TeamResource($team, $resource); - $em->persist($tr); - - //if there is media, add those to the team as well - - $media = $resource->getMedia(); - - if (count($media) > 0) { - foreach ($media as $m): - $tr = new TeamResource($team, $m); + $tr = new TeamResource($team, $resource); $em->persist($tr); - endforeach; + + //if there is media, add those to the team as well + $media = $resource->getMedia(); + if (count($media) > 0) { + foreach ($media as $m): + $tr = new TeamResource($team, $m); + $em->persist($tr); + endforeach; + } } - endforeach; $em->flush(); } }