Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions app/Http/Middleware/SetTenant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* SPDX-FileCopyrightText: 2025 SecPal Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace App\Http\Middleware;

use App\Models\TenantKey;
use Closure;
use Illuminate\Http\Request;
use Spatie\Permission\PermissionRegistrar;
use Symfony\Component\HttpFoundation\Response;

/**
* SetTenant middleware extracts tenant ID from request path or header,
* validates tenant membership, and sets the permissions team ID for RBAC.
*/
class SetTenant
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$tenantId = $this->extractTenantId($request);

if ($tenantId === null) {
return response()->json([
'error' => 'Tenant ID is required',
'message' => 'Please provide tenant ID in path (/tenants/{tenant}) or X-Tenant header',
], 400);
}

// Verify tenant exists
if (! TenantKey::where('id', $tenantId)->exists()) {
return response()->json([
'error' => 'Tenant not found',
'message' => 'The specified tenant does not exist',
], 404);
}

// Set tenant for Spatie Permission (team-based permissions)
app(PermissionRegistrar::class)->setPermissionsTeamId($tenantId);

// Store tenant ID in request for use in controllers
$request->merge(['tenant_id' => $tenantId]);

return $next($request);
}

/**
* Extract tenant ID from request path or header.
*
* Priority: path parameter > X-Tenant header
*/
private function extractTenantId(Request $request): ?int
{
// Try to get from route parameter first
$tenantId = $request->route('tenant');

if ($tenantId !== null) {
return (int) $tenantId;
}

// Fallback to X-Tenant header
$headerTenant = $request->header('X-Tenant');

if ($headerTenant !== null) {
return (int) $headerTenant;
}

return null;
}
}
4 changes: 3 additions & 1 deletion bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->alias([
'tenant' => \App\Http\Middleware\SetTenant::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
Expand Down
208 changes: 208 additions & 0 deletions config/permission.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php

/*
* SPDX-FileCopyrightText: 2025 SecPal Contributors
*
* SPDX-License-Identifier: CC0-1.0
*/

return [

'models' => [

/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/

'permission' => Spatie\Permission\Models\Permission::class,

/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/

'role' => Spatie\Permission\Models\Role::class,

],

'table_names' => [

/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/

'roles' => 'roles',

/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/

'permissions' => 'permissions',

/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/

'model_has_permissions' => 'model_has_permissions',

/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/

'model_has_roles' => 'model_has_roles',

/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/

'role_has_permissions' => 'role_has_permissions',
],

'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',

/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/

'model_morph_key' => 'model_id',

/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/

'team_foreign_key' => 'tenant_id',
],

/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/

'register_permission_check_method' => true,

/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,

/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttached
* \Spatie\Permission\Events\RoleDetached
* \Spatie\Permission\Events\PermissionAttached
* \Spatie\Permission\Events\PermissionDetached
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,

/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/

'teams' => true,

/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,

/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/

'use_passport_client_credentials' => false,

/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/

'display_permission_in_exception' => false,

/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/

'display_role_in_exception' => false,

/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/

'enable_wildcard_permission' => false,

/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,

/* Cache-specific settings */

'cache' => [

/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/

'expiration_time' => \DateInterval::createFromDateString('24 hours'),

/*
* The cache key used to store all permissions.
*/

'key' => 'spatie.permission.cache',

/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/

'store' => 'default',
],
];
Loading