Skip to main content

Overview

GAC’s entity and role system provides a flexible way to manage permissions and restrictions across different types of users and organizational structures. The system supports multiple entity types, role-based access control (RBAC), and a sophisticated priority mechanism.

Entity Types

GAC distinguishes between three primary entity types:
// Entity type mapping (GAC.php:17)
protected array $entityTypeKeys = [
    'user'   => '1',
    'client' => '2'
];
Entity type '0' is reserved for roles, and '3' is reserved for global restrictions.

Users

Individual users of the system. Most common entity type.
$gac->setEntity('user', 12345);

Clients

Organizations, companies, or client accounts that may have different access requirements.
$gac->setEntity('client', 789);

Setting the Entity

Before loading permissions or restrictions, you must specify which entity to work with:
// Set entity method (GAC.php:25-29)
public function setEntity(string $entityType, string|int $entityId) : GAC {
    // Convert friendly names to internal codes
    $this->entityType = (string) ($this->entityTypeKeys[$entityType] ?? $entityType);
    $this->entityId = $entityId;
    return $this;
}
Attempting to load permissions or restrictions without setting an entity first will throw an exception.
// Validation (GAC.php:141-143)
if (empty($this->entityType) || empty($this->entityId)) {
    throw new \Exception('Entity type and ID must be set before loading data.');
}

Role System

Role Assignment

Entities can be assigned multiple roles, each with a specific priority value:
// Get roles query (DatabaseAdapter.php:49-59)
SELECT b.id, b.code, a.priority
FROM `gac_role_entity` AS a 
INNER JOIN `gac_role` AS b ON a.role_id = b.id
WHERE a.entity_type = :entity_type 
  AND a.entity_id = :entity_id 
  AND a.is_disabled = '0' 
  AND b.is_disabled = '0' 
  AND a.deleted_at IS NULL 
  AND b.deleted_at IS NULL
ORDER BY a.priority ASC

Role Data Structure

Role information is retrieved and cached per entity:
// Get entity role data (GAC.php:59-72)
public function getEntityRoleData(bool $reset = false) {
    if ($reset || empty($this->entityRoleData)) {
        $data = ['list' => [], 'priority' => []];
        $result = $this->databaseAdapter->getRoles(
            $this->entityType, 
            $this->entityId
        );
        
        foreach ($result as $key => $role) {
            $data['priority'][$role['id']] = (int) $role['priority'];
            $data['list'][] = $role['id'];
        }
        
        $this->entityRoleData = $data;
    }
    
    return $this->entityRoleData;
}
This creates a structure like:
[
    'list' => [101, 102, 103],  // Array of role IDs
    'priority' => [
        101 => 10,  // Admin role, priority 10
        102 => 20,  // Manager role, priority 20
        103 => 30   // User role, priority 30
    ]
]

Priority System

The priority system determines which permissions and restrictions take effect when multiple sources grant access to the same resource.

Priority Values

1

Personal (Priority -1)

Permissions and restrictions assigned directly to the entity always take precedence.
// Personal priority (GAC.php:382-383)
if ($record['from_entity_type'] !== '0') {
    $record['priority'] = -1; // Personal permissions first
}
2

Role-Based (Priority 0+)

Permissions inherited from roles use the role’s assigned priority value.
// Role priority (GAC.php:384-386)
else {
    // Use role priority from assignment
    $record['priority'] = $roleData['priority'][$record['from_entity_id']] ?? 100;
}
3

Default Fallback (Priority 100)

If a role priority is not found, default to 100.

Priority Sorting

Permissions and restrictions are sorted by priority before processing:
// Sort by priority (GAC.php:402-405)
if (!empty($roleData['priority'])) {
    usort($permissions, function(array $a, array $b) {
        return $a['priority'] <=> $b['priority'];
    });
}
Lower priority values are processed first. Personal permissions with priority -1 will always override role-based permissions.

First Match Wins

Once a permission is assigned for a module, subsequent matches are ignored:
// First match logic (GAC.php:428)
if (!array_key_exists($moduleData['code'], $response)) {
    // This is the first permission for this module - use it
    $response[$moduleData['code']] = [
        'i' => $permission['id'],
        'd' => $moduleData['is_developing'],
        'f' => $permission['feature'],
        'l' => $permission['level']
    ];
}
// Else: skip this permission, module already has one

Priority Examples

Example 1: Personal Override

// Entity has:
// - Personal permission: users module, read+write (priority -1)
// - Admin role: users module, full access (priority 10)
// - User role: users module, read-only (priority 20)

// Result: Personal permission is used (read+write only)

Example 2: Role Hierarchy

// Entity has:
// - Admin role: users module, full access (priority 10)
// - Manager role: users module, read+update (priority 20)  
// - Staff role: users module, read-only (priority 30)

// Result: Admin role permission is used (full access)

Example 3: Mixed Sources

// Entity has:
// - Personal: reports module, read-only (priority -1)
// - Admin role: reports module, full access (priority 10)
// - Admin role: users module, full access (priority 10)
// - Manager role: settings module, read+update (priority 20)

// Result:
// - reports: read-only (personal)
// - users: full access (admin role)
// - settings: read+update (manager role)

