<?php
//------------------------------------------------------------------------------
// src/Security/AttachmentVoter.php
//------------------------------------------------------------------------------
namespace App\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Security\Core\Security;
use Doctrine\Persistence\ManagerRegistry;
use App\Entity\Access;
use App\Entity\APIRest\AccessAPI;
use App\Entity\Config\Config;
use App\Entity\Config\Module;
use App\Entity\Common\Attachment;
use App\Entity\HR\AccessFunction;
use App\Entity\Ikea\ServiceOrder;
use App\Entity\Security\Acl;
use App\Entity\Security\AclPermission;
use App\Services\LogTools;
use App\Services\Config\ModuleTools;
class AttachmentVoter extends Voter
{
//--------------------------------------------------------------------------------
// is_granted constants
const VIEW = "view_attachment";
const DELETE = "delete_attachment";
// Plan.io Task #4084
const EDIT_CONFIDENTIALITY = "edit_confidentiality";
const VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_MISSIONS = "view_confidential_attachments_for_missions";
const VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_INDIVIDUALS = "view_confidential_attachments_for_individuals";
const VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_IKEA_OS = "view_confidential_attachments_for_ikea_service_orders";
const IS_GRANTED_CONSTANTS = array(
self::VIEW,
self::DELETE,
self::EDIT_CONFIDENTIALITY,
self::VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_MISSIONS,
self::VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_INDIVIDUALS,
self::VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_IKEA_OS,
);
//--------------------------------------------------------------------------------
// acl constants
const ACL_PERM_VIEW_MISSION_ATTACHMENT = "mission_view_confidential_attachment";
const ACL_PERM_VIEW_INDIVIDUAL_ATTACHMENT = "ind_view_confidential_attachment";
const ACL_PERM_VIEW_IKEA_OS_ATTACHMENT = "ikea_service_order_view_confidential_attachment";
//--------------------------------------------------------------------------------
public function __construct(AccessDecisionManagerInterface $accessDecisionManager, ManagerRegistry $doctrine, ModuleTools $moduleTools, LogTools $logTools)
{
$this->accessDecisionManager = $accessDecisionManager;
$this->em = $doctrine->getManager();
$this->moduleTools = $moduleTools;
$this->logTools = $logTools;
$this->aclRepository = $this->em->getRepository(Acl::class);
$this->aclPermissionRepository = $this->em->getRepository(AclPermission::class);
}
// Plan.io Task #4453 [See AccessVoter for details]
public function supportsAttribute(string $attribute): bool
{
return in_array($attribute, self::IS_GRANTED_CONSTANTS, true);
}
protected function supports(string $attribute, $subject): bool
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, self::IS_GRANTED_CONSTANTS))
{
return false;
}
// only vote on Attachment objects inside this voter
if ($subject !== null && !$subject instanceof Attachment)
{
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// Plan.io Task #3707
if ($user instanceof AccessAPI)
{
if ($user->getAccess() === null)
{
return false;
}
$user = $user->getAccess();
}
// Plan.io Task #3707
// At this point $user is an object of Access type
// even if the $token->getUser() is AccessAPI
if (!$user instanceof Access)
{
// the user must be logged in; if not, deny access
return false;
}
// The user must have a function; if not deny access
$function = $user->getFunction();
if ($function === null) return false;
// Plan.io Task #3710 : Get current group
$currentGroup = $user->getSocietyGroup();
if ($currentGroup === null)
return false;
$this->currentGroup = $currentGroup;
// Check current group affectation
// or mission sharing situation
if ($subject !== null)
{
if (!$this->checkSocietyGroupAffectation($subject, $currentGroup))
{
return false;
}
// Plan.io Task #3427
if (!$this->checkConfidentialityForAttachment($subject, $user, $function))
{
return false;
}
}
// you know $subject is a Attachment object, thanks to supports
/** @var Attachment $attachment */
$attachment = $subject;
switch ($attribute)
{
case self::VIEW:
{
$answer = $this->canView($token, $attachment, $user, $function);
return $answer;
}
case self::EDIT_CONFIDENTIALITY:
return $this->canEditConfidentiality($token, $attachment, $user, $function);
case self::DELETE:
return $this->canDelete($token, $attachment, $user, $function);
case self::VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_MISSIONS:
return $this->checkConfidentialityForMission($user, $function);
case self::VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_INDIVIDUALS:
return $this->checkConfidentialityForIndividual($user, $function);
case self::VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_IKEA_OS:
return $this->checkConfidentialityForIkeaServiceOrder($user, $function);
}
throw new \LogicException('This code should not be reached!');
}
private function checkSocietyGroupAffectation($attachment, $currentGroup)
{
// Several cases are possible
// 1. currentGroup = attachment.societyGroup
// 2. currentGroup = attachment.mission.societyGroupOwner
// 3. currentGroup = attachment.mission.societyGroupAuthor
// 4. currentGroup = attachment.mission.shareDemand.receiver
// Plan.io Task #4071
// Conditions 2, 3 and 4 should also be adapted
// to attachments shared from parent missions to children
// If attachment belongs to mission.parent
// then attachment.mission is not the correct mission being shared
// We need the child mission ...
// 2.bis. currentGroup = attachment.childMission.societyGroupOwner
// 3.bis. currentGroup = attachment.childMission.societyGroupAuthor
// 4.bis. currentGroup = attachment.childMission.shareDemand.receiver
$childMission = null;
$mission = $attachment->getMission();
if ($mission !== null && $mission->hasChildren())
{
foreach ($mission->getChildren() as $child)
{
if ($child->hasParentAttachment($attachment))
{
$childMission = $child;
break;
}
}
}
// Plan.io Task #4071
// And also adapted to attachments from children which are visible on parents
// We need the parent mission ...
// 2.bis.bis. currentGroup = attachment.parentMission.societyGroupOwner
// 3.bis.bis. currentGroup = attachment.parentMission.societyGroupAuthor
// 4.bis.bis. currentGroup = attachment.parentMission.shareDemand.receiver
$parentMission = null;
$mission = $attachment->getMission();
if ($mission !== null)
{
$parentMission = $mission->getParent();
}
$attachmentSocietyGroup = $attachment->getSocietyGroup();
if ($attachmentSocietyGroup === null)
{
return false;
}
// 1. currentGroup = attachment.societyGroup
if ($currentGroup->equals($attachmentSocietyGroup))
{
return true;
}
// 2. and 3. Check mission
$mission = $attachment->getMission();
if ($mission === null)
{
return false;
}
// 2. currentGroup = attachment.mission.societyGroupOwner
if ($currentGroup->equals($mission->getSocietyGroupOwner()))
{
return true;
}
// 3. currentGroup = attachment.mission.societyGroupAuthor
if ($currentGroup->equals($mission->getSocietyGroupAuthor()))
{
return true;
}
if ($childMission !== null)
{
// 2.bis. currentGroup = attachment.childMission.societyGroupOwner
if ($currentGroup->equals($childMission->getSocietyGroupOwner()))
{
return true;
}
// 3.bis. currentGroup = attachment.childMission.societyGroupAuthor
if ($currentGroup->equals($childMission->getSocietyGroupAuthor()))
{
return true;
}
}
if ($parentMission !== null)
{
// 2.bis.bis. currentGroup = attachment.parentMission.societyGroupOwner
if ($currentGroup->equals($parentMission->getSocietyGroupOwner()))
{
return true;
}
// 3.bis.bis. currentGroup = attachment.parentMission.societyGroupAuthor
if ($currentGroup->equals($parentMission->getSocietyGroupAuthor()))
{
return true;
}
}
// The following conditions must be considered in OR case
// 4 || 4.bis || 4.bis.bis
$stayOnFour = true;
$stayOnFourBis = true;
$stayOnFourBisBis = true;
// 4. currentGroup = attachment.mission.shareDemand.receiver
$demands = $mission->getShareDemandsForReceiverSocietyGroup($currentGroup);
if (count($demands) < 1)
{
$stayOnFour = false;
}
$childDemands = array();
if ($childMission !== null)
{
// 4.bis. currentGroup = attachment.childMission.shareDemand.receiver
$childDemands = $childMission->getShareDemandsForReceiverSocietyGroup($currentGroup);
if (count($childDemands) < 1)
{
$stayOnFourBis = false;
}
}
$parentDemands = array();
if ($parentMission !== null)
{
// 4.bis.bis. currentGroup = attachment.parentMission.shareDemand.receiver
$parentDemands = $parentMission->getShareDemandsForReceiverSocietyGroup($currentGroup);
if (count($parentDemands) < 1)
{
$stayOnFourBisBis = false;
}
}
// The following conditions must be considered in OR case
// 4 || 4.bis || 4.bis.bis
if (!($stayOnFour || $stayOnFourBis || $stayOnFourBisBis))
{
return false;
}
// Plan.io Task #4071 : Mix $demands and $childDemands and $parentDemands
if ($demands instanceof ArrayCollection) $demands = $demands->toArray();
if ($childDemands instanceof ArrayCollection) $childDemands = $childDemands->toArray();
if ($parentDemands instanceof ArrayCollection) $parentDemands = $parentDemands->toArray();
$demands = array_merge($demands, $childDemands);
$demands = array_merge($demands, $parentDemands);
// Only allow accessing files for share demands that are in the default status or accepted
// Expiration should be checked separetly, and before anything else
foreach ($demands as $demand)
{
if ($demand->isExpired())
{
continue;
}
if ($demand->isAnnulled())
{
continue;
}
if ($demand->isRefused())
{
continue;
}
// If we are here it means that we have a default / accepted demand for this file
if ($demand->isDefault())
{
return true;
}
if ($demand->isAccepted())
{
return true;
}
}
// If we are here all hope is lost
return false;
}
// Plan.io Task #3427
// This checks if the user can access the confidential attachments for missions
private function checkConfidentialityForMission(Access $user, AccessFunction $function)
{
// Get Acl_Permission
$aclPerm = $this->aclPermissionRepository->findOneByName(self::ACL_PERM_VIEW_MISSION_ATTACHMENT);
if ($aclPerm === null) return false;
// Get Acl
$acl = $this->aclRepository->findOneBy(array(
'function' => $function,
'permission' => $aclPerm
));
if ($acl === null) return false;
// Since only one acl type can exist
// we can return the result of the acl_permission
return $acl->getValue();
}
// Plan.io Task #3427
// This checks if the user can access the confidential attachments for clients
private function checkConfidentialityForIndividual(Access $user, AccessFunction $function)
{
// Get Acl_Permission
$aclPerm = $this->aclPermissionRepository->findOneByName(self::ACL_PERM_VIEW_INDIVIDUAL_ATTACHMENT);
if ($aclPerm === null) return false;
// Get Acl
$acl = $this->aclRepository->findOneBy(array(
'function' => $function,
'permission' => $aclPerm
));
if ($acl === null) return false;
// Since only one acl type can exist
// we can return the result of the acl_permission
return $acl->getValue();
}
// Plan.io Task #3939
// This checks if the user can access the confidential attachments for ikea service orders
private function checkConfidentialityForIkeaServiceOrder(Access $user, AccessFunction $function)
{
// Get Acl_Permission
$aclPerm = $this->aclPermissionRepository->findOneByName(self::ACL_PERM_VIEW_IKEA_OS_ATTACHMENT);
if ($aclPerm === null) return false;
// Get Acl
$acl = $this->aclRepository->findOneBy(array(
'function' => $function,
'permission' => $aclPerm
));
if ($acl === null) return false;
// Since only one acl type can exist
// we can return the result of the acl_permission
return $acl->getValue();
}
// Plan.io Task #3427, modified by #3939
// This checks if a given attachment is available to the user
private function checkConfidentialityForAttachment(Attachment $attachment, Access $user, AccessFunction $function)
{
if ($attachment->isNotConfidential())
{
// Confidentiality does not apply
return true;
}
$mission = $attachment->getMission();
$client = $attachment->getClient();
// Plan.io Task #3939
// Only fetch the IkeaServiceOrder if both the client and the mission are null
$ikeaServiceOrder = null;
if ($client === null && $mission === null)
{
$ikeaOrderNumber = $attachment->getIkeaOrderNumber();
if (!empty($ikeaOrderNumber))
{
$societyGroup = $attachment->getSocietyGroup();
if ($societyGroup !== null)
{
$ikeaServiceOrder = $this->em->getRepository(ServiceOrder::class)
->findOneBy(array(
'societyGroup' => $societyGroup,
'orderNumber' => $ikeaOrderNumber,
));
}
}
}
// Plan.io Task #3939
// Third possible case : IkeaServiceOrder :: client
// in this case the attachment does not have neither a client, nor a mission attached
// however it is fetchd when viewing a task for example
// Quick fix : fetch the client from the IkeaServiceOrder
// Longterm solution : add VIEW_CONFIDENTIAL_ATTACHMENTS_FOR_IKEA_SERVICE_ORDERS (TODO)
// if ($client === null)
// {
// $ikeaOrderNumber = $attachment->getIkeaOrderNumber();
// if (!empty($ikeaOrderNumber))
// {
// $societyGroup = $attachment->getSocietyGroup();
// if ($societyGroup !== null)
// {
// $ikeaServiceOrder = $this->em->getRepository(ServiceOrder::class)
// ->findOneBy(array(
// 'societyGroup' => $societyGroup,
// 'orderNumber' => $ikeaOrderNumber,
// ));
// if ($ikeaServiceOrder !== null)
// {
// $client = $ikeaServiceOrder->getClient();
// }
// }
// }
// }
if ($mission === null && $client === null && $ikeaServiceOrder === null)
{
// Confidentiality does not apply
return true;
}
// This is the special case when both the client and the mission are null
// This means that we are viewing files that are only affected to the Ikea::ServiceOrder
if ($ikeaServiceOrder !== null)
{
// Get Acl_Permission
$aclPerm = $this->aclPermissionRepository->findOneByName(self::ACL_PERM_VIEW_IKEA_OS_ATTACHMENT);
if ($aclPerm === null) return false;
// Get Acl
$acl = $this->aclRepository->findOneBy(array(
'function' => $function,
'permission' => $aclPerm
));
if ($acl === null) return false;
// Since only one acl type can exist
// we can return the result of the acl_permission
return $acl->getValue();
}
if ($mission !== null)
{
// Get Acl_Permission
$aclPerm = $this->aclPermissionRepository->findOneByName(self::ACL_PERM_VIEW_MISSION_ATTACHMENT);
if ($aclPerm === null) return false;
// Get Acl
$acl = $this->aclRepository->findOneBy(array(
'function' => $function,
'permission' => $aclPerm
));
if ($acl === null) return false;
// Since only one acl type can exist
// we can return the result of the acl_permission
return $acl->getValue();
}
if ($client !== null)
{
$individual = $client->getIndividual();
if ($individual === null)
{
// Confidentiality does not apply
return true;
}
// Get Acl_Permission
$aclPerm = $this->aclPermissionRepository->findOneByName(self::ACL_PERM_VIEW_INDIVIDUAL_ATTACHMENT);
if ($aclPerm === null) return false;
// Get Acl
$acl = $this->aclRepository->findOneBy(array(
'function' => $function,
'permission' => $aclPerm
));
if ($acl === null) return false;
// Since only one acl type can exist
// we can return the result of the acl_permission
return $acl->getValue();
}
// Actually this code should never be reached ;)
// Plan.io Task #3939
// If this code should never be reached, just return false
return false;
// return true;
}
private function canView($token, Attachment $attachment, Access $user, AccessFunction $function)
{
// If the Attachment belongs to a HumanResource,
// check permission for that first
if ($attachment->getHumanResource() !== null)
{
if (!$this->accessDecisionManager->decide($token, ['list_human_resource_attachments'], $attachment->getHumanResource()))
{
return false;
}
}
return $this->checkConfidentialityForAttachment($attachment, $user, $function);
return true;
}
// Plan.io Task #4084
private function canEditConfidentiality($token, Attachment $attachment, Access $user, AccessFunction $function)
{
// First things first : in order to edit it, one should be able to view it
if (!$this->canView($token, $attachment, $user, $function))
{
return false;
}
// Plan.io Task #4071 : Deny changing confidentiality on parentAttachments
if ($attachment->isParentAttachment())
{
return false;
}
// Deny changing confidentiality if the Attachment belongs to a shared Mission
// and the Attachment was created by the societyGroupAuthor
// $currentGroup is the one trying to change the confidentiality
// $societyGroup is the author of the Attachment
// They should match
$societyGroup = $attachment->getSocietyGroup();
$mission = $attachment->getMission();
if ($mission !== null && $mission->isShared())
{
if (!$this->currentGroup->equals($societyGroup))
{
return false;
}
}
// All looks good
return true;
}
private function canDelete(TokenInterface $token, Attachment $attachment, Access $user, AccessFunction $function)
{
// Plan.io Task #3621
if ($attachment->cannotBeDeleted())
{
return false;
}
if ($attachment->getMission() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_mission'], $attachment->getMission()))
{
return true;
}
// Plan.io Task #4071 : Make an exception for children of Mission
// No longer needed, but one never knows, so keep the code just in case
// $parent = $attachment->getMission()->getParent();
// if ($parent !== null)
// {
// if ($this->accessDecisionManager->decide($token, ['edit_mission'], $parent))
// {
// return true;
// }
// }
}
if ($attachment->getDevis() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_devis'], $attachment->getDevis()))
{
return true;
}
}
if ($attachment->getClient() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_client'], $attachment->getClient()))
{
return true;
}
}
if ($attachment->getSupplier() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_supplier'], $attachment->getSupplier()))
{
return true;
}
}
if ($attachment->getHumanResource() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_human_resource'], $attachment->getHumanResource()))
{
return true;
}
}
if ($attachment->getCost() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_cost'], $attachment->getCost()))
{
return true;
}
}
if ($attachment->getCommand() !== null)
{
// Plan.io Task #4036
// Deny Attachments for PunchOut Commands
if ($attachment->getCommand()->isPunchOut())
{
return false;
}
if ($this->accessDecisionManager->decide($token, ['edit_command'], $attachment->getCommand()))
{
return true;
}
}
if ($attachment->getApplication() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_application'], $attachment->getApplication()))
{
return true;
}
}
// Equimpment related stuff
if ($attachment->getEquipment() !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_equipment'], $attachment->getEquipment()))
{
return true;
}
}
if ($attachment->getVehicleMaintenance() !== null)
{
$vehicle = $attachment->getVehicleMaintenance()->getVehicle();
if ($vehicle !== null)
{
$equipment = $vehicle->getEquipment();
if ($equipment !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_equipment'], $equipment))
{
return true;
}
}
}
}
if ($attachment->getDamage() !== null)
{
$equipment = $attachment->getDamage()->getEquipment();
if ($equipment !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_equipment'], $equipment))
{
return true;
}
}
}
// Plan.io Task #3621
if (!empty($attachment->getIkeaOrderNumber()))
{
// Get one ServiceOrder to test the permissions
$serviceOrder = $this->em->getRepository(ServiceOrder::class)->findOneBy(array(
'orderNumber' => $attachment->getIkeaOrderNumber(),
'societyGroup' => $attachment->getSocietyGroup(),
));
if ($serviceOrder !== null)
{
if ($this->accessDecisionManager->decide($token, ['edit_ikea_service_order'], $serviceOrder))
{
return true;
}
}
}
// Plan.io Task #3592 #4177
if (!empty($attachment->getWebappDocumentMultiple()))
{
// Only check author at this point
$author = $attachment->getAuthor();
if ($author === null)
{
return false;
}
if ($author->equals($user))
{
return true;
}
}
return false;
}
}