diff --git a/app/Services/MemberService.php b/app/Services/MemberService.php index 64fa711..eff19c6 100644 --- a/app/Services/MemberService.php +++ b/app/Services/MemberService.php @@ -237,88 +237,96 @@ public static function getUserInfoForLogin(int $userId): array })->values()->toArray(), ]; - // 4. 메뉴 권한 체크 (menu:{menu_id}.view 패턴) - // 4-1. 사용자 역할 기반 권한 - $userRolePermissions = DB::table('model_has_roles') - ->join('role_has_permissions', 'model_has_roles.role_id', '=', 'role_has_permissions.role_id') + // 4. 메뉴 권한 체크 (mng UserPermissionService와 동일한 로직) + $now = now(); + + // 4-1. 역할 권한 (user_roles 테이블) + $rolePermissions = DB::table('user_roles') + ->join('role_has_permissions', 'user_roles.role_id', '=', 'role_has_permissions.role_id') ->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id') - ->where('model_has_roles.model_type', User::class) - ->where('model_has_roles.model_id', $userId) - ->where('model_has_roles.tenant_id', $tenant->id) + ->where('user_roles.user_id', $userId) + ->where('user_roles.tenant_id', $tenant->id) + ->whereNull('user_roles.deleted_at') ->where('permissions.name', 'like', 'menu:%.view') - ->select('permissions.name'); - - // 4-2. 사용자 직접 권한 - $userDirectPermissions = DB::table('model_has_permissions') - ->join('permissions', 'model_has_permissions.permission_id', '=', 'permissions.id') - ->where('model_has_permissions.model_type', User::class) - ->where('model_has_permissions.model_id', $userId) - ->where('model_has_permissions.tenant_id', $tenant->id) - ->where('permissions.name', 'like', 'menu:%.view') - ->select('permissions.name'); - - // 4-3. 부서 역할 기반 권한 (User → department_user → Department → role → permissions) - $departmentRolePermissions = DB::table('department_user') - ->join('model_has_roles', function ($join) { - $join->on('department_user.department_id', '=', 'model_has_roles.model_id') - ->where('model_has_roles.model_type', '=', Department::class); - }) - ->join('role_has_permissions', 'model_has_roles.role_id', '=', 'role_has_permissions.role_id') - ->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id') - ->where('department_user.user_id', $userId) - ->where('department_user.tenant_id', $tenant->id) - ->where('permissions.name', 'like', 'menu:%.view') - ->select('permissions.name'); - - // 4-4. 모든 권한 통합 (UNION) - $rolePermissions = $userRolePermissions - ->union($userDirectPermissions) - ->union($departmentRolePermissions) - ->pluck('name') + ->pluck('permissions.name') ->toArray(); - // 4-2. Override 권한 (명시적 허용/차단) - $overrides = DB::table('permission_overrides') - ->join('permissions', 'permission_overrides.permission_id', '=', 'permissions.id') + // 4-2. 부서 권한 (permission_overrides에서 Department 타입, effect=1) + $deptPermissions = DB::table('department_user') + ->join('permission_overrides', function ($join) use ($now) { + $join->on('permission_overrides.model_id', '=', 'department_user.department_id') + ->where('permission_overrides.model_type', '=', Department::class) + ->whereNull('permission_overrides.deleted_at') + ->where('permission_overrides.effect', 1) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_from') + ->orWhere('permission_overrides.effective_from', '<=', $now); + }) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_to') + ->orWhere('permission_overrides.effective_to', '>=', $now); + }); + }) + ->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id') + ->whereNull('department_user.deleted_at') + ->where('department_user.user_id', $userId) + ->where('department_user.tenant_id', $tenant->id) ->where('permission_overrides.tenant_id', $tenant->id) - ->where('permission_overrides.model_type', User::class) - ->where('permission_overrides.model_id', $userId) ->where('permissions.name', 'like', 'menu:%.view') - ->where(function ($q) { + ->pluck('permissions.name') + ->toArray(); + + // 4-3. 개인 ALLOW 권한 (permission_overrides에서 User 타입, effect=1) + // Note: mng는 App\Models\User를 사용하므로 하드코딩 + $personalAllows = DB::table('permission_overrides') + ->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id') + ->where('permission_overrides.model_type', 'App\\Models\\User') + ->where('permission_overrides.model_id', $userId) + ->where('permission_overrides.tenant_id', $tenant->id) + ->where('permission_overrides.effect', 1) + ->whereNull('permission_overrides.deleted_at') + ->where(function ($q) use ($now) { $q->whereNull('permission_overrides.effective_from') - ->orWhere('permission_overrides.effective_from', '<=', now()); + ->orWhere('permission_overrides.effective_from', '<=', $now); }) - ->where(function ($q) { + ->where(function ($q) use ($now) { $q->whereNull('permission_overrides.effective_to') - ->orWhere('permission_overrides.effective_to', '>=', now()); + ->orWhere('permission_overrides.effective_to', '>=', $now); }) - ->select('permissions.name', 'permission_overrides.effect') - ->get() - ->keyBy('name'); + ->where('permissions.name', 'like', 'menu:%.view') + ->pluck('permissions.name') + ->toArray(); - // 4-3. 최종 권한 계산: (기본 || override allow) && !override deny + // 4-4. 개인 DENY 권한 (permission_overrides에서 User 타입, effect=0) + // Note: mng는 App\Models\User를 사용하므로 하드코딩 + $personalDenies = DB::table('permission_overrides') + ->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id') + ->where('permission_overrides.model_type', 'App\\Models\\User') + ->where('permission_overrides.model_id', $userId) + ->where('permission_overrides.tenant_id', $tenant->id) + ->where('permission_overrides.effect', 0) + ->whereNull('permission_overrides.deleted_at') + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_from') + ->orWhere('permission_overrides.effective_from', '<=', $now); + }) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_to') + ->orWhere('permission_overrides.effective_to', '>=', $now); + }) + ->where('permissions.name', 'like', 'menu:%.view') + ->pluck('permissions.name') + ->toArray(); + + // 4-5. 최종 권한 계산: (역할 OR 부서 OR 개인ALLOW) - 개인DENY + $allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows)); + $effectivePermissions = array_diff($allAllowed, $personalDenies); + + // 메뉴 ID 추출 $allowedMenuIds = []; - $allMenuPermissions = array_unique(array_merge( - $rolePermissions, - $overrides->keys()->toArray() - )); - - foreach ($allMenuPermissions as $permName) { + foreach ($effectivePermissions as $permName) { if (preg_match('/^menu:(\d+)\.view$/', $permName, $matches)) { - $menuId = (int) $matches[1]; - - // Override deny 체크 - if (isset($overrides[$permName]) && $overrides[$permName]->effect === -1) { - continue; // 강제 차단 - } - - // Override allow 또는 기본 Role 권한 - if ( - (isset($overrides[$permName]) && $overrides[$permName]->effect === 1) || - in_array($permName, $rolePermissions, true) - ) { - $allowedMenuIds[] = $menuId; - } + $allowedMenuIds[] = (int) $matches[1]; } } @@ -334,12 +342,12 @@ public static function getUserInfoForLogin(int $userId): array ->toArray(); } - // 6. 역할(Role) 정보 조회 - $roles = DB::table('model_has_roles') - ->join('roles', 'model_has_roles.role_id', '=', 'roles.id') - ->where('model_has_roles.model_type', User::class) - ->where('model_has_roles.model_id', $userId) - ->where('model_has_roles.tenant_id', $tenant->id) + // 6. 역할(Role) 정보 조회 (user_roles 테이블 사용 - mng와 동일) + $roles = DB::table('user_roles') + ->join('roles', 'user_roles.role_id', '=', 'roles.id') + ->where('user_roles.user_id', $userId) + ->where('user_roles.tenant_id', $tenant->id) + ->whereNull('user_roles.deleted_at') ->select('roles.id', 'roles.name', 'roles.description') ->get() ->toArray(); diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php index 170ada6..b20a2f0 100644 --- a/app/Services/MenuService.php +++ b/app/Services/MenuService.php @@ -3,6 +3,8 @@ namespace App\Services; use App\Models\Commons\Menu; +use App\Models\Members\User; +use App\Models\Tenants\Department; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; @@ -16,20 +18,32 @@ protected static function tenantId(): ?int protected static function actorId(): ?int { - $user = app('api_user'); // 컨테이너에 주입된 인증 사용자(객체 or 배열) + $uid = app('api_user'); - return is_object($user) ? ($user->id ?? null) : ($user['id'] ?? null); + return $uid ? (int) $uid : null; } /** - * 메뉴 목록 조회 + * 메뉴 목록 조회 (사용자 권한 기반 필터링) */ public static function index(array $params) { $tenantId = self::tenantId(); + $userId = self::actorId(); + + // 권한이 있는 메뉴 ID 목록 조회 + $allowedMenuIds = self::getAllowedMenuIds($userId, $tenantId); $q = Menu::query()->withShared($tenantId); + // 권한 기반 필터링 적용 + if (! empty($allowedMenuIds)) { + $q->whereIn('id', $allowedMenuIds); + } else { + // 권한이 없으면 빈 결과 반환 + $q->whereRaw('1 = 0'); + } + if (array_key_exists('parent_id', $params)) { $q->where('parent_id', $params['parent_id']); } @@ -46,6 +60,110 @@ public static function index(array $params) return $q->get(); } + /** + * 사용자가 접근 가능한 메뉴 ID 목록 조회 (mng UserPermissionService와 동일한 로직) + */ + protected static function getAllowedMenuIds(?int $userId, ?int $tenantId): array + { + if (! $userId || ! $tenantId) { + return []; + } + + $now = now(); + + // 1. 역할 권한 (user_roles 테이블) + $rolePermissions = DB::table('user_roles') + ->join('role_has_permissions', 'user_roles.role_id', '=', 'role_has_permissions.role_id') + ->join('permissions', 'role_has_permissions.permission_id', '=', 'permissions.id') + ->where('user_roles.user_id', $userId) + ->where('user_roles.tenant_id', $tenantId) + ->whereNull('user_roles.deleted_at') + ->where('permissions.name', 'like', 'menu:%.view') + ->pluck('permissions.name') + ->toArray(); + + // 2. 부서 권한 (permission_overrides에서 Department 타입, effect=1) + $deptPermissions = DB::table('department_user') + ->join('permission_overrides', function ($join) use ($now) { + $join->on('permission_overrides.model_id', '=', 'department_user.department_id') + ->where('permission_overrides.model_type', '=', Department::class) + ->whereNull('permission_overrides.deleted_at') + ->where('permission_overrides.effect', 1) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_from') + ->orWhere('permission_overrides.effective_from', '<=', $now); + }) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_to') + ->orWhere('permission_overrides.effective_to', '>=', $now); + }); + }) + ->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id') + ->whereNull('department_user.deleted_at') + ->where('department_user.user_id', $userId) + ->where('department_user.tenant_id', $tenantId) + ->where('permission_overrides.tenant_id', $tenantId) + ->where('permissions.name', 'like', 'menu:%.view') + ->pluck('permissions.name') + ->toArray(); + + // 3. 개인 ALLOW 권한 (permission_overrides에서 User 타입, effect=1) + // Note: mng는 App\Models\User를 사용하므로 하드코딩 + $personalAllows = DB::table('permission_overrides') + ->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id') + ->where('permission_overrides.model_type', 'App\\Models\\User') + ->where('permission_overrides.model_id', $userId) + ->where('permission_overrides.tenant_id', $tenantId) + ->where('permission_overrides.effect', 1) + ->whereNull('permission_overrides.deleted_at') + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_from') + ->orWhere('permission_overrides.effective_from', '<=', $now); + }) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_to') + ->orWhere('permission_overrides.effective_to', '>=', $now); + }) + ->where('permissions.name', 'like', 'menu:%.view') + ->pluck('permissions.name') + ->toArray(); + + // 4. 개인 DENY 권한 (permission_overrides에서 User 타입, effect=0) + // Note: mng는 App\Models\User를 사용하므로 하드코딩 + $personalDenies = DB::table('permission_overrides') + ->join('permissions', 'permissions.id', '=', 'permission_overrides.permission_id') + ->where('permission_overrides.model_type', 'App\\Models\\User') + ->where('permission_overrides.model_id', $userId) + ->where('permission_overrides.tenant_id', $tenantId) + ->where('permission_overrides.effect', 0) + ->whereNull('permission_overrides.deleted_at') + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_from') + ->orWhere('permission_overrides.effective_from', '<=', $now); + }) + ->where(function ($q) use ($now) { + $q->whereNull('permission_overrides.effective_to') + ->orWhere('permission_overrides.effective_to', '>=', $now); + }) + ->where('permissions.name', 'like', 'menu:%.view') + ->pluck('permissions.name') + ->toArray(); + + // 5. 최종 권한 계산: (역할 OR 부서 OR 개인ALLOW) - 개인DENY + $allAllowed = array_unique(array_merge($rolePermissions, $deptPermissions, $personalAllows)); + $effectivePermissions = array_diff($allAllowed, $personalDenies); + + // 메뉴 ID 추출 + $allowedMenuIds = []; + foreach ($effectivePermissions as $permName) { + if (preg_match('/^menu:(\d+)\.view$/', $permName, $matches)) { + $allowedMenuIds[] = (int) $matches[1]; + } + } + + return $allowedMenuIds; + } + /** * 메뉴 단건 조회 */