Skip to the content.

← Back to Services

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

$resource (mixed): The entity or table to check permissions for

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

  1. Super User Check: Returns null immediately if user is a super user (bypass all restrictions)

  2. Resource Resolution: Converts string table names or table instances to entity instances

  3. 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
  4. Policy Method Conversion: Converts action to policy method name
    • 'edit''canEdit'
    • 'view''canView'
  5. Permission Lookup: Queries the user’s policies for the specific policy class and method

  6. Scope Evaluation: Returns appropriate value based on permission scoping rule
    • Permission::SCOPE_GLOBAL → Returns null
    • Permission::SCOPE_BRANCH_ONLY → Returns specific branch IDs
    • Permission::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:

getPolicyClassFromTableName(string $tableName): string

Protected helper that converts table names to policy class names:

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

  1. PermissionsLoader: Leverages existing getPolicies() method
  2. Authorization Framework: Integrates with CakePHP’s authorization system
  3. Policy Classes: Works with all existing policy classes (BasePolicy and subclasses)
  4. Scoping Rules: Respects all permission scoping rules

Compatible With

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

  1. UI Enhancement: Populate dropdowns with only relevant branches
  2. Better UX: Users don’t see branches they can’t access
  3. Security: Enforces authorization at the UI level
  4. Consistency: Works seamlessly with existing authorization system
  5. Flexibility: Works with any entity and any action
  6. Performance: Leverages existing caching in PermissionsLoader

Troubleshooting

Problem: Always getting empty array

Check:

  1. User has appropriate role assignments
  2. Role has permissions with policy mappings
  3. Permission policies reference correct policy class and method
  4. Branch assignments are set in member_roles table

Problem: Getting null when expecting specific branches

Check:

  1. User is not a super user
  2. Permission scoping_rule is not set to SCOPE_GLOBAL

Problem: Wrong branches returned

Check:

  1. branch_id values in member_roles table
  2. Permission scoping_rule (BRANCH_ONLY vs BRANCH_AND_CHILDREN)
  3. Branch hierarchy if using BRANCH_AND_CHILDREN

Problem: Plugin entities not working

Check:

  1. Plugin policy class exists and follows naming convention
  2. Plugin name matches exactly in table name (case-sensitive)
  3. Policy class is in correct namespace (PluginName\Policy\)

Best Practices

  1. Cache Results: If calling multiple times with same parameters, cache the result
  2. Use in beforeRender: Make common checks available to all views via AppController::beforeRender()
  3. Create Helper Methods: Wrap common patterns in controller helper methods
  4. Check Empty vs Null: Always distinguish between [] (no permission) and null (global)
  5. Order Matters: Apply authorization filters AFTER other query conditions for better performance
  6. Test Both Cases: Test with both global and limited permissions

Future Enhancements

Potential improvements being considered:

  1. Caching: Add method-level caching for repeated calls
  2. Method Overloads: Create convenience methods for common entities
  3. Stimulus.js Integration: Add JavaScript controller for dynamic branch filtering
  4. Table Helper: Add helper method to BranchesTable for easier integration
  5. Permission Preview: Add UI to show which branches a user can access for which actions

← Back to Services