Skip to content

Commit

Permalink
Add README for example app. (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
yashtewari authored Feb 21, 2021
1 parent 629a63b commit 4e5909d
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 1 deletion.
180 changes: 180 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
## Example

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](./app/config/routes.yaml):

1. Viewing blogs, using `GET` methods on `/blog/{user}/{blog-slug}` resources
2. Creating, updating or deleting blogs using `POST`, `PUT` and `DELETE` methods on the same resources
3. 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:

1. The `anyone` role can be anyone. Even if the `user` header is missing in the request, permission to view blogs is granted.
2. The `member` role is assigned to registered users, who can create, update and delete _their own blogs_, but can only view other's blogs.
3. The `admin` role is assigned to admins, who can access the admin console, as well as create, update and delete _anyone's blogs_.

#### Setting up the Policy Decision Point (PDP)

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

This comment has been minimized.

Copy link
@amirbenun

amirbenun Feb 21, 2021

I would try to use buildsecurity/pdp instead

```

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 comment has been minimized.

Copy link
@amirbenun

amirbenun Feb 21, 2021

Which means that is no longer going to work. But you could add key and secret to the running command.

```

This loads the [RBAC data](./policy/rbac.json) 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](./policy/symfony_authz.rego),

```
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.

#### Setting up the Symfony server

- [Install Symfony](https://symfony.com/doc/current/setup.html)

Make sure you're in the `/example` directory of this repository, and run

```
symfony serve --dir=app
```

Your app is now running.

#### How it works

Take a look at the [RBAC data](./policy/rbac.json).

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.

This comment has been minimized.

Copy link
@amirbenun

amirbenun Feb 21, 2021

Or maybe the snippet should come here instead


The [policy file](./policy/symfony_authz.rego) 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":{

This comment has been minimized.

Copy link
@amirbenun

amirbenun Feb 21, 2021

Note that we still have to add source and destination here.
Also I see that headers is a map[string]:string[] which is inconsistent with our other middlewares where we have map[string]:string

"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` and `blog_slug`
- `resource.requirements` -- these are the authz requirements defined on [the controller](./app/src/Controller/Controller.php) using the middleware. The PDP makes sure that the requester _role_ has the necessary _permissions_ to fulfill the _requirements_ for this controller.

#### Try it out!

##### View a blog

This comment has been minimized.

Copy link
@amirbenun

amirbenun Feb 21, 2021

I would add here a snippet of the users from our rego file, with explanation that the main usage will connect a db instead.


```
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.

##### Create and update 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...

##### Admin access

```
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'
```
2 changes: 1 addition & 1 deletion example/app/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ parameters:
# Policy Decision Point (PDP) configuration.
pdp.port: 8181
pdp.hostname: http://localhost
pdp.policy.path: /authorize
pdp.policy.path: /symfony/authz
pdp.readTimeout.milliseconds: 5000
pdp.connectionTimeout.milliseconds: 5000
pdp.retry.maxAttempts: 2
Expand Down
37 changes: 37 additions & 0 deletions example/policy/rbac.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"roles": {
"admin": {
"permissions": [
"admin_console.view"
],
"sub_roles": ["member", "anyone"]
},
"member": {
"permissions": [
"blog.create",
"blog.edit",
"blog.delete"
],
"sub_roles": ["anyone"]
},
"anyone": {
"permissions": [
"blog.view"
]
}
},
"users": {
"alice": {
"id": 1,
"role": "admin"
},
"bob": {
"id": 2,
"role": "member"
},
"charlie": {
"id": 3,
"role": "member"
}
}
}
67 changes: 67 additions & 0 deletions example/policy/symfony_authz.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package symfony.authz

import data.datasources.RBAC

default allow = false

# Find the user, or set to default user if not found.

default user = {
"id": 0,
"role": "anyone",
}

user = x {
x := RBAC.users[username]
username == input.request.headers.user[0]
}

# Extract role based on user

role := RBAC.roles[user.role]

# Collect all permissions for this role into a single list,
# including permissions from sub-roles.

sub_role_perm_list := [RBAC.roles[subrole].permissions | subrole = role.sub_roles[_]]

sub_role_permissions = x {
x = [perm | perm = sub_role_perm_list[_][_]]
}

permissions := array.concat(role.permissions, sub_role_permissions)

# We need to apply some special logic if this request needs
# member-level permissions. This rule evaluates to true if any
# of the requirements are member-level.

requires_member_permissions {
perm := input.resources.requirements[_]
perm == RBAC.roles[rolename].permissions[_]
rolename == "member"
}

# Make decision: authz should not be allowed if:
# - Any of the requirements in the input are not available in this user's permissions
# - This user has role member and is requesting access to another member's resource

any_requirements_not_match {
count(input.resources.requirements) != count([1 | req = input.resources.requirements[_]; req == permissions[_]])
}

allow {
not any_requirements_not_match

# If the user is a member, make sure they are making changes
# to their own resource.
user.role == "member"
requires_member_permissions

input.request.headers.user[0] == input.resources.attributes.user
}

allow {
not any_requirements_not_match

user.role != "member"
}

0 comments on commit 4e5909d

Please sign in to comment.