Repository navigation
Releases: muzakkirhussain011/wardowl
Release list
wardowl 2.1.0
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, aValueErrorfor a query that cannot be searched through its own fault, raised byQuery.embedded,Query.keywords, the retriever's checks ofk, Vespa's keyword mode andFaissBackend(a query vector whose length is notdim). Code that catchesValueErrorkeeps 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-wideSSL_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 fromRAMPART_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 ingestandrampart verify-auditnow needRAMPART_KEY_SECRET(orRAMPART_DEMO=1), asrampart servealready did. A request from a disabled principal is now audited, without anything it carried, and counted asaccount_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/verifyor the dashboard, is audited too. -
SpiceDBAuthorizerchecks only the user by default (group_subjects=False, wasTrue), 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 aspermission view = viewer - banneda 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, passgroup_subjects=Trueto 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. -
AuthorizationErrormessages are fixed and never carry the text of the exception behind them, which stays the error's__cause__(and so in logged tracebacks;RetrievalEvent.causestill names its type). The retriever wrapped an authorizer's or redactor's exception asauthorizer failed: <its text>, so a failure such ascould not reach opa.internal:8181 as svc-wardowl (password hunter2)reached a model through Haystack'sAgent, 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, Cerbosroles, OPA's filter, SpiceDB'scaveat_contextand the OIDC key fetch.
Fixed
-
Example gateway:
/v1/chat/completionsaccepts a message'scontentas 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: trueon/v1/chat/completionsanswers in OpenAI's server-sent events (chat.completion.chunkobjects, a last chunk withfinish_reasonand therampartblock, a usage chunk withstream_options.include_usage, thendata: [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, asingest_not_allowedandaudit_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_MODELmakes thelocal-llmprofile pull a smaller chat model and serve it under the name the policy approves.RAMPART_DEFAULT_MODELis gone: nothing read it, since the policy chooses the model. -
KetoAuthorizersends 10 tuples per batch check by default (batch_size=10, was 100). Keto refuses more thanlimit.max_batch_check_sizetuples, 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. -
SpanRedactorhides 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 asha256mismatch. 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 with0 <= start < end, and the other malformed cases) still fail the retrieval. -
ChromaBackendwrites in batches of at mostbatch_sizerecords: 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,deleteandset_metadatasent everything in one call, which Chroma refuses above its maximum. Every record is checked before the first batch is written. -
An open
CircuitBreakerraisesAuthorizationErrorcaused by the newwardowl.authorizers.CircuitOpenError, soRetrievalEvent.causeis"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):kover the retriever'smax_candidates, only a vector for a keyword backend, only text for a backend without an embed function, or a vector of the wrong length forFaissBackend. 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
AllOfwhose parts all give filters that scope is the whole filter, even withstrategy="postfilter". The authorizer table lists every built-in authorizer. Combining and the Chroma page's speed advice say the same. - Combining shows
TenantAuthorizer'sattributeas it is (None, meaning the field's name) and how one chunk serves several tenants through a list-valuedtenantfield. - Vespa:
namespaceonly 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: indextenantin multi-tenant collections. Cassandra:create_tabledoes 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
sha256of a short value can be recovered by guessing, so the spans must not reach callers. - Extending says
check_authorizerproves consistency, not a correct policy, and shows a test of known cases. - SpiceDB explains why
group_subjects=Truefails under an intersection or an exclusion, and what to do instead. Casbin says what a customrequestskips 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-callsubject=wins overfor_subject. Haystack: what a model can set on the agent tool,inputs_from_state, andoutputs_to_statefor getting the documents back. MCP: a stdio server for a signed-in user. Agent frameworks: themcp2 client takes it...
wardowl 2.0.1
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
- The lock file moves filelock to 4.2.4 and setuptools to 84.2.1, past GHSA-2g95-wg33-c5x2, GHSA-567q-5qvw-pf92, GHSA-9vhv-jxww-xfm5, GHSA-crv4-6mhj-j5jh, GHSA-jqc4-5cvw-7w85, GHSA-v4x8-3wr5-3wq8 and GHSA-xmj3-9gf7-h52w. Both are build and test tools; the package does not depend on them.
wardowl 2.0.0
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 wardowland importwardowl. The repository and the documentation moved togithub.com/muzakkirhussain011/wardowlandmuzakkirhussain011.github.io/wardowl(the old addresses redirect). Everything named after the project changes with it: classes (NeedToKnowRetrieveris nowWardowlRetriever), environment variables (NEEDTOKNOW_*is nowWARDOWL_*), the default cache namespace and metric names, and the field names the backends store (needtoknow_terms,needtoknow_idandneedtoknow_metadataare nowwardowl_terms,wardowl_idandwardowl_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: callinvalidate()once the upgrade is done, or allow forttl. -
The operations guide's
set_metadataexample merges the stored metadata instead of replacing it with the changed permission alone. -
The public filter translators raise the new
wardowl.backends.NothingMatches(aValueError) for a filter that can match nothing, instead of returningNoneor{}, 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_yqlandfilter_to_pinecone. Chroma'sNEVERconstant is removed. The backends still return no hits for such a filter without asking the store. -
A search with
kabove what the store returns in one query raisesValueErrorinstead 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. -
PineconeBackendwaits 30 seconds by default (wait=30.0, wasNone) for a search to see each write, so a revoked permission or a deleted chunk is out of searches when the call returns.wait=Nonekeeps the old behaviour. -
Haystack:
WardowlRetriever'ssubjectinput also accepts a function returning theSubject, 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 inpipeline.connect("embedder.embedding", "retriever.query_embedding").
Added
PineconeBackend(thepineconeextra): dense vector search on a Pinecone index through its data plane API (2026-07), with the permission filter as the query's metadata filter:$inon hex-encoded, type-tagged terms, joined with$andand$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 explicitnamespace. 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, withValueError, a second kind of authorizer under a Redis client (orSharedCache) 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 keys1and"1", lists and tuples, the attributes1,Trueand1.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, andttlandlocal_ttlmust be finite. -
SpanRedactorhides the whole text of a chunk without its spans field (required=Trueby default;[]still means nothing to hide, andrequired=Falserestores the old behaviour). A missing field showed the chunk in full, andset_metadata, which replaces the metadata, dropped the field whenever permissions were updated without it. A span may now carrysha256(wardowl.redaction.span_digestof the text it covers); a chunk whose text no longer matches is hidden whole. -
Haystack:
BoundWardowlRetrievertakes the subject fromsubject_provider, a function such as one reading a contextvar the web layer sets, and has nosubjectinput.WardowlRetrievertakes the subject as a run input, soComponentTooloffered 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_groupsandtenantincluded, into the documents a model, prompts, callbacks and traces receive. LangChain documents keeprelevance_score. -
Subject.attributesis 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.addkeeps 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 ofdeleteandset_metadata, so searches kept returning them with their old metadata after a revocation. -
QdrantBackendandWeaviateBackendderive 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_metadataanddeletenow act only on a point whose storedwardowl_idis 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, withoutwardowl_id, is reached by its UUID written exactly as search results report it. Collections must be indexed again, as the rename already requires. -
PineconeBackendstores a chunk id that starts withsha256-under its SHA-256, like ids Pinecone cannot hold, so no chunk id can share a record with another's hashed id.set_metadataanddeletefetch the records first (GET /vectors/fetch) and leave alone any record that holds another chunk id. -
MilvusBackend.deleteandset_metadataselect rows withid in {ids}and the ids as a filter parameter. pymilvus'sget(ids=...)anddelete(ids=...)write string ids into the expression between quotes, so an id such asa','bselected the chunksaandbas well. -
set_metadatanever 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 whileset_metadataran could be inserted again with its old text, and a concurrentaddcould have its new text replaced by the old. -
CassandraBackend.set_metadatauses plain writes, likeaddanddelete, 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. -
PgVectorBackendmatches an integer value only where the stored number reads as that integer, as the reference semantics do: jsonb compares numbers by value, soIn("level", [1])used to match{"level": 1.0}. Metadata keys that are not strings are refused, since JSON turned the key1into"1", which a filter on the field"1"then matched. -
Chroma's
filter_to_whererefuses field names that contain::or equalwardowl_metadata, which addressed the stored list fields or the JSON copy rather than a metadata field. -
CedarAuthorizerno 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 aforbid ... when { resource has x && ... }stopped applying and apermitcould let the chunk through (asensitivityof3.0from a store that returns every number as a float, say). Integers, and floats without a fraction, now become Ce...
needtoknow 1.6.0
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(thecassandraextra): vector search on Apache Cassandra 5.0 with Storage-Attached Indexes. The filter becomes an OR of ANDs on type-tagged terms in an indexedset<text>; each AND is one ANN query with boundCONTAINSparameters, run concurrently, and the bestkare kept; pastmax_queries, one query on the terms every match needs is filtered in the process.set_metadatauses a lightweight transaction so an id that is not stored creates no row;create_tablecreates 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(thevespaextra): 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 andexamples/vespais 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
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(thefaissextra): 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, andsaveandloadto a directory. Passes the conformance kit.CasbinAuthorizer(thecasbinextra): decisions from the Casbin enforcer you already use, in the process, withbatch_enforce. Requests are(subject id, object, action), or your own for ABAC models;group_subjects=Truealso asks with each of the subject's groups, for allow-only models. An enforcer that raises or answers with anything but booleans raisesAuthorizationError. 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=Truealso 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 raisesAuthorizationError. Tested against Keto 26.2.0 and with the conformance kit.- An HTTP service,
needtoknow.server(theserverextra), so applications in any language get permission-aware retrieval:create_app(retriever, authenticate)servesPOST /v1/retrieve,GET /healthzand an OpenAPI description. The subject comes from the verified bearer token alone;OIDCAuthenticatorverifies 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.SpanRedactorhides spans listed in a chunk's metadata, each with its own readers;PatternRedactorhides 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 raiseAuthorizationError.
Full changelog: https://github.com/muzakkirhussain011/wardowl/blob/main/CHANGELOG.md
needtoknow 1.4.0
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
RankFusionhybrid 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.causenames 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
FLATindex.
- Security fixes found by review:
recheck=Falsenow matches hits against the filter locally.- A deadline-cut OpenFGA list is no longer used as a complete filter.
- A Cerbos
groupsattribute 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
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 tottl. Aninvalidate()of everyone deleted every id's epoch, including one that a concurrentinvalidate()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 callinginvalidate(), a read of the global epoch already in flight could put back the epoch it replaced, for up tolocal_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 twicelocal_ttl, not withinlocal_ttlas documented: an entry accepted against a copy of the global epoch was kept for anotherlocal_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 raisedUnicodeDecodeError, so every lookup asked the permission service and logged a warning. invalidate()raisesTypeErrorfor 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: callinvalidate()once every replica runs 1.3.3, or allow forttl.
pip install --upgrade "needtoknow>=1.3.3"
Full changelog: v1.3.2...v1.3.3
needtoknow 1.2.2
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 tottl. Aninvalidate()of everyone deleted every id's epoch, including one that a concurrentinvalidate()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 callinginvalidate(), a read of the global epoch already in flight could put back the epoch it replaced, for up tolocal_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 twicelocal_ttl, not withinlocal_ttlas documented: an entry accepted against a copy of the global epoch was kept for anotherlocal_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 raisedUnicodeDecodeError, so every lookup asked the permission service and logged a warning. invalidate()raisesTypeErrorfor 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: callinvalidate()once every replica runs 1.2.2, or allow forttl.
pip install --upgrade "needtoknow>=1.2.2,<1.3"
Full changelog: v1.2.1...v1.2.2
needtoknow 1.3.2
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
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