Every time a customer clicks Place order, a vendor updates a product listing, or a support agent pulls up a case, a silent question is being resolved: Is this action allowed? In e-commerce, that question is answered dozens of times per session, across multiple roles and overlapping data boundaries. Getting it wrong has real consequences. The retail sector recorded $3.54 million worth of data breaches in 2025, an increased value from the $3.48 million in 2024. Nearly half of those breaches involved customers' personally identifiable information.
E-commerce platforms are complex by nature. A single transaction touches customer data, payment details, inventory records, and fulfillment workflows, all in real time. The access rules that govern each of those interactions rarely stay simple for long. A new vendor tier, a compliance requirement, or a seasonal surge in support volume can each expose gaps in a rigid permissions model. Cerbos is designed specifically for this: an open-source, language-agnostic authorization layer that evaluates access decisions against policies you define, separately from your application code.
This article walks through three representative scenarios that show how real e-commerce business rules translate into authorization policies using Cerbos. We will cover customer data access, vendor product management, and order lifecycle control. Each scenario starts with the business requirement and ends with a working policy. If you want to follow along hands-on, the Cerbos quickstart gets a local policy decision point running in under five minutes.
Authorization in e-commerce: why the basics break fast
Authorization decides what an authenticated user can do. Authentication tells you who they are. Authorization tells you what they are allowed to touch. The distinction matters because most e-commerce security incidents are not about someone breaking in with stolen credentials. They are about someone with valid credentials doing something they should not be allowed to do.
According to Verizon's 2025 DBIR Retail Snapshot, the retail sector logged 837 incidents last year with 419 confirmed breaches, and the threat mix looks exactly like what you'd expect: System Intrusion, Social Engineering, and Basic Web Application Attacks made up 93% of them. Financially motivated actors still dominate (100% of retail breaches), but the more interesting signal is the jump in Espionage-motivated attacks, which went from essentially zero (1%) to 9% in a single year. That's not noise. That's a shift worth paying attention to.
On the bot side, the 2025 Imperva Bad Bot Report puts retail's bad bot share at 59% of all web traffic, well above the global average of 37%, and 44% of advanced bot attacks are going straight at APIs. If your inventory, pricing, or product APIs aren't behind solid access controls, they're already being probed.
The pattern is consistent enough that OWASP put broken access control at the top of its security risk list, where it has remained across multiple editions, detected in 94% of tested applications. Their framing of the problem is precise: failures occur when users can "act outside of their intended permissions," leading to unauthorized data access or the execution of business functions beyond a user's limits. The mechanism is familiar to anyone who has maintained a multi-tenant platform: a roles table gets you started, exceptions accumulate, and the gap between what the model says and what the business actually requires quietly widens.
The most common starting point for access control is Role-Based Access Control. Assign someone the "vendor" role, define what vendors can do, and enforce it. This works until the rules get more specific. A vendor should only be able to edit their own products, not every product on the platform. A customer service representative should only be able to view orders assigned to them, not the entire order database. A senior support agent working a high-priority case should have permissions a junior agent on a routine inquiry does not.
These are not edge cases. They are standard operating requirements in any multi-vendor marketplace. RBAC alone handles the first layer. To handle the rest, you need Attribute-Based Access Control, where access decisions factor in user attributes, resource attributes, and context. The table below shows the difference in practice.
| Model | Definition | Example |
|---|---|---|
| RBAC (Role-Based Access Control) | Access is determined by the role assigned to the user. | If role = "vendor" then ALLOW update on product listings. |
| ABAC (Attribute-Based Access Control) | Access is determined by attributes of the user, the resource, and the environment. | If vendor.owned_products contains product_id AND product.stock_amount > 0 then ALLOW update. |
| PBAC (Policy-Based Access Control) | A structured combination of RBAC and ABAC expressed as explicit policies. | Policy: ALLOW vendor to update product IF vendor owns product AND product is in stock AND vendor status is active. |
In practice, e-commerce platforms benefit most from PBAC: policies that combine role membership with attribute conditions. This is the approach the scenarios below use. Each policy is expressed and evaluated using Cerbos, which externalizes authorization logic from application code and lets you manage, test, and audit access rules as a standalone layer.
Scenario 1: Customer data access
Consider a marketplace platform where multiple roles need to interact with customer records, but each has different access requirements. A customer can view and update their own profile. An admin can view any customer record and delete records that fail compliance checks. A customer service representative can only view records assigned to them, and can update a record only when the case is high priority.
This setup already contains several conditions that cannot be expressed through role membership alone. The customer service restriction is particularly instructive: the same role gets different permissions depending on a resource attribute (case priority) and a relationship attribute (whether the record is assigned to that agent). That combination requires ABAC.
This matters beyond functionality. GDPR and similar regulations require platforms to demonstrate that personal data access is governed by documented, auditable controls. According to a report by DLA Piper, GDPR fines resulted in $1.2 billion in 2024. Authorization policies that enforce data minimization and access scoping are part of how you avoid being on the wrong side of that number.
Principals, actions, and resources
Principals: Customer, Admin, Customer Service Representative
Actions: view, update, delete
Resource: Customer Data (personal details, contact information, account preferences)
Authorization policy
Allow access to customer data based on ownership, assignment, and case priority. Customers can view and update their own records. Admins can view any record and delete non-compliant ones. Customer service representatives can view records assigned to them, and can update those records only when the case priority is high.
Key attributes
| Category | Attribute | Description |
|---|---|---|
| Principal | assigned_customer_data | The customer records linked to the CSR |
| Principal | group | Operational team; must be "customer_service" for CSR rules |
| Resource | ownedBy | The customer ID that owns the record |
| Resource | isCompliant | Whether the record meets compliance requirements |
| Resource | case_priority | Priority level assigned to the customer case |
Cerbos policy
derived_roles.yaml
apiVersion: api.cerbos.dev/v1
derivedRoles:
name: customer\_data\_roles
definitions:
\- name: record\_owner
parentRoles: \["customer"\]
condition:
match:
expr: request.resource.attr.ownedBy \== request.principal.id
\- name: assigned\_csr
parentRoles: \["customer\_service\_representative"\]
condition:
match:
all:
of:
\- expr: request.principal.attr.group \== "customer\_service"
\- expr: request.resource.id in request.principal.attr.assigned\_customer\_data
resource_policy.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: "default"
resource: "customer\_data"
importDerivedRoles:
\- customer\_data\_roles
rules:
\- actions: \["view"\]
effect: EFFECT\_ALLOW
derivedRoles: \["record\_owner"\]
\- actions: \["view", "update"\]
effect: EFFECT\_ALLOW
roles: \["admin"\]
\- actions: \["delete"\]
effect: EFFECT\_ALLOW
roles: \["admin"\]
condition:
match:
expr: request.resource.attr.isCompliant \== false
\- actions: \["view"\]
effect: EFFECT\_ALLOW
derivedRoles: \["assigned\_csr"\]
\- actions: \["update"\]
effect: EFFECT\_ALLOW
derivedRoles: \["assigned\_csr"\]
condition:
match:
expr: request.resource.attr.case\_priority \== "high"
\- actions: \["update"\]
effect: EFFECT\_ALLOW
derivedRoles: \["record\_owner"\]
Note: Derived roles encode the ownership and assignment relationships cleanly. The record_owner role is granted dynamically when the customer's ID matches the resource's ownedBy attribute. The assigned_csr role is granted when the CSR belongs to the customer_service group and the record ID appears in their assigned_customer_data list. From there, the resource policy applies conditions on top of role membership.
This policy prevents a CSR from browsing the full customer database, blocks admins from deleting compliant records arbitrarily, and ensures customers are contained to their own data. Each restriction reflects a real business rule, and each is expressed once in the policy rather than scattered across application handlers.
Scenario 2: Vendor product management
Product inventory is one of the most frequently accessed resources on any marketplace. Vendors update listings, adjust prices, and manage stock. Admins oversee the catalog. Customers and support agents can read product data but should not be able to modify it. The rules that govern this seem straightforward until you get into the specifics.
A vendor should only be able to edit their own products. They should not be able to update a listing if the product is out of stock, since there is nothing to fulfill. A new vendor can only create listings if their account status is active , which prevents suspended or under-review sellers from adding products. An admin can update any product regardless of stock, and all authenticated users can view available inventory.
Unauthorized product modifications and inventory manipulation are among the top vectors for platform abuse in multi-vendor marketplaces. Global e-commerce fraud losses reached $48 billion in 2023, and for every $100 in fraudulent orders, businesses lose an average of $207 when factoring in wholesale costs, shipping, chargeback fees, and processing. Authorization controls on inventory data are not just operational hygiene. They directly affect platform integrity and vendor trust.
Principals, actions, and resources
Principals: Customer, Vendor, Admin, Customer Service Representative
Actions: view, update, create
Resource: Product Inventory (product listings, stock quantities, pricing, availability)
Authorization policy
Allow vendors to view and update only the products they own, provided stock is available. Allow vendors to create new listings only if their account is active. Admins can update any product. Customers and CSRs can view products that are in stock. All view access requires the user to be authenticated.
Key attributes
| Category | Attribute | Description |
|---|---|---|
| Principal | owned_products | Set of product IDs the vendor manages |
| Principal | vendor_status | Whether the vendor is active or inactive |
| Principal | isAuthenticated | Whether the user has successfully logged in |
| Resource | ownedBy | The vendor ID that owns this product |
| Resource | stock_amount | Current inventory count for the product |
Cerbos policy
derived_roles.yaml
apiVersion: api.cerbos.dev/v1
derivedRoles:
name: inventory\_roles
definitions:
\- name: product\_owner
parentRoles: \["vendor"\]
condition:
match:
all:
of:
\- expr: request.resource.id in request.principal.attr.owned\_products
\- expr: request.resource.attr.ownedBy \== request.principal.id
\- name: active\_vendor
parentRoles: \["vendor"\]
condition:
match:
expr: request.principal.attr.vendor\_status \== "active"
\- name: authenticated\_user
parentRoles: \["customer", "customer\_service\_representative"\]
condition:
match:
expr: request.principal.attr.isAuthenticated \== true
resource_policy.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: "default"
resource: "product\_inventory"
importDerivedRoles:
\- inventory\_roles
rules:
\- actions: \["view"\]
effect: EFFECT\_ALLOW
derivedRoles: \["authenticated\_user", "product\_owner"\]
condition:
match:
expr: request.resource.attr.stock\_amount \> 0
\- actions: \["view"\]
effect: EFFECT\_ALLOW
roles: \["admin"\]
\- actions: \["update"\]
effect: EFFECT\_ALLOW
derivedRoles: \["product\_owner"\]
condition:
match:
expr: request.resource.attr.stock\_amount \> 0
\- actions: \["update"\]
effect: EFFECT\_ALLOW
roles: \["admin"\]
\- actions: \["create"\]
effect: EFFECT\_ALLOW
derivedRoles: \["active\_vendor"\]
Note: The product_owner derived role combines two checks: the resource ID must be in the vendor's owned_products list, and the resource's ownedBy attribute must match the vendor's principal ID. Both conditions must be true before any update permission is considered. This dual check prevents partial ownership claims and ensures the resource attribute and principal attribute are consistent with each other.
The stock check on view access is deliberate. Showing customers products that cannot be fulfilled creates support overhead and erodes trust. Filtering at the authorization layer rather than the UI layer means it is enforced consistently, regardless of which interface or API client is making the request.
Scenario 3: Order lifecycle management
Orders are the most sensitive resource in an e-commerce system. They carry payment details, delivery addresses, and transaction history. The access rules around them are also the most time-sensitive, because orders move through a lifecycle and the permissions at each stage differ.
A customer can view and update their own orders, but only while the order is still pending. Once completed or rejected, they lose write access. A vendor can view and update orders relevant to their products while pending. An admin can view all orders without restriction. A customer service representative can view orders assigned to them, and can update those orders, but only after at least 24 hours have elapsed since order creation. This time-based condition is a real operational requirement: it prevents premature intervention in orders that are still being processed automatically.
The stakes here are high. Returns fraud and abuse accounted for 13.7% of all e-commerce returns in 2023, amounting to over $100 billion in losses for retailers. Orders that can be modified freely, by anyone with access, at any point in the lifecycle, are a major vector for that kind of abuse. The 24-hour hold and the pending-only update window are policy-level defenses against it.
Principals, actions, and resources
Principals: Customer, Vendor, Admin, Customer Service Representative
Actions: view, update
Resource: Order Information (purchase transactions, payment details, delivery statuses)
Authorization policy
Allow customers to view their own orders and update pending ones. Allow vendors to view and update orders assigned to them while pending. Allow admins to view all orders. Allow customer service representatives to view orders assigned to them, and update those orders only after 24 hours have elapsed since order creation.
Key attributes
| Category | Attribute | Description |
|---|---|---|
| Principal | assigned_orders | The order IDs assigned to the CSR or vendor |
| Resource | ownedBy | The customer ID that placed the order |
| Resource | order_status | Current status: pending, completed, or rejected |
| Resource | creation_date | ISO 8601 timestamp of when the order was created |
Cerbos policy
derived_roles.yaml
apiVersion: api.cerbos.dev/v1
derivedRoles:
name: order\_roles
definitions:
\- name: order\_owner
parentRoles: \["customer"\]
condition:
match:
expr: request.resource.attr.ownedBy \== request.principal.id
\- name: assigned\_vendor
parentRoles: \["vendor"\]
condition:
match:
expr: request.resource.id in request.principal.attr.assigned\_orders
\- name: assigned\_csr\_order
parentRoles: \["customer\_service\_representative"\]
condition:
match:
expr: request.resource.id in request.principal.attr.assigned\_orders
resource_policy.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: "default"
resource: "order\_information"
importDerivedRoles:
\- order\_roles
constants:
local:
PENDING\_STATUS: "pending"
CSR\_HOLD\_DURATION: "24h"
rules:
\- actions: \["view"\]
effect: EFFECT\_ALLOW
derivedRoles: \["order\_owner", "assigned\_vendor", "assigned\_csr\_order"\]
\- actions: \["view"\]
effect: EFFECT\_ALLOW
roles: \["admin"\]
\- actions: \["update"\]
effect: EFFECT\_ALLOW
derivedRoles: \["order\_owner"\]
condition:
match:
expr: request.resource.attr.order\_status \== constants.PENDING\_STATUS
\- actions: \["update"\]
effect: EFFECT\_ALLOW
derivedRoles: \["assigned\_vendor"\]
condition:
match:
expr: request.resource.attr.order\_status \== constants.PENDING\_STATUS
\- actions: \["update"\]
effect: EFFECT\_ALLOW
roles: \["admin"\]
\- actions: \["update"\]
effect: EFFECT\_ALLOW
derivedRoles: \["assigned\_csr\_order"\]
condition:
match:
all:
of:
\- expr: request.resource.attr.order\_status \== constants.PENDING\_STATUS
\- expr: timestamp(request.resource.attr.creation\_date).timeSince() \>=
duration(constants.CSR\_HOLD\_DURATION)
Note: The 24-hour hold on CSR updates is expressed as a time-based condition using Cerbos's timestamp() and timeSince() functions. This condition is evaluated at request time against live data. If a CSR tries to update an order 20 hours after creation, the request is denied. At hour 25, it succeeds. No application-level timer or separate job is needed to manage this. The policy handles it. Using local constants for the status string and the duration value also means that if the business rule changes from 24 hours to 48 hours, you update it in one place.
Testing your policies with Cerbos
Policies that are not tested are policies that will surprise you. Cerbos includes a built-in test framework that lets you write test cases alongside your policy files. You define a principal, an action, a resource, and the expected outcome, and Cerbos verifies that the policy behaves correctly. You can explore all three scenarios from this article in the Cerbos Playground, where you can experiment with the policies and run test cases directly in the browser.
A test for the CSR 24-hour order hold looks like this:
order_csr_update_test.yaml
name: "CSR cannot update order before 24 hours"
description: "Verify the time-based hold on CSR order updates"
principals:
csr\_agent:
id: "csr\_001"
roles: \["customer\_service\_representative"\]
attributes:
group: "customer\_service"
assigned\_orders: \["order\_3", "order\_5"\]
resources:
recent\_order:
kind: "order\_information"
id: "order\_3"
attributes:
order\_status: "pending"
creation\_date: "2025-07-12T10:00:00.000Z" \# less than 24h ago
ownedBy: "customer\_7"
tests:
\- name: "Deny update before 24h window"
input:
principals: \["csr\_agent"\]
resources: \["recent\_order"\]
actions: \["update"\]
expected:
\- principal: csr\_agent
resource: recent\_order
actions:
update: EFFECT\_DENY
Running this test against the policy confirms the denial. Swap the creation_date to more than 24 hours in the past and rerun, the outcome flips to EFFECT_ALLOW. This kind of explicit, reproducible verification is how you build confidence that a policy behaves correctly across all the cases that matter, not just the happy path.
Decoupled authorization and why it matters at scale
All three scenarios share a structural pattern: authorization logic lives in policy files, not in application code. The application sends a request to Cerbos with a principal, an action, and a resource. Cerbos evaluates the policies and returns a decision. The application enforces it.
This separation has real consequences for how platforms scale. When access rules are embedded in application code, every change means a code change, a review cycle, a deployment. When a rule is wrong in production, rolling it back requires the same process. With externalized policies, you update the policy, test it, and push it through Cerbos Hub without touching the application. The policy propagates to all running PDP instances without restarts.
For e-commerce platforms specifically, this matters because access rules change frequently. New vendor tiers appear. Support teams get reorganized. A compliance requirement adds a new condition to customer data access. With Cerbos, those changes are policy updates, not engineering tickets. The teams responsible for compliance and operations can review and validate policy changes directly, because the policies are readable and testable in isolation.
The compliance dimension is increasingly difficult to ignore. Data privacy compliance requirements in e-commerce under GDPR, PCI DSS, and similar regulations require platforms to demonstrate that personal data access is governed by documented, auditable controls. Externalized authorization policies are that documentation. Every access decision is backed by a policy rule, and Cerbos Hub provides structured audit logs that show which policy evaluated which request and what the outcome was.
Key takeaways
-
RBAC alone is not enough for e-commerce. Role membership covers the first layer, but multi-vendor platforms require attribute conditions to express ownership, assignment, and context.
-
Derived roles in Cerbos let you encode relationships cleanly. Ownership and assignment conditions become named roles that resource policies can reference directly.
-
Time-based conditions are a first-class feature. The 24-hour CSR hold in Scenario 3 is expressed in one policy expression and evaluated at request time with no external state.
-
Stock and status conditions belong in authorization. Filtering unavailable products or blocking updates on completed orders at the policy layer means the rule is enforced consistently across all clients and APIs.
-
Externalized policies reduce engineering overhead. Business rules that change frequently, like vendor tiers or support team assignments, can be updated in policy without touching application code.
-
Test your policies the same way you test your code. Cerbos's built-in test framework catches edge cases before they reach production.
-
Audit logs from Cerbos Hub provide the traceability that compliance with GDPR, PCI DSS, and similar standards requires.
Conclusion
E-commerce authorization is not a one-time problem. Platforms evolve, roles multiply, and compliance requirements tighten. A system that encodes access rules in application code becomes a liability the moment those rules need to change.
The three scenarios in this article cover the core authorization challenges in any marketplace: customer data access, vendor inventory management, and order lifecycle control. Each demonstrates how a business requirement becomes a testable, auditable policy. The patterns transfer directly to more complex cases, such as multi-region vendor restrictions, tiered support access, or dynamic pricing permissions tied to account attributes.
If you want to explore these policies hands-on, the Cerbos Playground has all three scenarios ready to run. If you are ready to move beyond the playground, try out Cerbos for free, or book a call with a Cerbos engineer to see how externalized authorization fits your platform.
Learn more aout multitenant authorization on this page, or feel free to explore our ebook One size does not fit all: A guide to multitenant authorization.
To explore mapping business requirements to authorization policy for other domains, check out the foundational guide, or dive into these blogs specific to the the fintech, insurance, utilities, aviation, automotive, MedTech and HR fields.
FAQ
Tagged in




