This example demonstrates usage of the OPA Symfony middleware to enforce Role Based Access Control (RBAC) on endpoints of a basic RESTful "blogging app" with three kinds of routes:
- Viewing blogs, using
GET
methods on/blog/{user}/{blog-slug}
resources - Creating, updating or deleting blogs using
POST
,PUT
andDELETE
methods on the same resources - Viewing an "admin console" on
/admin/console
We'll pass a HTTP request header -- user
from our mock client to identify who is making the request. In production, for example, this could be replaced by a JSON Web Token, or any other authentication model that fits your infrastructure.
The authz policy that we're going to load into the Policy Decision Point (PDP) is defined to enforce different levels of access to three kinds of roles:
- The
anyone
role can be anyone. Even if theuser
header is missing in the request, permission to view blogs is granted. - The
member
role is assigned to registered users, who can create, update and delete their own blogs, but can only view other's blogs. - The
admin
role is assigned to admins, who can access the admin console, as well as create, update and delete anyone's blogs.
For the following steps, please make sure you have the latest version of Docker installed.
Let's start up the OPA server using Docker on a separate terminal.
docker pull openpolicyagent/opa
docker run -p 8181:8181 openpolicyagent/opa \
run --server --log-level debug
Running with debug logs will show you full authz request payloads.
Then, clone and cd
into the /example
directory of this repository, and run,
curl --location --request PUT 'http://localhost:8181/v1/data/datasources/RBAC' \
--data-binary "@./policy/rbac.json"
This loads the RBAC data into the PDP, which becomes part of the authorization context. Any data from any source can be loaded in order to inform authorization decisions. Next, we load the policy,
curl --location --request PUT 'http://localhost:8181/v1/policies/symfony/authz' \
--data-binary "@./policy/symfony_authz.rego"
And that's it! The Symfony middleware can now make authz requests to the PDP, and based on the authz policy, the input sent with the request, and other data available to it, the PDP will return an authz response.
Again, make sure you're in the /example
directory of this repository, and build the image:
docker build -t symfony-mw-example .
Since OPA is running on your host machine, app container needs to access your localhost
. This can be done using the PDP_HOSTNAME
environment variable, and the host.docker.internal
DNS name which resolves to your host IP.
docker run \
--rm \
-p 8000:8000 \
-e PDP_HOSTNAME=host.docker.internal \
-e PDP_PORT=8181 \
-e PDP_POLICY_PATH=/symfony/authz \
symfony-mw-example
The host.docker.internal
option isn't available on Linux, so we'll use host networking instead.
docker run \
--rm \
--network host \
-e PDP_HOSTNAME=localhost \
-e PDP_PORT=8181 \
-e PDP_POLICY_PATH=/symfony/authz \
symfony-mw-example
Your app is now running.
Note: if you started the PDP server with non-default address, or loaded policy to a different path from the one given above, you need to either use the PDP_HOSTNAME
, PDP_PORT
and PDP_POLICY_PATH
environment variables as overrides, or change the service configuration file and rebuild the Docker container.
Take a look at the RBAC data.
You'll notice a few things:
- There are three users, Alice, Bob and Charlie.
- Alice is an admin, whereas Bob and Charlie are members.
- Permissions define the access limits of a role.
- Sub-roles define a hierarchy of roles.
The policy file contains the logic that makes our decisions, along with useful comments that show each step.
You may notice the input
object in the policy. This is what it looks like when our middleware sends it as payload to the PDP:
{
"input":{
"request":{
"headers":{
"host":[
"localhost:8000"
],
"user-agent":[
"curl\/7.74.0"
],
"content-length":[
"0"
],
"accept":[
"*\/*"
],
"user":[
"charlie"
],
"x-forwarded-for":[
"::1"
],
"accept-encoding":[
"gzip"
],
"content-type":[
""
],
"mod-rewrite":[
"On"
],
"x-php-ob-level":[
"1"
]
},
"method":"POST",
"path":"\/blog\/bob\/some-blog",
"query":[
],
"scheme":"http"
},
"resources":{
"attributes":{
"user":"bob",
"blog_slug":"some-blog"
},
"requirements":[
"blog.create"
]
}
}
}
It contains:
- HTTP request information, including headers, method, query values and query path.
resource.attributes
-- these are the route parameters and values -- in this case,user
andblog_slug
resource.requirements
-- these are the authz requirements defined on the controller using the middleware. The PDP makes sure that the requester role has the necessary permissions to fulfill the requirements for this controller.
curl --location --request GET 'http://localhost:8000/blog/bob/some-blog'
Even though we didn't pass a user
header with the request, we can view the blog.
curl --location --request POST 'http://localhost:8000/blog/bob/some-blog'
That doesn't work! Since we're running the Symfony debug server, we get a generated HTML page describing AccessDeniedHttpException
and a strack trace. In production, you would have a Symfony kernel.exception
handler that generates an appropriate HTML page for your users, or redirect them somewhere else, and so on.
Let's try making this request as Bob instead,
curl --location --request POST 'http://localhost:8000/blog/bob/some-blog' \
--H 'user: bob'
That works. Can Charlie update this blog?
curl --location --request PUT 'http://localhost:8000/blog/bob/some-blog' \
--H 'user: charlie'
No, members can only create, update and delete their own blogs. Alice on the other hand...
curl --location --request DELETE 'http://localhost:8000/blog/charlie/some-other-blog' \
--H 'user: alice'
Since Alice is the admin, she's allowed to delete Charlie's blog.
Finally, we see that only Alice can view admin console.
curl --location --request GET 'http://localhost:8000/admin/console' \
--H 'user: alice'