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:

  • Core is always consumable without declaration
  • Same module can always call itself
  • Modules without a consumes declaration cannot call any other module (deny by default)
  • Violations throw ModuleApiAccessDeniedException immediately

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:

  1. Consumer check — Subscribers should only listen to events from modules they declare in api.consumes. Unauthorized subscriptions are logged as warnings.

  2. 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 finally blocks, 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

  1. Declare api.consumes — List every module your code calls. Undeclared calls will throw exceptions.
  2. Mark cross-tenant APIs — Set crossTenant: true on any endpoint that should be callable from other tenants.
  3. Validate handler inputs — The bus passes raw params to your handler. Validate and sanitize within your handler methods.
  4. Use tenant-scoped queries — When handling cross-tenant calls, ensure your queries are scoped to the correct tenant context.
  5. Test listener classes exist — Run php artisan module:bus:list after changes to verify all listeners resolve.
  6. Monitor audit logs — Review cross-tenant access patterns for anomalies during development.