Application Authorization
Standardizing application authorization logic for applications improves security posture, creates a more consistent user experience and saves engineering time. This section contains guides for IAM Engineers and Architects which demonstrate how to standardize authorization logic across their applications using either Enterprise OPA or Open Policy Agent (OPA).
Readers will get the best value out of these guides if they have at least an introductory knowledge of OPA and Rego basics (see Recommended Reading).
Common Model
While authorization models are varied, the basic building blocks of access control decisions commonly refer to subject, action, and resource.
- Subject
The actor of an access control decision: a human operating a browser, or a program using an API (e.g. "alice", "bob").
- Resource
The object that is acted on (e.g. "picture", "profile", "bank account").
- Action
The act of the access control decision: how a resource is referred to (e.g. "read", "delete", "update").
This model is proven to adapt well to many different use cases. We recommend that you use a similar model in your own applications.
API Authorization: Your First Model
Service Interaction
We recommend starting with API-based authorization for applications. With this architecture, the calling application requests authorization decisions from OPA for API calls made to the application.
In the examples that follow, we use HTTP requests to show messages. However, the same ideas are easily translated to a range of different RPC methods and protocols.
Now that you understand the flow of requests across your application's service lifecycle, you need to zoom in on the call from the application to OPA for decision requests and responses.
An example HTTP request to OPA would look like:
POST /v1/data/{path:.+}
Content-Type: application/json
{
"input": {
// Decision Request Input
}
}
And a sample response from OPA would look like:
HTTP/1.1 200 OK
Content-Type: application/json
{
"result": {
// Decision Response Output
}
}
These show three architectural decisions to be made:
Note that (3.) isn't annotated in the examples above:
It's the /v1/data/{path:+}
of the decision request POST request.
It corresponds to the package
stanzas in your Rego policies.
Decision Request Input Schema
The decision request must provide data required to make authorization decisions. This means both the required data to classify the request and run the correct policy, as well as any data that might be needed by that policy to make decisions.
We recommend the following input schema to start with:
Resource contains information about the requested application resource.
Parameter | Type | Value | Description |
---|---|---|---|
input.resource.type | String | endpoint | A constant describing the type of resource being accessed |
input.resource.id | String | Endpoint servlet path | E.g. /documents/1 |
Action contains information about the request: in our architecture, these are protocol-terms of HTTP.
Parameter | Type | Value | Description |
---|---|---|---|
input.action.name | String | GET , POST , PUT , PATCH , HEAD , OPTIONS , TRACE , or DELETE | HTTP request method |
input.action.protocol | String | HTTP protocol for request, e.g. HTTP 1.1 | |
input.action.headers | Map[String, Any] | HTTP headers of request | Not guaranteed to be present |
Context contains additional information about the request. This can be useful for enforcing business rules related to the connection rather than the operation.
Parameter | Type | Value | Description |
---|---|---|---|
input.context.type | String | http | A constant describing the type of contextual information provided |
input.context.host | String | HTTP remote host of request | |
input.context.ip | String | HTTP remote IP of request | |
input.context.port | String | HTTP remote port for request | |
input.context.data | Map[String, Any] | Optional supplemental data | |
input.context.appID | String | Application API routing ID | See Advanced Architecture |
Modeling the subject can be more tricky but we recommend the following to begin with:
Parameter | Type | Value | Description |
---|---|---|---|
input.subject.type | String | user | A constant describing the kind of subject being provided |
input.subject.id | String | User ID or username | ID representing the subject being authorized |
For example, here is a complete request following the above model showing the data that would be sent to OPA for a policy decision:
POST /v1/data/{path:.+}
Content-Type: application/json
{
"input": {
"resource": {
"type": "endpoint",
"id": "/documents/1"
},
"action": {
"name": "GET",
"protocol": "HTTP 1.1",
"headers": {
":authority": "example-app",
":method": "POST",
":path": "/pets/dogs",
"accept": "*/*",
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQWxpY2lhIFNtaXRoc29uaWFuIiwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInVzZXJuYW1lIjoiYWxpY2UifQ.md2KPJFH9OgBq-N0RonGdf5doGYRO_1miN8ugTSeTYc",
"content-length": "0",
"user-agent": "curl/7.68.0-DEV",
"x-ext-auth-allow": "yes",
"x-forwarded-proto": "http",
"x-request-id": "1455bbb0-0623-4810-a2c6-df73ffd8863a"
}
},
"subject": {
"type": "user",
"id": "bob"
},
"context": {
"type": "http",
"host": "example-app"
}
}
}
Decision Response Output Schema
We recommend the following output schema to start with:
Parameter | Type | Value | Description |
---|---|---|---|
decision | Boolean | true or false | The policy decision: true indicates the request should be allowed to proceed |
context.reason | Any | Information backing the decision | E.g. "quota exceeded" (String) or {"authn": "token expired"} (Map[String,String]) |
On the wire, an example looks like this:
HTTP/1.1 200 OK
Content-Type: application/json
{
"result": {
"decision": false,
"context": {
"reason": "insufficient access"
}
}
}
For security reasons, the contextual information returned by OPA may not be what your application is returning to the user: In some situations, it would be prudent to return "not found" when the reason is "unauthorized", such as not to give away the existence of the requested resource.
Also, localization of error messages would typically happen outside of OPA.
Policy Structure and Paths
Let the application's API design determine the structure of your policies. Each service that contributes to your to-be-authorized API is assigned an identifier, and that is used to construct the policies for those service.
package app["{appIdentifier}"].authz
import rego.v1
default allow := false # TODO: add other definitions
These separate packages are evaluated by a top-level policy. That policy is also responsible for building the Decision Response Output Schema.
package authz
import rego.v1
default decision := false
decision if {
app_id := app_from_resource(input.resource)
data.app[app_id].allow # may use `input.resource`, `input.action` etc
}
context["reason"] := "unauthorized" if not decision
# implementation of app_from_resource
This policy is queried via POST /v1/data/authz
with the appropriate input (see Decision Request Input Schema).
Complete Example
In our example application, a document service, two services contribute to the API:
- "store-service" takes care of create, read, update, delete (CRUD) operations
- "publish-service" allows changing the visibility of a document
Subjects of our example include "Alice" and "Bob". Alice has full access to "store-service", and can retrieve if a document has been published. Only Bob is permitted to change the visibility of documents. Bob has full access to "store-service". No other subjects exist in the example system.
The policies for the two services are:
package app["store-service"].authz
import rego.v1
default allow := false
allow if {
subject_matches(input.subject, {"alice", "bob"})
action_matches(input.action)
}
subject_matches(subject, allowed) if {
subject.type == "user"
subject.id in allowed
}
action_matches(action) if action.name in {"POST", "GET", "PUT", "DELETE"} # CRUD
package app["publish-service"].authz
import rego.v1
default allow := false
allow if {
subject_matches(input.subject, {"alice", "bob"})
action_matches(input.action)
allowed(input.subject.id, input.action.name)
}
subject_matches(subject, allowed) if {
subject.type == "user"
subject.id in allowed
}
action_matches(action) if action.name in {"GET", "POST"} # POST updates published status
allowed("bob", _) # bob has full access
allowed("alice", "GET")
The top-level policy, with the previously-missing app_from_resource
, looks like this;
package authz
import rego.v1
default decision := false
decision if {
app_id := app_from_resource(input.resource)
data.app[app_id].authz.allow
}
context["reason"] := "unauthorized" if not decision
app_from_resource(resource) := "store-service" if {
resource.type == "endpoint"
path := split(resource.id, "/")
count(path) in {2, 3}
path[1] == "documents"
}
app_from_resource(resource) := "publish-service" if {
resource.type == "endpoint"
path := split(resource.id, "/")
count(path) == 4
path[1] == "documents"
path[3] == "published"
}
Evaluations
package authz_test
import rego.v1
test_alice_can_create_documents if {
data.authz.decision with input.subject as {"type": "user", "id": "alice"}
with input.resource as {"type": "endpoint", "id": "/documents"}
with input.action as {"name": "POST"}
}
test_alice_can_read_documents if {
data.authz.decision with input.subject as {"type": "user", "id": "alice"}
with input.resource as {"type": "endpoint", "id": "/documents/3321"}
with input.action as {"name": "GET"}
}
test_alice_can_retrieve_if_doc_is_published if {
data.authz.decision with input.subject as {"type": "user", "id": "alice"}
with input.resource as {"type": "endpoint", "id": "/documents/3321/published"}
with input.action as {"name": "GET"}
}
test_alice_can_not_change_published if {
not data.authz.decision with input.subject as {"type": "user", "id": "alice"}
with input.resource as {"type": "endpoint", "id": "/documents/3321/published"}
with input.action as {"name": "POST"}
}
test_bob_can_change_published if {
data.authz.decision with input.subject as {"type": "user", "id": "bob"}
with input.resource as {"type": "endpoint", "id": "/documents/3321/published"}
with input.action as {"name": "POST"}
}
Architecture Guides
Advanced Architecture for Application Authorization
How to expand your policy structure for a multitude of services constituting a real-world application.
Role-Based Access Control (RBAC)
How to use OPA for applications using RBAC
Attribute-Based Access Control (ABAC)
How to use OPA for applications using ABAC
Relationship-Based Access Control (ReBAC)
How to use OPA for applications using ReBAC