Files
desk-moloni/modules/desk_moloni/libraries/DocumentAccessControl.php
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

575 lines
19 KiB
PHP

<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Document Access Control Library
* Handles security and permissions for client document access
*
* @package Desk-Moloni
* @version 3.0.0
* @author Descomplicar Business Solutions
*/
class DocumentAccessControl
{
private $CI;
private $cachePrefix = 'desk_moloni_access_';
private $cacheTimeout = 300; // 5 minutes
public function __construct()
{
$this->CI =& get_instance();
// Load required models
$this->CI->load->model('clients_model');
$this->CI->load->model('invoices_model');
$this->CI->load->model('estimates_model');
// Initialize cache
$this->CI->load->driver('cache');
}
/**
* Check if client can access a specific document
*
* @param int $clientId
* @param int $documentId
* @param string $documentType Optional document type for optimization
* @return bool
*/
public function canAccessDocument($clientId, $documentId, $documentType = null)
{
// Input validation
if (!is_numeric($clientId) || !is_numeric($documentId) || $clientId <= 0 || $documentId <= 0) {
return false;
}
// Check cache first
$cacheKey = $this->cachePrefix . "doc_{$clientId}_{$documentId}";
$cachedResult = $this->CI->cache->get($cacheKey);
if ($cachedResult !== false) {
return $cachedResult === 'allowed';
}
$hasAccess = false;
try {
// Verify client exists and is active
if (!$this->_isClientActiveAndValid($clientId)) {
$this->_cacheAccessResult($cacheKey, false);
return false;
}
// If document type is specified, check only that type
if ($documentType) {
$hasAccess = $this->_checkDocumentTypeAccess($clientId, $documentId, $documentType);
} else {
// Check all document types
$hasAccess = $this->_checkInvoiceAccess($clientId, $documentId) ||
$this->_checkEstimateAccess($clientId, $documentId) ||
$this->_checkCreditNoteAccess($clientId, $documentId) ||
$this->_checkReceiptAccess($clientId, $documentId);
}
// Cache the result
$this->_cacheAccessResult($cacheKey, $hasAccess);
} catch (Exception $e) {
log_message('error', 'Document access control error: ' . $e->getMessage());
$hasAccess = false;
}
return $hasAccess;
}
/**
* Check if client can access multiple documents
*
* @param int $clientId
* @param array $documentIds
* @return array Associative array [documentId => bool]
*/
public function canAccessMultipleDocuments($clientId, array $documentIds)
{
$results = [];
foreach ($documentIds as $documentId) {
$results[$documentId] = $this->canAccessDocument($clientId, $documentId);
}
return $results;
}
/**
* Get list of document IDs accessible by client
*
* @param int $clientId
* @param string $documentType Optional filter by document type
* @param array $filters Optional additional filters
* @return array
*/
public function getAccessibleDocuments($clientId, $documentType = null, array $filters = [])
{
// Input validation
if (!is_numeric($clientId) || $clientId <= 0) {
return [];
}
// Check if client is valid
if (!$this->_isClientActiveAndValid($clientId)) {
return [];
}
$documentIds = [];
try {
if (!$documentType || $documentType === 'invoice') {
$invoiceIds = $this->_getClientInvoiceIds($clientId, $filters);
$documentIds = array_merge($documentIds, $invoiceIds);
}
if (!$documentType || $documentType === 'estimate') {
$estimateIds = $this->_getClientEstimateIds($clientId, $filters);
$documentIds = array_merge($documentIds, $estimateIds);
}
if (!$documentType || $documentType === 'credit_note') {
$creditNoteIds = $this->_getClientCreditNoteIds($clientId, $filters);
$documentIds = array_merge($documentIds, $creditNoteIds);
}
if (!$documentType || $documentType === 'receipt') {
$receiptIds = $this->_getClientReceiptIds($clientId, $filters);
$documentIds = array_merge($documentIds, $receiptIds);
}
} catch (Exception $e) {
log_message('error', 'Get accessible documents error: ' . $e->getMessage());
return [];
}
return array_unique($documentIds);
}
/**
* Validate document access with detailed security checks
*
* @param int $clientId
* @param int $documentId
* @param string $action Action being performed (view, download, etc.)
* @return array Validation result with details
*/
public function validateDocumentAccess($clientId, $documentId, $action = 'view')
{
$result = [
'allowed' => false,
'reason' => 'Access denied',
'document_type' => null,
'security_level' => 'standard'
];
try {
// Basic validation
if (!is_numeric($clientId) || !is_numeric($documentId) || $clientId <= 0 || $documentId <= 0) {
$result['reason'] = 'Invalid parameters';
return $result;
}
// Check client validity
if (!$this->_isClientActiveAndValid($clientId)) {
$result['reason'] = 'Client not active or invalid';
return $result;
}
// Check document existence and ownership
$documentInfo = $this->_getDocumentInfo($documentId);
if (!$documentInfo) {
$result['reason'] = 'Document not found';
return $result;
}
if ($documentInfo['client_id'] != $clientId) {
$result['reason'] = 'Document does not belong to client';
$this->_logSecurityViolation($clientId, $documentId, $action, 'ownership_violation');
return $result;
}
// Check action permissions
if (!$this->_isActionAllowed($documentInfo['type'], $action)) {
$result['reason'] = 'Action not allowed for document type';
return $result;
}
// Check document-specific security rules
if (!$this->_checkDocumentSecurityRules($documentInfo, $action)) {
$result['reason'] = 'Document security rules violation';
return $result;
}
// All checks passed
$result['allowed'] = true;
$result['reason'] = 'Access granted';
$result['document_type'] = $documentInfo['type'];
$result['security_level'] = $this->_getDocumentSecurityLevel($documentInfo);
} catch (Exception $e) {
log_message('error', 'Document access validation error: ' . $e->getMessage());
$result['reason'] = 'System error during validation';
}
return $result;
}
/**
* Log security violation attempt
*
* @param int $clientId
* @param int $documentId
* @param string $action
* @param string $violationType
*/
public function logSecurityViolation($clientId, $documentId, $action, $violationType)
{
$this->_logSecurityViolation($clientId, $documentId, $action, $violationType);
}
/**
* Clear access cache for client
*
* @param int $clientId
*/
public function clearClientAccessCache($clientId)
{
// This would clear all cached access results for the client
// Implementation depends on cache driver capabilities
$pattern = $this->cachePrefix . "doc_{$clientId}_*";
// For file cache, we'd need to scan and delete
// For Redis, we could use pattern deletion
// For now, we'll just document the intent
log_message('info', "Access cache cleared for client {$clientId}");
}
// Private Methods
/**
* Check if client is active and valid
*/
private function _isClientActiveAndValid($clientId)
{
$client = $this->CI->clients_model->get($clientId);
return $client && $client['active'] == 1;
}
/**
* Check access for specific document type
*/
private function _checkDocumentTypeAccess($clientId, $documentId, $documentType)
{
switch ($documentType) {
case 'invoice':
return $this->_checkInvoiceAccess($clientId, $documentId);
case 'estimate':
return $this->_checkEstimateAccess($clientId, $documentId);
case 'credit_note':
return $this->_checkCreditNoteAccess($clientId, $documentId);
case 'receipt':
return $this->_checkReceiptAccess($clientId, $documentId);
default:
return false;
}
}
/**
* Check invoice access
*/
private function _checkInvoiceAccess($clientId, $documentId)
{
$invoice = $this->CI->invoices_model->get($documentId);
return $invoice && $invoice['clientid'] == $clientId;
}
/**
* Check estimate access
*/
private function _checkEstimateAccess($clientId, $documentId)
{
$estimate = $this->CI->estimates_model->get($documentId);
return $estimate && $estimate['clientid'] == $clientId;
}
/**
* Check credit note access
*/
private function _checkCreditNoteAccess($clientId, $documentId)
{
// Credit notes in Perfex CRM are typically linked to invoices
$creditNote = $this->CI->db->get_where('tblcreditnotes', ['id' => $documentId])->row_array();
return $creditNote && $creditNote['clientid'] == $clientId;
}
/**
* Check receipt access
*/
private function _checkReceiptAccess($clientId, $documentId)
{
// Receipts are typically payment records in Perfex CRM
$receipt = $this->CI->db->get_where('tblinvoicepaymentrecords', ['id' => $documentId])->row_array();
if (!$receipt) {
return false;
}
// Check if the payment belongs to an invoice owned by the client
$invoice = $this->CI->invoices_model->get($receipt['invoiceid']);
return $invoice && $invoice['clientid'] == $clientId;
}
/**
* Cache access result
*/
private function _cacheAccessResult($cacheKey, $hasAccess)
{
$value = $hasAccess ? 'allowed' : 'denied';
$this->CI->cache->save($cacheKey, $value, $this->cacheTimeout);
}
/**
* Get client invoice IDs
*/
private function _getClientInvoiceIds($clientId, array $filters = [])
{
$this->CI->db->select('id');
$this->CI->db->where('clientid', $clientId);
// Apply filters
if (isset($filters['status'])) {
$this->CI->db->where('status', $filters['status']);
}
if (isset($filters['from_date'])) {
$this->CI->db->where('date >=', $filters['from_date']);
}
if (isset($filters['to_date'])) {
$this->CI->db->where('date <=', $filters['to_date']);
}
$query = $this->CI->db->get('tblinvoices');
return array_column($query->result_array(), 'id');
}
/**
* Get client estimate IDs
*/
private function _getClientEstimateIds($clientId, array $filters = [])
{
$this->CI->db->select('id');
$this->CI->db->where('clientid', $clientId);
// Apply filters
if (isset($filters['status'])) {
$this->CI->db->where('status', $filters['status']);
}
if (isset($filters['from_date'])) {
$this->CI->db->where('date >=', $filters['from_date']);
}
if (isset($filters['to_date'])) {
$this->CI->db->where('date <=', $filters['to_date']);
}
$query = $this->CI->db->get('tblestimates');
return array_column($query->result_array(), 'id');
}
/**
* Get client credit note IDs
*/
private function _getClientCreditNoteIds($clientId, array $filters = [])
{
$this->CI->db->select('id');
$this->CI->db->where('clientid', $clientId);
// Apply filters if table exists
if ($this->CI->db->table_exists('tblcreditnotes')) {
if (isset($filters['from_date'])) {
$this->CI->db->where('date >=', $filters['from_date']);
}
if (isset($filters['to_date'])) {
$this->CI->db->where('date <=', $filters['to_date']);
}
$query = $this->CI->db->get('tblcreditnotes');
return array_column($query->result_array(), 'id');
}
return [];
}
/**
* Get client receipt IDs
*/
private function _getClientReceiptIds($clientId, array $filters = [])
{
// Get receipts through invoice payments
$this->CI->db->select('tblinvoicepaymentrecords.id');
$this->CI->db->join('tblinvoices', 'tblinvoices.id = tblinvoicepaymentrecords.invoiceid');
$this->CI->db->where('tblinvoices.clientid', $clientId);
// Apply filters
if (isset($filters['from_date'])) {
$this->CI->db->where('tblinvoicepaymentrecords.date >=', $filters['from_date']);
}
if (isset($filters['to_date'])) {
$this->CI->db->where('tblinvoicepaymentrecords.date <=', $filters['to_date']);
}
$query = $this->CI->db->get('tblinvoicepaymentrecords');
return array_column($query->result_array(), 'id');
}
/**
* Get document information
*/
private function _getDocumentInfo($documentId)
{
// Try to find document in different tables
// Check invoices
$invoice = $this->CI->db->get_where('tblinvoices', ['id' => $documentId])->row_array();
if ($invoice) {
return [
'id' => $documentId,
'type' => 'invoice',
'client_id' => $invoice['clientid'],
'status' => $invoice['status'],
'data' => $invoice
];
}
// Check estimates
$estimate = $this->CI->db->get_where('tblestimates', ['id' => $documentId])->row_array();
if ($estimate) {
return [
'id' => $documentId,
'type' => 'estimate',
'client_id' => $estimate['clientid'],
'status' => $estimate['status'],
'data' => $estimate
];
}
// Check credit notes
if ($this->CI->db->table_exists('tblcreditnotes')) {
$creditNote = $this->CI->db->get_where('tblcreditnotes', ['id' => $documentId])->row_array();
if ($creditNote) {
return [
'id' => $documentId,
'type' => 'credit_note',
'client_id' => $creditNote['clientid'],
'status' => $creditNote['status'] ?? 'active',
'data' => $creditNote
];
}
}
// Check receipts (payment records)
$receipt = $this->CI->db->get_where('tblinvoicepaymentrecords', ['id' => $documentId])->row_array();
if ($receipt) {
// Get client ID from associated invoice
$invoice = $this->CI->db->get_where('tblinvoices', ['id' => $receipt['invoiceid']])->row_array();
if ($invoice) {
return [
'id' => $documentId,
'type' => 'receipt',
'client_id' => $invoice['clientid'],
'status' => 'paid',
'data' => $receipt
];
}
}
return null;
}
/**
* Check if action is allowed for document type
*/
private function _isActionAllowed($documentType, $action)
{
$allowedActions = [
'invoice' => ['view', 'download', 'print'],
'estimate' => ['view', 'download', 'print'],
'credit_note' => ['view', 'download', 'print'],
'receipt' => ['view', 'download', 'print']
];
return isset($allowedActions[$documentType]) &&
in_array($action, $allowedActions[$documentType]);
}
/**
* Check document-specific security rules
*/
private function _checkDocumentSecurityRules($documentInfo, $action)
{
// Example security rules:
// Draft documents may have restricted access
if ($documentInfo['type'] === 'estimate' && $documentInfo['status'] == 1) {
// Draft estimate - only allow view
return $action === 'view';
}
// Cancelled documents may be read-only
if (isset($documentInfo['data']['status']) && $documentInfo['data']['status'] == 5) {
// Cancelled - only allow view
return $action === 'view';
}
// All other cases are allowed by default
return true;
}
/**
* Get document security level
*/
private function _getDocumentSecurityLevel($documentInfo)
{
// Determine security level based on document properties
if ($documentInfo['type'] === 'invoice' &&
isset($documentInfo['data']['total']) &&
$documentInfo['data']['total'] > 10000) {
return 'high'; // High-value invoices
}
return 'standard';
}
/**
* Log security violation
*/
private function _logSecurityViolation($clientId, $documentId, $action, $violationType)
{
$logData = [
'client_id' => $clientId,
'document_id' => $documentId,
'action' => $action,
'violation_type' => $violationType,
'ip_address' => $this->CI->input->ip_address(),
'user_agent' => $this->CI->input->user_agent(),
'timestamp' => date('Y-m-d H:i:s')
];
// Log to system log
log_message('warning', 'Security violation: ' . json_encode($logData));
// Could also save to database security log table if it exists
if ($this->CI->db->table_exists('tblsecurity_violations')) {
$this->CI->db->insert('tblsecurity_violations', $logData);
}
}
}