KWIP

KWIP Technology

Guarantees and limitations

PyScoped 2.0 documentation.

This document describes PyScoped 2.0.0a1. It supersedes the universal invariants in legacy/v1/. An alpha is available for review and integration testing, not a claim of established production reliability or legal compliance.

Enforcement boundary

The caller is trusted application code. It authenticates actors and authorizes scope membership before entering context. All members admitted into a scope share the library's scope-level read/write access. Additional roles, object permissions, sharing rules, and membership revocation checks remain in the application.

An enforced model's scoped root queryset contains a predicate on the existing scope column. Joins from that queryset into other enforced models receive additional ON-clause scope predicates; outer joins retain parents with no visible related row. Supported materialization, aggregates, counts, and existence checks retain that predicate. Querysets/subqueries cannot be evaluated under another context, even if their earlier results are cached. Supported instance writes check the persisted row and proposed scope before mutation. Normal scope changes fail. A request context is an authorization snapshot; immediate revocation during a long request/stream is not promised. Reauthorize at application boundaries as necessary.

This does not protect raw SQL/RawSQL expressions, manually constructed plain QuerySets, direct use of _base_manager or other private APIs, custom methods that bypass or override the guards, unmanaged external database writers, historical migration model operations, or unregistered models. An object already returned to application code cannot be made unreadable by a context change. FK and one-to-one descriptors between enforced models use scoped target queries and recheck context even when the related object is cached. Relationships from unregistered/audit-only models and direct use of Django's private base manager remain outside this boundary. No database RLS or sandboxing of arbitrary Python code is claimed.

Mutation and audit contract

Supported mutations are ordinary model save (including partial and expression updates), scoped queryset update, model/queryset deletion, and Django cascades. Every registered write needs an actor context in both audit and enforcement modes. Failure to write the audit event rolls back the application write. An enclosing transaction can roll both back. Custom model save logic is wrapped transactionally; external side effects still need application on_commit/outbox handling.

Queryset update, ordinary bulk_create, and bulk_update retain native Django SQL and save-signal semantics: no custom save methods or save signals are invoked. They lock/snapshot affected persisted rows and append per-row events in the same database transaction. Affected rows are materialized. Queryset update accepts direct concrete- field expressions; custom annotation aliases as update values are outside the initial contract. Conflict-handling bulk inserts and duplicate bulk-update identities are rejected rather than producing ambiguous history. These rules include async variants. Skipped no-op save(update_fields=[]) has Django's existing no-write behavior. A save that writes unchanged values still records an attributed event.

Deletion follows Django's normal deletion semantics, with retained history. Scope- mismatched cascades are rejected. Unsupported collector field updates (SET_NULL, SET_DEFAULT, custom on_delete) and implicit M2M configurations fail checks. PyScoped does not claim universal soft deletion or resurrection.

Audit snapshots include only explicitly selected concrete fields, using actual persisted values after save. An allowlist avoids copying everything automatically; it is not a secret detector. Snapshots are not encrypted by this package. Configure application/database access, retention, backups, and encryption to fit your needs. Actor IDs are references supplied by trusted application code, not copied users.

History integrity and recovery

Each (model label, primary key) history has a revision counter and SHA-256 hash chain, locked on the same database as the application row. History cannot be reassigned to another scope when a PK is reused. Public event mutation helpers reject edits, but administrative SQL remains able to change data. Verification checks revisions, hashes, scope, and the retained head; it detects a removed tail when the head remains. An administrator who replaces both the events and head can forge a new consistent chain. Keep independent trusted checkpoints if that threat is in scope.

history() returns eagerly authorized events. The Django AuditEvent and Resource models are internal administrative storage interfaces, not tenant-facing query APIs. Never expose them directly through admin/API endpoints without your own authorization.

Backfill records a baseline observed now, commits per row, and is idempotent. It does not claim old events or automatic reconciliation of untracked changes. Restore checks the history, expected revision, and current selected values; applies selected scalar fields to a live row; and appends a new restore event. It does not change PKs, scope, relationships, unselected fields, deleted rows, or external state. A changed audit field schema requires explicit history migration before restoration.

Evidence and limits

The release record lists exact tested versions and results. No external adoption, production audit, or performance result is inferred from unit tests. PostgreSQL is the concurrent-write target; SQLite is the local development target. Other database engines and future Django releases are not supported until verified.

Download Markdown · All documentation