Skip to content

Latest commit

 

History

History
358 lines (269 loc) · 12.1 KB

File metadata and controls

358 lines (269 loc) · 12.1 KB

ACL

The ACL library provides a flexible framework for defining permissions and granting access to protected entities and system functionality.

This section assumes you are familiar with the Symfony framework (app kernel, configurations), the ApiPlatform framework (api resources and properties) and the Doctrine ORM library.

Table of Contents

Synopsis

  1. Activate the ACL library.
  2. Create a Doctrine entity and expose it as a RESTful api endpoint.
  3. Protect the entity with the ACL library.
  4. Describe how the entity can be accessed.
  5. Grant users access to the protected entity.

Overview

In order to better understand the ACL framework, we will go through each steps required for creating a new Doctrine entity, exposing it as a RESTful api endpoint and fully protecting it using the ACL framework.

1. Activate the acl library

To begin, you will need to enable the security bundle in the Symfony app kernel:

app/AppKernel.php

    // ...

    public function registerBundles()
    {
        $bundles = [
            // ...
            new Ds\Component\Security\Bridge\Symfony\Bundle\DsSecurityBundle(),
        ];

        // ...
    }

    // ...

Additionally, the acl library needs to be enabled in the Symfony configurations:

app/config/config.yml

// ...

ds_security:
    acl: true

2. Create a Doctrine entity and expose it as a RESTful api endpoint

For the purpose of this demo, we will be creating a directory listing of services.

Services represents government-offered services available to its citizen. For example: "Report a Pothole", "Request a Birth Certificate", etc.

In order to create such listing, we will create a new Doctrine entity named Service and annotate it with ApiPlatform in order to expose it as a JSON-based api endpoint at /services:

src/AppBundle/Entity/Service.php

<?php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource
 * @ORM\Entity
 */
class Service
{
    /**
     * @ApiProperty
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(name="id", type="integer")
     */
    protected $id;

    /**
     * @ApiProperty
     * @ORM\Column(name="title", type="string")
     */
    protected $title;

    /**
     * @ApiProperty
     * @ORM\Column(name="description", type="string")
     */
    protected $description;
}

Setting aside Symfony's firewalls configurations, the /services endpoint is open to the public in its current state.

Sending an HTTP GET request to /services will return a 200 OK response with the following body:

[
    {
        "id": 1,
        "title": "Report a Pothole",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
        "id": 2,
        "title": "Request a Birth Certificate",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    }
]

Sending an HTTP POST request to /services with body:

{
    "title": "Report a Graffiti",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}

will return a 201 CREATED response with body:

{
    "id": 3,
    "title": "Report a Graffiti",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}

3. Protect the entity with the ACL library

Protecting an entity with the ACL library is fairly straight forward. Implementing the Secured interface will activate the ACL guard on the entity:

src/AppBundle/Entity/Service.php

<?php

// ...

use Ds\Component\Security\Model\Type\Secured;

class Service implements Secured
{
    // ...
}

The ACL library is integrated with the ApiPlatform framework through it's event system and will properly guard entities from read or write access based on the granted permissions (which well talk about later).

Sending an HTTP GET or POST request to /services will now return a 403 FORBIDDEN. This is due to the fact that no one has been granted read or write access to the Service entity.

4. Describe how the entity can be accessed

Prior to granting access to the Service entity to various users, the ACL library requires us to describe how the entity can be accessed.

For the purpose of this demo, we will define all possible permissions on the Service entity. Simply add the following to the Symfony configurations:

app/config/config.yml

ds_security:
    acl: true
    permissions:
        service:             { entity:   AppBundle\Entity\Service,               attributes: [BROWSE, READ, EDIT, ADD, DELETE] }
        service_id:          { property: AppBundle\Entity\Service.id,            attributes: [BROWSE, READ, EDIT] }
        service_title:       { property: AppBundle\Entity\Service.title,         attributes: [BROWSE, READ, EDIT] }
        service_description: { property: AppBundle\Entity\Service.description,   attributes: [BROWSE, READ, EDIT] }

Here, we are creating four new permissions, named service, service_id, service_title and service_description. These names must be unique and are later used when granting access.

  • The permission named service is of type entity, meaning we are defining a permission that makes the AppBundle\Entity\Service entity eligible to be browsed, read, edited, added or deleted.
  • The permission named service_id is of type property, meaning we are defining a permission that makes the id property of the AppBundle\Entity\Service entity eligible to be browsed, read or edited. The same can be said respectively for each other property-based permissions.