Practical Usage

Basic Setup

$gac = new GAC();
$gac->setDatabase($pdo);
$gac->setCache();

// For a user
$gac->setEntity('user', 12345);
$permissions = $gac->getPermissions();

// For a client
$gac->setEntity('client', 789);
$clientPermissions = $gac->getPermissions();

Inspecting Role Data

$gac->setEntity('user', 12345);
$roleData = $gac->getEntityRoleData();

echo "Assigned Roles: " . implode(', ', $roleData['list']);
// Output: Assigned Roles: 101, 102, 103

foreach ($roleData['priority'] as $roleId => $priority) {
    echo "Role $roleId has priority $priority\n";
}
// Output:
// Role 101 has priority 10
// Role 102 has priority 20  
// Role 103 has priority 30

Forcing Role Data Refresh

// Refresh role data from database
$roleData = $gac->getEntityRoleData(true);

Database Schema

Role Entity Assignment

The gac_role_entity table links entities to roles:
CREATE TABLE gac_role_entity (
    id INT PRIMARY KEY,
    role_id INT,           -- Foreign key to gac_role
    entity_type TINYINT,   -- 1=user, 2=client
    entity_id INT,         -- User or client ID
    priority INT,          -- Priority value
    is_disabled TINYINT,
    deleted_at TIMESTAMP
);

Role Definition

The gac_role table defines available roles:
CREATE TABLE gac_role (
    id INT PRIMARY KEY,
    code VARCHAR(255),     -- Unique role identifier
    is_disabled TINYINT,
    deleted_at TIMESTAMP
);

Permission and Restriction Queries

Both permissions and restrictions query across entity and role assignments:
// Permissions query (DatabaseAdapter.php:67-74)
$query = 'SELECT id, from_entity_type, from_entity_id, 
                 to_entity_type, to_entity_id, feature, level'
       . ' FROM `gac_module_access`'
       . ' WHERE ((`from_entity_type` = ? AND `from_entity_id` = ?)'; // Personal
       
foreach ($roleIds as $key => $id) {
    $query .= ' OR (`from_entity_type` = \'0\' AND `from_entity_id` = ?)'; // Roles
}

$query .= ') AND `deleted_at` IS NULL AND `is_disabled` = \'0\''
       . ' ORDER BY `from_entity_type` DESC';
The same pattern applies to restrictions queries (see DatabaseAdapter.php:86-96).

Cache Key Generation

Cache keys are generated based on entity type and ID:
// Cache key generation (GAC.php:49-57)
public function getCacheKey(string $type) : string {
    if ($type == 'restrictions_global') {
        return $this->cachekey . '_r_global';
    } else {
        $type = substr($type, 0, 1); // 'p' or 'r'
        return $this->cachekey . '_' . $type . '_' 
             . $this->entityType . '_' . $this->entityId;
    }
}
Example Keys:
  • gac_p_1_12345 - Permissions for user 12345
  • gac_r_2_789 - Restrictions for client 789
  • gac_r_global - Global restrictions

Purging Cache by Entity Type

GAC can purge cache for specific entity types:
// Purge by entity type (GAC.php:270-285)
if ($entityType == 'user') {
    $list['1'] = $entityIds;
}
elseif ($entityType == 'client') {
    $list['2'] = $entityIds;
}
elseif($entityType == 'role') {
    // Get all entities with this role
    $result = $this->databaseAdapter->getEntitiesByRoles($entityIds);
    foreach ($result as $record) {
        $list[$record['entity_type']] ??= [];
        $list[$record['entity_type']][$record['entity_id']] = $record['entity_id'];
    }
}

Usage Examples

// Purge cache for specific users
$gac->purgePermissionsBy('user', [12345, 67890]);

// Purge cache for a client
$gac->purgeRestrictionsBy('client', [789]);

// Purge all entities with a specific role
$gac->purgePermissionsBy('role', [101]);

// Purge everything
$gac->purgePermissionsBy('global');

Best Practices

Use Roles for Groups

Assign roles for department, position, or access level groupings.

Personal for Exceptions

Use personal permissions only for exceptions or temporary overrides.

Lower Priority = Higher Importance

Remember: lower priority numbers are processed first (most important).

Keep Priorities Spaced

Use increments of 10 (10, 20, 30) to allow for future insertions.

Common Patterns

Multi-Tenant Setup

Use clients for tenant isolation:
// Each client has different access
$gac->setEntity('client', $tenantId);
$permissions = $gac->getPermissions();

Hierarchical Roles

Create role hierarchy with priorities:
Super Admin (priority: 5)
├── Admin (priority: 10)
│   ├── Manager (priority: 20)
│   │   └── Staff (priority: 30)
│   └── Supervisor (priority: 25)
└── Support (priority: 15)

Temporary Access

Grant temporary elevated access via personal permissions:
// User normally has Staff role (priority 30)
// Temporarily grant Admin permissions as personal (priority -1)
// Personal permissions override role-based access

Permissions

How permissions work with roles

Restrictions

Role-based restrictions

Caching

Cache strategies for entities

Build docs developers (and LLMs) love