<?php
declare(strict_types=1);
namespace SHServ\Integrations\GAuth;
final class PermissionResolver
{
/** @var array<string, array<int, string>> */
private static array $cache = [];
/**
* Resolve effective permissions for a user.
* Algorithm: union(role_defaults, group_permissions, user_grants) minus explicit_denies.
* Results are cached for the lifetime of the current request.
*/
public function resolve(int $userId, string $systemRole): array
{
$cacheKey = "{$userId}:{$systemRole}";
if (isset(self::$cache[$cacheKey])) {
return self::$cache[$cacheKey];
}
// Fast path: superadmin
if ($systemRole === 'superadmin') {
$result = $this->getAllPermissionSlugs();
self::$cache[$cacheKey] = $result;
return $result;
}
$effective = [];
// 1. Role defaults
$rolePerms = $this->getRolePermissions($systemRole);
foreach ($rolePerms as $slug => $granted) {
if ($granted) {
$effective[$slug] = true;
}
}
// 2. Group permissions (union)
$groupPerms = $this->getGroupPermissions($userId);
foreach ($groupPerms as $slug => $granted) {
if ($granted) {
$effective[$slug] = true;
}
}
// 3. User-level overrides
$userOverrides = $this->getUserOverrides($userId);
foreach ($userOverrides as $slug => $granted) {
if ($granted) {
$effective[$slug] = true;
} else {
unset($effective[$slug]);
}
}
$result = array_keys($effective);
self::$cache[$cacheKey] = $result;
return $result;
}
/**
* Check if user has a specific permission.
*/
public function has(int $userId, string $systemRole, string $permissionSlug): bool
{
if ($systemRole === 'superadmin') {
return true;
}
$perms = $this->resolve($userId, $systemRole);
return in_array($permissionSlug, $perms, true);
}
private function getRolePermissions(string $roleSlug): array
{
$tb = app()->thin_builder;
$result = $tb->select('shserv_roles', ['default_permissions'], [['slug', '=', $roleSlug]]);
if (!$result) {
return $roleSlug === 'superadmin' ? ['*' => true] : [];
}
$perms = json_decode($result[0]['default_permissions'] ?? '[]', true);
$map = [];
foreach ($perms as $slug) {
$map[$slug] = true;
}
// Handle wildcard '*' for superadmin
if (in_array('*', $perms, true)) {
try {
$all = $tb->select('shserv_permissions', ['slug']);
if ($all) {
foreach ($all as $row) {
$map[$row['slug']] = true;
}
}
} catch (\Throwable $e) {
$map['*'] = true;
}
}
return $map;
}
private function getGroupPermissions(int $userId): array
{
$tb = app()->thin_builder;
try {
$result = $tb->query("
SELECT p.permission_slug, p.granted
FROM shserv_group_permissions p
JOIN shserv_group_members m ON m.group_id = p.group_id
WHERE m.user_id = {$userId}
");
} catch (\Throwable $e) {
return [];
}
if (!$result) {
return [];
}
$map = [];
foreach ($result as $row) {
$map[$row['permission_slug']] = (bool) $row['granted'];
}
return $map;
}
private function getUserOverrides(int $userId): array
{
$tb = app()->thin_builder;
$result = $tb->select('shserv_user_permissions', ['permission_slug', 'granted'], [['user_id', '=', $userId]]);
if (!$result) {
return [];
}
$map = [];
foreach ($result as $row) {
$map[$row['permission_slug']] = (bool) $row['granted'];
}
return $map;
}
private function getAllPermissionSlugs(): array
{
$tb = app()->thin_builder;
try {
$result = $tb->select('shserv_permissions', ['slug']);
} catch (\Throwable $e) {
return ['*'];
}
if (!$result) {
return ['*'];
}
return array_column($result, 'slug');
}
}