Claudio D'Alicandro
(@ClaudioSThought on twitter)
Backend Dev @Terravision
The Requirements
The Toolbox:
The Symfony security layer
Authentication and authorization
Access COntrol for urls
# app/config/security.yml
security:
# ...
access_control:
- { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
- { path: ^/admin, roles: ROLE_USER_HOST, host: symfony\.com$ }
- { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
- { path: ^/admin, roles: ROLE_USER }
this doesn't help...
access control for Objects
ACL (Access control LIST)
In complex applications, you will often face the problem that access decisions cannot only be based on the person (Token) who is requesting access, but also involve a domain object that access is being requested for. This is where the ACL system comes in.
-- symfony.com
...Interesting...
Using ACL's isn't trivial, and for simpler use cases, it may be overkill.
-- symfony.com
Cheering!
Acl: how to use it?
Must be enabled, and will be persisted outside of our control
# app/config/security.yml
security:
acl:
connection: default
$ php app/console init:acl
public function addCommentAction(Post $post)
{
$comment = new Comment();
// ... setup $form, and submit data
if ($form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($comment);
$entityManager->flush();
// creating the ACL
$aclProvider = $this->get('security.acl.provider');
$objectIdentity = ObjectIdentity::fromDomainObject($comment);
$acl = $aclProvider->createAcl($objectIdentity);
// retrieving the security identity of the currently logged-in user
$securityContext = $this->get('security.context');
$user = $securityContext->getToken()->getUser();
$securityIdentity = UserSecurityIdentity::fromAccount($user);
// grant owner access
$acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
$aclProvider->updateAcl($acl);
}
}
The Controller with ACL
the object
Inside the ACLs we have to define the object identity and Access Control Entries, eachone join with a Security Identity and a Scope among the following:
- Object-scope
- Class-scope
- Object-field-scope
- Class-field-scope
the user
The owner identity is defined in the User Security Identity
(there is also a Role Security Identity)
and there are three levels of ownership
- OPERATOR
- MASTER
- OWNER
Ok great, but:
- Is a considerable effort
- I have to clarify the ownership
- Access control is a domain logic
There is an easier way?
Wait...
RBAC
class RoleVoter implements VoterInterface
{
// ...
}
ACL
class AclVoter implements VoterInterface
{
// ...
}
Maybe there is a solution!
Voters
The voters are simple services that respond to certain questions and abstain when the question asked is not their responsibility.
interface VoterInterface
{
const ACCESS_GRANTED = 1;
const ACCESS_ABSTAIN = 0;
const ACCESS_DENIED = -1;
/**
* [...]
*/
public function supportsAttribute($attribute);
/**
* [...]
*/
public function supportsClass($class);
/**
* [...]
*/
public function vote(TokenInterface $token, $object, array $attributes);
}
Attributes
Questions posed to the security system
class RoleVoter implements VoterInterface
{
private $prefix;
public function __construct($prefix = 'ROLE_')
{
$this->prefix = $prefix;
}
public function supportsAttribute($attribute)
{
return 0 === strpos($attribute, $this->prefix);
}
// ...
}
Decision Strategies
-
affermative (default)
-
unanimous
-
consensus
# app/config/security.yml
security:
access_decision_manager:
# strategy can be: affirmative, unanimous or consensus
strategy: unanimous
Ok, let's use a custom voter to implement this feature!
The controller without any kind of security limitation
class ExampleController extends Controller
{
public function getOrderAction($orderIdentifier)
{
if ($orderView = $this->getOrderView($orderIdentifier)) {
return new JsonResponse($orderView);
}
throw $this->createNotFoundException();
}
private function getOrderView($orderIdentifier)
{
return $this
->get('example.order.use_case.fetch_order_view_from_order_identifier')
->fetch($orderIdentifier);
}
}
the ownership check service
<?php
namespace Example\Order\Service;
use Example\Order\DTO\OrderView;
use Example\Order\Rule\OrderViewOwnerCheck;
use Example\User\Entity\User;
class OrderViewOwnerChecker implements OrderViewOwnerCheckerInterface
{
public function checkUserOwnershipOnOrder(User $user, OrderView $orderView)
{
return OrderViewOwnerCheck::isThisUserOwnerOfThisOrderView($user, $orderView);
}
}
Define attributes
<?php
namespace Example\Order\Security\Attributes;
final class OrderViewAttributes
{
const CAN_READ_THIS_ORDER_VIEW = 'can_read_this_order_view';
const CAN_EDIT_THIS_ORDER_VIEW = 'can_edit_this_order_view';
const CAN_INSERT_THIS_ORDER_VIEW = 'can_insert_this_order_view';
const CAN_DELETE_THIS_ORDER_VIEW = 'can_delete_this_order_view';
}
custom voter
class OrderViewReadVoter implements VoterInterface
{
const ORDER_VIEW_CLASS = 'Example\Order\DTO\OrderView';
private $orderViewOwnerChecker;
public function __construct(OrderViewOwnerCheckerInterface $orderViewOwnerChecker)
{
$this->orderViewOwnerChecker = $orderViewOwnerChecker;
}
public function supportsAttribute($attribute)
{
return in_array($attribute, [
OrderViewAttributes::CAN_READ_THE_ORDER_VIEW
], true);
}
public function supportsClass($class)
{
$reflectionClass = new \ReflectionClass($class);
return $class === self::ORDER_VIEW_CLASS
|| $reflectionClass->isSubclassOf(self::ORDER_VIEW_CLASS);
}
// [...]
}
class OrderViewReadVoter implements VoterInterface
{
// [...]
public function vote(TokenInterface $token, $object, array $attributes)
{
$result = VoterInterface::ACCESS_ABSTAIN;
if (!$object instanceof OrderView) {
return $result;
}
$user = $token->getUser();
if (!is_object($user)) {
return VoterInterface::ACCESS_DENIED;
}
foreach ($attributes as $attribute) {
if ($this->supportsAttribute($attribute)) {
$result = VoterInterface::ACCESS_DENIED;
if ($this->orderViewOwnerChecker->checkUserOwnershipOnOrderView($user, $object)) {
return VoterInterface::ACCESS_GRANTED;
}
}
}
return $result;
}
}
Configuring the voter
<!-- [...] -->
<service class="Example\Infrastructure\ExampleBundle\Voter\OrderViewReadVoter"
id="example_infrastructure.example_bundle.voter.order_view_read_voter">
<argument id="example_order.domain_service.order_view_owner_checker" type="service"/>
<tag name="security.voter" />
</service>
<!-- [...] -->
Ask the question to the security context
class ExampleController extends Controller
{
public function getOrderAction($orderIdentifier)
{
if ($orderView = $this->getOrderView($orderIdentifier)) {
if (!$this->isAuthorized())
{
throw $this->createAccessDeniedException();
}
return new JsonResponse($orderView);
}
throw $this->createNotFoundException();
}
private function isAuthorized()
{
return $this->get('security.context')
->isGranted(
OrderViewAttributes::CAN_READ_THIS_ORDER_VIEW,
$orderView
);
}
private function getOrderView($orderIdentifier)
{
return $this->get('example.order.use_case.fetch_order_view_from_order_identifier')
->fetch($orderIdentifier);
}
}
Well done !!
The future
// Symfony 2.6
$this->denyAccessUnlessGranted('ROLE_EDIT', $item, 'You cannot edit this item.');
// Previous Symfony versions
if (false === $this->get('security.context')->isGranted('ROLE_EDIT', $item)) {
throw $this->createAccessDeniedException('You cannot edit this item.');
}
abstract class AbstractVoter implements VoterInterface
{
public function supportsAttribute($attribute);
public function supportsClass($class);
public function vote(TokenInterface $token, $object, array $attributes);
abstract protected function getSupportedClasses();
abstract protected function getSupportedAttributes();
abstract protected function isGranted($attribute, $object, $user = null);
}
class PostVoter extends AbstractVoter
{
const VIEW = 'view';
const EDIT = 'edit';
protected function getSupportedAttributes()
{
return array(self::VIEW, self::EDIT);
}
protected function getSupportedClasses()
{
return array('Acme\DemoBundle\Entity\Post');
}
protected function isGranted($attribute, $post, $user = null)
{
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return false;
}
// custom business logic to decide if the given user can view
// and/or edit the given post
if ($attribute == self::VIEW && !$post->isPrivate()) {
return true;
}
if ($attribute == self::EDIT && $user->getId() === $post->getOwner()->getId()) {
return true;
}
return false;
}
}
Questions?
Go
AND
VOTE!
on Joind!
https://joind.in/talk/view/12218
The Right to Vote(rs)
By Claudio D'Alicandro
The Right to Vote(rs)
Prima o poi ogni sviluppatore dovrà confrontarsi con la gestione dei premessi su un dato oggetto, limitando le interazioni possibili in base al concetto di possesso. Il layer di sicurezza di symfony apparentemente non copre questa specifica necessità se non mediante l'ACE (Access Control Engine), studiato sicuramente per necessità più complesse. Le ACL sono infatti uno strumento molto complesso, che viene spesso utilizzato per 1% del suo potenziale; il class-scope, l'object-scope, il class-field-scope e l'object-field-scope sono veramente necessari all'interno del nostro dominio? Con i voters possiamo regolare l'accesso ad una determinata azione su una risorsa a partire dall'effettivo possesso della stessa, senza utilizzare le ACL e quindi senza preoccuparci di performance e mantenibilità, senza prenderci carico della gestione di un sistema così complesso, sfruttando una delle gemme nascoste di Symfony2 e mantenendo l'attenzione esclusivamente sulla logica di dominio.
- 3,201