From b8d25c22da6a7a82ab69f5dc9d2b61f16a851094 Mon Sep 17 00:00:00 2001 From: Jeremy Postlethwaite Date: Sun, 25 Feb 2024 13:39:45 -0800 Subject: [PATCH] GH-11 --- config/playground-auth.php | 36 +++- src/Can.php | 368 +++++++++++++++++++++++++++++++++++++ src/Facades/Can.php | 25 +++ src/ServiceProvider.php | 6 +- 4 files changed, 425 insertions(+), 10 deletions(-) create mode 100644 src/Can.php create mode 100644 src/Facades/Can.php diff --git a/config/playground-auth.php b/config/playground-auth.php index 5cced3e..dab338d 100644 --- a/config/playground-auth.php +++ b/config/playground-auth.php @@ -73,14 +73,21 @@ | | Options for Sanctum: | - PLAYGROUND_AUTH_USER_PRIVILEGES - allow saving privileges in the user model. - | - PLAYGROUND_AUTH_VERIFY === privileges + | - PLAYGROUND_AUTH_VERIFY === sanctum | + | Verification: + | - admin: $user->isAdmin() + | - policy: $user->can() + | - privileges: $user->hasPrivilege() + | - roles: $user->hasRole() + | - sanctum: $user->currentAccessToken()->can() + | - user: ! empty($user) */ /** - * @var string verify user|privileges|roles + * @var string verify admin|user|policy|privileges|roles|sanctum */ - 'verify' => env('PLAYGROUND_AUTH_VERIFY', 'privileges'), + 'verify' => env('PLAYGROUND_AUTH_VERIFY', 'sanctum'), /** * @var bool sanctum Enable Sanctum @@ -101,19 +108,22 @@ * @var bool hasRole Enable if the user model has $user->hasRole($role) */ 'hasRole' => (bool) env('PLAYGROUND_AUTH_HAS_ROLE', false), - // 'hasRole' => (bool) env('PLAYGROUND_AUTH_HAS_ROLE', true), /** * @var bool userRole Enable if the user model has the attribute User::$role */ 'userRole' => (bool) env('PLAYGROUND_AUTH_USER_ROLE', false), - // 'userRole' => (bool) env('PLAYGROUND_AUTH_USER_ROLE', true), /** * @var bool userRoles Enable if the user model has the attribute User::$roles */ 'userRoles' => (bool) env('PLAYGROUND_AUTH_USER_ROLES', false), - // 'userRoles' => (bool) env('PLAYGROUND_AUTH_USER_ROLES', true), + + /** + * @var string canDefault The default privilege for Auth\Can::class checks. + * A value is required for Sanctum checks. + */ + 'canDefault' => env('PLAYGROUND_AUTH_CAN_DEFAULT', 'app'), /* |-------------------------------------------------------------------------- @@ -122,6 +132,8 @@ | | Enabling Sanctum provides token and API key support. | + | TODO We could/shoud add Passport support. + | */ 'token' => [ @@ -150,20 +162,26 @@ * @var bool roles Check the user role(s) for applying abilities. */ 'roles' => (bool) env('PLAYGROUND_AUTH_TOKEN_ROLES', false), - // 'roles' => (bool) env('PLAYGROUND_AUTH_TOKEN_ROLES', true), /** * @var bool privileges Allow the attribute User::$privileges to be used for authorization. */ 'privileges' => (bool) env('PLAYGROUND_AUTH_TOKEN_PRIVILEGES', false), - // 'privileges' => (bool) env('PLAYGROUND_AUTH_TOKEN_PRIVILEGES', true), /** * @var bool sanctum The token will use Sanctum. */ 'sanctum' => (bool) env('PLAYGROUND_AUTH_TOKEN_SANCTUM', true), - // 'sanctum' => (bool) env('PLAYGROUND_AUTH_TOKEN_SANCTUM', false), + /** + * @var bool session Save the token in the session. + */ + 'session' => (bool) env('PLAYGROUND_AUTH_TOKEN_SESSION', false), + + /** + * @var string session_name The session name for the token. + */ + 'session_name' => env('PLAYGROUND_AUTH_TOKEN_SESSION_NAME', 'sanctum'), ], /* diff --git a/src/Can.php b/src/Can.php new file mode 100644 index 0000000..01eb528 --- /dev/null +++ b/src/Can.php @@ -0,0 +1,368 @@ + + */ + protected array $implementations = [ + 'admin' => [], + 'policy' => [], + 'privileges' => [], + 'roles' => [], + 'sanctum' => [], + 'user' => [], + ]; + + protected bool $init = false; + + protected string $verify; + + protected ?Authenticatable $user = null; + + protected ?PersonalAccessToken $sanctumToken = null; + + protected bool $noSanctumToken = false; + + protected bool $sanctum = false; + + protected bool $hasPrivilege = false; + + protected bool $userPrivileges = false; + + protected bool $hasRole = false; + + protected bool $userRole = false; + + protected bool $userRoles = false; + + protected bool $sessionToken = false; + + protected string $sessionTokenName = ''; + + protected string $canDefault = ''; + + protected function matches(?Authenticatable $user): bool + { + if (! $user && ! $this->user) { + return true; + } + + if (! $user && $this->user) { + return false; + } + + if ($user && ! $this->user) { + return false; + } + + $currentUserId = $this->user?->getAttribute('id'); + $userId = $user?->getAttribute('id'); + + return $currentUserId && $currentUserId === $userId; + } + + protected function init(?Authenticatable $user): self + { + if ($this->init && ! $this->matches($user)) { + $this->init = false; + } + + if ($this->init) { + return $this; + } + + $this->user = $user; + $this->sanctumToken = null; + $this->noSanctumToken = false; + + $config = config('playground-auth'); + $config = is_array($config) ? $config : []; + + $this->verify = ''; + + if (! empty($config['verify']) + && is_string($config['verify']) + && array_key_exists($config['verify'], $this->implementations) + ) { + $this->verify = $config['verify']; + } + + $this->sanctum = ! empty($config['sanctum']); + $this->hasPrivilege = ! empty($config['hasPrivilege']); + $this->userPrivileges = ! empty($config['userPrivileges']); + $this->hasRole = ! empty($config['hasRole']); + $this->userRole = ! empty($config['userRole']); + $this->userRoles = ! empty($config['userRoles']); + + $this->canDefault = ''; + if (! empty($config['canDefault']) + && is_string($config['canDefault']) + ) { + $this->canDefault = $config['canDefault']; + } + + $this->sessionToken = false; + $this->sessionTokenName = ''; + + if (! empty($config['token']) + && is_array($config['token']) + ) { + $this->sessionToken = ! empty($config['token']['session']); + + if (! empty($config['token']['session_name']) + && is_string($config['token']['session_name']) + ) { + $this->sessionTokenName = $config['token']['session_name']; + } + } + + $this->init = true; + + return $this; + } + + protected function reset(?Authenticatable $user): self + { + $this->init = false; + + $this->init($user); + + return $this; + } + + public function isGuest(?Authenticatable $user): bool + { + $isGuest = empty($user); + + if ($this->userRole + && is_callable([$user, 'hasRole']) + && $user->hasRole('guest') + ) { + $isGuest = true; + } + + return $isGuest; + } + + /** + * @param array $_privileges + * @return array + */ + public function map(array &$_privileges, ?Authenticatable $user): array + { + $this->init($user); + + foreach ($_privileges as $entity => $options) { + if (is_array($_privileges[$entity])) { + $_privileges[$entity]['allow'] = $this->access( + $user, + is_array($options) ? $options : [] + ); + } + } + + return $_privileges; + } + + /** + * @return array + */ + public function wildcards(string $privilege): array + { + $wildcards = ['*']; + + if (empty($privilege) || $privilege === '*') { + return $wildcards; + } + + $stringable = Str::of($privilege); + + if (! $stringable->contains(':')) { + return $wildcards; + } + + $hasWildcard = $stringable->endsWith('*'); + + $exploded = explode(':', $privilege); + + if ($hasWildcard) { + array_pop($exploded); + } + + $p = ''; + $wc = ''; + foreach ($exploded as $key) { + if ($key && is_string($key)) { + if ($p) { + $p .= ':'; + } + $p .= $key; + $wc = $p.':*'; + + if (! in_array($wc, $wildcards)) { + $wildcards[] = $wc; + } + } + } + + return $wildcards; + } + + /** + * @param array $options + */ + public function access(?Authenticatable $user, array $options = []): bool + { + $this->init($user); + + $isGuest = $this->isGuest($this->user); + + if ($isGuest) { + // Deny if guest is not permitted + if (empty($options['guest'])) { + return false; + } + } + + $hash = null; + + $any = ! empty($options['any']); + + $privilege = $this->canDefault; + if (! empty($options['privilege']) && is_string($options['privilege'])) { + $privilege = $options['privilege']; + } + + /** + * @var array $roles + */ + $roles = []; + if (! empty($options['roles']) && is_array($options['roles'])) { + foreach ($options['roles'] as $role) { + if (! empty($role) + && is_string($role) + && ! in_array($role, $roles) + ) { + $roles[] = $role; + } + } + } + + if ($this->verify === 'sanctum') { + + if (empty($privilege)) { + // A privilege is required for checking. + return false; + } + + if ($this->noSanctumToken) { + return false; + } + + if (! $this->sanctumToken && is_callable([$this->user, 'currentAccessToken'])) { + + // Check if the user already has their token assigned. + $this->sanctumToken = $this->user->currentAccessToken(); + + if (empty($this->sanctumToken) + && $this->sessionToken + && $this->sessionTokenName + ) { + $hash = session($this->sessionTokenName); + + if ($hash && is_string($hash)) { + $this->sanctumToken = PersonalAccessToken::findToken($hash); + if (! $this->sanctumToken) { + $this->noSanctumToken = true; + } + } + } + } + + if (! $this->sanctumToken) { + return false; + } + + // Check the provided privilege before wildcards. + if ($this->sanctumToken->can($privilege)) { + return true; + } + + foreach ($this->wildcards($privilege) as $wildcard) { + if ($wildcard && is_string($wildcard)) { + if ($this->sanctumToken->can($wildcard)) { + return true; + } + } + } + + return false; + + } elseif ($this->verify === 'policy') { + if (is_callable([$this->user, 'can']) + && $this->user->can($privilege) + ) { + return true; + } + } elseif ($this->verify === 'admin') { + if (is_callable([$this->user, 'isAdmin']) + && $this->user->isAdmin() + ) { + return true; + } + } elseif ($this->verify === 'privileges') { + if (is_callable([$this->user, 'hasPrivilege']) + && $this->user->hasPrivilege($privilege) + ) { + return true; + } + } elseif ($this->verify === 'roles') { + $allowed = false; + $denied = false; + if (is_callable([$this->user, 'hasRole'])) { + if ($any) { + foreach ($roles as $role) { + if ($this->user->hasRole($role)) { + $allowed = true; + } + } + } else { + foreach ($roles as $role) { + if ($this->user->hasRole($role)) { + $allowed = true; + } else { + $denied = true; + } + } + } + } + + return $allowed && ! $denied; + } elseif ($this->verify === 'user') { + return ! $isGuest; + } + + return false; + } +} diff --git a/src/Facades/Can.php b/src/Facades/Can.php new file mode 100644 index 0000000..71afdcf --- /dev/null +++ b/src/Facades/Can.php @@ -0,0 +1,25 @@ +loadTranslationsFrom( - dirname(__DIR__).'/resources/lang', + dirname(__DIR__).'/lang', $this->package ); } @@ -64,6 +64,10 @@ public function register(): void sprintf('%1$s/config/%2$s.php', dirname(__DIR__), $this->package), $this->package ); + + $this->app->scoped('playground-auth-can', function () { + return new Can(); + }); } public function about(): void