Skip to content

Releases: muzakkirhussain011/wardowl

wardowl 2.1.0

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 24 Oct 18:57
bf25bd0

Fixes found by real-world acceptance scenarios, run against real servers, a real identity provider and real models: a SpiceDB setting that let a banned user read through their group, a permission service's error text reaching models, Ory Keto's batch limit, stale redaction spans failing every retrieval, Chroma's batch limit, and the example gateway's streaming and OpenAI content parts. The scenarios join the project and run weekly against the release on PyPI. SpiceDB's group_subjects and Keto's batch_size change their defaults; see Security and Fixed.

Added

  • Acceptance scenarios (acceptance/): the applications a company builds with wardowl, run from the documentation against real servers, a real identity provider (Keycloak), real embedding and reranking models, and a local LLM, with every result checked against an oracle of who may read what that does not use wardowl. They cover 14 stores, seven permission systems, LangChain, LlamaIndex, Haystack and MCP, the HTTP service, day-2 operations with servers stopped mid-traffic, redaction and reranking, a team's own store and policy, and the example gateway: 3,912 checks, run weekly against the release on PyPI. A Kaggle notebook runs the assistant at scale on GPUs.

  • wardowl.InvalidQuery, a ValueError for a query that cannot be searched through its own fault, raised by Query.embedded, Query.keywords, the retriever's checks of k, Vespa's keyword mode and FaissBackend (a query vector whose length is not dim). Code that catches ValueError keeps working; a backend of your own can raise it.

  • OIDCAuthenticator(ssl_context=...) fetches the identity provider's keys with that context, for a provider whose certificate a private CA signed, instead of a process-wide SSL_CERT_FILE.

  • InMemoryBackend.delete(ids), as every other backend has; ids not stored are ignored.

  • ChromaBackend(batch_size=...).

