Security is one, if not the most important architecture characteristic. When we talk about security we usually talk in terms of authentication and authorization. Authentication refers to the ability of a user(or service) to access functionalities provided by other services. What are those functionalities are controlled by the authorization process. Two of the most used strategies are Role-Based Access Control (RBAC) and Attribute-Based Access Control(ABAC).
RBAC is based on assigning roles to users. Roles are composed of permissions. For example, if we talk about organizations we may have CreateOrganization permission, DeleteOrganization permission, UpdateOrganization permission, ViewOrganization etc. An admin role would have all this roles embedded while a super role could create and update organizations. Plain and simple.
What about if we want to control a fixed set of organizations? Like regional. A regional manager can work within his regions. In this case roles are not enough. We need fine grained control. ABAC is an generalization of RBAC in the sense that role is just another attribute. These attributes can be fixed or dynamic. When I say dynamic I refer to the fact that they may be computed on the fly. The attributes are bundled into policies, which are then assigned to services. A policy is usually(not mandatory) composed of a resource, action and environment values(this is the dynamic part).
Open Policy Agent(OPA) is a rule engine that makes the use of ABAC simple. We can define policies using Rego. Let’s see it action.
docker run -p 8181:8181 openpolicyagent/opa run --server --log-level debug
If everything is fine you should see
{"addrs":[":8181"],"diagnostic-addrs":[],"insecure_addr":"","level":"info","msg":"Initializing server.","time":"2020-07-27T12:46:40Z"}
{"headers":{"Content-Type":["application/json"],"User-Agent":["Open Policy Agent/0.20.5 (linux, amd64)"]},"level":"debug","method":"POST","msg":"Sending request.","time":"2020-07-27T12:46:40Z","url":"https://telemetry.openpolicyagent.org/v1/version"}
{"err":"Post https://telemetry.openpolicyagent.org/v1/version: context deadline exceeded","level":"debug","msg":"Unable to send OPA version report.","time":"2020-07-27T12:46:41Z"}
Now onto defining the policy. For this I would recommend to use the playground. There are examples including JWT tokens which in most cases this will part of the authorization.
package sergiuoltean.test
default allow = false
allow {
group == "RegionalManager"
input.action == "Read"
input.resource = "Organization"
data.east[input.organization]
}
group := g {
g := payload.group
}
payload := p {
token := input.input
startswith(token, "Bearer ")
t := substring(token, count("Bearer "), -1)
[_, p, _] := io.jwt.decode(t)
}
This will allow access to all the users that are RegionalManager who are trying to read the organization and they are in the east organization list. The full example can be run in the playground.
input.action == "Read"
input.resource = "Organization"
The input field represents the the input that is send to be validated against the policy. It is a json.
data.east[input.organization]
The data represent the place where we can keep data that has purpose in the authorization process. In most cases this needs to be syncronized from your own data stores. For example if your organization data is kept in a database a migration process must be put in place to copy it to OPA. For tactics on how to achieve this check out the documentation on external data.
Cool. Onto the integration with our services. OPA has also a REST API. Before going any further I would recommend to take a look at OPA document model in order to understand what data and input is.
The first thing we need to do is to load our policy into the server.
curl --location --request PUT 'localhost:8181/v1/policies/test' \
--header 'Content-Type: text/plain' \
--data-raw 'package sergiuoltean.test
default allow = false
allow {
group == "RegionalManager"
input.action == "Read"
input.resource = "Organization"
data.east[input.organization]
}
#-------------- Utilities --------------
group := g {
g := payload.group
}
payload := p {
token := input.input
startswith(token, "Bearer ")
t := substring(token, count("Bearer "), -1)
[_, p, _] := io.jwt.decode(t)
}'
The server log should look like
{"client_addr":"172.17.0.1:36854","level":"info","msg":"Received request.","req_body":"package sergiuoltean.test\n\ndefault allow = false\n\nallow {\n group == \"RegionalManager\"\n input.action == \"Read\"\n input.resource = \"Organization\"\n data.east[input.organization]\n}\n#-------------- Utilities --------------\n\ngroup := g {\n g := payload.group\n}\n\npayload := p {\n token := input.input\n\tstartswith(token, \"Bearer \")\n\tt := substring(token, count(\"Bearer \"), -1)\n\t[_, p, _] := io.jwt.decode(t)\n}","req_id":5,"req_method":"PUT","req_params":{},"req_path":"/v1/policies/test","time":"2020-07-28T08:18:46Z"}
{"client_addr":"172.17.0.1:36854","level":"info","msg":"Sent response.","req_id":5,"req_method":"PUT","req_path":"/v1/policies/test","resp_body":"{}","resp_bytes":2,"resp_duration":0.166154,"resp_status":200,"time":"2020-07-28T08:18:46Z"}
We need to place our custom data inside the OPA server. Careful at the url /data/east. Basically allows us to call data.east inside the policy definition.
curl --location --request PUT 'localhost:8181/v1/data/east' \
--header 'Content-Type: application/json' \
--data-raw '{
"Org1": {
"location": "New York"
},
"Org2": {
"location": "Boston"
},
"Org3": {
"location": "Washington"
}
}'
We verify that everything is in place
curl --location --request GET 'localhost:8181/v1/data'
{
"result": {
"east": {
"Org1": {
"location": "New York"
},
"Org2": {
"location": "Boston"
},
"Org3": {
"location": "Washington"
}
},
"sergiuoltean": {
"test": {
"allow": false
}
}
}
}
Next step is to run an input against the policy.
curl --location --request POST 'http://localhost:8181/v1/data/sergiuoltean/test' \
--header 'Content-Type: application/json' \
--data-raw '{
"input": {
"action": "Read",
"input": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1OTU4NjE4MzksImV4cCI6MTYyNzM5NzgzOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjoiTWFuYWdlciIsImdyb3VwIjoiUmVnaW9uYWxNYW5hZ2VyIn0.75A_tZe6DNCGp7Y_58acnU7xMP7rcQ3Pk46O9owS5_4",
"organization": "Org2",
"resource": "Organization"
}
}'
At the end of this call we will have our answer.
{
"result": {
"allow": true,
"group": "RegionalManager",
"payload": {
"Email": "jrocket@example.com",
"GivenName": "Johnny",
"Role": "Manager",
"Surname": "Rocket",
"aud": "www.example.com",
"exp": 1627397839,
"group": "RegionalManager",
"iat": 1595861839,
"iss": "Online JWT Builder",
"sub": "jrocket@example.com"
}
}
}
Everything seems in order. We are allowed to perform the action. Another important thing to observe is that the response is 200 if the query is successful. That being said if we are not authorized we still get a 200 response code, but the response body will contain “allow”: false.
How can I validate the request against all policies and not specific ones like : http://localhost:8181/v1/data/sergiuoltean/test
Because we may have many policies for the same resource type, no ?
LikeLike