Authorization and Amazon Verified Permissions - A New Way to Manage Permissions Part XI: Batch Authorization
Tags: aws, avp, authorization, cedar
Published: 2023-12-06 17:12:10

Hello again in the series about building authorization with Amazon Verified Permissions (AVP) and Cedar. In the previous post, we focused on the [AVP-CLI tool](https://dev.to/aws-builders/authorization-and-amazon-verified-permissions-a-new-way-to-manage-permissions-part-x-avp-cli-aih) (which we will also use today). This time, we will focus on a new feature called batch authorization, which, in my opinion, is a game changer in terms of using AVP.

## Batch Authorization

[Batch authorization](https://aws.amazon.com/about-aws/whats-new/2023/11/amazon-verified-permissions-supports-batch-authorization/) in AVP allows you to process up to 30 authorization decisions for a single principal or resource in a single API call. The entities of an API request can contain up to 100 principals and up to 100 resources.

One of the many objections to AVP was the theory that it is too expensive as a service. Now, where we can make many requests in one, it allows for a significant reduction in [costs](https://aws.amazon.com/verified-permissions/pricing/) and an increase in performance, as everything goes in one request (**and we will pay just for one request**).

## `isAuthorized` vs `batchIsAuthorized`

Traditionally, the `isAuthorized` action in AVP is used to process a single authorization request (If you use Test Bench in the AVP console, the AVP is using this API operation to make the authorization decision). This involves sending an individual request for each authorization decision, which can be resource-intensive, especially when multiple decisions are needed.

An example of an `isAuthorized` request structure can be found below:
```json
{
    "policyStoreId": "your-policy-store-id",
    "principal": {
        "entityType": "Principal::Type",
        "entityId": "entity_id"
    },
    "action": {
        "actionType": "Action::Type",
        "actionId": "action_id"
    },
    "resource": {
        "entityType": "Resource::Type",
        "entityId": "resource_id"
    },
    "entities": {
        "entityList": []
    },
    "context": {
        "contextMap": {}
    }
}
```

This JSON file outlines the format for a typical single authorization request, including details about the principal, action, resource, entities, and context. More examples of authorization requests you can found in the avp-cli [scenarios](https://github.com/Pigius/avp-cli/tree/main/scenarios).

The response looks like this:

```json
response {
  decision: 'ALLOW',
  determiningPolicies: [ { policyId: 'ID_OF_THE_POLICY' } ],
  errors: []
}
```
It contains:
- **decision** - authz decision that indicated if the authorization request should be allowed or denied.
- **determiningPolicies** - The list of determining policies used to make the authorization decision.
- **errors** - Errors that occurred while making an authorization decision.

The `batchIsAuthorized` [action](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_BatchIsAuthorized.html), however, allows for processing multiple authorization requests in a single API call. This is particularly useful when you need to make several authorization decisions simultaneously, either for one principal across multiple resources or for one resource across multiple principals. The structure of a `batchIsAuthorized` request is as follows:

```json
{
   "policyStoreId": "your-policy-store-id",
   "entities": {
      "entityList": [
         {
            "identifier": {
               "entityType": "Principal::Type",
               "entityId": "entity_id"
            },
            "attributes": {},
            "parents": []
         },
         {
            "identifier": {
               "entityType": "Resource::Type",
               "entityId": "entity_id"
            },
            "attributes": {},
            "parents": []
         },
         // Additional entities can be included here
      ]
   },
   "requests": [
      {
         "principal": {
            "entityType": "Principal::Type",
            "entityId": "entity_id"
         },
         "action": {
            "actionType": "Action::Type",
            "actionId": "action_id"
         },
         "resource": {
            "entityType": "Resource::Type",
            "entityId": "resource_id"
         },
         "context": {
            "contextMap": {}
         }
      },
      // Additional requests can be included here
   ]
}
```
In this format, the entities section defines a common list of entities (principals and resources) involved in the batch request. The requests array then specifies individual authorization requests, each comprising a principal, action, resource, and context.

The response looks like this:
```json
"results": [
    {
      "decision": "ALLOW",
      "determiningPolicies": [
        {
          "policyId": "policyId"
        }
      ],
      "errors": [],
      "request": {
        "action": {
          "actionType": "EcommerceStore::Action",
          "actionId": "View"
        },
        "context": {
          "contextMap": {}
        },
        "principal": {
          "entityType": "EcommerceStore::User",
          "entityId": "Ken"
        },
        "resource": {
          "entityType": "EcommerceStore::Product",
          "entityId": "Hat"
        }
      }
    },
    {
      "decision": "ALLOW",
      "determiningPolicies": [
        {
          "policyId": "policyId"
        }
      ],
      "errors": [],
      "request": {
        "action": {
          "actionType": "EcommerceStore::Action",
          "actionId": "GetDiscount"
        },
        "context": {
          "contextMap": {}
        },
        "principal": {
          "entityType": "EcommerceStore::User",
          "entityId": "Ken"
        },
        "resource": {
          "entityType": "EcommerceStore::Product",
          "entityId": "Hat"
        }
      }
    },
...
```
As we can see, we have `results` which is a series of Allow or Deny decisions for each request and the policies that produced them. We have an array of authorization decision objects, and with that, we have a `request` key, which is the authorization request that initiated the decision.

## E-commerce example
To demonstrate the power of batch authorization, I've added a new scenario in the AVP CLI tool - the [Batch Scenario](https://github.com/Pigius/avp-cli/blob/main/scenarios/ecommerceBatchScenario/ecommerceBatchScenario.json) Scenario. This scenario simulates an e-commerce platform with various user roles, actions, and policies.

## Schema Overview


![Schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kxjzraam9o6b4h71ibg9.png)




Our [schema](https://github.com/Pigius/avp-cli/blob/main/scenarios/ecommerceBatchScenario/schema.json) includes entities like `Product`, `Role`, `User`, and `Order`, each with specific attributes. Actions such as `View`, `Edit`, `Buy`, `GetDiscount`, and `Preorder` are defined to mimic typical e-commerce operations.
![Entity types](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/el7lawgpnuzwgeaynrd4.png)

## Policies

We've crafted policies that cater to different user roles and scenarios:

### Admin Role - Order Editing Policy

```json
permit(
    principal in EcommerceStore::Role::"admin",
    action in [EcommerceStore::Action::"Edit"],
    resource
)
when { resource.status == "paid" };
```
This policy allows users with the admin role to edit orders. However, it restricts editing based on the order's status. Admins can only edit orders that are in the "paid" status, ensuring that orders in other stages of processing are not altered.

### Customer Role - Basic Access Policy

```json
permit(
  principal in EcommerceStore::Role::"customer",
  action in [EcommerceStore::Action::"Buy", EcommerceStore::Action::"View"],
  resource
);
```
Customers are granted the ability to perform basic actions like `Buy` and `View` on products. This policy is straightforward and applies to all customers, allowing them to browse and purchase products from the e-commerce platform.

### Premium Membership - Special Privileges Policy

```json
permit (
    principal,
    action in
        [EcommerceStore::Action::"GetDiscount",
         EcommerceStore::Action::"Preorder"],
    resource
)
when { principal has premiumMembership && principal.premiumMembership == true };
```
This policy is designed for users with a premium membership. It allows them to access special actions like `GetDiscount` and `Preorder` on products. The policy checks if the user has a `premiumMembership` attribute set to `true`, indicating their premium status.

## Testing Policies

I've designed two types of tests for this scenario: single authorization requests and batch authorization requests, each serving a unique purpose in our testing strategy.

### Single Authorization Request Tests

These tests focus on individual authorization requests, assessing one action at a time. They help us verify the specific behavior of our policies in granular scenarios. Examples include:

**Admin Editing Paid Orders:** Checks if an admin user (Daniel) can edit orders with the status "paid."
**Customer Viewing Products:** Verifies that a customer (Tom) can view any product.
**Premium Member Discounts:** Ensures a premium member (Ken) can access discounts on products.

### Batch Authorization Tests

Batch tests are more comprehensive, allowing us to evaluate multiple authorization requests in a single API call. We've prepared two types of batch tests:

**Resource-Oriented Batch Test:** This test checks multiple resources against a single principal to determine what actions they can perform. For instance, it can assess which orders an admin user is authorized to edit. This approach is particularly useful for scenarios where a user's role or status might grant them varying levels of access across different resources.

**Principal-Oriented Batch Test:** Conversely, this test evaluates multiple actions for a single principal on a specific resource. It helps us understand the range of actions a user (like Ken) can perform on a particular product. This type of test is essential for applications where user permissions vary based on the resource they are interacting with.

To test the `BatchIsAuthorized`, we cannot do this through the Test Bench, where we can test our `isAuthorized` requests to AVP. To solve this problem, I added the option to test `batchIsAuthorized` to avp-cli. You need to select it from the manual approach option, select make a batch authorization decision, and provide a path to the json test file (use `structureBatchAuthorizationRequest.json` as a reference) It looks like this:

```bash
? What would you like to do? Manual approach
? What would you like to do? Make a batch authorization decision
? Enter the path for batch authorization json test file structureBatchAuthorizationRequest.json
Making batch authorization decision...
┌──────────┬──────────────────────────────┬────────────────────┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────┬──────────────────────────────
│ Decision │ Determining Policies         │ Errors             │ Policy Store ID              │ Principal                    │ Action                       │ Resource                     
├──────────┼──────────────────────────────┼────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────
│ ALLOW    │ Vz4ZUYv36HwY6v65XTix4i       │                    │ JahHZkVn82ryyn5qMfBXG9       │ EcommerceStore::User::Ken    │ EcommerceStore::Action::View │ EcommerceStore::Product::Hat 
├──────────┼──────────────────────────────┼────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────
│ ALLOW    │ 4ZbngosNQ8ZCbaBcEQ4SE9       │                    │ JahHZkVn82ryyn5qMfBXG9       │ EcommerceStore::User::Ken    │ EcommerceStore::Action::GetD │ EcommerceStore::Product::Hat 
│          │                              │                    │                              │                              │ iscount                      │                              
├──────────┼──────────────────────────────┼────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────
│ ALLOW    │ Vz4ZUYv36HwY6v65XTix4i       │                    │ JahHZkVn82ryyn5qMfBXG9       │ EcommerceStore::User::Ken    │ EcommerceStore::Action::Buy  │ EcommerceStore::Product::Hat 
├──────────┼──────────────────────────────┼────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────
│ ALLOW    │ 4ZbngosNQ8ZCbaBcEQ4SE9       │                    │ JahHZkVn82ryyn5qMfBXG9       │ EcommerceStore::User::Ken    │ EcommerceStore::Action::Preo │ EcommerceStore::Product::Hat 
│          │                              │                    │                              │                              │ rder                         │                              
├──────────┼──────────────────────────────┼────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────
│ DENY     │                              │                    │ JahHZkVn82ryyn5qMfBXG9       │ EcommerceStore::User::Ken    │ EcommerceStore::Action::Edit │ EcommerceStore::Product::Hat 
└──────────┴──────────────────────────────┴────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────
```

For our Batch Scenario, we have also the option to test prepared batch authorization test requests like this:

```bash
? What would you like to do? Test Batch Authorization Scenario
? Choose a scenario ecommerceBatchScenario
? Choose a test (Use arrow keys)
❯ Checking multiple actions for a single principal (For specific product, What action can be taken by Ken 
  Checking multiple resources, to check which orders can admin edit.
...
```
Remember to update `policy_store_id` in [here](https://github.com/Pigius/avp-cli/blob/main/scenarios/ecommerceBatchScenario/multiple_actions_batch_test.json#L2) and [here](https://github.com/Pigius/avp-cli/blob/main/scenarios/ecommerceBatchScenario/multiple_resource_batch_test.json#L2).

Example test structure:

```json
{
   "policyStoreId": "your-policy-store-id",
   "entities": {
      "entityList": [
         {
            "identifier": {
               "entityType": "EcommerceStore::User",
               "entityId": "Ken"
            },
            "attributes": {
               "premiumMembership": {
                  "boolean": true
               }
            },
            "parents": [
               {
                  "entityType": "EcommerceStore::Role",
                  "entityId": "customer"
               }
            ]
         },
         {
            "identifier": {
               "entityType": "EcommerceStore::Product",
               "entityId": "Hat"
            },
            "attributes": {},
            "parents": []
         }
      ]
   },
   "requests": [
      {
         "principal": {
            "entityType": "EcommerceStore::User",
            "entityId": "Ken"
         },
         "action": {
            "actionType": "EcommerceStore::Action",
            "actionId": "View"
         },
         "resource": {
            "entityType": "EcommerceStore::Product",
            "entityId": "Hat"
         },
         "context": {
            "contextMap": {}
         }
      },
      {
         "principal": {
            "entityType": "EcommerceStore::User",
            "entityId": "Ken"
         },
         "action": {
            "actionType": "EcommerceStore::Action",
            "actionId": "GetDiscount"
         },
         "resource": {
            "entityType": "EcommerceStore::Product",
            "entityId": "Hat"
         },
         "context": {
            "contextMap": {}
         }
      },
      {
         "principal": {
            "entityType": "EcommerceStore::User",
            "entityId": "Ken"
         },
         "action": {
            "actionType": "EcommerceStore::Action",
            "actionId": "Buy"
         },
         "resource": {
            "entityType": "EcommerceStore::Product",
            "entityId": "Hat"
         },
         "context": {
            "contextMap": {}
         }
      },
      {
         "principal": {
            "entityType": "EcommerceStore::User",
            "entityId": "Ken"
         },
         "action": {
            "actionType": "EcommerceStore::Action",
            "actionId": "Preorder"
         },
         "resource": {
            "entityType": "EcommerceStore::Product",
            "entityId": "Hat"
         },
         "context": {
            "contextMap": {}
         }
      },
      {
         "principal": {
            "entityType": "EcommerceStore::User",
            "entityId": "Ken"
         },
         "action": {
            "actionType": "EcommerceStore::Action",
            "actionId": "Edit"
         },
         "resource": {
            "entityType": "EcommerceStore::Product",
            "entityId": "Hat"
         },
         "context": {
            "contextMap": {}
         }
      }
   ]
}
```



## Conclusion

- Batch authorization in AVP offers a powerful and efficient way to handle multiple authorization requests simultaneously (up to 30 requests principal or request oriented, with up to 100 entities (principals and resources)). 
- It will decrease costs for requests to AVP as well as improve performance.
- Keep in mind that you cannot use the `BatchIsAuthorized` with the `token` option, it's not supported currently.
- Keep in mind that you cannot use the `BatchIsAuthorized` within the Test Bench currently.