coati-payroll 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of coati-payroll might be problematic. Click here for more details.
- coati_payroll/__init__.py +415 -0
- coati_payroll/app.py +95 -0
- coati_payroll/audit_helpers.py +904 -0
- coati_payroll/auth.py +123 -0
- coati_payroll/cli.py +1318 -0
- coati_payroll/config.py +219 -0
- coati_payroll/demo_data.py +813 -0
- coati_payroll/enums.py +278 -0
- coati_payroll/forms.py +1769 -0
- coati_payroll/formula_engine/__init__.py +81 -0
- coati_payroll/formula_engine/ast/__init__.py +110 -0
- coati_payroll/formula_engine/ast/ast_visitor.py +259 -0
- coati_payroll/formula_engine/ast/expression_evaluator.py +228 -0
- coati_payroll/formula_engine/ast/safe_operators.py +131 -0
- coati_payroll/formula_engine/ast/type_converter.py +172 -0
- coati_payroll/formula_engine/data_sources.py +752 -0
- coati_payroll/formula_engine/engine.py +247 -0
- coati_payroll/formula_engine/exceptions.py +52 -0
- coati_payroll/formula_engine/execution/__init__.py +24 -0
- coati_payroll/formula_engine/execution/execution_context.py +52 -0
- coati_payroll/formula_engine/execution/step_executor.py +62 -0
- coati_payroll/formula_engine/execution/variable_store.py +59 -0
- coati_payroll/formula_engine/novelty_codes.py +206 -0
- coati_payroll/formula_engine/results/__init__.py +20 -0
- coati_payroll/formula_engine/results/execution_result.py +59 -0
- coati_payroll/formula_engine/steps/__init__.py +30 -0
- coati_payroll/formula_engine/steps/assignment_step.py +71 -0
- coati_payroll/formula_engine/steps/base_step.py +48 -0
- coati_payroll/formula_engine/steps/calculation_step.py +42 -0
- coati_payroll/formula_engine/steps/conditional_step.py +122 -0
- coati_payroll/formula_engine/steps/step_factory.py +58 -0
- coati_payroll/formula_engine/steps/tax_lookup_step.py +45 -0
- coati_payroll/formula_engine/tables/__init__.py +24 -0
- coati_payroll/formula_engine/tables/bracket_calculator.py +51 -0
- coati_payroll/formula_engine/tables/table_lookup.py +161 -0
- coati_payroll/formula_engine/tables/tax_table.py +32 -0
- coati_payroll/formula_engine/validation/__init__.py +24 -0
- coati_payroll/formula_engine/validation/schema_validator.py +37 -0
- coati_payroll/formula_engine/validation/security_validator.py +52 -0
- coati_payroll/formula_engine/validation/tax_table_validator.py +205 -0
- coati_payroll/formula_engine_examples.py +153 -0
- coati_payroll/i18n.py +54 -0
- coati_payroll/initial_data.py +613 -0
- coati_payroll/interes_engine.py +450 -0
- coati_payroll/liquidacion_engine/__init__.py +25 -0
- coati_payroll/liquidacion_engine/engine.py +267 -0
- coati_payroll/locale_config.py +165 -0
- coati_payroll/log.py +138 -0
- coati_payroll/model.py +2410 -0
- coati_payroll/nomina_engine/__init__.py +87 -0
- coati_payroll/nomina_engine/calculators/__init__.py +30 -0
- coati_payroll/nomina_engine/calculators/benefit_calculator.py +79 -0
- coati_payroll/nomina_engine/calculators/concept_calculator.py +254 -0
- coati_payroll/nomina_engine/calculators/deduction_calculator.py +105 -0
- coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +51 -0
- coati_payroll/nomina_engine/calculators/perception_calculator.py +75 -0
- coati_payroll/nomina_engine/calculators/salary_calculator.py +86 -0
- coati_payroll/nomina_engine/domain/__init__.py +27 -0
- coati_payroll/nomina_engine/domain/calculation_items.py +52 -0
- coati_payroll/nomina_engine/domain/employee_calculation.py +53 -0
- coati_payroll/nomina_engine/domain/payroll_context.py +44 -0
- coati_payroll/nomina_engine/engine.py +188 -0
- coati_payroll/nomina_engine/processors/__init__.py +28 -0
- coati_payroll/nomina_engine/processors/accounting_processor.py +171 -0
- coati_payroll/nomina_engine/processors/accumulation_processor.py +90 -0
- coati_payroll/nomina_engine/processors/loan_processor.py +227 -0
- coati_payroll/nomina_engine/processors/novelty_processor.py +42 -0
- coati_payroll/nomina_engine/processors/vacation_processor.py +67 -0
- coati_payroll/nomina_engine/repositories/__init__.py +32 -0
- coati_payroll/nomina_engine/repositories/acumulado_repository.py +83 -0
- coati_payroll/nomina_engine/repositories/base_repository.py +40 -0
- coati_payroll/nomina_engine/repositories/config_repository.py +102 -0
- coati_payroll/nomina_engine/repositories/employee_repository.py +34 -0
- coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +58 -0
- coati_payroll/nomina_engine/repositories/novelty_repository.py +54 -0
- coati_payroll/nomina_engine/repositories/planilla_repository.py +52 -0
- coati_payroll/nomina_engine/results/__init__.py +24 -0
- coati_payroll/nomina_engine/results/error_result.py +28 -0
- coati_payroll/nomina_engine/results/payroll_result.py +53 -0
- coati_payroll/nomina_engine/results/validation_result.py +39 -0
- coati_payroll/nomina_engine/services/__init__.py +22 -0
- coati_payroll/nomina_engine/services/accounting_voucher_service.py +708 -0
- coati_payroll/nomina_engine/services/employee_processing_service.py +173 -0
- coati_payroll/nomina_engine/services/payroll_execution_service.py +374 -0
- coati_payroll/nomina_engine/services/snapshot_service.py +295 -0
- coati_payroll/nomina_engine/validators/__init__.py +31 -0
- coati_payroll/nomina_engine/validators/base_validator.py +48 -0
- coati_payroll/nomina_engine/validators/currency_validator.py +50 -0
- coati_payroll/nomina_engine/validators/employee_validator.py +87 -0
- coati_payroll/nomina_engine/validators/period_validator.py +44 -0
- coati_payroll/nomina_engine/validators/planilla_validator.py +136 -0
- coati_payroll/plugin_manager.py +176 -0
- coati_payroll/queue/__init__.py +33 -0
- coati_payroll/queue/driver.py +127 -0
- coati_payroll/queue/drivers/__init__.py +22 -0
- coati_payroll/queue/drivers/dramatiq_driver.py +268 -0
- coati_payroll/queue/drivers/huey_driver.py +390 -0
- coati_payroll/queue/drivers/noop_driver.py +54 -0
- coati_payroll/queue/selector.py +121 -0
- coati_payroll/queue/tasks.py +764 -0
- coati_payroll/rate_limiting.py +83 -0
- coati_payroll/rbac.py +183 -0
- coati_payroll/report_engine.py +512 -0
- coati_payroll/report_export.py +208 -0
- coati_payroll/schema_validator.py +167 -0
- coati_payroll/security.py +77 -0
- coati_payroll/static/styles.css +1044 -0
- coati_payroll/system_reports.py +573 -0
- coati_payroll/templates/auth/login.html +189 -0
- coati_payroll/templates/base.html +283 -0
- coati_payroll/templates/index.html +227 -0
- coati_payroll/templates/macros.html +146 -0
- coati_payroll/templates/modules/calculation_rule/form.html +78 -0
- coati_payroll/templates/modules/calculation_rule/index.html +102 -0
- coati_payroll/templates/modules/calculation_rule/schema_editor.html +1159 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/form.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/index.html +170 -0
- coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +193 -0
- coati_payroll/templates/modules/config_calculos/index.html +44 -0
- coati_payroll/templates/modules/configuracion/index.html +90 -0
- coati_payroll/templates/modules/currency/form.html +47 -0
- coati_payroll/templates/modules/currency/index.html +64 -0
- coati_payroll/templates/modules/custom_field/form.html +62 -0
- coati_payroll/templates/modules/custom_field/index.html +78 -0
- coati_payroll/templates/modules/deduccion/form.html +1 -0
- coati_payroll/templates/modules/deduccion/index.html +1 -0
- coati_payroll/templates/modules/employee/form.html +254 -0
- coati_payroll/templates/modules/employee/index.html +76 -0
- coati_payroll/templates/modules/empresa/form.html +74 -0
- coati_payroll/templates/modules/empresa/index.html +71 -0
- coati_payroll/templates/modules/exchange_rate/form.html +47 -0
- coati_payroll/templates/modules/exchange_rate/import.html +93 -0
- coati_payroll/templates/modules/exchange_rate/index.html +114 -0
- coati_payroll/templates/modules/liquidacion/index.html +58 -0
- coati_payroll/templates/modules/liquidacion/nueva.html +51 -0
- coati_payroll/templates/modules/liquidacion/ver.html +91 -0
- coati_payroll/templates/modules/payroll_concepts/audit_log.html +146 -0
- coati_payroll/templates/modules/percepcion/form.html +1 -0
- coati_payroll/templates/modules/percepcion/index.html +1 -0
- coati_payroll/templates/modules/planilla/config.html +190 -0
- coati_payroll/templates/modules/planilla/config_deducciones.html +129 -0
- coati_payroll/templates/modules/planilla/config_empleados.html +116 -0
- coati_payroll/templates/modules/planilla/config_percepciones.html +113 -0
- coati_payroll/templates/modules/planilla/config_prestaciones.html +118 -0
- coati_payroll/templates/modules/planilla/config_reglas.html +120 -0
- coati_payroll/templates/modules/planilla/ejecutar_nomina.html +106 -0
- coati_payroll/templates/modules/planilla/form.html +197 -0
- coati_payroll/templates/modules/planilla/index.html +144 -0
- coati_payroll/templates/modules/planilla/listar_nominas.html +91 -0
- coati_payroll/templates/modules/planilla/log_nomina.html +135 -0
- coati_payroll/templates/modules/planilla/novedades/form.html +177 -0
- coati_payroll/templates/modules/planilla/novedades/index.html +170 -0
- coati_payroll/templates/modules/planilla/ver_nomina.html +477 -0
- coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +231 -0
- coati_payroll/templates/modules/plugins/index.html +71 -0
- coati_payroll/templates/modules/prestacion/form.html +1 -0
- coati_payroll/templates/modules/prestacion/index.html +1 -0
- coati_payroll/templates/modules/prestacion_management/dashboard.html +150 -0
- coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +195 -0
- coati_payroll/templates/modules/prestamo/approve.html +156 -0
- coati_payroll/templates/modules/prestamo/condonacion.html +249 -0
- coati_payroll/templates/modules/prestamo/detail.html +443 -0
- coati_payroll/templates/modules/prestamo/form.html +203 -0
- coati_payroll/templates/modules/prestamo/index.html +150 -0
- coati_payroll/templates/modules/prestamo/pago_extraordinario.html +211 -0
- coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +181 -0
- coati_payroll/templates/modules/report/admin_index.html +125 -0
- coati_payroll/templates/modules/report/detail.html +129 -0
- coati_payroll/templates/modules/report/execute.html +266 -0
- coati_payroll/templates/modules/report/index.html +95 -0
- coati_payroll/templates/modules/report/permissions.html +64 -0
- coati_payroll/templates/modules/settings/index.html +274 -0
- coati_payroll/templates/modules/shared/concept_form.html +201 -0
- coati_payroll/templates/modules/shared/concept_index.html +145 -0
- coati_payroll/templates/modules/tipo_planilla/form.html +70 -0
- coati_payroll/templates/modules/tipo_planilla/index.html +68 -0
- coati_payroll/templates/modules/user/form.html +65 -0
- coati_payroll/templates/modules/user/index.html +76 -0
- coati_payroll/templates/modules/user/profile.html +81 -0
- coati_payroll/templates/modules/vacation/account_detail.html +149 -0
- coati_payroll/templates/modules/vacation/account_form.html +52 -0
- coati_payroll/templates/modules/vacation/account_index.html +68 -0
- coati_payroll/templates/modules/vacation/dashboard.html +156 -0
- coati_payroll/templates/modules/vacation/initial_balance_bulk.html +149 -0
- coati_payroll/templates/modules/vacation/initial_balance_form.html +93 -0
- coati_payroll/templates/modules/vacation/leave_request_detail.html +158 -0
- coati_payroll/templates/modules/vacation/leave_request_form.html +61 -0
- coati_payroll/templates/modules/vacation/leave_request_index.html +98 -0
- coati_payroll/templates/modules/vacation/policy_detail.html +176 -0
- coati_payroll/templates/modules/vacation/policy_form.html +152 -0
- coati_payroll/templates/modules/vacation/policy_index.html +79 -0
- coati_payroll/templates/modules/vacation/register_taken_form.html +178 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/en/LC_MESSAGES/messages.po +7283 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
- coati_payroll/translations/es/LC_MESSAGES/messages.po +7374 -0
- coati_payroll/vacation_service.py +451 -0
- coati_payroll/version.py +18 -0
- coati_payroll/vistas/__init__.py +64 -0
- coati_payroll/vistas/calculation_rule.py +307 -0
- coati_payroll/vistas/carga_inicial_prestacion.py +423 -0
- coati_payroll/vistas/config_calculos.py +72 -0
- coati_payroll/vistas/configuracion.py +87 -0
- coati_payroll/vistas/constants.py +17 -0
- coati_payroll/vistas/currency.py +112 -0
- coati_payroll/vistas/custom_field.py +120 -0
- coati_payroll/vistas/employee.py +305 -0
- coati_payroll/vistas/empresa.py +153 -0
- coati_payroll/vistas/exchange_rate.py +341 -0
- coati_payroll/vistas/liquidacion.py +205 -0
- coati_payroll/vistas/payroll_concepts.py +580 -0
- coati_payroll/vistas/planilla/__init__.py +38 -0
- coati_payroll/vistas/planilla/association_routes.py +238 -0
- coati_payroll/vistas/planilla/config_routes.py +158 -0
- coati_payroll/vistas/planilla/export_routes.py +175 -0
- coati_payroll/vistas/planilla/helpers/__init__.py +34 -0
- coati_payroll/vistas/planilla/helpers/association_helpers.py +161 -0
- coati_payroll/vistas/planilla/helpers/excel_helpers.py +29 -0
- coati_payroll/vistas/planilla/helpers/form_helpers.py +97 -0
- coati_payroll/vistas/planilla/nomina_routes.py +488 -0
- coati_payroll/vistas/planilla/novedad_routes.py +227 -0
- coati_payroll/vistas/planilla/routes.py +145 -0
- coati_payroll/vistas/planilla/services/__init__.py +26 -0
- coati_payroll/vistas/planilla/services/export_service.py +687 -0
- coati_payroll/vistas/planilla/services/nomina_service.py +233 -0
- coati_payroll/vistas/planilla/services/novedad_service.py +126 -0
- coati_payroll/vistas/planilla/services/planilla_service.py +34 -0
- coati_payroll/vistas/planilla/validators/__init__.py +18 -0
- coati_payroll/vistas/planilla/validators/planilla_validators.py +40 -0
- coati_payroll/vistas/plugins.py +45 -0
- coati_payroll/vistas/prestacion.py +272 -0
- coati_payroll/vistas/prestamo.py +808 -0
- coati_payroll/vistas/report.py +432 -0
- coati_payroll/vistas/settings.py +29 -0
- coati_payroll/vistas/tipo_planilla.py +134 -0
- coati_payroll/vistas/user.py +172 -0
- coati_payroll/vistas/vacation.py +1045 -0
- coati_payroll-0.0.2.dist-info/LICENSE +201 -0
- coati_payroll-0.0.2.dist-info/METADATA +581 -0
- coati_payroll-0.0.2.dist-info/RECORD +243 -0
- coati_payroll-0.0.2.dist-info/WHEEL +5 -0
- coati_payroll-0.0.2.dist-info/entry_points.txt +2 -0
- coati_payroll-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Copyright 2025 BMO Soluciones, S.A.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Formula engine package.
|
|
15
|
+
|
|
16
|
+
This package contains the refactored formula engine with modular architecture:
|
|
17
|
+
- AST parsing and evaluation using Visitor pattern
|
|
18
|
+
- Step execution using Strategy pattern
|
|
19
|
+
- Validation, tables, execution, and results modules
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
# Import from formula_engine_examples
|
|
25
|
+
# EXAMPLE_PROGRESSIVE_TAX_SCHEMA is the new generic name
|
|
26
|
+
# EXAMPLE_IR_NICARAGUA_SCHEMA is kept for backward compatibility (deprecated)
|
|
27
|
+
from coati_payroll.formula_engine_examples import (
|
|
28
|
+
EXAMPLE_PROGRESSIVE_TAX_SCHEMA,
|
|
29
|
+
EXAMPLE_IR_NICARAGUA_SCHEMA,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Import main engine and functions
|
|
33
|
+
from .engine import FormulaEngine, calculate_with_rule, get_available_sources_for_ui
|
|
34
|
+
|
|
35
|
+
# Import exceptions
|
|
36
|
+
from .exceptions import CalculationError, FormulaEngineError, TaxEngineError, ValidationError
|
|
37
|
+
|
|
38
|
+
# Import utilities for backward compatibility
|
|
39
|
+
from .ast.type_converter import safe_divide, to_decimal
|
|
40
|
+
|
|
41
|
+
# Import submodules
|
|
42
|
+
from .ast import (
|
|
43
|
+
ALLOWED_AST_TYPES,
|
|
44
|
+
ASTVisitor,
|
|
45
|
+
COMPARISON_OPERATORS,
|
|
46
|
+
ExpressionEvaluator,
|
|
47
|
+
SAFE_FUNCTIONS,
|
|
48
|
+
SAFE_OPERATORS,
|
|
49
|
+
SafeASTVisitor,
|
|
50
|
+
)
|
|
51
|
+
from .data_sources import AVAILABLE_DATA_SOURCES
|
|
52
|
+
from .novelty_codes import NOVELTY_CODES
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# Main engine
|
|
56
|
+
"FormulaEngine",
|
|
57
|
+
"calculate_with_rule",
|
|
58
|
+
"get_available_sources_for_ui",
|
|
59
|
+
# Exceptions
|
|
60
|
+
"FormulaEngineError",
|
|
61
|
+
"TaxEngineError",
|
|
62
|
+
"ValidationError",
|
|
63
|
+
"CalculationError",
|
|
64
|
+
# Examples
|
|
65
|
+
"EXAMPLE_PROGRESSIVE_TAX_SCHEMA",
|
|
66
|
+
"EXAMPLE_IR_NICARAGUA_SCHEMA", # Deprecated alias for backward compatibility
|
|
67
|
+
# Utilities
|
|
68
|
+
"to_decimal",
|
|
69
|
+
"safe_divide",
|
|
70
|
+
# AST modules
|
|
71
|
+
"ASTVisitor",
|
|
72
|
+
"SafeASTVisitor",
|
|
73
|
+
"ExpressionEvaluator",
|
|
74
|
+
"SAFE_OPERATORS",
|
|
75
|
+
"COMPARISON_OPERATORS",
|
|
76
|
+
"SAFE_FUNCTIONS",
|
|
77
|
+
"ALLOWED_AST_TYPES",
|
|
78
|
+
# Data sources
|
|
79
|
+
"AVAILABLE_DATA_SOURCES",
|
|
80
|
+
"NOVELTY_CODES",
|
|
81
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Copyright 2025 BMO Soluciones, S.A.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""AST parsing and evaluation modules.
|
|
15
|
+
|
|
16
|
+
This package provides secure, enterprise-grade evaluation of mathematical
|
|
17
|
+
expressions for payroll formulas. It implements a whitelist-based security
|
|
18
|
+
model that prevents arbitrary code execution while maintaining financial precision.
|
|
19
|
+
|
|
20
|
+
Security Architecture:
|
|
21
|
+
|
|
22
|
+
1. **Whitelist-Based Validation**: Only explicitly approved AST node types
|
|
23
|
+
and functions are allowed. Any attempt to use unapproved operations is
|
|
24
|
+
rejected with a clear security violation message.
|
|
25
|
+
|
|
26
|
+
2. **No Dynamic Code Execution**: The system does NOT use eval(), exec(),
|
|
27
|
+
compile(), or any dynamic method resolution (getattr). All code paths
|
|
28
|
+
are explicit and auditable.
|
|
29
|
+
|
|
30
|
+
3. **DoS Prevention**: Expression length and AST depth are bounded to prevent
|
|
31
|
+
denial-of-service attacks via extremely long or deeply nested expressions.
|
|
32
|
+
|
|
33
|
+
4. **Financial Precision**: All calculations use Python's Decimal type to
|
|
34
|
+
maintain precision required for payroll calculations.
|
|
35
|
+
|
|
36
|
+
5. **Immutable Context**: Variable contexts are read-only during evaluation,
|
|
37
|
+
preventing side effects and ensuring deterministic results.
|
|
38
|
+
|
|
39
|
+
Allowed Operations:
|
|
40
|
+
- Arithmetic: +, -, *, /, //, %, **
|
|
41
|
+
- Functions: min, max, abs, round
|
|
42
|
+
- Variables: Pre-defined in execution context
|
|
43
|
+
- Constants: Numeric literals only
|
|
44
|
+
|
|
45
|
+
Prohibited Operations:
|
|
46
|
+
- File I/O, network access, system calls
|
|
47
|
+
- Import statements, attribute access
|
|
48
|
+
- Lambda functions, list comprehensions
|
|
49
|
+
- Any Python builtin not explicitly whitelisted
|
|
50
|
+
|
|
51
|
+
Usage Example:
|
|
52
|
+
```python
|
|
53
|
+
from coati_payroll.formula_engine.ast import ExpressionEvaluator
|
|
54
|
+
from decimal import Decimal
|
|
55
|
+
|
|
56
|
+
variables = {
|
|
57
|
+
'salario_base': Decimal('5000'),
|
|
58
|
+
'bono': Decimal('1000')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
evaluator = ExpressionEvaluator(variables)
|
|
62
|
+
result = evaluator.evaluate('salario_base * 1.15 + bono')
|
|
63
|
+
# result = Decimal('6750')
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Security Notes:
|
|
67
|
+
- This module is designed for use with untrusted input (JSON rules)
|
|
68
|
+
- All security validations are fail-safe (reject by default)
|
|
69
|
+
- Adding new functions or operators requires security review
|
|
70
|
+
- Regular security audits are recommended
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
from .ast_visitor import ASTVisitor, SafeASTVisitor
|
|
74
|
+
from .expression_evaluator import ExpressionEvaluator
|
|
75
|
+
from .safe_operators import (
|
|
76
|
+
SAFE_OPERATORS,
|
|
77
|
+
COMPARISON_OPERATORS,
|
|
78
|
+
SAFE_FUNCTIONS,
|
|
79
|
+
ALLOWED_AST_TYPES,
|
|
80
|
+
MAX_EXPRESSION_LENGTH,
|
|
81
|
+
MAX_AST_DEPTH,
|
|
82
|
+
MAX_FUNCTION_ARGS,
|
|
83
|
+
validate_safe_function_call,
|
|
84
|
+
)
|
|
85
|
+
from .type_converter import (
|
|
86
|
+
to_decimal,
|
|
87
|
+
safe_divide,
|
|
88
|
+
MAX_DECIMAL_DIGITS,
|
|
89
|
+
MAX_DECIMAL_VALUE,
|
|
90
|
+
MIN_DECIMAL_VALUE,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"ASTVisitor",
|
|
95
|
+
"SafeASTVisitor",
|
|
96
|
+
"ExpressionEvaluator",
|
|
97
|
+
"SAFE_OPERATORS",
|
|
98
|
+
"COMPARISON_OPERATORS",
|
|
99
|
+
"SAFE_FUNCTIONS",
|
|
100
|
+
"ALLOWED_AST_TYPES",
|
|
101
|
+
"MAX_EXPRESSION_LENGTH",
|
|
102
|
+
"MAX_AST_DEPTH",
|
|
103
|
+
"MAX_FUNCTION_ARGS",
|
|
104
|
+
"validate_safe_function_call",
|
|
105
|
+
"to_decimal",
|
|
106
|
+
"safe_divide",
|
|
107
|
+
"MAX_DECIMAL_DIGITS",
|
|
108
|
+
"MAX_DECIMAL_VALUE",
|
|
109
|
+
"MIN_DECIMAL_VALUE",
|
|
110
|
+
]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Copyright 2025 BMO Soluciones, S.A.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""AST Visitor pattern for safe expression evaluation.
|
|
15
|
+
|
|
16
|
+
This module implements a secure AST visitor that evaluates mathematical expressions
|
|
17
|
+
without using dynamic method dispatch (getattr). All node types are explicitly
|
|
18
|
+
handled to prevent any possibility of code injection or unexpected behavior.
|
|
19
|
+
|
|
20
|
+
Security Features:
|
|
21
|
+
- Explicit visitor methods for each allowed node type
|
|
22
|
+
- No dynamic method resolution (no getattr/setattr)
|
|
23
|
+
- Whitelist-based approach - unknown nodes are rejected
|
|
24
|
+
- All operations maintain Decimal precision
|
|
25
|
+
- Division by zero is handled safely
|
|
26
|
+
|
|
27
|
+
The visitor pattern ensures that only pre-approved AST node types can be processed,
|
|
28
|
+
and each type has an explicit, auditable handler method.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from abc import ABC, abstractmethod
|
|
34
|
+
from decimal import Decimal
|
|
35
|
+
|
|
36
|
+
import ast
|
|
37
|
+
|
|
38
|
+
from ..exceptions import CalculationError
|
|
39
|
+
from .safe_operators import SAFE_FUNCTIONS, SAFE_OPERATORS
|
|
40
|
+
from .type_converter import to_decimal
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ASTVisitor(ABC):
|
|
44
|
+
"""Visitor pattern base class for AST traversal.
|
|
45
|
+
|
|
46
|
+
This abstract base class defines the interface for AST visitors.
|
|
47
|
+
Implementations must provide a visit() method that safely evaluates
|
|
48
|
+
AST nodes and returns Decimal results.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def visit(self, node: ast.AST) -> Decimal:
|
|
53
|
+
"""Visit and evaluate an AST node.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
node: The AST node to visit and evaluate
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The evaluated result as a Decimal
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
CalculationError: If the node cannot be safely evaluated
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SafeASTVisitor(ASTVisitor):
|
|
68
|
+
"""Safe visitor for evaluating mathematical expressions.
|
|
69
|
+
|
|
70
|
+
This visitor implements a secure evaluation strategy:
|
|
71
|
+
1. Explicit dispatch - no dynamic method resolution
|
|
72
|
+
2. Whitelist approach - only known node types are processed
|
|
73
|
+
3. Immutable context - variables are read-only during evaluation
|
|
74
|
+
4. Decimal precision - all numeric operations maintain precision
|
|
75
|
+
5. Safe error handling - division by zero returns 0
|
|
76
|
+
|
|
77
|
+
Security Note:
|
|
78
|
+
This class does NOT use getattr() or any dynamic dispatch mechanism.
|
|
79
|
+
All node types are handled by explicit if/elif chains to prevent
|
|
80
|
+
any possibility of method injection or unexpected behavior.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, variables: dict[str, Decimal]):
|
|
84
|
+
"""Initialize visitor with variable context.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
variables: Dictionary of variable names to Decimal values.
|
|
88
|
+
This dictionary is not modified during evaluation.
|
|
89
|
+
"""
|
|
90
|
+
self.variables = variables
|
|
91
|
+
|
|
92
|
+
def visit(self, node: ast.AST) -> Decimal:
|
|
93
|
+
"""Visit and evaluate an AST node using explicit dispatch.
|
|
94
|
+
|
|
95
|
+
This method uses explicit type checking with match/case instead of
|
|
96
|
+
dynamic method resolution (getattr) for security and maintainability.
|
|
97
|
+
Each node type is explicitly handled, making the code easier to audit
|
|
98
|
+
and preventing any possibility of method injection.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
node: AST node to evaluate
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Decimal result of evaluation
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
CalculationError: If the node type is not supported or evaluation fails
|
|
108
|
+
"""
|
|
109
|
+
match node:
|
|
110
|
+
case ast.Constant():
|
|
111
|
+
return self.visit_constant(node)
|
|
112
|
+
case ast.Name():
|
|
113
|
+
return self.visit_name(node)
|
|
114
|
+
case ast.BinOp():
|
|
115
|
+
return self.visit_binop(node)
|
|
116
|
+
case ast.UnaryOp():
|
|
117
|
+
return self.visit_unaryop(node)
|
|
118
|
+
case ast.Call():
|
|
119
|
+
return self.visit_call(node)
|
|
120
|
+
case _:
|
|
121
|
+
raise CalculationError(
|
|
122
|
+
f"Unsupported AST node type: {type(node).__name__}. "
|
|
123
|
+
"Only Constant, Name, BinOp, UnaryOp, and Call nodes are allowed."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def visit_constant(self, node: ast.Constant) -> Decimal:
|
|
127
|
+
"""Visit a constant node (numeric literal).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
node: AST Constant node containing a numeric value
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The constant value as a Decimal
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
CalculationError: If the constant cannot be converted to Decimal
|
|
137
|
+
"""
|
|
138
|
+
return to_decimal(node.value)
|
|
139
|
+
|
|
140
|
+
def visit_name(self, node: ast.Name) -> Decimal:
|
|
141
|
+
"""Visit a variable name node.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
node: AST Name node representing a variable reference
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The value of the variable from the context
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
CalculationError: If the variable is not defined in the context
|
|
151
|
+
"""
|
|
152
|
+
if node.id not in self.variables:
|
|
153
|
+
raise CalculationError(
|
|
154
|
+
f"Undefined variable: '{node.id}'. " f"Available variables: {', '.join(sorted(self.variables.keys()))}"
|
|
155
|
+
)
|
|
156
|
+
return self.variables[node.id]
|
|
157
|
+
|
|
158
|
+
def visit_binop(self, node: ast.BinOp) -> Decimal:
|
|
159
|
+
"""Visit a binary operation node (e.g., a + b, x * y).
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
node: AST BinOp node representing a binary operation
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The result of the binary operation as a Decimal
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
CalculationError: If the operator is not whitelisted or operation fails
|
|
169
|
+
"""
|
|
170
|
+
left = self.visit(node.left)
|
|
171
|
+
right = self.visit(node.right)
|
|
172
|
+
|
|
173
|
+
op_type = type(node.op)
|
|
174
|
+
op_func = SAFE_OPERATORS.get(op_type)
|
|
175
|
+
if not op_func:
|
|
176
|
+
raise CalculationError(
|
|
177
|
+
f"Operator '{op_type.__name__}' is not allowed. " f"Allowed operators: +, -, *, /, //, %, **"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if op_type in (ast.Div, ast.FloorDiv, ast.Mod) and right == 0:
|
|
181
|
+
return Decimal("0")
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
result = op_func(left, right)
|
|
185
|
+
return to_decimal(result)
|
|
186
|
+
except (OverflowError, ValueError) as e:
|
|
187
|
+
raise CalculationError(f"Arithmetic error in operation '{left} {op_type.__name__} {right}': {e}") from e
|
|
188
|
+
except Exception as e:
|
|
189
|
+
raise CalculationError(f"Unexpected error in binary operation: {e}") from e
|
|
190
|
+
|
|
191
|
+
def visit_unaryop(self, node: ast.UnaryOp) -> Decimal:
|
|
192
|
+
"""Visit a unary operation node (e.g., -x, +y).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
node: AST UnaryOp node representing a unary operation
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The result of the unary operation as a Decimal
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
CalculationError: If the unary operator is not allowed
|
|
202
|
+
"""
|
|
203
|
+
operand = self.visit(node.operand)
|
|
204
|
+
|
|
205
|
+
op_type = type(node.op)
|
|
206
|
+
if op_type == ast.UAdd:
|
|
207
|
+
return to_decimal(+operand)
|
|
208
|
+
elif op_type == ast.USub:
|
|
209
|
+
return to_decimal(-operand)
|
|
210
|
+
else:
|
|
211
|
+
raise CalculationError(
|
|
212
|
+
f"Unary operator '{op_type.__name__}' is not allowed. " "Only unary + and - are permitted."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def visit_call(self, node: ast.Call) -> Decimal:
|
|
216
|
+
"""Visit a function call node (e.g., max(a, b), round(x, 2)).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
node: AST Call node representing a function call
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The result of the function call as a Decimal
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
CalculationError: If the function is not whitelisted or call fails
|
|
226
|
+
"""
|
|
227
|
+
if not isinstance(node.func, ast.Name):
|
|
228
|
+
raise CalculationError(
|
|
229
|
+
"Only direct named function calls are allowed. " "Attribute access and lambda functions are prohibited."
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
func_name = node.func.id
|
|
233
|
+
if func_name not in SAFE_FUNCTIONS:
|
|
234
|
+
raise CalculationError(
|
|
235
|
+
f"Function '{func_name}' is not in the whitelist. "
|
|
236
|
+
f"Allowed functions: {', '.join(sorted(SAFE_FUNCTIONS.keys()))}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if node.keywords:
|
|
240
|
+
raise CalculationError(
|
|
241
|
+
f"Keyword arguments are not allowed in function '{func_name}'. " "Use positional arguments only."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
args = [self.visit(arg) for arg in node.args]
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if func_name == "round" and len(args) > 1:
|
|
248
|
+
if args[1] < 0 or args[1] > 10:
|
|
249
|
+
raise CalculationError(f"round() precision must be between 0 and 10, got {args[1]}")
|
|
250
|
+
result = SAFE_FUNCTIONS[func_name](args[0], int(args[1]))
|
|
251
|
+
else:
|
|
252
|
+
result = SAFE_FUNCTIONS[func_name](*args)
|
|
253
|
+
return to_decimal(result)
|
|
254
|
+
except TypeError as e:
|
|
255
|
+
raise CalculationError(f"Invalid arguments for function '{func_name}': {e}") from e
|
|
256
|
+
except ValueError as e:
|
|
257
|
+
raise CalculationError(f"Invalid value in function '{func_name}': {e}") from e
|
|
258
|
+
except Exception as e:
|
|
259
|
+
raise CalculationError(f"Error calling function '{func_name}': {e}") from e
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Copyright 2025 BMO Soluciones, S.A.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Expression evaluator using AST visitor pattern.
|
|
15
|
+
|
|
16
|
+
This module provides secure evaluation of mathematical expressions from JSON rules.
|
|
17
|
+
It implements multiple layers of security:
|
|
18
|
+
|
|
19
|
+
1. Expression Length Validation: Prevents DoS attacks via extremely long expressions
|
|
20
|
+
2. AST Depth Validation: Prevents stack overflow from deeply nested expressions
|
|
21
|
+
3. Whitelist-based AST Validation: Only approved node types are allowed
|
|
22
|
+
4. Safe Visitor Pattern: No dynamic code execution or attribute access
|
|
23
|
+
5. Decimal Precision: All calculations maintain financial precision
|
|
24
|
+
|
|
25
|
+
Security Model:
|
|
26
|
+
- Input expressions are parsed into Abstract Syntax Trees (AST)
|
|
27
|
+
- AST is validated against a whitelist of allowed node types
|
|
28
|
+
- AST depth is checked to prevent stack overflow
|
|
29
|
+
- Evaluation uses explicit visitor pattern (no eval/exec/compile)
|
|
30
|
+
- All operations are deterministic and side-effect free
|
|
31
|
+
|
|
32
|
+
Example Safe Expression:
|
|
33
|
+
'salario_base * 1.15 + max(bono, 1000)'
|
|
34
|
+
|
|
35
|
+
Example Unsafe Expression (rejected):
|
|
36
|
+
'__import__("os").system("rm -rf /")'
|
|
37
|
+
'open("/etc/passwd").read()'
|
|
38
|
+
'[x for x in range(1000000)]'
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import ast
|
|
44
|
+
from decimal import Decimal
|
|
45
|
+
from typing import Callable
|
|
46
|
+
|
|
47
|
+
from coati_payroll.i18n import _
|
|
48
|
+
from coati_payroll.log import TRACE_LEVEL_NUM, is_trace_enabled, log
|
|
49
|
+
|
|
50
|
+
from ..exceptions import CalculationError
|
|
51
|
+
from .ast_visitor import SafeASTVisitor
|
|
52
|
+
from .safe_operators import ALLOWED_AST_TYPES, MAX_EXPRESSION_LENGTH, MAX_AST_DEPTH
|
|
53
|
+
from .type_converter import to_decimal
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ExpressionEvaluator:
|
|
57
|
+
"""Evaluates mathematical expressions safely using AST.
|
|
58
|
+
|
|
59
|
+
This class provides enterprise-grade secure expression evaluation for
|
|
60
|
+
payroll formulas. It prevents code injection, DoS attacks, and ensures
|
|
61
|
+
all calculations are deterministic and auditable.
|
|
62
|
+
|
|
63
|
+
Security Features:
|
|
64
|
+
- Expression length limits (prevents DoS)
|
|
65
|
+
- AST depth limits (prevents stack overflow)
|
|
66
|
+
- Whitelist-based validation (prevents code injection)
|
|
67
|
+
- No dynamic code execution (no eval/exec/compile)
|
|
68
|
+
- Immutable variable context (prevents side effects)
|
|
69
|
+
- Comprehensive error messages for debugging
|
|
70
|
+
|
|
71
|
+
Thread Safety:
|
|
72
|
+
This class is thread-safe as long as the variables dictionary is not
|
|
73
|
+
modified during evaluation. Each evaluation creates a new visitor instance.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, variables: dict[str, Decimal], trace_callback: Callable[[str], None] | None = None):
|
|
77
|
+
"""Initialize expression evaluator.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
variables: Dictionary of variable names to Decimal values.
|
|
81
|
+
This dictionary is not modified during evaluation.
|
|
82
|
+
trace_callback: Optional callback for trace logging.
|
|
83
|
+
Should be thread-safe if used in concurrent contexts.
|
|
84
|
+
"""
|
|
85
|
+
self.variables = variables
|
|
86
|
+
self.trace_callback = trace_callback or self._default_trace
|
|
87
|
+
|
|
88
|
+
def _default_trace(self, message: str) -> None:
|
|
89
|
+
"""Default trace callback."""
|
|
90
|
+
if is_trace_enabled():
|
|
91
|
+
try:
|
|
92
|
+
log.log(TRACE_LEVEL_NUM, message)
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def evaluate(self, expression: str) -> Decimal:
|
|
97
|
+
"""Safely evaluate a mathematical expression using AST.
|
|
98
|
+
|
|
99
|
+
This method implements multiple layers of security validation:
|
|
100
|
+
1. Input validation (type, length)
|
|
101
|
+
2. Syntax validation (AST parsing)
|
|
102
|
+
3. Security validation (whitelist checking)
|
|
103
|
+
4. Depth validation (stack overflow prevention)
|
|
104
|
+
5. Safe evaluation (visitor pattern)
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
expression: Mathematical expression string (e.g., 'a + b * 2')
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Result of the expression as Decimal
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
CalculationError: If expression is invalid, unsafe, or evaluation fails
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
>>> evaluator = ExpressionEvaluator({'x': Decimal('10'), 'y': Decimal('5')})
|
|
117
|
+
>>> evaluator.evaluate('x + y')
|
|
118
|
+
Decimal('15')
|
|
119
|
+
>>> evaluator.evaluate('max(x, y) * 2')
|
|
120
|
+
Decimal('20')
|
|
121
|
+
"""
|
|
122
|
+
if not expression or not isinstance(expression, str):
|
|
123
|
+
return Decimal("0")
|
|
124
|
+
|
|
125
|
+
expression = expression.strip()
|
|
126
|
+
if not expression:
|
|
127
|
+
return Decimal("0")
|
|
128
|
+
|
|
129
|
+
if len(expression) > MAX_EXPRESSION_LENGTH:
|
|
130
|
+
raise CalculationError(
|
|
131
|
+
f"Expression too long ({len(expression)} characters). "
|
|
132
|
+
f"Maximum allowed: {MAX_EXPRESSION_LENGTH} characters. "
|
|
133
|
+
"This limit prevents denial-of-service attacks."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.trace_callback(_("Evaluando expresión: '%(expr)s'") % {"expr": expression})
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
tree = ast.parse(expression, mode="eval")
|
|
140
|
+
|
|
141
|
+
self._validate_ast_security(tree.body)
|
|
142
|
+
self._validate_ast_depth(tree.body)
|
|
143
|
+
|
|
144
|
+
visitor = SafeASTVisitor(self.variables)
|
|
145
|
+
result = visitor.visit(tree.body)
|
|
146
|
+
final_result = to_decimal(result)
|
|
147
|
+
|
|
148
|
+
self.trace_callback(
|
|
149
|
+
_("Resultado expresión '%(expr)s' => %(res)s") % {"expr": expression, "res": final_result}
|
|
150
|
+
)
|
|
151
|
+
return final_result
|
|
152
|
+
except SyntaxError as e:
|
|
153
|
+
raise CalculationError(
|
|
154
|
+
f"Invalid expression syntax in '{expression}': {e}. "
|
|
155
|
+
"Check for unmatched parentheses, invalid operators, or typos."
|
|
156
|
+
) from e
|
|
157
|
+
except ZeroDivisionError:
|
|
158
|
+
return Decimal("0")
|
|
159
|
+
except CalculationError:
|
|
160
|
+
raise
|
|
161
|
+
except Exception as e:
|
|
162
|
+
raise CalculationError(f"Unexpected error evaluating expression '{expression}': {e}") from e
|
|
163
|
+
|
|
164
|
+
def _validate_ast_security(self, node: ast.AST) -> None:
|
|
165
|
+
"""Validate that an AST node only contains safe operations.
|
|
166
|
+
|
|
167
|
+
This method implements a whitelist-based security model. It walks the
|
|
168
|
+
entire AST and verifies that every node is in the allowed list. This
|
|
169
|
+
prevents code injection attacks like:
|
|
170
|
+
- Import statements: __import__('os').system('rm -rf /')
|
|
171
|
+
- Attribute access: obj.__class__.__bases__[0].__subclasses__()
|
|
172
|
+
- List comprehensions: [x for x in range(999999999)]
|
|
173
|
+
- Lambda functions: (lambda: exec('malicious code'))()
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
node: AST node to validate (typically the root of the expression tree)
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
CalculationError: If any unsafe operation is detected
|
|
180
|
+
"""
|
|
181
|
+
for child in ast.walk(node):
|
|
182
|
+
if not isinstance(child, ALLOWED_AST_TYPES):
|
|
183
|
+
raise CalculationError(
|
|
184
|
+
f"Security violation: AST node type '{child.__class__.__name__}' is not allowed. "
|
|
185
|
+
"Only basic arithmetic operations and whitelisted functions are permitted. "
|
|
186
|
+
"This restriction prevents code injection and arbitrary code execution."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if isinstance(child, ast.Call):
|
|
190
|
+
if not isinstance(child.func, ast.Name):
|
|
191
|
+
raise CalculationError(
|
|
192
|
+
"Security violation: Only direct named function calls are allowed. "
|
|
193
|
+
"Attribute access (e.g., obj.method()) and lambda functions are prohibited."
|
|
194
|
+
)
|
|
195
|
+
from .safe_operators import SAFE_FUNCTIONS
|
|
196
|
+
|
|
197
|
+
if child.func.id not in SAFE_FUNCTIONS:
|
|
198
|
+
raise CalculationError(
|
|
199
|
+
f"Security violation: Function '{child.func.id}' is not in the whitelist. "
|
|
200
|
+
f"Allowed functions: {', '.join(sorted(SAFE_FUNCTIONS.keys()))}. "
|
|
201
|
+
"This restriction prevents execution of arbitrary Python functions."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _validate_ast_depth(self, node: ast.AST, current_depth: int = 0) -> None:
|
|
205
|
+
"""Validate that AST depth does not exceed maximum to prevent stack overflow.
|
|
206
|
+
|
|
207
|
+
Deeply nested expressions can cause stack overflow during evaluation:
|
|
208
|
+
- Example: ((((((((((x + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1) + 1)
|
|
209
|
+
|
|
210
|
+
This validation prevents denial-of-service attacks via deeply nested
|
|
211
|
+
expressions that could exhaust the call stack.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
node: AST node to validate
|
|
215
|
+
current_depth: Current depth in the tree (used for recursion)
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
CalculationError: If AST depth exceeds maximum allowed
|
|
219
|
+
"""
|
|
220
|
+
if current_depth > MAX_AST_DEPTH:
|
|
221
|
+
raise CalculationError(
|
|
222
|
+
f"Expression too complex: AST depth ({current_depth}) exceeds maximum ({MAX_AST_DEPTH}). "
|
|
223
|
+
"Simplify the expression by breaking it into multiple steps. "
|
|
224
|
+
"This limit prevents stack overflow attacks."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
for child in ast.iter_child_nodes(node):
|
|
228
|
+
self._validate_ast_depth(child, current_depth + 1)
|