Navigating Authz in Microservices: From Traditional Approach to a Reimagined AWS IAM Evaluation
In distributed systems featuring a microservices architecture, authorization management is challenging. Services often cater to both type User, the customers (the ones that pays for your business), and Internal Users, such as system engineers or operations teams.
While these user groups access the same data, their (User) data structures and authorization requirements vary. The prevalent solution? Separate endpoint handlers, each tailored to the unique authorization needs of Consumers and Internal Users.
erDiagram
Transaction {
string id
int amount
string userid
string payment_metod
string region
}
User ||--o{ Authorized-Consumer : is
User {
string id
string name
}
InternalUser ||--o{ Authorized-InternalUser : is
InternalUser {
string id
string name
string role
}
Transaction ||--o{ Authorized-Consumer : Own
Transaction ||--o{ Authorized-InternalUser : HasAccess
Consumers typically access data directly related to them, like transaction details, authorized using session or token info. In contrast, Internal Users have varying access; a director might see all data, while a regional manager only views region-specific transactions. As more user types emerge, services grapple with diverse authorization requirements.
RBAC & ABAC Solution #
There are tools available to specifically address this, Casbin and OPA (Open Policy Agent) streamline authorization by using specific Domain-Specific Languages (DSL) for rule/policy definitions. This decouples authorization from the service, allowing dynamic policy updates without altering the service, ensuring adaptability and reduced error potential.
Sample Casbin Policy (cited from medium.com by Upal Saha)
[request_definition]
r = user_id, feature, action
[policy_definition]
p = user_id, feature, action
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = p.user_id == r.user_id && ((g(p.feature, r.feature) ||
(p.feature == r.feature)) && (p.action == r.action ||
p.action == 'edit' && r.action == 'view') || p.feature == 'admin')
Sample OPA Policy (cited from openpolicyagent.org)
package httpapi.authz
# bob is alice's manager, and betty is charlie's.
subordinates := {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]}
default allow := false
# Allow users to get their own salaries.
allow {
input.method == "GET"
input.path == ["finance", "salary", input.user]
}
# Allow managers to get their subordinates' salaries.
allow {
some username
input.method == "GET"
input.path = ["finance", "salary", username]
subordinates[input.user][_] == username
}
While tools like Casbin and OPA provide extensive flexibility for request evaluation, the complexity of authorization logic remains within the request process. Rather than being resolved, it’s merely shifted: from within the service to an external location.
Alternative Approach #
Essentially, a service’s authorization hinges on 5 primary logics:
- Ascertain if a user is authorized to perform a specific action.
- Ensure that the retrieved (persisted) resource corresponds to the user’s permissible resource attributes.
- For new data actions (creation/update), check its alignment with the user’s permissible resource attributes.
- An explicit effect logic, gauging if an action is permissible or restricted, essentially toggling between affirmation and negation.
- Contextual constraints play a role, such as those based on location or defined timeframes.
While the initial three logics are foundational, the fourth provides nuanced convenience. The fifth, when narrowed to a microservice context, can potentially be managed within the service’s purview.
The essential mentioned above are exemplified in AWS’s Identity and Access Management (IAM). IAM policies not only dictate user permissions for AWS resources and actions, but they’re also designed to be self-explanatory. This clarity in policy definition arguably makes it comprehensible even for non-technical individuals. By confirming user access rights, assessing resource compatibility, and utilizing explicit “Allow” or “Deny” decisions often influenced by contextual conditions, AWS IAM showcases an effective and transparent approach to authorization.
Sample IAM Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"iam:CreateAccessKey",
"iam:DeleteAccessKey",
"iam:ListAccessKeys",
"iam:UpdateAccessKey"
],
"Resource": "arn:aws:iam::245500951992:user/${aws:username}",
"Effect": "Allow",
"Sid": "AllowManageOwnAccessKeys"
}
]
}
The approach offers unparalleled clarity and predictability. as the example above when constructing the Resource matcher, variable evaluations come into play. Yet, whether these evaluations are compiled once upon user access—stored statically in a session/token or recalculated with every request, the inherent logic remains simple and straightforward.
Disclaimer: I am not advocating against the use of Casbin or OPA. I haven’t spent extensive hours working directly with these tools. My observations and judgments are primarily based on my interpretation of their official documentation, academic papers, and articles on their integration. Always consider direct hands-on experience or expert advice when making decisions on tool adoption.
Re-Implementing IAM Evaluation Process. #
I’m experimenting to re-implementing the IAM evaluation process. This experiment manifests as both a service library and a Kong plugin. Rather than being a pure separation of authorization, it adopts a hybrid approach, integrating the service itself into the enforcement process.
For write operations, the Kong plugin can handle authorization based solely on the permissions and the request. For read operations, such as searches or viewing single resources, the Kong plugin forwards essential information to the service. This allows the service to match the resource with the relevant permissions.
sequenceDiagram
Actor U as Authenticated User
Participant K as Kong
Participant KP as Kong Plugin
Participant S as Service
U->>K: Make Request
activate K
activate K
K->>KP: Intercept Request
deactivate K
activate KP
activate KP
KP->>KP : Evaluate
Note over KP: Based on Action Matching with the Permission
alt is Negative
KP-->>K : Set Status 403
Deactivate KP
Activate K
K -->> U : Response 403
deactivate K
else is Positive
Activate KP
KP-->>K : Continue
Deactivate KP
Note Over KP,K: Plugin enchance the request <br/> Header with Resource Matcher info
alt is Write
KP->>KP : Evaluate
Activate KP
Note over KP : Based on Resource Matcher from <br/>the Permission againts the Payload
alt is Negative
KP-->>K : Set Status 403
deactivate KP
activate K
K-->>U : Response 403
deactivate K
else is Positive
activate KP
KP-->>K : Continue
deactivate KP
deactivate KP
end
else is Read
activate K
K->>S : Forward Request
deactivate K
deactivate K
activate S
S->>S : Evaluate
Note Over S : Based on Resource Matcher from <br/>the Permission againts the persisted resource <br/> Using the library
alt is Match
S-->>U : Response 200
else is Unmatch
S-->>U : Response 403
end
deactivate S
end
end
Static Permission in Header #
In this experiment, authenticated users receive static permissions in the header. I solely focus on the post-authentication evaluation process, highlighting a stateless authorization approach. Although permissions can be embedded in JWT tokens, this might inflate the token size.
Given potential size concerns, I designed a concise permission format. Still, the experiment doesn’t completely address varying token sizes due to diverse user permissions. Thus, using external storage solutions like Redis could be a future consideration.
a single permission definition format as follow:
{
"a" : "people-view",
"s" : [
"a/userid/sq:123"
"a/type/si:admin,manager"
]
}
from example above, the user is granted to do “people-view” action, allow for userid equal to 1234, or allow for type in admin and manager. the machanism to breakdown the matcher as follow:
- split the string with
/
- index 0 is for the Effect, it used constant to Map
a
forAllow
,d
forDeny
- the next index is pair key value & operator
["userid", "sq:123"]
it meansuserid
user string equal operator to match123
Available Effect constant are
Short | Definition |
---|---|
d | for Deny |
a | for Allow |
fd | for Forwarding Deny for Upstream service used to Match with the resource |
fa | for Forwarding Allow for Upstream service used to Match with the resource |
Available Matching operator arb
Short | Definition |
---|---|
sq | String Equal |
si | String Included in Array |
bt | Boolean True |
bf | Boolean false |
Demo Source Code #
The source code complete with an Docker compose of it available here. what Available in this demo
- Kong (with the plugin, named kong-warden) for API
http://localhost:8000/
& admin :http://localhost:8001/
- Konga (for kong UI), access through
http://localhost:1337/
, creds : usenamekong
, password :kongkong
- postgres for kong & konga db
- sample service called
people_service
How to start #
Start navigating to the source directory then run docker compose.
$ docker-compose up
the docker compose had configured everything for you. add service,routes,configured the plugin for the people_service
People Service #
in this service, it’s expose a very basic REST API written in go, use echo. it simulates the storage by reading a static file people.json
Method | Path | Description | Action Tag |
---|---|---|---|
GET | / | for index | people-index |
GET | /:id | for view single resource | people-view |
POST | / | for create new resource | people-create |
PUT | /:id | for update a resource | people-update |
a single resource looks like this.
{
"id": "64ca72af2a14ca3ffc10faa2",
"guid": "1a6a07d2-8919-4d44-a3de-cf744fbdf77e",
"isActive": true,
"balance": 3552.75,
"picture": "http://placehold.it/32x32",
"age": 38,
"eyeColor": "blue",
"name": "Corrine Roy",
"gender": "female",
"company": "DADABASE"
}
Testing the endpoint #
- Get People index response everything
$ curl --location 'http://localhost:8000/people/' \ --header 'Content-Type: application/json' \ --header 'X-Warden-Permissions: [{"a":"people-index"}]'
- get People index response only for
eyeColor
field with value equal toblue
$ curl --location 'http://localhost:8000/people/' \ --header 'Content-Type: application/json' \ --header 'X-Warden-Permissions: [{"a":"people-index","s":["fa/eyeColor/sq:blue"]}]'
- Response 200, when creating people with
eyeColor
=blue
$ curl --location 'http://localhost:8000/people/' \ --header 'Content-Type: application/json' \ --header 'X-Warden-Permissions: [{"a":"people-create","s":["fa/eyeColor/sq:blue"]}]' \ --data '{ "age": 38, "balance": 3552.75, "company": "DADABASE", "eyeColor": "blue", "gender": "female", "guid": "1a6a07d2-8919-4d44-a3de-cf744fbdf77e", "id": "64ca72af2a14ca3ffc10faa2", "isActive": true, "name": "Corrine Roy", "picture": "http://placehold.it/32x32" }'
- Response 403, when creating people with
eyeColor
!=blue
$ curl --location 'http://localhost:8000/people/' \ --header 'Content-Type: application/json' \ --header 'X-Warden-Permissions: [{"a":"people-create","s":["fa/eyeColor/sq:blue"]}]' \ --data '{ "age": 38, "balance": 3552.75, "company": "DADABASE", "eyeColor": "yellow", "gender": "female", "guid": "1a6a07d2-8919-4d44-a3de-cf744fbdf77e", "id": "64ca72af2a14ca3ffc10faa2", "isActive": true, "name": "Corrine Roy", "picture": "http://placehold.it/32x32" }'
if you use postman to try this curl, you can easily playing around by modifying the header
X-Warden-Permissions
to see how the response will tailored accordingly.
Conclusion #
managing authorization in microservices can be intricate. While tools like Casbin and OPA help, they may simply relocate the complexity. my exploration into reimplementing the AWS IAM process suggests a potentially simpler and more integrated solution. However, the effectiveness of such a tool will depend on the specific microservices system in use. Hence, it is crucial to continuously innovate and tailor solutions for effective authorization management.