Internally, the ACL library integrates with the ApiPlatform framework and maps permission attributes to HTTP request methods. Essentially, the attribute:

  • BROWSE maps to GET /services
  • READ maps to GET /services/{id}
  • EDIT maps to PUT /services/{id}
  • ADD maps to POST /services
  • DELETE maps to DELETE /services/{id}

BROWSE and READ are both read-based attributes. However, they distinguish themselves based on whether we are reading a collection of entities versus a single entity. This becomes particularly useful in scenarios where some users are only granted browsing a collection and only on a few properties and not necessarily reading single entities or vice versa.

Also, if we wanted for example to completely disable Service entities from being deleted at the code-level, simply removing the DELETE attribute on the entity permission would completely block the DELETE HTTP method.

You may want to consult the full documentation on permissions, which includes all the possible types and attributes and described in much more details.

5. Grant users access to the protected entity

Despite defining all the permissions above, the Service entity is still not accessible. We have only made it eligible to be accessed, through explicit channels, using a nomenclature that the ACL library understands.

The next step is granting users specific permissions through what we call access cards.

An access card is essentially a collection of granted permissions saved in the database and is associated with a user or role. A user may have zero, one or multiple access cards associated with it through direct associations or via a role he belongs to. Access cards associated with a user are compiled early on in the execution of the request and are retrieved from the database through the identity UUID found in the JWT token.

For the purpose of this demo, we will create two access cards: one for a Staff member named Alex representing an administrator and one for an Individual named Morgan representing a citizen.

Alex's access card

{
    // ...
    "assignee": "Staff",
    "assigneeUuid": "703fb098-40df-486d-8e08-65d892b4c288", // Alex's Staff UUID
    "permissions": [
        {
            "scope": "generic",
            "key": "service",
            "attributes": ["BROWSE", "READ", "EDIT", "ADD", "DELETE"]
            // ...
        },
        {
            "scope": "generic",
            "key": "service_id",
            "attributes": ["BROWSE", "READ", "EDIT"]
            // ...
        },
        {
            "scope": "generic",
            "key": "service_title",
            "attributes": ["BROWSE", "READ", "EDIT"]
            // ...
        },
        {
            "scope": "generic",
            "key": "service_description",
            "attributes": ["BROWSE", "READ", "EDIT"]
            // ...
        }
    ]
}

Here, we want Alex to have full access to Service entities so that he may manage all services. The access card above essentially grants Alex all permissions possible on Service entities. Alex may BROWSE, READ, EDIT, ADD and DELETE any Service entities and BROWSE, READ and EDIT any properties of the Service entity.

Sending an HTTP GET request to /services as Alex will return a 200 OK with the following body:

[
    {
        "id": 1,
        "title": "Report a Pothole",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
        "id": 2,
        "title": "Request a Birth Certificate",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    }
]

Sending an HTTP POST request to /services as Alex with body:

{
    "title": "Report a Graffiti",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}

will return a 201 CREATED response with body:

{
    "id": 3,
    "title": "Report a Graffiti",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}

Morgan's access card

{
    // ...
    "assignee": "Individual",
    "assigneeUuid": "1106a13c-6673-401c-95df-56f2477627ab", // Morgan's Individual UUID
    "permissions": [
        {
            "scope": "generic",
            "key": "service",
            "attributes": ["BROWSE", "READ"]
            // ...
        },
        {
            "scope": "generic",
            "key": "service_title",
            "attributes": ["BROWSE", "READ"]
            // ...
        },
        {
            "scope": "generic",
            "key": "service_description",
            "attributes": ["BROWSE", "READ"]
            // ...
        }
    ]
}

Here, we want Morgan to have at most read access so that he may consult government services he may be interested in. The access card above essentially grants Morgan only BROWSE and READ permissions on Service entities. Also, he only has BROWSE and READ permissions to the title and description properties of the Service entity.

Sending an HTTP GET request to /services as Morgan will return a 200 OK with the following body:

[
    {
        "title": "Report a Pothole",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
        "title": "Request a Birth Certificate",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    }
]

Sending an HTTP POST request to /services as Morgan with body:

{
    "title": "Report a Graffiti",
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}

will return a 403 FORBIDDEN.

Documentation

Further documentation can be found at the following pages: