diff --git a/module/Organizations/config/module.config.php b/module/Organizations/config/module.config.php index e67bcdc72..98c92bc53 100644 --- a/module/Organizations/config/module.config.php +++ b/module/Organizations/config/module.config.php @@ -151,11 +151,13 @@ 'allow' => array( 'route/lang/organizations', 'Organizations/InviteEmployee', + 'Entity/Organization' => [ 'edit' => 'Organizations/Write' ], ), ), ), 'assertions' => array( 'invokables' => array( + 'Organizations/Write' => 'Organizations\Acl\Assertion\WriteAssertion', ), ), ), diff --git a/module/Organizations/src/Organizations/Acl/Assertion/WriteAssertion.php b/module/Organizations/src/Organizations/Acl/Assertion/WriteAssertion.php new file mode 100644 index 000000000..09af08b41 --- /dev/null +++ b/module/Organizations/src/Organizations/Acl/Assertion/WriteAssertion.php @@ -0,0 +1,36 @@ + + */ + +/** */ +namespace Organizations\Acl\Assertion; + +use Auth\Entity\UserInterface; +use Core\Entity\PermissionsInterface; +use Organizations\Entity\OrganizationInterface; +use Zend\Permissions\Acl\Acl; +use Zend\Permissions\Acl\Assertion\AssertionInterface; +use Zend\Permissions\Acl\Resource\ResourceInterface; +use Zend\Permissions\Acl\Role\RoleInterface; + +/** + * ${CARET} + * + * @author Mathias Gelhausen + * @todo write test + */ +class WriteAssertion implements AssertionInterface +{ + public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null) + { + return 'edit' == $privilege + && $role instanceOf UserInterface + && $resource instanceOf OrganizationInterface + && $resource->getPermissions()->isGranted($role, PermissionsInterface::PERMISSION_CHANGE); + } +} \ No newline at end of file diff --git a/module/Organizations/src/Organizations/Controller/IndexController.php b/module/Organizations/src/Organizations/Controller/IndexController.php index c430ef4f9..0570ccef9 100644 --- a/module/Organizations/src/Organizations/Controller/IndexController.php +++ b/module/Organizations/src/Organizations/Controller/IndexController.php @@ -12,6 +12,7 @@ use Core\Entity\Collection\ArrayCollection; use Core\Form\SummaryForm; +use Organizations\Exception\MissingParentOrganizationException; use Zend\Mvc\Controller\AbstractActionController; use Organizations\Repository; use Organizations\Form; @@ -124,7 +125,7 @@ public function editAction() /* @var $handler \Organizations\Controller\Plugin\GetOrganizationHandler */ $handler = $this->plugin('Organizations/GetOrganizationHandler'); $org = $handler->process($this->params(), true); - } catch (\RuntimeException $e) { + } catch (MissingParentOrganizationException $e) { return $this->getErrorViewModel('no-parent'); } diff --git a/module/Organizations/src/Organizations/Controller/Plugin/GetOrganizationHandler.php b/module/Organizations/src/Organizations/Controller/Plugin/GetOrganizationHandler.php index 12925bbaf..68d31f26a 100644 --- a/module/Organizations/src/Organizations/Controller/Plugin/GetOrganizationHandler.php +++ b/module/Organizations/src/Organizations/Controller/Plugin/GetOrganizationHandler.php @@ -11,6 +11,7 @@ /** */ namespace Organizations\Controller\Plugin; +use Organizations\Exception\MissingParentOrganizationException; use Zend\Mvc\Controller\Plugin\AbstractPlugin; use Core\Repository\RepositoryService; use Auth\AuthenticationService; @@ -94,7 +95,7 @@ public function process(Params $params,$allowDraft = true) /* @var $parent \Organizations\Entity\OrganizationReference */ $parent = $user->getOrganization(); if (!$parent->hasAssociation()) { - throw new \RuntimeException('You cannot create organizations, because you do not belong to a parent organization. Use "User menu -> create my organization" first.'); + throw new MissingParentOrganizationException('You cannot create organizations, because you do not belong to a parent organization. Use "User menu -> create my organization" first.'); } $organization->setParent($parent->getOrganization()); } @@ -109,6 +110,9 @@ public function process(Params $params,$allowDraft = true) if (!$organization) { throw new \RuntimeException('No Organization found with id "' . $organizationId . '"'); } + + $this->acl->check($organization, 'edit'); + return $organization; } } \ No newline at end of file diff --git a/module/Organizations/src/Organizations/Entity/Organization.php b/module/Organizations/src/Organizations/Entity/Organization.php index 4ed6554c5..0287904c8 100644 --- a/module/Organizations/src/Organizations/Entity/Organization.php +++ b/module/Organizations/src/Organizations/Entity/Organization.php @@ -3,7 +3,7 @@ * YAWIK * * @copyright (c) 2013 - 2016 Cross Solution (http://cross-solution.de) - * @license MIT + * @license MIT */ namespace Organizations\Entity; @@ -11,14 +11,15 @@ use Auth\Entity\UserInterface; use Core\Entity\AbstractIdentifiableModificationDateAwareEntity as BaseEntity; use Core\Entity\Collection\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Core\Entity\DraftableEntityInterface; +use Core\Entity\EntityInterface; +use Core\Entity\Hydrator\EntityHydrator; use Core\Entity\Permissions; use Core\Entity\PermissionsInterface; -use Core\Entity\EntityInterface; +use Doctrine\Common\Collections\Collection; +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Zend\Hydrator\HydratorInterface; -use Core\Entity\Hydrator\EntityHydrator; -use Core\Entity\DraftableEntityInterface; +use Zend\Permissions\Acl\Resource\ResourceInterface; /** * The organization. @@ -31,11 +32,14 @@ * }, name="fulltext") * }) * - * @todo write test + * @todo write test * @author Mathias Weitz * @author Mathias Gelhausen */ -class Organization extends BaseEntity implements OrganizationInterface, DraftableEntityInterface +class Organization extends BaseEntity implements + OrganizationInterface, + DraftableEntityInterface, + ResourceInterface { /** * Event name of post construct event. @@ -52,7 +56,7 @@ class Organization extends BaseEntity implements OrganizationInterface, Draftabl * @ODM\Index */ protected $externalId; - + /** * The actual name of the organization. * @@ -60,7 +64,7 @@ class Organization extends BaseEntity implements OrganizationInterface, Draftabl * @ODM\ReferenceOne(targetDocument="\Organizations\Entity\OrganizationName", simple=true, cascade="persist") */ protected $organizationName; - + /** * Only for sorting/searching purposes * @@ -76,7 +80,7 @@ class Organization extends BaseEntity implements OrganizationInterface, Draftabl * @ODM\EmbedOne(targetDocument="\Core\Entity\Permissions") */ protected $permissions; - + /** * primary logo of an organization * @@ -92,11 +96,12 @@ class Organization extends BaseEntity implements OrganizationInterface, Draftabl * @ODM\Boolean */ protected $isDraft = false; - + /** * Organization contact data. * - * @ODM\EmbedOne(targetDocument="\Organizations\Entity\OrganizationContact") */ + * @ODM\EmbedOne(targetDocument="\Organizations\Entity\OrganizationContact") + */ protected $contact; /** @@ -110,7 +115,7 @@ class Organization extends BaseEntity implements OrganizationInterface, Draftabl /** * The parent of this organization. * - * @see setParent() + * @see setParent() * @var OrganizationInterface | null * @ODM\ReferenceOne(targetDocument="\Organizations\Entity\Organization", simple=true, nullable=true) * @since 0.18 @@ -166,11 +171,36 @@ class Organization extends BaseEntity implements OrganizationInterface, Draftabl /** * Default values Workflow * - * @var WorkflowSettingsInterface $workflowSettings; + * @var WorkflowSettingsInterface $workflowSettings ; * @ODM\EmbedOne(targetDocument="\Organizations\Entity\WorkflowSettings") */ protected $workflowSettings; + /** + * Returns the string identifier of the Resource + * + * @return string + */ + public function getResourceId() + { + return 'Entity/Organization'; + } + + + /** + * Gets the organization name + * + * @return string + */ + public function getName() + { + if (empty($this->organizationName)) { + return ''; + } + + return $this->organizationName->name; + } + /** * Sets the parent of an organization * @@ -185,6 +215,15 @@ public function setParent(OrganizationInterface $parent) return $this; } + /** + * @deprecated + * @return array + */ + public function getSearchableProperties() + { + return array(); + } + /** * Gets the parent of an organization * @@ -194,7 +233,17 @@ public function setParent(OrganizationInterface $parent) */ public function getParent($returnSelf = false) { - return $this->parent ?: ($returnSelf ? $this : null); + return $this->parent ? : ($returnSelf ? $this : null); + } + + /** + * Gets the Draft flag + * + * @return bool + */ + public function isDraft() + { + return $this->isDraft; } /** @@ -207,6 +256,20 @@ public function getHiringOrganizations() return $this->hiringOrganizations; } + /** + * Sets the draft flag + * + * @param bool $flag + * + * @return $this + */ + public function setIsDraft($flag) + { + $this->isDraft = (bool) $flag; + + return $this; + } + /** * @return bool */ @@ -215,6 +278,18 @@ public function isHiringOrganization() return null !== $this->parent; } + /** + * Returns true, if the user belongs to the organization. + * + * @param UserInterface $user + * + * @return bool + */ + public function isAssociated(UserInterface $user) + { + return $this->isOwner($user) || $this->isEmployee($user); + } + /** * Sets the external id. * @@ -225,7 +300,20 @@ public function isHiringOrganization() public function setExternalId($externalId) { $this->externalId = $externalId; - return $this; + + return $this; + } + + /** + * Checks, if a User is the owner of an organization + * + * @param UserInterface $user + * + * @return bool + */ + public function isOwner(UserInterface $user) + { + return $this->getUser()->getId() == $user->getId(); } /** @@ -238,8 +326,21 @@ public function getExternalId() return $this->externalId; } + /** + * Returns true, if a User is an employee of the organization + * + * @param UserInterface $user + * + * @return bool + */ + public function isEmployee(UserInterface $user) + { + return $this->refs && in_array($user->getId(), $this->refs->getEmployeeIds()); + } + /** * @todo review this + * * @param HydratorInterface $hydrator * * @return $this @@ -249,6 +350,39 @@ public function setHydrator(HydratorInterface $hydrator) return $this; } + /** + * Updates the organizationsPermissions to allow all employees to view this organization. + * + * In case of a HiringOrganization Permissions are granted to all employees of the parent + * organization. + * + * @ODM\PreUpdate + * @ODM\PrePersist + * @since 0.18 + */ + public function updatePermissions() + { + if ($this->isHiringOrganization()) { + $organization = $this->getParent(); + $owner = $organization->getUser(); + + $this->setUser($owner); + } else { + $organization = $this; + } + + /* @var $employees null | ArrayCollection | \Doctrine\ODM\MongoDB\PersistentCollection */ + $employees = $organization->getEmployees(); + + $perms = $this->getPermissions(); + + foreach ($employees as $emp) { + /* @var $emp \Organizations\Entity\Employee */ + $perms->grant($emp->getUser(), PermissionsInterface::PERMISSION_CHANGE, false); + } + $perms->build(); + } + /** * * @todo review this * @return EntityHydrator @@ -273,6 +407,7 @@ public function setOrganizationName(OrganizationName $organizationName) $this->organizationName = $organizationName; $this->organizationName->refCounterInc()->refCompanyCounterInc(); $this->_organizationName = $organizationName->getName(); + return $this; } @@ -287,28 +422,6 @@ public function getOrganizationName() } - /** - * Gets the organization name - * - * @return string - */ - public function getName() - { - if (empty($this->organizationName)) { - return ''; - } - return $this->organizationName->name; - } - - /** - * @deprecated - * @return array - */ - public function getSearchableProperties() - { - return array(); - } - /** * Gets the Permissions of an organization * @@ -323,6 +436,7 @@ public function getPermissions() } $this->setPermissions($permissions); } + return $this->permissions; } @@ -340,6 +454,7 @@ public function setPermissions(PermissionsInterface $permissions) $permissions->grant($this->user, Permissions::PERMISSION_ALL); } $this->permissions = $permissions; + return $this; } @@ -363,8 +478,8 @@ public function getPermissionsUserIds($type = null) // if we have a user, grant him full access to all associated permissions. $user = $this->getUser(); $spec = $user - ? $spec = array(PermissionsInterface::PERMISSION_ALL => array($this->getUser()->getId())) - : array(); + ? $spec = array(PermissionsInterface::PERMISSION_ALL => array($this->getUser()->getId())) + : array(); if (null === $type || ('Job/Permissions' != $type && 'Application' != $type)) { return $spec; @@ -372,10 +487,10 @@ public function getPermissionsUserIds($type = null) if ('Job/Permissions' == $type) { $change = EmployeePermissionsInterface::JOBS_CHANGE; - $view = EmployeePermissionsInterface::JOBS_VIEW; + $view = EmployeePermissionsInterface::JOBS_VIEW; } else { $change = EmployeePermissionsInterface::APPLICATIONS_CHANGE; - $view = EmployeePermissionsInterface::APPLICATIONS_VIEW; + $view = EmployeePermissionsInterface::APPLICATIONS_VIEW; } $employees = $this->isHiringOrganization() @@ -409,6 +524,7 @@ public function getPermissionsUserIds($type = null) public function setImage(OrganizationImage $image = null) { $this->image = $image; + return $this; } @@ -435,6 +551,7 @@ public function setContact(EntityInterface $contact = null) $contact = new OrganizationContact($contact); } $this->contact = $contact; + return $this; } @@ -448,31 +565,10 @@ public function getContact() if (!$this->contact instanceof OrganizationContact) { $this->contact = new OrganizationContact(); } - return $this->contact; - } - /** - * Gets the Draft flag - * - * @return bool - */ - public function isDraft() - { - return $this->isDraft; + return $this->contact; } - /** - * Sets the draft flag - * - * @param bool $flag - * - * @return $this - */ - public function setIsDraft($flag) - { - $this->isDraft = (bool) $flag; - return $this; - } /** * Gets the default description of an organization. @@ -562,9 +658,11 @@ public function getEmployee($userOrId) * Gets a list of Employees by a user role * * @param string $role + * * @return ArrayCollection */ - public function getEmployeesByRole($role){ + public function getEmployeesByRole($role) + { $employees = new ArrayCollection(); /* @var \Organizations\Entity\Employee $employee */ @@ -573,77 +671,10 @@ public function getEmployeesByRole($role){ $employees->add($employee); } } - return $employees; - } - /** - * Checks, if a User is the owner of an organization - * - * @param UserInterface $user - * - * @return bool - */ - public function isOwner(UserInterface $user) - { - return $this->getUser()->getId() == $user->getId(); - } - - /** - * Returns true, if a User is an employee of the organization - * - * @param UserInterface $user - * - * @return bool - */ - public function isEmployee(UserInterface $user) - { - return $this->refs && in_array($user->getId(), $this->refs->getEmployeeIds()); + return $employees; } - /** - * Returns true, if the user belongs to the organization. - * - * @param UserInterface $user - * - * @return bool - */ - public function isAssociated(UserInterface $user) - { - return $this->isOwner($user) || $this->isEmployee($user); - } - - /** - * Updates the organizationsPermissions to allow all employees to view this organization. - * - * In case of a HiringOrganization Permissions are granted to all employees of the parent - * organization. - * - * @ODM\PreUpdate - * @ODM\PrePersist - * @since 0.18 - */ - public function updatePermissions() - { - if ($this->isHiringOrganization()) { - $organization = $this->getParent(); - $owner = $organization->getUser(); - - $this->setUser($owner); - } else { - $organization = $this; - } - - /* @var $employees null | ArrayCollection | \Doctrine\ODM\MongoDB\PersistentCollection */ - $employees = $organization->getEmployees(); - - $perms = $this->getPermissions(); - - foreach ($employees as $emp) { - /* @var $emp \Organizations\Entity\Employee */ - $perms->grant($emp->getUser(), PermissionsInterface::PERMISSION_CHANGE, false); - } - $perms->build(); - } public function setUser(UserInterface $user) { @@ -652,6 +683,7 @@ public function setUser(UserInterface $user) } $this->user = $user; $this->getPermissions()->grant($user, Permissions::PERMISSION_ALL); + return $this; } @@ -682,9 +714,10 @@ public function getJobs() */ public function getTemplate() { - if (null === $this->template){ + if (null === $this->template) { $this->template = new Template(); } + return $this->template; } @@ -695,7 +728,8 @@ public function getTemplate() */ public function setTemplate(TemplateInterface $template) { - $this->template=$template; + $this->template = $template; + return $this; } @@ -704,10 +738,12 @@ public function setTemplate(TemplateInterface $template) * * @return WorkflowSettings|WorkflowSettingsInterface */ - public function getWorkflowSettings(){ + public function getWorkflowSettings() + { if (null == $this->workflowSettings) { $this->workflowSettings = new WorkflowSettings(); } + return $this->workflowSettings; } @@ -718,8 +754,10 @@ public function getWorkflowSettings(){ * * @return self */ - public function setWorkflowSettings($workflowSettings){ - $this->workflowSettings=$workflowSettings; + public function setWorkflowSettings($workflowSettings) + { + $this->workflowSettings = $workflowSettings; + return $this; } } \ No newline at end of file diff --git a/module/Organizations/src/Organizations/Exception/ExceptionInterface.php b/module/Organizations/src/Organizations/Exception/ExceptionInterface.php new file mode 100644 index 000000000..d74b27677 --- /dev/null +++ b/module/Organizations/src/Organizations/Exception/ExceptionInterface.php @@ -0,0 +1,22 @@ + + */ + +/** */ +namespace Organizations\Exception; + +/** + * Marker interface for Organizations module exception + * + * @author Mathias Gelhausen + * @since 0.25.2 + */ +interface ExceptionInterface +{ + +} \ No newline at end of file diff --git a/module/Organizations/src/Organizations/Exception/MissingParentOrganizationException.php b/module/Organizations/src/Organizations/Exception/MissingParentOrganizationException.php new file mode 100644 index 000000000..3f7570f57 --- /dev/null +++ b/module/Organizations/src/Organizations/Exception/MissingParentOrganizationException.php @@ -0,0 +1,22 @@ + + */ + +/** */ +namespace Organizations\Exception; + +/** + * Exception thrown, if a recruiter tries to create a hiring organization when parent organization is not created yet. + * + * @author Mathias Gelhausen + * @since 0.25.2 + */ +class MissingParentOrganizationException extends \RuntimeException implements ExceptionInterface +{ + +} \ No newline at end of file diff --git a/module/Organizations/test/OrganizationsTest/Acl/Assertion/WriteAssertionTest.php b/module/Organizations/test/OrganizationsTest/Acl/Assertion/WriteAssertionTest.php new file mode 100644 index 000000000..6933c2fe4 --- /dev/null +++ b/module/Organizations/test/OrganizationsTest/Acl/Assertion/WriteAssertionTest.php @@ -0,0 +1,81 @@ + + */ + +/** */ +namespace OrganizationsTest\Acl\Assertion; + +use Auth\Entity\User; +use CoreTestUtils\TestCase\AssertInheritanceTrait; +use \Organizations\Acl\Assertion\WriteAssertion; +use Organizations\Entity\Organization; +use Zend\Permissions\Acl\Acl; +use \Zend\Permissions\Acl\Assertion\AssertionInterface; +use Zend\Permissions\Acl\Resource\GenericResource; +use Zend\Permissions\Acl\Role\GenericRole; + +/** + * Tests for \Organizations\Acl\Assertion\WriteAssertion + * + * @covers \Organizations\Acl\Assertion\WriteAssertion + * @author Mathias Gelhausen + * + */ +class WriteAssertionTest extends \PHPUnit_Framework_TestCase +{ + use AssertInheritanceTrait; + + private $target = WriteAssertion::class; + + private $inheritance = [ AssertionInterface::class ]; + + public function dataProvider() + { + $invalidRole = new GenericRole('test'); + $validRole = new User(); + $validRole->setId('testId'); + $invalidResource = new GenericResource('test'); + $validResourceWoPerms = new Organization(); + $validResourceWPerms = new Organization(); + $validResourceWPerms->setUser($validRole); + + return [ + [ $invalidRole, $invalidResource, 'invalidPrivilege', false ], + [ $validRole, $invalidResource, 'invalid', false ], + [ $invalidRole, $validResourceWoPerms, 'invalid', false ], + [ $invalidRole, $validResourceWPerms, 'invalid', false ], + [ $invalidRole, $invalidResource, 'edit', false ], + [ $validRole, $invalidResource, 'edit', false ], + [ $validRole, $validResourceWPerms, 'invalid', false ], + [ $validRole, $validResourceWoPerms, 'edit', false ], + [ $validRole, $validResourceWPerms, 'edit', true ], + + ]; + } + + /** + * @dataProvider dataProvider + * + * @param $role + * @param $resource + * @param $privilege + * @param $expect + */ + public function testReturnsExpectedResult($role, $resource, $privilege, $expect) + { + $acl = new Acl(); + $result = $this->target->assert($acl, $role, $resource, $privilege); + + if ($expect) { + $this->assertTrue($result); + + } else { + $this->assertFalse($result); + } + } +} \ No newline at end of file diff --git a/module/Organizations/test/OrganizationsTest/Exception/MissingParentOrganizationExceptionTest.php b/module/Organizations/test/OrganizationsTest/Exception/MissingParentOrganizationExceptionTest.php new file mode 100644 index 000000000..154ceef86 --- /dev/null +++ b/module/Organizations/test/OrganizationsTest/Exception/MissingParentOrganizationExceptionTest.php @@ -0,0 +1,31 @@ + + */ + +/** */ +namespace OrganizationsTest\Exception; + +use CoreTestUtils\TestCase\AssertInheritanceTrait; +use Organizations\Exception\ExceptionInterface; +use Organizations\Exception\MissingParentOrganizationException; + +/** + * Tests for \Organizations\Exception\MissingParentOrganizationException + * + * @covers \Organizations\Exception\MissingParentOrganizationException + * @author Mathias Gelhausen + * + */ +class MissingParentOrganizationExceptionTest extends \PHPUnit_Framework_TestCase +{ + use AssertInheritanceTrait; + + private $target = MissingParentOrganizationException::class; + + private $inheritance = [ ExceptionInterface::class ]; +} \ No newline at end of file