Supercharging your policy rules with self-service custom roles

Published by Sam Lock on February 07, 2023
Supercharging your policy rules with self-service custom roles

A little while ago, we released the blog post: Mapping business requirements to authorization policies. It laid the foundations for how to approach writing your authorization policies from the ground up and discussed different patterns for implementing each.

In this post (the next in a series 👀), we’ll dig a little deeper and look at how to implement a more specific feature: self-service custom roles.

What?

This is a fair response. I mean, we already have all of the flexibility that ABAC gives us - what else do we need?! Well, bear with me, dear reader, whilst I endeavour to explain an approach that might open up even more doors to access-control freedom.

🤔

Consider this scenario: you’re an admin in a multi-tenant system and you want a method by which you can copy an existing role, and then select which permissions/actions to enable or disable for each.

Surely this can’t be achieved with static policies?

Aha! Well, prepare to be adequately surprised. Believe it or not, this approach can absolutely be achieved with static policies. In fact, this is the idiomatic approach to handling these types of scenarios in Cerbos.

Before we dive into an example, let’s provide a little refresher on how access decisions are made in Cerbos:

  • Cerbos is stateless. Your policies and only your policies are baked into the PDP instance.
  • When access control requests are made to Cerbos, the body of the request contains all of the necessary data required to make that decision. This data is freeform - it’s up to the user to decide what’s required and how to structure it.
  • The freeform data can be simple top-level attributes like booleans, integers, floats, and strings, or more complex types, such as arrays, maps, or user defined structs.
  • Cerbos uses this contextual information to make decisions against the defined policies.

A map, you say?

Yes! Maps are great, because by using them, we are no longer constrained to single, static attribute values. Suddenly, we can provide further context (a key in this instance), and our static data becomes a little more dynamic.

Take a look at this example; a resource policy for a resource of type workspace:

apiVersion: "api.cerbos.dev/v1"
resourcePolicy:
  version: "default"
  resource: "workspace"
  rules:
    - actions:
        - workspace:view
        - pii:view
      effect: EFFECT_ALLOW
      roles:
        - USER
      condition:
        match:
          expr: P.attr.workspaces[R.id].role == "OWNER"

Notice how the condition relies on context passed in within the P.attr.workspaces map. The key is the resource ID; the value in this case is a custom struct representing a workspace. Calling the role attribute on that struct returns a predefined value "OWNER".

Now, for our principal, we can provide lots of rich, custom data for each workspace in the organization! These workspaces are constructed from the state in your database of choice, and presented to Cerbos within that single P.attr.workspaces map 👌.

I’m going to need an example!

I got you. Take a look at this; we can grant access to a principal with the USER role, by constructing the following request payload:

cat <<EOF | curl --silent "http://localhost:3592/api/check/resources?pretty" -d @-
{
  "requestId": "quickstart",
  "principal": {
    "id": "123",
    "roles": [
      "USER"
    ],
    "attr": {
      "workspaces": {
        "workspaceA": {
          "role": "OWNER"
        },
        "workspaceB": {
          "role": "MEMBER"
        }
      }
    }
  },
  "resources": [
    {
      "actions": [
        "workspace:view",
        "pii:view"
      ],
      "resource": {
        "id": "workspaceA",
        "kind": "workspace"
      }
    },
    {
      "actions": [
        "workspace:view",
        "pii:view"
      ],
      "resource": {
        "id": "workspaceB",
        "kind": "workspace"
      }
    }
  ]
}
EOF

Here, our principal is trying to carry out workspace:view and pii:view actions on two resources, imaginatively named workspaceA and workspaceB. When building the request, our app consulted our database and constructed the principal attribute data:

    …
    "attr": {
      "workspaces": {
        "workspaceA": {
          "role": "OWNER"
        },
        "workspaceB": {
          "role": "MEMBER"
        }
      }

Cerbos then uses this to make all the decisions for us, et voila 🎉!

Ok, that’s cool and all, but I need to create actual dynamic policies on the fly...

Well, we got you there too 😎. We even wrote an entire blog post about dynamic policy management! But seeing as we’re here, I’ll summarise it again for you.

Firstly, you’re going to need a mutable data store to store your policies. Follow that link, scroll down, and you’ll see the ones that are currently supported by Cerbos. These DBs act as a dynamic store in which Cerbos can add, update and retrieve policies.

As an example, here’s the simplest case – an in-memory instance of SQLite that runs alongside the Cerbos PDP:

storage:
  driver: "sqlite3"
  sqlite3:
    dsn: ":memory:"

There’s every chance that an ephemeral data-store such as the above isn’t going to cut the mustard in your real-life production application, but you get the idea.

OK, so now we have our backing data store, we need to tell Cerbos that it’s allowed to talk to the Admin API. Add (something like) this to your config:

server:
  adminAPI:
    adminCredentials:
      passwordHash: JDJ5JDEwJGJWcFRKUzJKRzYxOTJERWs5SzZaS2VSb2Z1cXNSeTYzam9NR1U5UkVKM3BtZ1VLQUVuM0xlCgo= # passwordHash is the base64-encoded bcrypt hash of the password to use for authentication.
      username: cerbos # Username is the hardcoded username to use for authentication.
    enabled: true # Enabled defines whether the admin API is enabled.

You’ll then have a choice. You can form your own requests and hit the API directly, or alternatively; some of the SDKs have the functionality built in – check out the Go and Javascript examples (we’re continuously improving our library of SDKs so check back for others!).

If you want to see this in action, then check out our full demo. It uses a Go backend and a React client; complete with an editing interface using the Monaco editor.


So there we have it; two wildly different approaches to achieving authorization via powerful, dynamic custom roles in your application. If you have any questions or feedback, head over to our Slack community and introduce yourself!

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team