Files
easystream-main/f_core/f_classes/class.rbac.php
SamiAhmed7777 0b7e2d0a5b feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
2025-10-21 00:39:45 -07:00

925 lines
31 KiB
PHP

<?php
/*******************************************************************************************************************
| Software Name : EasyStream
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
| Software Author : (c) Sami Ahmed
|*******************************************************************************************************************
|
|*******************************************************************************************************************
| This source file is subject to the EasyStream Proprietary License Agreement.
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|*******************************************************************************************************************
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|*******************************************************************************************************************/
defined('_ISVALID') or header('Location: /error');
/**
* Role-Based Access Control (RBAC) System
*/
class VRBAC
{
private static $instance = null;
private $db;
private $logger;
private $cache;
private $userRoles = [];
private $rolePermissions = [];
// Default system roles
const ROLE_GUEST = 'guest';
const ROLE_MEMBER = 'member';
const ROLE_VERIFIED = 'verified';
const ROLE_STREAMER = 'streamer';
const ROLE_CREATOR = 'creator';
const ROLE_MODERATOR = 'moderator';
const ROLE_ADMIN = 'admin';
const ROLE_SUPER_ADMIN = 'super_admin';
// Permission categories
const PERM_CONTENT = 'content';
const PERM_USER = 'user';
const PERM_ADMIN = 'admin';
const PERM_SYSTEM = 'system';
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct()
{
$this->db = VDatabase::getInstance();
$this->logger = VLogger::getInstance();
$this->cache = VRedis::getInstance();
$this->initializeDefaultRoles();
}
/**
* Check if user has permission
* @param int|string $userId User ID or current session user
* @param string $permission Permission name
* @param array $context Additional context for permission check
* @return bool True if user has permission
*/
/**
* Flexible permission checker supporting both legacy and new call signatures.
*
* Supported usages:
* - hasPermission($userId, $permission, $context = [])
* - hasPermission($permission, $userId = 'current', $context = [])
* - hasPermission($permission, $contextArray) // uses current session user
*
* @param mixed $arg1 User ID or permission name depending on signature
* @param mixed $arg2 Permission name, user ID, or context array
* @param mixed $arg3 Context array when using legacy signature
* @return bool
*/
public function hasPermission($arg1 = null, $arg2 = null, $arg3 = [])
{
try {
$permission = null;
$userId = null;
$context = [];
if (is_string($arg1) && ($arg2 === null || is_scalar($arg2) || is_array($arg2))) {
// New-style signature: ($permission, $userId?, $context?)
$permission = $arg1;
if (is_array($arg2) && ($arg3 === [] || $arg3 === null)) {
$context = $arg2;
$userId = 'current';
} else {
$userId = $arg2;
$context = is_array($arg3) ? $arg3 : [];
}
} else {
// Legacy signature: ($userId, $permission, $context)
$userId = $arg1;
$permission = $arg2;
$context = is_array($arg3) ? $arg3 : [];
}
if (is_array($userId)) {
// Handle accidental signature: hasPermission($permission, $context)
$context = $userId;
$userId = 'current';
}
if (!is_array($context)) {
$context = [];
}
if (!$permission || !is_string($permission)) {
$this->logger->warning('Permission check called without a valid permission name', [
'arg1' => $arg1,
'arg2' => $arg2,
'arg3' => $arg3
]);
return false;
}
// Handle current user
if ($userId === 'current' || $userId === null) {
$userId = $_SESSION['USER_ID'] ?? null;
}
if (!$userId) {
// Guest user - check guest permissions
return $this->checkGuestPermission($permission, $context);
}
// Get user roles
$userRoles = $this->getUserRoles($userId);
if (empty($userRoles)) {
// No roles assigned - treat as guest
return $this->checkGuestPermission($permission, $context);
}
// Check if any role has the permission
foreach ($userRoles as $role) {
if ($this->roleHasPermission($role, $permission, $context)) {
// Log permission grant for audit
$this->logPermissionCheck($userId, $permission, true, $role, $context);
return true;
}
}
// Check for resource-specific permissions
if (isset($context['resource_type']) && isset($context['resource_id'])) {
if ($this->hasResourcePermission($userId, $permission, $context['resource_type'], $context['resource_id'])) {
$this->logPermissionCheck($userId, $permission, true, 'resource_specific', $context);
return true;
}
}
// Log permission denial
$this->logPermissionCheck($userId, $permission, false, implode(',', $userRoles), $context);
return false;
} catch (Exception $e) {
$this->logger->error('Permission check failed', [
'user_id' => $userId,
'permission' => $permission,
'context' => $context,
'error' => $e->getMessage()
]);
// Fail secure - deny permission on error
return false;
}
}
/**
* Check if a user (or current session) has a specific role.
* Falls back to guest semantics when no authenticated user is present.
*
* @param string $role Target role name
* @param int|string|null $userId User identifier, `'current'` (default) uses session
* @return bool
*/
public function hasRole($role, $userId = 'current')
{
try {
if ($userId === 'current' || $userId === null) {
$userId = $_SESSION['USER_ID'] ?? null;
}
if (!$userId) {
return $role === self::ROLE_GUEST;
}
$roles = $this->getUserRoles($userId);
return in_array($role, $roles, true);
} catch (Exception $e) {
$this->logger->error('Role check failed', [
'user_id' => $userId,
'role' => $role,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Assign role to user
* @param int $userId User ID
* @param string $role Role name
* @param int $assignedBy User ID who assigned the role
* @param string $reason Reason for assignment
* @return bool Success status
*/
public function assignRole($userId, $role, $assignedBy = null, $reason = '')
{
try {
// Validate role exists
if (!$this->roleExists($role)) {
throw new Exception("Role does not exist: {$role}");
}
// Check if user already has this role
if ($this->userHasRole($userId, $role)) {
return true; // Already has role
}
// Insert role assignment
$assignmentData = [
'user_id' => $userId,
'role_name' => $role,
'assigned_by' => $assignedBy,
'assigned_at' => date('Y-m-d H:i:s'),
'reason' => $reason,
'status' => 'active'
];
$assignmentId = $this->db->doInsert('db_user_roles', $assignmentData);
if ($assignmentId) {
// Clear user role cache
$this->clearUserRoleCache($userId);
// Log role assignment
$this->logger->info('Role assigned to user', [
'user_id' => $userId,
'role' => $role,
'assigned_by' => $assignedBy,
'reason' => $reason
]);
// Send notification to user
$this->sendRoleNotification($userId, 'role_assigned', $role);
$this->handleRoleAssignmentSideEffects($userId, $role);
return true;
}
return false;
} catch (Exception $e) {
$this->logger->error('Role assignment failed', [
'user_id' => $userId,
'role' => $role,
'assigned_by' => $assignedBy,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Remove role from user
* @param int $userId User ID
* @param string $role Role name
* @param int $removedBy User ID who removed the role
* @param string $reason Reason for removal
* @return bool Success status
*/
public function removeRole($userId, $role, $removedBy = null, $reason = '')
{
try {
// Update role assignment status
$updateData = [
'status' => 'revoked',
'revoked_by' => $removedBy,
'revoked_at' => date('Y-m-d H:i:s'),
'revoke_reason' => $reason
];
$result = $this->db->doUpdate(
'db_user_roles',
['user_id', 'role_name', 'status'],
$updateData,
[$userId, $role, 'active']
);
if ($result) {
// Clear user role cache
$this->clearUserRoleCache($userId);
// Log role removal
$this->logger->info('Role removed from user', [
'user_id' => $userId,
'role' => $role,
'removed_by' => $removedBy,
'reason' => $reason
]);
// Send notification to user
$this->sendRoleNotification($userId, 'role_removed', $role);
$this->handleRoleRemovalSideEffects($userId, $role);
return true;
}
return false;
} catch (Exception $e) {
$this->logger->error('Role removal failed', [
'user_id' => $userId,
'role' => $role,
'removed_by' => $removedBy,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Get user roles
* @param int $userId User ID
* @return array User roles
*/
public function getUserRoles($userId)
{
if (isset($this->userRoles[$userId])) {
return $this->userRoles[$userId];
}
// Try cache first
$cacheKey = "user_roles_{$userId}";
if ($this->cache->isConnected()) {
$cachedRoles = $this->cache->get($cacheKey);
if ($cachedRoles !== false) {
$this->userRoles[$userId] = $cachedRoles;
return $cachedRoles;
}
}
// Get from database
$query = "SELECT role_name FROM db_user_roles WHERE user_id = ? AND status = 'active'";
$result = $this->db->doQuery($query, [$userId]);
$roles = [];
while ($row = $this->db->doFetch($result)) {
$roles[] = $row['role_name'];
}
// Add default member role if user has no roles
if (empty($roles) && $this->userExists($userId)) {
$roles[] = self::ROLE_MEMBER;
}
// Cache roles
$this->userRoles[$userId] = $roles;
if ($this->cache->isConnected()) {
$this->cache->set($cacheKey, $roles, 3600); // Cache for 1 hour
}
return $roles;
}
/**
* Check if role has permission
* @param string $role Role name
* @param string $permission Permission name
* @param array $context Permission context
* @return bool True if role has permission
*/
public function roleHasPermission($role, $permission, $context = [])
{
// Get role permissions
$rolePermissions = $this->getRolePermissions($role);
// Check direct permission
if (in_array($permission, $rolePermissions)) {
return true;
}
// Check wildcard permissions
$permissionParts = explode('.', $permission);
for ($i = count($permissionParts) - 1; $i > 0; $i--) {
$wildcardPerm = implode('.', array_slice($permissionParts, 0, $i)) . '.*';
if (in_array($wildcardPerm, $rolePermissions)) {
return true;
}
}
// Check context-specific permissions
if (!empty($context)) {
return $this->checkContextualPermission($role, $permission, $context);
}
return false;
}
/**
* Get role permissions
* @param string $role Role name
* @return array Role permissions
*/
public function getRolePermissions($role)
{
if (isset($this->rolePermissions[$role])) {
return $this->rolePermissions[$role];
}
// Try cache first
$cacheKey = "role_permissions_{$role}";
if ($this->cache->isConnected()) {
$cachedPermissions = $this->cache->get($cacheKey);
if ($cachedPermissions !== false) {
$this->rolePermissions[$role] = $cachedPermissions;
return $cachedPermissions;
}
}
// Get from database
$query = "SELECT permission_name FROM db_role_permissions WHERE role_name = ? AND status = 'active'";
$result = $this->db->doQuery($query, [$role]);
$permissions = [];
while ($row = $this->db->doFetch($result)) {
$permissions[] = $row['permission_name'];
}
// Add default permissions for built-in roles
$permissions = array_merge($permissions, $this->getDefaultRolePermissions($role));
// Cache permissions
$this->rolePermissions[$role] = $permissions;
if ($this->cache->isConnected()) {
$this->cache->set($cacheKey, $permissions, 3600); // Cache for 1 hour
}
return $permissions;
}
/**
* Create custom role
* @param string $roleName Role name
* @param string $displayName Display name
* @param string $description Role description
* @param array $permissions Initial permissions
* @param int $createdBy User ID who created the role
* @return bool Success status
*/
public function createRole($roleName, $displayName, $description = '', $permissions = [], $createdBy = null)
{
try {
// Check if role already exists
if ($this->roleExists($roleName)) {
throw new Exception("Role already exists: {$roleName}");
}
// Insert role
$roleData = [
'role_name' => $roleName,
'display_name' => $displayName,
'description' => $description,
'created_by' => $createdBy,
'created_at' => date('Y-m-d H:i:s'),
'status' => 'active'
];
$roleId = $this->db->doInsert('db_roles', $roleData);
if ($roleId) {
// Add permissions
foreach ($permissions as $permission) {
$this->addRolePermission($roleName, $permission);
}
$this->logger->info('Custom role created', [
'role_name' => $roleName,
'display_name' => $displayName,
'permissions' => $permissions,
'created_by' => $createdBy
]);
return true;
}
return false;
} catch (Exception $e) {
$this->logger->error('Role creation failed', [
'role_name' => $roleName,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Add permission to role
* @param string $role Role name
* @param string $permission Permission name
* @return bool Success status
*/
public function addRolePermission($role, $permission)
{
try {
$permissionData = [
'role_name' => $role,
'permission_name' => $permission,
'granted_at' => date('Y-m-d H:i:s'),
'status' => 'active'
];
$result = $this->db->doInsert('db_role_permissions', $permissionData);
if ($result) {
// Clear role permission cache
$this->clearRolePermissionCache($role);
$this->logger->info('Permission added to role', [
'role' => $role,
'permission' => $permission
]);
return true;
}
return false;
} catch (Exception $e) {
$this->logger->error('Failed to add role permission', [
'role' => $role,
'permission' => $permission,
'error' => $e->getMessage()
]);
return false;
}
}
/**
* Initialize default system roles and permissions
*/
private function initializeDefaultRoles()
{
$defaultRoles = [
self::ROLE_GUEST => [
'display_name' => 'Guest',
'description' => 'Non-registered users',
'permissions' => [
'content.view',
'content.search',
'user.register'
]
],
self::ROLE_MEMBER => [
'display_name' => 'Member',
'description' => 'Registered users',
'permissions' => [
'content.*',
'user.profile.edit',
'user.upload.basic',
'user.comment',
'user.like',
'user.subscribe'
]
],
self::ROLE_VERIFIED => [
'display_name' => 'Verified User',
'description' => 'Email verified users',
'permissions' => [
'content.*',
'user.*',
'upload.advanced'
]
],
self::ROLE_STREAMER => [
'display_name' => 'Streamer',
'description' => 'Users allowed to broadcast live streams',
'permissions' => [
'live.stream.basic',
'live.stream.advanced',
'live.stream.record',
'live.stream.quality'
]
],
self::ROLE_CREATOR => [
'display_name' => 'Content Creator',
'description' => 'Content creators with enhanced publishing features',
'permissions' => [
'content.*',
'user.*',
'upload.*',
'analytics.view',
'monetization.basic'
]
],
self::ROLE_MODERATOR => [
'display_name' => 'Moderator',
'description' => 'Content moderators',
'permissions' => [
'content.*',
'user.*',
'moderation.*',
'admin.content.moderate',
'admin.users.moderate'
]
],
self::ROLE_ADMIN => [
'display_name' => 'Administrator',
'description' => 'Site administrators',
'permissions' => [
'content.*',
'user.*',
'admin.*',
'system.settings',
'system.logs.view'
]
],
self::ROLE_SUPER_ADMIN => [
'display_name' => 'Super Administrator',
'description' => 'Full system access',
'permissions' => [
'*' // All permissions
]
]
];
// This would typically be run during installation/setup
// For now, we'll store the default permissions in memory
foreach ($defaultRoles as $roleName => $roleData) {
$this->rolePermissions[$roleName] = $roleData['permissions'];
}
}
/**
* Get default permissions for built-in roles
* @param string $role Role name
* @return array Default permissions
*/
private function getDefaultRolePermissions($role)
{
$defaultPermissions = [
self::ROLE_GUEST => [
'content.view',
'content.search',
'user.register'
],
self::ROLE_MEMBER => [
'content.view',
'content.search',
'user.profile.edit',
'user.upload.basic',
'user.comment',
'user.like',
'user.subscribe'
],
self::ROLE_VERIFIED => [
'content.view',
'content.search',
'user.profile.edit',
'user.upload.basic',
'user.upload.advanced',
'user.comment',
'user.like',
'user.subscribe'
],
self::ROLE_STREAMER => [
'live.stream.basic',
'live.stream.advanced',
'live.stream.record',
'live.stream.quality'
],
self::ROLE_CREATOR => [
'content.view',
'content.search',
'user.profile.edit',
'user.upload.basic',
'user.upload.advanced',
'user.comment',
'user.like',
'user.subscribe',
'analytics.view',
'monetization.basic'
],
self::ROLE_MODERATOR => [
'content.view',
'content.search',
'content.moderate',
'user.profile.edit',
'user.moderate',
'admin.content.moderate',
'admin.users.moderate'
],
self::ROLE_ADMIN => [
'content.*',
'user.*',
'admin.*',
'system.settings',
'system.logs.view'
],
self::ROLE_SUPER_ADMIN => [
'*'
]
];
return $defaultPermissions[$role] ?? [];
}
/**
* Helper methods
*/
private function checkGuestPermission($permission, $context)
{
$guestPermissions = $this->getDefaultRolePermissions(self::ROLE_GUEST);
return in_array($permission, $guestPermissions) || in_array('*', $guestPermissions);
}
private function roleGrantsStreaming($role)
{
$streamingRoles = [
self::ROLE_STREAMER,
self::ROLE_ADMIN,
self::ROLE_SUPER_ADMIN
];
return in_array($role, $streamingRoles, true);
}
private function handleRoleAssignmentSideEffects($userId, $role)
{
if (!$this->roleGrantsStreaming($role)) {
return;
}
try {
VStreamKeyManager::getInstance()->activateStreaming($userId);
} catch (Exception $e) {
$this->logger->error('Failed to activate streaming during role assignment', [
'user_id' => $userId,
'role' => $role,
'error' => $e->getMessage()
]);
}
}
private function handleRoleRemovalSideEffects($userId, $role)
{
if (!$this->roleGrantsStreaming($role)) {
return;
}
try {
// If the user still has streaming permission via another role, do not disable.
if ($this->hasPermission($userId, 'live.stream.basic')) {
return;
}
VStreamKeyManager::getInstance()->deactivateStreaming($userId);
} catch (Exception $e) {
$this->logger->error('Failed to deactivate streaming during role removal', [
'user_id' => $userId,
'role' => $role,
'error' => $e->getMessage()
]);
}
}
private function roleExists($role)
{
$query = "SELECT COUNT(*) as count FROM db_roles WHERE role_name = ? AND status = 'active'";
$result = $this->db->doQuery($query, [$role]);
$row = $this->db->doFetch($result);
return ($row['count'] > 0) || in_array($role, [
self::ROLE_GUEST, self::ROLE_MEMBER, self::ROLE_VERIFIED,
self::ROLE_CREATOR, self::ROLE_MODERATOR, self::ROLE_ADMIN, self::ROLE_SUPER_ADMIN
]);
}
private function userHasRole($userId, $role)
{
$userRoles = $this->getUserRoles($userId);
return in_array($role, $userRoles);
}
private function userExists($userId)
{
$query = "SELECT COUNT(*) as count FROM db_accountuser WHERE usr_id = ?";
$result = $this->db->doQuery($query, [$userId]);
$row = $this->db->doFetch($result);
return $row['count'] > 0;
}
private function hasResourcePermission($userId, $permission, $resourceType, $resourceId)
{
// Check if user owns the resource
if ($this->userOwnsResource($userId, $resourceType, $resourceId)) {
return true;
}
// Check resource-specific permissions
$query = "SELECT COUNT(*) as count FROM db_resource_permissions
WHERE user_id = ? AND resource_type = ? AND resource_id = ?
AND permission_name = ? AND status = 'active'";
$result = $this->db->doQuery($query, [$userId, $resourceType, $resourceId, $permission]);
$row = $this->db->doFetch($result);
return $row['count'] > 0;
}
private function userOwnsResource($userId, $resourceType, $resourceId)
{
$ownershipQueries = [
'video' => "SELECT COUNT(*) as count FROM db_videofiles WHERE file_key = ? AND usr_id = ?",
'channel' => "SELECT COUNT(*) as count FROM db_channels WHERE ch_id = ? AND usr_id = ?",
'playlist' => "SELECT COUNT(*) as count FROM db_playlists WHERE pl_id = ? AND usr_id = ?"
];
if (!isset($ownershipQueries[$resourceType])) {
return false;
}
$result = $this->db->doQuery($ownershipQueries[$resourceType], [$resourceId, $userId]);
$row = $this->db->doFetch($result);
return $row['count'] > 0;
}
private function checkContextualPermission($role, $permission, $context)
{
// Implement context-specific permission logic
// For example, moderators can only moderate content in their assigned categories
if ($role === self::ROLE_MODERATOR && isset($context['category'])) {
// Check if moderator is assigned to this category
$query = "SELECT COUNT(*) as count FROM db_moderator_categories
WHERE role_name = ? AND category = ? AND status = 'active'";
$result = $this->db->doQuery($query, [$role, $context['category']]);
$row = $this->db->doFetch($result);
return $row['count'] > 0;
}
return false;
}
private function clearUserRoleCache($userId)
{
unset($this->userRoles[$userId]);
if ($this->cache->isConnected()) {
$this->cache->delete("user_roles_{$userId}");
}
}
private function clearRolePermissionCache($role)
{
unset($this->rolePermissions[$role]);
if ($this->cache->isConnected()) {
$this->cache->delete("role_permissions_{$role}");
}
}
private function logPermissionCheck($userId, $permission, $granted, $role, $context)
{
$this->logger->info('Permission check', [
'user_id' => $userId,
'permission' => $permission,
'granted' => $granted,
'role' => $role,
'context' => $context,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
]);
}
private function sendRoleNotification($userId, $type, $role)
{
try {
$queue = new VQueue();
$notificationData = [
'type' => 'in_app',
'recipient' => $userId,
'subject' => $type === 'role_assigned' ? 'Role Assigned' : 'Role Removed',
'message' => $type === 'role_assigned'
? "You have been assigned the role: {$role}"
: "Your role has been removed: {$role}",
'template_data' => [
'notification_type' => $type,
'data' => ['role' => $role]
]
];
$queue->enqueue('NotificationJob', $notificationData, 'notifications');
} catch (Exception $e) {
$this->logger->error('Failed to send role notification', [
'user_id' => $userId,
'type' => $type,
'role' => $role,
'error' => $e->getMessage()
]);
}
}
}