Navigating Authz in Microservices: From Traditional Approach to a Reimagined AWS IAM Evaluation

go, kong, authz

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:

  1. Ascertain if a user is authorized to perform a specific action.
  2. Ensure that the retrieved (persisted) resource corresponds to the user’s permissible resource attributes.
  3. For new data actions (creation/update), check its alignment with the user’s permissible resource attributes.
  4. An explicit effect logic, gauging if an action is permissible or restricted, essentially toggling between affirmation and negation.
  5. 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:

  1. split the string with /
  2. index 0 is for the Effect, it used constant to Map a for Allow, d for Deny
  3. the next index is pair key value & operator ["userid", "sq:123"] it means userid user string equal operator to match 123

Detail Selector Spec

Available Effect constant are

ShortDefinition
dfor Deny
afor Allow
fdfor Forwarding Deny for Upstream service used to Match with the resource
fafor Forwarding Allow for Upstream service used to Match with the resource

Available Matching operator arb

ShortDefinition
sqString Equal
siString Included in Array
btBoolean True
bfBoolean false

Demo Source Code #

The source code complete with an Docker compose of it available here. what Available in this demo

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

MethodPathDescriptionAction Tag
GET/for indexpeople-index
GET/:idfor view single resourcepeople-view
POST/for create new resourcepeople-create
PUT/:idfor update a resourcepeople-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 #

  1. Get People index response everything
    $ curl --location 'http://localhost:8000/people/' \
    --header 'Content-Type: application/json' \
    --header 'X-Warden-Permissions: [{"a":"people-index"}]'
    
  2. get People index response only for eyeColor field with value equal to blue
    $ curl --location 'http://localhost:8000/people/' \
    --header 'Content-Type: application/json' \
    --header 'X-Warden-Permissions: [{"a":"people-index","s":["fa/eyeColor/sq:blue"]}]'
    
  3. 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"
    }'
    
  4. 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.