truthound 1.0.8__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.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
truthound/rbac/policy.py
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
"""Policy engine and evaluators for RBAC.
|
|
2
|
+
|
|
3
|
+
This module provides the policy evaluation engine and various policy
|
|
4
|
+
evaluators for making access control decisions.
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
The policy engine follows a pipeline design:
|
|
8
|
+
|
|
9
|
+
AccessContext
|
|
10
|
+
│
|
|
11
|
+
├── PolicyEngine
|
|
12
|
+
│ │
|
|
13
|
+
│ ├── PolicyEvaluator 1 (e.g., RoleBasedEvaluator)
|
|
14
|
+
│ │
|
|
15
|
+
│ ├── PolicyEvaluator 2 (e.g., ABACEvaluator)
|
|
16
|
+
│ │
|
|
17
|
+
│ └── PolicyEvaluator N
|
|
18
|
+
│
|
|
19
|
+
v
|
|
20
|
+
AccessDecision
|
|
21
|
+
|
|
22
|
+
Policy Combination:
|
|
23
|
+
- DENY_OVERRIDES: Any deny results in deny (default, most secure)
|
|
24
|
+
- ALLOW_OVERRIDES: Any allow results in allow
|
|
25
|
+
- FIRST_APPLICABLE: First matching policy decides
|
|
26
|
+
- UNANIMOUS: All must agree
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import threading
|
|
32
|
+
import time
|
|
33
|
+
from abc import ABC
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from typing import Any, Callable, Sequence
|
|
38
|
+
|
|
39
|
+
from truthound.rbac.core import (
|
|
40
|
+
AccessContext,
|
|
41
|
+
AccessDecision,
|
|
42
|
+
Condition,
|
|
43
|
+
Permission,
|
|
44
|
+
PermissionEffect,
|
|
45
|
+
PolicyEvaluator,
|
|
46
|
+
Principal,
|
|
47
|
+
Role,
|
|
48
|
+
RoleStore,
|
|
49
|
+
PrincipalStore,
|
|
50
|
+
PermissionDeniedError,
|
|
51
|
+
PolicyEvaluationError,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Policy Combination Algorithms
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PolicyCombination(Enum):
|
|
61
|
+
"""Policy combination algorithms."""
|
|
62
|
+
|
|
63
|
+
DENY_OVERRIDES = "deny_overrides" # Any deny wins
|
|
64
|
+
ALLOW_OVERRIDES = "allow_overrides" # Any allow wins
|
|
65
|
+
FIRST_APPLICABLE = "first_applicable" # First match wins
|
|
66
|
+
UNANIMOUS = "unanimous" # All must allow
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# Policy Types
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Policy:
|
|
76
|
+
"""A policy that defines access rules.
|
|
77
|
+
|
|
78
|
+
Policies bind permissions to subjects (principals, roles) with
|
|
79
|
+
optional conditions.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> policy = Policy(
|
|
83
|
+
... id="data_analyst_read",
|
|
84
|
+
... name="Data Analysts Read Access",
|
|
85
|
+
... effect=PermissionEffect.ALLOW,
|
|
86
|
+
... subjects=["role:data_analyst"],
|
|
87
|
+
... resources=["dataset:*"],
|
|
88
|
+
... actions=["read", "list"],
|
|
89
|
+
... )
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
id: str
|
|
93
|
+
name: str
|
|
94
|
+
description: str = ""
|
|
95
|
+
|
|
96
|
+
# Effect when policy matches
|
|
97
|
+
effect: PermissionEffect = PermissionEffect.ALLOW
|
|
98
|
+
|
|
99
|
+
# Who this policy applies to
|
|
100
|
+
subjects: list[str] = field(default_factory=list) # e.g., ["role:admin", "user:123"]
|
|
101
|
+
|
|
102
|
+
# What resources this policy covers
|
|
103
|
+
resources: list[str] = field(default_factory=list) # e.g., ["dataset:*", "validation:*"]
|
|
104
|
+
|
|
105
|
+
# What actions are allowed/denied
|
|
106
|
+
actions: list[str] = field(default_factory=list) # e.g., ["read", "write", "*"]
|
|
107
|
+
|
|
108
|
+
# Conditions for conditional policies (ABAC)
|
|
109
|
+
conditions: list[Condition] = field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
# Priority (higher = evaluated first)
|
|
112
|
+
priority: int = 0
|
|
113
|
+
|
|
114
|
+
# Status
|
|
115
|
+
enabled: bool = True
|
|
116
|
+
|
|
117
|
+
# Tenant scope
|
|
118
|
+
tenant_id: str | None = None
|
|
119
|
+
|
|
120
|
+
# Metadata
|
|
121
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
122
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
123
|
+
|
|
124
|
+
def matches_subject(self, principal: Principal) -> bool:
|
|
125
|
+
"""Check if the policy matches the principal."""
|
|
126
|
+
if not self.subjects:
|
|
127
|
+
return True # Empty subjects means applies to all
|
|
128
|
+
|
|
129
|
+
for subject in self.subjects:
|
|
130
|
+
if subject == "*":
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
if ":" in subject:
|
|
134
|
+
subject_type, subject_id = subject.split(":", 1)
|
|
135
|
+
|
|
136
|
+
if subject_type == "role":
|
|
137
|
+
if subject_id == "*" or subject_id in principal.roles:
|
|
138
|
+
return True
|
|
139
|
+
elif subject_type == "user":
|
|
140
|
+
if subject_id == "*" or subject_id == principal.id:
|
|
141
|
+
return True
|
|
142
|
+
elif subject_type == "type":
|
|
143
|
+
if subject_id == principal.type.value:
|
|
144
|
+
return True
|
|
145
|
+
else:
|
|
146
|
+
# Plain role name
|
|
147
|
+
if subject in principal.roles:
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def matches_resource(self, resource: str) -> bool:
|
|
153
|
+
"""Check if the policy matches the resource."""
|
|
154
|
+
if not self.resources:
|
|
155
|
+
return True # Empty resources means applies to all
|
|
156
|
+
|
|
157
|
+
for pattern in self.resources:
|
|
158
|
+
if pattern == "*":
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
if self._matches_pattern(pattern, resource):
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
def matches_action(self, action: str) -> bool:
|
|
167
|
+
"""Check if the policy matches the action."""
|
|
168
|
+
if not self.actions:
|
|
169
|
+
return True # Empty actions means applies to all
|
|
170
|
+
|
|
171
|
+
for allowed_action in self.actions:
|
|
172
|
+
if allowed_action == "*" or allowed_action == action:
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def _matches_pattern(self, pattern: str, value: str) -> bool:
|
|
178
|
+
"""Check if a pattern matches a value (supports wildcards)."""
|
|
179
|
+
if pattern == "*":
|
|
180
|
+
return True
|
|
181
|
+
if "*" not in pattern:
|
|
182
|
+
return pattern == value
|
|
183
|
+
|
|
184
|
+
# Simple wildcard matching
|
|
185
|
+
if pattern.endswith("*"):
|
|
186
|
+
return value.startswith(pattern[:-1])
|
|
187
|
+
if pattern.startswith("*"):
|
|
188
|
+
return value.endswith(pattern[1:])
|
|
189
|
+
|
|
190
|
+
# Contains wildcard
|
|
191
|
+
parts = pattern.split("*")
|
|
192
|
+
if len(parts) == 2:
|
|
193
|
+
return value.startswith(parts[0]) and value.endswith(parts[1])
|
|
194
|
+
|
|
195
|
+
return pattern == value
|
|
196
|
+
|
|
197
|
+
def evaluate_conditions(self, context: AccessContext) -> bool:
|
|
198
|
+
"""Evaluate all conditions for this policy."""
|
|
199
|
+
if not self.conditions:
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
for condition in self.conditions:
|
|
203
|
+
if not condition.evaluate(context):
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> dict[str, Any]:
|
|
209
|
+
"""Convert to dictionary."""
|
|
210
|
+
return {
|
|
211
|
+
"id": self.id,
|
|
212
|
+
"name": self.name,
|
|
213
|
+
"description": self.description,
|
|
214
|
+
"effect": self.effect.value,
|
|
215
|
+
"subjects": self.subjects,
|
|
216
|
+
"resources": self.resources,
|
|
217
|
+
"actions": self.actions,
|
|
218
|
+
"conditions": [c.to_dict() for c in self.conditions],
|
|
219
|
+
"priority": self.priority,
|
|
220
|
+
"enabled": self.enabled,
|
|
221
|
+
"tenant_id": self.tenant_id,
|
|
222
|
+
"created_at": self.created_at.isoformat(),
|
|
223
|
+
"metadata": self.metadata,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# =============================================================================
|
|
228
|
+
# Policy Evaluators
|
|
229
|
+
# =============================================================================
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class RoleBasedEvaluator(PolicyEvaluator):
|
|
233
|
+
"""Role-based access control evaluator.
|
|
234
|
+
|
|
235
|
+
Checks permissions based on roles assigned to the principal.
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
>>> evaluator = RoleBasedEvaluator(role_store)
|
|
239
|
+
>>> decision = evaluator.evaluate(context)
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def __init__(
|
|
243
|
+
self,
|
|
244
|
+
role_store: RoleStore,
|
|
245
|
+
deny_by_default: bool = True,
|
|
246
|
+
) -> None:
|
|
247
|
+
self._role_store = role_store
|
|
248
|
+
self._deny_by_default = deny_by_default
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def name(self) -> str:
|
|
252
|
+
return "role_based"
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def priority(self) -> int:
|
|
256
|
+
return 100
|
|
257
|
+
|
|
258
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
259
|
+
"""Evaluate access based on roles."""
|
|
260
|
+
start_time = time.time()
|
|
261
|
+
|
|
262
|
+
if context.principal is None:
|
|
263
|
+
return AccessDecision.deny("No principal in context")
|
|
264
|
+
|
|
265
|
+
principal = context.principal
|
|
266
|
+
required_permission = context.get_required_permission()
|
|
267
|
+
|
|
268
|
+
# Check direct permissions first
|
|
269
|
+
for perm in principal.direct_permissions:
|
|
270
|
+
if perm.matches(required_permission):
|
|
271
|
+
if perm.effect == PermissionEffect.DENY:
|
|
272
|
+
return AccessDecision.deny(
|
|
273
|
+
f"Permission explicitly denied: {perm}"
|
|
274
|
+
)
|
|
275
|
+
return AccessDecision.allow(
|
|
276
|
+
f"Direct permission granted: {perm}",
|
|
277
|
+
[perm],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Check role permissions
|
|
281
|
+
matching_permissions: list[Permission] = []
|
|
282
|
+
for role_id in principal.roles:
|
|
283
|
+
permissions = self._role_store.get_all_permissions(role_id)
|
|
284
|
+
for perm in permissions:
|
|
285
|
+
if perm.matches(required_permission):
|
|
286
|
+
if perm.effect == PermissionEffect.DENY:
|
|
287
|
+
return AccessDecision.deny(
|
|
288
|
+
f"Permission denied by role {role_id}: {perm}"
|
|
289
|
+
)
|
|
290
|
+
matching_permissions.append(perm)
|
|
291
|
+
|
|
292
|
+
if matching_permissions:
|
|
293
|
+
elapsed = (time.time() - start_time) * 1000
|
|
294
|
+
decision = AccessDecision.allow(
|
|
295
|
+
f"Permission granted by roles",
|
|
296
|
+
matching_permissions,
|
|
297
|
+
)
|
|
298
|
+
decision.evaluation_time_ms = elapsed
|
|
299
|
+
return decision
|
|
300
|
+
|
|
301
|
+
if self._deny_by_default:
|
|
302
|
+
return AccessDecision.deny(
|
|
303
|
+
f"No matching permission for {required_permission}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Return neutral (let other evaluators decide)
|
|
307
|
+
return AccessDecision(
|
|
308
|
+
allowed=False,
|
|
309
|
+
reason="No matching role permission",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class PolicyBasedEvaluator(PolicyEvaluator):
|
|
314
|
+
"""Policy-based access control evaluator.
|
|
315
|
+
|
|
316
|
+
Evaluates access based on explicit policies.
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
>>> evaluator = PolicyBasedEvaluator()
|
|
320
|
+
>>> evaluator.add_policy(Policy(...))
|
|
321
|
+
>>> decision = evaluator.evaluate(context)
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(
|
|
325
|
+
self,
|
|
326
|
+
combination: PolicyCombination = PolicyCombination.DENY_OVERRIDES,
|
|
327
|
+
) -> None:
|
|
328
|
+
self._policies: list[Policy] = []
|
|
329
|
+
self._combination = combination
|
|
330
|
+
self._lock = threading.RLock()
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def name(self) -> str:
|
|
334
|
+
return "policy_based"
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def priority(self) -> int:
|
|
338
|
+
return 90
|
|
339
|
+
|
|
340
|
+
def add_policy(self, policy: Policy) -> None:
|
|
341
|
+
"""Add a policy."""
|
|
342
|
+
with self._lock:
|
|
343
|
+
self._policies.append(policy)
|
|
344
|
+
# Sort by priority (descending)
|
|
345
|
+
self._policies.sort(key=lambda p: p.priority, reverse=True)
|
|
346
|
+
|
|
347
|
+
def remove_policy(self, policy_id: str) -> bool:
|
|
348
|
+
"""Remove a policy by ID."""
|
|
349
|
+
with self._lock:
|
|
350
|
+
for i, policy in enumerate(self._policies):
|
|
351
|
+
if policy.id == policy_id:
|
|
352
|
+
del self._policies[i]
|
|
353
|
+
return True
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def get_policy(self, policy_id: str) -> Policy | None:
|
|
357
|
+
"""Get a policy by ID."""
|
|
358
|
+
with self._lock:
|
|
359
|
+
for policy in self._policies:
|
|
360
|
+
if policy.id == policy_id:
|
|
361
|
+
return policy
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
365
|
+
"""Evaluate access based on policies."""
|
|
366
|
+
start_time = time.time()
|
|
367
|
+
|
|
368
|
+
if context.principal is None:
|
|
369
|
+
return AccessDecision.deny("No principal in context")
|
|
370
|
+
|
|
371
|
+
resource = context.resource
|
|
372
|
+
action = context.action if isinstance(context.action, str) else context.action.value
|
|
373
|
+
|
|
374
|
+
matching_policies: list[tuple[Policy, PermissionEffect]] = []
|
|
375
|
+
|
|
376
|
+
with self._lock:
|
|
377
|
+
for policy in self._policies:
|
|
378
|
+
if not policy.enabled:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
# Check tenant scope
|
|
382
|
+
if policy.tenant_id and policy.tenant_id != context.tenant_id:
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
# Check if policy matches
|
|
386
|
+
if not policy.matches_subject(context.principal):
|
|
387
|
+
continue
|
|
388
|
+
if not policy.matches_resource(resource):
|
|
389
|
+
continue
|
|
390
|
+
if not policy.matches_action(action):
|
|
391
|
+
continue
|
|
392
|
+
if not policy.evaluate_conditions(context):
|
|
393
|
+
continue
|
|
394
|
+
|
|
395
|
+
matching_policies.append((policy, policy.effect))
|
|
396
|
+
|
|
397
|
+
if not matching_policies:
|
|
398
|
+
return AccessDecision(
|
|
399
|
+
allowed=False,
|
|
400
|
+
reason="No matching policy",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Apply combination algorithm
|
|
404
|
+
decision = self._combine_decisions(matching_policies)
|
|
405
|
+
decision.evaluation_time_ms = (time.time() - start_time) * 1000
|
|
406
|
+
return decision
|
|
407
|
+
|
|
408
|
+
def _combine_decisions(
|
|
409
|
+
self,
|
|
410
|
+
matches: list[tuple[Policy, PermissionEffect]],
|
|
411
|
+
) -> AccessDecision:
|
|
412
|
+
"""Combine policy decisions based on combination algorithm."""
|
|
413
|
+
if self._combination == PolicyCombination.DENY_OVERRIDES:
|
|
414
|
+
# Any deny wins
|
|
415
|
+
for policy, effect in matches:
|
|
416
|
+
if effect == PermissionEffect.DENY:
|
|
417
|
+
return AccessDecision.deny(
|
|
418
|
+
f"Denied by policy: {policy.name}"
|
|
419
|
+
)
|
|
420
|
+
# All allows
|
|
421
|
+
policy, _ = matches[0]
|
|
422
|
+
return AccessDecision.allow(
|
|
423
|
+
f"Allowed by policy: {policy.name}",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
elif self._combination == PolicyCombination.ALLOW_OVERRIDES:
|
|
427
|
+
# Any allow wins
|
|
428
|
+
for policy, effect in matches:
|
|
429
|
+
if effect == PermissionEffect.ALLOW:
|
|
430
|
+
return AccessDecision.allow(
|
|
431
|
+
f"Allowed by policy: {policy.name}",
|
|
432
|
+
)
|
|
433
|
+
# All denies
|
|
434
|
+
policy, _ = matches[0]
|
|
435
|
+
return AccessDecision.deny(
|
|
436
|
+
f"Denied by policy: {policy.name}"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
elif self._combination == PolicyCombination.FIRST_APPLICABLE:
|
|
440
|
+
# First match wins
|
|
441
|
+
policy, effect = matches[0]
|
|
442
|
+
if effect == PermissionEffect.ALLOW:
|
|
443
|
+
return AccessDecision.allow(
|
|
444
|
+
f"Allowed by policy: {policy.name}",
|
|
445
|
+
)
|
|
446
|
+
return AccessDecision.deny(
|
|
447
|
+
f"Denied by policy: {policy.name}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
elif self._combination == PolicyCombination.UNANIMOUS:
|
|
451
|
+
# All must allow
|
|
452
|
+
for policy, effect in matches:
|
|
453
|
+
if effect == PermissionEffect.DENY:
|
|
454
|
+
return AccessDecision.deny(
|
|
455
|
+
f"Denied by policy: {policy.name} (unanimous required)"
|
|
456
|
+
)
|
|
457
|
+
policy, _ = matches[0]
|
|
458
|
+
return AccessDecision.allow(
|
|
459
|
+
f"Unanimously allowed",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return AccessDecision.deny("Unknown combination algorithm")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class ABACEvaluator(PolicyEvaluator):
|
|
466
|
+
"""Attribute-Based Access Control evaluator.
|
|
467
|
+
|
|
468
|
+
Evaluates access based on attributes of the principal, resource,
|
|
469
|
+
and environment.
|
|
470
|
+
|
|
471
|
+
Example:
|
|
472
|
+
>>> evaluator = ABACEvaluator()
|
|
473
|
+
>>> evaluator.add_rule(
|
|
474
|
+
... condition=Condition("resource.owner_id", ConditionOperator.EQUALS, "${principal.id}"),
|
|
475
|
+
... effect=PermissionEffect.ALLOW,
|
|
476
|
+
... )
|
|
477
|
+
"""
|
|
478
|
+
|
|
479
|
+
def __init__(self) -> None:
|
|
480
|
+
self._rules: list[tuple[list[Condition], PermissionEffect, str]] = []
|
|
481
|
+
self._lock = threading.RLock()
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def name(self) -> str:
|
|
485
|
+
return "abac"
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def priority(self) -> int:
|
|
489
|
+
return 80
|
|
490
|
+
|
|
491
|
+
def add_rule(
|
|
492
|
+
self,
|
|
493
|
+
conditions: list[Condition],
|
|
494
|
+
effect: PermissionEffect = PermissionEffect.ALLOW,
|
|
495
|
+
description: str = "",
|
|
496
|
+
) -> None:
|
|
497
|
+
"""Add an ABAC rule."""
|
|
498
|
+
with self._lock:
|
|
499
|
+
self._rules.append((conditions, effect, description))
|
|
500
|
+
|
|
501
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
502
|
+
"""Evaluate access based on attributes."""
|
|
503
|
+
start_time = time.time()
|
|
504
|
+
|
|
505
|
+
if context.principal is None:
|
|
506
|
+
return AccessDecision.deny("No principal in context")
|
|
507
|
+
|
|
508
|
+
with self._lock:
|
|
509
|
+
for conditions, effect, description in self._rules:
|
|
510
|
+
all_match = True
|
|
511
|
+
for condition in conditions:
|
|
512
|
+
if not condition.evaluate(context):
|
|
513
|
+
all_match = False
|
|
514
|
+
break
|
|
515
|
+
|
|
516
|
+
if all_match:
|
|
517
|
+
elapsed = (time.time() - start_time) * 1000
|
|
518
|
+
if effect == PermissionEffect.ALLOW:
|
|
519
|
+
decision = AccessDecision.allow(
|
|
520
|
+
f"ABAC rule matched: {description}"
|
|
521
|
+
)
|
|
522
|
+
else:
|
|
523
|
+
decision = AccessDecision.deny(
|
|
524
|
+
f"ABAC rule denied: {description}"
|
|
525
|
+
)
|
|
526
|
+
decision.evaluation_time_ms = elapsed
|
|
527
|
+
return decision
|
|
528
|
+
|
|
529
|
+
return AccessDecision(
|
|
530
|
+
allowed=False,
|
|
531
|
+
reason="No matching ABAC rule",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class OwnershipEvaluator(PolicyEvaluator):
|
|
536
|
+
"""Ownership-based access control evaluator.
|
|
537
|
+
|
|
538
|
+
Grants access if the principal owns the resource.
|
|
539
|
+
|
|
540
|
+
Example:
|
|
541
|
+
>>> evaluator = OwnershipEvaluator(owner_field="owner_id")
|
|
542
|
+
>>> # Principal can access resources they own
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
def __init__(
|
|
546
|
+
self,
|
|
547
|
+
owner_field: str = "owner_id",
|
|
548
|
+
actions: list[str] | None = None, # Actions allowed for owners
|
|
549
|
+
) -> None:
|
|
550
|
+
self._owner_field = owner_field
|
|
551
|
+
self._actions = actions or ["read", "update", "delete"]
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def name(self) -> str:
|
|
555
|
+
return "ownership"
|
|
556
|
+
|
|
557
|
+
@property
|
|
558
|
+
def priority(self) -> int:
|
|
559
|
+
return 70
|
|
560
|
+
|
|
561
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
562
|
+
"""Evaluate access based on ownership."""
|
|
563
|
+
if context.principal is None:
|
|
564
|
+
return AccessDecision.deny("No principal in context")
|
|
565
|
+
|
|
566
|
+
action = context.action if isinstance(context.action, str) else context.action.value
|
|
567
|
+
|
|
568
|
+
# Check if action is allowed for owners
|
|
569
|
+
if action not in self._actions and "*" not in self._actions:
|
|
570
|
+
return AccessDecision(
|
|
571
|
+
allowed=False,
|
|
572
|
+
reason="Action not covered by ownership evaluator",
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Get owner from resource attributes
|
|
576
|
+
owner_id = context.resource_attributes.get(self._owner_field)
|
|
577
|
+
if owner_id is None:
|
|
578
|
+
return AccessDecision(
|
|
579
|
+
allowed=False,
|
|
580
|
+
reason="No owner information in resource",
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Check ownership
|
|
584
|
+
if owner_id == context.principal.id:
|
|
585
|
+
return AccessDecision.allow(
|
|
586
|
+
f"Principal owns the resource"
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return AccessDecision(
|
|
590
|
+
allowed=False,
|
|
591
|
+
reason="Principal does not own the resource",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class TenantIsolationEvaluator(PolicyEvaluator):
|
|
596
|
+
"""Tenant isolation evaluator.
|
|
597
|
+
|
|
598
|
+
Ensures principals can only access resources within their tenant.
|
|
599
|
+
|
|
600
|
+
Example:
|
|
601
|
+
>>> evaluator = TenantIsolationEvaluator()
|
|
602
|
+
>>> # Denies cross-tenant access
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
def __init__(
|
|
606
|
+
self,
|
|
607
|
+
tenant_field: str = "tenant_id",
|
|
608
|
+
enforce_strict: bool = True,
|
|
609
|
+
) -> None:
|
|
610
|
+
self._tenant_field = tenant_field
|
|
611
|
+
self._enforce_strict = enforce_strict
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def name(self) -> str:
|
|
615
|
+
return "tenant_isolation"
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def priority(self) -> int:
|
|
619
|
+
return 200 # High priority - check early
|
|
620
|
+
|
|
621
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
622
|
+
"""Evaluate tenant isolation."""
|
|
623
|
+
if context.principal is None:
|
|
624
|
+
return AccessDecision.deny("No principal in context")
|
|
625
|
+
|
|
626
|
+
principal_tenant = context.principal.tenant_id
|
|
627
|
+
resource_tenant = context.resource_attributes.get(self._tenant_field)
|
|
628
|
+
|
|
629
|
+
# If no tenant info, depends on strict mode
|
|
630
|
+
if resource_tenant is None:
|
|
631
|
+
if self._enforce_strict:
|
|
632
|
+
return AccessDecision(
|
|
633
|
+
allowed=False,
|
|
634
|
+
reason="No tenant information in resource",
|
|
635
|
+
)
|
|
636
|
+
return AccessDecision(
|
|
637
|
+
allowed=True,
|
|
638
|
+
reason="Tenant check skipped - no tenant info",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Check tenant match
|
|
642
|
+
if principal_tenant != resource_tenant:
|
|
643
|
+
return AccessDecision.deny(
|
|
644
|
+
f"Cross-tenant access denied: {principal_tenant} != {resource_tenant}"
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
return AccessDecision.allow("Same tenant access")
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class SuperuserEvaluator(PolicyEvaluator):
|
|
651
|
+
"""Superuser evaluator.
|
|
652
|
+
|
|
653
|
+
Grants all permissions to superuser/admin principals.
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
>>> evaluator = SuperuserEvaluator(superuser_roles={"superadmin"})
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
def __init__(
|
|
660
|
+
self,
|
|
661
|
+
superuser_roles: set[str] | None = None,
|
|
662
|
+
superuser_ids: set[str] | None = None,
|
|
663
|
+
) -> None:
|
|
664
|
+
self._superuser_roles = superuser_roles or {"superadmin", "system_admin"}
|
|
665
|
+
self._superuser_ids = superuser_ids or {"system"}
|
|
666
|
+
|
|
667
|
+
@property
|
|
668
|
+
def name(self) -> str:
|
|
669
|
+
return "superuser"
|
|
670
|
+
|
|
671
|
+
@property
|
|
672
|
+
def priority(self) -> int:
|
|
673
|
+
return 1000 # Highest priority
|
|
674
|
+
|
|
675
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
676
|
+
"""Check if principal is a superuser."""
|
|
677
|
+
if context.principal is None:
|
|
678
|
+
return AccessDecision(allowed=False, reason="No principal")
|
|
679
|
+
|
|
680
|
+
# Check superuser IDs
|
|
681
|
+
if context.principal.id in self._superuser_ids:
|
|
682
|
+
return AccessDecision.allow("Superuser access (by ID)")
|
|
683
|
+
|
|
684
|
+
# Check superuser roles
|
|
685
|
+
if context.principal.roles & self._superuser_roles:
|
|
686
|
+
return AccessDecision.allow("Superuser access (by role)")
|
|
687
|
+
|
|
688
|
+
return AccessDecision(
|
|
689
|
+
allowed=False,
|
|
690
|
+
reason="Not a superuser",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
# =============================================================================
|
|
695
|
+
# Policy Engine
|
|
696
|
+
# =============================================================================
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@dataclass
|
|
700
|
+
class PolicyEngineConfig:
|
|
701
|
+
"""Configuration for the policy engine."""
|
|
702
|
+
|
|
703
|
+
combination: PolicyCombination = PolicyCombination.DENY_OVERRIDES
|
|
704
|
+
deny_by_default: bool = True
|
|
705
|
+
log_decisions: bool = True
|
|
706
|
+
cache_decisions: bool = True
|
|
707
|
+
cache_ttl_seconds: int = 60
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class PolicyEngine:
|
|
711
|
+
"""Central policy engine for access control.
|
|
712
|
+
|
|
713
|
+
Coordinates multiple policy evaluators to make access decisions.
|
|
714
|
+
|
|
715
|
+
Example:
|
|
716
|
+
>>> engine = PolicyEngine()
|
|
717
|
+
>>> engine.add_evaluator(RoleBasedEvaluator(role_store))
|
|
718
|
+
>>> engine.add_evaluator(OwnershipEvaluator())
|
|
719
|
+
>>>
|
|
720
|
+
>>> decision = engine.evaluate(context)
|
|
721
|
+
>>> if decision.allowed:
|
|
722
|
+
... process_request()
|
|
723
|
+
"""
|
|
724
|
+
|
|
725
|
+
def __init__(
|
|
726
|
+
self,
|
|
727
|
+
config: PolicyEngineConfig | None = None,
|
|
728
|
+
) -> None:
|
|
729
|
+
self._config = config or PolicyEngineConfig()
|
|
730
|
+
self._evaluators: list[PolicyEvaluator] = []
|
|
731
|
+
self._decision_cache: dict[str, tuple[AccessDecision, float]] = {}
|
|
732
|
+
self._lock = threading.RLock()
|
|
733
|
+
|
|
734
|
+
def add_evaluator(self, evaluator: PolicyEvaluator) -> None:
|
|
735
|
+
"""Add a policy evaluator."""
|
|
736
|
+
with self._lock:
|
|
737
|
+
self._evaluators.append(evaluator)
|
|
738
|
+
# Sort by priority (descending)
|
|
739
|
+
self._evaluators.sort(key=lambda e: e.priority, reverse=True)
|
|
740
|
+
|
|
741
|
+
def remove_evaluator(self, name: str) -> bool:
|
|
742
|
+
"""Remove an evaluator by name."""
|
|
743
|
+
with self._lock:
|
|
744
|
+
for i, evaluator in enumerate(self._evaluators):
|
|
745
|
+
if evaluator.name == name:
|
|
746
|
+
del self._evaluators[i]
|
|
747
|
+
return True
|
|
748
|
+
return False
|
|
749
|
+
|
|
750
|
+
def evaluate(self, context: AccessContext) -> AccessDecision:
|
|
751
|
+
"""Evaluate access for the given context.
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
context: Access context containing principal, resource, action
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
AccessDecision indicating whether access is allowed.
|
|
758
|
+
"""
|
|
759
|
+
start_time = time.time()
|
|
760
|
+
|
|
761
|
+
# Check cache
|
|
762
|
+
cache_key = self._get_cache_key(context)
|
|
763
|
+
if self._config.cache_decisions and cache_key:
|
|
764
|
+
cached = self._get_cached_decision(cache_key)
|
|
765
|
+
if cached:
|
|
766
|
+
return cached
|
|
767
|
+
|
|
768
|
+
# Collect decisions from all evaluators
|
|
769
|
+
decisions: list[tuple[PolicyEvaluator, AccessDecision]] = []
|
|
770
|
+
|
|
771
|
+
with self._lock:
|
|
772
|
+
for evaluator in self._evaluators:
|
|
773
|
+
try:
|
|
774
|
+
decision = evaluator.evaluate(context)
|
|
775
|
+
decisions.append((evaluator, decision))
|
|
776
|
+
except Exception as e:
|
|
777
|
+
# Log error but continue with other evaluators
|
|
778
|
+
if self._config.log_decisions:
|
|
779
|
+
pass # Would log here
|
|
780
|
+
|
|
781
|
+
if not decisions:
|
|
782
|
+
return AccessDecision.deny("No evaluators configured")
|
|
783
|
+
|
|
784
|
+
# Combine decisions
|
|
785
|
+
final_decision = self._combine_decisions(decisions)
|
|
786
|
+
final_decision.evaluation_time_ms = (time.time() - start_time) * 1000
|
|
787
|
+
|
|
788
|
+
# Cache decision
|
|
789
|
+
if self._config.cache_decisions and cache_key:
|
|
790
|
+
self._cache_decision(cache_key, final_decision)
|
|
791
|
+
|
|
792
|
+
return final_decision
|
|
793
|
+
|
|
794
|
+
def _combine_decisions(
|
|
795
|
+
self,
|
|
796
|
+
decisions: list[tuple[PolicyEvaluator, AccessDecision]],
|
|
797
|
+
) -> AccessDecision:
|
|
798
|
+
"""Combine decisions from multiple evaluators."""
|
|
799
|
+
if self._config.combination == PolicyCombination.DENY_OVERRIDES:
|
|
800
|
+
# Any deny wins
|
|
801
|
+
for evaluator, decision in decisions:
|
|
802
|
+
if decision.allowed is False and decision.effect == PermissionEffect.DENY:
|
|
803
|
+
return decision
|
|
804
|
+
|
|
805
|
+
# Check for any allow
|
|
806
|
+
for evaluator, decision in decisions:
|
|
807
|
+
if decision.allowed:
|
|
808
|
+
return decision
|
|
809
|
+
|
|
810
|
+
elif self._config.combination == PolicyCombination.ALLOW_OVERRIDES:
|
|
811
|
+
# Any allow wins
|
|
812
|
+
for evaluator, decision in decisions:
|
|
813
|
+
if decision.allowed:
|
|
814
|
+
return decision
|
|
815
|
+
|
|
816
|
+
elif self._config.combination == PolicyCombination.FIRST_APPLICABLE:
|
|
817
|
+
# First definitive decision wins
|
|
818
|
+
for evaluator, decision in decisions:
|
|
819
|
+
if decision.effect in (PermissionEffect.ALLOW, PermissionEffect.DENY):
|
|
820
|
+
return decision
|
|
821
|
+
|
|
822
|
+
elif self._config.combination == PolicyCombination.UNANIMOUS:
|
|
823
|
+
# All must allow
|
|
824
|
+
for evaluator, decision in decisions:
|
|
825
|
+
if not decision.allowed:
|
|
826
|
+
return decision
|
|
827
|
+
|
|
828
|
+
# All allowed
|
|
829
|
+
_, decision = decisions[0]
|
|
830
|
+
return decision
|
|
831
|
+
|
|
832
|
+
# Default: deny
|
|
833
|
+
if self._config.deny_by_default:
|
|
834
|
+
return AccessDecision.deny("No matching policy (deny by default)")
|
|
835
|
+
|
|
836
|
+
return AccessDecision(
|
|
837
|
+
allowed=False,
|
|
838
|
+
reason="No definitive decision",
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
def _get_cache_key(self, context: AccessContext) -> str | None:
|
|
842
|
+
"""Generate a cache key for the context."""
|
|
843
|
+
if context.principal is None:
|
|
844
|
+
return None
|
|
845
|
+
|
|
846
|
+
parts = [
|
|
847
|
+
context.principal.id,
|
|
848
|
+
context.resource,
|
|
849
|
+
context.action if isinstance(context.action, str) else context.action.value,
|
|
850
|
+
context.tenant_id or "",
|
|
851
|
+
]
|
|
852
|
+
return ":".join(parts)
|
|
853
|
+
|
|
854
|
+
def _get_cached_decision(self, cache_key: str) -> AccessDecision | None:
|
|
855
|
+
"""Get a cached decision."""
|
|
856
|
+
with self._lock:
|
|
857
|
+
if cache_key in self._decision_cache:
|
|
858
|
+
decision, cached_at = self._decision_cache[cache_key]
|
|
859
|
+
if time.time() - cached_at < self._config.cache_ttl_seconds:
|
|
860
|
+
return decision
|
|
861
|
+
else:
|
|
862
|
+
del self._decision_cache[cache_key]
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
def _cache_decision(self, cache_key: str, decision: AccessDecision) -> None:
|
|
866
|
+
"""Cache a decision."""
|
|
867
|
+
with self._lock:
|
|
868
|
+
# Limit cache size
|
|
869
|
+
if len(self._decision_cache) > 10000:
|
|
870
|
+
# Remove oldest entries
|
|
871
|
+
sorted_keys = sorted(
|
|
872
|
+
self._decision_cache.keys(),
|
|
873
|
+
key=lambda k: self._decision_cache[k][1],
|
|
874
|
+
)
|
|
875
|
+
for key in sorted_keys[:1000]:
|
|
876
|
+
del self._decision_cache[key]
|
|
877
|
+
|
|
878
|
+
self._decision_cache[cache_key] = (decision, time.time())
|
|
879
|
+
|
|
880
|
+
def invalidate_cache(
|
|
881
|
+
self,
|
|
882
|
+
principal_id: str | None = None,
|
|
883
|
+
resource: str | None = None,
|
|
884
|
+
) -> None:
|
|
885
|
+
"""Invalidate cached decisions."""
|
|
886
|
+
with self._lock:
|
|
887
|
+
if principal_id is None and resource is None:
|
|
888
|
+
self._decision_cache.clear()
|
|
889
|
+
return
|
|
890
|
+
|
|
891
|
+
keys_to_remove = []
|
|
892
|
+
for key in self._decision_cache:
|
|
893
|
+
parts = key.split(":")
|
|
894
|
+
if principal_id and parts[0] == principal_id:
|
|
895
|
+
keys_to_remove.append(key)
|
|
896
|
+
elif resource and parts[1] == resource:
|
|
897
|
+
keys_to_remove.append(key)
|
|
898
|
+
|
|
899
|
+
for key in keys_to_remove:
|
|
900
|
+
del self._decision_cache[key]
|
|
901
|
+
|
|
902
|
+
def check(
|
|
903
|
+
self,
|
|
904
|
+
principal: Principal,
|
|
905
|
+
resource: str,
|
|
906
|
+
action: str,
|
|
907
|
+
resource_attributes: dict[str, Any] | None = None,
|
|
908
|
+
) -> AccessDecision:
|
|
909
|
+
"""Convenience method for checking access.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
principal: Principal requesting access
|
|
913
|
+
resource: Resource being accessed
|
|
914
|
+
action: Action being performed
|
|
915
|
+
resource_attributes: Optional resource attributes for ABAC
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
AccessDecision.
|
|
919
|
+
"""
|
|
920
|
+
context = AccessContext(
|
|
921
|
+
principal=principal,
|
|
922
|
+
resource=resource,
|
|
923
|
+
action=action,
|
|
924
|
+
resource_attributes=resource_attributes or {},
|
|
925
|
+
tenant_id=principal.tenant_id,
|
|
926
|
+
)
|
|
927
|
+
return self.evaluate(context)
|
|
928
|
+
|
|
929
|
+
def require(
|
|
930
|
+
self,
|
|
931
|
+
principal: Principal,
|
|
932
|
+
resource: str,
|
|
933
|
+
action: str,
|
|
934
|
+
resource_attributes: dict[str, Any] | None = None,
|
|
935
|
+
) -> None:
|
|
936
|
+
"""Check access and raise if denied.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
principal: Principal requesting access
|
|
940
|
+
resource: Resource being accessed
|
|
941
|
+
action: Action being performed
|
|
942
|
+
resource_attributes: Optional resource attributes
|
|
943
|
+
|
|
944
|
+
Raises:
|
|
945
|
+
PermissionDeniedError: If access is denied.
|
|
946
|
+
"""
|
|
947
|
+
decision = self.check(principal, resource, action, resource_attributes)
|
|
948
|
+
if not decision.allowed:
|
|
949
|
+
raise PermissionDeniedError(
|
|
950
|
+
decision.reason,
|
|
951
|
+
principal_id=principal.id,
|
|
952
|
+
resource=resource,
|
|
953
|
+
action=action,
|
|
954
|
+
)
|