Security

  • Example gateway (examples/gateway): the audit record links the records of one question by an HMAC-SHA256 under a key derived from RAMPART_KEY_SECRET (question_hmac) instead of an unkeyed SHA-256 (question_sha256), which anyone who could read the log could reverse when the only unknown in a question was a masked phone or ID number, by hashing every candidate. rampart ingest and rampart verify-audit now need RAMPART_KEY_SECRET (or RAMPART_DEMO=1), as rampart serve already did. A request from a disabled principal is now audited, without anything it carried, and counted as account_disabled; it was refused without a record, like a request with an unknown key. Every read of the audit log, through /v1/audit, /v1/audit/verify or the dashboard, is audited too.

  • SpiceDBAuthorizer checks only the user by default (group_subjects=False, was True), and SpiceDB follows the group memberships it stores. With groups checked as subject sets of their own, a chunk was allowed when the user or any group had the permission, so under an exclusion such as permission view = viewer - banned a banned user still read a document granted to their group (the group is not banned), and under an intersection a grant through a group was refused. If your group memberships live only in the token and your permissions neither exclude nor intersect, pass group_subjects=True to keep the old behaviour; otherwise write the memberships to SpiceDB (group:<name>#member@user:<id>). A real-SpiceDB test checks that a banned member of a permitted group is denied.

  • AuthorizationError messages are fixed and never carry the text of the exception behind them, which stays the error's __cause__ (and so in logged tracebacks; RetrievalEvent.cause still names its type). The retriever wrapped an authorizer's or redactor's exception as authorizer failed: <its text>, so a failure such as could not reach opa.internal:8181 as svc-wardowl (password hunter2) reached a model through Haystack's Agent, which shows tool errors to it, and through LangChain and LlamaIndex agents. The same holds for the LlamaIndex postprocessor, the HTTP client the permission services share (which now names the exception's type, not its text), Casbin, Cedar, Cerbos roles, OPA's filter, SpiceDB's caveat_context and the OIDC key fetch.

Fixed

  • Example gateway: /v1/chat/completions accepts a message's content as OpenAI's list of text parts, joined with newlines, as well as a string; it refused the list with 422, so SDKs and chat UIs that send it got no answer. The 4,000-character limit applies to the joined text, a message holds at most 50 parts, and a part that is not text (an image, audio, a file) is refused with 422 naming its type.

  • Example gateway: stream: true on /v1/chat/completions answers in OpenAI's server-sent events (chat.completion.chunk objects, a last chunk with finish_reason and the rampart block, a usage chunk with stream_options.include_usage, then data: [DONE]). It was ignored and answered with one JSON body, which streaming clients could not read. The answer is checked in full first and then streamed in pieces, so the policy, masking, audit and metrics are those of any other answer, and a denied request is the usual 403.

  • Example gateway: denied ingests and audit reads are counted in rampart_policy_denials_total, as ingest_not_allowed and audit_not_allowed; only denied questions were, although the reason codes existed. The dashboard's denials by reason count every action too.

  • Example gateway: Compose passes the gateway's settings through, with the gateway's defaults. RAMPART_LLM_API_KEY, needed for a model server that wants a key, RAMPART_MAX_BODY_BYTES, the embedding model and dimension and the tuning settings could not be set. RAMPART_LOCAL_MODEL makes the local-llm profile pull a smaller chat model and serve it under the name the policy approves. RAMPART_DEFAULT_MODEL is gone: nothing read it, since the policy chooses the model.

  • KetoAuthorizer sends 10 tuples per batch check by default (batch_size=10, was 100). Keto refuses more than limit.max_batch_check_size tuples, 10 unless configured, with HTTP 400, so with both defaults every check of more than 10 chunks failed, closed. Raise both together for larger batches. The real-Keto CI tests now send more than 10 tuples.

  • SpanRedactor hides a chunk whole when one of its spans ends past the text, as when the text was shortened after its spans were written, as it does for a sha256 mismatch. It used to raise, so every retrieval that reached the chunk failed, for its readers too. Spans that are wrong whatever the text (not two integers with 0 <= start < end, and the other malformed cases) still fail the retrieval.

  • ChromaBackend writes in batches of at most batch_size records: the client's own maximum (get_max_batch_size(), about 5,400 for a local client) by default, or 1,000 when the collection reports none. add, delete and set_metadata sent everything in one call, which Chroma refuses above its maximum. Every record is checked before the first batch is written.

  • An open CircuitBreaker raises AuthorizationError caused by the new wardowl.authorizers.CircuitOpenError, so RetrievalEvent.cause is "CircuitOpenError". It had no cause, which monitoring could not tell from a malformed answer.

  • The HTTP service answers 422, echoing nothing, for a query it cannot search as sent (the new wardowl.InvalidQuery): k over the retriever's max_candidates, only a vector for a keyword backend, only text for a backend without an embed function, or a vector of the wrong length for FaissBackend. These answered 500. Other errors remain server faults and answer 500.

Documentation

  • Concepts says what each strategy sends to the store: post-filtering searches inside the authorizer's scope when it has one, and for an AllOf whose parts all give filters that scope is the whole filter, even with strategy="postfilter". The authorizer table lists every built-in authorizer. Combining and the Chroma page's speed advice say the same.
  • Combining shows TenantAuthorizer's attribute as it is (None, meaning the field's name) and how one chunk serves several tenants through a list-valued tenant field.
  • Vespa: namespace only names document paths, a search covers the whole schema, and tenants are kept apart by the permission filter or by a schema each. Redis: each index needs its own prefix, and clients need socket timeouts. Qdrant: index tenant in multi-tenant collections. Cassandra: create_table does not create the keyspace. Chroma, Elasticsearch and OpenSearch: searches are approximate, with what tunes their recall.
  • Hybrid search lists Vespa's keyword mode, and Your own vectors covers LanceDB, FAISS, Vespa, Cassandra and Pinecone and what a query vector of the wrong size raises.
  • Redaction warns that a span's sha256 of a short value can be recovered by guessing, so the spans must not reach callers.
  • Extending says check_authorizer proves consistency, not a correct policy, and shows a test of known cases.
  • SpiceDB explains why group_subjects=True fails under an intersection or an exclusion, and what to do instead. Casbin says what a custom request skips and must do itself. Keto and OpenFGA show how to keep tenants apart in one server. OPA recommends files or bundles over policies uploaded through its API.
  • Operations, Security and the integrations overview name all five HTTP authorizers, and the shared cache's Redis client gets timeouts. Revocation covers Keto, Cedar and Casbin.
  • HTTP service: mapping a real provider's tokens, with Keycloak's sub, group paths, admin-only claims and audience mapper. LangChain: a per-call subject= wins over for_subject. Haystack: what a model can set on the agent tool, inputs_from_state, and outputs_to_state for getting the documents back. MCP: a stdio server for a signed-in user. Agent frameworks: the mcp 2 client takes it...
Read more

wardowl 2.0.1

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 24 Oct 14:51
f2f9a2b

The wardowl logo arrives, on PyPI, in the README and in 3D on the documentation's front page, and the build tools in the lock file move past seven advisories.

Documentation

  • The README, PyPI page and documentation carry the wardowl logo, a midnight and gold owl guarding a document, with a favicon and a social card. The documentation's front page shows it in 3D: the owl watches, and every ten seconds the redaction bar on its document lifts for a grant or stays for a denial. The model loads after the page, from a 1.5 MB file, and the still image stays for reduced motion, saved data or browsers without WebGL.
  • The security policy, the upgrade guide and the changelog describe needtoknow as it now is on PyPI: archived, with a last release that only installs wardowl.

Security

wardowl 2.0.0

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 24 Oct 12:50
05c3107

The first release named wardowl: until 1.6.0 the project was needtoknow. It adds the Pinecone backend and the fixes from the project's own security audit of every module, and it is a major release because several of those fixes change behaviour that 1.x code may rely on. Upgrading lists what to change.

Changed

  • The project is renamed wardowl. Install it with pip install wardowl and import wardowl. The repository and the documentation moved to github.com/muzakkirhussain011/wardowl and muzakkirhussain011.github.io/wardowl (the old addresses redirect). Everything named after the project changes with it: classes (NeedToKnowRetriever is now WardowlRetriever), environment variables (NEEDTOKNOW_* is now WARDOWL_*), the default cache namespace and metric names, and the field names the backends store (needtoknow_terms, needtoknow_id and needtoknow_metadata are now wardowl_terms, wardowl_id and wardowl_metadata), so collections written by needtoknow must be indexed again. needtoknow 1.6.0 remains on PyPI and gets no further releases.

  • Shared-cache keys changed, so entries written by an earlier version are not read after an upgrade, and during a rolling upgrade an invalidate() made by one version does not clear the other's entries: call invalidate() once the upgrade is done, or allow for ttl.

  • The operations guide's set_metadata example merges the stored metadata instead of replacing it with the changed permission alone.

  • The public filter translators raise the new wardowl.backends.NothingMatches (a ValueError) for a filter that can match nothing, instead of returning None or {}, which store clients read as no filter at all: filter_to_query (Elasticsearch, OpenSearch, Redis), filter_to_where (Chroma), filter_to_qdrant, filter_to_mongo, filter_to_search, filter_to_expression (Milvus), filter_to_weaviate, filter_to_sql (LanceDB), filter_to_yql and filter_to_pinecone. Chroma's NEVER constant is removed. The backends still return no hits for such a filter without asking the store.

  • A search with k above what the store returns in one query raises ValueError instead of returning that many hits as if no more matched: 10,000 for Elasticsearch, OpenSearch, MongoDB Atlas Vector Search and Pinecone, and 1,000 for Cassandra.

  • PineconeBackend waits 30 seconds by default (wait=30.0, was None) for a search to see each write, so a revoked permission or a deleted chunk is out of searches when the call returns. wait=None keeps the old behaviour.

  • Haystack: WardowlRetriever's subject input also accepts a function returning the Subject, called on every run. Because the input accepts any type, a connection to the component by component name alone is ambiguous: name the sockets, as in pipeline.connect("embedder.embedding", "retriever.query_embedding").

Added

  • PineconeBackend (the pinecone extra): dense vector search on a Pinecone index through its data plane API (2026-07), with the permission filter as the query's metadata filter: $in on hex-encoded, type-tagged terms, joined with $and and $or, so Pinecone applies it inside the search. wait= makes writes wait until a search sees them, for serverless indexes' eventual consistency; ids that Pinecone cannot store are kept under their SHA-256; the API key never appears in errors. Tested with the conformance kit against a Pinecone serverless index.

Security

  • CachingAuthorizer(shared=...) requires an explicit namespace. Shared-cache keys held only the namespace, subject, kind, action and chunk, and the namespace defaulted to "wardowl", so two caching authorizers sharing one Redis answered for each other: AllOf(CachingAuthorizer(TenantAuthorizer(), shared=r), CachingAuthorizer(OpenFGAAuthorizer(...), shared=r)) skipped OpenFGA. A process also refuses, with ValueError, a second kind of authorizer under a Redis client (or SharedCache) and namespace it already uses.

  • Cache keys are exact. Their parts were joined with NUL and no lengths, values were encoded with json.dumps(default=repr), and this process's keys compared subjects with ==, so {1} and "{1}", the metadata keys 1 and "1", lists and tuples, the attributes 1, True and 1.0, and an action and chunk id containing NUL could share a cached decision. Keys are now digests of a length-prefixed, type-tagged encoding, and a subject or chunk holding a value of a type that has no exact encoding is not cached: the authorizer is asked every time.

  • A shared-cache entry whose expiry is NaN or infinite is a miss. float() accepted both, and a NaN expiry gave a local copy that never expired. Non-finite or non-positive lifetimes are never stored, and ttl and local_ttl must be finite.

  • SpanRedactor hides the whole text of a chunk without its spans field (required=True by default; [] still means nothing to hide, and required=False restores the old behaviour). A missing field showed the chunk in full, and set_metadata, which replaces the metadata, dropped the field whenever permissions were updated without it. A span may now carry sha256 (wardowl.redaction.span_digest of the text it covers); a chunk whose text no longer matches is hidden whole.

  • Haystack: BoundWardowlRetriever takes the subject from subject_provider, a function such as one reading a contextvar the web layer sets, and has no subject input. WardowlRetriever takes the subject as a run input, so ComponentTool offered it to an agent's model, which could search as anyone; agents must use the bound component.

  • The LangChain, LlamaIndex and Haystack retrievers return no chunk metadata unless named in metadata_fields, as the HTTP service and the MCP tool already did. They passed every key, allowed_users, allowed_groups and tenant included, into the documents a model, prompts, callbacks and traces receive. LangChain documents keep relevance_score.

  • Subject.attributes is a deep copy that cannot be changed (read-only dicts, tuples, frozensets), and a subject's id and groups must be strings. Changing the attributes of a subject already used as a cache key, directly or through a list the caller still held, could hand a later subject the decision cached for the original.

  • FaissBackend.add keeps only the last document of an id repeated in one call. Each copy used to get its own slot, and the earlier copies stayed in the index, out of reach of delete and set_metadata, so searches kept returning them with their old metadata after a revocation.

  • QdrantBackend and WeaviateBackend derive every point or object id from the SHA-256 of the chunk id (point_id), a chunk id that is already a UUID included. A UUID used to be kept as it was, so another spelling of it (upper case, braces, urn:uuid:), or an id chosen to equal the UUID derived from another, landed on the same point. set_metadata and delete now act only on a point whose stored wardowl_id is the id asked for (Qdrant: a filter on the payload; Weaviate: the property, checked before each update and required by each delete), so they never change or remove a chunk that holds another id. A point or object written by another tool, without wardowl_id, is reached by its UUID written exactly as search results report it. Collections must be indexed again, as the rename already requires.

  • PineconeBackend stores a chunk id that starts with sha256- under its SHA-256, like ids Pinecone cannot hold, so no chunk id can share a record with another's hashed id. set_metadata and delete fetch the records first (GET /vectors/fetch) and leave alone any record that holds another chunk id.

  • MilvusBackend.delete and set_metadata select rows with id in {ids} and the ids as a filter parameter. pymilvus's get(ids=...) and delete(ids=...) write string ids into the expression between quotes, so an id such as a','b selected the chunks a and b as well.

  • set_metadata never writes a whole chunk back. Milvus updates only the metadata and terms with a partial update (Milvus 2.6.2 or newer), LanceDB with an update-only merge of those columns, Chroma with an update (removing stale list fields in the same call), and Qdrant sets only the payload's metadata key. Before, a chunk deleted while set_metadata ran could be inserted again with its old text, and a concurrent add could have its new text replaced by the old.

  • CassandraBackend.set_metadata uses plain writes, like add and delete, instead of a lightweight transaction. Cassandra orders writes to a row by timestamp, and the transaction's Paxos timestamp does not order with the client timestamps of plain writes, so a permission change could be applied over a later one. Ids are looked up first, so an id that is not stored still creates no row; a chunk deleted between the lookup and the update is left without text or vector, which an ANN search never returns.

  • PgVectorBackend matches an integer value only where the stored number reads as that integer, as the reference semantics do: jsonb compares numbers by value, so In("level", [1]) used to match {"level": 1.0}. Metadata keys that are not strings are refused, since JSON turned the key 1 into "1", which a filter on the field "1" then matched.

  • Chroma's filter_to_where refuses field names that contain :: or equal wardowl_metadata, which addressed the stored list fields or the JSON copy rather than a metadata field.

  • CedarAuthorizer no longer leaves out metadata values Cedar cannot hold. A float, None or an integer beyond 64 bits used to vanish from a chunk's attributes, so a forbid ... when { resource has x && ... } stopped applying and a permit could let the chunk through (a sensitivity of 3.0 from a store that returns every number as a float, say). Integers, and floats without a fraction, now become Ce...

Read more

needtoknow 1.6.0

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 23 Oct 15:41
f46870a

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

Vespa and Apache Cassandra join the stores, each tested with the conformance kit against a real server in CI. Vespa applies the permission filter inside its nearest-neighbor and BM25 search; Cassandra applies it in the WHERE of each ANN query, with every value bound as a parameter.

Added

  • CassandraBackend (the cassandra extra): vector search on Apache Cassandra 5.0 with Storage-Attached Indexes. The filter becomes an OR of ANDs on type-tagged terms in an indexed set<text>; each AND is one ANN query with bound CONTAINS parameters, run concurrently, and the best k are kept; past max_queries, one query on the terms every match needs is filtered in the process. set_metadata uses a lightweight transaction so an id that is not stored creates no row; create_table creates the table and indexes. Tested with the conformance kit against Cassandra 5.0.9. Writes go 4 at a time (write_concurrency), as Cassandra 5.0's vector index accepts few concurrent writes, and a refused write is retried once.
  • VespaBackend (the vespa extra): nearest-neighbor or BM25 search on Vespa through its HTTP APIs, with the permission filter in the YQL query as conditions on hex-encoded, type-tagged terms, so Vespa applies it inside the search and no metadata value can change the query. HNSW or exact search; schema(dim) writes the schema and examples/vespa is a complete application package. Tested with the conformance kit against Vespa 8.763.13, in vector, keyword and hybrid search.

Full changelog: https://github.com/muzakkirhussain011/wardowl/blob/main/CHANGELOG.md

needtoknow 1.5.0

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 23 Oct 13:06
50a2f3c

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

Applications in any language can now use needtoknow through an HTTP service that takes the user from a verified OIDC token. Redaction hides the parts of a permitted chunk a user may not see, before a reranker or a model reads it. Ory Keto and Casbin join the permission systems, and FAISS joins the stores, with exact pre-filtering in the process.

Added

  • FaissBackend (the faiss extra): vector search with FAISS in the process. The permission filter is matched against the chunks' metadata and passed to FAISS as an ID selector, so pre-filtering is exact. Exact search by default, or any trained index that supports ID selectors; add, delete, set_metadata, and save and load to a directory. Passes the conformance kit.
  • CasbinAuthorizer (the casbin extra): decisions from the Casbin enforcer you already use, in the process, with batch_enforce. Requests are (subject id, object, action), or your own for ABAC models; group_subjects=True also asks with each of the subject's groups, for allow-only models. An enforcer that raises or answers with anything but booleans raises AuthorizationError. Tested against the reference answer on generated corpora and with the conformance kit.
  • KetoAuthorizer: relationship checks against Ory Keto (POST /relation-tuples/batch/check), with no extra dependency. Each chunk's document is a Keto object; group_subjects=True also checks the subject's groups as subject sets. Keto has no reverse lookup, so the retriever post-filters. A result carrying an error, a malformed answer or a wrong number of results raises AuthorizationError. Tested against Keto 26.2.0 and with the conformance kit.
  • An HTTP service, needtoknow.server (the server extra), so applications in any language get permission-aware retrieval: create_app(retriever, authenticate) serves POST /v1/retrieve, GET /healthz and an OpenAPI description. The subject comes from the verified bearer token alone; OIDCAuthenticator verifies JWTs against the identity provider's published keys (asymmetric algorithms only, issuer, audience, expiry, an optional tenant claim). Unknown request fields are refused, a failed permission check answers 503 without details, and hits carry no metadata unless named.
  • Redaction: PermissionedRetriever(redactor=...) hides parts of the chunks a subject may read, after the permission check and before a reranker or a model sees them. SpanRedactor hides spans listed in a chunk's metadata, each with its own readers; PatternRedactor hides text matching patterns unless the subject is in an exempt group. A redactor may change text and metadata but not which hits are returned, and one that fails makes the retrieval raise AuthorizationError.

Full changelog: https://github.com/muzakkirhussain011/wardowl/blob/main/CHANGELOG.md

needtoknow 1.4.0

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 22 Oct 14:36
e3d3730

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

Nine of the ten stores now serve both halves of hybrid search under the permission filter, Cedar joins the permission systems, and every package carries SLSA build provenance.

Highlights

  • Keyword search (BM25) on every store but Chroma. Now on Qdrant, Milvus, MongoDB Atlas, Weaviate, Redis and LanceDB too, under the same permission filter as vector search. One index serves both halves of RankFusion hybrid search.
  • Cedar authorizer (needtoknow[cedar]). Cedar policies are evaluated in the process. A chunk whose evaluation errors is denied, never allowed.
  • Permission sync from Google Drive. Who may read a file, written onto its chunks for MetadataACLAuthorizer.
  • Operations:
    • RetrievalEvent.cause names the exception behind a failure.
    • CachingAuthorizer.stats() reports the cache's hit and failure counts.
    • OpenFGAAuthorizer(consistency=...) chooses OpenFGA's consistency mode.
    • Redis can use an exact FLAT index.
  • Security fixes found by review:
    • recheck=False now matches hits against the filter locally.
    • A deadline-cut OpenFGA list is no longer used as a complete filter.
    • A Cerbos groups attribute can no longer replace the subject's groups.
    • Cancelled retrievals now produce an audit event.
    • The gateway example hashes API keys with scrypt.
  • For reviewers and operators: a threat model (17 threats), the security invariants with their tests, revocation and consistency, an incident runbook, a compliance mapping (ISO/IEC 27001, SOC 2, GDPR, HIPAA, OWASP LLM08), agent-framework guides, and a versioning and deprecation policy.
  • Supply chain: SLSA provenance on every package. After release, a workflow installs it from PyPI and runs the security test suites from the attested sdist.

Python 3.11 or newer

Python 3.10 is no longer supported. It reaches its end of life on 2026-10-31.

pip install --upgrade "needtoknow>=1.4"

Full changelog: https://github.com/muzakkirhussain011/wardowl/blob/main/CHANGELOG.md#140---2026-10-22

needtoknow 1.3.3

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 21 Oct 23:21
997c5ad

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

Closes three gaps that remained in CachingAuthorizer.invalidate() after 1.3.2, found by a review of the code. Upgrade if you use a shared cache and call invalidate(). Advisory GHSA-prfv-2pwr-5rx2 covers this fix.

Security

  • In rare interleavings, a decision revoked with invalidate() could still be used afterwards, for up to ttl. An invalidate() of everyone deleted every id's epoch, including one that a concurrent invalidate() of that id had just written, so a retrieval in flight could store its earlier answer where every replica accepted it. And in the process calling invalidate(), a read of the global epoch already in flight could put back the epoch it replaced, for up to local_ttl. Epochs now live outside the keys that invalidation deletes, and a read never replaces a newer epoch.
  • An invalidate() of everyone reached other replicas within twice local_ttl, not within local_ttl as documented: an entry accepted against a copy of the global epoch was kept for another local_ttl. Copies now expire with that epoch.

Fixed

  • Epochs are stored as hex text. Random bytes broke shared caches whose Redis client decodes responses (decode_responses=True): reads raised UnicodeDecodeError, so every lookup asked the permission service and logged a warning.
  • invalidate() raises TypeError for an id that is not a string: invalidate(42) for the id "42" missed this process's own entries.
  • A retrieval event that cannot be built is logged and dropped, instead of replacing the retrieval's result or error, and query fingerprints accept a lone surrogate.

Changed

  • An id's epoch has a new key in the shared cache. During a rolling upgrade, an invalidate() of an id made by one version does not reach the other version's entries: call invalidate() once every replica runs 1.3.3, or allow for ttl.
pip install --upgrade "needtoknow>=1.3.3"

Full changelog: v1.3.2...v1.3.3

needtoknow 1.2.2

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 21 Oct 22:51
16e1ef9

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

Closes three gaps that remained in CachingAuthorizer.invalidate() after 1.2.1, found by a review of the code. Upgrade if you use a shared cache and call invalidate(); or move to 1.3.3.

Security

  • In rare interleavings, a decision revoked with invalidate() could still be used afterwards, for up to ttl. An invalidate() of everyone deleted every id's epoch, including one that a concurrent invalidate() of that id had just written, so a retrieval in flight could store its earlier answer where every replica accepted it. And in the process calling invalidate(), a read of the global epoch already in flight could put back the epoch it replaced, for up to local_ttl. Epochs now live outside the keys that invalidation deletes, and a read never replaces a newer epoch.
  • An invalidate() of everyone reached other replicas within twice local_ttl, not within local_ttl as documented: an entry accepted against a copy of the global epoch was kept for another local_ttl. Copies now expire with that epoch.

Fixed

  • Epochs are stored as hex text. Random bytes broke shared caches whose Redis client decodes responses (decode_responses=True): reads raised UnicodeDecodeError, so every lookup asked the permission service and logged a warning.
  • invalidate() raises TypeError for an id that is not a string: invalidate(42) for the id "42" missed this process's own entries.

Changed

  • An id's epoch has a new key in the shared cache. During a rolling upgrade, an invalidate() of an id made by one version does not reach the other version's entries: call invalidate() once every replica runs 1.2.2, or allow for ttl.
pip install --upgrade "needtoknow>=1.2.2,<1.3"

Full changelog: v1.2.1...v1.2.2

needtoknow 1.3.2

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 19 Oct 17:37
3c41723

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

Completes the 1.3.1 fix for CachingAuthorizer.invalidate(). Upgrade if you use a shared cache.

pip install --upgrade needtoknow

Security. With a shared cache (RedisCache), invalidate() could still leave the revoked decision in its own process for up to local_ttl (1 second by default): a retrieval running during the call could copy the old entry from the shared cache after the process dropped its own entries and before the new epoch was written. The process now drops its entries again once the new epoch is in place. The 1.3.1 revocation test under concurrent traffic found it; a new test forces a retrieval into that moment, for one subject and for invalidate() of everything. Other replicas were not affected beyond their documented local_ttl, and a cache without a shared store had no such gap.

1.3.2 is released from the release/1.3.x branch and contains only this fix on top of 1.3.1. The same change is in 1.2.1 for the 1.2 line. The files on this release are signed with Sigstore and come with an SBOM; PyPI carries attestations for each one.

Full changelog: v1.3.1...v1.3.2

needtoknow 1.3.1

Choose a tag to compare

@muzakkirhussain011 muzakkirhussain011 released this 19 Oct 16:44
2fc4aa7

Published as needtoknow, the project's name before 2.0.0. The project is now wardowl: pip install wardowl. See the documentation and the upgrade guide.

A security fix for permission caching. Upgrade if you use CachingAuthorizer, and above all if you call invalidate() to revoke access.

pip install --upgrade needtoknow

Security. CachingAuthorizer.invalidate() could be undone by a retrieval already in flight. An answer the permission service gave just before a revocation, to a call that finished just after it, was cached afterwards and used for up to ttl (30 seconds by default), in that process and, through a shared Redis cache, in every replica. Now each process discards answers obtained by calls that began before an invalidate(), and invalidate() stores a new epoch in the shared cache, under which entries written before it are refused. New tests revoke a permission under concurrent retrievals, with one and three replicas, and check that no retrieval starting after invalidate() returns sees the document (other replicas within local_ttl).

Changed. invalidate(subject) takes a Subject or a subject id, and drops the answers for every subject with that id, whatever groups and attributes they came with. Before, it dropped only those of a subject equal in every field, so a revocation hook that knew only the user id cleared nothing for a subject with groups or a tenant attribute. Shared cache entries have new keys and carry an epoch: entries written by 1.3.0 and earlier are not read, so the first requests after the upgrade ask the permission service. During a rolling upgrade, call invalidate() once every replica runs 1.3.1, or allow for ttl.

Fixed. filter_from_dict refuses a filter nested more than 100 levels deep with ValueError instead of recursing until RecursionError, and the shared cache treats such an entry as a miss.

1.3.1 is released from the release/1.3.x branch and contains only these fixes; the features merged since 1.3.0 come in 1.4.0. The files on this release are signed with Sigstore and come with an SBOM; PyPI carries attestations for each one.

Full changelog: v1.3.0...v1.3.1