We launched new developer portal. For the latest documentation visit developer.cloudentity.com

Protecting applications and APIs in ACP using Open Policy Agent

Instructions for developers on how to use Rego policies to protect APIs and applications in ACP.

About Rego policies in ACP

ACP allows you to create policies in two ways:

The Open Policy Agent (OPA) is an open source, general-purpose policy engine that enables unified, context-aware policy enforcement across the entire stack.

OPA gives you a high-level declarative language called Rego to author and enforce policies across your stack. When you query OPA for a policy decision, OPA evaluates the rules and data (which you provide) to produce an answer. The policy decision is sent back as the result of the query.

ACP contains an embedded Rego editor which also includes code samples to help you get started. When creating policies in ACP, keep in mind to take the policy type into account.

Policy type Description
User User policies validate requests involving user interaction. They can be assigned on a workspace level (Token issue policy), application level (User policy) and service scope level (Consent grant policy).
Developer Developer policies validate client registration and developer subscriptions to a given scope. They can be assigned on a workspace level (Client registration policy) and service scope level (Client assignment policy).
Machine to machine These policies validate a token request coming from a client using the Client credentials OAuth 2.0 flow. They can be assigned on a workspace level (Machine token policy) and service scope level (Machine to Machine policy).
Dynamic Client Registration DCR policies are used to validate Dynamic Client Registration requests.
API Request An API policy validates requests coming to APIs protected by a gateway bound to ACP.

When ready, the policy can be assigned to one of its designated execution points.

Prerequisites

  • Access to an ACP tenant

  • Understanding of the Rego syntax

Create a policy

The video below shows how to create a Rego policy and run it in test mode.

  1. In your workspace, go to Governance > Policies from the sidebar.

  2. In the Policies view, select CREATE POLICY.

  3. In the Create Policy popup window

    1. Select the Policy type from the dropdown menu.

    2. Specify the Policy name.

    3. Select REGO as the Policy language.

    4. Select Create.

    Result

    The OPA policy editor opens.

  4. To define your policy, enter the policy code in the OPA language into the editor. Check the below request templates for help - they are also available in the right-hand policy menu.

    When ready, Save your policy. You can now assign it to a valid execution point (see the Policy Types table above).

ACP request schema

The following schema is valid for all requests coming from ACP. For that reason, it is also used in the policy test mode. Note the three top-level objects:

  • authn_ctx contains the authentication context claims, including scopes.
  • contexts contains dynamic scopes in contexts.scopes.users.*.
  • request contains data specific to the HTTP request itself.

ACP would typically send input resembling the one below to the policy engine:

{
  "authn_ctx": {
    "scp": [
      "scope_name"
    ],
    "sub": "joe",
    "groups": [
      "group_name"
    ],
    "email": "testjoe@cloudentity.com",
    "email_verified": "testjoe@cloudentity.com",
    "phone_number": "+1-555-6616-899",
    "phone_number_verified": "+1-555-6616-899",
    "address": {
      "formatted": "",
      "street_address": "1463  Perry Street",
      "locality": "Dayton",
      "region": "Kentucky",
      "country": "US",
      "postal_code": "41074"
    },
    "name": "Joe Test",
    "given_name": "Joe",
    "middle_name": "",
    "family_name": "Test",
    "nickname": "joe",
    "preferred_username": "testjoe",
    "profile": "",
    "picture": "",
    "website": "",
    "gender": "male",
    "birthdate": "1960-10-09",
    "zoneinfo": "",
    "locale": "",
    "updated_at": ""
  },
  "contexts": {
    "scopes": {
      "users.*": [
        {
          "params": [
            "joe"
          ],
          "requested_name": "users.joe"
        }
      ]
    }
  },
  "request": {
    "headers": {
      "Content-Type": [
        "application/json"
      ],
      "X-Custom-Header": [
        "BOT_DETECTED"
      ]
    },
    "method": "POST",
    "path_params": {
      "users": "admins"
    },
    "query_params": {
      "limit": [
        "1000"
      ],
      "offset": [
        "100"
      ]
    },
    "path": "/doawesomethings"
  }
}

Your policies can verify all data passed in the above schema and validate requests based on it. Check the policy tips below and start writing!

Scope check policy

To write a policy checking for a scope in the request, you can use the following template:

package acp.authz

default allow = false
scope := "sample_service:write"

allow {
  input.authn_ctx.scp[_] == scope
}

This policy validates the request when the required scope ("sample_service:write") is found in the authentication context (input.authn_ctx.scp[_]).

Dynamic scope check policy

To write a policy checking for a dynamic scope in the request, you can use the following template:

package acp.authz

default allow = false

allow {
  input.scopes["users.*"][_].params[0] == input.authn_ctx.sub
}

