
Oro Security
- 4 installs
- 2 repo stars
- Updated July 22, 2026
- netresearch/orocommerce-skill
Helps with security tasks.
About
oro-security is a Claude Code skill for security. It helps solo builders move faster with AI-assisted coding.
- oro-security
- Security
- AI-coding skill
Oro Security by the numbers
- 4 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,742 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netresearch/orocommerce-skill --skill oro-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 2 |
| Last updated | July 22, 2026 |
| Repository | netresearch/orocommerce-skill ↗ |
What it does
Helps with security tasks.
Files
OroCommerce v6.1 Security & ACL Configuration
Entity ACL Configuration
Place in Resources/config/oro/acls.yml:
acls:
acme_demo_document_view:
type: entity
class: Acme\Bundle\DemoBundle\Entity\Document
permission: VIEW
description: View documents
acme_demo_document_edit:
type: entity
class: Acme\Bundle\DemoBundle\Entity\Document
permission: EDITKey structure: type: entity, class (FQCN), permission (VIEW, CREATE, EDIT, DELETE, ASSIGN, EXECUTE).
PHP 8 Acl Attribute
Declare ACL requirements directly on controller methods:
use Oro\Bundle\SecurityBundle\Attribute\Acl;
class DocumentController
{
#[Acl(
id: 'acme_demo_document_view',
type: 'entity',
class: 'Acme\Bundle\DemoBundle\Entity\Document',
permission: 'VIEW'
)]
public function viewAction(Document $document)
{
// ACL is checked before this method executes
}
}Attributes are preferred over YAML bindings — they're colocated with the code they protect and type-safe.
Ownership and Organization
Ownership determines who can access an entity. Four scopes: USER, BUSINESS_UNIT, ORGANIZATION, GLOBAL.
CRITICAL: If ownership is USER or BUSINESS_UNIT, the entity MUST have an organization field. Missing it causes permission checks to fail silently.
Define in Resources/config/oro/entity.yml:
entities:
Acme\Bundle\DemoBundle\Entity\Document:
ownership:
owner_type: USER
owner_field_name: owner
organization_field_name: organizationAclHelper: Query Filtering
ACL checks do NOT happen automatically on queries. You must explicitly apply filtering:
class DocumentRepository
{
public function findApprovedDocuments(User $user)
{
$qb = $this->createQueryBuilder('d')
->where('d.status = :status')
->setParameter('status', 'approved');
$this->aclHelper->apply($qb);
return $qb->getQuery()->getResult();
}
}Without $aclHelper->apply($qb), the query returns all entities regardless of permissions.
For custom access rules (AccessRuleInterface), see security-patterns.md.
Key Pitfalls
1. Query filtering is not automatic: Must call $aclHelper->apply($qb) explicitly. Common mistake: fetching all entities and filtering in PHP rather than at DB level.
2. AclAncestor skips object-level checks: Use AclAncestor only for class-level permission verification (list pages). For specific objects, use full Acl attributes or isGranted().
3. Organization field is mandatory for USER/BUSINESS_UNIT ownership: Omitting it causes permission checks to return false unexpectedly.
For AclAncestor details, custom permissions, field-level ACL, common patterns, debugging, and additional pitfalls, see security-patterns.md.
Version Notes
See permission-matrix.md for the full permission/ownership matrix, v6.1 notes, and v7.0 notes.
Permission and Ownership Matrix — OroCommerce v6.1
Permission Types
Six permission types control access to entities and actions:
| Permission | Applies To | Use Case |
|---|---|---|
VIEW | Entity fields | Read access |
CREATE | Entity class | Create new instances |
EDIT | Entity fields | Modify values |
DELETE | Entity instances | Remove from database |
ASSIGN | Entity instances | Change owner/reassign |
EXECUTE | Actions/Workflows | Trigger operations |
Ownership Scopes
Ownership determines the set of users who can access an entity based on their role/organization membership:
| Ownership Type | Accessible To | Requires | Use Case |
|---|---|---|---|
GLOBAL | All users | None | System-wide entities (Products, Categories) |
ORGANIZATION | Users in owning organization | organization field | Multi-tenant organization entities |
BUSINESS_UNIT | Users in owning business unit and parent units | organization, owner field | Department-level records |
USER | The owning user + their team leads | organization, owner field | Personal/user-owned records |
Permission Resolution Matrix
When a user requests permission on an entity, Symfony ACL + OroCommerce ownership rules determine the result:
For GLOBAL Ownership
User Role has VIEW permission on Entity class?
├─ Yes → Grant VIEW access to all instances
└─ No → DenyExample: All users can view Products (unless explicitly denied by role).
For ORGANIZATION Ownership
User Role has permission on Entity class?
├─ Yes
│ └─ Entity.organization == User.organization?
│ ├─ Yes → Grant
│ └─ No → Deny
└─ No → DenyExample: User can VIEW Document only if Document belongs to their organization.
For BUSINESS_UNIT Ownership
User Role has permission on Entity class?
├─ Yes
│ └─ Entity.owner.businessUnit in User.businessUnits or User.businessUnit.parent?
│ ├─ Yes → Grant
│ └─ No → Deny
└─ No → DenyExample: User can VIEW Document if they're in the owner's business unit or a parent unit.
For USER Ownership
User Role has permission on Entity class?
├─ Yes
│ └─ Entity.owner == User OR Entity.owner in User.subordinates?
│ ├─ Yes → Grant
│ └─ No → Deny
└─ No → DenyExample: User can VIEW Document if they own it or are the owner's manager.
Common Configurations
Product (Global, Public Read)
# entity.yml
Acme\Product\Entity\Product:
ownership:
owner_type: GLOBAL
# acls.yml
acme_product_view:
type: entity
class: Acme\Product\Entity\Product
permission: VIEWAll authenticated users can view all products.
Document (Organization-Scoped)
# entity.yml
Acme\Document\Entity\Document:
ownership:
owner_type: ORGANIZATION
organization_field_name: organization
# acls.yml
acme_document_view:
type: entity
class: Acme\Document\Entity\Document
permission: VIEWUsers can only view documents in their organization.
Order (Business Unit + User Owned)
# entity.yml
Acme\Order\Entity\Order:
ownership:
owner_type: BUSINESS_UNIT
owner_field_name: sales_manager
organization_field_name: organization
# acls.yml
acme_order_edit:
type: entity
class: Acme\Order\Entity\Order
permission: EDITSales managers can edit orders in their business unit; higher-level managers can edit orders in subordinate units.
Permission × Ownership Interaction
Scenario: User with EDIT on Organization-Owned Document
User Role: EDIT permission on Document class
Document: organization = Acme Corp (User in Acme Corp)
Result: GrantedScenario: User with DELETE on Organization-Owned Document
User Role: DELETE permission on Document class
Document: organization = Acme Corp (User NOT in Acme Corp)
Result: DeniedScenario: Manager with ASSIGN on Business Unit Document
User Role: ASSIGN permission on Document class, Manager of Business Unit B
Document: owner.businessUnit = Business Unit A (sibling, not subordinate)
Result: Denied (cannot assign across business units)Role-Based Access Control (RBAC) Setup
Roles define which permissions are granted to which users. In the UI, administrators create roles and assign permissions:
System > Users > Roles > Create Role
Name: Sales Manager
Permissions:
- Entity: Document
Permission: VIEW (Organization)
✓ Granted
- Entity: Document
Permission: EDIT (Organization)
✓ Granted
- Entity: Document
Permission: DELETE (Organization)
✗ DeniedThis role grants VIEW and EDIT on all documents in the user's organization, but prevents deletion.
Field-Level Permissions
Combine entity-level and field-level ACLs to restrict access to sensitive fields:
acme_document_view:
type: entity
class: Acme\Document\Entity\Document
permission: VIEW
acme_document_price_view:
type: entity
class: Acme\Document\Entity\Document
permission: VIEW
field_name: priceResolution: 1. User must have acme_document_view (entity-level) 2. If accessing the price field, must also have acme_document_price_view
Example:
{% if attribute_is_granted('VIEW', document) %}
<!-- Can view the document -->
<p>Name: {{ document.name }}</p>
{% if attribute_is_granted('VIEW', document, 'price') %}
<!-- Can view the price field -->
<p>Price: {{ document.price }}</p>
{% endif %}
{% endif %}Troubleshooting Permission Issues
Issue: User Can View But Not Edit
Check: 1. Entity ownership type matches user's organization/business unit 2. Role has EDIT permission granted in the UI 3. Custom access rules don't block EDIT 4. Field-level EDIT permissions (if defined)
Issue: ACL Grants Permission But Query Returns Empty
Check: 1. $aclHelper->apply($qb) is called on the query builder 2. Entity has proper ownership field populated 3. User's organization/business unit matches entity
Issue: Permission Matrix Shows Granted But isGranted() Returns False
Check: 1. Correct permission type (VIEW vs EDIT vs DELETE) 2. Correct object passed to isGranted() 3. User is authenticated (not Anonymous) 4. Custom access rules are not denying
Advanced: Custom Access Rules
Access rules filter queries based on complex logic beyond ownership. Example: allow viewing documents created after a date:
use Oro\Bundle\SecurityBundle\AccessRule\AccessRuleInterface;
use Oro\Bundle\SecurityBundle\AccessRule\Criteria;
use Oro\Bundle\SecurityBundle\AccessRule\Expr\Comparison;
use Oro\Bundle\SecurityBundle\AccessRule\Expr\Path;
class DocumentAccessRule implements AccessRuleInterface
{
#[\Override]
public function isApplicable(Criteria $criteria): bool
{
return true;
}
#[\Override]
public function process(Criteria $criteria): void
{
$criteria->andExpression(
new Comparison(new Path('owner'), Comparison::EQ, $currentUserId)
);
}
}Register with the correct service tag:
Acme\Bundle\DemoBundle\AccessRule\DocumentAccessRule:
tags:
- { name: oro_security.access_rule, type: ORM, entityClass: Acme\Bundle\DemoBundle\Entity\Document }This rule is applied automatically to all Document queries.
Access rules and role-based permissions compose: both conditions must be satisfied.
User allowed by role? AND Document passes access rule?
├─ Yes, Yes → Grant
└─ Otherwise → DenySecurity Patterns Reference
AclAncestor for Reuse
Reuse existing ACL definitions with AclAncestor. This checks class-level permissions only:
#[\Oro\Bundle\SecurityBundle\Attribute\AclAncestor(
id: 'acme_demo_document_view'
)]
public function listAction()
{
// Checks acme_demo_document_view permission on Document class
// Does NOT check object-level permissions on individual documents
return $this->render('list.html.twig');
}Critical limitation: AclAncestor does NOT check object-level permissions. It's suitable for list/index pages where you want to verify the user can view the entity type in general. For operations on specific objects, use full Acl attributes or explicit checks.
Action-Type ACLs
Action ACLs define non-entity permissions (for arbitrary operations):
acls:
acme_demo_bulk_export:
type: action
label: Bulk Export Documents
description: Export documents in bulkAction ACLs don't reference a class and are checked manually in code:
if (!$this->isGranted('acme_demo_bulk_export')) {
throw $this->createAccessDeniedException();
}Custom Permissions
Define application-specific permissions in Resources/config/oro/permissions.yml:
permissions:
DOCUMENT_BULK_EXPORT:
label: oro.acme.document.permissions.bulk_export.label
description: oro.acme.document.permissions.bulk_export.description
apply_to_entities:
- Acme\Bundle\DemoBundle\Entity\Document
group_names: [default]
DOCUMENT_ARCHIVE:
label: oro.acme.document.permissions.archive.label
apply_to_entities:
- Acme\Bundle\DemoBundle\Entity\DocumentCustom permissions are managed in the UI and checked with isGranted():
if (!$this->isGranted('DOCUMENT_BULK_EXPORT')) {
throw $this->createAccessDeniedException();
}Labels should be translation keys; use oro.acme.* namespace for consistency.
Important: Permissions without groups won't appear in the permissions matrix. Always specify group_names: [default] or a custom group.
AccessRuleInterface Full Example
Create custom query filtering logic by implementing AccessRuleInterface. The interface uses Criteria (not QueryBuilder) and expression objects:
namespace Acme\Bundle\DemoBundle\AccessRule;
use Oro\Bundle\SecurityBundle\AccessRule\AccessRuleInterface;
use Oro\Bundle\SecurityBundle\AccessRule\Criteria;
use Oro\Bundle\SecurityBundle\AccessRule\Expr\Comparison;
use Oro\Bundle\SecurityBundle\AccessRule\Expr\Path;
use Oro\Bundle\SecurityBundle\Authentication\TokenAccessorInterface;
class DocumentAccessRule implements AccessRuleInterface
{
public function __construct(
private readonly TokenAccessorInterface $tokenAccessor
) {}
#[\Override]
public function isApplicable(Criteria $criteria): bool
{
return true; // Tag options handle entity filtering; use for complex logic only
}
#[\Override]
public function process(Criteria $criteria): void
{
$criteria->andExpression(
new Comparison(
new Path('owner'),
Comparison::EQ,
$this->tokenAccessor->getUserId()
)
);
}
}Register in services.yml with entity class and type specified in the tag:
Acme\Bundle\DemoBundle\AccessRule\DocumentAccessRule:
tags:
- { name: oro_security.access_rule, type: ORM, entityClass: Acme\Bundle\DemoBundle\Entity\Document }The entityClass tag option filters which entities the rule applies to. The type option specifies the query type (typically ORM). The isApplicable() method provides additional runtime control beyond tag filtering.
Field-Level ACL
Restrict access to specific entity fields. Define in acls.yml:
acls:
acme_demo_document_price:
type: entity
class: Acme\Bundle\DemoBundle\Entity\Document
permission: VIEW
field_name: priceThis permission controls visibility of the price field specifically. The entity-level VIEW permission must also be granted to view any fields.
Use in Twig with attribute_is_granted():
{% if attribute_is_granted('VIEW', entity, 'price') %}
<p>Price: {{ entity.price }}</p>
{% endif %}In PHP, check with isGranted():
if ($this->isGranted('VIEW', $entity, 'price')) {
$price = $entity->getPrice();
}Note: The three-argument isGranted($permission, $object, $field) call is Oro's extended authorization checker (Oro\Bundle\SecurityBundle\Authorization\AuthorizationChecker), not standard Symfony. Standard Symfony isGranted() only accepts two arguments.
Binding ACLs to Controllers (YAML approach)
The bindings section in acls.yml auto-protects controller methods:
acls:
acme_demo_document_view:
type: entity
class: Acme\Bundle\DemoBundle\Entity\Document
permission: VIEW
bindings:
- class: Acme\Bundle\DemoBundle\Controller\DocumentController
method: viewActionBindings work by matching controller class + method name. If ACL fails, a 403 response is returned automatically. Note: Bindings protect the controller but don't prevent access in services.
Common Patterns
Protect a Service Method
public function approveDocument(Document $document): void
{
if (!$this->isGranted('EDIT', $document)) {
throw $this->createAccessDeniedException();
}
// ... approval logic
}Check Ownership
public function canModify(Document $document): bool
{
return $document->getOwner() === $this->getUser();
}Fetch Only Accessible Entities
$qb = $repository->createQueryBuilder('d');
$this->aclHelper->apply($qb, 'VIEW');
$documents = $qb->getQuery()->getResult();Check Permission in Twig
{% if is_granted('EDIT', entity) %}
<a href="{{ path('edit_route') }}">Edit</a>
{% endif %}Testing and Debugging
Check permissions in the backend UI at System > Users > Roles. Filter by entity/permission to verify ACL configuration.
For debugging:
// Check if user has permission
$isGranted = $this->isGranted('VIEW', $entity);
// With field-level
$isGranted = $this->isGranted('VIEW', $entity, 'price');
// Debug why denied — check ownership
if (!$isGranted) {
dump($entity->getOwner());
dump($this->getUser());
}Enable query logging to see ACL-filtered queries:
doctrine:
dbal:
logging: trueConsole Commands
Load and validate ACL definitions:
bin/console security:permission:configuration:loadThis command parses acls.yml and permissions.yml, validates syntax, and registers permissions. Run after modifying security config.
Additional Pitfalls
1. Bindings don't protect services: Bindings only protect controller entry points. Business logic in services must check ACL explicitly.
2. ACL caching: After modifying acls.yml, run security:permission:configuration:load. New permissions may not be visible in the UI until cache is cleared.
3. Attribute class paths must be exact: The class path in #[Acl(..., class: '...')] must match exactly (case-sensitive, full namespace).
4. Custom permissions require group assignment: Permissions without groups won't appear in the permissions matrix. Always specify group_names: [default] or custom group.
Security — v6.1 Notes
Key Changes from v5.1
- PHP 8 attributes introduced for ACL (preferred over
bindingsinacls.yml) AclHelperservice standardized for query filtering- Access rules (
AccessRuleInterface) expanded - Custom permissions (
permissions.yml) refined - Ownership types stabilized (GLOBAL, ORGANIZATION, BUSINESS_UNIT, USER)
Known Limitations
1. No attribute-level ownership: Ownership is entity-wide, not per-attribute 2. No dynamic field ACL: Field permissions are static, cannot change at runtime 3. No group-level permissions: Permissions apply to individual users, not ad-hoc groups 4. Query filtering is manual: Must call $aclHelper->apply() explicitly
Cache and Loading
ACL permissions are cached in the database and Symfony cache. After modifying acls.yml or permissions.yml:
bin/console cache:clear
bin/console security:permission:configuration:loadThe security:permission:configuration:load command validates YAML syntax and registers permissions in the database. Run during deployment.
Migration from v5.1
1. Replace bindings: in acls.yml with #[Acl(...)] on controller methods (both work in v6.1, attributes preferred) 2. Verify all createQueryBuilder() calls use $aclHelper->apply($qb) where ACL filtering is expected 3. Confirm all USER/BUSINESS_UNIT-owned entities have owner and organization fields 4. Test custom AccessRuleInterface implementations 5. Run security:permission:configuration:load after all changes
Performance
ACL checks add query complexity. For bulk operations:
- Load IDs first, then filter by ACL
- Use
AclHelper::apply()once per batch, not per entity - Cache permission results within request scope
- Use queued operations for large bulk exports
Security — v7.0 Notes
v7.0 is not yet released. This file will be updated when v7.0 stabilizes.
Expected Changes
- TBD