Skip to content
Open
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
17 changes: 17 additions & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,21 @@
return [
'default_owner_key' => 'user_id',
'default_override_key' => 'author_id',

'cache' => [
'enabled' => true,
'ttl' => 3600, // 1 hour
'prefix' => 'fortress:auth:',
],

'rate_limiting' => [
'enabled' => true,
'max_attempts' => 60,
'decay_minutes' => 1,
],

'audit_logging' => [
'enabled' => true,
'channel' => 'stack',
],
];
60 changes: 60 additions & 0 deletions src/Adapters/DefaultAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Laravelplus\Fortress\Adapters;

use Illuminate\Contracts\Auth\Authenticatable;
use Laravelplus\Fortress\Contracts\PermissionAdapter;
use Laravelplus\Fortress\Contracts\RoleAdapter;

final class DefaultAdapter implements PermissionAdapter, RoleAdapter
{
public function __construct(
private ?Authenticatable $user = null
) {
}

public function setUser(?Authenticatable $user): void
{
$this->user = $user;
}

public function hasPermissionTo(string $permission): bool
{
return method_exists($this->user, 'hasPermissionTo')
? $this->user->hasPermissionTo($permission)
: false;
}

public function hasAnyPermission(array $permissions): bool
{
return false;
}

public function hasAllPermissions(array $permissions): bool
{
return method_exists($this->user, 'hasAllPermissions')
? $this->user->hasAllPermissions($permissions)
: false;
}

public function hasRole(string $role): bool
{
return method_exists($this->user, 'hasRole')
? $this->user->hasRole($role)
: false;
}

public function hasAnyRole(array $roles): bool
{
return false;
}

public function hasAllRoles(array $roles): bool
{
return method_exists($this->user, 'hasAllRoles')
? $this->user->hasAllRoles($roles)
: false;
}
}
23 changes: 23 additions & 0 deletions src/Contracts/PermissionAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Laravelplus\Fortress\Contracts;

interface PermissionAdapter
{
/**
* Check if the user has the given permission.
*/
public function hasPermissionTo(string $permission): bool;

/**
* Check if the user has any of the given permissions.
*/
public function hasAnyPermission(array $permissions): bool;

/**
* Check if the user has all of the given permissions.
*/
public function hasAllPermissions(array $permissions): bool;
}
23 changes: 23 additions & 0 deletions src/Contracts/RoleAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Laravelplus\Fortress\Contracts;

interface RoleAdapter
{
/**
* Check if the user has the given role.
*/
public function hasRole(string $role): bool;

/**
* Check if the user has any of the given roles.
*/
public function hasAnyRole(array $roles): bool;

/**
* Check if the user has all of the given roles.
*/
public function hasAllRoles(array $roles): bool;
}
15 changes: 15 additions & 0 deletions src/Exceptions/AuthenticationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Laravelplus\Fortress\Exceptions;

use Exception;

final class AuthenticationException extends Exception
{
public function __construct(string $message = 'Authentication failed', int $code = 401)
{
parent::__construct($message, $code);
}
}
15 changes: 15 additions & 0 deletions src/Exceptions/InvalidConfigurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Laravelplus\Fortress\Exceptions;

use Exception;

final class InvalidConfigurationException extends Exception
{
public function __construct(string $message = 'Invalid configuration', int $code = 500)
{
parent::__construct($message, $code);
}
}
15 changes: 15 additions & 0 deletions src/Exceptions/UnauthorizedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Laravelplus\Fortress\Exceptions;

use Exception;

final class UnauthorizedException extends Exception
{
public function __construct(string $message = 'Unauthorized action', int $code = 403)
{
parent::__construct($message, $code);
}
}
143 changes: 123 additions & 20 deletions src/Fortress.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,143 @@

namespace Laravelplus\Fortress;

use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Config\Repository as Config;
use Laravelplus\Fortress\Adapters\DefaultAdapter;
use Laravelplus\Fortress\Contracts\PermissionAdapter;
use Laravelplus\Fortress\Contracts\RoleAdapter;
use Laravelplus\Fortress\Exceptions\AuthenticationException;
use Laravelplus\Fortress\Exceptions\InvalidConfigurationException;
use Laravelplus\Fortress\Exceptions\UnauthorizedException;

