zpm-packages/server-access-laravel
Server Access for Laravel by ZPM Packages gives your Laravel app a clean, framework-native way to manage server access. Configure access rules, integrate checks with routes and resources, and keep authorization logic simple, consistent, and easy to maintain.
1. Install and inspect before writing code
composer require zpm-packages/server-access-laravel
php artisan vendor:publish --provider="ZpmPackages\ServerAccess\ServerAccessServiceProvider"
Publish both the config and the migrations in one pass if the package offers tags (--tag=server-access-config, --tag=server-access-migrations); otherwise run the vendor:publish command twice and accept the provider prompt. Then inspect what you got:
config/server-access.php — the mode switch (direct vs database), connection/credential settings, and cache TTLs. Read this file end to end; it is the package's real API surface.database/migrations/*_create_server_access_*.php — confirms the table names, foreign keys, and whether there's an expires_at / revoked_at column. This tells you whether access is time-boxed or boolean.bindings, singleton, mergeConfigFrom) — reveals the contract you can swap and whether anything is registered as a singleton you shouldn't mutate at runtime.vendor/zpm-packages/server-access/src/ — the underlying domain package. The Laravel layer is thin; the real rules (how an access check is resolved, what "direct mode" means) live one level down.Run php artisan migrate only after you've confirmed the table names don't collide with anything existing.
2. First use case: gate a route behind server access
The fastest meaningful integration is a single server record plus an access check on a route:
// App\Models\Server is yours, or use the package model if one is published
$server = ServerAccess::servers()->create([
'name' => 'web-01',
'host' => '10.0.1.10',
'mode' => 'database', // or 'direct'
]);
ServerAccess::grant($user, $server, ['role' => 'operator']);
Then protect routes:
Route::get('/servers/{server}/terminal', TerminalController::class)
->middleware(['auth', 'server-access']);
If the middleware alias isn't auto-registered, check bootstrap/app.php (Laravel 11+) or Kernel.php — the provider may only register the alias when a config flag is enabled.
Before you build on it: with 2 stars and zero dependents, treat the README as a starting hypothesis, not documentation. Verify every config key against the source using php artisan tinker → config('server-access') and confirm the migration columns match what the docs claim. Budget an hour to read the source before your first PR.
Explicit mode selection per environment. The package advertises two modes: database-backed (access records in your DB, auditable, revocable) and direct (bypasses the DB, resolves access from configuration/direct source). Keep the mode in config, not in code:
// config/server-access.php
'mode' => env('SERVER_ACCESS_MODE', 'database'),
Use database in every environment where you need an audit trail. Reserve direct for local dev and CI, where you don't want to seed an access table. Never let direct leak into production via a missing env var — set the default to database and assert it in a health check.
Wrap checks in a Gate ability rather than calling the package directly. This keeps you decoupled and gives you one place to change when the package's API shifts:
// AppServiceProvider::boot()
Gate::define('operate-server', function (User $user, $server) {
return ServerAccess::allows($user, $server, 'operator');
});
Then $this->authorize('operate-server', $server) in controllers and @can('operate-server', $server) in Blade. Every call site now depends on your abstraction, which matters for a package with this adoption level.
Bulk-resolve access instead of checking in a loop. Listing 50 servers and calling allows() per row is an N+1 against the access table. Look for a bulk resolver (ServerAccess::allowsFor($user, $servers) or a scoped query builder) and fall back to eager-loading the relation:
$servers = ServerAccess::servers()
->with(['access' => fn ($q) => $q->where('user_id', $user->id)])
->get();
Treat grant/revoke as domain events. Fire your own event alongside the package call so audit logging, Slack notifications, and cache busting have a hook:
DB::transaction(function () use ($user, $server) {
ServerAccess::grant($user, $server, ['role' => 'operator', 'expires_at' => now()->addDays(7)]);
AccessGranted::dispatch($user, $server);
});
If the package dispatches its own events, listen to those instead and delete your wrappers — but verify the event class actually exists before relying on it.
Sync from your identity source, don't hand-maintain. Access tables rot. Write a scheduled command that reconciles against your source of truth (team membership, LDAP group, IdP claims):
// app/Console/Commands/SyncServerAccess.php
foreach ($desiredGrants as $grant) {
ServerAccess::grant(...$grant);
}
ServerAccess::query()->whereNotIn('id', $desiredIds)->delete();
Run it nightly and make it idempotent. This is the single highest-leverage thing you'll build on top of the package.
Drive automation through the same access layer. Scheduled jobs and deploy scripts should fetch credentials via the package rather than reading .env directly, so revocation takes effect immediately:
$conn = ServerAccess::connectionFor($user, $server); // however it's exposed
Testing. Bind a fake or swap the contract in TestCase::setUp so unit tests don't hit the DB. If no fake ships, wrap the package behind your own interface and fake that.
flush()/forget() helper or you'll be calling Cache::tags(...)->flush(). Test revoke→request explicitly.config:cache in production will freeze your env-driven mode. If your deploy runs php artisan config:cache, changing SERVER_ACCESS_MODE later does nothing until you re-cache. Add it to your deploy checklist.APP_KEY rotation. If server credentials are encrypted at rest (they should be), rotating APP_KEY without re-encrypting will silently break every connection. Confirm which key/cipher the package uses before any key rotation and rehearse it in staging.server_access, servers, or access_grants, a collision with an existing table is plausible. Read the published migration before running it, and rename in the migration file (not after) if needed.expires_at, check whether null means "never" or "expired", and whether the package filters expired rows in SQL or in PHP. Encode that in a test."zpm-packages/server-access-laravel": "1.2.3") rather than ^. Run composer outdated deliberately, not automatically, and read the diff before upgrading.AppServiceProvider, (2) events dispatched on grant/revoke, (3) a driver/resolver registration hook in the config. If none exist, your only safe extension point is wrapping the facade — do that early, before call sites proliferate.php artisan tinker → app('server-access') to confirm the singleton resolves, then config('server-access') and ServerAccess::allows($user, $server) step by step. If checks return false unexpectedly, verify the model's primary key type (UUID vs int) and that the pivot/access row is actually using the right foreign key — mismatched key types are the most common silent failure in packages like this.How can I help you explore Laravel packages today?