velocity-python 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {velocity_python-0.2.1/src/velocity_python.egg-info → velocity_python-0.2.2}/PKG-INFO +1 -1
- {velocity_python-0.2.1 → velocity_python-0.2.2}/pyproject.toml +1 -1
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/__init__.py +1 -1
- velocity_python-0.2.2/src/velocity/aws/authz.py +428 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/data_service.py +98 -9
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/web_handler.py +115 -9
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/schema.py +1 -0
- velocity_python-0.2.2/src/velocity/aws/user_data.py +404 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2/src/velocity_python.egg-info}/PKG-INFO +1 -1
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity_python.egg-info/SOURCES.txt +6 -0
- velocity_python-0.2.2/tests/test_authz.py +474 -0
- velocity_python-0.2.2/tests/test_data_service_owner_scope.py +259 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_ensure_infrastructure.py +1 -0
- velocity_python-0.2.2/tests/test_pgserver_user_data.py +188 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_query_missing_table_unlocked.py +2 -0
- velocity_python-0.2.2/tests/test_user_data.py +494 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/LICENSE +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/README.md +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/setup.cfg +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/blobs.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/base.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/dispatch.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/infra.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/jobs.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/provisioning.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/retention.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/security.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/capabilities/single_session.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/ci_gate.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/frontend_checks.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/facets.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/failures.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/feature_flags.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/audit_trail.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/client_errors.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/client_input.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/row_scoped.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/job_lifecycle.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/mirror.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/ops.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/queue_defaults.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_deploy_least_privilege.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_deploy_policy_size.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/lazy_engine.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/fragments.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/tunnel.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/logging.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/_mirror_core.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/money.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/plaid_api.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/plaid_webhook.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/redact.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/schema.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/testing/__init__.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/testing/fake_db.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_access_error_403.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_amplify_redirects.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_asset_references.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_assets_service.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_async_support.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_audit_mixin.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_aws_package_hygiene.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_blob_store.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_capabilities.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_capability_catalog.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_check_changelog.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_ci_gate.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_client_errors_mixin.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_client_input_contract_export.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_client_sql_boundary.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_client_table_gate_case_and_pointers.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_cognito_user_cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_context_flags_loader.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_context_queue_job.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_conv_combine.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_data_service_query_cap.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_data_service_renumber.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_data_service_row_number.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_data_service_save_as.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_data_service_security_gates.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_db_tunnel.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_declared_column_types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_decorators.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_deploy_config_fields.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_deploy_engine.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_deploy_frontend_checks.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_deploy_operations_queue.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_dlq_constant.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_docs_snippets.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_email_processing.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_enqueue_metric_namespace.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_export_workbook.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_facets.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_facets_and_where.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_failures.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_feature_flags.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_job_handler.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_job_lifecycle.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_job_results.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_json_columns.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_lambda_handler_user_cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_lazy_engine.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_merge_on_conflict.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_mirror.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_mirror_upsert_public.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_observability.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_on_action_default.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_on_commit.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_ops.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_audit_fixes.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_business_agnostic.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_charge_reference.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_extra_bounds.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_lazy_imports.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pdf.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pgserver_bulk_cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pgserver_capability_types.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pgserver_ensure_infrastructure.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pgserver_get_tables.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pgserver_pointer_joins.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pgserver_search_export.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_plaid_api.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_plaid_webhook.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_pointer_join_sql.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_postgres_initializer_config.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_postgres_py_type.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_query_cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_raw_sql_prefix_policy.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_read_cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_readme_imports.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_response.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_result_returning.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_row_scoped_data_service.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_s3_bucket_config.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_s3_list_client_files.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_s3_upload.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_schema_lock_config_other_backends.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_search_paging_export.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_security_capability.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_session_control.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_single_session_capability.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_sms_twilio_signature.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_ssm_config_require.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_table_alter.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_table_claim.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_table_find_cache.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_table_purge.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_testing_fake_db.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_tx_lifecycle_retry.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_tx_read_cache_and_bulk.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_user_not_provisioned.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_write_hook_create_flow.py +0 -0
- {velocity_python-0.2.1 → velocity_python-0.2.2}/tests/test_write_hook_empty_strings.py +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Who may do what, decided in one place.
|
|
2
|
+
|
|
3
|
+
One pure function answers every authorization question::
|
|
4
|
+
|
|
5
|
+
can(principal, action, resource, context) -> Decision
|
|
6
|
+
|
|
7
|
+
* **Deny by default.** Nothing is allowed unless a rule permits it.
|
|
8
|
+
* **A forbid beats any permit.**
|
|
9
|
+
* **An error can only take access away.** A permit rule whose condition
|
|
10
|
+
raises is skipped; a forbid rule whose condition raises forbids. Either
|
|
11
|
+
way the rule is named in ``Decision.errors`` and logged.
|
|
12
|
+
* **No I/O.** A decision is id comparisons and set lookups. Whoever already
|
|
13
|
+
loaded the row passes its owner in; this module never loads data.
|
|
14
|
+
|
|
15
|
+
Rules are data, declared beside the feature they govern and registered once
|
|
16
|
+
at import::
|
|
17
|
+
|
|
18
|
+
from velocity.aws import authz
|
|
19
|
+
|
|
20
|
+
RULES = (
|
|
21
|
+
authz.permit("notes.own", {"notes.read", "notes.write"}, when=authz.owner),
|
|
22
|
+
authz.permit("notes.read_any", "notes.read",
|
|
23
|
+
when=authz.holds("notes.read_any"), audit=True),
|
|
24
|
+
authz.forbid("notes.masquerade_write", "notes.write",
|
|
25
|
+
when=authz.masquerading),
|
|
26
|
+
)
|
|
27
|
+
authz.register(*RULES)
|
|
28
|
+
|
|
29
|
+
Permissions are plain names a principal holds. Code never checks a role:
|
|
30
|
+
a role is a bundle of permissions, and which bundle a user gets is the
|
|
31
|
+
site's decision (``principal_permissions`` on the handler).
|
|
32
|
+
|
|
33
|
+
**On a handler.** :func:`principal_for` builds the principal from the user
|
|
34
|
+
row the handler already loaded; :func:`decide` asks; :func:`require` asks,
|
|
35
|
+
refuses with a 403, and records the use of any rule marked ``audit`` in
|
|
36
|
+
``sys_security_events`` *before* the access happens (no audit row, no
|
|
37
|
+
access). The handler attributes they read:
|
|
38
|
+
|
|
39
|
+
========================== ================================================
|
|
40
|
+
``authz_mode`` ``"off"`` (default), ``"log"`` or ``"enforce"``:
|
|
41
|
+
how the *existing* generic surfaces (the
|
|
42
|
+
DataService table gate) use the decision. New
|
|
43
|
+
actions written against :func:`require` are
|
|
44
|
+
always enforced.
|
|
45
|
+
``authz_rules`` a handler's own rules, beside the registered ones
|
|
46
|
+
``principal_permissions`` ``(current_user) -> iterable of names``; the
|
|
47
|
+
default grants none
|
|
48
|
+
========================== ================================================
|
|
49
|
+
|
|
50
|
+
The request shape and the decision rule are those of AWS's Cedar policy
|
|
51
|
+
language, on purpose: the rules can move there without changing a caller.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
|
|
56
|
+
import logging
|
|
57
|
+
from dataclasses import dataclass, field
|
|
58
|
+
from fnmatch import fnmatchcase
|
|
59
|
+
from typing import Any, Callable, Optional
|
|
60
|
+
|
|
61
|
+
from velocity.aws import ssm_config
|
|
62
|
+
from velocity.aws.handlers.exceptions import AccessError
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
PERMIT = "permit"
|
|
67
|
+
FORBID = "forbid"
|
|
68
|
+
|
|
69
|
+
OFF = "off"
|
|
70
|
+
LOG = "log"
|
|
71
|
+
ENFORCE = "enforce"
|
|
72
|
+
MODES = (OFF, LOG, ENFORCE)
|
|
73
|
+
|
|
74
|
+
# The action the DataService table gate asks about, and its resource type.
|
|
75
|
+
DATA_ACCESS = "data.access"
|
|
76
|
+
TABLE = "table"
|
|
77
|
+
SYSTEM_PREFIX = "sys_"
|
|
78
|
+
|
|
79
|
+
# The audit sink's ``guard`` value for a use of an ``audit`` rule.
|
|
80
|
+
PRIVILEGED_ACCESS = "privileged_access"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class Principal:
|
|
85
|
+
"""Who is asking. ``user_sys_id`` is the row id in the site's user table
|
|
86
|
+
(never an email). ``real_user`` is set in a masquerade session: the
|
|
87
|
+
administrator who is acting as this user."""
|
|
88
|
+
|
|
89
|
+
user_sys_id: Optional[int] = None
|
|
90
|
+
permissions: frozenset = frozenset()
|
|
91
|
+
tenant: Any = None
|
|
92
|
+
real_user: Any = None
|
|
93
|
+
|
|
94
|
+
def holds(self, permission):
|
|
95
|
+
return permission in self.permissions
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class Resource:
|
|
100
|
+
"""The specific object: its type, id, owner (a user row id) and tenant."""
|
|
101
|
+
|
|
102
|
+
type: str
|
|
103
|
+
id: Any = None
|
|
104
|
+
owner: Optional[int] = None
|
|
105
|
+
tenant: Any = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class Rule:
|
|
110
|
+
name: str
|
|
111
|
+
effect: str
|
|
112
|
+
actions: frozenset
|
|
113
|
+
when: Optional[Callable] = None
|
|
114
|
+
audit: bool = False
|
|
115
|
+
|
|
116
|
+
def covers(self, action):
|
|
117
|
+
return any(
|
|
118
|
+
action == pattern or ("*" in pattern and fnmatchcase(action, pattern))
|
|
119
|
+
for pattern in self.actions
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class Decision:
|
|
125
|
+
"""The answer. ``rule`` is the rule that decided (``None`` when nothing
|
|
126
|
+
permitted); ``audit`` is true when that rule's use must be recorded."""
|
|
127
|
+
|
|
128
|
+
allowed: bool
|
|
129
|
+
action: str
|
|
130
|
+
rule: Optional[str] = None
|
|
131
|
+
audit: bool = False
|
|
132
|
+
errors: tuple = field(default_factory=tuple)
|
|
133
|
+
|
|
134
|
+
def __bool__(self):
|
|
135
|
+
return self.allowed
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _rule(effect, name, actions, when, audit):
|
|
139
|
+
if not isinstance(name, str) or not name:
|
|
140
|
+
raise ValueError("A rule needs a name.")
|
|
141
|
+
if isinstance(actions, str):
|
|
142
|
+
actions = (actions,)
|
|
143
|
+
actions = frozenset(actions or ())
|
|
144
|
+
if not actions or not all(isinstance(a, str) and a for a in actions):
|
|
145
|
+
raise ValueError(f"Rule '{name}' names no action.")
|
|
146
|
+
if when is not None and not callable(when):
|
|
147
|
+
raise ValueError(f"Rule '{name}': 'when' must be callable.")
|
|
148
|
+
return Rule(name, effect, actions, when, bool(audit))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def permit(name, actions, *, when=None, audit=False):
|
|
152
|
+
"""Allow ``actions`` (a name, or names; ``*`` is a wildcard) when
|
|
153
|
+
``when(principal, action, resource, context)`` is true. ``audit=True``
|
|
154
|
+
marks a privileged rule: each use is recorded."""
|
|
155
|
+
return _rule(PERMIT, name, actions, when, audit)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def forbid(name, actions, *, when=None):
|
|
159
|
+
"""Refuse ``actions`` when ``when(...)`` is true, whatever permits."""
|
|
160
|
+
return _rule(FORBID, name, actions, when, False)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── conditions ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def owner(principal, action, resource, context):
|
|
167
|
+
"""The resource belongs to the principal."""
|
|
168
|
+
return (
|
|
169
|
+
principal.user_sys_id is not None
|
|
170
|
+
and resource is not None
|
|
171
|
+
and resource.owner is not None
|
|
172
|
+
and resource.owner == principal.user_sys_id
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def masquerading(principal, action, resource, context):
|
|
177
|
+
"""An administrator is acting as this user."""
|
|
178
|
+
return principal.real_user is not None or bool((context or {}).get("masquerade"))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def signed_in(principal, action, resource, context):
|
|
182
|
+
return principal.user_sys_id is not None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def holds(permission):
|
|
186
|
+
"""The principal holds the named permission."""
|
|
187
|
+
|
|
188
|
+
def condition(principal, action, resource, context):
|
|
189
|
+
return principal.holds(permission)
|
|
190
|
+
|
|
191
|
+
return condition
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def table(*names):
|
|
195
|
+
"""The resource is one of these tables."""
|
|
196
|
+
wanted = frozenset(names)
|
|
197
|
+
|
|
198
|
+
def condition(principal, action, resource, context):
|
|
199
|
+
return resource is not None and resource.type == TABLE and resource.id in wanted
|
|
200
|
+
|
|
201
|
+
return condition
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def all_of(*conditions):
|
|
205
|
+
def condition(principal, action, resource, context):
|
|
206
|
+
return all(c(principal, action, resource, context) for c in conditions)
|
|
207
|
+
|
|
208
|
+
return condition
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def any_of(*conditions):
|
|
212
|
+
def condition(principal, action, resource, context):
|
|
213
|
+
return any(c(principal, action, resource, context) for c in conditions)
|
|
214
|
+
|
|
215
|
+
return condition
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _ordinary_table(principal, action, resource, context):
|
|
219
|
+
return (
|
|
220
|
+
resource is not None
|
|
221
|
+
and resource.type == TABLE
|
|
222
|
+
and isinstance(resource.id, str)
|
|
223
|
+
and not resource.id.startswith(SYSTEM_PREFIX)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── the registry ───────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
# Until each site declares rules per table, an ordinary table stays reachable
|
|
230
|
+
# through the generic Data actions exactly as the table gate, the rwx hooks
|
|
231
|
+
# and the tenant scope already allow. System tables (``sys_…``) get no such
|
|
232
|
+
# rule: they are refused unless the handler declares one.
|
|
233
|
+
BUILTIN_RULES = (permit("data.ordinary_tables", DATA_ACCESS, when=_ordinary_table),)
|
|
234
|
+
|
|
235
|
+
_REGISTERED = {}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def register(*rules):
|
|
239
|
+
"""Add rules to the ones every handler evaluates. Registering the same
|
|
240
|
+
rule again is a no-op; a different rule under a taken name is an error."""
|
|
241
|
+
for rule in rules:
|
|
242
|
+
if not isinstance(rule, Rule):
|
|
243
|
+
raise ValueError("register() takes rules made by permit() or forbid().")
|
|
244
|
+
known = _REGISTERED.get(rule.name)
|
|
245
|
+
if known is not None and known != rule:
|
|
246
|
+
raise ValueError(f"An authorization rule named '{rule.name}' exists.")
|
|
247
|
+
_REGISTERED[rule.name] = rule
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def registered():
|
|
251
|
+
"""Every rule a handler evaluates before its own."""
|
|
252
|
+
return BUILTIN_RULES + tuple(_REGISTERED.values())
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ── the decision ───────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def can(principal, action, resource=None, context=None, *, rules=None):
|
|
259
|
+
"""Decide. See the module docstring for the rule."""
|
|
260
|
+
rules = registered() if rules is None else tuple(rules)
|
|
261
|
+
context = context or {}
|
|
262
|
+
permitted = None
|
|
263
|
+
errors = []
|
|
264
|
+
if not isinstance(principal, Principal) or not isinstance(action, str):
|
|
265
|
+
return Decision(False, str(action))
|
|
266
|
+
for rule in rules:
|
|
267
|
+
if not rule.covers(action):
|
|
268
|
+
continue
|
|
269
|
+
try:
|
|
270
|
+
matched = rule.when is None or bool(
|
|
271
|
+
rule.when(principal, action, resource, context)
|
|
272
|
+
)
|
|
273
|
+
except Exception:
|
|
274
|
+
logger.warning(
|
|
275
|
+
"authorization rule %s failed on %s", rule.name, action, exc_info=True
|
|
276
|
+
)
|
|
277
|
+
errors.append(rule.name)
|
|
278
|
+
# an error can only take access away
|
|
279
|
+
matched = rule.effect == FORBID
|
|
280
|
+
if not matched:
|
|
281
|
+
continue
|
|
282
|
+
if rule.effect == FORBID:
|
|
283
|
+
return Decision(False, action, rule.name, False, tuple(errors))
|
|
284
|
+
if permitted is None:
|
|
285
|
+
permitted = rule
|
|
286
|
+
if permitted is None:
|
|
287
|
+
return Decision(False, action, None, False, tuple(errors))
|
|
288
|
+
return Decision(True, action, permitted.name, permitted.audit, tuple(errors))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ── on a handler ───────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def mode_of(handler):
|
|
295
|
+
"""``AuthzMode`` config first, then the handler's ``authz_mode``. Blank
|
|
296
|
+
is ``off``; a value that is not a mode is ``enforce`` (fail closed)."""
|
|
297
|
+
raw = None
|
|
298
|
+
try:
|
|
299
|
+
raw = ssm_config.getenv("AuthzMode", None)
|
|
300
|
+
except Exception:
|
|
301
|
+
raw = None
|
|
302
|
+
raw = str(raw or getattr(handler, "authz_mode", None) or OFF).strip().lower()
|
|
303
|
+
return raw if raw in MODES else ENFORCE
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _session(context):
|
|
307
|
+
try:
|
|
308
|
+
return context.session() or {}
|
|
309
|
+
except Exception:
|
|
310
|
+
return {}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def principal_for(handler, context=None):
|
|
314
|
+
"""The principal for the request ``handler`` is serving, from the user
|
|
315
|
+
row it already loaded. No query."""
|
|
316
|
+
user = getattr(handler, "current_user", None) or {}
|
|
317
|
+
try:
|
|
318
|
+
user_sys_id = int(user.get("sys_id"))
|
|
319
|
+
except (TypeError, ValueError, AttributeError):
|
|
320
|
+
user_sys_id = None
|
|
321
|
+
if user_sys_id is not None and user_sys_id <= 0:
|
|
322
|
+
user_sys_id = None
|
|
323
|
+
permissions = ()
|
|
324
|
+
hook = getattr(handler, "principal_permissions", None)
|
|
325
|
+
if callable(hook) and user_sys_id is not None:
|
|
326
|
+
try:
|
|
327
|
+
permissions = hook(user) or ()
|
|
328
|
+
except Exception:
|
|
329
|
+
# no permissions is the safe answer
|
|
330
|
+
logger.warning("principal_permissions failed", exc_info=True)
|
|
331
|
+
permissions = ()
|
|
332
|
+
context = context if context is not None else getattr(handler, "context", None)
|
|
333
|
+
masquerade = _session(context).get("masquerade") if context is not None else None
|
|
334
|
+
real_user = None
|
|
335
|
+
if masquerade:
|
|
336
|
+
real_user = (
|
|
337
|
+
masquerade.get("real_user") if isinstance(masquerade, dict) else None
|
|
338
|
+
) or True
|
|
339
|
+
return Principal(
|
|
340
|
+
user_sys_id=user_sys_id,
|
|
341
|
+
permissions=frozenset(p for p in permissions if isinstance(p, str)),
|
|
342
|
+
tenant=user.get("tenant") if isinstance(user, dict) else None,
|
|
343
|
+
real_user=real_user,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def rules_for(handler):
|
|
348
|
+
return registered() + tuple(getattr(handler, "authz_rules", None) or ())
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def decide(handler, action, resource=None, context=None):
|
|
352
|
+
"""The decision for the request ``handler`` is serving."""
|
|
353
|
+
principal = principal_for(handler, context)
|
|
354
|
+
facts = {"masquerade": principal.real_user is not None}
|
|
355
|
+
return can(principal, action, resource, facts, rules=rules_for(handler))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def require(handler, tx, context, action, resource=None):
|
|
359
|
+
"""Decide and enforce, whatever ``authz_mode`` says: for actions written
|
|
360
|
+
against the policy. A denial raises :class:`AccessError` (403). A use of
|
|
361
|
+
an ``audit`` rule is recorded first, and refused when it cannot be."""
|
|
362
|
+
decision = decide(handler, action, resource, context)
|
|
363
|
+
if not decision.allowed:
|
|
364
|
+
logger.warning(
|
|
365
|
+
"authorization refused %s on %s (%s)",
|
|
366
|
+
action,
|
|
367
|
+
_describe(resource),
|
|
368
|
+
decision.rule or "no rule permits it",
|
|
369
|
+
)
|
|
370
|
+
raise AccessError("You do not have access to that.")
|
|
371
|
+
if decision.audit and not _record(handler, tx, context, decision, resource):
|
|
372
|
+
raise AccessError("That access could not be recorded, so it was refused.")
|
|
373
|
+
return decision
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
_WOULD_REFUSE = set()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def gate(handler, action, resource=None):
|
|
380
|
+
"""The decision for an EXISTING generic surface, under ``authz_mode``:
|
|
381
|
+
``off`` allows without asking, ``log`` asks and logs what it would
|
|
382
|
+
refuse (once per container per action and resource) and allows,
|
|
383
|
+
``enforce`` returns the decision's answer."""
|
|
384
|
+
mode = mode_of(handler)
|
|
385
|
+
if mode == OFF:
|
|
386
|
+
return True
|
|
387
|
+
decision = decide(handler, action, resource)
|
|
388
|
+
if decision.allowed:
|
|
389
|
+
return True
|
|
390
|
+
if mode == LOG:
|
|
391
|
+
key = (action, getattr(resource, "type", None), getattr(resource, "id", None))
|
|
392
|
+
if key not in _WOULD_REFUSE:
|
|
393
|
+
_WOULD_REFUSE.add(key)
|
|
394
|
+
logger.warning(
|
|
395
|
+
"authz (log only) would refuse %s on %s (%s)",
|
|
396
|
+
action,
|
|
397
|
+
_describe(resource),
|
|
398
|
+
decision.rule or "no rule permits it",
|
|
399
|
+
)
|
|
400
|
+
return True
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _describe(resource):
|
|
405
|
+
if resource is None:
|
|
406
|
+
return "-"
|
|
407
|
+
return f"{resource.type}:{resource.id}"
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _record(handler, tx, context, decision, resource):
|
|
411
|
+
# imported here: the capabilities package pulls in more than a pure
|
|
412
|
+
# decision module should load at import
|
|
413
|
+
from velocity.aws.capabilities.security import record_event
|
|
414
|
+
|
|
415
|
+
session = _session(context)
|
|
416
|
+
actor = (
|
|
417
|
+
(session.get("masquerade") or {}).get("real_user")
|
|
418
|
+
if isinstance(session.get("masquerade"), dict)
|
|
419
|
+
else None
|
|
420
|
+
) or session.get("email_address")
|
|
421
|
+
return record_event(
|
|
422
|
+
tx,
|
|
423
|
+
PRIVILEGED_ACCESS,
|
|
424
|
+
f"{decision.action} on {_describe(resource)} by rule {decision.rule}",
|
|
425
|
+
aws_event=getattr(handler, "aws_event", None),
|
|
426
|
+
context=context,
|
|
427
|
+
user_email=actor,
|
|
428
|
+
)
|
{velocity_python-0.2.1 → velocity_python-0.2.2}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
@@ -13,7 +13,7 @@ import re
|
|
|
13
13
|
from io import BytesIO
|
|
14
14
|
from types import MappingProxyType
|
|
15
15
|
|
|
16
|
-
from velocity.aws import dirty_pipeline, ssm_config
|
|
16
|
+
from velocity.aws import authz, dirty_pipeline, ssm_config
|
|
17
17
|
from velocity.aws import facets
|
|
18
18
|
from velocity.aws.handlers.exceptions import AccessError, AlertError, BadRequestError
|
|
19
19
|
from velocity.aws.handlers.mixins.client_input import MODES, STRICT, ClientInputGuard
|
|
@@ -72,6 +72,9 @@ def _fk_blocked_delete_message(table, exc):
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
75
|
+
# Owner-scoped tables already warned about under the legacy client SQL mode
|
|
76
|
+
# (once per container, not once per request).
|
|
77
|
+
_OWNER_SCOPE_WARNED = set()
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
TEXT_TYPES = frozenset(
|
|
@@ -695,6 +698,44 @@ class DataServiceMixin:
|
|
|
695
698
|
client_denied_tables = frozenset()
|
|
696
699
|
client_allowed_tables = None
|
|
697
700
|
|
|
701
|
+
# The authorization decision point (velocity.aws.authz) at the client
|
|
702
|
+
# table gate. ``"off"`` (default): the gate is as above. ``"log"``: every
|
|
703
|
+
# table is also put to the rules and what they would refuse is logged,
|
|
704
|
+
# nothing is refused. ``"enforce"``: a table no rule permits is refused.
|
|
705
|
+
# The ``AuthzMode`` config key overrides this per environment. Out of the
|
|
706
|
+
# box the rules permit every ordinary table and NO system table
|
|
707
|
+
# (``sys_…``): a handler that serves one in a grid declares it, e.g.
|
|
708
|
+
# authz_rules = (authz.permit("flags.admin", authz.DATA_ACCESS,
|
|
709
|
+
# when=authz.all_of(authz.table("sys_feature_flags"),
|
|
710
|
+
# authz.holds("site.admin"))),)
|
|
711
|
+
# ``principal_permissions(current_user)`` says which permissions the
|
|
712
|
+
# signed-in user holds; the default is none.
|
|
713
|
+
authz_mode = authz.OFF
|
|
714
|
+
authz_rules = ()
|
|
715
|
+
|
|
716
|
+
def principal_permissions(self, current_user):
|
|
717
|
+
return ()
|
|
718
|
+
|
|
719
|
+
# Tables whose every row belongs to ONE signed-in user: ``{table: owner
|
|
720
|
+
# column}``. The owner column holds the user's sign-in email as the
|
|
721
|
+
# session carries it. Each hooked action (query, find, read, write,
|
|
722
|
+
# delete) confines the caller to their own rows, and the direct actions
|
|
723
|
+
# refuse the table — the rule ``tenant_scope`` gives a site, applied by
|
|
724
|
+
# the framework itself and resolved BEFORE the site's ``tenant_scope``,
|
|
725
|
+
# so an override that forgets ``super()`` cannot switch it off.
|
|
726
|
+
#
|
|
727
|
+
# ``user_data`` is here because the framework owns it: per-user settings
|
|
728
|
+
# written by ``store-user-data`` and read back by the browser through the
|
|
729
|
+
# generic ``query``, which until this rule let any signed-in user read or
|
|
730
|
+
# overwrite another user's rows by naming their ``userid``.
|
|
731
|
+
#
|
|
732
|
+
# Enforced under the strict client SQL mode. Under legacy mode the
|
|
733
|
+
# browser can send raw SQL around any row predicate, so nothing here can
|
|
734
|
+
# be a boundary: the rule is skipped and a warning is logged, leaving
|
|
735
|
+
# legacy behaviour exactly as it was. Set the attribute to ``{}`` to opt
|
|
736
|
+
# a handler out.
|
|
737
|
+
owner_scoped_tables = MappingProxyType({"user_data": "userid"})
|
|
738
|
+
|
|
698
739
|
# How much SQL-shaped input the boundary accepts from the browser
|
|
699
740
|
# (velocity.aws.handlers.mixins.client_input): "strict" (Contract C — dict
|
|
700
741
|
# filters, plain columns, structured @@search) or "legacy" (string
|
|
@@ -795,7 +836,9 @@ class DataServiceMixin:
|
|
|
795
836
|
if table in self.client_denied_tables:
|
|
796
837
|
return False
|
|
797
838
|
allowed = self.client_allowed_tables
|
|
798
|
-
|
|
839
|
+
if allowed is not None and table not in allowed:
|
|
840
|
+
return False
|
|
841
|
+
return authz.gate(self, authz.DATA_ACCESS, authz.Resource(authz.TABLE, table))
|
|
799
842
|
|
|
800
843
|
def _assert_client_table(self, table, label="table"):
|
|
801
844
|
"""The table gate every client action passes (SEC 4.2)."""
|
|
@@ -914,9 +957,12 @@ class DataServiceMixin:
|
|
|
914
957
|
)
|
|
915
958
|
|
|
916
959
|
def _assert_direct_unscoped(self, table, context):
|
|
917
|
-
"""A tenant-scoped table is never reachable through a
|
|
918
|
-
(they cannot apply the scope)."""
|
|
919
|
-
if
|
|
960
|
+
"""A tenant-scoped or owner-scoped table is never reachable through a
|
|
961
|
+
direct action (they cannot apply the scope)."""
|
|
962
|
+
if (
|
|
963
|
+
self._owner_scope(context, table) is not None
|
|
964
|
+
or self.tenant_scope(context, table) is not None
|
|
965
|
+
):
|
|
920
966
|
raise AlertError(
|
|
921
967
|
f"Direct access to '{table}' is not permitted; "
|
|
922
968
|
"use the standard data actions."
|
|
@@ -995,11 +1041,47 @@ class DataServiceMixin:
|
|
|
995
1041
|
"""
|
|
996
1042
|
return None
|
|
997
1043
|
|
|
1044
|
+
def _owner_scope(self, context, table):
|
|
1045
|
+
"""``(column, values)`` confining ``table`` to the signed-in user, or
|
|
1046
|
+
``None`` when the table is not in :attr:`owner_scoped_tables` — or
|
|
1047
|
+
when this handler runs the legacy client SQL mode, where no row
|
|
1048
|
+
predicate is a boundary (see the attribute's comment).
|
|
1049
|
+
|
|
1050
|
+
The values are the session's email as it stands and lowercased: rows
|
|
1051
|
+
written before emails were normalized keep matching. The first value
|
|
1052
|
+
is the one stamped on a new row. A session without an email is
|
|
1053
|
+
refused."""
|
|
1054
|
+
column = (self.owner_scoped_tables or {}).get(table)
|
|
1055
|
+
if not column:
|
|
1056
|
+
return None
|
|
1057
|
+
if self.client_sql_mode_effective != STRICT:
|
|
1058
|
+
if table not in _OWNER_SCOPE_WARNED:
|
|
1059
|
+
_OWNER_SCOPE_WARNED.add(table)
|
|
1060
|
+
logger.warning(
|
|
1061
|
+
"%s rows are not confined to their owner: this handler "
|
|
1062
|
+
"runs the legacy client SQL mode, under which no row rule "
|
|
1063
|
+
"can be enforced. Move it to strict mode.",
|
|
1064
|
+
table,
|
|
1065
|
+
)
|
|
1066
|
+
return None
|
|
1067
|
+
session = context.session() if hasattr(context, "session") else None
|
|
1068
|
+
email = (session or {}).get("email_address")
|
|
1069
|
+
if not isinstance(email, str) or not email.strip():
|
|
1070
|
+
raise AlertError("Access denied: no signed-in user for this session.")
|
|
1071
|
+
email = email.strip()
|
|
1072
|
+
return column, tuple(dict.fromkeys((email, email.lower())))
|
|
1073
|
+
|
|
998
1074
|
def _resolve_tenant_scope(self, tx, table, context):
|
|
999
1075
|
"""``(column, values)`` for ``table``, or ``None`` (unscoped). Fails
|
|
1000
1076
|
closed: a scope under any client SQL mode but strict, a malformed
|
|
1001
|
-
scope, an empty value, or a table without the column is refused.
|
|
1002
|
-
|
|
1077
|
+
scope, an empty value, or a table without the column is refused.
|
|
1078
|
+
|
|
1079
|
+
A table in :attr:`owner_scoped_tables` takes the framework's owner
|
|
1080
|
+
scope; the site's :meth:`tenant_scope` is not consulted for it."""
|
|
1081
|
+
scope = self._owner_scope(context, table)
|
|
1082
|
+
owned = scope is not None
|
|
1083
|
+
if scope is None:
|
|
1084
|
+
scope = self.tenant_scope(context, table)
|
|
1003
1085
|
if scope is None:
|
|
1004
1086
|
return None
|
|
1005
1087
|
# The predicate is only a boundary when the client cannot send raw
|
|
@@ -1030,7 +1112,12 @@ class DataServiceMixin:
|
|
|
1030
1112
|
except Exception: # noqa: BLE001 — unknown schema is not scoped schema
|
|
1031
1113
|
available = set()
|
|
1032
1114
|
if column.lower() not in available:
|
|
1033
|
-
|
|
1115
|
+
# An owner-scoped table that does not exist yet (user_data on a
|
|
1116
|
+
# fresh site, before anyone has saved a setting) has no rows to
|
|
1117
|
+
# confine: keep the scope and let the read come back empty, as it
|
|
1118
|
+
# did before. A table that exists WITHOUT the column is refused.
|
|
1119
|
+
if not (owned and not available):
|
|
1120
|
+
raise AlertError(f"Access denied: {table} is not tenant-scoped.")
|
|
1034
1121
|
return column, values
|
|
1035
1122
|
|
|
1036
1123
|
@staticmethod
|
|
@@ -1066,10 +1153,12 @@ class DataServiceMixin:
|
|
|
1066
1153
|
f"Access denied: {column} must be one of your own values."
|
|
1067
1154
|
)
|
|
1068
1155
|
if not keys and creating:
|
|
1069
|
-
if len(values) != 1:
|
|
1156
|
+
if len(values) != 1 and table not in (self.owner_scoped_tables or {}):
|
|
1070
1157
|
raise AlertError(
|
|
1071
1158
|
f"Access denied: choose the {column} for this {table} record."
|
|
1072
1159
|
)
|
|
1160
|
+
# an owner scope's values are one person's email in two spellings:
|
|
1161
|
+
# the first is the session's own
|
|
1073
1162
|
incoming[column] = values[0]
|
|
1074
1163
|
|
|
1075
1164
|
def _check_tenant_row(self, table, scope, row):
|