implemented operation based permissions
This commit is contained in:
@@ -4,6 +4,7 @@ namespace KTXC\Controllers;
|
|||||||
|
|
||||||
use KTXC\Http\Response\JsonResponse;
|
use KTXC\Http\Response\JsonResponse;
|
||||||
use KTXC\Module\ModuleManager;
|
use KTXC\Module\ModuleManager;
|
||||||
|
use KTXC\Security\Authorization\PermissionChecker;
|
||||||
use KTXC\Service\UserService;
|
use KTXC\Service\UserService;
|
||||||
use KTXC\SessionIdentity;
|
use KTXC\SessionIdentity;
|
||||||
use KTXF\Controller\ControllerAbstract;
|
use KTXF\Controller\ControllerAbstract;
|
||||||
@@ -17,20 +18,33 @@ class InitController extends ControllerAbstract
|
|||||||
private readonly SessionIdentity $userIdentity,
|
private readonly SessionIdentity $userIdentity,
|
||||||
private readonly ModuleManager $moduleManager,
|
private readonly ModuleManager $moduleManager,
|
||||||
private readonly UserService $userService,
|
private readonly UserService $userService,
|
||||||
|
private readonly PermissionChecker $permissionChecker,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[AuthenticatedRoute('/init', name: 'init', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/init',
|
||||||
|
name: 'init',
|
||||||
|
methods: ['GET']
|
||||||
|
)]
|
||||||
public function index(): JsonResponse {
|
public function index(): JsonResponse {
|
||||||
|
|
||||||
$configuration = [];
|
$configuration = [];
|
||||||
|
|
||||||
// modules
|
// modules - filter by permissions
|
||||||
$configuration['modules'] = [];
|
$configuration['modules'] = [];
|
||||||
foreach ($this->moduleManager->list() as $module) {
|
foreach ($this->moduleManager->list() as $module) {
|
||||||
if (!method_exists($module, 'bootUi')) {
|
if (!method_exists($module, 'bootUi')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$configuration['modules'][$module->handle()] = $module->bootUi();
|
|
||||||
|
// Check if user has permission to view this module
|
||||||
|
// Allow access if user has: {module_handle}, {module_handle}.*, or * permission
|
||||||
|
$handle = $module->handle();
|
||||||
|
if (!$this->hasModuleViewPermission($handle)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuration['modules'][$handle] = $module->bootUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
// tenant
|
// tenant
|
||||||
@@ -46,7 +60,8 @@ class InitController extends ControllerAbstract
|
|||||||
'identifier' => $this->userIdentity->identifier(),
|
'identifier' => $this->userIdentity->identifier(),
|
||||||
'identity' => $this->userIdentity->identity()->getIdentity(),
|
'identity' => $this->userIdentity->identity()->getIdentity(),
|
||||||
'label' => $this->userIdentity->label(),
|
'label' => $this->userIdentity->label(),
|
||||||
'permissions' => [], // TODO: Implement permissions
|
'roles' => $this->userIdentity->identity()->getRoles(),
|
||||||
|
'permissions' => $this->userIdentity->identity()->getPermissions(),
|
||||||
],
|
],
|
||||||
'profile' => $this->userService->getEditableFields($this->userIdentity->identifier()),
|
'profile' => $this->userService->getEditableFields($this->userIdentity->identifier()),
|
||||||
'settings' => $this->userService->fetchSettings(),
|
'settings' => $this->userService->fetchSettings(),
|
||||||
@@ -56,4 +71,29 @@ class InitController extends ControllerAbstract
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has permission to view a module
|
||||||
|
*
|
||||||
|
* Checks for the following permissions (in order):
|
||||||
|
* 1. {module_handle} - module access permission
|
||||||
|
* 2. {module_handle}.* - wildcard for the module
|
||||||
|
* 3. * - global wildcard
|
||||||
|
*
|
||||||
|
* @param string $moduleHandle The module handle to check
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function hasModuleViewPermission(string $moduleHandle): bool
|
||||||
|
{
|
||||||
|
// Core module is always accessible to authenticated users
|
||||||
|
if ($moduleHandle === 'core') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for specific module permission or wildcard permissions
|
||||||
|
return $this->permissionChecker->canAny([
|
||||||
|
"{$moduleHandle}",
|
||||||
|
"{$moduleHandle}.*",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ class ModuleController extends ControllerAbstract
|
|||||||
private readonly ModuleManager $moduleManager
|
private readonly ModuleManager $moduleManager
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
#[AuthenticatedRoute('/modules/list', name: 'modules.index', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/modules/list',
|
||||||
|
name: 'modules.index',
|
||||||
|
methods: ['GET'],
|
||||||
|
permissions: ['module_manager.modules.view']
|
||||||
|
)]
|
||||||
public function index(): JsonResponse
|
public function index(): JsonResponse
|
||||||
{
|
{
|
||||||
$modules = $this->moduleManager->list(false);
|
$modules = $this->moduleManager->list(false);
|
||||||
@@ -21,7 +26,12 @@ class ModuleController extends ControllerAbstract
|
|||||||
return new JsonResponse(['modules' => $modules]);
|
return new JsonResponse(['modules' => $modules]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[AuthenticatedRoute('/modules/manage', name: 'modules.manage', methods: ['POST'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/modules/manage',
|
||||||
|
name: 'modules.manage',
|
||||||
|
methods: ['POST'],
|
||||||
|
permissions: ['module_manager.modules.manage']
|
||||||
|
)]
|
||||||
public function manage(string $handle, string $action): JsonResponse
|
public function manage(string $handle, string $action): JsonResponse
|
||||||
{
|
{
|
||||||
// Verify module exists
|
// Verify module exists
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ class UserProfileController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* @return JsonResponse Profile data with editability metadata
|
* @return JsonResponse Profile data with editability metadata
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/user/profile', name: 'user.profile.read', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/user/profile',
|
||||||
|
name: 'user.profile.read',
|
||||||
|
methods: ['GET'],
|
||||||
|
permissions: ['user.profile.read']
|
||||||
|
)]
|
||||||
public function read(): JsonResponse
|
public function read(): JsonResponse
|
||||||
{
|
{
|
||||||
$userId = $this->userIdentity->identifier();
|
$userId = $this->userIdentity->identifier();
|
||||||
@@ -50,7 +55,12 @@ class UserProfileController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* @return JsonResponse Updated profile data
|
* @return JsonResponse Updated profile data
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/user/profile', name: 'user.profile.update', methods: ['PUT', 'PATCH'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/user/profile',
|
||||||
|
name: 'user.profile.update',
|
||||||
|
methods: ['PUT', 'PATCH'],
|
||||||
|
permissions: ['user.profile.update']
|
||||||
|
)]
|
||||||
public function update(array $data): JsonResponse
|
public function update(array $data): JsonResponse
|
||||||
{
|
{
|
||||||
$userId = $this->userIdentity->identifier();
|
$userId = $this->userIdentity->identifier();
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ class UserSettingsController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* @return JsonResponse Settings data as key-value pairs
|
* @return JsonResponse Settings data as key-value pairs
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/user/settings', name: 'user.settings.read', methods: ['GET'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/user/settings',
|
||||||
|
name: 'user.settings.read',
|
||||||
|
methods: ['GET'],
|
||||||
|
permissions: ['user.settings.read']
|
||||||
|
)]
|
||||||
public function read(): JsonResponse
|
public function read(): JsonResponse
|
||||||
{
|
{
|
||||||
// Fetch all settings (no filter)
|
// Fetch all settings (no filter)
|
||||||
@@ -48,7 +53,12 @@ class UserSettingsController extends ControllerAbstract
|
|||||||
*
|
*
|
||||||
* @return JsonResponse Updated settings data
|
* @return JsonResponse Updated settings data
|
||||||
*/
|
*/
|
||||||
#[AuthenticatedRoute('/user/settings', name: 'user.settings.update', methods: ['PUT', 'PATCH'])]
|
#[AuthenticatedRoute(
|
||||||
|
'/user/settings',
|
||||||
|
name: 'user.settings.update',
|
||||||
|
methods: ['PUT', 'PATCH'],
|
||||||
|
permissions: ['user.settings.update']
|
||||||
|
)]
|
||||||
public function update(array $data): JsonResponse
|
public function update(array $data): JsonResponse
|
||||||
{
|
{
|
||||||
$this->userService->storeSettings($data);
|
$this->userService->storeSettings($data);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use KTXC\Http\Response\Response;
|
|||||||
use KTXC\Routing\Router;
|
use KTXC\Routing\Router;
|
||||||
use KTXC\Routing\Route;
|
use KTXC\Routing\Route;
|
||||||
use KTXC\SessionIdentity;
|
use KTXC\SessionIdentity;
|
||||||
|
use KTXC\Security\Authorization\PermissionChecker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router middleware
|
* Router middleware
|
||||||
@@ -16,7 +17,8 @@ class RouterMiddleware implements MiddlewareInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Router $router,
|
private readonly Router $router,
|
||||||
private readonly SessionIdentity $sessionIdentity
|
private readonly SessionIdentity $sessionIdentity,
|
||||||
|
private readonly PermissionChecker $permissionChecker
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||||
@@ -37,6 +39,16 @@ class RouterMiddleware implements MiddlewareInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check permissions (if any specified)
|
||||||
|
if ($match->authenticated && !empty($match->permissions)) {
|
||||||
|
if (!$this->permissionChecker->canAny($match->permissions)) {
|
||||||
|
return new Response(
|
||||||
|
Response::$statusTexts[Response::HTTP_FORBIDDEN],
|
||||||
|
Response::HTTP_FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatch to the controller
|
// Dispatch to the controller
|
||||||
$response = $this->router->dispatch($match, $request);
|
$response = $this->router->dispatch($match, $request);
|
||||||
|
|
||||||
|
|||||||
101
core/lib/Module/Module.php
Normal file
101
core/lib/Module/Module.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTXC\Module;
|
||||||
|
|
||||||
|
use KTXF\Module\ModuleInstanceAbstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core Module
|
||||||
|
*
|
||||||
|
* Provides core system functionality and permissions
|
||||||
|
*/
|
||||||
|
class Module extends ModuleInstanceAbstract
|
||||||
|
{
|
||||||
|
public function __construct() {}
|
||||||
|
|
||||||
|
public function handle(): string
|
||||||
|
{
|
||||||
|
return 'core';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return 'Core System';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author(): string
|
||||||
|
{
|
||||||
|
return 'Ktrix';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'Core system functionality and user features';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function version(): string
|
||||||
|
{
|
||||||
|
return '1.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permissions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// Core User Permissions
|
||||||
|
'user.profile.read' => [
|
||||||
|
'label' => 'Read Own Profile',
|
||||||
|
'description' => 'View own user profile information',
|
||||||
|
'group' => 'User Profile'
|
||||||
|
],
|
||||||
|
'user.profile.update' => [
|
||||||
|
'label' => 'Update Own Profile',
|
||||||
|
'description' => 'Edit own user profile information',
|
||||||
|
'group' => 'User Profile'
|
||||||
|
],
|
||||||
|
'user.settings.read' => [
|
||||||
|
'label' => 'Read Own Settings',
|
||||||
|
'description' => 'View own user settings',
|
||||||
|
'group' => 'User Settings'
|
||||||
|
],
|
||||||
|
'user.settings.update' => [
|
||||||
|
'label' => 'Update Own Settings',
|
||||||
|
'description' => 'Edit own user settings',
|
||||||
|
'group' => 'User Settings'
|
||||||
|
],
|
||||||
|
|
||||||
|
// Module Management
|
||||||
|
'module_manager.modules.view' => [
|
||||||
|
'label' => 'View Modules',
|
||||||
|
'description' => 'View list of installed and available modules',
|
||||||
|
'group' => 'Module Management'
|
||||||
|
],
|
||||||
|
'module_manager.modules.manage' => [
|
||||||
|
'label' => 'Manage Modules',
|
||||||
|
'description' => 'Install, uninstall, enable, and disable modules',
|
||||||
|
'group' => 'Module Management'
|
||||||
|
],
|
||||||
|
'module_manager.modules.*' => [
|
||||||
|
'label' => 'Full Module Management',
|
||||||
|
'description' => 'All module management operations',
|
||||||
|
'group' => 'Module Management'
|
||||||
|
],
|
||||||
|
|
||||||
|
// System Administration
|
||||||
|
'system.admin' => [
|
||||||
|
'label' => 'System Administrator',
|
||||||
|
'description' => 'Full system access (superuser)',
|
||||||
|
'group' => 'System Administration'
|
||||||
|
],
|
||||||
|
'*' => [
|
||||||
|
'label' => 'All Permissions',
|
||||||
|
'description' => 'Grants access to all features and operations',
|
||||||
|
'group' => 'System Administration'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bootUi(): ?array
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,13 @@ class ModuleManager
|
|||||||
public function list(bool $installedOnly = true, $enabledOnly = true): ModuleCollection
|
public function list(bool $installedOnly = true, $enabledOnly = true): ModuleCollection
|
||||||
{
|
{
|
||||||
$modules = New ModuleCollection();
|
$modules = New ModuleCollection();
|
||||||
|
|
||||||
|
// Always include core module
|
||||||
|
$coreModule = $this->coreModule();
|
||||||
|
if ($coreModule) {
|
||||||
|
$modules['core'] = new ModuleObject($coreModule, null);
|
||||||
|
}
|
||||||
|
|
||||||
// load all modules from store
|
// load all modules from store
|
||||||
$entries = $this->repository->list();
|
$entries = $this->repository->list();
|
||||||
foreach ($entries as $entry) {
|
foreach ($entries as $entry) {
|
||||||
@@ -497,4 +504,92 @@ class ModuleManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available permissions from all modules
|
||||||
|
*
|
||||||
|
* @return array Grouped permissions with metadata
|
||||||
|
*/
|
||||||
|
public function availablePermissions(): array
|
||||||
|
{
|
||||||
|
$permissions = [];
|
||||||
|
|
||||||
|
foreach ($this->list() as $module) {
|
||||||
|
$modulePermissions = $module->permissions();
|
||||||
|
|
||||||
|
foreach ($modulePermissions as $permission => $meta) {
|
||||||
|
$permissions[$permission] = array_merge($meta, [
|
||||||
|
'module' => $module->handle()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by category
|
||||||
|
$grouped = [];
|
||||||
|
foreach ($permissions as $permission => $meta) {
|
||||||
|
$group = $meta['group'] ?? 'Other';
|
||||||
|
|
||||||
|
if (!isset($grouped[$group])) {
|
||||||
|
$grouped[$group] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$grouped[$group][$permission] = $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort groups alphabetically
|
||||||
|
ksort($grouped);
|
||||||
|
|
||||||
|
return $grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if a permission exists
|
||||||
|
*/
|
||||||
|
public function permissionExists(string $permission): bool
|
||||||
|
{
|
||||||
|
foreach ($this->list() as $module) {
|
||||||
|
$modulePermissions = $module->permissions();
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (isset($modulePermissions[$permission])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard match (e.g., user_manager.users.create matches user_manager.users.*)
|
||||||
|
foreach (array_keys($modulePermissions) as $registered) {
|
||||||
|
if (str_ends_with($registered, '.*')) {
|
||||||
|
$prefix = substr($registered, 0, -2);
|
||||||
|
if (str_starts_with($permission, $prefix . '.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Get the core module instance
|
||||||
|
*/
|
||||||
|
private function coreModule(): ?\KTXF\Module\ModuleInstanceInterface
|
||||||
|
{
|
||||||
|
if (isset($this->moduleInstances['core'])) {
|
||||||
|
return $this->moduleInstances['core'];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$coreModuleClass = \KTXC\Module\Module::class;
|
||||||
|
if (!class_exists($coreModuleClass)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = $this->container->get($coreModuleClass);
|
||||||
|
$this->moduleInstances['core'] = $instance;
|
||||||
|
return $instance;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logger->error('Failed to load core module', [
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,4 +158,9 @@ class ModuleObject implements JsonSerializable
|
|||||||
return $this->instance?->bootUi() ?? null;
|
return $this->instance?->bootUi() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function permissions(): array
|
||||||
|
{
|
||||||
|
return $this->instance?->permissions() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Route
|
|||||||
public readonly string $className,
|
public readonly string $className,
|
||||||
public readonly string $classMethodName,
|
public readonly string $classMethodName,
|
||||||
public readonly array $classMethodParameters = [],
|
public readonly array $classMethodParameters = [],
|
||||||
|
public readonly array $permissions = [],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function withParams(array $params): self
|
public function withParams(array $params): self
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class Router
|
|||||||
className: $reflectionClass->getName(),
|
className: $reflectionClass->getName(),
|
||||||
classMethodName: $reflectionMethod->getName(),
|
classMethodName: $reflectionMethod->getName(),
|
||||||
classMethodParameters: $reflectionMethod->getParameters(),
|
classMethodParameters: $reflectionMethod->getParameters(),
|
||||||
|
permissions: $route instanceof AuthenticatedRoute ? $route->permissions : [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
core/lib/Security/Authorization/PermissionChecker.php
Normal file
124
core/lib/Security/Authorization/PermissionChecker.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace KTXC\Security\Authorization;
|
||||||
|
|
||||||
|
use KTXC\SessionIdentity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission Checker
|
||||||
|
* Provides granular permission checking with support for wildcards
|
||||||
|
*/
|
||||||
|
class PermissionChecker
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly SessionIdentity $sessionIdentity
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has a specific permission
|
||||||
|
* Supports wildcards: user_manager.users.* matches all user actions
|
||||||
|
*
|
||||||
|
* @param string $permission Permission to check (e.g., "user_manager.users.create")
|
||||||
|
* @param mixed $resource Optional resource for resource-based permissions
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function can(string $permission, mixed $resource = null): bool
|
||||||
|
{
|
||||||
|
$identity = $this->sessionIdentity->identity();
|
||||||
|
|
||||||
|
if (!$identity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user permissions from identity
|
||||||
|
$userPermissions = $identity->getPermissions() ?? [];
|
||||||
|
|
||||||
|
// Super admin bypass - check for admin role
|
||||||
|
$roles = $identity->getRoles() ?? [];
|
||||||
|
if (in_array('admin', $roles) || in_array('system.admin', $roles)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (in_array($permission, $userPermissions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard match: user_manager.users.* allows user_manager.users.create
|
||||||
|
foreach ($userPermissions as $userPerm) {
|
||||||
|
if (str_ends_with($userPerm, '.*')) {
|
||||||
|
$prefix = substr($userPerm, 0, -2);
|
||||||
|
if (str_starts_with($permission, $prefix . '.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full wildcard: * grants all permissions
|
||||||
|
if (in_array('*', $userPermissions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has ANY of the permissions (OR logic)
|
||||||
|
*
|
||||||
|
* @param array $permissions Array of permissions to check
|
||||||
|
* @param mixed $resource Optional resource for resource-based permissions
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canAny(array $permissions, mixed $resource = null): bool
|
||||||
|
{
|
||||||
|
if (empty($permissions)) {
|
||||||
|
return true; // No permissions required
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
if ($this->can($permission, $resource)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has ALL permissions (AND logic)
|
||||||
|
*
|
||||||
|
* @param array $permissions Array of permissions to check
|
||||||
|
* @param mixed $resource Optional resource for resource-based permissions
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function canAll(array $permissions, mixed $resource = null): bool
|
||||||
|
{
|
||||||
|
if (empty($permissions)) {
|
||||||
|
return true; // No permissions required
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($permissions as $permission) {
|
||||||
|
if (!$this->can($permission, $resource)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all permissions for the current user
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getUserPermissions(): array
|
||||||
|
{
|
||||||
|
$identity = $this->sessionIdentity->identity();
|
||||||
|
|
||||||
|
if (!$identity) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $identity->getPermissions() ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,14 +51,44 @@ class SessionIdentity
|
|||||||
|
|
||||||
public function permissions(): array
|
public function permissions(): array
|
||||||
{
|
{
|
||||||
$permissions = $this->identityData?->getPermissions() ?? [];
|
return $this->identityData?->getPermissions() ?? [];
|
||||||
$permissions[] = 'ROLE_USER';
|
}
|
||||||
return array_unique($permissions);
|
|
||||||
|
public function roles(): array
|
||||||
|
{
|
||||||
|
return $this->identityData?->getRoles() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasPermission(string $permission): bool
|
public function hasPermission(string $permission): bool
|
||||||
{
|
{
|
||||||
return in_array($permission, $this->permissions());
|
$permissions = $this->permissions();
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (in_array($permission, $permissions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard match
|
||||||
|
foreach ($permissions as $userPerm) {
|
||||||
|
if (str_ends_with($userPerm, '.*')) {
|
||||||
|
$prefix = substr($userPerm, 0, -2);
|
||||||
|
if (str_starts_with($permission, $prefix . '.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full wildcard
|
||||||
|
if (in_array('*', $permissions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasRole(string $role): bool
|
||||||
|
{
|
||||||
|
return in_array($role, $this->roles());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,23 @@ export function useUser() {
|
|||||||
const identifier = computed(() => store.identifier);
|
const identifier = computed(() => store.identifier);
|
||||||
const identity = computed(() => store.identity);
|
const identity = computed(() => store.identity);
|
||||||
const label = computed(() => store.label);
|
const label = computed(() => store.label);
|
||||||
|
const roles = computed(() => store.roles);
|
||||||
const permissions = computed(() => store.permissions);
|
const permissions = computed(() => store.permissions);
|
||||||
|
|
||||||
const hasPermission = (permission: string): boolean => {
|
const hasPermission = (permission: string): boolean => {
|
||||||
return store.permissions.includes(permission);
|
return store.hasPermission(permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAnyPermission = (perms: string[]): boolean => {
|
||||||
|
return store.hasAnyPermission(perms);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAllPermissions = (perms: string[]): boolean => {
|
||||||
|
return store.hasAllPermissions(perms);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasRole = (role: string): boolean => {
|
||||||
|
return store.hasRole(role);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async (): Promise<void> => {
|
const logout = async (): Promise<void> => {
|
||||||
@@ -66,8 +79,12 @@ export function useUser() {
|
|||||||
identifier,
|
identifier,
|
||||||
identity,
|
identity,
|
||||||
label,
|
label,
|
||||||
|
roles,
|
||||||
permissions,
|
permissions,
|
||||||
hasPermission,
|
hasPermission,
|
||||||
|
hasAnyPermission,
|
||||||
|
hasAllPermissions,
|
||||||
|
hasRole,
|
||||||
logout,
|
logout,
|
||||||
|
|
||||||
// Profile
|
// Profile
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const useUserStore = defineStore('userStore', () => {
|
|||||||
const identifier = computed(() => auth.value?.identifier ?? null);
|
const identifier = computed(() => auth.value?.identifier ?? null);
|
||||||
const identity = computed(() => auth.value?.identity ?? null);
|
const identity = computed(() => auth.value?.identity ?? null);
|
||||||
const label = computed(() => auth.value?.label ?? null);
|
const label = computed(() => auth.value?.label ?? null);
|
||||||
|
const roles = computed(() => auth.value?.roles ?? []);
|
||||||
const permissions = computed(() => auth.value?.permissions ?? []);
|
const permissions = computed(() => auth.value?.permissions ?? []);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -148,6 +149,61 @@ export const useUserStore = defineStore('userStore', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Permission Checking
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has a specific permission
|
||||||
|
* Supports wildcards: user_manager.users.* matches all user actions
|
||||||
|
*/
|
||||||
|
function hasPermission(permission: string): boolean {
|
||||||
|
const userPermissions = permissions.value;
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (userPermissions.includes(permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard match
|
||||||
|
for (const userPerm of userPermissions) {
|
||||||
|
if (userPerm.endsWith('.*')) {
|
||||||
|
const prefix = userPerm.slice(0, -2);
|
||||||
|
if (permission.startsWith(prefix + '.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full wildcard
|
||||||
|
if (userPermissions.includes('*')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has ANY of the permissions (OR logic)
|
||||||
|
*/
|
||||||
|
function hasAnyPermission(perms: string[]): boolean {
|
||||||
|
return perms.some(p => hasPermission(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has ALL permissions (AND logic)
|
||||||
|
*/
|
||||||
|
function hasAllPermissions(perms: string[]): boolean {
|
||||||
|
return perms.every(p => hasPermission(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has a specific role
|
||||||
|
*/
|
||||||
|
function hasRole(role: string): boolean {
|
||||||
|
return roles.value.includes(role);
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Initialize from /init endpoint
|
// Initialize from /init endpoint
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -180,6 +236,7 @@ export const useUserStore = defineStore('userStore', () => {
|
|||||||
identifier,
|
identifier,
|
||||||
identity,
|
identity,
|
||||||
label,
|
label,
|
||||||
|
roles,
|
||||||
permissions,
|
permissions,
|
||||||
|
|
||||||
// Profile getters
|
// Profile getters
|
||||||
@@ -206,6 +263,12 @@ export const useUserStore = defineStore('userStore', () => {
|
|||||||
getSetting,
|
getSetting,
|
||||||
setSetting,
|
setSetting,
|
||||||
|
|
||||||
|
// Permission actions
|
||||||
|
hasPermission,
|
||||||
|
hasAnyPermission,
|
||||||
|
hasAllPermissions,
|
||||||
|
hasRole,
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
init,
|
init,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface AuthenticatedUser {
|
|||||||
identifier: string;
|
identifier: string;
|
||||||
identity: string;
|
identity: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
roles?: string[];
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,10 @@ const layoutStore = useLayoutStore();
|
|||||||
</v-app>
|
</v-app>
|
||||||
</v-locale-provider>
|
</v-locale-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-main.page-wrapper {
|
||||||
|
height: calc(100vh - 64px) !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -46,4 +46,28 @@ abstract class ModuleInstanceAbstract implements ModuleInstanceInterface
|
|||||||
// Override in specific modules if needed
|
// Override in specific modules if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissions provided by this module
|
||||||
|
*
|
||||||
|
* @return array Permission definitions with metadata
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* return [
|
||||||
|
* 'user_manager.users.view' => [
|
||||||
|
* 'label' => 'View Users',
|
||||||
|
* 'description' => 'View user list and details',
|
||||||
|
* 'group' => 'User Management'
|
||||||
|
* ],
|
||||||
|
* 'user_manager.users.*' => [
|
||||||
|
* 'label' => 'Full User Management',
|
||||||
|
* 'description' => 'All user management permissions',
|
||||||
|
* 'group' => 'User Management'
|
||||||
|
* ]
|
||||||
|
* ];
|
||||||
|
*/
|
||||||
|
public function permissions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ use Attribute;
|
|||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
|
||||||
class AuthenticatedRoute
|
class AuthenticatedRoute
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param string $path Route path
|
||||||
|
* @param string $name Route name
|
||||||
|
* @param array $methods HTTP methods
|
||||||
|
* @param array $permissions Required permissions (OR logic - user needs ANY)
|
||||||
|
* Examples:
|
||||||
|
* - ['user.profile.edit'] - exact permission
|
||||||
|
* - ['user_manager.users.*'] - wildcard permission
|
||||||
|
* - ['user.edit.own', 'user.edit.any'] - multiple options
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $path,
|
public readonly string $path,
|
||||||
public readonly string $name,
|
public readonly string $name,
|
||||||
|
|||||||
Reference in New Issue
Block a user