Module Security
The module system enforces multiple layers of security to prevent unauthorized access, data leakage between tenants, and abuse of the inter-module communication bus.
Access Control Layers
Module A calls Module B
│
├─ 1. Caller Detection — identify which module is making the call
├─ 2. Consume Check — does caller declare target in api.consumes?
├─ 3. Cross-Tenant Check — is source→target tenant access allowed?
├─ 4. Rate Limit Check — is the caller within its rate budget?
└─ 5. Audit Log — record cross-tenant calls for compliance
1. Strict Module Consumption
Modules must explicitly declare which other modules they consume in module.json:
{
"api": {
"consumes": ["Core", "ResellerCatalog", "ResellerOrders"]
}
}
Rules:
Coreis always consumable without declaration- Same module can always call itself
- Modules without a
consumesdeclaration cannot call any other module (deny by default) - Violations throw
ModuleApiAccessDeniedExceptionimmediately
Non-module code (app controllers, commands, middleware) is allowed to call any module API but each call is logged with a stack trace for visibility:
[debug] ModuleBus: Non-module caller accessing 'ResellerAdmin' API
trace: App\Http\Controllers\AdminController::dashboard → ...
2. Cross-Tenant Hierarchy Enforcement
The TenantContextManager validates every cross-tenant call against the tenant hierarchy:
| Source | Target | Allowed? |
|---|---|---|
| Central context (null) | Any tenant | Yes |
| Super admin | Any tenant | Yes |
| Parent tenant | Child/descendant tenant | Yes |
| Child tenant | Parent tenant | No |
| Sibling tenant | Sibling tenant | No |
| Unrelated tenant | Any tenant | No |
Denied calls throw CrossTenantAccessDeniedException.
Workaround for child→parent communication: Use the UplineRequestJob pattern — dispatch a job that runs in central context (null source), which can then access any tenant.
3. Event Subscription Validation
When modules register event subscribers, the system validates that:
-
Consumer check — Subscribers should only listen to events from modules they declare in
api.consumes. Unauthorized subscriptions are logged as warnings. -
Listener existence — Listener classes that don't exist on disk are skipped with an error log, preventing silent failures at runtime.
[warning] ModuleBus: Module 'VendorModule' subscribes to 'Payment.payment.processed'
but does not declare 'Payment' in api.consumes
[error] ModuleBus: Listener class 'Modules\VendorModule\Listeners\OnPayment' for event
'Payment.payment.processed' does not exist — skipping
4. Event Fan-Out Protection
Event fan-out is rate-limited to prevent cascade explosions:
- Fan-out is capped at 50 target tenants per minute per module (configurable)
publishLocal()is used in fan-out jobs to prevent re-fan-out loops — an event received via fan-out never triggers another fan-out- Source tenant is always excluded from fan-out targets (no self-notification)
Rate Limiting
Every ModuleBus operation is rate-limited per module per minute:
| Operation | Default Limit | Description |
|---|---|---|
api_call |
500/min | Synchronous in-process API calls |
api_call_async |
200/min | Async calls dispatched to Horizon |
cross_tenant |
100/min | Cross-tenant sync or async calls |
event_publish |
300/min | Events published to the event bus |
event_fanout |
50/min | Fan-out jobs dispatched (per target) |
Exceeding a limit throws ModuleBusRateLimitException. The exception includes the limit type, module name, and target for debugging.
Customizing Limits
// config/module-bus.php
'rate_limits' => [
'default' => [
'api_call' => 500,
'cross_tenant' => 100,
],
// Higher limits for a high-throughput module
'Orders' => [
'api_call' => 2000,
'event_publish' => 1000,
],
],
Audit Logging
All cross-tenant calls are recorded in the module_bus_audit_log table:
| Column | Description |
|---|---|
call_type |
cross_tenant, api_call, event_publish, event_fanout |
caller_module |
Module initiating the call |
target_module |
Module being called |
method_alias |
API method or event key |
source_tenant_id |
Tenant where the call originated |
target_tenant_id |
Tenant being accessed |
status |
success, failed, access_denied, rate_limited |
duration_ms |
Call execution time in milliseconds |
error |
Error message on failure |
created_at |
Timestamp |
What Gets Logged
- All cross-tenant API calls (success and failure)
- Access denied attempts (module consumption violations)
- Rate limit violations
Retention
Records are automatically pruned after the configured retention period (default: 90 days). Add to your scheduler:
$schedule->command('model:prune', ['--model' => ModuleBusAuditLog::class])->daily();
Or manually purge:
php artisan module:bus:audit --purge
Querying
# View recent audit entries
php artisan module:bus:audit
# Filter by module, tenant, status
php artisan module:bus:audit --module=ResellerAdmin --status=failed --days=7
# Aggregated statistics
php artisan module:bus:audit --stats
Job Timeouts
All ModuleBus queue jobs enforce strict timeouts:
| Job | Timeout | Purpose |
|---|---|---|
ModuleApiCallJob |
60s | Same-tenant async calls |
CrossTenantApiCallJob |
90s | Cross-tenant async calls |
ModuleEventFanOutJob |
30s | Event delivery to other tenants |
Jobs that exceed their timeout are killed and retried (up to 3 times). This prevents hung handlers from blocking the entire queue worker.
Tenant Context Isolation
The TenantContextManager uses execution-scoped context stacks to prevent concurrent Horizon jobs from interfering with each other:
- Each PHP Fiber (or the main execution thread) gets its own context stack
- Nested tenant switches push/pop from the stack correctly
- Context is always restored in
finallyblocks, even on exceptions
This means multiple queue workers processing different tenant contexts simultaneously won't corrupt each other's state.
Health Monitoring
The module health endpoint (GET /api/v1/modules/health, super_admin only) reports:
- Module load errors — modules that failed to load at boot
- Handler issues — API handlers that reference missing classes or methods
- Listener issues — Event listeners that reference missing classes
- Registry statistics — Number of registered endpoints, events, and consumers
A healthy status means all declared handlers and listeners exist. A degraded status means at least one is missing.
Security Checklist for Module Authors
- Declare
api.consumes— List every module your code calls. Undeclared calls will throw exceptions. - Mark cross-tenant APIs — Set
crossTenant: trueon any endpoint that should be callable from other tenants. - Validate handler inputs — The bus passes raw params to your handler. Validate and sanitize within your handler methods.
- Use tenant-scoped queries — When handling cross-tenant calls, ensure your queries are scoped to the correct tenant context.
- Test listener classes exist — Run
php artisan module:bus:listafter changes to verify all listeners resolve. - Monitor audit logs — Review cross-tenant access patterns for anomalies during development.