6.2 Authorization Helpers
Last Updated: November 4, 2025
Purpose: Helper methods for authorization and permission management
getBranchIdsForAction()
Overview
A helper method in the Member entity that retrieves all branch IDs where a user has permission to perform a specific policy action. This is essential for populating dropdowns, autocomplete fields, and filtering queries to show only branches a user is authorized to work with.
Method Signature
public function getBranchIdsForAction(string $action, mixed $resource): ?array
Location: src/Model/Entity/Member.php
Parameters
$action (string): The policy action/method name without the ‘can’ prefix
- Examples:
'edit','view','delete','add' - Automatically converts to policy method format (e.g.,
'edit'→'canEdit')
$resource (mixed): The entity or table to check permissions for
- Can be a string table name:
'Members','Branches','Officers.Officers' - Can be an entity instance:
$membersTable->newEmptyEntity() - Can be a table instance:
$membersTable
Return Values
| Return Value | Meaning | Action |
|---|---|---|
null |
Global permission or super user | Load ALL branches |
[2, 11, 14, ...] |
Limited to specific branches | Load only these branches |
[] (empty array) |
No permission | Show error or hide UI |
How It Works
-
Super User Check: Returns
nullimmediately if user is a super user (bypass all restrictions) -
Resource Resolution: Converts string table names or table instances to entity instances
- Policy Class Resolution: Determines the correct policy class for the entity
'Members'→App\Policy\MembersPolicy'Officers.Officers'→Officers\Policy\OfficersPolicy- Entity instances → Policy based on entity namespace
- Policy Method Conversion: Converts action to policy method name
'edit'→'canEdit''view'→'canView'
-
Permission Lookup: Queries the user’s policies for the specific policy class and method
- Scope Evaluation: Returns appropriate value based on permission scoping rule
Permission::SCOPE_GLOBAL→ ReturnsnullPermission::SCOPE_BRANCH_ONLY→ Returns specific branch IDsPermission::SCOPE_BRANCH_AND_CHILDREN→ Returns branch IDs with hierarchy
Helper Methods
resolvePolicyClass(mixed $resource): ?string
Protected helper that resolves the policy class name from a resource:
- Handles table instances
- Handles entity instances
- Handles plugin entities
- Returns fully qualified policy class name
getPolicyClassFromTableName(string $tableName): string
Protected helper that converts table names to policy class names:
- Handles plugin tables (e.g.,
'Officers.Officers') - Handles standard app tables (e.g.,
'Members')
Usage Examples
Example 1: Basic Dropdown Population
// In controller action
public function add()
{
$member = $this->Members->newEmptyEntity();
$user = $this->Authentication->getIdentity();
// Get authorized branches
$branchIds = $user->getBranchIdsForAction('add', 'Members');
// Build branch list
if ($branchIds === null) {
// Global permission - all branches
$branches = $this->fetchTable('Branches')
->find('list')
->orderBy(['name' => 'ASC'])
->all();
} elseif (!empty($branchIds)) {
// Limited permission - specific branches
$branches = $this->fetchTable('Branches')
->find('list')
->where(['id IN' => $branchIds])
->orderBy(['name' => 'ASC'])
->all();
} else {
// No permission
$branches = [];
$this->Flash->error(__('You do not have permission to add members.'));
}
$this->set(compact('member', 'branches'));
}
Example 2: Query Filtering
// In controller index action
public function index()
{
$user = $this->Authentication->getIdentity();
$branchIds = $user->getBranchIdsForAction('view', 'Members');
$query = $this->Members->find();
// Apply branch filtering based on permissions
if ($branchIds === null) {
// No filtering needed - user can see all
} elseif (!empty($branchIds)) {
// Filter to authorized branches
$query->where(['Members.branch_id IN' => $branchIds]);
} else {
// No access - return empty result
$query->where(['1 = 0']);
}
$members = $this->paginate($query);
$this->set(compact('members'));
}
Example 3: AJAX Autocomplete Endpoint
// In controller
public function autocomplete()
{
$this->request->allowMethod(['get']);
$user = $this->Authentication->getIdentity();
// Get authorized branches
$branchIds = $user->getBranchIdsForAction('edit', 'Members');
$query = $this->fetchTable('Branches')->find();
// Apply search term
if ($term = $this->request->getQuery('term')) {
$query->where(['name LIKE' => "%{$term}%"]);
}
// Apply authorization filter
if ($branchIds === null) {
// Global - no filter needed
} elseif (!empty($branchIds)) {
$query->where(['id IN' => $branchIds]);
} else {
// No access - empty result
$query->where(['1 = 0']);
}
$branches = $query->limit(20)->all();
$this->set(compact('branches'));
$this->viewBuilder()->setOption('serialize', ['branches']);
}
Example 4: Conditional Template Rendering
// In template or element
<?php
$user = $this->getRequest()->getAttribute('identity');
$editBranches = $user->getBranchIdsForAction('edit', 'Members');
?>
<?php if ($editBranches !== []): ?>
<?= $this->Form->control('branch_id', [
'options' => $branches, // Passed from controller
'label' => __('Branch')
]); ?>
<?php else: ?>
<p class="text-danger">
<?= __('You do not have permission to edit members in any branch.') ?>
</p>
<?php endif; ?>
Example 5: Helper Method in Controller
Create a reusable helper method in AppController or specific controllers:
/**
* Get branches for a specific action, formatted for display
*
* @param string $action The action (edit, view, add, delete)
* @param string $resource The resource table name
* @return array List of branches or empty array
*/
protected function _getBranchesForAction(string $action, string $resource): array
{
$user = $this->Authentication->getIdentity();
$branchIds = $user->getBranchIdsForAction($action, $resource);
$branchesTable = $this->fetchTable('Branches');
if ($branchIds === null) {
// Global permission
return $branchesTable->find('list')
->orderBy(['name' => 'ASC'])
->all()
->toArray();
}
if (empty($branchIds)) {
// No permission
$this->Flash->error(__('You do not have permission for this action.'));
return [];
}
// Limited permission
return $branchesTable->find('list')
->where(['id IN' => $branchIds])
->orderBy(['name' => 'ASC'])
->all()
->toArray();
}
// Usage in action:
public function add()
{
$branches = $this->_getBranchesForAction('add', 'Members');
$this->set(compact('branches'));
}
Example 6: Plugin Entities
// Works with plugin entities
$user = $this->Authentication->getIdentity();
// Using string table name (plugin notation)
$branchIds = $user->getBranchIdsForAction('edit', 'Officers.Officers');
// Or using entity instance
$officer = $this->fetchTable('Officers.Officers')->newEmptyEntity();
$branchIds = $user->getBranchIdsForAction('edit', $officer);
Example 7: Multiple Actions Check
// Check multiple actions to determine UI state
public function edit($id)
{
$member = $this->Members->get($id);
$user = $this->Authentication->getIdentity();
$canEdit = $user->getBranchIdsForAction('edit', 'Members');
$canDelete = $user->getBranchIdsForAction('delete', 'Members');
// Determine if current member's branch is in authorized list
$canEditThis = ($canEdit === null) || in_array($member->branch_id, $canEdit ?? []);
$canDeleteThis = ($canDelete === null) || in_array($member->branch_id, $canDelete ?? []);
$this->set(compact('member', 'canEditThis', 'canDeleteThis'));
}
Example 8: beforeRender in AppController
Make branch IDs available to all views:
// In AppController
public function beforeRender(EventInterface $event)
{
parent::beforeRender($event);
if ($user = $this->Authentication->getIdentity()) {
// Make common permission checks available to all views
$viewBranches = $user->getBranchIdsForAction('view', 'Members');
$editBranches = $user->getBranchIdsForAction('edit', 'Members');
$this->set(compact('viewBranches', 'editBranches'));
}
}
Example 9: Service Layer Usage
// In a service class
class MemberService
{
public function getAuthorizedMembersForUser(Member $user): array
{
$branchIds = $user->getBranchIdsForAction('view', 'Members');
$membersTable = TableRegistry::getTableLocator()->get('Members');
$query = $membersTable->find();
if ($branchIds === null) {
// No filtering
} elseif (!empty($branchIds)) {
$query->where(['branch_id IN' => $branchIds]);
} else {
return []; // No permission
}
return $query->all()->toArray();
}
}
Quick Reference
Common Action Names
| Action String | Policy Method | Common Use |
|---|---|---|
'edit' |
canEdit() |
Editing existing records |
'view' |
canView() |
Viewing records |
'add' |
canAdd() |
Creating new records |
'delete' |
canDelete() |
Deleting records |
'index' |
canIndex() |
Listing records |
Resource Formats
// String table name
$branchIds = $user->getBranchIdsForAction('edit', 'Members');
// Entity instance
$member = $membersTable->newEmptyEntity();
$branchIds = $user->getBranchIdsForAction('edit', $member);
// Plugin table (string notation)
$branchIds = $user->getBranchIdsForAction('edit', 'Officers.Officers');
// Table instance
$membersTable = $this->fetchTable('Members');
$branchIds = $user->getBranchIdsForAction('edit', $membersTable);
Copy-Paste Template
// Standard pattern for controller actions
public function myAction()
{
$user = $this->Authentication->getIdentity();
$branchIds = $user->getBranchIdsForAction('ACTION', 'TABLE_NAME');
if ($branchIds === null) {
// Global access - load all branches
$branches = $this->fetchTable('Branches')->find('list')->all();
} elseif (!empty($branchIds)) {
// Limited access - load specific branches
$branches = $this->fetchTable('Branches')
->find('list')
->where(['id IN' => $branchIds])
->all();
} else {
// No access
$this->Flash->error('Permission denied');
return $this->redirect(['action' => 'index']);
}
$this->set(compact('branches'));
}
Integration with KMP Architecture
Uses Existing Systems
- PermissionsLoader: Leverages existing
getPolicies()method - Authorization Framework: Integrates with CakePHP’s authorization system
- Policy Classes: Works with all existing policy classes (BasePolicy and subclasses)
- Scoping Rules: Respects all permission scoping rules
Compatible With
- All existing policy classes
- Plugin entities and policies
- Branch hierarchy system
- Role-based access control (RBAC)
- Warrant-based permissions
Example 9: Service Layer Integration
Create service classes that use getBranchIdsForAction() for data access control:
// In a service class
namespace App\Service;
use Cake\ORM\Locator\LocatorAwareTrait;
class MemberService
{
use LocatorAwareTrait;
/**
* Get members that the current user can edit
*
* @param \App\Model\Entity\Member $user Current user
* @return \Cake\ORM\Query Query with authorized members
*/
public function getEditableMembersForUser($user)
{
$branchIds = $user->getBranchIdsForAction('edit', 'Members');
$membersTable = $this->fetchTable('Members');
$query = $membersTable->find();
if ($branchIds === null) {
// Global access - return all
return $query;
} elseif (!empty($branchIds)) {
// Limited access - filter by branches
return $query->where(['branch_id IN' => $branchIds]);
}
// No access - return empty result
return $query->where(['1 = 0']);
}
/**
* Check if user can perform action on any branch
*
* @param \App\Model\Entity\Member $user Current user
* @param string $action Action to check
* @param string $resource Resource/table name
* @return bool True if user has any access
*/
public function hasAnyAccess($user, string $action, string $resource): bool
{
$branchIds = $user->getBranchIdsForAction($action, $resource);
return $branchIds === null || !empty($branchIds);
}
}
Usage in controller:
public function index()
{
$user = $this->Authentication->getIdentity();
$memberService = new \App\Service\MemberService();
if (!$memberService->hasAnyAccess($user, 'view', 'Members')) {
$this->Flash->error(__('You do not have permission to view members.'));
return $this->redirect(['controller' => 'Pages', 'action' => 'display', 'home']);
}
$query = $memberService->getEditableMembersForUser($user);
$members = $this->paginate($query);
$this->set(compact('members'));
}
Example 10: Stimulus.js Integration for Dynamic Branch Selection
Create a Stimulus controller for client-side branch filtering:
// In assets/js/controllers/branch-selector-controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
action: String, // The policy action (edit, view, etc.)
entity: String, // The entity/table name (Members, Officers.Officers, etc.)
url: String // API endpoint URL
}
static targets = ["select"]
/**
* Connect and load authorized branches
*/
connect() {
this.loadBranches();
}
/**
* Fetch authorized branches from server
*/
async loadBranches() {
try {
const url = this.buildUrl();
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.updateBranchDropdown(data.branches);
} catch (error) {
console.error('Failed to load branches:', error);
this.showError('Failed to load branch options');
}
}
/**
* Build API URL with action and entity parameters
*/
buildUrl() {
const url = new URL(this.urlValue, window.location.origin);
url.searchParams.set('action', this.actionValue);
url.searchParams.set('entity', this.entityValue);
return url.toString();
}
/**
* Update select dropdown with branches
*/
updateBranchDropdown(branches) {
const select = this.selectTarget;
// Clear existing options except first (empty/prompt)
while (select.options.length > 1) {
select.remove(1);
}
// Add branch options
branches.forEach(branch => {
const option = document.createElement('option');
option.value = branch.id;
option.textContent = branch.name;
select.appendChild(option);
});
// Re-enable select if it was disabled
select.disabled = branches.length === 0;
}
/**
* Show error message
*/
showError(message) {
// Implementation depends on your notification system
console.error(message);
}
}
// Register controller
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["branch-selector"] = BranchSelectorController;
HTML usage:
<!-- In template -->
<div data-controller="branch-selector"
data-branch-selector-action-value="edit"
data-branch-selector-entity-value="Members"
data-branch-selector-url-value="<?= $this->Url->build(['controller' => 'Branches', 'action' => 'authorized']) ?>">
<?= $this->Form->control('branch_id', [
'label' => __('Branch'),
'empty' => __('-- Select Branch --'),
'data-branch-selector-target' => 'select',
'disabled' => true // Enabled after loading
]) ?>
</div>
Controller endpoint:
// In BranchesController
public function authorized()
{
$this->request->allowMethod(['get']);
$action = $this->request->getQuery('action');
$entity = $this->request->getQuery('entity');
$user = $this->Authentication->getIdentity();
$branchIds = $user->getBranchIdsForAction($action, $entity);
$query = $this->Branches->find('list');
if ($branchIds === null) {
// Global access - all branches
} elseif (!empty($branchIds)) {
$query->where(['id IN' => $branchIds]);
} else {
// No access - empty result
$query->where(['1 = 0']);
}
$branches = $query->all()->toArray();
// Convert to array format for JSON
$branchList = [];
foreach ($branches as $id => $name) {
$branchList[] = ['id' => $id, 'name' => $name];
}
$this->set('branches', $branchList);
$this->viewBuilder()->setOption('serialize', ['branches']);
}
Benefits
- UI Enhancement: Populate dropdowns with only relevant branches
- Better UX: Users don’t see branches they can’t access
- Security: Enforces authorization at the UI level
- Consistency: Works seamlessly with existing authorization system
- Flexibility: Works with any entity and any action
- Performance: Leverages existing caching in PermissionsLoader
Troubleshooting
Problem: Always getting empty array
Check:
- User has appropriate role assignments
- Role has permissions with policy mappings
- Permission policies reference correct policy class and method
- Branch assignments are set in
member_rolestable
Problem: Getting null when expecting specific branches
Check:
- User is not a super user
- Permission scoping_rule is not set to
SCOPE_GLOBAL
Problem: Wrong branches returned
Check:
branch_idvalues inmember_rolestable- Permission
scoping_rule(BRANCH_ONLY vs BRANCH_AND_CHILDREN) - Branch hierarchy if using BRANCH_AND_CHILDREN
Problem: Plugin entities not working
Check:
- Plugin policy class exists and follows naming convention
- Plugin name matches exactly in table name (case-sensitive)
- Policy class is in correct namespace (
PluginName\Policy\)
Best Practices
- Cache Results: If calling multiple times with same parameters, cache the result
- Use in beforeRender: Make common checks available to all views via
AppController::beforeRender() - Create Helper Methods: Wrap common patterns in controller helper methods
- Check Empty vs Null: Always distinguish between
[](no permission) andnull(global) - Order Matters: Apply authorization filters AFTER other query conditions for better performance
- Test Both Cases: Test with both global and limited permissions
Future Enhancements
Potential improvements being considered:
- Caching: Add method-level caching for repeated calls
- Method Overloads: Create convenience methods for common entities
- Stimulus.js Integration: Add JavaScript controller for dynamic branch filtering
- Table Helper: Add helper method to
BranchesTablefor easier integration - Permission Preview: Add UI to show which branches a user can access for which actions
Related Documentation
- Authorization System: 4.4 RBAC Security Architecture
- Policy Classes: 4.4 RBAC Security Architecture
- Member Entity: Member.php
- Services: 6. Services