- 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.
575 lines
19 KiB
PHP
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);
|
|
}
|
|
}
|
|
} |