final class Fortress
{
/**
* Authorize a user based on roles, permissions, or ownership.
*/
public static function authorize(
array $roles = [],
array|string|null $permissions = null,
private PermissionAdapter $permissionAdapter;
private RoleAdapter $roleAdapter;

public function __construct(
private readonly Config $config,
private readonly Cache $cache
) {
$this->permissionAdapter = new DefaultAdapter();
$this->roleAdapter = new DefaultAdapter();
}

public function getConfig(): Config
{
return $this->config;
}

public function getCache(): Cache
{
return $this->cache;
}

public function getPermissionAdapter(): PermissionAdapter
{
return $this->permissionAdapter;
}

public function getRoleAdapter(): RoleAdapter
{
return $this->roleAdapter;
}

public function setPermissionAdapter(PermissionAdapter $adapter): void
{
$this->permissionAdapter = $adapter;
}

public function setRoleAdapter(RoleAdapter $adapter): void
{
$this->roleAdapter = $adapter;
}

public function authorize(
?Authenticatable $user,
bool $public = false,
array|string $permissions = [],
array|string $roles = [],
?string $owner = null,
?string $overrideKey = null
): bool {
$user = Auth::user();
if ($public === true) {
return true;
}

if (!$user) {
throw new AuthenticationException('User not authenticated');
}

abort_if(!$user, 401, 'User not authenticated.');
$permissions = is_string($permissions) ? [$permissions] : $permissions;
$roles = is_string($roles) ? [$roles] : $roles;

// Roles check
abort_if(!empty($roles) && !$user->hasAnyRole($roles), 403, 'Unauthorized role.');
if (!empty($roles)) {
$userId = $user->getAuthIdentifier();
$cacheKey = "fortress:auth:roles:{$userId}";
$ttl = $this->config->get('fortress.cache.ttl', 3600);

// Permissions check
abort_if($permissions && !$user->hasPermissionTo($permissions), 403, 'Unauthorized permission.');
$hasRole = $this->cache->remember($cacheKey, $ttl, function () use ($roles) {
return $this->roleAdapter->hasAnyRole($roles);
});

// Ownership check
if (!$owner) {
return true;
if (!$hasRole) {
throw new UnauthorizedException('User does not have required roles');
}
}

if (!empty($permissions)) {
$userId = $user->getAuthIdentifier();
$cacheKey = "fortress:auth:permissions:{$userId}";
$ttl = $this->config->get('fortress.cache.ttl', 3600);

$hasPermission = $this->cache->remember($cacheKey, $ttl, function () use ($permissions) {
return $this->permissionAdapter->hasAnyPermission($permissions);
});

if (!$hasPermission) {
throw new UnauthorizedException('User does not have required permissions');
}
}

$key = $overrideKey ?? config('fortress.default_override_key');
$ownerInstance = app($owner);
if ($owner) {
$model = app($owner);
$key = $overrideKey ?? $this->config->get('fortress.ownership_key', 'user_id');

abort_if(!property_exists($ownerInstance, $key), 500, "Key '{$key}' does not exist on the model.");
abort_if($ownerInstance->{$key} !== $user->id, 403, 'Unauthorized ownership.');
if (!property_exists($model, $key)) {
throw new InvalidConfigurationException("Invalid ownership key: {$key}");
}

if ($model->{$key} !== $user->getAuthIdentifier()) {
throw new UnauthorizedException('User does not own this resource');
}
}

return true;
}

public function batchAuthorize(Authenticatable $user, array $resources): array
{
$results = [];

foreach ($resources as $resource) {
try {
$this->authorize(
user: $user,
public: $resource['public'] ?? false,
permissions: $resource['permissions'] ?? [],
roles: $resource['roles'] ?? [],
owner: $resource['owner'] ?? null,
overrideKey: $resource['overrideKey'] ?? null
);

$results[$resource['id']] = true;
} catch (AuthenticationException|UnauthorizedException) {
$results[$resource['id']] = false;
}
}

return $results;
}
}
Loading