alchemy/acl-bundle
Symfony bundle providing a simple ACL API. Configure object types, alias your UserRepository, and add Redis cache for access tokens. Exposes endpoints to list, upsert, and delete ACEs by user/group, object type/id, with permission masks and wildcards.
Install the Bundle (via Composer):
composer require alchemy/acl-bundle
Configure ACL Objects (in config/packages/alchemy_acl.yaml):
alchemy_acl:
objects:
publication: App\Models\Publication
asset: App\Models\Asset
Alias UserRepository (in config/services.yaml):
services:
Alchemy\AclBundle\Repository\UserRepositoryInterface: '@App\Repositories\UserRepository'
Enable Redis Cache (optional, in config/cache.php):
'pools' => [
'accessToken.cache' => [
'adapter' => 'redis',
'provider' => 'redis://redis',
],
],
First Use Case: Grant a user full access to a publication via API:
curl -X PUT http://your-app/permissions/ace \
-H "Content-Type: application/json" \
-d '{
"userType": "user",
"userId": "user-123",
"objectType": "publication",
"objectId": "pub-42",
"mask": 7
}'
Permission Assignment:
PUT /permissions/ace for dynamic ACLs (e.g., admin dashboards).{
"userType": "group",
"userId": "editors-group",
"objectType": "asset",
"objectId": null, // Applies to all assets
"mask": 4 // Edit permission (binary 100)
}
Permission Querying:
curl http://your-app/permissions/aces?userType=user&userId=user-123&objectType=publication
public function checkAccess($userId, $objectType, $objectId, $requiredMask) {
$response = Http::get("/permissions/aces", [
'userType' => 'user',
'userId' => $userId,
'objectType' => $objectType,
'objectId' => $objectId,
]);
$aces = json_decode($response, true);
foreach ($aces as $ace) {
if ($ace['mask'] & $requiredMask) return true;
}
return false;
}
Metadata Integration (v1.1.0+):
{
"userType": "user",
"userId": "admin-456",
"objectType": "publication",
"objectId": "pub-42",
"mask": 7,
"metadata": {
"reason": "editorial_override",
"expires_at": "2024-12-31"
}
}
Group-Based Permissions:
// In UserRepository
public function addToGroup($userId, $groupId) {
// Sync group membership
$this->emit('group.updated', [$groupId]);
}
Laravel Service Layer: Create a facade or service to abstract API calls:
namespace App\Services;
use Illuminate\Support\Facades\Http;
class AclService {
public function hasAccess($userId, $objectType, $objectId, $permission) {
$response = Http::get("/permissions/aces", [
'userType' => 'user',
'userId' => $userId,
'objectType' => $objectType,
'objectId' => $objectId,
]);
$aces = json_decode($response, true);
return collect($aces)->contains(fn($ace) => ($ace['mask'] & $permission));
}
}
Policy Integration:
Extend Laravel’s Policy class to use the ACL bundle:
namespace App\Policies;
use App\Services\AclService;
class PublicationPolicy {
protected $acl;
public function __construct(AclService $acl) {
$this->acl = $acl;
}
public function edit($user, Publication $publication) {
return $this->acl->hasAccess(
$user->id,
'publication',
$publication->id,
4 // Edit mask
);
}
}
Event-Driven Sync:
Listen for group.updated events to refresh cached permissions:
Event::listen('group.updated', function ($groupId) {
// Clear Redis cache for group-based ACEs
Cache::forget("acl:group:{$groupId}");
});
Middleware for API Routes: Protect API endpoints with ACL checks:
Route::middleware(['acl:publication.edit'])->group(function () {
Route::put('/publications/{id}', [PublicationController::class, 'update']);
});
(Requires custom middleware using the AclService.)
Mask Confusion:
mask is a bitwise integer (e.g., 7 = 111 in binary = read/write/execute).mask: 7 grants all permissions, but mask: 4 (edit) + mask: 2 (read) ≠ mask: 6 (read + edit) due to bitwise OR logic.class PermissionMask {
public const READ = 1;
public const WRITE = 2;
public const EXECUTE = 4;
}
Null ObjectId:
objectId: null applies the ACE to all objects of the objectType.objectId: null for publication) can cause performance issues when querying ACEs.objectId values unless global rules are intentional.Redis Cache Quirks:
accessToken.cache pool must be named exactly accessToken.cache for auto-wiring.framework:
cache:
pools:
accessToken.cache: # Must match this name
adapter: cache.adapter.redis
UserRepository Aliasing:
UserRepositoryInterface implementation.ServiceNotFoundException.config/services.yaml:
services:
Alchemy\AclBundle\Repository\UserRepositoryInterface: '@App\Repositories\UserRepository'
Metadata Limitations:
/permissions/aces queries.reason, expires_at) and avoid large payloads.Symfony 7 Dependency Conflicts:
symfony/* packages.symfony/options-resolver, causing runtime errors.composer.json overrides:
"extra": {
"laravel": {
"dont-discover": ["symfony/*"]
}
}
Enable API Debugging:
curl -v http://your-app/permissions/aces
400 Bad Request errors if required fields (e.g., mask) are missing.Log ACE Queries:
AclService to trace permission checks:
public function hasAccess($userId, $objectType, $objectId, $permission) {
Log::debug("Checking ACL for user {$userId}, object {$objectType}/{$objectId}, mask {$permission}");
// ... existing logic
}
Inspect Database:
How can I help you explore Laravel packages today?