This policy validates the request when the required value is found in the input.scopes object, where dynamic scopes are stored.

HTTP request check policy

To write a policy checking the HTTP request parameters, you can use the following template:

package acp.authz

default allow = false

allow {
  input.request.method == "POST"
  input.request.headers["X-Custom-Header"][_] == "REGULAR_USER"
}

This policy validates the request only for a POST request containing a specific header.

HTTP header names format

REGO policies by their definition are case-sensitive when matching HTTP header names, but ACP authorizers follow the RFC-2616 specification which states that header names are case-insensitive. To allow authorizers to correctly validate REGO policies, header names are normalized to follow the canonical format.

Canonicalization converts the first letter and any letter following the hyphen to upper case and the rest of the letters are converted to lower case.

It means that if a request is to be validated and contains a header like, for example, x-custom-header, before the header is validated, the header is converted to follow the canonical format X-Custom-Header.

As the policy check is case sensitive for REGO policies, your REGO policy that checks request headers must have the header in the canonical format as you can see in the HTTP request check policy example above.

MFA enforcement policy

To write a policy checking the MFA validation status of the user, you can use the following template:

package acp.authz

default allow = false

allow {
    input.login.verified_recovery_methods[_] = "mfa"
}

recovery = ["mfa"]

This policy validates the request only if the user has completed MFA. Otherwise, the user is prompted for an OTP code in accordance with the tenant’s MFA setup.

HTTP call status check policy

To write a policy executing an HTTP call and checking the status, you can use the following template:

package acp.authz

default allow = false

allow {
  response := http.send({
    "method" : "GET",
    "url": "https://docs.authorization.cloudentity.com"
  })
  response.status_code == 200

}

This policy validates the request only if the request returns a given status (200 in the above policy).

Group membership check policy

To write a policy checking for user’s group membership, you can use the following template:

package acp.authz

default allow = false
group := "admins"

allow {
  input.authn_ctx.groups[_] == group
}

This policy validates the request only if the admins value is found in the authn_ctx.group object inside the authentication context (i.e. the user is an admin).

Secret check policy

You can retrieve a secret value for comparison via Rego policy. The below policy compares the secret value from SECRET_NAME against the name parameter passed in the authentication context:

package acp.authz

default allow = false

allow {
   input.secrets.SECRET_NAME == input.authn_ctx.name

}

This policy validates the request only if the value of a secret called SECRET_NAME matches the value of the name attribute from the authentication context.

Header injection for Istio policies

Note

The technique described here works for the Istio authorizer only.

When a policy for the Istio authorizer is resolved, all globally defined policy variables are injected as headers. Such a policy can only be assigned to APIs behind the Istio gateway bound to ACP, therefore it must always have the API request type. Considering we have the following policy:

package acp.authz

default allow = false
subject := input.authn_ctx.sub
expiration := input.authn_ctx.exp
issuer := input.authn_ctx.iss
scopes := input.authn_ctx.scp
tenantid := input.authn_ctx.tid

allow {
  true
}

Upon policy validation, the authentication context values defined as global variables (outside of the allow document) are extracted and injected as headers in the request received by the target service (the values below are encoded):

X-Output-Issuer: Imh0dHBzOi8vYWNwLmFjcC1zeXN0ZW06ODQ0My9kZWZhdWx0L2RlZmF1bHQi
X-Output-Expiration: MTYzNTk2OTQ2OA==
X-Output-Tenantid: ImRlZmF1bHQi
X-Output-Scopes: WyJlbWFpbCIsImludHJvc3BlY3RfdG9rZW5zIiwibGlzdF9jbGllbnRzX3dpdGhfYWNjZXNzIiwibWFuYWdlX2NvbnNlbnRzIiwib2ZmbGluZV9hY2Nlc3MiLCJvcGVuaWQiLCJwcm9maWxlIiwicmV2b2tlX2NsaWVudF9hY2Nlc3MiLCJyZXZva2VfdG9rZW5zIiwidmlld19jb25zZW50cyJd
X-Output-Subject: InVzZXIi

The Istio sidecar configuration and the default ACP headers (X-Output-Allow, X-Auth-Ctx) are injected as well.

Embedded policies

For REGO policies that are embedded within a Cloudentity policy, if the output contains the same keys, it is merged and the keys are overwritten. The key is set to the key of the last resolved REGO policy.

For example, in your Cloudentity policy there are two embedded REGO policies, A and B. The policy A has headers X and Y, and the B policy has headers Y and Z. The Y header is common for both policies. It’s value is set to the value of Y header of the B policy as B is the last REGO policy embedded in the Cloudentity policy. Both the X and the Z headers remain the same.

Having defined a policy, it’s time to assign it to an execution point and test it. Check the following resources for help: